import type { AnyCreature, CreatureId } from "@initiative/domain"; import { type IDBPDatabase, openDB } from "idb"; const DB_NAME = "initiative-bestiary"; const STORE_NAME = "sources"; const DB_VERSION = 4; interface CachedSourceInfo { readonly sourceCode: string; readonly displayName: string; readonly creatureCount: number; readonly cachedAt: number; readonly system?: string; } interface CachedSourceRecord { sourceCode: string; displayName: string; creatures: AnyCreature[]; cachedAt: number; creatureCount: number; system?: string; } let db: IDBPDatabase | null = null; let dbFailed = false; // In-memory fallback when IndexedDB is unavailable const memoryStore = new Map(); function scopedKey(system: string, sourceCode: string): string { return `${system}:${sourceCode}`; } async function getDb(): Promise { if (db) return db; if (dbFailed) return null; try { db = await openDB(DB_NAME, DB_VERSION, { upgrade(database, oldVersion, _newVersion, transaction) { if (oldVersion < 1) { database.createObjectStore(STORE_NAME, { keyPath: "sourceCode", }); } if ( oldVersion < DB_VERSION && database.objectStoreNames.contains(STORE_NAME) ) { // Clear cached creatures so they get re-normalized with latest rendering void transaction.objectStore(STORE_NAME).clear(); } }, }); return db; } catch { dbFailed = true; console.warn( "IndexedDB unavailable — bestiary cache will not persist across sessions.", ); return null; } } export async function cacheSource( system: string, sourceCode: string, displayName: string, creatures: AnyCreature[], ): Promise { const key = scopedKey(system, sourceCode); const record: CachedSourceRecord = { sourceCode: key, displayName, creatures, cachedAt: Date.now(), creatureCount: creatures.length, system, }; const database = await getDb(); if (database) { await database.put(STORE_NAME, record); } else { memoryStore.set(key, record); } } export async function isSourceCached( system: string, sourceCode: string, ): Promise { const key = scopedKey(system, sourceCode); const database = await getDb(); if (database) { const record = await database.get(STORE_NAME, key); return record !== undefined; } return memoryStore.has(key); } export async function getCachedSources( system?: string, ): Promise { const database = await getDb(); let records: CachedSourceRecord[]; if (database) { records = await database.getAll(STORE_NAME); } else { records = [...memoryStore.values()]; } const filtered = system ? records.filter((r) => r.system === system) : records; return filtered.map((r) => ({ sourceCode: r.system ? r.sourceCode.slice(r.system.length + 1) : r.sourceCode, displayName: r.displayName, creatureCount: r.creatureCount, cachedAt: r.cachedAt, system: r.system, })); } export async function clearSource( system: string, sourceCode: string, ): Promise { const key = scopedKey(system, sourceCode); const database = await getDb(); if (database) { await database.delete(STORE_NAME, key); } else { memoryStore.delete(key); } } export async function clearAll(): Promise { const database = await getDb(); if (database) { await database.clear(STORE_NAME); } else { memoryStore.clear(); } } export async function loadAllCachedCreatures(): Promise< Map > { const map = new Map(); const database = await getDb(); let records: CachedSourceRecord[]; if (database) { records = await database.getAll(STORE_NAME); } else { records = [...memoryStore.values()]; } for (const record of records) { for (const creature of record.creatures) { map.set(creature.id, creature); } } return map; }