import { DEFAULT_IDB_DATABASE, DEFAULT_IDB_OBJECT_STORE, } from "../constants/index"; import type { SyncQueueEntry } from "../types/common"; import type { SyncQueueStore } from "./sync-queue.types"; /** Options consumed by {@link IndexedDbStore}. */ export interface IndexedDbStoreOptions { /** `IDBFactory` seam. Defaults to `globalThis.indexedDB`. */ factory?: IDBFactory; /** Database name. Defaults to `__suaveplan_pwa__`. */ databaseName?: string; /** Object-store name. Defaults to `sync_queue`. */ objectStoreName?: string; } /** * IndexedDB-backed `SyncQueueStore`. Default production store: scales beyond * the 5 MiB `localStorage` budget and lets large request bodies persist * without blocking the main thread on JSON serialisation. */ export class IndexedDbStore implements SyncQueueStore { private readonly _factory: IDBFactory; private readonly _databaseName: string; private readonly _objectStoreName: string; public constructor(options: IndexedDbStoreOptions = {}) { const candidate = options.factory ?? (globalThis as { indexedDB?: IDBFactory }).indexedDB; if (candidate === undefined) { throw new Error( "IndexedDbStore requires an IDBFactory; none was supplied and globalThis.indexedDB is undefined" ); } this._factory = candidate; this._databaseName = options.databaseName ?? DEFAULT_IDB_DATABASE; this._objectStoreName = options.objectStoreName ?? DEFAULT_IDB_OBJECT_STORE; } public async load(): Promise[]> { const db = await this._open(); try { return await new Promise[]>( (resolve, reject) => { const tx = db.transaction( this._objectStoreName, "readonly" ); const store = tx.objectStore(this._objectStoreName); const req = store.getAll(); req.onsuccess = () => { const result = req.result as Record[]; resolve(result); }; req.onerror = () => reject(req.error ?? new Error("idb getAll failed")); } ); } finally { db.close(); } } public async save(entries: readonly SyncQueueEntry[]): Promise { const db = await this._open(); try { await new Promise((resolve, reject) => { const tx = db.transaction(this._objectStoreName, "readwrite"); const store = tx.objectStore(this._objectStoreName); store.clear(); for (const entry of entries) { store.put(entry); } tx.oncomplete = () => resolve(); tx.onerror = () => reject(tx.error ?? new Error("idb save failed")); tx.onabort = () => reject(tx.error ?? new Error("idb save aborted")); }); } finally { db.close(); } } private async _open(): Promise { return await new Promise((resolve, reject) => { const req = this._factory.open(this._databaseName, 1); req.onupgradeneeded = () => { // `onupgradeneeded` only fires when the requested version is // higher than the on-disk version; at v1 fresh installs there // is no existing object store to clash with. req.result.createObjectStore(this._objectStoreName, { keyPath: "id", }); }; req.onsuccess = () => resolve(req.result); req.onerror = () => reject(req.error ?? new Error("idb open failed")); req.onblocked = () => reject(new Error("idb open blocked by another connection")); }); } }