Skip to content

feat(client): speak WebSocket as well as Server-Sent Events - #2421

Merged
alexander-akait merged 3 commits into
mainfrom
feat/client-transports
Sep 24, 2026
Merged

alexander-akait merged 3 commits into
mainfrom
feat/client-transports

Conversation

@alexander-akait

@alexander-akait alexander-akait commented Sep 24, 2026 •

Copy link
Copy Markdown
Member

Summary

The browser runtime can now carry its events over a WebSocket, a transport of your own, or Server-Sent Events as before:

entry: ["webpack-dev-middleware/client?transport=ws", "./src/app.js"];

This is the client half of #2420. That PR gave the server hot.transport: "ws" but the runtime still built an EventSource inline, so there was nothing on the other end of it — which is what CodeRabbit flagged there. With this, transport: "ws" works end to end.

Next, in its own PR: webpack-dev-server drops its own client-src/ and lib/servers/ and reuses these.

How it fits together

A transport is a class constructed with the url, behind onOpen / onClose / onMessage / close. That is deliberately the shape webpack-dev-server's client.webSocketTransport already has, so every custom client written for it works here unchanged. It is not the factory shape hot.transport takes on the server — the asymmetry is the price of not breaking those, and it seemed the better trade.

An injected __webpack_dev_server_client__ wins over both built-ins, whatever transport says, which is how webpack-dev-server will keep passing its own.

Reconnecting moved out of the transports into one loop they share — but the two policies are kept apart on purpose:

retries delay
sse infinite steady timeout
ws reconnect (10) exponential backoff

Folding both onto webpack-dev-server's 10-attempt backoff would have been tidier, and it is what "share the reconnect loop" sounds like it should mean. It also silently regresses every Server-Sent Events user who has this installed today: the runtime currently retries for as long as the page is open, and would instead give up after roughly seventeen minutes, leaving a tab that outlived a slow server restart quietly disconnected. So the loop is shared and the policy is the caller's.

For the same reason the transports own only liveness, not retries:

  • EventSourceClient keeps the silence watchdog. A dead Server-Sent Events connection frequently never fires error — a proxy that stops forwarding, a laptop that slept — so silence past timeout is what detects it. It reports that as a close and lets the shared loop reconnect.
  • WebSocketClient needs no watchdog: the browser reports the drop itself, and the server already pings for half-open sockets.

The per-path connection cache is untouched, so several entries on one page still share one connection.

Client options

transport ("sse" | "ws") and reconnect are new. The runtime also accepts webpack-dev-server's spellings — webSocketURL, live-reload — so that PR needs no translation layer.

./client/sse and ./client/ws are exported, both so a custom client can extend one instead of starting over, and because that is where webpack-dev-server will get its default transport from.

What kind of change does this PR introduce?

feat

Did you add tests for your changes?

Yes — test/client-socket.test.js, seven cases over createSocket with a transport driven from the test: message fan-out to every listener, client options reaching the constructor, the backoff growing between attempts, the backoff starting over once a connection opens, giving up at the retry limit, retrying indefinitely when told to, and not reconnecting after close().

Each was checked against a deliberately broken implementation. Removing the retry limit fails exactly the limit case; removing the attempt = 0 on open fails exactly the backoff-reset case — so they pin their own behavior rather than passing together. The ES5 guard covers the new files.

Two things I could not verify locally and would rather say than leave to be found:

  1. The browser e2e suites do not run in my sandbox — createRequireEsmError, because puppeteer is ESM-only and jest cannot require it here. That fails identically on a clean main, so it is environmental rather than caused by this change, but it does mean the real browser path is unexercised until CI runs it. The "ws" path in particular has no end-to-end coverage yet on my side.
  2. test/logging.test.js fails 74/74 on a clean main as well, unrelated to this and to feat(hot): serve the events over a WebSocket too #2420.

Everything else: 6784 tests pass, full lint clean, build clean.

Does this PR introduce a breaking change?

No. transport defaults to "sse", the Server-Sent Events path keeps its retry behavior, and the runtime's exports are unchanged. The EventSource warning at startup now names whichever transport is actually missing, so asking for a WebSocket on a browser without EventSource no longer warns about the wrong thing.

If relevant, what needs to be documented once your changes are merged or what have you already documented?

Documented here: the client transport and reconnect options, the ./client/sse and ./client/ws entry points, and a section on writing a client of your own — including that reconnecting is the runtime's job, not the client's.

Use of AI

AI-assisted (Claude Code). It was used to write the transports, the shared reconnect loop, the tests and the documentation, and to verify them: each new test was run against a broken implementation to confirm it fails, and both pre-existing failures above were confirmed by running those suites on a clean checkout. All output was reviewed before committing.

