Capnweb
JavaScript/TypeScript-native, low-boilerplate, object-capability RPC system
Install / Use
/learn @cloudflare/CapnwebREADME
Cap'n Web: A JavaScript-native RPC system
Cap'n Web is a spiritual sibling to Cap'n Proto (and is created by the same author), but designed to play nice in the web stack. That means:
- Like Cap'n Proto, it is an object-capability protocol. ("Cap'n" is short for "capabilities and".) We'll get into this more below, but it's incredibly powerful.
- Unlike Cap'n Proto, Cap'n Web has no schemas. In fact, it has almost no boilerplate whatsoever. This means it works more like the JavaScript-native RPC system in Cloudflare Workers.
- 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. 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:
- Supports bidirectional calling. The client can call the server, and the server can also call the client.
- Supports passing functions by reference: If you pass a function over RPC, the recipient receives a "stub". When they call the stub, they actually make an RPC back to you, invoking the function where it was created. This is how bidirectional calling happens: the client passes a callback to the server, and then the server can call it later.
- Similarly, supports passing objects by reference: If a class extends the special marker type
RpcTarget, then instances of that class are passed by reference, with method calls calling back to the location where the object was created. - Supports promise pipelining. When you start an RPC, you get back a promise. Instead of awaiting it, you can immediately use the promise in dependent RPCs, thus performing a chain of calls in a single network round trip.
- Supports capability-based security patterns.
Installation
npm i capnweb
Example
A client looks like this:
import { newWebSocketRpcSession } from "capnweb";
// One-line setup.
let api = newWebSocketRpcSession("wss://example.com/api");
// Call a method on the server!
let result = await api.hello("World");
console.log(result);
Here's the server:
import { RpcTarget, newWorkersRpcResponse } from "capnweb";
// This is the server implementation.
class MyApiServer extends RpcTarget {
hello(name) {
return `Hello, ${name}!`
}
}
// Standard Cloudflare Workers HTTP handler.
//
// (Node and other runtimes are supported too; see below.)
export default {
fetch(request, env, ctx) {
// Parse URL for routing.
let url = new URL(request.url);
// Serve API at `/api`.
if (url.pathname === "/api") {
return newWorkersRpcResponse(request, new MyApiServer());
}
// You could serve other endpoints here...
return new Response("Not found", {status: 404});
}
}
More complicated example
Here's an example that:
- Uses TypeScript
- Sends multiple calls, where the second call depends on the result of the first, in one round trip.
We declare our interface in a shared types file:
interface PublicApi {
// Authenticate the API token, and returned the authenticated API.
authenticate(apiToken: string): AuthedApi;
// Get a given user's public profile info. (Doesn't require authentication.)
getUserProfile(userId: string): Promise<UserProfile>;
}
interface AuthedApi {
getUserId(): number;
// Get the user IDs of all the user's friends.
getFriendIds(): number[];
}
type UserProfile = {
name: string;
photoUrl: string;
}
(Note: you don't have to declare your interface separately. The client could just use import("./server").ApiServer as the type.)
On the server, we implement the interface as an RpcTarget:
import { newWorkersRpcResponse, RpcTarget } from "capnweb";
class ApiServer extends RpcTarget implements PublicApi {
// ... implement PublicApi ...
}
export default {
async fetch(req, env, ctx) {
// ... same as previous example ...
}
}
On the client, we can use it in a batch request:
import { newHttpBatchRpcSession } from "capnweb";
let api = newHttpBatchRpcSession<PublicApi>("https://example.com/api");
// Call authenticate(), but don't await it. We can use the returned promise
// to make "pipelined" calls without waiting.
let authedApi: RpcPromise<AuthedApi> = api.authenticate(apiToken);
// Make a pipelined call to get the user's ID. Again, don't await it.
let userIdPromise: RpcPromise<number> = authedApi.getUserId();
// Make another pipelined call to fetch the user's public profile, based on
// the user ID. Notice how we can use `RpcPromise<T>` in the parameters of a
// call anywhere where T is expected. The promise will be replaced with its
// resolution before delivering the call.
let profilePromise = api.getUserProfile(userIdPromise);
// Make another call to get the user's friends.
let friendsPromise = authedApi.getFriendIds();
// That only returns an array of user IDs, but we want all the profile info
// too, so use the magic .map() function to get them, too! Still one round
// trip.
let friendProfilesPromise = friendsPromise.map((id: RpcPromise<number>) => {
return { id, profile: api.getUserProfile(id) };
});
// Now await the promises. The batch is sent at this point. It's important
// to simultaneously await all promises for which you actually want the
// result. If you don't actually await a promise before the batch is sent,
// the system detects this and doesn't actually ask the server to send the
// return value back!
let [profile, friendProfiles] =
await Promise.all([profilePromise, friendProfilesPromise]);
console.log(`Hello, ${profile.name}!`);
// Note that at this point, the `api` and `authedApi` stubs no longer work,
// because the batch is done. You must start a new batch.
Alternatively, for a long-running interactive application, we can set up a persistent WebSocket connection:
import { newWebSocketRpcSession } from "capnweb";
// We declare `api` with `using` so that it'll be disposed at the end of the
// scope, which closes the connection. `using` is a fairly new JavaScript
// feature, part of the "explicit resource management" spec. Alternatively,
// we could declare `api` with `let` or `const` and make sure to call
// `api[Symbol.dispose]()` to dispose it and close the connection later.
using api = newWebSocketRpcSession<PublicApi>("wss://example.com/api");
// Usage is exactly the same, except we don't have to await all the promises
// at once.
// Authenticate and get the user ID in one round trip. Note we use `using`
// again so that `authedApi` will be disposed when we're done with it. In
// this case, it won't close the connection (since it's not the main stub),
// but disposing it does release the `AuthedApi` object on the server side.
using authedApi: RpcPromise<AuthedApi> = api.authenticate(apiToken);
let userId: number = await authedApi.getUserId();
// ... continue calling other methods, now or in the future ...
RPC Basics
Pass-by-value types
The following types can be passed over RPC (in arguments or return values), and will be passed "by value", meaning the content is serialized, producing a copy at the receiving end:
- Primitive values: strings, numbers, booleans, null, undefined
- Plain objects (e.g., from object literals)
- Arrays
bigintDateUint8ArrayErrorand its well-known subclassesReadableStreamandWritableStream, with automatic flow control.Headers,Request, andResponsefrom the Fetch API.
The following types are not supported as of this writing, but may be added in the future:
MapandSetArrayBufferand typed arrays other thanUint8ArrayRegExp
The following are intentionally NOT supported:
- Application-defined classes that do not extend
RpcTarget. - Cyclic values. Messages are serialized strictly as trees (like JSON).
RpcTarget
To export an interface over RPC, you must write a class that extends RpcTarget. Extending RpcTarget tells the RPC system: instances of this class are pass-by-reference. When an instance is passed over RPC, the object should NOT be serialized. Instead, the RPC message will contain a "stub" that points back to the original target object. Invoking this stub calls back over RPC.
When you send someone an RpcTarget reference, they will be able to call any class method over RPC, including getters. They will not, however, be able to access "own" properties. In precise JavaScript terms, they can access prototype properties but not instance properties. This policy is intended to "do the right thing" for typical JavaScript code, where private members are typically stored as instance properties.
WARNING: If you are using TypeScript, note that declaring a method private does not hide it from RPC, because TypeScript annotations are "erased" at runtime, so cannot be enforced. To actually make methods private, you must prefix their names with #, which makes them private for JavaScript (not just TypeScript). Names prefixed with # are never available over RPC.
Functions
When a plain function is passed over RPC, it will be treated similarly to an RpcTarget. The function will be replaced by a stub which, when invoked, calls back over RPC to the original function object.
If the function has any own properties, those will be available over RPC. Note that this differs from RpcTarget: With RpcTarget, own properties are not exposed, but with functions, only own properties are exposed. Generally functions don't have properties
Related Skills
node-connect
349.0kDiagnose OpenClaw node connection and pairing failures for Android, iOS, and macOS companion apps
frontend-design
109.4kCreate distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
openai-whisper-api
349.0kTranscribe audio via OpenAI Audio Transcriptions API (Whisper).
qqbot-media
349.0kQQBot 富媒体收发能力。使用 <qqmedia> 标签,系统根据文件扩展名自动识别类型(图片/语音/视频/文件)。
