// The deliberate offline MODE: a sticky, device-scoped reactive flag the app reads to gate the // network, tint the chrome blue and show only local games. It is the player's own choice (the // 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, 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()); // True while the current offline state was entered automatically (no network detected), not chosen // by the player. An auto offline self-heals to online when the network returns; a deliberate one is // left as the player's choice. let auto = $state(false); /** offlineMode exposes the reactive offline flags; read them in markup / $derived. */ export const offlineMode = { /** active is true while the app is in offline mode (deliberate or auto-detected). */ get active(): boolean { return active; }, /** auto is true while offline was entered automatically (no network), not by the player. */ get auto(): boolean { return auto; }, }; /** * setOfflineMode enters or leaves offline mode. By default it persists the choice (device-scoped) — * a deliberate choice (the Settings toggle, or the cold-start "no connection" dialog). Pass * persist=false for a transient, auto-detected offline (a cold start with no network interface): the * flag holds for the session but is not saved, so the next launch re-evaluates the network. */ export function setOfflineMode(on: boolean, persist = true): void { active = on; auto = on && !persist; // a non-persisted offline is auto-detected; a persisted one is deliberate if (persist) saveOfflinePref(on); } /** The toggle-flip readiness wait: entering offline waits at most this long for the enabled * variants' dictionaries before reverting to online (the fetch then continues in the background). */ export const TOGGLE_READY_BUDGET_MS = 5000; /** * requestOffline attempts to enter offline mode from the Settings toggle. It fetches the enabled * variants' dictionaries cache-first and, if every one is available within the readiness budget, * switches to a deliberate (persisted) offline mode and returns true. Otherwise it stays online and * returns false — the caller shows the "needs internet" note — while the fetch keeps warming the * cache so a later flip is instant. The readiness glue and the dict loader are imported dynamically, * so neither is pulled into the main bundle. Returns false with no profile. */ export async function requestOffline(prof: Profile | null, budgetMs = TOGGLE_READY_BUDGET_MS): Promise { if (!prof) return false; const m = await import('./dict/offlineready'); const ready = await m.ensureOfflineDicts(prof, budgetMs); if (ready) setOfflineMode(true); return ready; } // 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; }); }