Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/bun-support.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"capnweb": patch
---

Add support for [Bun](https://bun.sh/) WebSocket servers via `newBunWebSocketRpcHandler()`.
71 changes: 70 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Cap'n Web is a spiritual sibling to [Cap'n Proto](https://capnproto.org) (and is
* That said, it integrates nicely with TypeScript.
* 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.
* It works over HTTP, WebSocket, and postMessage() out-of-the-box, with the ability to extend it to other transports easily.
* It works in all major browsers, Cloudflare Workers, Node.js, and other modern JavaScript runtimes.
* It works in all major browsers, Cloudflare Workers, Node.js, Bun, Deno, and other modern JavaScript runtimes.
The whole thing compresses (minify+gzip) to under 10kB with no dependencies.

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

### HTTP server on Bun

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()`:

```ts
import { RpcTarget, newBunWebSocketRpcHandler, newHttpBatchRpcResponse } from "capnweb";

class MyApiImpl extends RpcTarget implements MyApi {
// ... define API, same as above ...
}

// Create a WebSocket handler that manages RPC sessions automatically.
// The callback is invoked once per connection to create a fresh API instance.
let rpcHandler = newBunWebSocketRpcHandler(() => new MyApiImpl());

Bun.serve({
async fetch(req, server) {
let url = new URL(req.url);
if (url.pathname === "/api") {
// Upgrade WebSocket requests.
if (req.headers.get("upgrade")?.toLowerCase() === "websocket") {
if (server.upgrade(req)) return;
return new Response("WebSocket upgrade failed", { status: 500 });
}

// Handle HTTP batch requests.
let response = await newHttpBatchRpcResponse(req, new MyApiImpl());
response.headers.set("Access-Control-Allow-Origin", "*");
return response;
}

return new Response("Not Found", { status: 404 });
},

// Pass the handler directly — no manual wiring needed.
websocket: rpcHandler,
});
```

If you need to attach custom data to connections or add your own logic to the WebSocket handlers, use `newBunWebSocketRpcSession()` instead, which gives you direct access to the transport:

```ts
import { newBunWebSocketRpcSession, newHttpBatchRpcResponse, RpcTarget } from "capnweb";

class MyApiImpl extends RpcTarget implements MyApi {
// ... define API, same as above ...
}

Bun.serve({
fetch(req, server) {
let userId = authenticate(req);
server.upgrade(req, { data: { userId } });
},
websocket: {
open(ws) {
let { stub, transport } = newBunWebSocketRpcSession(ws, new MyApiImpl());
ws.data.transport = transport;
},
message(ws, msg) { ws.data.transport.dispatchMessage(msg); },
close(ws, code, reason) { ws.data.transport.dispatchClose(code, reason); },
error(ws, err) { ws.data.transport.dispatchError(err); },
},
});
```

### HTTP server on other runtimes

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:
Expand All @@ -654,6 +719,8 @@ function newWebSocketRpcSession(
: Disposable;
```

Note: Bun's `ServerWebSocket` does not implement the standard `WebSocket` `addEventListener` interface. If you are using Bun, use the dedicated `newBunWebSocketRpcHandler()` or `newBunWebSocketRpcSession()` described above.

### HTTP server using Hono

If your app is built on [Hono](https://hono.dev/) (on any runtime it supports), check out [`@hono/capnweb`](https://github.com/honojs/middleware/tree/main/packages/capnweb).
Expand Down Expand Up @@ -735,4 +802,6 @@ let stub: RemoteMainInterface = session.getRemoteMain();
// Now we can call methods on the stub.
```

Cap'n Web's built-in transports (WebSocket, MessagePort, HTTP batch, and Bun ServerWebSocket) are all implemented on top of this interface.

Note that sessions are entirely symmetric: neither side is defined as the "client" nor the "server". Each side can optionally expose a "main interface" to the other. In typical scenarios with a logical client and server, the server exposes a main interface but the client does not.
Loading
Loading