import type { KeyValueDatabase, KeyValueDatabaseFactory, } from "@vrtmrz/livesync-commonlib/compat/interfaces/KeyValueDatabase"; import { deleteDB, openDB, type IDBPDatabase } from "idb"; import { serialized } from "octagonal-wheels/concurrency/lock"; /** Creates an application-owned IndexedDB key-value factory for browser runtimes. */ export function createBrowserKeyValueDatabaseFactory(): KeyValueDatabaseFactory { const cache = new Map(); return async (databaseKey) => await serialized(`OpenBrowserKeyValueDatabase-${databaseKey}`, async () => { const cached = cache.get(databaseKey); if (cached && !cached.isDestroyed) { return cached; } if (cached) { await cached.ensuredDestroyed; cache.delete(databaseKey); } const database = new BrowserKeyValueDatabase(databaseKey); await database.getIsReady(); cache.set(databaseKey, database); return database; }); } class BrowserKeyValueDatabase implements KeyValueDatabase { private databasePromise?: Promise>; private destroyed = false; private destroyedPromise?: Promise; constructor(private readonly databaseKey: string) {} get isDestroyed(): boolean { return this.destroyed; } get ensuredDestroyed(): Promise { return this.destroyedPromise ?? Promise.resolve(); } async getIsReady(): Promise { await this.ensureDatabase(); return !this.destroyed; } private ensureDatabase(): Promise> { if (this.destroyed) { throw new Error("Database is destroyed"); } this.databasePromise ??= openDB(this.databaseKey, undefined, { upgrade: (database) => { if (!database.objectStoreNames.contains(this.databaseKey)) { database.createObjectStore(this.databaseKey); } }, blocking: () => { void this.closeDatabase(); }, terminated: () => { this.databasePromise = undefined; }, }).catch((error: unknown) => { this.databasePromise = undefined; throw error; }); return this.databasePromise; } private get database(): Promise> { return this.ensureDatabase(); } private async closeDatabase(): Promise { const databasePromise = this.databasePromise; this.databasePromise = undefined; if (databasePromise) { (await databasePromise).close(); } } async get(key: IDBValidKey): Promise { return (await (await this.database).get(this.databaseKey, key)) as T; } async set(key: IDBValidKey, value: T): Promise { await (await this.database).put(this.databaseKey, value, key); return key; } async del(key: IDBValidKey): Promise { await (await this.database).delete(this.databaseKey, key); } async clear(): Promise { await (await this.database).clear(this.databaseKey); } async keys(query?: IDBValidKey | IDBKeyRange, count?: number): Promise { return await (await this.database).getAllKeys(this.databaseKey, query, count); } async close(): Promise { await this.closeDatabase(); } async destroy(): Promise { if (this.destroyedPromise) { await this.destroyedPromise; return; } this.destroyed = true; this.destroyedPromise = (async () => { await this.closeDatabase(); await deleteDB(this.databaseKey); })(); await this.destroyedPromise; } }