# How Capnweb RPC Handles Promise Pipelining and Object Capabilities in Cloudflare Computer

> Discover how Capnweb RPC leverages promise pipelining and object capabilities in Cloudflare Computer for efficient, chained remote operations via typed JavaScript Proxies.

- Repository: [Cloudflare/computer](https://github.com/cloudflare/computer)
- Tags: internals
- Published: 2026-08-13

---

**Capnweb RPC implements promise pipelining and object capabilities in Cloudflare Computer by exposing WebSocket connections as disposable, typed JavaScript Proxies (`RpcStub`) that queue method calls into pipelined promises, allowing callers to chain remote operations without awaiting intermediate network round-trips.**

Cloudflare Computer uses **capnweb** as the RPC framing layer between a Durable Object (the host) and the in-container `computerd` process. The architecture treats remote interfaces as **object capabilities** represented by typed stubs, enabling fine-grained permission control and automatic resource cleanup while maintaining Cap’n Proto–style semantics over WebSocket or HTTP-batch transports.

## Object-Capability Stubs and the Proxy Pattern

Capnweb models remote services as **capability objects** that grant the holder the right to invoke specific methods. When a client establishes a session, capnweb constructs a root stub implementing the `WorkspaceRPC` interface—a composite of `SyncRPC` and `ShellRPC`—and returns it as a fully typed JavaScript `Proxy`.

### Creating Typed Capabilities with `RpcStub`

When you call `newWebSocketRpcSession()` (or `nodeHttpBatchRpcResponse`), the library builds a **root stub** that intercepts every property access and forwards it to the remote side. Because the stub is a `Proxy` implementing the exact shape of the remote interface, TypeScript can provide fully typed views through `createSyncClient` and `createWorkspaceClient` in [`packages/rpc/src/client.ts`](https://github.com/cloudflare/computer/blob/main/packages/rpc/src/client.ts) (lines 47–48).

```typescript
// packages/rpc/src/client.ts
const stub = newWebSocketRpcSession(ws as unknown as globalThis.WebSocket)
             as RpcStub<SyncRPC>;

```

Possession of the stub constitutes the capability; no other code can fabricate one because the stub’s prototype is hidden behind the proxy. This pattern enforces **capability-based security**: you can only invoke methods if you hold the reference.

### Capability Disposal and Resource Cleanup

Stubs are **disposable** resources. Calling `Symbol.dispose` (or the `close()` helper) tears down the underlying session, sends a clean abort frame to the remote side, and releases the capability, preventing further calls. The implementation in [`packages/rpc/src/client.ts`](https://github.com/cloudflare/computer/blob/main/packages/rpc/src/client.ts) (lines 55–62) invokes dispose on the root stub before closing the WebSocket:

```typescript
(target as unknown as Disposable)[Symbol.dispose]?.();

```

This explicit lifecycle management allows the remote `computerd` process to free associated resources immediately rather than waiting for TCP timeouts.

## Promise Pipelining for Low-Latency RPC

Capnweb’s RPC model supports **pipelined promises**: a property read returns an `RpcPromise` (or another `RpcStub`) immediately, without waiting for the remote call to finish. This enables call-chaining where subsequent operations are queued and sent in a single frame once the network is ready.

### The `RpcPromise` Proxy Chain

When you access a property on the stub (e.g., `.sync.push`), the proxy records the *path* and returns a thenable `RpcPromise`. If you chain further property accesses, the proxy accumulates the complete path and sends the serialized request in one frame. The remote side resolves each step in order, preserving call sequence while eliminating round-trip latency for intermediate steps.

```typescript
// Pipelined: push() and property access queued without intermediate await
const pushResult = await client.sync.push({ senderRev, changes });
const water = await client.sync.watermarks();

```

### Intercepting Calls for Observability

The client wrapper intercepts method calls, records start times, and attaches `.then`/`.catch` handlers to emit `onRPCEvent` telemetry. This logic resides in the `get` handler of the proxy in [`packages/rpc/src/client.ts`](https://github.com/cloudflare/computer/blob/main/packages/rpc/src/client.ts) (lines 54–88):

```typescript
const result = (value as (...a: unknown[]) => unknown)(...args);
if (result && typeof (result as { then?: unknown }).then === "function") {
    return (result as Promise<unknown>).then(
        v => { onEvent(...); return v; },
        err => { onEvent(...); throw err; }
    );
}

```

Because `RpcPromise` is already a thenable, downstream code can keep chaining calls without awaiting each individual RPC, achieving the classic promise-pipelining pattern used by Cap’n Proto’s RPC system.

## Managing the Capability Lifecycle

### Acquisition via Session Establishment

A client obtains a capability by establishing a capnweb session through `newWebSocketRpcSession` or `nodeHttpBatchRpcResponse`. The returned `RpcStub` is the sole holder of the capability. The library multiplexes concurrent RPCs over the single WebSocket, with RPC identifiers generated internally by capnweb.

### Automatic Re-establishment on Disconnect

If the underlying WebSocket disconnects, the Durable Object discards the old stub and lazily creates a new one on the next RPC. The capability is recreated automatically, preserving the same interface shape for callers while ensuring that stale capabilities cannot be used after transport failure.

## Wire Contract and Interface Types

The wire contract is defined in [`packages/rpc/src/interface.ts`](https://github.com/cloudflare/computer/blob/main/packages/rpc/src/interface.ts). It declares the two halves of the `WorkspaceRPC` interface:

```typescript
export interface WorkspaceRPC {
  sync: SyncRPC;
  shell: ShellRPC;
}

```

- **`SyncRPC`** methods (`push`, `fetchChanges`, `hasObjects`, `pushObjects`) return **Promises** or `ReadableStream`s that are pipelined through the stub.
- **`ShellRPC`** methods (`exec`, `getExec`, `killExec`, `disposeExec`) return promises resolving to `ReadableStream<ExecEvent>`, treating streams as first-class capnweb values that propagate back-pressure from the consumer to the spawned process.

## Code Examples

Below are practical usage patterns demonstrating capability acquisition, promise pipelining, and proper disposal.

```typescript
// ==== Creating a Sync client (raw sync half) ====
import { createSyncClient } from "@cloudflare/computer/rpc";

const client = createSyncClient({
  url: "ws://localhost:45678/ws",
  onRPCEvent: ev => console.log(ev),   // optional observability
});

// Push a batch of changes (pipelined)
await client.sync.push({
  senderRev: 42,
  changes: changeStream,               // ReadableStream<ChangeEntry>
});

// Fetch changes after a known cursor
const { currentCursor, stream } = await client.sync.fetchChanges({
  after: { rev: 100, path: null },
});
for await (const entry of stream) {
  console.log(entry);
}

// ==== Using the composite Workspace client ====
import { createWorkspaceClient } from "@cloudflare/computer/rpc";

const wsClient = createWorkspaceClient({ url: "ws://localhost:45678/ws" });

// Pipeline a push and then read a watermark in a single async flow
const pushResult = await wsClient.sync.push({ senderRev: 0, changes: changeStream });
const water = await wsClient.sync.watermarks();
console.log(`rev ${pushResult.rev}, current ${water.currentRev}`);

// Execute a command and stream its output
const { id, events } = await wsClient.shell.exec({ source: "ls -l /data" });
for await (const ev of events) {
  if (ev.name === "stdout") console.log(new TextDecoder().decode(ev.value));
}
await wsClient.shell.disposeExec({ id });

```

## Summary

- **Object-capability stubs** are implemented as JavaScript Proxies in [`packages/rpc/src/client.ts`](https://github.com/cloudflare/computer/blob/main/packages/rpc/src/client.ts), where `newWebSocketRpcSession` returns a typed `RpcStub<WorkspaceRPC>` that grants access to remote methods.
- **Promise pipelining** allows callers to chain property accesses and method calls (e.g., `client.sync.push().then(...)`) without awaiting intermediate network round-trips, with the proxy recording paths and batching serialized requests.
- **Explicit disposal** via `Symbol.dispose` (invoked through `close()`) sends clean abort frames to the remote side, enabling immediate resource cleanup in the `computerd` process.
- **Automatic re-establishment** ensures that disconnected WebSockets result in fresh capability stubs on the next call, maintaining interface continuity while preventing use of stale references.

## Frequently Asked Questions

### What is an object capability in Capnweb RPC?

An object capability is a typed JavaScript Proxy (`RpcStub`) that represents the right to invoke methods on a remote interface. Possession of the stub—returned by `newWebSocketRpcSession` or `nodeHttpBatchRpcResponse`—grants the holder permission to call any method defined on `WorkspaceRPC`, `SyncRPC`, or `ShellRPC`. The stub cannot be fabricated by other code, enforcing security through reference availability rather than access control lists.

### How does promise pipelining reduce latency?

Promise pipelining reduces latency by allowing callers to queue multiple method calls and property traversals without waiting for each individual network round-trip. When you access `client.sync.push`, the proxy returns an `RpcPromise` immediately and records the path; subsequent operations are batched into a single frame. The remote side processes the pipeline in order, returning results for all queued calls at once, effectively collapsing nested round-trips into one.

### How do you properly close a Capnweb RPC session?

Invoke `Symbol.dispose` on the root stub before closing the underlying WebSocket. The `createSyncClient` and `createWorkspaceClient` adapters provide a `close()` helper that performs this cleanup (see [`packages/rpc/src/client.ts`](https://github.com/cloudflare/computer/blob/main/packages/rpc/src/client.ts), lines 55–62). This sends a clean abort frame to the remote side, signaling the `computerd` process to release resources associated with that capability.

### What happens when the WebSocket disconnects?

When the WebSocket disconnects, the Durable Object discards the stale `RpcStub`. On the next RPC attempt, the system lazily establishes a new capnweb session and returns a fresh stub implementing the same interface. This re-establishment is transparent to calling code, which continues to interact with the new capability as if it were the original, ensuring resilience without manual reconnection logic.