diff --git a/src/lib/indexeddb.ts b/src/lib/indexeddb.ts index 5c7b139e..3ecab164 100644 --- a/src/lib/indexeddb.ts +++ b/src/lib/indexeddb.ts @@ -12,132 +12,101 @@ export type IDBAdapter = { export type IDBAdapters = IDBAdapter[] -export enum IDBStatus { - Ready = "ready", - Closed = "closed", - Opening = "opening", - Closing = "closing", - Initial = "initial", -} - export type IDBOptions = { name: string version: number } export class IDB { - idbp: Maybe> + adapters: IDBAdapters = [] + connection: Maybe> unsubscribers: Maybe - status = IDBStatus.Initial constructor(readonly options: IDBOptions) {} - async init(adapters: IDBAdapters) { - if (this.idbp) { - throw new Error("Unable to initialize a database that isn't yet closed") - } + async connect() { + if (!this.connection) { + const {name, version} = this.options + const adapters = this.adapters - this.status = IDBStatus.Opening + this.connection = openDB(name, version, { + upgrade(idbDb: IDBPDatabase) { + const names = new Set(adapters.map(a => a.name)) - this.idbp = openDB(this.options.name, this.options.version, { - upgrade(idbDb: IDBPDatabase) { - const names = new Set(adapters.map(a => a.name)) - - for (const table of idbDb.objectStoreNames) { - if (!names.has(table)) { - idbDb.deleteObjectStore(table) + for (const table of idbDb.objectStoreNames) { + if (!names.has(table)) { + idbDb.deleteObjectStore(table) + } } - } - for (const {name, keyPath} of adapters) { - try { - idbDb.createObjectStore(name, {keyPath}) - } catch (e) { - console.warn(e) + for (const {name, keyPath} of adapters) { + try { + idbDb.createObjectStore(name, {keyPath}) + } catch (e) { + console.warn(e) + } } - } - }, - blocked() {}, - blocking() {}, - }) - - return this.idbp.then(async idbp => { - window.addEventListener("beforeunload", () => idbp.close()) + }, + blocked() {}, + blocking() {}, + }) this.unsubscribers = await Promise.all(adapters.map(({name, init}) => init(this.table(name)))) + } - this.status = IDBStatus.Ready - }) + return this.connection } table = (name: string) => new IDBTable(this, name) - _withIDBP = async (f: (db: IDBPDatabase) => Promise) => { - if (this.status === IDBStatus.Initial) { - throw new Error("Database was accessed in initial state") - } - - // If we're closing, ignore any lingering requests - if ([IDBStatus.Closed, IDBStatus.Closing].includes(this.status)) return - - return f(await this.idbp!) - } - getAll = async (table: string): Promise => { - const result = await this._withIDBP(async idbp => { - const tx = idbp.transaction(table, "readwrite") - const store = tx.objectStore(table) - const result = await store.getAll() + const connection = await this.connect() + const tx = connection.transaction(table, "readwrite") + const store = tx.objectStore(table) + const result = await store.getAll() - await tx.done - - return result - }) + await tx.done return result || [] } - bulkPut = async (table: string, data: Iterable) => - this._withIDBP(async idbp => { - const tx = idbp.transaction(table, "readwrite") - const store = tx.objectStore(table) + bulkPut = async (table: string, data: Iterable) => { + const connection = await this.connect() + const tx = connection.transaction(table, "readwrite") + const store = tx.objectStore(table) - await Promise.all( - Array.from(data).map(item => { - try { - store.put(item) - } catch (e) { - console.error(e, item) - } - }), - ) + await Promise.all( + Array.from(data).map(item => { + try { + store.put(item) + } catch (e) { + console.error(e, item) + } + }), + ) - await tx.done - }) + await tx.done + } - bulkDelete = async (table: string, ids: Iterable) => - this._withIDBP(async idbp => { - const tx = idbp.transaction(table, "readwrite") - const store = tx.objectStore(table) + bulkDelete = async (table: string, ids: Iterable) => { + const connection = await this.connect() + const tx = connection.transaction(table, "readwrite") + const store = tx.objectStore(table) - await Promise.all(Array.from(ids).map(id => store.delete(id))) - await tx.done - }) + await Promise.all(Array.from(ids).map(id => store.delete(id))) + await tx.done + } - close = () => - this._withIDBP(async idbp => { - this.unsubscribers!.forEach(call) - this.status = IDBStatus.Closing + close = () => { + this.unsubscribers?.forEach(call) + this.unsubscribers = undefined - await idbp.close() - - this.idbp = undefined - this.unsubscribers = undefined - this.status = IDBStatus.Closed - }) + this.connection?.then(c => c.close()) + this.connection = undefined + } clear = async () => { - await this.close() + await this.connection?.then(c => c.close()) await deleteDB(this.options.name, { blocked() {}, }) diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index f3adba9f..424a4b1b 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -88,6 +88,9 @@ } }) + // Cleanup on page close + window.addEventListener("beforeunload", () => db.close()) + const unsubscribe = call(async () => { const unsubscribers: Unsubscriber[] = [] @@ -110,10 +113,14 @@ }), ]) + // Set up our storage adapters + db.adapters = storage.adapters + // Wait until data storage is initialized before syncing other stuff - if (!db.idbp) { - await db.init(storage.adapters) - } + await db.connect() + + // Close the database connection on reload + unsubscribers.push(() => db.close()) // Add our extra policies now that we're set up defaultSocketPolicies.push(...policies)