// Client-side accumulator + beacon for local move-preview adoption telemetry. It counts a // few events — the app cold start, dictionary loads by result, and move previews by path — // and posts the running deltas to the edge periodically and when the app is backgrounded, // so the backend can chart adoption (docs/ARCHITECTURE.md §11). Best-effort throughout: a // failed beacon keeps its counts to retry, and nothing here throws into the caller. It lives // outside the lazy dict subsystem so the cold start is recorded even when no game is opened. import { gateway } from './gateway'; interface Counts { cold_start: number; dict_fetched: number; dict_cache_hit: number; dict_miss: number; preview_local: number; preview_network: number; } function zero(): Counts { return { cold_start: 0, dict_fetched: 0, dict_cache_hit: 0, dict_miss: 0, preview_local: 0, preview_network: 0 }; } let counts = zero(); let started = false; function total(c: Counts): number { return c.cold_start + c.dict_fetched + c.dict_cache_hit + c.dict_miss + c.preview_local + c.preview_network; } // flush posts the accumulated deltas and resets them; on failure it folds the batch back in // so the next flush retries. Best-effort. async function flush(): Promise { if (total(counts) === 0) return; const batch = counts; counts = zero(); try { await gateway.reportLocalEval({ ...batch }); } catch { counts.cold_start += batch.cold_start; counts.dict_fetched += batch.dict_fetched; counts.dict_cache_hit += batch.dict_cache_hit; counts.dict_miss += batch.dict_miss; counts.preview_local += batch.preview_local; counts.preview_network += batch.preview_network; } } /** * startLocalEvalMetrics records the app cold start and begins the periodic beacon. Called once * at launch (a repeat call is a no-op). It also flushes when the app is backgrounded, so a * batch is not lost when the tab is closed. */ export function startLocalEvalMetrics(): void { if (started) return; started = true; counts.cold_start += 1; setInterval(() => void flush(), 60_000); if (typeof document !== 'undefined') { document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'hidden') void flush(); }); } } /** noteDictFetched: a dictionary was downloaded and cached (an IndexedDB fill). */ export function noteDictFetched(): void { counts.dict_fetched += 1; } /** noteDictCacheHit: a dictionary loaded from the IndexedDB cache (no download). */ export function noteDictCacheHit(): void { counts.dict_cache_hit += 1; } /** noteDictMiss: a warm-up did not produce a usable dictionary (failed or timed out). */ export function noteDictMiss(): void { counts.dict_miss += 1; } /** notePreviewLocal: a move preview was computed on-device. */ export function notePreviewLocal(): void { counts.preview_local += 1; } /** notePreviewNetwork: a move preview fell back to the network. */ export function notePreviewNetwork(): void { counts.preview_network += 1; }