Rooms
Live cursors
Batch outbound points. Replay inbound offsets. Last position per user.
await room.attach();
room.cursors.subscribe("update", (cursor) => {
draw(cursor.userId, cursor.x, cursor.y);
});
window.addEventListener("mousemove", (event) => {
room.cursors.set({ x: event.clientX, y: event.clientY });
});
const all = room.cursors.getAll();
const me = await room.cursors.getSelf();
const others = await room.cursors.getOthers();
const page = await room.cursors.history({ limit: 50 });Outbound points batch (~100ms). Inbound replay uses offset so the remote pointer travels instead of teleporting. The Room DO skips fan-out when you are the only socket.
On connect, the DO sends the last cursor per user (snapshot: true). await history() pages the in-session ring plus the Room DO trail from GET /rooms/:id/live (cursorHistory, cap 200). Hibernated DOs start empty — this is not a durable channel.history of every past point.
useCursors() is the React hook on FluxyRoomProvider. useLiveCursors({ roomId }) is the store path. UI: Cursors / Cursor in @fluxy-chat/ui.
Presence layers (presenceKey)
Every frame above rides the default cursor layer. Pass presenceKey to publish a named layer instead — an agent pointer next to the human one, never on top of it:
sendCursor({ x, y, presenceKey: "agent", label: "assistant" });Layers are keyed userId (default) or userId#presenceKey in liveCursors and room.cursors.getAll(). getSelf(), get(userId) and useOthers() read the default layer only, so a layer can never clobber the human pointer. useLiveCursors({ roomId }).cursors returns every layer.
A bot user publishes its own layer by connecting with its token and sending the same cursor frames with presenceKey: "agent" — no extra API. Server-side agent cursors (from the Worker's own agent runtime) ride the internal announce path with the same frame shape and default presenceKey: "agent"; they are ephemeral (no snapshot, no history).