Stackedit/src/services/localDbSvc.js

364 lines
9.5 KiB
JavaScript
Raw Normal View History

2017-07-27 08:23:54 +00:00
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;