Build · Client integration

Delivery, replay, and cursors

Follow an event from commit to callback, then learn what survives a connection loss or a client restart.

RMC delivers committed room history through an authorized selection. Replay and live delivery are the same subscription: read after a sequence, deliver eligible records, and continue when the room commits more.

For focused definitions, open events, publishers, subscriptions, or cursors and checkpoints.

The client publishes with a stable client event ID. RMC rechecks current authority and idempotency, then SQLite commits the ordered event. Subscription delivery reads authorized committed history.
A commit precedes delivery. External application effects remain a separate boundary.Open SVG ↗Excalidraw source ↓

Commit is the durable boundary

The publication enters a per-room command queue. The runtime reloads the participant's authority when processing it, checks idempotency, and commits the event to SQLite. Only then does it wake readers. A slow subscription does not hold that publication queue while its application callback runs.

This gives you ordering within a room and a durable source for reconnect. It does not make an external API call and a consumer checkpoint one transaction. If your handler charges a card or sends an email, that destination needs its own idempotency policy.

The boundary is implemented in publication.go, runtime.go, and subscription.go. Subscription tests cover slow readers, replay ordering, and revocation.

Select the events you need

TypeScript
const subscription = room.subscribe({
  selector: {
    channelIds: ["chat", "support"],
    channelTypes: ["text"],
    eventTypes: ["text.message.*"],
  },
  afterSeq: 0,
  onEvent: async (event) => {
    await saveMessage(event); // Your application's ordered handler.
  },
});

Values within one dimension combine with OR. Dimensions combine with AND. The event must also satisfy current authorization and visibility. Empty dimensions impose no restriction.

Dimension Matching rule
channelIds Exact channel IDs
channelTypes Exact types resolved from the room's channel definitions
eventTypes Exact types or a trailing * prefix, such as delegation.*; * matches all

Selectors are copied when the SDK creates the subscription. To change a filter, close the subscription and create another. Reusing an old cursor with a broader filter skips newly eligible history before that cursor; choose the replay policy deliberately. See selector validation.

Cursor means processed position

afterSeq is exclusive. A subscription starting at afterSeq: 42 scans room records after 42. Its cursor advances only after its event or checkpoint callback completes successfully.

The cursor can exceed the last event you saw. The runtime must advance past records hidden by a selector or authorization; it sends an ordered checkpoint describing how far it scanned.

The room contains chat at sequence 41, filtered vision at 42, hidden private data at 43, chat at 44, and filtered work at 45. The consumer processes events 41 and 44, then checkpoint 45. It can resume after 45 without rescanning hidden history.
A checkpoint advances through scanned history after earlier visible events are processed.Open SVG ↗Excalidraw source ↓

In the diagram, a chat consumer receives events 41 and 44. Records 42, 43, and 45 do not match its authorized selection. After processing event 44, it can process checkpoint 45. Resuming after 45 avoids repeatedly scanning that hidden tail. A missing sequence in your feed is normal, not evidence that delivery lost an event.

TypeScript
room.subscribe({
  selector: {channelIds: ["chat"]},
  afterSeq: savedCursor,
  onEvent: async (event) => {
    await projection.applyOnce(event.id, event.payload);
  },
  onCheckpoint: async (cursor) => {
    await projection.saveCursor(cursor);
  },
});

projection and savedCursor are application-owned in this fragment. applyOnce illustrates the required duplicate policy; the SDK does not provide a projection store. The callback queue ensures that checkpoint persistence cannot overtake an earlier event handler. It does not make those two application operations atomic. See the restart-safe projection recipe.

Multiplexing shares transport, not progress

Each RoomClient opens its WebSocket lazily when the first subscription starts. Closing its final subscription closes the socket. A chat component and a work panel can share the room handle while keeping distinct selectors, callback queues, cursors, and lifetimes.

The SDK serializes async handlers per subscription. Another subscription can make progress while one handler waits. Each SDK subscription has a bounded pending queue of 128 frames; overflow closes that subscription at its last successful cursor. A slow network connection can still affect all subscriptions sharing that socket. Independent queues do not imply unlimited buffering or transport isolation.

Recover at the right level

Situation Current SDK behavior Your response
Transport drops Reconnects with exponential backoff, starting at 250 ms and capped at 10 s; resubscribes from each processed cursor Keep handles; expose connection state if useful
Application handler rejects Closes that subscription and calls onError Fix the failure, then explicitly resubscribe from subscription.cursor
Pending queue overflows Closes that subscription; committed events remain in the log Reduce callback work or persist a small projection quickly, then replay
Participant revoked / policy close Ends access; no automatic reauthentication with a replacement token Return to your application's authorization flow
Page or process restarts In-memory handles and cursors are gone Restore application-persisted progress or rebuild from history
AbortSignal aborts Closes that subscription Use a new subscription for a new scope

onError can also report a transient connection problem, so it does not by itself mean that the subscription closed. Inspect closed or onStateChange. The states are connecting, authenticating, live, reconnecting, and closed.

live means the server acknowledged the subscription, not that replay is caught up. A checkpoint at or beyond a known target sequence is a better synchronization point when your application needs one. The SDK suppresses duplicate event sequences at or below its processed cursor. A callback already running when a disconnect happens can still finish; disconnect does not roll back its side effects.

Read stream.ts for the exact client behavior and its tests for failure and reconnect scenarios.

What is not promised

There is no cross-room total order, consumer-group work distribution, server-persisted named consumer, or exactly-once side-effect transaction. A subscription ID identifies a handle; it is not a durable checkpoint name. Keep your projection, cursor, participant identity, and selector definition together when progress must survive restart.

Search the documentation

Type to search all guides.

Diagram

100%Open original ↗