Skip to content

fix(client): make reconnect work on Server-Sent Events, and say where timeout applies - #2457

Merged
alexander-akait merged 2 commits into
mainfrom
fix/reconnect-and-timeout-per-transport
Oct 3, 2026
Merged

alexander-akait merged 2 commits into
mainfrom
fix/reconnect-and-timeout-per-transport

Conversation

@alexander-akait

@alexander-akait alexander-akait commented Oct 2, 2026 •

Copy link
Copy Markdown
Member

What

Two client options were each inert on one transport, which came out of looking at whether timeout, reconnect and autoConnect could be grouped into one option. The grouping is cosmetic; this is the part that was actually broken.

// before
clientOptions: { timeout: options.timeout },
retries: isEventSource ? Infinity : options.reconnect,

reconnect did nothing over Server-Sent Events — the default transport. It was overridden to Infinity, so asking for a bounded number of attempts was ignored. It is honoured now. Unset still means keep trying for as long as the page is open: a dev server is expected to come back, and a tab left open across a restart has to find it again.

timeout did nothing over a WebSocket. It was handed to a client whose constructor is constructor(url) and takes no options at all.

Why timeout is documented rather than implemented

My first instinct was to give the WebSocket client the same silence watchdog the EventSource one has. That is wrong, and the reason is in the servers:

EventSourceServer   client.write("data: 💓\n\n")   // data, visible to JavaScript
WebSocketServer     client.ping(() => {})          // a protocol ping

The browser answers a ping in its network stack and never shows it to JavaScript. A silence watchdog over a WebSocket would therefore fire on a connection that is healthy and merely idle.

And it is not needed. The half-open case the watchdog exists for is already handled where the pong is visible — the server sees the missing one and terminates the socket, so the browser gets a real close and reconnects:

// WebSocketServer
if (awaitingPong.has(client)) {
  client.terminate();
  continue;
}

So timeout is Server-Sent Events only, it says so in the schema, the typedef and the README, and it is no longer passed to a client that drops it.

Both decisions in one testable place

They moved into client-src/utils/socket-options.js, a pure function — which is what makes them testable at all, since client-src/index.js connects on import. Eight cases: one per transport per option, plus reconnect: 0 meaning none rather than unset.

A correction

The README already said "sse" ignored reconnect. I had said neither doc mentioned it; that was true of the schema only. Both describe the new behaviour now.

Verified

  • npm run lint — clean (eslint, prettier, cspell, tsc, client types, schema-check)
  • unit — 7144 passed, 22 suites
  • e2e — 167 passed, 14 suites, 20 snapshots

Next, not here

connect: false | { retries, timeout } would fold these three into one option. Worth doing once they behave, not before — grouping them while two were inert would have made connect: { retries: 10 } look even more like it worked on the default transport. Noted in #2454.

🤖 Generated with Claude Code

https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA


Generated by Claude Code

Summary by CodeRabbit

  • New Features
    • Server-Sent Events retry indefinitely by default while the page remains open; WebSocket connections stop after 10 attempts. You can set a retry limit for either transport.
    • The timeout option applies only to Server-Sent Events. WebSocket connections use protocol pings, which are not visible to JavaScript.
  • Documentation
    • Clarified how retry limits, silence timeouts, and heartbeats work for each transport.

…re `timeout` applies

Two options were each inert on one transport.

`reconnect` was overridden to `Infinity` over Server-Sent Events, so asking
for a bounded number of attempts on the default transport did nothing. It
is honoured now. Unset still means keep trying for as long as the page is
open: a dev server is expected to come back, and a tab left open across a
restart has to find it again.

`timeout` was handed to a WebSocket client whose constructor is
`constructor(url)` and takes no options at all. The fix there is not to
implement the watchdog, which was my first instinct and is wrong — the two
transports send different kinds of heartbeat:

    EventSourceServer   client.write("data: 💓\n\n")   data, visible to JS
    WebSocketServer     client.ping(() => {})          a protocol ping

The browser answers a ping in its network stack and never shows it to
JavaScript, so a silence watchdog over a WebSocket would fire on a
connection that is healthy and merely idle. The half-open case it would
have caught is already handled where the pong is visible: the server sees
the missing one and terminates the socket, so the browser gets a real
`close` and reconnects. So `timeout` is Server-Sent Events only, it is
documented as such, and it is no longer passed to a client that drops it.

Both decisions moved into `client-src/utils/socket-options.js`, which is a
pure function and therefore testable — eight cases, one per transport per
option, including `reconnect: 0` meaning none rather than unset.

