Skip to content

Commit d6150dd

Browse files
authored
fix(devframe): bind a runtime-appropriate RPC WebSocket transport on Bun/Deno (#322)
1 parent 94ad6cb commit d6150dd

9 files changed

Lines changed: 543 additions & 23 deletions

File tree

‎.github/workflows/ci.yml‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,38 @@ jobs:
1818
lint: pnpm run lint && pnpm run knip
1919
build-for-lint: true
2020

21+
# The RPC transport binds a native WebSocket on Bun/Deno (crossws's
22+
# Bun/Deno adapters over `Bun.serve` / `Deno.serve`) and falls back to SSE
23+
# for a shared foreign `node:http` server. This runs the cross-runtime smoke
24+
# test under each runtime so that binding can't regress (issue #317).
25+
runtime:
26+
runs-on: ubuntu-latest
27+
timeout-minutes: 15
28+
strategy:
29+
fail-fast: false
30+
matrix:
31+
runtime: [bun, deno]
32+
steps:
33+
- uses: actions/checkout@v7
34+
- uses: pnpm/action-setup@v6
35+
- uses: actions/setup-node@v7
36+
with:
37+
node-version: 22
38+
cache: pnpm
39+
- name: Set up Bun
40+
if: matrix.runtime == 'bun'
41+
uses: oven-sh/setup-bun@v2
42+
- name: Set up Deno
43+
if: matrix.runtime == 'deno'
44+
uses: denoland/setup-deno@v2
45+
with:
46+
deno-version: v2.x
47+
- run: pnpm install --frozen-lockfile
48+
- name: Build devframe
49+
run: pnpm --filter devframe run build
50+
- name: Run the ${{ matrix.runtime }} RPC transport smoke test
51+
run: pnpm run test:runtime:${{ matrix.runtime }}
52+
2153
e2e:
2254
runs-on: ubuntu-latest
2355
timeout-minutes: 15

‎docs/content/6.errors/DF0075.md‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
---
2+
title: 'DF0075: No RPC Transport On This Runtime'
3+
description: 'On Bun/Deno a shared server needs crossws Node adapter and SSE is disabled, so no RPC transport is advertised.'
4+
---
5+
6+
## Message
7+
8+
> On {runtime} the shared server's WebSocket upgrade needs crossws's Node adapter, which refuses to run off Node — and SSE is disabled, so this instance advertises no RPC transport at all.
9+
10+
## Cause
11+
12+
Sharing a host's `node:http` server (the `server` tier) drives the WebSocket upgrade through crossws's Node adapter, which runs only on Node. On Bun and Deno the socket falls back to the SSE endpoint — but here `sse: false` turned that endpoint off too, so the instance has no way for a client to reach its RPC surface.
13+
14+
## Example
15+
16+
```ts
17+
import { initHub } from '@devframes/hub/initiate'
18+
19+
// Running on Bun/Deno, sharing the host's node:http server:
20+
const hub = initHub({
21+
server: viteHttpServer,
22+
sse: false, // ✗ removes the only transport left on Bun/Deno
23+
})
24+
```
25+
26+
## Fix
27+
28+
Keep the SSE endpoint enabled (drop `sse: false`) so clients connect over it on Bun/Deno, or move the socket to a side-car — `ws: { sidecar: true }` binds the native WebSocket adapter (`Bun.serve` / `Deno.serve`) on its own port, where a real WebSocket works.
29+
30+
## Source
31+
32+
- [`packages/devframe/src/node/instance-shell.ts`](https://github.com/devframes/devframe/blob/main/packages/devframe/src/node/instance-shell.ts) — the shared instance shell warns this when a shared-server WebSocket binding falls back on Bun/Deno and the SSE endpoint is disabled, for both `initDevframe` and `initHub`.

‎docs/content/6.errors/DF0076.md‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
---
2+
title: 'DF0076: WebSocket Upgrade Unsupported On This Runtime'
3+
description: 'attach / handleUpgrade drive a raw node:http upgrade into crossws Node adapter, which refuses to run on Bun/Deno.'
4+
---
5+
6+
## Message
7+
8+
> `attach` / `handleUpgrade` drive a raw `node:http` upgrade into crossws's Node adapter, which refuses to run on {runtime}.
9+
10+
## Cause
11+
12+
`attach(server)` and `handleUpgrade(req, socket, head)` hand a raw `node:http` upgrade socket to crossws's Node adapter. That adapter runs only on Node — Bun and Deno expose WebSockets as `fetch` upgrades through `Bun.serve` / `Deno.serve` instead, so there is no `node:http` upgrade socket for the adapter to take over.
13+
14+
## Example
15+
16+
```ts
17+
import { initHub } from '@devframes/hub/initiate'
18+
19+
// Running on Bun/Deno:
20+
const hub = initHub({ base: '/__devframes/' })
21+
hub.attach(myNodeHttpServer) // ✗ throws DF0076 on Bun/Deno
22+
```
23+
24+
## Fix
25+
26+
On Bun/Deno, serve the advertised `__ws` route from `Bun.serve` / `Deno.serve` and complete the upgrade with `attachBunWsTransport` / `attachDenoWsTransport` (see the `hub-deno-minimal` example), or connect over the SSE endpoint instead — it rides the instance's ordinary HTTP surface and needs no upgrade wiring. A side-car (`ws: { sidecar: true }`) also binds the native WebSocket adapter for you on its own port.
27+
28+
## Source
29+
30+
- [`packages/devframe/src/node/instance-shell.ts`](https://github.com/devframes/devframe/blob/main/packages/devframe/src/node/instance-shell.ts) — the shared instance shell throws this from `attach` / `handleUpgrade` on Bun/Deno, for both `initDevframe` and `initHub`.

‎package.json‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@
3030
"test": "pnpm run build && vitest",
3131
"test:e2e": "pnpm run build && playwright test",
3232
"test:e2e:ui": "pnpm run build && playwright test --ui",
33+
"test:runtime": "tsx packages/devframe/test/runtime-smoke.ts",
34+
"test:runtime:bun": "bun packages/devframe/test/runtime-smoke.ts",
35+
"test:runtime:deno": "deno run -A --node-modules-dir=manual packages/devframe/test/runtime-smoke.ts",
3336
"test:ecosystem": "tsx scripts/ecosystem-ci.ts",
3437
"release": "bumpp -r",
3538
"typecheck": "pnpm run verify:typecheck-coverage && turbo run typecheck",

‎packages/devframe/src/node/diagnostics.ts‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,5 +199,15 @@ export const diagnostics = defineDiagnostics({
199199
`\`rpc.snapshot\` names "${p.method}", but no RPC function is registered under that id — nothing to bake into the static build.`,
200200
fix: 'Check the method id, and ensure the service/plugin that registers it is installed (e.g. declared in `services`) before the build collects the dump.',
201201
},
202+
DF0075: {
203+
why: (p: { runtime: string }) =>
204+
`On ${p.runtime} the shared server's WebSocket upgrade needs crossws's Node adapter, which refuses to run off Node — and SSE is disabled, so this instance advertises no RPC transport at all.`,
205+
fix: 'Keep the SSE endpoint enabled (drop `sse: false`) so clients connect over it on Bun/Deno, or move the socket to a side-car (`ws: { sidecar: true }`) which binds the native WebSocket adapter on its own port.',
206+
},
207+
DF0076: {
208+
why: (p: { runtime: string }) =>
209+
`\`attach\` / \`handleUpgrade\` drive a raw \`node:http\` upgrade into crossws's Node adapter, which refuses to run on ${p.runtime}.`,
210+
fix: 'On Bun/Deno, serve the advertised `__ws` route from `Bun.serve` / `Deno.serve` with `attachBunWsTransport` / `attachDenoWsTransport` (see the hub-deno-minimal example), or connect over the SSE endpoint instead.',
211+
},
202212
},
203213
})

0 commit comments

Comments
 (0)