added YIndexedDB
This commit is contained in:
189
src/Persistences/IndexedDBPersistence.mjs
Normal file
189
src/Persistences/IndexedDBPersistence.mjs
Normal file
@@ -0,0 +1,189 @@
|
||||
/* global indexedDB, location, BroadcastChannel */
|
||||
|
||||
import Y from '../Y.mjs'
|
||||
import { createMutualExclude } from '../Util/mutualExclude.mjs'
|
||||
import { decodePersisted, encodeStructsDS, encodeUpdate } from './decodePersisted.mjs'
|
||||
import BinaryDecoder from '../Util/Binary/Decoder.mjs'
|
||||
import BinaryEncoder from '../Util/Binary/Encoder.mjs'
|
||||
/*
|
||||
* Request to Promise transformer
|
||||
*/
|
||||
function rtop (request) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
request.onerror = function (event) {
|
||||
reject(new Error(event.target.error))
|
||||
}
|
||||
request.onblocked = function () {
|
||||
location.reload()
|
||||
}
|
||||
request.onsuccess = function (event) {
|
||||
resolve(event.target.result)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function openDB (room) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
let request = indexedDB.open(room)
|
||||
request.onupgradeneeded = function (event) {
|
||||
const db = event.target.result
|
||||
if (db.objectStoreNames.contains('updates')) {
|
||||
db.deleteObjectStore('updates')
|
||||
}
|
||||
db.createObjectStore('updates', {autoIncrement: true})
|
||||
}
|
||||
request.onerror = function (event) {
|
||||
reject(new Error(event.target.error))
|
||||
}
|
||||
request.onblocked = function () {
|
||||
location.reload()
|
||||
}
|
||||
request.onsuccess = function (event) {
|
||||
const db = event.target.result
|
||||
db.onversionchange = function () { db.close() }
|
||||
resolve(db)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function persist (room) {
|
||||
let t = room.db.transaction(['updates'], 'readwrite')
|
||||
let updatesStore = t.objectStore('updates')
|
||||
return rtop(updatesStore.getAll())
|
||||
.then(updates => {
|
||||
// apply all previous updates before deleting them
|
||||
room.mutex(() => {
|
||||
updates.forEach(update => {
|
||||
decodePersisted(y, new BinaryDecoder(update))
|
||||
})
|
||||
})
|
||||
const encoder = new BinaryEncoder()
|
||||
encodeStructsDS(y, encoder)
|
||||
// delete all pending updates
|
||||
rtop(updatesStore.clear()).then(() => {
|
||||
// write current model
|
||||
updatesStore.put(encoder.createBuffer())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function saveUpdate (room, updateBuffer) {
|
||||
const db = room.db
|
||||
if (db !== null) {
|
||||
const t = db.transaction(['updates'], 'readwrite')
|
||||
const updatesStore = t.objectStore('updates')
|
||||
const updatePut = rtop(updatesStore.put(updateBuffer))
|
||||
rtop(updatesStore.count()).then(cnt => {
|
||||
if (cnt >= PREFERRED_TRIM_SIZE) {
|
||||
persist(room)
|
||||
}
|
||||
})
|
||||
return updatePut
|
||||
} else {
|
||||
room.createdStructs.push(update)
|
||||
return room.dbPromise
|
||||
}
|
||||
}
|
||||
|
||||
const PREFERRED_TRIM_SIZE = 400
|
||||
|
||||
export default class IndexedDBPersistence {
|
||||
constructor () {
|
||||
this._rooms = new Map()
|
||||
addEventListener('unload', () => {
|
||||
this._rooms.forEach(room => {
|
||||
if (room.db !== null) {
|
||||
room.db.close()
|
||||
} else {
|
||||
room._db.then(db => db.close())
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
connectY (roomName, y) {
|
||||
if (this._rooms.has(roomName)) {
|
||||
throw new Error('A Y instance is already bound to this room!')
|
||||
}
|
||||
let room = {
|
||||
db: null,
|
||||
dbPromise: null,
|
||||
channel: null,
|
||||
mutex: createMutualExclude(),
|
||||
y,
|
||||
createdStructs: [] // document updates before db created
|
||||
}
|
||||
if (typeof BroadcastChannel !== 'undefined') {
|
||||
room.channel = new BroadcastChannel('__yjs__' + room)
|
||||
room.channel.addEventListener('message', e => {
|
||||
room.mutex(function () {
|
||||
decodePersisted(y, new BinaryDecoder(e.data))
|
||||
})
|
||||
})
|
||||
}
|
||||
y.on('destroyed', () => {
|
||||
this.disconnectY(roomName, y)
|
||||
})
|
||||
y.on('afterTransaction', (y, transaction) => {
|
||||
room.mutex(() => {
|
||||
if (transaction.encodedStructsLen > 0) {
|
||||
const encoder = new BinaryEncoder()
|
||||
const update = new BinaryEncoder()
|
||||
encodeUpdate(y, transaction.encodedStructs, update)
|
||||
const updateBuffer = update.createBuffer()
|
||||
if (room.channel !== null) {
|
||||
room.channel.postMessage(updateBuffer)
|
||||
}
|
||||
if (transaction.encodedStructsLen > 0) {
|
||||
if (room.db === null) {
|
||||
room.createdStructs.push(updateBuffer)
|
||||
} else {
|
||||
saveUpdate(room, updateBuffer)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
return room.dbPromise = openDB(roomName)
|
||||
.then(db => {
|
||||
room.db = db
|
||||
const t = room.db.transaction(['updates'], 'readwrite')
|
||||
const updatesStore = t.objectStore('updates')
|
||||
return rtop(updatesStore.getAll()).then(updates => {
|
||||
// apply all previous updates before deleting them
|
||||
room.mutex(() => {
|
||||
y.transact(() => {
|
||||
updates.forEach(update => {
|
||||
decodePersisted(y, new BinaryDecoder(update))
|
||||
})
|
||||
}, true)
|
||||
})
|
||||
return Promise.all(room.createdStructs.map(update => {
|
||||
return saveUpdate(room, update)
|
||||
})).then(() => {
|
||||
room.createdStructs = []
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
disconnectY (roomName) {
|
||||
const {
|
||||
db, channel
|
||||
} = this._rooms.get(roomName)
|
||||
db.close()
|
||||
if (channel !== null) {
|
||||
channel.close()
|
||||
}
|
||||
this._rooms.delete(roomName)
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all persisted data that belongs to a room.
|
||||
* Automatically destroys all Yjs all Yjs instances that persist to
|
||||
* the room. If `destroyYjsInstances = false` the persistence functionality
|
||||
* will be removed from the Yjs instances.
|
||||
*/
|
||||
removePersistedData (roomName, destroyYjsInstances = true) {
|
||||
this.disconnectY(roomName)
|
||||
return rtop(indexedDB.deleteDatabase(roomName))
|
||||
}
|
||||
}
|
||||
@@ -1,174 +0,0 @@
|
||||
/* global indexedDB, location, BroadcastChannel */
|
||||
|
||||
import Y from '../Y.mjs'
|
||||
|
||||
/*
|
||||
* Request to Promise transformer
|
||||
*/
|
||||
function rtop (request) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
request.onerror = function (event) {
|
||||
reject(new Error(event.target.error))
|
||||
}
|
||||
request.onblocked = function () {
|
||||
location.reload()
|
||||
}
|
||||
request.onsuccess = function (event) {
|
||||
resolve(event.target.result)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function openDB (room) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
let request = indexedDB.open(room)
|
||||
window.r1 = request
|
||||
request.onupgradeneeded = function (event) {
|
||||
const db = event.target.result
|
||||
if (db.objectStoreNames.contains('model')) {
|
||||
db.deleteObjectStore('updates')
|
||||
db.deleteObjectStore('model')
|
||||
db.deleteObjectStore('custom')
|
||||
}
|
||||
db.createObjectStore('updates', {autoIncrement: true})
|
||||
db.createObjectStore('model')
|
||||
db.createObjectStore('custom')
|
||||
}
|
||||
request.onerror = function (event) {
|
||||
reject(new Error(event.target.error))
|
||||
}
|
||||
request.onblocked = function () {
|
||||
location.reload()
|
||||
}
|
||||
request.onsuccess = function (event) {
|
||||
const db = event.target.result
|
||||
db.onversionchange = function () { db.close() }
|
||||
resolve(db)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const PREFERRED_TRIM_SIZE = 500
|
||||
|
||||
export default class IndexedDBPersistence extends Y.AbstractPersistence {
|
||||
constructor (opts) {
|
||||
super(opts)
|
||||
window.addEventListener('unload', () => {
|
||||
this.ys.forEach(function (cnf, y) {
|
||||
if (cnf.db !== null) {
|
||||
cnf.db.close()
|
||||
} else {
|
||||
cnf._db.then(db => db.close())
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
init (y) {
|
||||
let cnf = this.ys.get(y)
|
||||
let room = y.room
|
||||
cnf.db = null
|
||||
const dbOpened = openDB(room)
|
||||
dbOpened.then(db => {
|
||||
cnf.db = db
|
||||
})
|
||||
if (typeof BroadcastChannel !== 'undefined') {
|
||||
cnf.channel = new BroadcastChannel('__yjs__' + room)
|
||||
cnf.channel.addEventListener('message', e => {
|
||||
cnf.mutualExclude(function () {
|
||||
y.transact(function () {
|
||||
Y.utils.integrateRemoteStructs(y, new Y.utils.BinaryDecoder(e.data))
|
||||
})
|
||||
})
|
||||
})
|
||||
} else {
|
||||
cnf.channel = null
|
||||
}
|
||||
return dbOpened
|
||||
}
|
||||
|
||||
deinit (y) {
|
||||
let cnf = this.ys.get(y)
|
||||
cnf.db.close()
|
||||
super.deinit(y)
|
||||
}
|
||||
|
||||
set (y, key, value) {
|
||||
const cnf = this.ys.get(y)
|
||||
const t = cnf.db.transaction(['custom'], 'readwrite')
|
||||
const customStore = t.objectStore('custom')
|
||||
return rtop(customStore.put(value, key))
|
||||
}
|
||||
|
||||
get (y, key) {
|
||||
const cnf = this.ys.get(y)
|
||||
const t = cnf.db.transaction(['custom'], 'readwrite')
|
||||
const customStore = t.objectStore('custom')
|
||||
return rtop(customStore.get(key))
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all persisted data that belongs to a room.
|
||||
* Automatically destroys all Yjs all Yjs instances that persist to
|
||||
* the room. If `destroyYjsInstances = false` the persistence functionality
|
||||
* will be removed from the Yjs instances.
|
||||
*/
|
||||
removePersistedData (room, destroyYjsInstances = true) {
|
||||
super.removePersistedData(room, destroyYjsInstances)
|
||||
return rtop(indexedDB.deleteDatabase(room))
|
||||
}
|
||||
|
||||
saveUpdate (y, update) {
|
||||
let cnf = this.ys.get(y)
|
||||
if (cnf.channel !== null) {
|
||||
cnf.channel.postMessage(update)
|
||||
}
|
||||
let t = cnf.db.transaction(['updates'], 'readwrite')
|
||||
let updatesStore = t.objectStore('updates')
|
||||
updatesStore.put(update)
|
||||
let cntP = rtop(updatesStore.count())
|
||||
cntP.then(cnt => {
|
||||
if (cnt >= PREFERRED_TRIM_SIZE) {
|
||||
this.persist(y)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
saveStruct (y, struct) {
|
||||
super.saveStruct(y, struct)
|
||||
}
|
||||
|
||||
retrieve (y) {
|
||||
let cnf = this.ys.get(y)
|
||||
let t = cnf.db.transaction(['updates', 'model'], 'readonly')
|
||||
let modelStore = t.objectStore('model')
|
||||
let updatesStore = t.objectStore('updates')
|
||||
return Promise.all([rtop(modelStore.get(0)), rtop(updatesStore.getAll())])
|
||||
.then(([model, updates]) => {
|
||||
super.retrieve(y, model, updates)
|
||||
})
|
||||
}
|
||||
|
||||
persist (y) {
|
||||
let cnf = this.ys.get(y)
|
||||
let db = cnf.db
|
||||
let t = db.transaction(['updates', 'model'], 'readwrite')
|
||||
let updatesStore = t.objectStore('updates')
|
||||
return rtop(updatesStore.getAll())
|
||||
.then(updates => {
|
||||
// apply pending updates before deleting them
|
||||
Y.AbstractPersistence.prototype.retrieve.call(this, y, null, updates)
|
||||
// get binary model
|
||||
let binaryModel = Y.AbstractPersistence.prototype.persist.call(this, y)
|
||||
// delete all pending updates
|
||||
if (updates.length > 0) {
|
||||
let modelStore = t.objectStore('model')
|
||||
modelStore.put(binaryModel, 0)
|
||||
updatesStore.clear()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof Y !== 'undefined') {
|
||||
extendYIndexedDBPersistence(Y) // eslint-disable-line
|
||||
}
|
||||
@@ -29,7 +29,7 @@ export function encodeStructsDS (y, encoder) {
|
||||
}
|
||||
|
||||
/**
|
||||
*Feed the Yjs instance with the persisted state
|
||||
* Feed the Yjs instance with the persisted state
|
||||
* @param {Yjs} y A Yjs instance.
|
||||
* @param {BinaryDecoder} decoder A Decoder instance that holds the file content.
|
||||
*/
|
||||
@@ -39,17 +39,13 @@ export function decodePersisted (y, decoder) {
|
||||
const contentType = decoder.readVarUint()
|
||||
switch (contentType) {
|
||||
case PERSIST_UPDATE:
|
||||
y.transact(() => {
|
||||
integrateRemoteStructs(y, decoder)
|
||||
})
|
||||
integrateRemoteStructs(y, decoder)
|
||||
break
|
||||
case PERSIST_STRUCTS_DS:
|
||||
y.transact(() => {
|
||||
integrateRemoteStructs(y, decoder)
|
||||
readDeleteSet(y, decoder)
|
||||
})
|
||||
integrateRemoteStructs(y, decoder)
|
||||
readDeleteSet(y, decoder)
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
}, true)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user