fix(net): detect a silently-dead live stream via a heartbeat watchdog #259

Merged
developer merged 1 commits from feature/stream-heartbeat-watchdog into development 2026-07-14 06:36:21 +00:00
Owner

Проблема

В простое (в игре/лобби, ничего не трогая) при включении авиарежима спиннер «Connecting…»/офлайн не появляется — в TG/VK можно ждать минутами. Появляется мгновенно только после сворачивания-разворачивания.

Корень: живой стрим (Connect server-stream поверх HTTP/2, не WebSocket) при пропаже радио умирает молча — клиенту не приходит ни FIN, ни RST, поэтому for await не ошибается. Хартбит от gateway (каждые 10с) есть, но клиент по нему ставил только «онлайн», без тайм-аута на его отсутствие. Зонд в online не вооружён, unary-вызовов в простое нет, а navigator offline в WebView TG/VK ненадёжен. Сворачивание чинит, потому что goForeground() делает unary-вызовы, которые падают сразу.

Что сделано (только клиент, без правок протокола/сервера)

  • Новый ui/src/lib/streamwatchdog.ts: сбрасывается на каждом доставленном событии (хартбит раз в 10с держит его), при тишине ~25с считает стрим мёртвым → reportOffline + reconnect. Фон-осведомлён (пауза на goBackground, возобновление на goForeground), так что троттлинг таймеров на возврате не даёт ложного офлайна.
  • Проводка в app.svelte.ts (open/onEvent/onError/close + background/foreground).
  • Mock-клиент теперь шлёт heartbeat (зеркалит прод), чтобы idle-сессия e2e не ловила ложный офлайн.
  • Заметка в docs/ARCHITECTURE.md.

Порог 25с завязан на GATEWAY_PUSH_HEARTBEAT_INTERVAL (10с) + запас; при подъёме интервала близко к порогу — риск ложных срабатываний (задокументировано).

Проверка

  • Unit: 7 тестов на watchdog (fake-timers) + весь пакет 629 зелёных; svelte-check 0/0; build.
  • E2e: полный прогон 248 passed (chromium+webkit), включая drop/restore и foreground-resync.
  • On-device в TG/VK — за владельцем (потерю радио в WebView в Playwright не воспроизвести): простой → авиарежим → спиннер за ~25-35с.

Это PR #1 из двух; следом — офлайн-UX внутри онлайн-игры (баннер/заморозка/чат-словарь), которому этот детект — предпосылка.

## Проблема В простое (в игре/лобби, ничего не трогая) при включении авиарежима спиннер «Connecting…»/офлайн **не появляется** — в TG/VK можно ждать минутами. Появляется мгновенно только после сворачивания-разворачивания. **Корень:** живой стрим (Connect server-stream поверх HTTP/2, не WebSocket) при пропаже радио умирает **молча** — клиенту не приходит ни FIN, ни RST, поэтому `for await` не ошибается. Хартбит от gateway (каждые 10с) есть, но клиент по нему ставил только «онлайн», без тайм-аута на его отсутствие. Зонд в `online` не вооружён, unary-вызовов в простое нет, а `navigator` offline в WebView TG/VK ненадёжен. Сворачивание чинит, потому что `goForeground()` делает unary-вызовы, которые падают сразу. ## Что сделано (только клиент, без правок протокола/сервера) - Новый `ui/src/lib/streamwatchdog.ts`: сбрасывается на каждом доставленном событии (хартбит раз в 10с держит его), при тишине ~25с считает стрим мёртвым → `reportOffline` + reconnect. Фон-осведомлён (пауза на `goBackground`, возобновление на `goForeground`), так что троттлинг таймеров на возврате не даёт ложного офлайна. - Проводка в `app.svelte.ts` (open/onEvent/onError/close + background/foreground). - Mock-клиент теперь шлёт heartbeat (зеркалит прод), чтобы idle-сессия e2e не ловила ложный офлайн. - Заметка в `docs/ARCHITECTURE.md`. **Порог 25с** завязан на `GATEWAY_PUSH_HEARTBEAT_INTERVAL` (10с) + запас; при подъёме интервала близко к порогу — риск ложных срабатываний (задокументировано). ## Проверка - Unit: 7 тестов на watchdog (fake-timers) + весь пакет 629 зелёных; svelte-check 0/0; build. - E2e: полный прогон **248 passed** (chromium+webkit), включая drop/restore и foreground-resync. - On-device в TG/VK — за владельцем (потерю радио в WebView в Playwright не воспроизвести): простой → авиарежим → спиннер за ~25-35с. Это PR #1 из двух; следом — офлайн-UX внутри онлайн-игры (баннер/заморозка/чат-словарь), которому этот детект — предпосылка.
developer added 1 commit 2026-07-14 06:28:17 +00:00
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
25381f70a3
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.
owner approved these changes 2026-07-14 06:35:43 +00:00
developer merged commit c34e2a8dbf into development 2026-07-14 06:36:21 +00:00
developer deleted branch feature/stream-heartbeat-watchdog 2026-07-14 06:36:21 +00:00
Sign in to join this conversation.
No Reviewers
No Label
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: developer/scrabble-game#259