// Background dictionary preload for offline readiness. An installed PWA with a confirmed email // warms the dictionaries for the player's enabled variants while online, so a later switch to // deliberate offline mode has the data it needs. The pure preloadDicts here takes its side effects // (getDawg, the session miss-breaker, sleep) as dependencies, so it unit-tests in the node env. The // eligibility and once/online guard live in offline.svelte.ts (kickDictPreload); the browser // orchestration that supplies the real side effects and raises the in-lobby warning lives in // preloadrun.ts, which offline.svelte.ts imports dynamically so neither the loader nor the // generator is pulled into the main bundle. import type { Variant } from '../model'; import type { Dawg } from './dawg'; /** PreloadDeps injects preloadDicts's side effects so the logic stays pure and testable. */ export interface PreloadDeps { /** getDawg resolves the (variant, version) reader, serving memory/IndexedDB before the network, * or null on any miss — mirrors the in-game loader. */ getDawg: (variant: Variant, version: string) => Promise; /** disabled reports whether the session dictionary miss-breaker has tripped (too many network * misses this session); when it has, a network fetch will not recover, so retries stop. */ disabled: () => boolean; /** sleep waits between retries; defaults to a real timer. */ sleep?: (ms: number) => Promise; /** retries is the number of extra attempts after the first (default 2). */ retries?: number; /** backoffMs is the base linear backoff between attempts (default 800). */ backoffMs?: number; } /** PreloadResult reports which enabled variants ended up available (ok) and which are still * missing (failed) after the preload — the caller surfaces a warning when failed is non-empty. */ export interface PreloadResult { ok: Variant[]; failed: Variant[]; } /** * preloadDicts fetches, via getDawg, the dictionary for each enabled variant that has a known * version, retrying transient misses with linear backoff. A variant with no known version, or one * still missing after the retry budget, lands in failed; the rest in ok. It never throws and stops * retrying a variant once the session miss-breaker (disabled) trips, since the network will not * recover this session — a cached dictionary is still served by getDawg regardless. */ export async function preloadDicts( versions: Partial>, enabled: readonly Variant[], deps: PreloadDeps, ): Promise { const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms))); const retries = deps.retries ?? 2; const backoffMs = deps.backoffMs ?? 800; const ok: Variant[] = []; const failed: Variant[] = []; for (const variant of enabled) { const version = versions[variant]; if (!version) { failed.push(variant); continue; } let dawg: Dawg | null = null; for (let attempt = 0; attempt <= retries; attempt++) { dawg = await deps.getDawg(variant, version); if (dawg || deps.disabled()) break; if (attempt < retries) await sleep(backoffMs * (attempt + 1)); } (dawg ? ok : failed).push(variant); } return { ok, failed }; }