一键导入
live-cursors
Live cursors and multiplayer presence with Rivet Actors: per-connection cursor state, realtime updates over events or raw WebSockets, and throttling.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Live cursors and multiplayer presence with Rivet Actors: per-connection cursor state, realtime updates over events or raw WebSockets, and throttling.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Build an AI agent backend with persistent memory: one Rivet Actor per conversation, queued message handling, and streaming LLM responses as realtime events.
Build a realtime chat room backend with Rivet Actors: one actor per room, SQLite-backed message history, and WebSocket broadcast to every connected client.
Build a collaborative text editor backend with Yjs CRDTs and Rivet Actors: per-document actors relay sync and awareness updates and persist snapshots.
Durable cron jobs with Rivet Actors: schedule.after and schedule.at timers survive restarts and crashes, plus re-arming recurring jobs and idempotent handlers.
Pragmatic patterns for building multiplayer games: matchmaking, tick loops, realtime state, interest management, and validation.
Multi-tenant data isolation with one Rivet Actor per tenant: the actor key is the tenant id, so each tenant gets its own isolated dataset and migrations.
| name | live-cursors |
| description | Live cursors and multiplayer presence with Rivet Actors: per-connection cursor state, realtime updates over events or raw WebSockets, and throttling. |
IMPORTANT: Before doing anything, you MUST read BASE_SKILL.md in this skill's directory. It contains essential guidance on debugging, error handling, state management, deployment, and project setup. Those rules and patterns apply to all RivetKit work. Everything below assumes you have already read and understood it.
If you need a reference implementation, read the raw working example code in these templates:
Patterns for building live cursors, multiplayer presence, and realtime cursor sharing with RivetKit. One room actor fans cursor positions out to every connected client, keyed per room with actor keys.
Start with one of the two working variants on GitHub. Both implement the same collaborative cursor canvas with persistent text labels; they differ only in transport.
| Variant | Starter Code | Transport | Presence Storage |
|---|---|---|---|
cursors | GitHub | Typed actions and events over the RivetKit connection | connState per connection |
cursors-raw-websocket | GitHub | Raw onWebSocket handler with a custom JSON message protocol | Socket map in createVars |
Use cursors by default: typed actions, typed events, and automatic connection tracking cover most apps with less code. Use cursors-raw-websocket when you need full control of the wire format, for example a custom JSON or binary protocol, or clients that do not use the RivetKit client library.
Presence is ephemeral by definition. A cursor position is only meaningful while its connection is alive, so it belongs in per-connection storage, not in persistent actor state. Persistent state is reserved for data that must survive disconnects and actor restarts.
| Data | Where It Lives | Why |
|---|---|---|
| Cursor position | connState (cursors) or the createVars socket map (cursors-raw-websocket) | Scoped to one connection and discarded with it. Stale presence cannot accumulate in storage. |
Text labels (textLabels) | Persistent actor state in both variants | Canvas content must survive disconnects and actor restarts. |
In the cursors variant, updateCursor writes c.conn.state.cursor and getRoomState rebuilds the presence snapshot by iterating c.conns.values(), so the cursor map is always derived from live connections rather than stored. See Connections for connState and State for persistence semantics.
cursors-raw-websocket variant pushes an init message with the current { cursors, textLabels } snapshot as soon as a socket connects. The cursors variant has no explicit join broadcast; the client calls the getRoomState action once after connecting to seed its local maps, and peers first see a new user on that user's first cursorMoved broadcast.updateCursor call writes the connection's presence entry, then broadcasts cursorMoved to all connections, including the sender.cursors variant handles leave in onDisconnect, broadcasting cursorRemoved with the connection's last cursor. The raw variant does the same from the socket close listener, then deletes the session from the vars.websockets map. Clients delete that user from their local cursor map, so stale cursors disappear the moment a tab closes.See Lifecycle for onDisconnect and createVars.
Neither example throttles. Both frontends send a cursor update on every raw mousemove event with no debounce or interval cap. That is fine for a demo, but a fast mouse on a high-refresh display can emit hundreds of events per second per user. The patterns below are recommended production hardening on top of the starter code, not something the examples implement.
| Layer | Pattern | Guidance |
|---|---|---|
| Client (smoothness) | Throttle to 20-30Hz | Sample the latest pointer position every 33-50ms and send only that. Drop intermediate moves, but always flush the final position so cursors settle at the true location. Interpolate between received positions on the rendering side. |
| Server (enforcement) | Per-connection rate limit | Track the last accepted update timestamp per connection and drop or coalesce updates arriving faster than your cap. Client throttles are cooperative; the actor is the enforcement boundary. |
Key: cursorRoom[roomId] (the frontend defaults roomId to "general")
Responsibility: Holds per-connection cursor presence in connState, persists shared text labels in actor state, and broadcasts cursor and text updates to all connections.
Actions
updateCursorupdateTextremoveTextgetRoomStateEvents
cursorMovedcursorRemovedtextUpdatedtextRemovedQueues
State
textLabels (persistent)connState.cursor per connection (ephemeral)Key: cursorRoom[roomId] (resolved via client.cursorRoom.getOrCreate(roomId))
Responsibility: Exposes a raw WebSocket endpoint, tracks live sockets and their cursors in a createVars map keyed by a sessionId query parameter, persists text labels, and manually fans JSON frames out to every socket.
Actions
getOrCreate (stub returning { status: "ok" }; the frontend resolves the actor ID with the client handle's getOrCreate(roomId).resolve(), which creates the actor without dispatching this action)getRoomStateQueues
State
textLabels (persistent)vars.websockets map of sessionId to socket and cursor (in-memory, lost on restart)The raw variant defines no RivetKit events. Its message names are type fields on raw JSON frames:
| Direction | Message type | Payload |
|---|---|---|
| Client to server | updateCursor | { userId, x, y } |
| Client to server | updateText | { id, userId, text, x, y } |
| Client to server | removeText | { id } |
| Server to client | init | { cursors, textLabels } snapshot on connect |
| Server to client | cursorMoved, textUpdated, textRemoved, cursorRemoved | The corresponding cursor, label, or ID payload |
sequenceDiagram
participant A as Client A
participant R as cursorRoom
participant B as Other Clients
A->>R: connect via useActor (cursorRoom[roomId])
A->>R: getRoomState()
R-->>A: {cursors, textLabels}
loop every mouse move
A->>R: updateCursor(userId, x, y)
Note over R: write c.conn.state.cursor
R-->>B: cursorMoved (broadcast)
end
A->>R: updateText(id, userId, text, x, y)
Note over R: upsert persistent state.textLabels
R-->>B: textUpdated (broadcast)
Note over A: tab closes
Note over R: onDisconnect reads conn.state.cursor
R-->>B: cursorRemoved (broadcast)
sequenceDiagram
participant A as Client A
participant R as cursorRoom
participant B as Other Clients
A->>R: getOrCreate(roomId).resolve()
R-->>A: actorId
A->>R: open WebSocket /gateway/{actorId}/websocket?sessionId=...
Note over R: close 1008 if sessionId is missing
Note over R: store socket in vars.websockets
R-->>A: init {cursors, textLabels}
loop every mouse move
A->>R: {type: "updateCursor"} frame
Note over R: update session cursor in vars
R-->>B: cursorMoved frame
end
Note over A: socket closes
R-->>B: cursorRemoved frame
Note over R: delete session from vars.websockets
Both examples ship without authentication so the presence pattern stays readable. Everything below is recommended hardening for production, not behavior the examples implement.
c.conn.id in the actions variant, a server-generated session ID in the raw variant). Never trust a client-supplied userId; in the examples it is a random client-generated string, so any client can impersonate or remove any cursor.updateText accepts arbitrary id and userId arguments and removeText accepts an arbitrary id, so any client can edit or delete any label.x and y to canvas bounds, cap text label length, and cap the total textLabels count so persistent state cannot grow unbounded.updateCursor (for example 30Hz) and on label writes, as described in Update Throttling.sessionId values rather than silently overwriting another session's socket entry.