feat(dev): replace raw Rsbuild HMR frames with a provider-owned Runtime App reload channel (#73) - #84
Conversation
🦋 Changeset detectedLatest commit: 8df8f6e 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 |
commit: |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d95625f299
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Replace the raw Rsbuild HMR WebSocket relay with a provider-owned reload channel. The client-surface endpoint now exposes subscribeReload, fed by the App environment compile hook; the proxy hosts its own one-way reload WebSocket, replays the current generation on reconnect, and refreshes the opaque App child only when the generation strictly advances. Rsbuild's private frames, dev.client.path, and webSocketToken leave the contract. Fixes #73 Co-authored-by: Zack Jackson <ScriptedAlchemy@users.noreply.github.com>
d95625f to
816c3d0
Compare
A reload frame that arrived while the shell's refresh fetch was still in flight advanced the seen generation without being applied, so the newer compilation never installed and the server's replay-on-reconnect stayed inert. Track the applied generation separately from the requested one: advance it only when a refresh actually installs an entry, and run a catch-up refresh when a newer generation was announced mid-flight. A failed refresh now leaves its generation unapplied, so a replayed frame retries instead of being swallowed.
Fixes #73
What this does
Runtime App refresh no longer depends on Rsbuild's private WebSocket protocol in any form. The reload signal now travels entirely over surfaces we own:
agent-bundle:rsc-runtime-app-reloadplugin (formerly…app-hmr-token) invokes a plainonAppReload()callback for each later successful, changed App environment compilation. The dedup logic (first compile, errored compile, unchanged hash, dev-server lifecycle) is the previoushot.send('full-reload')gate unchanged; only the emission changed.RsbuildRuntimeSessionrelays the callback to subscribers of the trusted client-surface endpoint's newsubscribeReload(listener) => unsubscribefield. The endpoint no longer carrieswebSocketOrigin/webSocketPath/webSocketToken; the Rsbuild HMR credential never leaves the compiler process.RuntimeClientSurfaceProxyhosts its own one-way WebSocket at/__agent_bundle_runtime/reload(inside the namespace it already owns for bootstrap). It authors both ends: the server broadcasts{"kind":"runtime-app-reload","generation":N}frames, and the bootstrap shell it serves is the only intended client. It never dials Rsbuild's WebSocket.Robustness gained over the previous relay
pagehideteardown, payload bounds, and the abort-safe entry refresh are preserved unchanged.Relationship to #76 (not a revert)
#76 (
24f0ea19) was the bounded conformance fix: it kept the raw Rsbuild socket but allowlisted only the provider-emittedfull-reloadkind, leaving privateok/hash/unknown frames inert. That removed the semantic error but still treated Rsbuild's undocumented envelope as an application protocol — the failure mode was silence, not misbehavior. This PR is the deeper replacement #76's comments pointed at: the envelope is no longer consumed at all, so the allowlist (and the token/path plumbing that fed the raw socket) is deleted rather than restored to its pre-#76 shape.API changes (published
agent-bundlepackage, minor bump via changeset)DevRuntimeClientSurfaceEndpoint:+subscribeReload,-webSocketOrigin,-webSocketPath,-webSocketToken(trusted server-only type).DevRuntimeClientSurfaceProxyBinding:-webSocketPath.McpAppPreviewAppsSnapshot.clientSurface:-webSocketPath(was a hardcoded'/rsbuild-hmr'literal the Workbench only validated, never used); the Workbench client validation is updated in the same commit.runtimeClientSurfaceReloadChannelPathfor observability/tests.Tests
ok/hash/full-reload) remain only to prove they are inert.onAppReloadgate (first/duplicate/failed/non-App compiles emit nothing; later changed compiles emit once, across dev-server restarts).overview.e2easserts a real source edit produces an owned advancing-generation frame and a single in-place child refresh;mcp-app-real.e2ecovers connection loss/recovery plus injected upgrade-style frames — a legacy{type:'full-reload'}frame stays inert while an owned newer-generation frame reinstalls the App once;runtime-playground-hmr.e2ecovers client connect/disconnect counters unchanged.Validation (all on this branch)
pnpm build,pnpm typecheck(root + workbench + create-agent-bundle), exampletypecheck,pnpm lint: pass.pnpm test:unit: pass.dev-provider.integration.test.ts,dev-invocation.integration.test.ts: pass.dev-workbench.test.ts,mcp-app-frame,mcp-app-preview-browser,mcp-page-app-browser,runtime-playground-hmr.e2e,runtime-playground.e2e,overview.e2e,mcp-app-real.e2e: pass, except one pre-existing failure indev-workbench.test.ts :: simulates and replays real epoch-bound hooks through the packaged foreground server, which fails identically on unmodifiedmain(28830bd5) in this environment and is unrelated to this change (hook replay, not the relay).Stayed in lane
No changes to #74/#75 territory (per-env output staging, cohort identity), no changes to the compile-observer FIFO pairing in
rsbuild.config.ts(the reload plugin is a separate plugin in the same file), and no changes torslib.ts,build.test.ts,hooks.test.ts, or packed-consumer suites.overview.e2eandmcp-app-real.e2echanged only where they previously asserted raw/rsbuild-hmrframes.