UNPKG

y-indexeddb

Version:

IndexedDB database adapter for Yjs

196 lines (180 loc) 5.9 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var Y = require('yjs'); var idb = require('lib0/indexeddb'); var promise = require('lib0/promise'); var observable = require('lib0/observable'); const customStoreName = 'custom'; const updatesStoreName = 'updates'; const PREFERRED_TRIM_SIZE = 500; /** * @param {IndexeddbPersistence} idbPersistence * @param {function(IDBObjectStore):void} [beforeApplyUpdatesCallback] * @param {function(IDBObjectStore):void} [afterApplyUpdatesCallback] */ const fetchUpdates = (idbPersistence, beforeApplyUpdatesCallback = () => {}, afterApplyUpdatesCallback = () => {}) => { const [updatesStore] = idb.transact(/** @type {IDBDatabase} */ (idbPersistence.db), [updatesStoreName]); // , 'readonly') return idb.getAll(updatesStore, idb.createIDBKeyRangeLowerBound(idbPersistence._dbref, false)).then(updates => { if (!idbPersistence._destroyed) { beforeApplyUpdatesCallback(updatesStore); Y.transact(idbPersistence.doc, () => { updates.forEach(val => Y.applyUpdate(idbPersistence.doc, val)); }, idbPersistence, false); afterApplyUpdatesCallback(updatesStore); } }) .then(() => idb.getLastKey(updatesStore).then(lastKey => { idbPersistence._dbref = lastKey + 1; })) .then(() => idb.count(updatesStore).then(cnt => { idbPersistence._dbsize = cnt; })) .then(() => updatesStore) }; /** * @param {IndexeddbPersistence} idbPersistence * @param {boolean} forceStore */ const storeState = (idbPersistence, forceStore = true) => fetchUpdates(idbPersistence) .then(updatesStore => { if (forceStore || idbPersistence._dbsize >= PREFERRED_TRIM_SIZE) { idb.addAutoKey(updatesStore, Y.encodeStateAsUpdate(idbPersistence.doc)) .then(() => idb.del(updatesStore, idb.createIDBKeyRangeUpperBound(idbPersistence._dbref, true))) .then(() => idb.count(updatesStore).then(cnt => { idbPersistence._dbsize = cnt; })); } }); /** * @param {string} name */ const clearDocument = name => idb.deleteDB(name); /** * @extends Observable<string> */ class IndexeddbPersistence extends observable.Observable { /** * @param {string} name * @param {Y.Doc} doc */ constructor (name, doc) { super(); this.doc = doc; this.name = name; this._dbref = 0; this._dbsize = 0; this._destroyed = false; /** * @type {IDBDatabase|null} */ this.db = null; this.synced = false; this._db = idb.openDB(name, db => idb.createStores(db, [ ['updates', { autoIncrement: true }], ['custom'] ]) ); /** * @type {Promise<IndexeddbPersistence>} */ this.whenSynced = promise.create(resolve => this.on('synced', () => resolve(this))); this._db.then(db => { this.db = db; /** * @param {IDBObjectStore} updatesStore */ const beforeApplyUpdatesCallback = (updatesStore) => idb.addAutoKey(updatesStore, Y.encodeStateAsUpdate(doc)); const afterApplyUpdatesCallback = () => { if (this._destroyed) return this this.synced = true; this.emit('synced', [this]); }; fetchUpdates(this, beforeApplyUpdatesCallback, afterApplyUpdatesCallback); }); /** * Timeout in ms untill data is merged and persisted in idb. */ this._storeTimeout = 1000; /** * @type {any} */ this._storeTimeoutId = null; /** * @param {Uint8Array} update * @param {any} origin */ this._storeUpdate = (update, origin) => { if (this.db && origin !== this) { const [updatesStore] = idb.transact(/** @type {IDBDatabase} */ (this.db), [updatesStoreName]); idb.addAutoKey(updatesStore, update); if (++this._dbsize >= PREFERRED_TRIM_SIZE) { // debounce store call if (this._storeTimeoutId !== null) { clearTimeout(this._storeTimeoutId); } this._storeTimeoutId = setTimeout(() => { storeState(this, false); this._storeTimeoutId = null; }, this._storeTimeout); } } }; doc.on('update', this._storeUpdate); this.destroy = this.destroy.bind(this); doc.on('destroy', this.destroy); } destroy () { if (this._storeTimeoutId) { clearTimeout(this._storeTimeoutId); } this.doc.off('update', this._storeUpdate); this.doc.off('destroy', this.destroy); this._destroyed = true; return this._db.then(db => { db.close(); }) } /** * Destroys this instance and removes all data from indexeddb. * * @return {Promise<void>} */ clearData () { return this.destroy().then(() => { idb.deleteDB(this.name); }) } /** * @param {String | number | ArrayBuffer | Date} key * @return {Promise<String | number | ArrayBuffer | Date | any>} */ get (key) { return this._db.then(db => { const [custom] = idb.transact(db, [customStoreName], 'readonly'); return idb.get(custom, key) }) } /** * @param {String | number | ArrayBuffer | Date} key * @param {String | number | ArrayBuffer | Date} value * @return {Promise<String | number | ArrayBuffer | Date>} */ set (key, value) { return this._db.then(db => { const [custom] = idb.transact(db, [customStoreName]); return idb.put(custom, value, key) }) } /** * @param {String | number | ArrayBuffer | Date} key * @return {Promise<undefined>} */ del (key) { return this._db.then(db => { const [custom] = idb.transact(db, [customStoreName]); return idb.del(custom, key) }) } } exports.IndexeddbPersistence = IndexeddbPersistence; exports.PREFERRED_TRIM_SIZE = PREFERRED_TRIM_SIZE; exports.clearDocument = clearDocument; exports.fetchUpdates = fetchUpdates; exports.storeState = storeState; //# sourceMappingURL=y-indexeddb.cjs.map