RPC server and typesafe method exposure
What the RPC server does
Section titled “What the RPC server does”createRpcServer turns a Durable Object into a WebSocket RPC endpoint.
It exposes three responsibilities:
- Accept WebSocket upgrades and attach hibernation-safe subscription metadata.
- Dispatch RPC requests to methods on the DO instance and respond with results.
- Emit events to subscribed sockets when a method succeeds.
WebSocket lifecycle
Section titled “WebSocket lifecycle”When fetch() receives an upgrade request:
- A
WebSocketPairis created. - The server socket is accepted via
ctx.acceptWebSocket. - A socket attachment is initialized with
{ subscriptions: [] }.
This attachment is the source of truth for which methods each socket subscribes to.
Subscription handling
Section titled “Subscription handling”createRpcServer understands two payload shapes:
- RPC requests:
{ id, method, args }. - Subscription requests:
{ id, type: 'subscribe' | 'unsubscribe', method }.
Subscription requests mutate the socket attachment by adding/removing method names. Events only go to sockets whose attachment includes the method (or '*').
RPC request flow
Section titled “RPC request flow”When a request arrives:
- The payload is parsed and validated.
- The method is looked up in the
methodsmap. - The method is invoked and the result is returned in a response.
- An event is emitted to all other subscribers of that method.
This gives you consistent outcomes: the caller gets the response, and all listeners get the same result as a broadcast event.
Events only broadcast when the mutation runs through the RPC server. If you update storage directly in the DO without going through the RPC flow, no event is emitted.
Advanced: server-initiated events with callAndEmit
Section titled “Advanced: server-initiated events with callAndEmit”Sometimes you want to emit an event from inside the DO without a client WebSocket request (for example, a scheduled task or a DO-to-DO call). The inventory DO exposes a typesafe helper that enforces allowed methods and argument types:
async callAndEmit<M extends EmitMethodName>({ method, args,}: { method: M; args: EmitArgs<M>;}): Promise<EmitReturn<M>> { if (!inventoryRpcMethods.includes(method)) { throw new Error(`Method ${method} is not allowed for callAndEmit.`); } return this.rpcServer.callAndEmit(method, args) as EmitReturn<M>;}This keeps server-initiated events typesafe and uses the same event payload that a WebSocket caller would trigger, so subscribers stay in sync.
Typesafe method exposure
Section titled “Typesafe method exposure”In the inventory DO, the server keeps RPC types in sync by:
- Defining a list of method names (
inventoryRpcMethods). - Deriving
InventoryRpcandCurrencyRpcfrom the DO class itself viaRpcFromTarget. - Using
Pickto restrict the exposed surface to the allowed methods.
const inventoryRpcMethods = [ 'addEntry', 'updateEntry', 'transferEntry', 'transferEntries', 'reorderEntries', 'removeEntry', 'removeEntries', 'updateCurrency', 'transferCurrency', 'getEntry', 'getCurrency',] as const;
export type InventoryRpc = Pick< RpcFromTarget<InstanceType<typeof InventoryDO>, 'fetch'>, (typeof inventoryRpcMethods)[number]>;This pattern prevents drift: if a method signature changes on the DO class, the client-facing RPC type changes with it.
Recommended wiring in a DO
Section titled “Recommended wiring in a DO”this.rpcServer = createRpcServer( this as unknown as DurableObjectLike, inventoryRpcMethods);And then forward the WebSocket hooks:
async fetch(request: Request) { return this.rpcServer.fetch(request);}
async webSocketMessage(ws: WebSocket, message: string | ArrayBuffer) { await this.rpcServer.handleMessage(ws, message);}The DO implementation stays focused on domain logic, while the server handles transport and subscription routing.
Output transform chaining
Section titled “Output transform chaining”createRpcServer also accepts resultTransforms, so server output (result in responses/events)
can be serialized in one place and deserialized by the client with the same profile.
const rpcTransforms = createRpcTransforms<InventoryRpc>() .method('transferCurrency', { name: 'better-result', serialize: serializeBetterResult, deserialize: deserializeBetterResult, }) .done();
this.rpcServer = createRpcServer( this as unknown as DurableObjectLike, inventoryRpcMethods, { resultTransforms: { response: rpcTransforms, event: rpcTransforms, }, });