// Persistent cache for dictionary DAWG blobs, keyed by `${variant}@${version}`. // It lives in its own IndexedDB database, separate from session.ts's 'scrabble' // DB, so the multi-hundred-KB binary blobs never interfere with the session / // prefs store or its schema versioning. Everything here is best-effort: any // failure resolves to a miss or a no-op and the caller falls back to the network. const DB_NAME = 'scrabble-dict'; const STORE = 'dawg'; let dbPromise: Promise | null | undefined; function openDb(): Promise | null { if (dbPromise !== undefined) return dbPromise; if (typeof indexedDB === 'undefined') { dbPromise = null; return null; } dbPromise = new Promise((resolve, reject) => { const req = indexedDB.open(DB_NAME, 1); req.onupgradeneeded = () => req.result.createObjectStore(STORE); req.onsuccess = () => resolve(req.result); req.onerror = () => reject(req.error); }).catch(() => { dbPromise = null; throw new Error('indexedDB unavailable'); }); return dbPromise; } /** dictKey is the persistent cache key for a (variant, version) dictionary. */ export function dictKey(variant: string, version: string): string { return `${variant}@${version}`; } /** idbGetDawg returns the cached blob for key, or null on a miss or any failure. */ export async function idbGetDawg(key: string): Promise { const db = openDb(); if (!db) return null; try { const d = await db; return await new Promise((resolve, reject) => { const r = d.transaction(STORE, 'readonly').objectStore(STORE).get(key); r.onsuccess = () => resolve((r.result ?? null) as ArrayBuffer | null); r.onerror = () => reject(r.error); }); } catch { return null; } } /** idbPutDawg stores the blob under key, swallowing any failure (best-effort). */ export async function idbPutDawg(key: string, data: ArrayBuffer): Promise { const db = openDb(); if (!db) return; try { const d = await db; await new Promise((resolve, reject) => { const tx = d.transaction(STORE, 'readwrite'); tx.objectStore(STORE).put(data, key); tx.oncomplete = () => resolve(); tx.onerror = () => reject(tx.error); }); } catch { /* best-effort: a failed persist just re-downloads next time */ } } let persistRequested = false; /** * requestPersist asks the browser (once) not to evict this origin's storage, so a * cached dictionary survives storage pressure. The grant is honoured unevenly * across platforms (see docs), so it is only a hint — the cache stays best-effort. */ export async function requestPersist(): Promise { if (persistRequested) return; persistRequested = true; try { if (typeof navigator !== 'undefined' && navigator.storage?.persist) { await navigator.storage.persist(); } } catch { /* ignore — persistence is only a hint */ } } /** * clearDictCache empties the dictionary blob store (backs the DebugPanel reset). The * store is cleared rather than the database deleted, to avoid a delete blocked on an * open handle. Best-effort: a failure just leaves the cache to be reused. */ export async function clearDictCache(): Promise { const db = openDb(); if (!db) return; try { const d = await db; await new Promise((resolve, reject) => { const tx = d.transaction(STORE, 'readwrite'); tx.objectStore(STORE).clear(); tx.oncomplete = () => resolve(); tx.onerror = () => reject(tx.error); }); } catch { /* best-effort */ } }