Re-work storage adapter a bit

This commit is contained in:
Jon Staab
2025-11-14 14:59:27 -08:00
parent 62f573eac0
commit bac433b640
2 changed files with 69 additions and 93 deletions
+59 -90
View File
@@ -12,132 +12,101 @@ export type IDBAdapter = {
export type IDBAdapters = IDBAdapter[] export type IDBAdapters = IDBAdapter[]
export enum IDBStatus {
Ready = "ready",
Closed = "closed",
Opening = "opening",
Closing = "closing",
Initial = "initial",
}
export type IDBOptions = { export type IDBOptions = {
name: string name: string
version: number version: number
} }
export class IDB { export class IDB {
idbp: Maybe<Promise<IDBPDatabase>> adapters: IDBAdapters = []
connection: Maybe<Promise<IDBPDatabase>>
unsubscribers: Maybe<Unsubscriber[]> unsubscribers: Maybe<Unsubscriber[]>
status = IDBStatus.Initial
constructor(readonly options: IDBOptions) {} constructor(readonly options: IDBOptions) {}
async init(adapters: IDBAdapters) { async connect() {
if (this.idbp) { if (!this.connection) {
throw new Error("Unable to initialize a database that isn't yet closed") 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, { for (const table of idbDb.objectStoreNames) {
upgrade(idbDb: IDBPDatabase) { if (!names.has(table)) {
const names = new Set(adapters.map(a => a.name)) idbDb.deleteObjectStore(table)
}
for (const table of idbDb.objectStoreNames) {
if (!names.has(table)) {
idbDb.deleteObjectStore(table)
} }
}
for (const {name, keyPath} of adapters) { for (const {name, keyPath} of adapters) {
try { try {
idbDb.createObjectStore(name, {keyPath}) idbDb.createObjectStore(name, {keyPath})
} catch (e) { } catch (e) {
console.warn(e) console.warn(e)
}
} }
} },
}, blocked() {},
blocked() {}, blocking() {},
blocking() {}, })
})
return this.idbp.then(async idbp => {
window.addEventListener("beforeunload", () => idbp.close())
this.unsubscribers = await Promise.all(adapters.map(({name, init}) => init(this.table(name)))) this.unsubscribers = await Promise.all(adapters.map(({name, init}) => init(this.table(name))))
}
this.status = IDBStatus.Ready return this.connection
})
} }
table = <T>(name: string) => new IDBTable<T>(this, name) table = <T>(name: string) => new IDBTable<T>(this, name)
_withIDBP = async <T>(f: (db: IDBPDatabase) => Promise<T>) => {
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 <T>(table: string): Promise<T[]> => { getAll = async <T>(table: string): Promise<T[]> => {
const result = await this._withIDBP(async idbp => { const connection = await this.connect()
const tx = idbp.transaction(table, "readwrite") const tx = connection.transaction(table, "readwrite")
const store = tx.objectStore(table) const store = tx.objectStore(table)
const result = await store.getAll() const result = await store.getAll()
await tx.done await tx.done
return result
})
return result || [] return result || []
} }
bulkPut = async <T>(table: string, data: Iterable<T>) => bulkPut = async <T>(table: string, data: Iterable<T>) => {
this._withIDBP(async idbp => { const connection = await this.connect()
const tx = idbp.transaction(table, "readwrite") const tx = connection.transaction(table, "readwrite")
const store = tx.objectStore(table) const store = tx.objectStore(table)
await Promise.all( await Promise.all(
Array.from(data).map(item => { Array.from(data).map(item => {
try { try {
store.put(item) store.put(item)
} catch (e) { } catch (e) {
console.error(e, item) console.error(e, item)
} }
}), }),
) )
await tx.done await tx.done
}) }
bulkDelete = async (table: string, ids: Iterable<string>) => bulkDelete = async (table: string, ids: Iterable<string>) => {
this._withIDBP(async idbp => { const connection = await this.connect()
const tx = idbp.transaction(table, "readwrite") const tx = connection.transaction(table, "readwrite")
const store = tx.objectStore(table) const store = tx.objectStore(table)
await Promise.all(Array.from(ids).map(id => store.delete(id))) await Promise.all(Array.from(ids).map(id => store.delete(id)))
await tx.done await tx.done
}) }
close = () => close = () => {
this._withIDBP(async idbp => { this.unsubscribers?.forEach(call)
this.unsubscribers!.forEach(call) this.unsubscribers = undefined
this.status = IDBStatus.Closing
await idbp.close() this.connection?.then(c => c.close())
this.connection = undefined
this.idbp = undefined }
this.unsubscribers = undefined
this.status = IDBStatus.Closed
})
clear = async () => { clear = async () => {
await this.close() await this.connection?.then(c => c.close())
await deleteDB(this.options.name, { await deleteDB(this.options.name, {
blocked() {}, blocked() {},
}) })
+10 -3
View File
@@ -88,6 +88,9 @@
} }
}) })
// Cleanup on page close
window.addEventListener("beforeunload", () => db.close())
const unsubscribe = call(async () => { const unsubscribe = call(async () => {
const unsubscribers: Unsubscriber[] = [] 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 // Wait until data storage is initialized before syncing other stuff
if (!db.idbp) { await db.connect()
await db.init(storage.adapters)
} // Close the database connection on reload
unsubscribers.push(() => db.close())
// Add our extra policies now that we're set up // Add our extra policies now that we're set up
defaultSocketPolicies.push(...policies) defaultSocketPolicies.push(...policies)