fix(net): detect a silently-dead live stream via a heartbeat watchdog
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
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.
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
// 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();
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user