flotilla/src/lib/indexeddb.ts

151 lines
3.7 KiB
TypeScript
Raw Normal View History

2025-11-12 01:49:22 +00:00
import {openDB, deleteDB} from "idb"
import type {IDBPDatabase} from "idb"
import {spec} from "@welshman/lib"
2025-11-12 01:49:22 +00:00
import type {Maybe} from "@welshman/lib"
2026-03-12 18:33:04 +00:00
export type IDBStore = {
2025-11-12 01:49:22 +00:00
name: string
2025-11-14 00:39:44 +00:00
keyPath: string
2025-11-12 01:49:22 +00:00
}
export type IDBOptions = {
name: string
2026-03-12 18:33:04 +00:00
stores: IDBStore[]
2025-11-12 01:49:22 +00:00
}
export class IDB {
2026-07-28 16:16:26 +00:00
connection: Maybe<Promise<Maybe<IDBPDatabase>>>
2026-02-09 19:59:58 +00:00
failedToConnect = false
2025-11-12 01:49:22 +00:00
constructor(readonly options: IDBOptions) {}
2026-07-28 16:16:26 +00:00
// Object stores can only be created during a version change, and which stores we need depends
// on who is logged in, so open at whatever version exists and bump it to reconcile the schema.
2026-07-28 16:16:26 +00:00
private open = async () => {
const {name, stores} = this.options
2026-08-13 00:37:57 +00:00
const blocking = () => this.close()
const db = await openDB(name, undefined, {blocking})
2026-07-28 16:16:26 +00:00
const missing = stores.filter(store => !db.objectStoreNames.contains(store.name))
const obsolete = Array.from(db.objectStoreNames).filter(
storeName => !stores.some(spec({name: storeName})),
)
2026-07-28 16:16:26 +00:00
if (missing.length === 0 && obsolete.length === 0) {
2026-07-28 16:16:26 +00:00
return db
}
const version = db.version + 1
db.close()
return openDB(name, version, {
upgrade(idbDb: IDBPDatabase) {
for (const {name, keyPath} of missing) {
idbDb.createObjectStore(name, {keyPath})
}
for (const storeName of obsolete) {
idbDb.deleteObjectStore(storeName)
}
2026-07-28 16:16:26 +00:00
},
2026-08-13 00:37:57 +00:00
blocked: (currentVersion, blockedVersion) =>
console.error(
`Upgrade of ${name} from ${currentVersion} to ${blockedVersion} is blocked by another connection`,
),
blocking,
2026-07-28 16:16:26 +00:00
})
}
2025-11-14 22:59:27 +00:00
async connect() {
2026-02-09 19:59:58 +00:00
if (!this.failedToConnect && !this.connection) {
2026-07-28 16:16:26 +00:00
this.connection = this.open().catch(e => {
2026-02-09 19:59:58 +00:00
console.error("Failed to connect to indexeddb", e)
2026-07-28 16:16:26 +00:00
2026-02-09 19:59:58 +00:00
this.failedToConnect = true
2026-07-28 16:16:26 +00:00
return undefined
})
2025-11-14 22:59:27 +00:00
}
2025-11-12 01:49:22 +00:00
2025-11-14 22:59:27 +00:00
return this.connection
2025-11-12 01:49:22 +00:00
}
table = <T>(name: string) => new IDBTable<T>(this, name)
2025-11-13 00:20:38 +00:00
getAll = async <T>(table: string): Promise<T[]> => {
2025-11-14 22:59:27 +00:00
const connection = await this.connect()
2026-02-09 19:59:58 +00:00
if (!connection) return []
2026-03-12 18:01:15 +00:00
const tx = connection.transaction(table, "readonly")
2025-11-14 22:59:27 +00:00
const store = tx.objectStore(table)
const result = await store.getAll()
2025-11-12 01:49:22 +00:00
2025-11-14 22:59:27 +00:00
await tx.done
2025-11-12 01:49:22 +00:00
2025-11-13 00:20:38 +00:00
return result || []
}
2025-11-14 22:59:27 +00:00
bulkPut = async <T>(table: string, data: Iterable<T>) => {
const connection = await this.connect()
2026-02-09 19:59:58 +00:00
if (!connection) return
2025-11-14 22:59:27 +00:00
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)
}
}),
)
2025-11-12 01:49:22 +00:00
2025-11-14 22:59:27 +00:00
await tx.done
}
2025-11-12 01:49:22 +00:00
2025-11-14 22:59:27 +00:00
bulkDelete = async (table: string, ids: Iterable<string>) => {
const connection = await this.connect()
2026-02-09 19:59:58 +00:00
if (!connection) return
2025-11-14 22:59:27 +00:00
const tx = connection.transaction(table, "readwrite")
const store = tx.objectStore(table)
2025-11-12 01:49:22 +00:00
2025-11-14 22:59:27 +00:00
await Promise.all(Array.from(ids).map(id => store.delete(id)))
await tx.done
}
2025-11-12 01:49:22 +00:00
2026-08-13 00:37:57 +00:00
close = async () => {
const connection = this.connection
2025-11-14 22:59:27 +00:00
this.connection = undefined
2026-08-13 00:37:57 +00:00
await connection?.then(c => c?.close())
2025-11-14 22:59:27 +00:00
}
2025-11-12 01:49:22 +00:00
clear = async () => {
2026-08-13 00:37:57 +00:00
await this.close()
2025-11-12 01:49:22 +00:00
await deleteDB(this.options.name, {
2026-08-13 00:37:57 +00:00
blocked: currentVersion =>
console.error(`Deletion of ${this.options.name} at ${currentVersion} is blocked`),
2025-11-12 01:49:22 +00:00
})
}
}
export class IDBTable<T> {
constructor(
readonly db: IDB,
readonly name: string,
) {}
getAll = () => this.db.getAll<T>(this.name)
bulkPut = (data: Iterable<T>) => this.db.bulkPut(this.name, data)
bulkDelete = (ids: Iterable<string>) => this.db.bulkDelete(this.name, ids)
}