fix(live): heartbeat the control channel so "live" reflects a reachable server #76

Merged
Claude-Run merged 1 commit from feat/VAPP-66-control-heartbeat into main 2026-07-03 14:28:11 +02:00
Member

The statusbar's connection indicator ("live"/online) was driven solely by the
browser WebSocket ready_state, which stays OPEN when the server dies without
a clean TCP close (half-open connection, network partition, killed process). The
live driver opened the socket and waited, but never used the heartbeat the
protocol already supports, so the indicator could read "live" for an offline
server. This was a half-implemented feature.

vervain-server's control.ashx already answers an application-level
{"action":"ping"} with {"action":"pong"} (ControlIn::Ping ->
ControlOut::Pong); the browser driver just never spoke it. Add a pure,
tick-driven Heartbeat state machine that sends a ping roughly every 30 s and,
if no frame answers within a ~10 s pong window, reports the socket dead. The wasm
driver records every inbound frame as activity, advances the heartbeat each poll
tick, and on Dead tears the socket down so the reconnect loop actively
re-verifies reachability. "live" is reasserted only after a fresh socket reopens,
which is itself a reachability check. Any inbound frame (the pong or any server
push) counts as proof of life, so a busy connection is never torn down.

The heartbeat logic is a pure state machine gated wasm OR test (like the frame
decoder) and unit-tested on the server build; a compile-time assertion enforces
that the pong timeout stays below the ping interval so the staleness window
always closes.

#VAPP-66

The statusbar's connection indicator ("live"/online) was driven solely by the browser WebSocket `ready_state`, which stays `OPEN` when the server dies without a clean TCP close (half-open connection, network partition, killed process). The live driver opened the socket and waited, but never used the heartbeat the protocol already supports, so the indicator could read "live" for an offline server. This was a half-implemented feature. vervain-server's control.ashx already answers an application-level `{"action":"ping"}` with `{"action":"pong"}` (`ControlIn::Ping` -> `ControlOut::Pong`); the browser driver just never spoke it. Add a pure, tick-driven `Heartbeat` state machine that sends a `ping` roughly every 30 s and, if no frame answers within a ~10 s pong window, reports the socket dead. The wasm driver records every inbound frame as activity, advances the heartbeat each poll tick, and on `Dead` tears the socket down so the reconnect loop actively re-verifies reachability. "live" is reasserted only after a fresh socket reopens, which is itself a reachability check. Any inbound frame (the pong or any server push) counts as proof of life, so a busy connection is never torn down. The heartbeat logic is a pure state machine gated `wasm OR test` (like the frame decoder) and unit-tested on the server build; a compile-time assertion enforces that the pong timeout stays below the ping interval so the staleness window always closes. #VAPP-66
fix(live): heartbeat the control channel so "live" reflects a reachable server
All checks were successful
Check / fmt + clippy + build + tests (pull_request) Successful in 1m5s
Create release / Create release from merged PR (pull_request) Has been skipped
d540ed88ea
The statusbar's connection indicator ("live"/online) was driven solely by the
browser WebSocket `ready_state`, which stays `OPEN` when the server dies without
a clean TCP close (half-open connection, network partition, killed process). The
live driver opened the socket and waited, but never used the heartbeat the
protocol already supports, so the indicator could read "live" for an offline
server. This was a half-implemented feature.

vervain-server's control.ashx already answers an application-level
`{"action":"ping"}` with `{"action":"pong"}` (`ControlIn::Ping` ->
`ControlOut::Pong`); the browser driver just never spoke it. Add a pure,
tick-driven `Heartbeat` state machine that sends a `ping` roughly every 30 s and,
if no frame answers within a ~10 s pong window, reports the socket dead. The wasm
driver records every inbound frame as activity, advances the heartbeat each poll
tick, and on `Dead` tears the socket down so the reconnect loop actively
re-verifies reachability. "live" is reasserted only after a fresh socket reopens,
which is itself a reachability check. Any inbound frame (the pong or any server
push) counts as proof of life, so a busy connection is never torn down.

The heartbeat logic is a pure state machine gated `wasm OR test` (like the frame
decoder) and unit-tested on the server build; a compile-time assertion enforces
that the pong timeout stays below the ping interval so the staleness window
always closes.

#VAPP-66
Claude-Run deleted branch feat/VAPP-66-control-heartbeat 2026-07-03 14:28:11 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/vervain-apps!76
No description provided.