feat!: share one instance shell; make the WebSocket binding explicit - #191
Merged
Conversation
`initDevframe` and `initHub` duplicated ~250 lines of the same wiring and
each carried machinery that belongs to their hosts. Both are now assembled
from one internal shell, and neither binds a port unless asked to.
- Add `createInstanceShell` (`devframe/internal`), owning the mount base and
h3 app, the lazily-derived origin and the auth banner that waits for it,
auth resolution, the WebSocket binding, connection meta, the fetch /
connect-middleware pair, the upgrade API and teardown. Each factory now
supplies only its own context, routes and diagnostics.
- Resolve the WS binding as `ws.port` > `server` > `ws.sidecar` (new) > the
host's own upgrades, via `instance.attach(server)` / `handleUpgrade()`.
The default tier builds its transport lazily, so an instance nobody
attaches binds nothing and loads nothing. `DF0055` / `DF0056` report an
upgrade hand-off to an instance that already owns its socket.
- Drop the implicit side-car: hosts whose handlers never see upgrades
(Next.js, Nitro, Rsbuild) now ask for one with `ws: { sidecar: true }`.
- Drop the `key` memoization (and `DF0053` / `DF8001`) — a host that
re-evaluates its modules owns the `globalThis` memo, which keeps the
lifecycle visible at the call site. `createDevframeNextHandler` keeps its
`key` option and memoizes internally, so Next hosts need no change.
- Drop the Bun tier: `instance.websocket` and `handler`'s second argument
are gone, and `handler` is exactly `(request: Request) => Promise<Response>`.
A Bun host binds the transport itself with `createContextRpcServer` +
`attachBunWsTransport`, as `examples/hub-hono-minimal/src/bun.ts` now shows
(`scripts/smoke-bun.ts` still covers it end to end).
- Fix `resolveDevframesInput`: `Array.isArray` left the readonly-array member
in the false branch (a type error), and an entry resolving to `[null]`
reached `normalizeDevframeEntry(null)`. Empty slots are filtered, so
conditional `devframes` lists work.
Also: `attachWsRpcTransport` gains `unbound`, `handleUpgrade` and `attach`;
`DevframeWsOptions` gains `sidecar`; the migration guide covers every step.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
✅ Deploy Preview for devfra ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
There was a problem hiding this comment.
Pull request overview
Refactors the Node “initiate” adapters to share a single internal instance shell and makes WebSocket transport binding explicitly host-driven (with an opt-in sidecar), updating public types, tests, examples, and migration docs to match the new transport tiers and breaking API surface.
Changes:
- Introduces
createInstanceShell()(unstabledevframe/internal) to centralize shared init wiring forinitDevframeandinitHub(origin/auth/meta/ws binding/teardown). - Changes the WebSocket binding contract to be explicit: adds
ws.sidecar, addsattach()/handleUpgrade(), removes implicit sidecar behavior, removeskey, and removes the Bun-specifichandlersecond arg +instance.websocket. - Migrates downstream consumers, examples, docs, and API snapshots; expands test coverage for the new binding tiers and refusal diagnostics (
DF0055/DF0056).
Reviewed changes
Copilot reviewed 43 out of 48 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/snapshots/tsnapi/devframe/internal.snapshot.js | Updates internal API snapshot exports for the new shell utilities. |
| tests/snapshots/tsnapi/devframe/internal.snapshot.d.ts | Updates internal type surface snapshot for createInstanceShell, samePath, and related types. |
| tests/snapshots/tsnapi/devframe/initiate.snapshot.d.ts | Updates initDevframe public API snapshot (handler signature, attach/handleUpgrade, removal of websocket/key). |
| tests/snapshots/tsnapi/devframe/index.snapshot.d.ts | Updates snapshot to include DevframeWsOptions.sidecar. |
| tests/snapshots/tsnapi/@devframes/hub/initiate.snapshot.d.ts | Updates hub initiate snapshot (handler signature, attach/handleUpgrade, DevframesInput, removal of key/websocket). |
| scripts/smoke-bun.ts | Updates Bun smoke test to use the new Bun wiring (startBunServer) and new handler shape. |
| packages/next/src/handler.ts | Switches Next helper to memoize handler on globalThis and explicitly request a WS sidecar. |
| packages/hub/src/node/mount-devframe.ts | Adjusts hub dock registration for mounted devframes (icon handling changed). |
| packages/hub/src/node/initiate.ts | Refactors hub init to use createInstanceShell, adds flexible devframes input resolution, and updates WS binding behavior/docs. |
| packages/hub/src/node/diagnostics.ts | Removes DF8001 (key memoization replacement diagnostic) along with key removal. |
| packages/hub/src/node/tests/initiate.test.ts | Updates tests for explicit WS binding tiers (unbound + attach; ws.sidecar). |
| packages/devframe/src/types/devframe.ts | Documents and adds DevframeWsOptions.sidecar. |
| packages/devframe/src/rpc/transports/ws-server.ts | Adds “unbound” mode plus handleUpgrade/attach APIs and attachment tracking for deterministic teardown. |
| packages/devframe/src/node/instance-shell.ts | Adds new shared instance shell implementing binding tiers and host-driven upgrades. |
| packages/devframe/src/node/diagnostics.ts | Removes DF0053 and adds DF0055/DF0056 for upgrade handoff misuse tiers. |
| packages/devframe/src/internal/index.ts | Exposes createInstanceShell/samePath and related types via devframe/internal. |
| packages/devframe/src/helpers/vite.ts | Updates viteDevBridge to explicitly request ws.sidecar when Vite has no node:http server to share. |
| packages/devframe/src/adapters/initiate.ts | Refactors initDevframe to use the shared shell; updates public surface and WS binding semantics. |
| packages/devframe/src/adapters/tests/initiate.test.ts | Reworks initiate adapter tests for new default/unbound + sidecar tiers and new DF0055/DF0056 behavior. |
| examples/hub-vite/src/vite-devframe-hub.ts | Updates Vite hub plugin to request explicit sidecar when server isn’t a node:http server. |
| examples/hub-vite-minimal/vite.config.ts | Moves hub init into configureServer, removes key, and adds explicit sidecar fallback when no node:http server. |
| examples/hub-rsbuild-minimal/rsbuild.config.ts | Removes key memo and configures explicit sidecar for Rsbuild (middleware stack can’t provide upgrades). |
| examples/hub-rsbuild-minimal/README.md | Updates README to describe explicit sidecar WS binding. |
| examples/hub-nitro-minimal/README.md | Updates README to describe explicit sidecar WS binding. |
| examples/hub-nitro-minimal/hub.ts | Replaces key memoization with explicit globalThis memo and explicit sidecar. |
| examples/hub-next/src/client/devframe/next-devframe-hub.ts | Removes key option and switches to globalThis memo; explicitly requests sidecar. |
| examples/hub-next/README.md | Updates Next example docs for explicit sidecar binding + memoization wording. |
| examples/hub-next-minimal/src/client/hub.ts | Removes key, uses globalThis memo, and explicitly requests sidecar. |
| examples/hub-hono-minimal/src/server.ts | Adds Node entry that attaches upgrades to the hub (hub.attach(server)). |
| examples/hub-hono-minimal/src/node.ts | Removes old Node entry file in favor of unified server.ts. |
| examples/hub-hono-minimal/src/bun.ts | Adds explicit Bun transport wiring using createContextRpcServer + attachBunWsTransport. |
| examples/hub-hono-minimal/src/app.ts | Updates runtime-agnostic app to use new handler signature and globalThis memo; transport wiring moved to runtime entries. |
| examples/hub-hono-minimal/README.md | Updates README to describe Node attach vs Bun fetch-upgrade wiring. |
| examples/hub-hono-minimal/package.json | Updates dev script to use src/server.ts. |
| docs/helpers/next.md | Updates Next helper docs (memoization and corrected import path for startHttpAndWs). |
| docs/guide/migration-0.9.md | Adds migration sections for explicit WS binding, removal of key, and removal of Bun tier from instances. |
| docs/guide/hub-initiate.md | Updates hub initiate guide for new shared socket tiers and new instance surface (attach/handleUpgrade). |
| docs/examples/hub-rsbuild-minimal.md | Updates docs page for explicit sidecar on Rsbuild. |
| docs/examples/hub-nitro-minimal.md | Updates docs page for explicit sidecar on Nitro. |
| docs/examples/hub-next.md | Updates docs page for explicit sidecar + memoization on Next. |
| docs/examples/hub-next-minimal.md | Updates docs page for explicit sidecar + memoization on Next minimal. |
| docs/examples/hub-hono-minimal.md | Updates docs page for Node attach vs Bun transport binding. |
| docs/errors/DF8001.md | Removes obsolete DF8001 doc (key memoization replaced). |
| docs/errors/DF0056.md | Adds new DF0056 doc for external advertised WS tier refusing upgrade handoff. |
| docs/errors/DF0055.md | Adds new DF0055 doc for refusing upgrade handoff when instance already owns transport. |
| docs/errors/DF0053.md | Removes obsolete DF0053 doc (key memoization replaced). |
| docs/adapters/initiate.md | Updates initiate adapter docs for new WS binding tiers, new instance surface, and globalThis memo guidance. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Comment on lines
94
to
98
| ctx.docks.register({ | ||
| id, | ||
| title: d.name, | ||
| icon: d.icon ?? 'ph:plug-duotone', | ||
| icon: d.icon, | ||
| // Definition-level `dock` defaults sit above the name/icon-derived |
Comment on lines
+429
to
+436
| function attach(server: NodeHttpServer): () => void { | ||
| assertUnbound() | ||
| server.on('upgrade', handleUpgrade) | ||
| // Build the transport now rather than on the first upgrade, so the socket | ||
| // is live (and its absolute URL published) the moment the host is. | ||
| void ensureTransport().then(() => publishWsEndpoint(server)).catch(() => {}) | ||
| return () => server.off('upgrade', handleUpgrade) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements the four TODOs left in
initDevframe/initHub: they duplicated ~250 lines of the same wiring, memoized instances on akey, carried a Bun tier, and started a side-car WebSocket server nobody asked for.One shared shell
New internal
createInstanceShell(exported from the unstabledevframe/internal) owns everything both factories had in common: the mount base and h3 app, the lazily-derived public origin and the auth banner that waits for it, auth resolution, the WebSocket binding, connection meta, the fetch / connect-middleware pair, the upgrade API, and teardown. Each factory now supplies only what makes it itself — its context, its routes, its diagnostics.packages/hub/src/node/initiate.ts−628 lines,packages/devframe/src/adapters/initiate.ts−544, against +462 for the shell.The WebSocket binding is the host's call
Nothing binds a port on its own. The local binding resolves:
ws.port— a side-car on that exact portserver— share the host'snode:httpupgrade at<base>__wsws: { sidecar: true }— new: a side-car on a free port, for hosts whose handlers never see upgrades (Next.js route handlers, Nitro, Rsbuild)instance.attach(server)routes a server'supgradeevents (returning a detach fn),instance.handleUpgrade(req, socket, head)completes one from a listener you already ownThat last tier builds its transport lazily, so an instance nobody attaches binds nothing and loads nothing.
ws.urlkeeps controlling the advertisement only (the tunnel pattern). Handing upgrades to an instance that already owns its socket reports the newDF0055, orDF0056on the advertise-onlyws.urltier.Breaking changes
All covered by new sections in
docs/guide/migration-0.9.md.ws: { sidecar: true }key(+DF0053/DF8001)globalThismemo at the call site;createDevframeNextHandlerkeeps itskeyand memoizes internally, so Next hosts need no changeinstance.websocket,handler's 2nd arghandleris exactly(request: Request) => Promise<Response>; a Bun host binds the transport itself withcreateContextRpcServer+attachBunWsTransport(devframe/rpc/transports/ws-bunstays published)Drive-by fix
resolveDevframesInputhad two type errors —Array.isArraydoesn't narrow the readonly-array member out of the false branch — and a latent crash: a lazy entry resolving to[null]reachednormalizeDevframeEntry(null). Empty slots are filtered now, so conditionaldevframeslists work.Consumers
@devframes/nextowns its memo and asks for a side-car explicitly;viteDevBridgeasks for one when middleware-mode Vite has no server to share; all seven hub examples migrate to theglobalThisrecipe and explicit transport wiring.examples/hub-hono-minimalsplits intosrc/server.ts(Node,hub.attach(serve(...))) andsrc/bun.ts(Bun's own transport over the hub context) — crossws' Node adapter refuses to run under Bun, so node-compat can't serve that socket.Verification
pnpm lint,pnpm knip,pnpm test(1081 passed; API snapshots regenerated — the diff is exactly the surface change above),pnpm buildandpnpm docs:buildall pass.Booted for real:
bun scripts/smoke-bun.ts(Bun fetch-upgrade RPC round-trip), the Hono Node entry (RPC over the attached upgrade),hub-vite-minimal(shared server →{path: '/__devframes/__ws'}),hub-nitro-minimal(side-car → 9777).pnpm typecheckhas one pre-existing, unrelated failure left alone:packages/hub-ui/scripts/build-css.tson duplicatedmagic-string(0.30.21 vs 1.0.0). It reproduces with these changes stashed; fixing it is a dependency-dedupe decision.🤖 Generated with Claude Code