2025-11-12 01:49:22 +00:00
|
|
|
import {openDB, deleteDB} from "idb"
|
|
|
|
|
import type {IDBPDatabase} from "idb"
|
|
|
|
|
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 add missing ones.
|
|
|
|
|
private open = async () => {
|
|
|
|
|
const {name, stores} = this.options
|
|
|
|
|
const db = await openDB(name)
|
|
|
|
|
const missing = stores.filter(store => !db.objectStoreNames.contains(store.name))
|
|
|
|
|
|
|
|
|
|
if (missing.length === 0) {
|
|
|
|
|
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})
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
blocked() {},
|
|
|
|
|
blocking() {},
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
|
2025-11-14 22:59:27 +00:00
|
|
|
close = () => {
|
2026-07-28 16:16:26 +00:00
|
|
|
this.connection?.then(c => c?.close())
|
2025-11-14 22:59:27 +00:00
|
|
|
this.connection = undefined
|
|
|
|
|
}
|
2025-11-12 01:49:22 +00:00
|
|
|
|
|
|
|
|
clear = async () => {
|
2026-07-28 16:16:26 +00:00
|
|
|
await this.connection?.then(c => c?.close())
|
2025-11-12 01:49:22 +00:00
|
|
|
await deleteDB(this.options.name, {
|
|
|
|
|
blocked() {},
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
}
|