Skip to content

Commit 7cb9132

Browse files
aron-cfkentonv
andauthored
Add bun support (#159)
* Add Bun ServerWebSocket support Bun uses callback-based WebSocket handlers registered on `Bun.serve()` rather than the standard `addEventListener` interface. Passing a Bun ServerWebSocket to `newWebSocketRpcSession()` errors because the `addEventListener` method does not exist (#61). A new BunWebSocketTransport implements the RpcTransport interface using the same resolver pattern as the existing WebSocket and MessagePort transports. It exposes dispatchMessage, dispatchClose, and dispatchError methods that Bun handler callbacks invoke to feed messages into the transport. Two convenience functions sit on top of the transport. `newBunWebSocketRpcHandler()` returns a complete handler object you can pass straight to `Bun.serve()`, wiring everything up automatically via `ws.data`. `newBunWebSocketRpcSession()` returns the stub and transport for users who need custom handler logic. The Bun-specific code ships in a separate entry point (index-bun.ts) mapped via a "bun" conditional export in package.json, following the same pattern as the existing workerd build. Other runtimes never load this code. The test suite in __tests__/bun.test.ts runs under Bun native test runner against real `Bun.serve()` instances with actual WebSocket connections. Run with `npm run test:bun` or `npm run test:ci`. Closes #61 * Add Bun to the CI test workflow Adds the oven-sh/setup-bun action so the Bun runtime is available in the Playwright container. The test step now runs `test:ci` which chains Vitest and Bun tests in sequence. * Document Bun support and development setup Adds an "HTTP server on Bun" section to the readme showing both the handler helper and the manual wiring escape hatch. Notes that the Bun helpers ship via a conditional export and are never loaded by other runtimes. Adds a development section documenting the test scripts: npm test for vitest only, npm run test:bun for the Bun suite, and npm run test:ci for both together. * Hoist platform conditions so TypeScript resolves per-platform types Previously, "bun" and "workerd" were nested inside "import"/"require", which meant TypeScript always fell through to the single top-level "types" entry and picked up index.d.ts regardless of the active condition. Each platform block is now a top-level condition with its own "types", "import", and "require" keys: - "bun" resolves to index-bun.d.ts, which exposes BunWebSocketTransport, BunServerWebSocket, and the Bun-specific session helpers - "workerd" shares the same declarations as the default but keeps a distinct runtime (index-workers.js) that imports cloudflare:workers and registers it for native RPC interop - The bare "types"/"import"/"require" fallback covers Node.js and every other environment * Update README.md Remove example of custom Bun websocket transport * Add changeset --------- Co-authored-by: aron-cf <aron-cf@users.noreply.github.com> Co-authored-by: Kenton Varda <kenton@cloudflare.com>
1 parent cfa1b95 commit 7cb9132

9 files changed

Lines changed: 594 additions & 11 deletions

File tree

.changeset/popular-pears-wink.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"capnweb": minor
3+
---
4+
5+
Added support for Bun's alternative WebSocket server API.

.github/workflows/test.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,17 @@ jobs:
2424
node-version: "22"
2525
cache: "npm"
2626

27+
- name: Setup Bun
28+
run: npm install -g bun
29+
2730
- name: Install dependencies
2831
run: npm ci
2932

3033
- name: Build project
3134
run: npm run build
3235

3336
- name: Run tests
34-
run: npm test
37+
run: npm run test:ci
3538

3639
- name: Run type tests
3740
run: npm run test:types

README.md

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ Cap'n Web is a spiritual sibling to [Cap'n Proto](https://capnproto.org) (and is
66
* That said, it integrates nicely with TypeScript.
77
* Also unlike Cap'n Proto, Cap'n Web's underlying serialization is human-readable. In fact, it's just JSON, with a little pre-/post-processing.
88
* It works over HTTP, WebSocket, and postMessage() out-of-the-box, with the ability to extend it to other transports easily.
9-
* It works in all major browsers, Cloudflare Workers, Node.js, and other modern JavaScript runtimes.
9+
* It works in all major browsers, Cloudflare Workers, Node.js, Bun, Deno, and other modern JavaScript runtimes.
1010
The whole thing compresses (minify+gzip) to under 10kB with no dependencies.
1111

1212
Cap'n Web is more expressive than almost every other RPC system, because it implements an object-capability RPC model. That means it:
@@ -630,6 +630,45 @@ Deno.serve(async (req) => {
630630
});
631631
```
632632

633+
### HTTP server on Bun
634+
635+
Bun's server-side WebSocket API uses [callback-based handlers](https://bun.sh/docs/runtime/http/websockets) instead of the standard `addEventListener` interface. Cap'n Web provides `newBunWebSocketRpcHandler()` which returns a handler object you can pass directly to `Bun.serve()`.
636+
637+
```ts
638+
import { RpcTarget, newBunWebSocketRpcHandler, newHttpBatchRpcResponse } from "capnweb";
639+
640+
class MyApiImpl extends RpcTarget implements MyApi {
641+
// ... define API, same as above ...
642+
}
643+
644+
// Create a WebSocket handler that manages RPC sessions automatically.
645+
// The callback is invoked once per connection to create a fresh API instance.
646+
let rpcHandler = newBunWebSocketRpcHandler(() => new MyApiImpl());
647+
648+
Bun.serve({
649+
async fetch(req, server) {
650+
let url = new URL(req.url);
651+
if (url.pathname === "/api") {
652+
// Upgrade WebSocket requests.
653+
if (req.headers.get("upgrade")?.toLowerCase() === "websocket") {
654+
if (server.upgrade(req)) return;
655+
return new Response("WebSocket upgrade failed", { status: 500 });
656+
}
657+
658+
// Handle HTTP batch requests.
659+
let response = await newHttpBatchRpcResponse(req, new MyApiImpl());
660+
response.headers.set("Access-Control-Allow-Origin", "*");
661+
return response;
662+
}
663+
664+
return new Response("Not Found", { status: 404 });
665+
},
666+
667+
// Pass the handler directly — no manual wiring needed.
668+
websocket: rpcHandler,
669+
});
670+
```
671+
633672
### HTTP server on other runtimes
634673

635674
Every runtime does HTTP handling and WebSockets a little differently, although most modern runtimes use the standard `Request` and `Response` types from the Fetch API, as well as the standard `WebSocket` API. You should be able to use these two functions (exported by `capnweb`) to implement both HTTP batch and WebSocket handling on all platforms:

0 commit comments

Comments
 (0)