e0d28733ff
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 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m41s
React to the network changing while the app is open (e.g. the player toggling flight mode), via passive online/offline events - no polling, no battery cost: - interface lost -> enter offline mode for the session (auto); - interface back -> if the offline was auto, verify the gateway is really reachable (an interface being up does not guarantee it) and return online; a deliberate offline (the toggle or the cold-start dialog) is left as-is. - offline.svelte: track `auto` (auto-detected vs the player's deliberate choice). - connection.svelte: checkReachable is now a pure one-shot (the caller decides); the reachability watcher never probes in offline mode (events drive recovery). - transport.ts: the reachability probe is exempt from the kill switch - it IS the mechanism that decides whether to return online, fired only deliberately. - app.svelte.ts: initNetworkReactivity wires the events (web-only, skipped in the mock); called from bootstrap. Online unaffected (skipped in the mock e2e): e2e 196. Mid-session reactivity is contour-verified.
81 lines
2.9 KiB
TypeScript
81 lines
2.9 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';
|
|
import { offlineMode } from './offline.svelte';
|
|
|
|
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)),
|
|
]);
|
|
return true;
|
|
} catch {
|
|
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;
|
|
// Never probe the network in offline mode (the kill switch); the online/offline events drive
|
|
// recovery there instead.
|
|
if (online || !probe || offlineMode.active) return;
|
|
probe().then(reportOnline, () => scheduleProbe(Math.min(attempt + 1, 6)));
|
|
},
|
|
backoffMs(attempt),
|
|
);
|
|
}
|