// Session + preferences persistence. The session token lives in memory for the app // session and is mirrored to IndexedDB when available (so a reload does not force a // re-login), with a localStorage fallback. Losing the store just means re-login — // acceptable, and for a guest it simply mints a fresh guest. import type { Session } from './model'; import type { ThemePref } from './theme'; import type { Locale } from './i18n/catalog'; import type { BoardLabelMode } from './boardlabels'; const DB_NAME = 'scrabble'; const STORE = 'kv'; const LS_PREFIX = 'scrabble.'; 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; } function lsGet(key: string): T | null { try { const v = localStorage.getItem(LS_PREFIX + key); return v ? (JSON.parse(v) as T) : null; } catch { return null; } } function lsSet(key: string, value: unknown): void { try { localStorage.setItem(LS_PREFIX + key, JSON.stringify(value)); } catch { /* storage unavailable — stay in-memory only */ } } function lsDel(key: string): void { try { localStorage.removeItem(LS_PREFIX + key); } catch { /* ignore */ } } async function kvGet(key: string): Promise { const db = openDb(); if (!db) return lsGet(key); 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 T | null); r.onerror = () => reject(r.error); }); } catch { return lsGet(key); } } async function kvSet(key: string, value: unknown): Promise { const db = openDb(); if (!db) return lsSet(key, value); try { const d = await db; await new Promise((resolve, reject) => { const tx = d.transaction(STORE, 'readwrite'); tx.objectStore(STORE).put(value, key); tx.oncomplete = () => resolve(); tx.onerror = () => reject(tx.error); }); } catch { lsSet(key, value); } } async function kvDel(key: string): Promise { const db = openDb(); if (!db) return lsDel(key); try { const d = await db; await new Promise((resolve, reject) => { const tx = d.transaction(STORE, 'readwrite'); tx.objectStore(STORE).delete(key); tx.oncomplete = () => resolve(); tx.onerror = () => reject(tx.error); }); } catch { lsDel(key); } } const SESSION_KEY = 'session'; const PREFS_KEY = 'prefs'; export function loadSession(): Promise { return kvGet(SESSION_KEY); } export function saveSession(s: Session): Promise { return kvSet(SESSION_KEY, s); } export function clearSession(): Promise { return kvDel(SESSION_KEY); } export interface Prefs { theme: ThemePref; locale: Locale; reduceMotion: boolean; boardLabels: BoardLabelMode; } export async function loadPrefs(): Promise> { return (await kvGet(PREFS_KEY)) ?? {}; } export function savePrefs(p: Prefs): Promise { return kvSet(PREFS_KEY, p); }