Cap'n Web

repository·main·Indexed 26 days ago

https://github.com/cloudflare/capnweb

A JavaScript/TypeScript-native RPC library designed for the web stack. It implements an object-capability model supporting bidirectional calling, passing functions and objects by reference, and promise pipelining to minimize network round trips. The ecosystem includes capnweb-validate for build-time and runtime validation of RPC services using decorators like @validateRpc() and functions like validateStub<T>().

Tokens
19.6K
Snippets
32
Records
112
Agent score
85%

What's inside capnweb

  1. Overview of the Worker-React example architecture

    main

    This example demonstrates a full-stack Cap'n Web implementation:

    • Server: A Cloudflare Worker exposing a Cap'n Web API at the /api endpoint (server/worker.ts). It uses @validateRpc() for server-boundary runtime validation.
    • Client: A React/Vite application (client/) that uses Cap'n Web client sessions wrapped with validateStub() for explicit client stub validation.
    • Validation: wrangler.jsonc is configured to run capnweb-validate build before starting. Worker validation output is located at .wrangler/validate/worker.ts.
  2. Explore Cap'n Web usage patterns via examples

    main

    The repository includes two primary examples demonstrating different deployment environments and advanced RPC patterns:

    1. batch-pipelining: Demonstrates a Node.js server and client setup. It focuses on batching and pipelining, showing how to execute a dependent sequence of RPC calls in a single HTTP round trip to improve performance compared to sequential calls.
    2. worker-react: Demonstrates a Cloudflare Worker backend paired with a React frontend. This shows how to implement the same RPC patterns from a browser-based application served by a Worker.
  3. Implement an HTTP server on Cloudflare Workers

    main

    Use newWorkersRpcResponse(request, apiImplementation) to create a Cloudflare Worker handler that supports both HTTP batch and WebSocket APIs simultaneously.

    Compatibility Note: Cap'n Web is compatible with Workers' built-in RPC. For best results, set your Workers compatibility date to 2026-01-20 or enable the rpc_params_dup_stubs flag.

    import { RpcTarget, newWorkersRpcResponse } from "capnweb";
    
    class MyApiImpl extends RpcTarget implements MyApi {
      getUserInfo(): UserInfo { return this.userInfo; }
      greet(name: string): string { return `Hello, ${name}!`; }
    };
    
    export default {
      fetch(request: Request, env, ctx) {
        let url = new URL(request.url);
        if (url.pathname === "/api") {
          return newWorkersRpcResponse(request, new MyApiImpl());
        }
        return new Response("Not found", {status: 404});
      }
    }
  4. Configure server-side RPC validation with @validateRpc()

    main

    Apply the @validateRpc() decorator to your service class (e.g., classes extending RpcTarget, WorkerEntrypoint, or DurableObject). This injects validators generated from your TypeScript types.

    By default, the RPC surface includes all public string-named methods and RPC-readable getters/properties. To restrict the RPC surface to a specific interface, use @validateRpc<SomeInterface>().

    import { newWorkersRpcResponse, RpcTarget } from "capnweb";
    import { validateRpc } from "capnweb-validate";
    
    type User = { id: string; name: string };
    
    @validateRpc()
    export class Api extends RpcTarget {
      async authenticate(sessionToken: string): Promise<User> {
        // ...
      }
    }
    
    export default {
      async fetch(request: Request, env: Env) {
        return newWorkersRpcResponse(request, new Api());
      },
    };
  5. Manage RPC resource disposal

    main

    Because garbage collection cannot trace across RPC connections, you must explicitly manage the lifecycle of stubs to prevent memory leaks on the remote end.

    Strategies

    1. Explicit Disposal: Use JavaScript's explicit resource management (using keyword or [Symbol.dispose]()) to notify the remote end that a stub is no longer needed.
    2. Short-lived Sessions: Use HTTP batch requests where stubs are implicitly disposed when the session ends. For long-lived WebSocket sessions, explicit disposal is highly recommended.

    Ownership Rules

    • The caller is responsible for disposing all stubs.
    • Stubs in parameters: Remain the property of the caller. The RPC system implicitly disposes the callee's duplicates when the call completes.
    • Stubs in results: Ownership transfers from the callee to the caller. The caller must dispose them. The RPC system disposes the callee's duplicates after the call completes and pipelining is finished.
    • Return values: If an RPC returns an object, it will always have a disposer. It is best practice to always dispose return values to ensure any future stubs added to the API are also cleaned up.
  6. Use HTTP Batching for pipelined RPC calls

    main
    To perform multiple dependent RPC calls in a single network round trip, use newHttpBatchRpcSession. Instead of awaiting every call immediately, you can treat the returned values as RpcPromise<T>. This allows you to chain calls (pipelining) where the input of one call is the promise of another. The batch is sent when you await the promises (e.g., via Promise.all).
  7. Stream data with flow control

    main

    You can pass ReadableStream or WritableStream objects over RPC. Cap'n Web automatically handles the following:

    • Automatic conversion: Creates equivalent streams at the receiving end.
    • Flow control: Uses the bandwidth-delay product to apply backpressure, ensuring high utilization while minimizing buffer bloat.
    • Multiplexing: Multiple streams can be sent over the same connection via multiplexing (similar to HTTP/2).
  8. Configure bundler plugins for capnweb-validate

    main

    Use the appropriate plugin for your bundler to transform modules in memory. Supported plugins include Vite, Rollup, Webpack, Rspack, Esbuild, and Farm.

    import capnwebValidate from "capnweb-validate/vite";     // or
    import capnwebValidate from "capnweb-validate/rollup";    // or
    import capnwebValidate from "capnweb-validate/webpack";   // or
    import capnwebValidate from "capnweb-validate/rspack";   // or
    import capnwebValidate from "capnweb-validate/esbuild";   // or
    import capnwebValidate from "capnweb-validate/farm";
    
    export default {
      plugins: [capnwebValidate()],
    };
  9. Implement RPC over MessagePort

    main

    Cap'n Web can communicate over MessagePort (e.g., between a browser window and a Web Worker).

    1. Create a MessageChannel.
    2. Initialize the server using newMessagePortRpcSession(port1, apiImplementation).
    3. Initialize the client using newMessagePortRpcSession<T>(port2).

    Security Note: Do not use a Window object directly as a port. Always create a MessageChannel and transfer one of its ports via postMessage() to ensure the sender is authenticated.

    import { RpcTarget, RpcStub, newMessagePortRpcSession } from "capnweb";
    
    class Greeter extends RpcTarget {
      greet(name: string): string { return `Hello, ${name}!`; }
    };
    
    let channel = new MessageChannel();
    newMessagePortRpcSession(channel.port1, new Greeter());
    using stub: RpcStub<Greeter> = newMessagePortRpcSession<Greeter>(channel.port2);
    
    console.log(await stub.greet("Alice"));
  10. Validate generic service classes

    main

    Because decorators are emitted at the class declaration, they cannot specialize validators for different type arguments in generic classes.

    1. Known types: Use an explicit interface at the decorator site: @validateRpc<Gatekeeper<GmailSession, number, undefined>>().
    2. Unconstrained types: If you use @validateRpc() on a class like ArrayCursor<T>, T defaults to any (with a warning). To validate against a constraint, use <T extends Session>.
    3. Silencing warnings: Use @validateRpc<Cursor<any>>() to keep positions permissive without warnings.
    @validateRpc<Gatekeeper<GmailSession, number, undefined>>()
    class GmailGatekeeper
      extends RpcTarget
      implements Gatekeeper<GmailSession, number, undefined> {
      // ...
    }
  11. Use the HTTP batch client with Promise Pipelining

    main

    The HTTP batch client allows multiple RPC calls to be sent in a single HTTP request. It supports Promise Pipelining: you can pass a Promise returned by one call as a parameter to another call in the same batch. The system automatically replaces the Promise with its resolution on the server side before execution, reducing round trips.

    To use it, call newHttpBatchRpcSession<T>(url) where T is your interface. Note that the batch is sent on the next I/O tick, so you must ensure all promises are explicitly awaited (or .then() called) before the tick occurs to ensure their results are returned.

    import { RpcTarget, RpcStub, newHttpBatchRpcSession } from "capnweb";
    
    interface MyApi extends RpcTarget {
      getUserInfo(): UserInfo;
      greet(name: string): string;
    };
    
    using stub: RpcStub<MyApi> = newHttpBatchRpcSession<MyApi>("https://example.com/api");
    
    let promise1 = stub.greet("Alice");
    let userInfoPromise = stub.getUserInfo();
    // Promise Pipelining: userInfoPromise.name is substituted on the server
    let promise3 = stub.greet(userInfoPromise.name);
    
    let [greeting1, greeting3] = await Promise.all([promise1, promise3]);