Files
scrabble-game/ui/src/lib/connection.svelte.ts
T
Ilia Denisov 5bc2ad3b6d
CI / changes (pull_request) Successful in 1s
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 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m44s
feat(offline): auto-detect no network at cold start + 'go offline?' dialog
A sticky-online cold start with no network hung the splash on adoptSession's
retrying profile fetch. Now, for an offline-capable web install with a cached
profile:
- No network interface (navigator.onLine === false) -> enter offline mode for
  the session (no dialog; the next launch re-evaluates).
- Interface up but the gateway is unreachable within 3s (a single-attempt
  reachability probe, not the 6-retry loop) -> a 'No connection. Enable offline
  mode?' dialog: Enable -> sticky offline; Keep trying -> the normal online
  adopt (retries, 'Connecting...').

- connection.svelte: checkReachable(timeout) - a bounded single probe.
- offline.svelte: setOfflineMode(on, persist) - auto-offline is session-only, a
  deliberate choice (dialog/toggle) is sticky.
- app.svelte.ts: the cold-start auto-detect in bootstrap + the dialog resolver;
  App.svelte renders the boot dialog. i18n en/ru.
- App-entry bundle budget 113->114 (the boot path cannot be lazy-loaded).

Online cold-start unaffected (auto-detect gated to isStandalone, off in the mock
e2e): e2e 196. The offline paths are contour-verified.

Next: PR2 - mid-session flight-mode reactivity (online/offline events).
2026-07-06 17:49:53 +02:00

80 lines
2.7 KiB
TypeScript

// Global connectivity signal. `online` is false while the app is actively failing to
// reach the gateway — a unary call retrying after a transport/rate-limit failure, or the live
// stream dropped. The transport and the live-stream owner report transitions; the UI reads
// `connection.online` to show the "Connecting…" indicator and to softly disable proactive
// actions. In mock mode nothing ever reports trouble, so it simply stays online.
//
// Recovery is guaranteed by a reachability watcher: while offline it periodically fires a
// registered probe (a lightweight read) until one succeeds, so the indicator clears even when no
// other traffic is in flight.
import { backoffMs } from './retry';
let online = $state(true);
let watchTimer: ReturnType<typeof setTimeout> | null = null;
let probe: (() => Promise<void>) | null = null;
export const connection = {
/** online is true when the app believes it can reach the gateway. */
get online(): boolean {
return online;
},
};
/** registerProbe installs the reachability probe the watcher fires while offline. The transport
* wires a cheap authenticated read; it should reject when there is no session. */
export function registerProbe(fn: () => Promise<void>): void {
probe = fn;
}
/**
* checkReachable runs the reachability probe once, bounded by timeoutMs, and reports whether the
* gateway answered — a single attempt (no retry loop), for the cold-start network decision — while
* updating the online signal. It reports false when no probe is registered or the timeout wins.
*/
export async function checkReachable(timeoutMs: number): Promise<boolean> {
if (!probe) return false;
try {
await Promise.race([
probe(),
new Promise<never>((_, reject) => setTimeout(() => reject(new Error('reachability timeout')), timeoutMs)),
]);
reportOnline();
return true;
} catch {
reportOffline();
return false;
}
}
/** reportOnline marks the gateway reachable and stops the watcher. */
export function reportOnline(): void {
online = true;
if (watchTimer) {
clearTimeout(watchTimer);
watchTimer = null;
}
}
/** reportOffline marks the gateway unreachable and starts the reachability watcher (once). */
export function reportOffline(): void {
online = false;
if (!watchTimer && probe) scheduleProbe(1);
}
/** resetConnection restores the online state and stops the watcher (e.g. on logout). */
export function resetConnection(): void {
reportOnline();
}
function scheduleProbe(attempt: number): void {
watchTimer = setTimeout(
() => {
watchTimer = null;
if (online || !probe) return;
probe().then(reportOnline, () => scheduleProbe(Math.min(attempt + 1, 6)));
},
backoffMs(attempt),
);
}