Stage 7 (wip): UI shell, libs, mock transport, screens (lobby->game), e2e smoke
- plain Svelte 5 + TS + Vite (no SvelteKit); CSS-token design system (Telegram-ready), hash router, IndexedDB session - pure libs: domain model, premium/value maps ported from solver, board replay, placement state machine, i18n en/ru - in-memory mock transport + seed data; pnpm start runs lobby->active game->board with no backend - board: pointer-drag + tap placement, MakeMove (popup / 1s-hold commit), two-state zoom, blank chooser, exchange, hint, word-check, chat - Playwright smoke (mock) green; svelte-check clean; mock bundle ~37 KB gzip
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
// 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';
|
||||
|
||||
const DB_NAME = 'scrabble';
|
||||
const STORE = 'kv';
|
||||
const LS_PREFIX = 'scrabble.';
|
||||
|
||||
let dbPromise: Promise<IDBDatabase> | null | undefined;
|
||||
|
||||
function openDb(): Promise<IDBDatabase> | null {
|
||||
if (dbPromise !== undefined) return dbPromise;
|
||||
if (typeof indexedDB === 'undefined') {
|
||||
dbPromise = null;
|
||||
return null;
|
||||
}
|
||||
dbPromise = new Promise<IDBDatabase>((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<T>(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<T>(key: string): Promise<T | null> {
|
||||
const db = openDb();
|
||||
if (!db) return lsGet<T>(key);
|
||||
try {
|
||||
const d = await db;
|
||||
return await new Promise<T | null>((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<T>(key);
|
||||
}
|
||||
}
|
||||
|
||||
async function kvSet(key: string, value: unknown): Promise<void> {
|
||||
const db = openDb();
|
||||
if (!db) return lsSet(key, value);
|
||||
try {
|
||||
const d = await db;
|
||||
await new Promise<void>((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<void> {
|
||||
const db = openDb();
|
||||
if (!db) return lsDel(key);
|
||||
try {
|
||||
const d = await db;
|
||||
await new Promise<void>((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<Session | null> {
|
||||
return kvGet<Session>(SESSION_KEY);
|
||||
}
|
||||
|
||||
export function saveSession(s: Session): Promise<void> {
|
||||
return kvSet(SESSION_KEY, s);
|
||||
}
|
||||
|
||||
export function clearSession(): Promise<void> {
|
||||
return kvDel(SESSION_KEY);
|
||||
}
|
||||
|
||||
export interface Prefs {
|
||||
theme: ThemePref;
|
||||
locale: Locale;
|
||||
reduceMotion: boolean;
|
||||
}
|
||||
|
||||
export async function loadPrefs(): Promise<Partial<Prefs>> {
|
||||
return (await kvGet<Prefs>(PREFS_KEY)) ?? {};
|
||||
}
|
||||
|
||||
export function savePrefs(p: Prefs): Promise<void> {
|
||||
return kvSet(PREFS_KEY, p);
|
||||
}
|
||||
Reference in New Issue
Block a user