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:
@@ -3,7 +3,11 @@
|
||||
// Settings toggle is the source of truth), distinct from connection.svelte.ts's transient
|
||||
// gateway-reachability signal. The pure persistence + readiness logic lives in offline.ts.
|
||||
|
||||
import { loadOfflinePref, saveOfflinePref } from './offline';
|
||||
import { loadOfflinePref, saveOfflinePref, offlinePreloadEligible } from './offline';
|
||||
import { isStandalone } from './pwa';
|
||||
import { insideTelegram } from './telegram';
|
||||
import { insideVK } from './vk';
|
||||
import type { Profile } from './model';
|
||||
|
||||
// Not named `state` (a svelte-check hazard: `$state` would then read as a store subscription).
|
||||
let active = $state(loadOfflinePref());
|
||||
@@ -21,3 +25,55 @@ export function setOfflineMode(on: boolean): void {
|
||||
active = on;
|
||||
saveOfflinePref(on);
|
||||
}
|
||||
|
||||
// The dict-preload warning: true when a first-lobby background preload could not fetch every
|
||||
// enabled variant's dictionary (typically a poor connection), so offline mode may be incomplete.
|
||||
// The lobby shows a notice in place of the ad banner while it holds.
|
||||
let preloadWarn = $state(false);
|
||||
|
||||
/** dictPreloadWarning exposes the reactive preload-failure flag; read it in markup / $derived. */
|
||||
export const dictPreloadWarning = {
|
||||
/** active is true while a first-lobby dictionary preload has left a variant unavailable. */
|
||||
get active(): boolean {
|
||||
return preloadWarn;
|
||||
},
|
||||
};
|
||||
|
||||
/** setDictPreloadWarning raises or clears the in-lobby preload-failure notice. */
|
||||
export function setDictPreloadWarning(on: boolean): void {
|
||||
preloadWarn = on;
|
||||
}
|
||||
|
||||
// A preload runs at most once at a time; it finishes before a fresh trigger (a repeated lobby
|
||||
// mount or a variant-preference change) can start another. Module-scoped so mounts do not stack.
|
||||
let preloadInFlight = false;
|
||||
|
||||
/**
|
||||
* kickDictPreload starts a background preload of the enabled variants' dictionaries for an
|
||||
* offline-capable install (a standalone web PWA with a confirmed email) while online, so a later
|
||||
* switch to offline mode already has the data. It is a no-op in a Telegram/VK mini-app, in a plain
|
||||
* browser tab, without a confirmed email, while offline, or when a preload is already running;
|
||||
* getDawg's caching makes a repeat run cheap. When warnOnFail is set (the first lobby entry), a
|
||||
* fetch failure raises the in-lobby notice, and a later successful run clears it. The dict loader
|
||||
* and generator are imported dynamically, so neither is pulled into the main bundle.
|
||||
*/
|
||||
export function kickDictPreload(prof: Profile | null, warnOnFail = false): void {
|
||||
if (preloadInFlight || !prof) return;
|
||||
const eligible = offlinePreloadEligible({
|
||||
hasEmail: !!prof.email,
|
||||
standalone: isStandalone(),
|
||||
inTelegram: insideTelegram(),
|
||||
inVK: insideVK(),
|
||||
online: typeof navigator === 'undefined' || navigator.onLine !== false,
|
||||
});
|
||||
if (!eligible) return;
|
||||
preloadInFlight = true;
|
||||
void import('./dict/preloadrun')
|
||||
.then((m) => m.runPreload(prof, warnOnFail))
|
||||
.catch(() => {
|
||||
/* best-effort warmup — a failed dynamic import just leaves offline data unprimed */
|
||||
})
|
||||
.finally(() => {
|
||||
preloadInFlight = false;
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user