🤖 Generated with Claude Code

https://claude.ai/code/session_01UjuMAuk9o6UazjHzcAQCTA


Generated by Claude Code

Summary by CodeRabbit

  • New Features
    • Added WebSocket as an alternative to Server-Sent Events (SSE) for the browser client; SSE remains the default.
    • Added configurable WebSocket reconnection attempts and support for custom transports.
    • Made the built-in SSE and WebSocket clients available for direct use.
  • Documentation
    • Updated the client options guide with transport selection, reconnection settings, and connection path details.
    • Added guidance for using custom transports and the built-in clients.

The runtime built its own EventSource inline, so the server's new
`hot.transport: "ws"` had nothing to talk to. The transport is a class
behind `onOpen`/`onClose`/`onMessage`/`close` now, chosen by a `transport`
query parameter, and a client injected as `__webpack_dev_server_client__`
wins over both — the shape webpack-dev-server's `client.webSocketTransport`
already has, so one written for it works here unchanged.

Reconnecting moves out of the transport into one loop the two share, which
keeps what each of them did: Server-Sent Events retry at a steady interval
for as long as the page is open, a WebSocket backs off and gives up after
`reconnect` attempts.
@changeset-bot

changeset-bot Bot commented Sep 24, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 6114e44

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 Minor

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 Sep 24, 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: 69722619-5b9e-49c1-888b-df79c90b1a37

📥 Commits

Reviewing files that changed from the base of the PR and between f741817 and 6114e44.

📒 Files selected for processing (2)
  • client-src/clients/createSocket.js
  • test/client-socket.test.js

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


Walkthrough

The client runtime now supports WebSocket transport alongside SSE and can use an injected custom client. Shared connection handling manages message delivery and reconnection. The runtime accepts transport and reconnection options, and the package exposes the built-in SSE and WebSocket clients. The README and changeset describe these additions, and tests cover connection wrapper behavior.

Merge Risk: ⚪ Minimal · up to 6114e

The transport changes appear ready to merge after normal checks; no unresolved connection or packaging failure was established.

🚥 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 primary change: adding WebSocket support alongside the existing Server-Sent Events client transport.
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 7 functions across 7 files.
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

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: 3


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: 1775ab81-01d2-40d3-a822-f492a48741c4

📥 Commits

Reviewing files that changed from the base of the PR and between dfdce46 and 015966b.

📒 Files selected for processing (9)
  • .changeset/client-transports.md
  • README.md
  • client-src/clients/EventSourceClient.js
  • client-src/clients/WebSocketClient.js
  • client-src/clients/createSocket.js
  • client-src/globals.d.ts
  • client-src/index.js
  • package.json
  • test/client-socket.test.js

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

Comment thread client-src/clients/WebSocketClient.js Outdated
Comment thread client-src/index.js Outdated
Comment thread package.json
`WebSocket` only learned to take a relative or `http(s):` url in 2024 —
Chrome 125, Firefox 124, Safari 17.3 — and throws on one before that. The
endpoint defaults to a path, so `transport=ws` threw outright on exactly the
older browsers this ES5 runtime exists to support. It is resolved to an
absolute `ws:`/`wss:` url first now.

The startup check also asked the browser for `EventSource` even where an
injected client was going to do the connecting, and the documented way to
reuse a built-in transport used `require()`, which the ESM client build
cannot answer.
Reconnecting moved into the shared loop, which announces each attempt the
way webpack-dev-server's always has. That is affordable for a WebSocket,
which gives up after ten tries; Server-Sent Events retry for as long as the
page is open, so it meant saying so every few seconds, all day — and they
had never said it at all. Two e2e snapshots pin exactly that silence.

Whether to announce an attempt is the caller's now, defaulting to whether
the attempts are bounded.
@codecov

codecov Bot commented Sep 24, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 83.00000% with 17 lines in your changes missing coverage. Please review.
✅ Project coverage is 95.45%. Comparing base (dfdce46) to head (6114e44).

Files with missing lines Patch % Lines
client-src/clients/WebSocketClient.js 43.75% 9 Missing ⚠️
client-src/index.js 66.66% 7 Missing ⚠️
client-src/clients/createSocket.js 96.87% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2421      +/-   ##
==========================================
- Coverage   96.18%   95.45%   -0.73%     
==========================================
  Files          14       17       +3     
  Lines        1783     1849      +66     
==========================================
+ Hits         1715     1765      +50     
- Misses         68       84      +16     

☔ 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.

@alexander-akait
alexander-akait merged commit c7a7990 into main Sep 24, 2026
20 of 22 checks passed
@alexander-akait
alexander-akait deleted the feat/client-transports branch September 24, 2026 18:57
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