y-indexeddb
Version:
IndexedDB database adapter for Yjs
185 lines (172 loc) • 5.54 kB
JavaScript
import * as Y from 'yjs'
import * as idb from 'lib0/indexeddb'
import * as promise from 'lib0/promise'
import { Observable } from 'lib0/observable'
const customStoreName = 'custom'
const updatesStoreName = 'updates'
export const PREFERRED_TRIM_SIZE = 500
/**
* @param {IndexeddbPersistence} idbPersistence
* @param {function(IDBObjectStore):void} [beforeApplyUpdatesCallback]
* @param {function(IDBObjectStore):void} [afterApplyUpdatesCallback]
*/
export 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
*/
export 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
*/
export const clearDocument = name => idb.deleteDB(name)
/**
* @extends Observable<string>
*/
export class IndexeddbPersistence extends 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)
})
}
}