25381f70a3
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 1m14s
CI / conformance (pull_request) Successful in 12s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m59s
An idle live stream whose connection dies without an error (airplane mode cutting the radio) left the net-state machine stuck "online": the streaming fetch neither errored nor delivered, no unary call was made, and the OS `navigator` offline hint is unreliable in the Telegram/VK WebViews — so the "Connecting…"/offline state only surfaced on the next foreground resync, not while idle. The gateway already pushes a keep-alive `heartbeat` every 10s, so add a client-side watchdog (streamwatchdog.ts) that resets on every delivered event and, after ~25s of silence, treats the stream as dropped (reportOffline + reconnect). It is background-aware (paused while suspended) so a throttled foreground return does not false-trip. The mock client mirrors the heartbeat so an idle e2e session stays online.
64 lines
2.3 KiB
TypeScript
64 lines
2.3 KiB
TypeScript
// A liveness watchdog for the live event stream. The gateway pushes a `heartbeat` event on a
|
|
// fixed cadence (GATEWAY_PUSH_HEARTBEAT_INTERVAL, default 10 s), so a healthy stream delivers at
|
|
// least one event well inside the timeout even when the game itself is idle. A network that dies
|
|
// silently — e.g. airplane mode cutting the radio — leaves the streaming fetch neither erroring
|
|
// nor delivering; without this watchdog the machine would stay "online" until the next active
|
|
// call fails (often only on a foreground resync). The watchdog resets on every delivered event
|
|
// and fires onDead once the stream has been silent for the whole timeout, so the app can report
|
|
// the drop and reconnect.
|
|
|
|
/** StreamWatchdog is the control surface returned by {@link createStreamWatchdog}. */
|
|
export interface StreamWatchdog {
|
|
/** reset (re)starts the countdown; call on stream open and on every delivered event. It also
|
|
* clears a prior pause, so a reopen after a suspend resumes watching. */
|
|
reset(): void;
|
|
/** pause stops the countdown while the app is backgrounded (timers are throttled while hidden,
|
|
* and a suspended stream legitimately delivers nothing — neither should trip a false drop). */
|
|
pause(): void;
|
|
/** resume restarts the countdown after a pause (on returning to the foreground). */
|
|
resume(): void;
|
|
/** clear stops the countdown without re-arming; call when the stream is closed or has errored. */
|
|
clear(): void;
|
|
}
|
|
|
|
/**
|
|
* createStreamWatchdog builds a watchdog that calls onDead once no reset has happened for
|
|
* timeoutMs. timeoutMs must exceed the server heartbeat interval plus proxy/jitter slack, or a
|
|
* healthy but idle stream would trip it.
|
|
*/
|
|
export function createStreamWatchdog(opts: { timeoutMs: number; onDead: () => void }): StreamWatchdog {
|
|
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
let paused = false;
|
|
|
|
function stop(): void {
|
|
if (timer !== null) {
|
|
clearTimeout(timer);
|
|
timer = null;
|
|
}
|
|
}
|
|
function arm(): void {
|
|
stop();
|
|
timer = setTimeout(opts.onDead, opts.timeoutMs);
|
|
}
|
|
|
|
return {
|
|
reset(): void {
|
|
paused = false;
|
|
arm();
|
|
},
|
|
pause(): void {
|
|
paused = true;
|
|
stop();
|
|
},
|
|
resume(): void {
|
|
if (paused) {
|
|
paused = false;
|
|
arm();
|
|
}
|
|
},
|
|
clear(): void {
|
|
stop();
|
|
},
|
|
};
|
|
}
|