UNPKG

@daaku/kombat-indexed-db

Version:

Kombat storage implemented using IndexedDB.

160 lines 6.02 kB
function latestMessageKey(msg) { return `${msg.dataset}:${msg.row}:${msg.column}`; } export function syncDatasetIndexedDB(db, prefix = '') { const dsName = (name) => `${prefix}${name}`; return async (changes) => { const storeNames = Object.keys(changes).map(dsName); const t = db.transaction(storeNames, 'readwrite'); await Promise.all(Object.keys(changes).map(async (dataset) => { const store = t.objectStore(dsName(dataset)); await Promise.all(Object.keys(changes[dataset]).map(async (id) => { let row = await store.get(id); if (!row) { row = { id, ...changes[dataset][id] }; } else { row = { ...row, ...changes[dataset][id] }; } await store.put(row); })); })); await t.done; }; } // eslint-disable-next-line @typescript-eslint/no-explicit-any export function syncDatasetMem(mem) { return async (changes) => { Object.keys(changes).map(datasetName => { let dataset = mem[datasetName]; if (!dataset) { mem[datasetName] = dataset = {}; } Object.keys(changes[datasetName]).map(id => { let row = dataset[id]; if (!row) { row = { id, ...changes[datasetName][id] }; } else { row = { ...row, ...changes[datasetName][id] }; } dataset[id] = row; }); }); }; } // eslint-disable-next-line @typescript-eslint/no-explicit-any export async function loadDatasetMem(mem, db, prefix = '') { ; (await db.getAll(`${prefix}message_latest`)).forEach(({ dataset, row, column, value }) => { let d = mem[dataset]; if (!d) { d = mem[dataset] = {}; } let r = d[row]; if (!r) { r = d[row] = { id: row }; } r[column] = value; }); } export class LocalIndexedDB { #db; #messageLogStoreName; #latestMessageStoreName; #messageMetaStoreName; #changeListeners = []; // Construct a LocalIndexedDB instance. constructor(internalPrefix = '') { this.#messageLogStoreName = `${internalPrefix}message_log`; this.#latestMessageStoreName = `${internalPrefix}message_latest`; this.#messageMetaStoreName = `${internalPrefix}message_meta`; } // Add a listener for changes. Returned function can be called to unsubscribe. listenChanges(cb) { this.#changeListeners.push(cb); return () => { this.#changeListeners = this.#changeListeners.filter(e => e != cb); }; } // This method should be called in your upgrade callback. upgradeDB(db) { if (!db.objectStoreNames.contains(this.#messageLogStoreName)) { db.createObjectStore(this.#messageLogStoreName, { keyPath: 'timestamp' }); } ; [this.#latestMessageStoreName, this.#messageMetaStoreName].forEach(name => { if (!db.objectStoreNames.contains(name)) { db.createObjectStore(name); } }); } // This should be called with the initialized DB before you begin using the // instance. setDB(db) { this.#db = db; } async applyChanges(messages) { // consolidate the changes by dataset as well as row id. // consolidating by row id is important because if we have multiple changes // to the same row we will not read changes made within the transaction, // there by causing only the last write to survive. const changes = {}; messages.map(msg => { let dataset = changes[msg.dataset]; if (!dataset) { dataset = changes[msg.dataset] = {}; } let row = dataset[msg.row]; if (!row) { row = dataset[msg.row] = {}; } row[msg.column] = msg.value; }); this.#changeListeners.forEach(c => c(changes)); } async storeMessages(messages) { const t = this.#db.transaction([this.#messageLogStoreName, this.#latestMessageStoreName], 'readwrite'); const messageLogStore = t.objectStore(this.#messageLogStoreName); const latestMessageStore = t.objectStore(this.#latestMessageStoreName); const results = await Promise.all(messages.map(async (msg) => { const row = await messageLogStore.get(msg.timestamp); if (!row) { await messageLogStore.put(msg); // just stored a new message, update latestMessage if necessary const key = latestMessageKey(msg); const existingLatest = await latestMessageStore.get(key); if (!existingLatest || existingLatest.timestamp < msg.timestamp) { await latestMessageStore.put(msg, key); } } return !row; })); await t.done; return results; } async queryMessages(since) { const t = this.#db.transaction(this.#messageLogStoreName); const results = []; let cursor = await t.store.openCursor(IDBKeyRange.lowerBound(since)); while (cursor) { results.push(cursor.value); cursor = await cursor.continue(); } await t.done; return results; } async queryLatestMessages(messages) { const t = this.#db.transaction(this.#latestMessageStoreName); const results = await Promise.all(messages.map(msg => t.store.get(latestMessageKey(msg)))); await t.done; return results; } async set(key, value) { await this.#db.put(this.#messageMetaStoreName, value, key); } async get(key) { return await this.#db.get(this.#messageMetaStoreName, key); } } //# sourceMappingURL=index.js.map