feat(offline): background-preload dictionaries for offline readiness
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 15s
CI / ui (pull_request) Successful in 1m10s
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m41s
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 15s
CI / ui (pull_request) Successful in 1m10s
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m41s
An installed PWA (standalone web + confirmed email) warms the dictionaries for the player's enabled variants while online — on lobby entry and on a variant-preference change — using the per-variant version the profile now advertises, so a later switch to offline mode already has the data. - dict/preload.ts: pure preloadDicts (retry + linear backoff, honours the session dictionary miss-breaker); node-tested. - dict/preloadrun.ts: lazy browser orchestration (real getDawg), imported dynamically so the loader and move generator stay out of the main bundle. - offline.svelte.ts: kickDictPreload, gated by the pure, tested offlinePreloadEligible, plus the reactive first-lobby preload warning. - Header shows a poor-connection notice in the ad-banner slot on a first-lobby preload failure (offline.preloadWarning, en/ru). - App-entry bundle budget 112->113 for the irreducible main-side wiring (documented in bundle-size.mjs); the heavy parts remain lazy chunks. Docs: ARCHITECTURE offline-mode + dict-preload mechanism.
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
// 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<Dawg | null>;
|
||||
/** 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<void>;
|
||||
/** 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<Record<Variant, string>>,
|
||||
enabled: readonly Variant[],
|
||||
deps: PreloadDeps,
|
||||
): Promise<PreloadResult> {
|
||||
const sleep = deps.sleep ?? ((ms) => new Promise<void>((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 };
|
||||
}
|
||||
Reference in New Issue
Block a user