Loading components
Batch cursor, stroke, and movement data into ~4 writes/sec on Convex, then replay each batch with original timing for smooth realtime-looking playback.
npm install @convex-dev/batched-replayBatched Replay lets senders accumulate high-frequency values (cursors, pen strokes, game positions) and flush them as a single Convex mutation every ~250ms, rather than writing on every frame. Watchers receive each batch and replay it with the original inter-sample timing, so movement looks smooth and continuous at a fraction of the write cost. Batches are persisted, enabling full session replay with configurable retention.
The @convex-dev/batched-replay component collects cursor positions locally and flushes them as a single mutation every 250ms using useSender. Watchers use usePlayback to replay each batch with its original inter-sample timing, so movement looks smooth and live while the actual write rate stays around 4 writes per second per sender.
Use useSender to batch pointer events into the component's append endpoint, and usePlayback with fromStart: true and an onSample callback to replay every stored stroke in order before continuing live. The onAppend callback lets you mirror completed strokes into your own tables so snapshots can be loaded instead of replaying the full history each time.
The useHistory hook loads the full stored batch history for a room and plays it back at a configurable speed multiplier, cutting idle gaps longer than maxGapMs. Batches are retained until retentionMs after they end, or forever if retentionMs is omitted, so complete session replays are available as long as you need them.
The onAppend callback in @convex-dev/batched-replay fires inside the same transaction as each stored batch, giving you access to the full MutationCtx to write presence, last-seen timestamps, or other derived state into your own tables. The callback receives typed batch data including room, stream, seq, start, end, and samples.
With the default 250ms flush interval, @convex-dev/batched-replay generates about 4 append mutations per second per active sender. Nothing is written while a sender is idle. Read calls scale with senders times viewers times flush rate, so ten people in a room all moving generates roughly 400 function calls per second.
Yes. The Sender, Player, and HistoryPlayer classes are plain TypeScript and exported directly from @convex-dev/batched-replay. The React hooks useSender, usePlayback, and useHistory are available from @convex-dev/batched-replay/react and wrap these classes.
The authorizeWrite callback in exposeApi lets you control the stream key the server assigns to each caller. A common pattern is to combine the signed-in user's tokenIdentifier with a per-tab session id, ignoring whatever stream key the client requested. This prevents stream key collisions and unauthorized writes while still supporting multiple tabs per user.
Batches are kept in Convex storage until retentionMs milliseconds after their end time, then deleted by a cron job that runs each minute. If retentionMs is omitted, batches are kept indefinitely, which enables full session replay but causes storage to grow with every sample ever sent. Set retentionMs to limit storage growth for high-frequency use cases.
The @convex-dev/batched-replay README explicitly warns against writing to a shared room-level document inside onAppend, because every sender's append would contend on the same document. Instead, write per-stream or per-user documents. For room-level aggregates, the README recommends using @convex-dev/sharded-counter or @convex-dev/aggregate.