140 lines
3.2 KiB
TypeScript
140 lines
3.2 KiB
TypeScript
import type { Creature, CreatureId } from "@initiative/domain";
|
|
import { type IDBPDatabase, openDB } from "idb";
|
|
|
|
const DB_NAME = "initiative-bestiary";
|
|
const STORE_NAME = "sources";
|
|
const DB_VERSION = 1;
|
|
|
|
export interface CachedSourceInfo {
|
|
readonly sourceCode: string;
|
|
readonly displayName: string;
|
|
readonly creatureCount: number;
|
|
readonly cachedAt: number;
|
|
}
|
|
|
|
interface CachedSourceRecord {
|
|
sourceCode: string;
|
|
displayName: string;
|
|
creatures: Creature[];
|
|
cachedAt: number;
|
|
creatureCount: number;
|
|
}
|
|
|
|
let db: IDBPDatabase | null = null;
|
|
let dbFailed = false;
|
|
|
|
// In-memory fallback when IndexedDB is unavailable
|
|
const memoryStore = new Map<string, CachedSourceRecord>();
|
|
|
|
async function getDb(): Promise<IDBPDatabase | null> {
|
|
if (db) return db;
|
|
if (dbFailed) return null;
|
|
|
|
try {
|
|
db = await openDB(DB_NAME, DB_VERSION, {
|
|
upgrade(database) {
|
|
if (!database.objectStoreNames.contains(STORE_NAME)) {
|
|
database.createObjectStore(STORE_NAME, {
|
|
keyPath: "sourceCode",
|
|
});
|
|
}
|
|
},
|
|
});
|
|
return db;
|
|
} catch {
|
|
dbFailed = true;
|
|
console.warn(
|
|
"IndexedDB unavailable — bestiary cache will not persist across sessions.",
|
|
);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export async function cacheSource(
|
|
sourceCode: string,
|
|
displayName: string,
|
|
creatures: Creature[],
|
|
): Promise<void> {
|
|
const record: CachedSourceRecord = {
|
|
sourceCode,
|
|
displayName,
|
|
creatures,
|
|
cachedAt: Date.now(),
|
|
creatureCount: creatures.length,
|
|
};
|
|
|
|
const database = await getDb();
|
|
if (database) {
|
|
await database.put(STORE_NAME, record);
|
|
} else {
|
|
memoryStore.set(sourceCode, record);
|
|
}
|
|
}
|
|
|
|
export async function isSourceCached(sourceCode: string): Promise<boolean> {
|
|
const database = await getDb();
|
|
if (database) {
|
|
const record = await database.get(STORE_NAME, sourceCode);
|
|
return record !== undefined;
|
|
}
|
|
return memoryStore.has(sourceCode);
|
|
}
|
|
|
|
export async function getCachedSources(): Promise<CachedSourceInfo[]> {
|
|
const database = await getDb();
|
|
if (database) {
|
|
const all: CachedSourceRecord[] = await database.getAll(STORE_NAME);
|
|
return all.map((r) => ({
|
|
sourceCode: r.sourceCode,
|
|
displayName: r.displayName,
|
|
creatureCount: r.creatureCount,
|
|
cachedAt: r.cachedAt,
|
|
}));
|
|
}
|
|
return [...memoryStore.values()].map((r) => ({
|
|
sourceCode: r.sourceCode,
|
|
displayName: r.displayName,
|
|
creatureCount: r.creatureCount,
|
|
cachedAt: r.cachedAt,
|
|
}));
|
|
}
|
|
|
|
export async function clearSource(sourceCode: string): Promise<void> {
|
|
const database = await getDb();
|
|
if (database) {
|
|
await database.delete(STORE_NAME, sourceCode);
|
|
} else {
|
|
memoryStore.delete(sourceCode);
|
|
}
|
|
}
|
|
|
|
export async function clearAll(): Promise<void> {
|
|
const database = await getDb();
|
|
if (database) {
|
|
await database.clear(STORE_NAME);
|
|
} else {
|
|
memoryStore.clear();
|
|
}
|
|
}
|
|
|
|
export async function loadAllCachedCreatures(): Promise<
|
|
Map<CreatureId, Creature>
|
|
> {
|
|
const map = new Map<CreatureId, Creature>();
|
|
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;
|
|
}
|