From 2f867b8e6c02e3ffae1b6192e4f4058ced50f9de Mon Sep 17 00:00:00 2001 From: Ilia Denisov Date: Mon, 6 Jul 2026 10:39:55 +0200 Subject: [PATCH] feat(offline): background-preload dictionaries for offline readiness MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- docs/ARCHITECTURE.md | 11 ++++- ui/scripts/bundle-size.mjs | 10 ++-- ui/src/components/Header.svelte | 20 +++++++- ui/src/lib/dict/preload.test.ts | 86 +++++++++++++++++++++++++++++++++ ui/src/lib/dict/preload.ts | 68 ++++++++++++++++++++++++++ ui/src/lib/dict/preloadrun.ts | 24 +++++++++ ui/src/lib/i18n/en.ts | 1 + ui/src/lib/i18n/ru.ts | 1 + ui/src/lib/offline.svelte.ts | 58 +++++++++++++++++++++- ui/src/lib/offline.test.ts | 12 ++++- ui/src/lib/offline.ts | 17 +++++++ ui/src/screens/Lobby.svelte | 9 +++- ui/src/screens/Profile.svelte | 4 ++ 13 files changed, 311 insertions(+), 10 deletions(-) create mode 100644 ui/src/lib/dict/preload.test.ts create mode 100644 ui/src/lib/dict/preload.ts create mode 100644 ui/src/lib/dict/preloadrun.ts diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index f1e113f..f46ce2d 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -1243,7 +1243,16 @@ browser-language detection — crawlers render with arbitrary languages). The fa mock build. The worker intercepts only top-level navigations (network-first with a cached-shell fallback), leaving `/assets/*` and the Connect stream untouched; it exists to satisfy Chromium's installability requirement (a registered SW, needed for install on Android) and is the single -growth point for a future opt-in offline mode. The gateway registers the `.webmanifest` MIME type +growth point for the opt-in **offline mode** (in progress): a deliberate, device-scoped Settings +toggle — distinct from the transient gateway-reachability signal — that tints the header blue with +an *Offline* chip and confines play to on-device `vs_ai` games. To have data ready before the +switch, the **Profile advertises the current dictionary version per variant** (`dict_versions`, +filled from the registry on the existing cold-start profile request — no extra round-trip), and an +eligible installed PWA (standalone web + confirmed email) **background-preloads** those dictionaries +— on lobby entry and on a variant-preference change — through the same three-tier loader, retried +with backoff and honouring the session miss-breaker; the move generator, the loader and the preload +orchestration stay in lazy chunks. A first-lobby preload failure shows a *poor-connection* notice in +the ad-banner slot. The gateway registers the `.webmanifest` MIME type in-process (the distroless image has no `/etc/mime.types`). Hash-named `/assets/*` are served `immutable` (a relaunch is a cache hit, not a re-download); the HTML shells are `no-cache` so a new deploy is picked up — both containers apply the same caching. An diff --git a/ui/scripts/bundle-size.mjs b/ui/scripts/bundle-size.mjs index 0403740..2cf27c9 100644 --- a/ui/scripts/bundle-size.mjs +++ b/ui/scripts/bundle-size.mjs @@ -20,10 +20,12 @@ import { join } from 'node:path'; const DIST = 'dist'; // Per-chunk gzip budgets in KB. The app entry was raised to 110 for the local move-preview -// wiring, then to 112 for the PWA install feature (the install CTA + the pwa detection / -// service-worker registration live in the app entry; the heavy dict subsystem stays in lazy -// chunks). Its scoped CSS lands in the CSS chunk, not this JS budget. -const BUDGET = { app: 112, shared: 30, landing: 5 }; +// wiring, to 112 for the PWA install feature, then to 113 for the offline-mode wiring: the +// dictionary-preload trigger and the offline-state reactives live in the app entry (the Header +// reads them, the lobby/profile screens fire the trigger), while the heavy parts — the dict +// loader, the move generator and the preload orchestration — stay in lazy chunks. Scoped CSS +// lands in the CSS chunk, not this JS budget. +const BUDGET = { app: 113, shared: 30, landing: 5 }; // gzipped returns the gzipped byte size of a built asset, or 0 when the reference is not a // local file (e.g. the Telegram SDK loaded from a CDN) or is missing. diff --git a/ui/src/components/Header.svelte b/ui/src/components/Header.svelte index 6e14256..3e385f2 100644 --- a/ui/src/components/Header.svelte +++ b/ui/src/components/Header.svelte @@ -1,7 +1,7 @@