Skip to content

Core: Reset WebsocketTransport heartbeat on every message, not just ping#35422

Merged
Sidnioulz merged 4 commits into
nextfrom
valentin/websocket-heartbeat-reset-on-any-message
Jul 9, 2026
Merged

Core: Reset WebsocketTransport heartbeat on every message, not just ping#35422
Sidnioulz merged 4 commits into
nextfrom
valentin/websocket-heartbeat-reset-on-any-message

Conversation

@valentinpalkovic

@valentinpalkovic valentinpalkovic commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Manual testing

N?A

What I did

Related to #35400, which fixes a "Server timed out" manager disconnect during large @storybook/addon-vitest runs. That PR's core-channel fix reorders WebsocketTransport.onmessage to check for ping and reset the heartbeat before calling the (potentially slow, when the channel is busy) channel handler, instead of after.

This PR implements that same underlying fix directly against next (code/core/src/channels/websocket/index.ts, which doesn't yet have #35400's change), but generalizes it one step further: the heartbeat now resets on every incoming message, not only on ping.

Why: any message arriving over the socket is itself proof the connection is alive - the server is actively sending data and it's reaching this tab. Gating the reset on the literal ping type leaves a gap: if the message backlog in front of the next ping is deep enough (many status-update events, each requiring real synchronous work to handle), the ping can still be delayed past the timeout window even while the connection is constantly, verifiably receiving traffic. Resetting on every message closes that gap directly, since forward progress through the backlog - of any kind - is what actually matters, not whether a ping happens to be interspersed in it.

this.socket.onmessage = ({ data }) => {
  const event = ...;
  invariant(this.handler, 'WebsocketTransport handler should be set');

  this.heartbeat(); // reset on any message, before the handler runs

  if (event.type === 'ping') {
    this.send({ type: 'pong' });
    return; // pings have no channel listeners
  }

  this.handler(event);
};

Notes for reviewers

Testing

yarn nx run core:check passes with no type errors. Formatted with oxfmt; lint has no new warnings beyond what already existed on this file.

Summary by CodeRabbit

Summary by CodeRabbit

  • Bug Fixes
    • Improved WebSocket heartbeat monitoring so every incoming message reliably resets the timeout.
    • Internal ping messages are now auto-answered with pong and are no longer delivered to the message handler.
    • ping no longer triggers handler errors; non-ping messages still forward normally (including error propagation).
    • When heartbeat times out, the connection closes with a timeout error.
  • Tests
    • Added coverage for heartbeat timeouts, ping/pong behavior, forwarding rules, and handler error handling.

Fixes the "Server timed out" manager disconnect on large addon-vitest
runs (see #35400) at its root, in the shared channel transport rather
than only reordering ping handling: any incoming message proves the
connection is alive, so the heartbeat now resets before the (possibly
slow) channel handler runs for every message, not only for a literal
ping. This also closes a gap the narrower ping-only fix leaves open -
if a backlog is deep enough that the next ping itself is stuck behind
many other queued messages, resetting only on ping doesn't help, since
nothing about processing those other messages pushes the deadline out.
@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ec303555-1818-4496-90e7-8c2004178292

📥 Commits

Reviewing files that changed from the base of the PR and between d0969af and 80483a7.

📒 Files selected for processing (1)
  • code/core/src/channels/websocket/index.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • code/core/src/channels/websocket/index.test.ts

📝 Walkthrough

Walkthrough

WebsocketTransport now resets its heartbeat on every incoming message and sends pong for ping messages without calling the channel handler. A new test suite covers timeout closing, ping/pong behavior, message forwarding, and handler error cases.

Changes

WebSocket heartbeat and ping handling

Layer / File(s) Summary
Onmessage heartbeat reset and ping short-circuit with tests
code/core/src/channels/websocket/index.ts, code/core/src/channels/websocket/index.test.ts
Heartbeat reset now runs for every message before event-type checks; ping messages send a pong and return early without invoking the channel handler; non-ping messages invoke the handler as before. New tests cover timeout closing, ping/pong exchange, handler forwarding rules, and heartbeat reset persisting even when the handler throws.

Estimated code review effort: 3 (Moderate) | ~20 minutes

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@code/core/src/channels/websocket/index.test.ts`:
- Line 50: The WebSocket test is stubbing a global with vi.stubGlobal but never
restoring it, which can leak MockWebSocket into other tests. Update the
websocket test setup in index.test.ts so the stubbed WebSocket global is cleaned
up by adding vi.unstubAllGlobals() in the existing afterEach alongside
vi.useRealTimers() and vi.clearAllMocks(), and keep the stub usage centralized
around MockWebSocket so the suite leaves no ambient globals behind.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 75bc0610-feeb-49a0-8bb1-523bffa36a63

📥 Commits

Reviewing files that changed from the base of the PR and between cbf8e4f and 5775319.

📒 Files selected for processing (2)
  • code/core/src/channels/websocket/index.test.ts
  • code/core/src/channels/websocket/index.ts

Comment thread code/core/src/channels/websocket/index.test.ts Outdated
Comment thread code/core/src/channels/websocket/index.ts Outdated
@yannbf yannbf added bug core ci:normal Run our default set of CI jobs (choose this for most PRs). qa:needed Pull Requests that will need manual QA prior to release. labels Jul 9, 2026
@storybook-app-bot

storybook-app-bot Bot commented Jul 9, 2026

Copy link
Copy Markdown

Package Benchmarks

Commit: 80483a7, ran on 9 July 2026 at 10:56:27 UTC

No significant changes detected, all good. 👏

@ndelangen

Copy link
Copy Markdown
Member

@valentinpalkovic, this replaces my PR, right?
#35400

Shall I close it?

@valentinpalkovic

Copy link
Copy Markdown
Contributor Author

Yes, though it needs some testing. Since you have replicated the error, can you try out the canary release (which I am triggering now)?

@Sidnioulz Sidnioulz added the upgrade:10.5 Issues/PRs found during 10.5 upgrade QA and post-release regressions label Jul 9, 2026
Copilot AI requested a review from Sidnioulz July 9, 2026 10:19
ndelangen and others added 2 commits July 9, 2026 12:33
Move the `WebSocket` global stub out of module scope into `beforeEach` and
add `vi.unstubAllGlobals()` to `afterEach`, so the suite no longer leaves an
ambient global behind for other test files in the same worker. CodeRabbit's
proposed fix (only adding `vi.unstubAllGlobals()` while keeping the stub at
module scope) would break the suite: after the first test's cleanup the stub
is gone and subsequent tests construct a real WebSocket, so `socketRef` is
never populated. Re-stubbing per test keeps all 7 tests passing while
complying with the repo's global-stubbing guideline.

Co-authored-by: Cursor <cursoragent@cursor.com>
@Sidnioulz Sidnioulz merged commit b7813f5 into next Jul 9, 2026
154 checks passed
@Sidnioulz Sidnioulz deleted the valentin/websocket-heartbeat-reset-on-any-message branch July 9, 2026 11:26
@ndelangen

Copy link
Copy Markdown
Member

I was still testing this @Sidnioulz, I'll need to do a follow-up, as this PR alone did not resolve the problem fully

This was referenced Jul 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug ci:normal Run our default set of CI jobs (choose this for most PRs). core qa:needed Pull Requests that will need manual QA prior to release. upgrade:10.5 Issues/PRs found during 10.5 upgrade QA and post-release regressions

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants