feat(client): speak WebSocket as well as Server-Sent Events - #2421
Conversation
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 detectedLatest commit: 6114e44 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
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 |
|
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 configurationConfiguration used: Organization UI Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (2)
Included review availability: Your plan provides up to 8 included reviews per hour; 5 remain after this review. WalkthroughThe 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 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)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Advanced
Run ID: 1775ab81-01d2-40d3-a822-f492a48741c4
📒 Files selected for processing (9)
.changeset/client-transports.mdREADME.mdclient-src/clients/EventSourceClient.jsclient-src/clients/WebSocketClient.jsclient-src/clients/createSocket.jsclient-src/globals.d.tsclient-src/index.jspackage.jsontest/client-socket.test.js
Included review availability: Your plan provides up to 8 included reviews per hour; 6 remain after this review.
`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 Report❌ Patch coverage is
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. 🚀 New features to boost your workflow:
|
Summary
The browser runtime can now carry its events over a WebSocket, a transport of your own, or Server-Sent Events as before:
This is the client half of #2420. That PR gave the server
hot.transport: "ws"but the runtime still built anEventSourceinline, 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/andlib/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'sclient.webSocketTransportalready has, so every custom client written for it works here unchanged. It is not the factory shapehot.transporttakes 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, whatevertransportsays, 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:
ssetimeoutwsreconnect(10)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:
EventSourceClientkeeps the silence watchdog. A dead Server-Sent Events connection frequently never fireserror— a proxy that stops forwarding, a laptop that slept — so silence pasttimeoutis what detects it. It reports that as a close and lets the shared loop reconnect.WebSocketClientneeds 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") andreconnectare new. The runtime also accepts webpack-dev-server's spellings —webSocketURL,live-reload— so that PR needs no translation layer../client/sseand./client/wsare 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 overcreateSocketwith 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 afterclose().Each was checked against a deliberately broken implementation. Removing the retry limit fails exactly the limit case; removing the
attempt = 0on 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:
createRequireEsmError, because puppeteer is ESM-only and jest cannot require it here. That fails identically on a cleanmain, 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.test/logging.test.jsfails 74/74 on a cleanmainas 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.
transportdefaults to"sse", the Server-Sent Events path keeps its retry behavior, and the runtime's exports are unchanged. TheEventSourcewarning at startup now names whichever transport is actually missing, so asking for a WebSocket on a browser withoutEventSourceno 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
transportandreconnectoptions, the./client/sseand./client/wsentry 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