feat(offline): gate the offline toggle on dictionary readiness
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Successful in 1m10s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 1s
CI / deploy (pull_request) Successful in 1m44s
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Successful in 1m10s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 1s
CI / deploy (pull_request) Successful in 1m44s
Flipping the Settings toggle to offline now checks that every enabled variant's dictionary is on the device before entering offline mode: it fetches missing ones cache-first and waits up to ~5 s (raceOfflineReady + the lazy dict/offlineready), greying the toggle meanwhile. If they cannot be readied in time it stays online and shows a 'needs internet' note, while the fetch keeps warming the cache in the background so a later flip is instant. Leaving offline is never gated. Prevents entering a half-baked offline mode (no dawg -> cannot create/play a local game) when the background preload has not finished (poor connection, or an immediate flip right after install). - offline.ts: raceOfflineReady (pure, injected sleep; unit-tested red->green) - dict/offlineready.ts: ensureOfflineDicts (cache-first preloadDicts, lazy chunk) - offline.svelte.ts: requestOffline + TOGGLE_READY_BUDGET_MS - Settings.svelte: checking/needsData state, disabled toggle, inline note - i18n: settings.offlineChecking / settings.offlineNeedsData (en+ru) - docs: FUNCTIONAL(+_ru) offline story + ARCHITECTURE offline paragraph
This commit is contained in:
@@ -42,6 +42,23 @@ export function missingDicts(enabled: readonly Variant[], hasDict: (v: Variant)
|
||||
return enabled.filter((v) => !hasDict(v));
|
||||
}
|
||||
|
||||
/**
|
||||
* raceOfflineReady runs the dictionary fetch `run` against a `budgetMs` wait and reports whether
|
||||
* offline mode can be entered now: ready only when the fetch resolves with nothing still failed
|
||||
* before the budget elapses. On a timeout the caller stops waiting but does NOT abort `run` — it
|
||||
* keeps warming the on-device cache so a later flip to offline is instant. The sleep is injected so
|
||||
* the logic stays pure and unit-tests in the node env.
|
||||
*/
|
||||
export async function raceOfflineReady(
|
||||
run: Promise<{ failed: readonly unknown[] }>,
|
||||
budgetMs: number,
|
||||
sleep: (ms: number) => Promise<void> = (ms) => new Promise((r) => setTimeout(r, ms)),
|
||||
): Promise<boolean> {
|
||||
const elapsed = sleep(budgetMs).then(() => null);
|
||||
const res = await Promise.race([run, elapsed]);
|
||||
return res !== null && res.failed.length === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* offlinePreloadEligible reports whether a background dictionary preload should run in this
|
||||
* context: an installed standalone web PWA (not a Telegram/VK mini-app, not a plain browser tab)
|
||||
|
||||
Reference in New Issue
Block a user