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.
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
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.
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.
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.