@daaku/kombat-indexed-db
Version:
Kombat storage implemented using IndexedDB.
160 lines • 6.02 kB
JavaScript
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