364 lines
9.5 KiB
JavaScript
364 lines
9.5 KiB
JavaScript
![]() |
import 'babel-polyfill';
|
||
|
import 'indexeddbshim';
|
||
|
import utils from './utils';
|
||
|
|
||
|
let indexedDB = window.indexedDB;
|
||
|
const localStorage = window.localStorage;
|
||
|
const dbVersion = 1;
|
||
|
|
||
|
// Use the shim on Safari or if indexedDB is not available
|
||
|
if (window.shimIndexedDB && (!indexedDB || (navigator.userAgent.indexOf('Chrome') === -1 && navigator.userAgent.indexOf('Safari') !== -1))) {
|
||
|
indexedDB = window.shimIndexedDB;
|
||
|
}
|
||
|
|
||
|
const deletedMarkerMaxAge = 1000;
|
||
|
|
||
|
function identity(value) {
|
||
|
return value;
|
||
|
}
|
||
|
|
||
|
function makeStore(storeName, schemaParameter) {
|
||
|
const schema = {
|
||
|
...schemaParameter,
|
||
|
updated: 'int',
|
||
|
};
|
||
|
|
||
|
const schemaKeys = Object.keys(schema);
|
||
|
const schemaKeysLen = schemaKeys.length;
|
||
|
const complexKeys = [];
|
||
|
let complexKeysLen = 0;
|
||
|
const attributeCheckers = {};
|
||
|
const attributeReaders = {};
|
||
|
const attributeWriters = {};
|
||
|
|
||
|
class Dao {
|
||
|
constructor(id, skipInit) {
|
||
|
this.id = id || utils.uid();
|
||
|
if (!skipInit) {
|
||
|
const fakeItem = {};
|
||
|
for (let i = 0; i < schemaKeysLen; i += 1) {
|
||
|
attributeReaders[schemaKeys[i]](fakeItem, this);
|
||
|
}
|
||
|
this.$dirty = true;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function createDao(id) {
|
||
|
return new Dao(id);
|
||
|
}
|
||
|
|
||
|
Object.keys(schema).forEach((key) => {
|
||
|
const value = schema[key];
|
||
|
const storedValueKey = `_${key}`;
|
||
|
let defaultValue = value.default === undefined ? '' : value.default;
|
||
|
let serializer = value.serializer || identity;
|
||
|
let parser = value.parser || identity;
|
||
|
if (value === 'int') {
|
||
|
defaultValue = 0;
|
||
|
} else if (value === 'object') {
|
||
|
defaultValue = 'null';
|
||
|
parser = JSON.parse;
|
||
|
serializer = JSON.stringify;
|
||
|
}
|
||
|
|
||
|
attributeReaders[key] = (dbItem, dao) => {
|
||
|
dao[storedValueKey] = dbItem[key] || defaultValue;
|
||
|
};
|
||
|
attributeWriters[key] = (dbItem, dao) => {
|
||
|
const storedValue = dao[storedValueKey];
|
||
|
if (storedValue && storedValue !== defaultValue) {
|
||
|
dbItem[key] = storedValue;
|
||
|
}
|
||
|
};
|
||
|
|
||
|
function getter() {
|
||
|
return this[storedValueKey];
|
||
|
}
|
||
|
|
||
|
function setter(param) {
|
||
|
const val = param || defaultValue;
|
||
|
if (this[storedValueKey] === val) {
|
||
|
return false;
|
||
|
}
|
||
|
this[storedValueKey] = val;
|
||
|
this.$dirty = true;
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
if (key === 'updated') {
|
||
|
Object.defineProperty(Dao.prototype, key, {
|
||
|
get: getter,
|
||
|
set: (value) => {
|
||
|
if (setter.call(this, value)) {
|
||
|
this.$dirtyUpdated = true;
|
||
|
}
|
||
|
}
|
||
|
})
|
||
|
} else if (value === 'string' || value === 'int') {
|
||
|
Object.defineProperty(Dao.prototype, key, {
|
||
|
get: getter,
|
||
|
set: setter
|
||
|
})
|
||
|
} else if (![64, 128] // Handle string64 and string128
|
||
|
.cl_some(function (length) {
|
||
|
if (value === 'string' + length) {
|
||
|
Object.defineProperty(Dao.prototype, key, {
|
||
|
get: getter,
|
||
|
set: function (value) {
|
||
|
if (value && value.length > length) {
|
||
|
value = value.slice(0, length)
|
||
|
}
|
||
|
setter.call(this, value)
|
||
|
}
|
||
|
})
|
||
|
return true
|
||
|
}
|
||
|
})
|
||
|
) {
|
||
|
// Other types go to complexKeys list
|
||
|
complexKeys.push(key)
|
||
|
complexKeysLen++
|
||
|
|
||
|
// And have complex readers/writers
|
||
|
attributeReaders[key] = function (dbItem, dao) {
|
||
|
const storedValue = dbItem[key]
|
||
|
if (!storedValue) {
|
||
|
storedValue = defaultValue
|
||
|
}
|
||
|
dao[storedValueKey] = storedValue
|
||
|
dao[key] = parser(storedValue)
|
||
|
}
|
||
|
attributeWriters[key] = function (dbItem, dao) {
|
||
|
const storedValue = serializer(dao[key])
|
||
|
dao[storedValueKey] = storedValue
|
||
|
if (storedValue && storedValue !== defaultValue) {
|
||
|
dbItem[key] = storedValue
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Checkers are only for complex types
|
||
|
attributeCheckers[key] = function (dao) {
|
||
|
return serializer(dao[key]) !== dao[storedValueKey]
|
||
|
}
|
||
|
}
|
||
|
})
|
||
|
|
||
|
const lastTx = 0
|
||
|
const storedSeqs = Object.create(null)
|
||
|
|
||
|
function readDbItem(item, daoMap) {
|
||
|
const dao = daoMap[item.id] || new Dao(item.id, true)
|
||
|
if (!item.updated) {
|
||
|
delete storedSeqs[item.id]
|
||
|
if (dao.updated) {
|
||
|
delete daoMap[item.id]
|
||
|
return true
|
||
|
}
|
||
|
return
|
||
|
}
|
||
|
if (storedSeqs[item.id] === item.seq) {
|
||
|
return
|
||
|
}
|
||
|
storedSeqs[item.id] = item.seq
|
||
|
for (const i = 0; i < schemaKeysLen; i++) {
|
||
|
attributeReaders[schemaKeys[i]](item, dao)
|
||
|
}
|
||
|
dao.$dirty = false
|
||
|
dao.$dirtyUpdated = false
|
||
|
daoMap[item.id] = dao
|
||
|
return true
|
||
|
}
|
||
|
|
||
|
function getPatch(tx, cb) {
|
||
|
let resetMap;
|
||
|
|
||
|
// We may have missed some deleted markers
|
||
|
if (lastTx && tx.txCounter - lastTx > deletedMarkerMaxAge) {
|
||
|
// Delete all dirty daos, user was asleep anyway...
|
||
|
resetMap = true
|
||
|
// And retrieve everything from DB
|
||
|
lastTx = 0
|
||
|
}
|
||
|
|
||
|
const hasChanged = !lastTx
|
||
|
const store = tx.objectStore(storeName)
|
||
|
const index = store.index('seq')
|
||
|
const range = $window.IDBKeyRange.lowerBound(lastTx, true)
|
||
|
const items = []
|
||
|
const itemsToDelete = []
|
||
|
index.openCursor(range).onsuccess = function (event) {
|
||
|
const cursor = event.target.result
|
||
|
if (!cursor) {
|
||
|
itemsToDelete.cl_each(function (item) {
|
||
|
store.delete(item.id)
|
||
|
})
|
||
|
items.length && debug('Got ' + items.length + ' ' + storeName + ' items')
|
||
|
// Return a patch, to apply changes later
|
||
|
return cb(function (daoMap) {
|
||
|
if (resetMap) {
|
||
|
Object.keys(daoMap).cl_each(function (key) {
|
||
|
delete daoMap[key]
|
||
|
})
|
||
|
storedSeqs = Object.create(null)
|
||
|
}
|
||
|
items.cl_each(function (item) {
|
||
|
hasChanged |= readDbItem(item, daoMap)
|
||
|
})
|
||
|
return hasChanged
|
||
|
})
|
||
|
}
|
||
|
const item = cursor.value
|
||
|
items.push(item)
|
||
|
// Remove old deleted markers
|
||
|
if (!item.updated && tx.txCounter - item.seq > deletedMarkerMaxAge) {
|
||
|
itemsToDelete.push(item)
|
||
|
}
|
||
|
cursor.continue()
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function writeAll(daoMap, tx) {
|
||
|
lastTx = tx.txCounter
|
||
|
const store = tx.objectStore(storeName)
|
||
|
|
||
|
// Remove deleted daos
|
||
|
const storedIds = Object.keys(storedSeqs)
|
||
|
const storedIdsLen = storedIds.length
|
||
|
for (const i = 0; i < storedIdsLen; i++) {
|
||
|
const id = storedIds[i]
|
||
|
if (!daoMap[id]) {
|
||
|
// Put a deleted marker to notify other tabs
|
||
|
store.put({
|
||
|
id: id,
|
||
|
seq: lastTx
|
||
|
})
|
||
|
delete storedSeqs[id]
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Put changes
|
||
|
const daoIds = Object.keys(daoMap)
|
||
|
const daoIdsLen = daoIds.length
|
||
|
for (i = 0; i < daoIdsLen; i++) {
|
||
|
const dao = daoMap[daoIds[i]]
|
||
|
const dirty = dao.$dirty
|
||
|
for (const j = 0; !dirty && j < complexKeysLen; j++) {
|
||
|
dirty |= attributeCheckers[complexKeys[j]](dao)
|
||
|
}
|
||
|
if (dirty) {
|
||
|
if (!dao.$dirtyUpdated) {
|
||
|
// Force update the `updated` attribute
|
||
|
dao.updated = Date.now()
|
||
|
}
|
||
|
const item = {
|
||
|
id: daoIds[i],
|
||
|
seq: lastTx
|
||
|
}
|
||
|
for (j = 0; j < schemaKeysLen; j++) {
|
||
|
attributeWriters[schemaKeys[j]](item, dao)
|
||
|
}
|
||
|
debug('Put ' + storeName + ' item')
|
||
|
store.put(item)
|
||
|
storedSeqs[item.id] = item.seq
|
||
|
dao.$dirty = false
|
||
|
dao.$dirtyUpdated = false
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return {
|
||
|
getPatch,
|
||
|
writeAll,
|
||
|
createDao,
|
||
|
Dao,
|
||
|
};
|
||
|
}
|
||
|
|
||
|
class Connection {
|
||
|
constructor() {
|
||
|
this.getTxCbs = [];
|
||
|
|
||
|
// Init connexion
|
||
|
const request = indexedDB.open('classeur-db', dbVersion);
|
||
|
|
||
|
request.onerror = () => {
|
||
|
throw new Error("Can't connect to IndexedDB.");
|
||
|
};
|
||
|
|
||
|
request.onsuccess = (event) => {
|
||
|
this.db = event.target.result;
|
||
|
localStorage.localDbVersion = this.db.version; // Safari does not support onversionchange
|
||
|
this.db.onversionchange = () => window.location.reload();
|
||
|
|
||
|
this.getTxCbs.forEach(cb => this.createTx(cb));
|
||
|
this.getTxCbs = null;
|
||
|
};
|
||
|
|
||
|
request.onupgradeneeded = (event) => {
|
||
|
const eventDb = event.target.result;
|
||
|
const oldVersion = event.oldVersion || 0;
|
||
|
function createStore(name) {
|
||
|
const store = eventDb.createObjectStore(name, { keyPath: 'id' });
|
||
|
store.createIndex('seq', 'seq', { unique: false });
|
||
|
}
|
||
|
|
||
|
// We don't use 'break' in this switch statement,
|
||
|
// the fall-through behaviour is what we want.
|
||
|
/* eslint-disable no-fallthrough */
|
||
|
switch (oldVersion) {
|
||
|
case 0:
|
||
|
[
|
||
|
'contents',
|
||
|
'files',
|
||
|
'folders',
|
||
|
'objects',
|
||
|
'app',
|
||
|
].forEach(createStore);
|
||
|
default:
|
||
|
}
|
||
|
/* eslint-enable no-fallthrough */
|
||
|
};
|
||
|
}
|
||
|
|
||
|
createTx(cb) {
|
||
|
if (!this.db) {
|
||
|
this.getTxCbs.push(cb);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// If DB version has changed (Safari support)
|
||
|
if (parseInt(localStorage.localDbVersion, 10) !== this.db.version) {
|
||
|
window.location.reload();
|
||
|
return;
|
||
|
}
|
||
|
const tx = this.db.transaction(this.db.objectStoreNames, 'readwrite');
|
||
|
// tx.onerror = (evt) => {
|
||
|
// dbg('Rollback transaction', evt);
|
||
|
// };
|
||
|
const store = tx.objectStore('app');
|
||
|
const request = store.get('txCounter');
|
||
|
request.onsuccess = () => {
|
||
|
tx.txCounter = request.result ? request.result.value : 0;
|
||
|
tx.txCounter += 1;
|
||
|
store.put({
|
||
|
id: 'txCounter',
|
||
|
value: tx.txCounter,
|
||
|
});
|
||
|
cb(tx);
|
||
|
};
|
||
|
}
|
||
|
}
|
||
|
|
||
|
class LocalDbStorage {
|
||
|
init(store) {
|
||
|
this.store = store;
|
||
|
store.subscribe((mutation, state) => {
|
||
|
console.log(mutation, state);
|
||
|
});
|
||
|
this.connection = new Connection();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
export default LocalDbStorage;
|