import {openDB, deleteDB} from "idb" import type {IDBPDatabase} from "idb" import {writable} from "svelte/store" import type {Unsubscriber, Writable} from "svelte/store" import {indexBy, throttle, fromPairs} from "@welshman/lib" import type {TrustedEvent, Repository} from "@welshman/util" import type {Tracker} from "@welshman/net" import {withGetter, adapter, throttled, custom} from "@welshman/store" export type IndexedDbAdapter = { keyPath: string store: Writable } export let db: IDBPDatabase export const dead = withGetter(writable(false)) export const subs: Unsubscriber[] = [] export const getAll = async (name: string) => { const tx = db.transaction(name, "readwrite") const store = tx.objectStore(name) const result = await store.getAll() await tx.done return result } export const bulkPut = async (name: string, data: any[]) => { const tx = db.transaction(name, "readwrite") const store = tx.objectStore(name) await Promise.all(data.map(item => store.put(item))) await tx.done } export const bulkDelete = async (name: string, ids: string[]) => { const tx = db.transaction(name, "readwrite") const store = tx.objectStore(name) await Promise.all(ids.map(id => store.delete(id))) await tx.done } export const initIndexedDbAdapter = async (name: string, adapter: IndexedDbAdapter) => { let prevRecords = await getAll(name) adapter.store.set(prevRecords) adapter.store.subscribe( async (currentRecords: any[]) => { if (dead.get()) { return } const currentIds = new Set(currentRecords.map(item => item[adapter.keyPath])) const removedRecords = prevRecords.filter(r => !currentIds.has(r[adapter.keyPath])) const prevRecordsById = indexBy(item => item[adapter.keyPath], prevRecords) const updatedRecords = currentRecords.filter(r => r !== prevRecordsById.get(r[adapter.keyPath])) prevRecords = currentRecords if (updatedRecords.length > 0) { await bulkPut(name, updatedRecords) } if (removedRecords.length > 0) { await bulkDelete( name, removedRecords.map(item => item[adapter.keyPath]), ) } }, ) } export const initStorage = async (name: string, version: number, adapters: Record) => { if (!window.indexedDB) return window.addEventListener("beforeunload", () => closeStorage()) if (db) { throw new Error("Db initialized multiple times") } db = await openDB(name, version, { upgrade(db: IDBPDatabase) { const names = Object.keys(adapters) for (const name of db.objectStoreNames) { if (!names.includes(name)) { db.deleteObjectStore(name) } } for (const [name, {keyPath}] of Object.entries(adapters)) { try { db.createObjectStore(name, {keyPath}) } catch (e) { console.warn(e) } } }, }) await Promise.all( Object.entries(adapters).map(([name, config]) => initIndexedDbAdapter(name, config)), ) } export const closeStorage = async () => { dead.set(true) subs.forEach(unsub => unsub()) await db?.close() } export const clearStorage = async () => { await closeStorage() await deleteDB(db.name) } export type StorageAdapterOptions = { throttle?: number migrate?: (items: any[]) => any[] } const migrate = (data: any[], options: StorageAdapterOptions) => options.migrate ? options.migrate(data) : data export const storageAdapters = { fromObjectStore: (store: Writable>, options: StorageAdapterOptions = {}) => ({ options, keyPath: "key", store: adapter({ store: throttled(options.throttle || 0, store), forward: (data: Record) => migrate(Object.entries(data).map(([key, value]) => ({key, value})), options), backward: (data: {key: string, value: T}[]) => fromPairs(data.map(({key, value}) => [key, value])), }), }), fromMapStore: (store: Writable>, options: StorageAdapterOptions = {}) => ({ options, keyPath: "key", store: adapter({ store: throttled(options.throttle || 0, store), forward: (data: Map) => migrate(Array.from(data.entries()).map(([key, value]) => ({key, value})), options), backward: (data: {key: string, value: T}[]) => new Map(data.map(({key, value}) => [key, value])), }), }), fromTracker: (tracker: Tracker, options: StorageAdapterOptions = {}) => ({ options, keyPath: 'key', store: custom(setter => { let onUpdate = () => setter( migrate( Array.from(tracker.relaysById.entries()) .map(([key, urls]) => ({key, value: Array.from(urls)})), options ) ) if (options.throttle) { onUpdate = throttle(options.throttle, onUpdate) } onUpdate() tracker.on('update', onUpdate) return () => tracker.off('update', onUpdate) }, { set: (data: {key: string, value: string[]}[]) => tracker.load(new Map(data.map(({key, value}) => [key, new Set(value)]))), }), }), fromRepository: (repository: Repository, options: StorageAdapterOptions = {}) => ({ options, keyPath: 'id', store: custom(setter => { let onUpdate = () => setter(migrate(repository.dump(), options)) if (options.throttle) { onUpdate = throttle(options.throttle, onUpdate) } onUpdate() repository.on('update', onUpdate) return () => repository.off('update', onUpdate) }, { set: (events: TrustedEvent[]) => repository.load(events), }), }), fromRepositoryAndTracker: ( repository: Repository, tracker: Tracker, options: StorageAdapterOptions = {} ) => ({ options, keyPath: 'id', store: custom(setter => { let onUpdate = () => { const events = migrate(repository.dump(), options) setter( events.map(event => { const relays = Array.from(tracker.getRelays(event.id)) return {id: event.id, event, relays} }) ) } if (options.throttle) { onUpdate = throttle(options.throttle, onUpdate) } onUpdate() tracker.on('update', onUpdate) repository.on('update', onUpdate) return () => { tracker.off('update', onUpdate) } }, { set: (items: {event: TrustedEvent, relays: string[]}[]) => { const events: TrustedEvent[] = [] const relaysById = new Map>() for (const {event, relays} of items) { events.push(event) relaysById.set(event.id, new Set(relays)) } repository.load(events) tracker.load(relaysById) }, }), }) }