Client-side DO client and subscriptions
What this client does
Section titled “What this client does”The @repo/client-side-do client gives you two layers:
createDOClientowns the WebSocket connection, queueing, retries, and event fan-out.createSubscriptionClientis a thin wrapper that instantiates the client and registers per-method handlers.
Think of the client as a reliable WebSocket RPC pipe with an opt-in event stream.
Connection lifecycle mental model
Section titled “Connection lifecycle mental model”createDOClient maintains a single socket per Durable Object URL and handles three state buckets:
- Pending requests: calls are stored in
pendingwith a resolve/reject pair. - Queued payloads: if the socket is not open, requests (including subscribe/unsubscribe) go into
queueup tomaxQueueSize. - Subscriptions: a
subscribersmap of method name -> handlers, used to fan out events.
On reconnect, the client:
- Re-sends pending payloads that were already sent before the disconnect.
- Re-subscribes to all methods in
subscribers. - Fires
onReconnecthandlers so you can re-bootstrap UI if needed.
This gives you a stable mental model: call RPC methods freely, and the client will deliver them once the socket is ready.
Subscription flow
Section titled “Subscription flow”Each handler you register through subscribe is wired to a specific RPC method name. Event flow looks like this:
subscribe(method, handler)sends a{ type: 'subscribe', method }payload.- The server stores the subscription on the socket attachment (see server doc).
- When a server emits an event, the client dispatches it to all handlers for that method.
- When the last handler is removed,
unsubscribeis sent to the server.
The same approach works for subscribeAll, which uses method '*' to receive all events.
How createSubscriptionClient fits in
Section titled “How createSubscriptionClient fits in”createSubscriptionClient returns a helper that builds the DO client and subscribes your handlers in one place:
const buildSubscriptionClient = createSubscriptionClient<InventoryRpc>({ baseUrl: buildInventoryWsUrl(inventoryId),});
const doClient = buildSubscriptionClient(inventoryId, { updateEntry: ({ result }) => { // update local collection },});You also get:
unsubscribe()to clean up handlers.onStatusChange,onRetry,onReconnectfor UI status or diagnostics.
Output transform chaining
Section titled “Output transform chaining”You can define RPC result transforms once and pass the same transform profile to both
createDOClient and createRpcServer. This is useful when you need custom serialization for
response/event results (for example, better-result values).
const rpcTransforms = createRpcTransforms<InventoryRpc>() .global({ name: 'better-result', serialize: serializeBetterResult, deserialize: deserializeBetterResult, }) .done();
const buildClient = createDOClient<InventoryRpc>({ baseUrl: buildInventoryWsUrl(inventoryId), resultTransforms: rpcTransforms,});Example: inventory collection wiring
Section titled “Example: inventory collection wiring”In the inventory collection setup, the client wiring typically looks like this:
buildSubscriptionClientbuilds acreateSubscriptionClientinstance with a WebSocket URL per inventory.- A
clientsByIdmap keeps one client per inventory DO. - The collection uses
createSubscriptionHandlersto transform incoming events into local collection changes.
The result: local state stays in sync with DO updates, with the client handling connection recovery under the hood.