The README already said `"sse"` ignored `reconnect`; the schema did not.
Both say what happens now.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA
@changeset-bot

changeset-bot Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 7ad7dad

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
webpack-dev-middleware Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai

coderabbitai Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: a0ff09cb-f329-412c-88dd-8276f71837f0

📥 Commits

Reviewing files that changed from the base of the PR and between 3ed6a60 and 7ad7dad.

📒 Files selected for processing (7)
  • .changeset/reconnect-and-timeout-per-transport.md
  • README.md
  • src/hot.js
  • src/options.check.js
  • src/options.json
  • test/client-socket-options.test.js
  • types/hot.d.ts
🚧 Files skipped from review as they are similar to previous changes (5)
  • src/hot.js
  • src/options.json
  • types/hot.d.ts
  • README.md
  • .changeset/reconnect-and-timeout-per-transport.md

Included review availability: This review used your included allowance. Your plan provides up to 8 included reviews per hour; 5 remain after this review.


Walkthrough

The client now selects socket retry and timeout settings by transport. Server-Sent Events default to unlimited retries and use the configured timeout for retry delay and client options. WebSocket settings use the configured retry count without a retry delay or client timeout option. Tests cover both transports, and the README and option documentation describe these behaviors.

Priority: ⬇️ Low

Merge Risk: ⚪ Minimal · up to 7ad7d

SSE reconnect limits are enforced, the normal client retains its timeout default, and the README limits timeout to SSE. No actionable merge-blocking issue remains.

Security Architecture Review

Security architecture risk: ⚪ Minimal · up to 7ad7d

The change makes configured SSE retry limits effective without changing connection destinations or credential handling. The reviewed failure and cleanup paths preserve bounded retries and explicit shutdown; no material security risk was identified in this change.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The changed behavior affects reconnection attempts to the browser client’s already-selected HMR endpoint. Explicit SSE limits reduce replacement attempts; the inspected change does not broaden destinations or credential authority.

Trust Boundaries and Controls

  • observed — Client overrides already allow an absolute endpoint and a token, and withToken places that token in the connection URL. The base-to-head comparison leaves these inputs and credential propagation unchanged; retry-policy extraction introduces no new credential-bearing destination path.

Resilience and Maintainability Implications

  • inferred — Native SSE retry behavior does not bypass the configured wrapper limit on the inspected failure path: the transport is closed before a counted replacement is scheduled. Existing closure guards and timer cleanup contain reconnect activity after exhaustion or explicit shutdown.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main changes: applying reconnect limits to Server-Sent Events and clarifying where timeout applies.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 3 functions across 6 files. (3 skipped: 3 …
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Autopilot is currently an internal CodeRabbit preview.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: 2fe58dd6-fa7a-4195-9460-053c0f2f65d2

📥 Commits

Reviewing files that changed from the base of the PR and between ad64775 and 3ed6a60.

📒 Files selected for processing (10)
  • .changeset/reconnect-and-timeout-per-transport.md
  • README.md
  • client-src/index.js
  • client-src/utils/socket-options.js
  • src/hot.js
  • src/options.check.js
  • src/options.json
  • test/client-socket-options.test.js
  • types/client/utils/socket-options.d.ts
  • types/hot.d.ts

Included review availability: This review used your included allowance. Your plan provides up to 8 included reviews per hour; 6 remain after this review.

Comment thread README.md Outdated
@codecov

codecov Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 96.36%. Comparing base (ad64775) to head (7ad7dad).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2457      +/-   ##
==========================================
+ Coverage   96.27%   96.36%   +0.08%     
==========================================
  Files          22       23       +1     
  Lines        2445     2449       +4     
==========================================
+ Hits         2354     2360       +6     
+ Misses         91       89       -2     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

The README quoted `10`, which is `createSocket`'s fallback and therefore
only what a WebSocket does. Unset over Server-Sent Events the retry count
is `Infinity` — the figure was wrong for the default transport, and had
been while the option was ignored there, so making it work is what made the
documentation wrong rather than merely incomplete.

    unset, "sse"  ->  Infinity   (this module's default)
    unset, "ws"   ->  10         (createSocket's, reached by passing nothing)
    set           ->  both

A test pins both, reading `createSocket`'s number out of its source rather
than restating it, since that is the half the docs do not own.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA
@alexander-akait
alexander-akait merged commit 792bb61 into main Oct 3, 2026
24 checks passed
@alexander-akait
alexander-akait deleted the fix/reconnect-and-timeout-per-transport branch October 3, 2026 10:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant