live-cursors

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Live Cursors and Presence

实时光标与在线状态

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.
重要提示:在进行任何操作之前,你必须阅读本技能目录下的
BASE_SKILL.md
。其中包含调试、错误处理、状态管理、部署和项目设置的关键指导。这些规则和模式适用于所有RivetKit工作。以下所有内容均假设你已阅读并理解该文档。

Working Examples

示例实现

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.
如果你需要参考实现,请查看以下模板中的完整示例代码:
使用RivetKit构建实时光标、多人在线状态和实时光标共享的模式。一个房间Actor会将光标位置分发给所有连接的客户端,通过actor keys按房间进行区分。

Starter Code

起始代码

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.
VariantStarter CodeTransportPresence Storage
cursors
GitHubTyped actions and events over the RivetKit connection
connState
per connection
cursors-raw-websocket
GitHubRaw
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.
从GitHub上的两个可用变体开始。两者都实现了带有持久文本标签的协作光标画布,仅在传输方式上有所不同。
变体起始代码传输方式在线状态存储
cursors
GitHub基于RivetKit连接的类型化actionsevents每个连接的
connState
cursors-raw-websocket
GitHub带有自定义JSON消息协议的原始
onWebSocket
处理器
createVars
中的Socket映射
默认使用
cursors
:类型化的动作、类型化的事件以及自动连接跟踪可以用更少的代码覆盖大多数应用场景。当你需要完全控制有线格式时(例如自定义JSON或二进制协议,或者不使用RivetKit客户端库的客户端),请使用
cursors-raw-websocket

Connection State vs Persistent State

连接状态 vs 持久状态

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.
DataWhere It LivesWhy
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.
在线状态本质上是临时的。光标位置仅在连接活跃时才有意义,因此它属于每个连接的存储,而非持久化的Actor状态。持久化状态仅用于必须在断开连接和Actor重启后仍保留的数据。
数据存储位置原因
光标位置
cursors
中的
connState
cursors-raw-websocket
中的
createVars
Socket映射
作用域为单个连接,连接断开时会被丢弃。存储中不会累积过期的在线状态。
文本标签(
textLabels
两个变体中的持久化Actor
state
画布内容必须在断开连接和Actor重启后仍保留。
cursors
变体中,
updateCursor
会写入
c.conn.state.cursor
getRoomState
通过遍历
c.conns.values()
重建在线状态快照,因此光标映射始终由活跃连接派生而来,而非存储。有关
connState
请参阅Connections,有关持久化语义请参阅State

Presence Lifecycle

在线状态生命周期

  • Join: The
    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.
  • Move: Every
    updateCursor
    call writes the connection's presence entry, then broadcasts
    cursorMoved
    to all connections, including the sender.
  • Leave: The
    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
.
  • 加入
    cursors-raw-websocket
    变体在Socket连接后立即推送包含当前
    { cursors, textLabels }
    快照的
    init
    消息。
    cursors
    变体没有显式的加入广播;客户端在连接后调用一次
    getRoomState
    动作来初始化本地映射,其他客户端会在新用户第一次广播
    cursorMoved
    时看到该用户。
  • 移动:每次调用
    updateCursor
    都会写入连接的在线状态条目,然后向所有连接广播
    cursorMoved
    ,包括发送者。
  • 离开
    cursors
    变体在
    onDisconnect
    中处理离开事件,广播包含该连接最后光标位置的
    cursorRemoved
    。原始变体在Socket的
    close
    监听器中执行相同操作,然后从
    vars.websockets
    映射中删除会话。客户端会从本地光标映射中删除该用户,因此当标签页关闭时,过期光标会立即消失。
有关
onDisconnect
createVars
请参阅Lifecycle

Update Throttling

更新限流

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.
LayerPatternGuidance
Client (smoothness)Throttle to 20-30HzSample 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 limitTrack 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.
两个示例都没有实现限流。两个前端都会在每次原始
mousemove
事件时发送光标更新,没有防抖或间隔限制。这在演示中没问题,但高速鼠标在高刷新率显示器上每秒可以发出数百个事件。以下模式是在起始代码基础上推荐的生产环境强化措施,示例中并未实现。
层级模式指导
客户端(流畅性)限流至20-30Hz每33-50ms采样最新指针位置并仅发送该位置。丢弃中间移动,但始终刷新最终位置,使光标停留在真实位置。在渲染端对接收的位置进行插值。
服务器(强制执行)每个连接的速率限制跟踪每个连接的最后接受更新时间戳,丢弃或合并超过限制频率的更新。客户端限流是协作式的,Actor是强制执行的边界。

Actors

Actors

  • 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
    • updateCursor
    • updateText
    • removeText
    • getRoomState
  • Events
    • cursorMoved
    • cursorRemoved
    • textUpdated
    • textRemoved
  • Queues
    • None
  • State
    • JSON
    • 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)
    • getRoomState
  • Queues
    • None
  • State
    • JSON
    • 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:
DirectionMessage
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
  • Key
    cursorRoom[roomId]
    (前端默认
    roomId
    "general"
  • 职责:在
    connState
    中保存每个连接的光标在线状态,在Actor状态中持久化共享文本标签,并向所有连接广播光标和文本更新。
  • Actions
    • updateCursor
    • updateText
    • removeText
    • getRoomState
  • Events
    • cursorMoved
    • cursorRemoved
    • textUpdated
    • textRemoved
  • Queues
  • State
    • JSON
    • textLabels
      (持久化)
    • 每个连接的
      connState.cursor
      (临时)
  • Key
    cursorRoom[roomId]
    (通过
    client.cursorRoom.getOrCreate(roomId)
    解析)
  • 职责:暴露原始WebSocket端点,在
    createVars
    映射中跟踪活跃Socket及其光标(以
    sessionId
    查询参数为键),持久化文本标签,并手动将JSON帧分发给每个Socket。
  • Actions
    • getOrCreate
      (返回
      { status: "ok" }
      的存根;前端通过客户端句柄的
      getOrCreate(roomId).resolve()
      解析Actor ID,无需分发此动作即可创建Actor)
    • getRoomState
  • Queues
  • State
    • JSON
    • textLabels
      (持久化)
    • vars.websockets
      sessionId
      到Socket和光标映射(内存存储,重启后丢失)
原始变体未定义RivetKit事件。其消息名称是原始JSON帧上的
type
字段:
方向消息
type
负载
客户端到服务器
updateCursor
{ userId, x, y }
客户端到服务器
updateText
{ id, userId, text, x, y }
客户端到服务器
removeText
{ id }
服务器到客户端
init
连接时的
{ cursors, textLabels }
快照
服务器到客户端
cursorMoved
,
textUpdated
,
textRemoved
,
cursorRemoved
对应的光标、标签或ID负载

Lifecycle

生命周期

cursors (Actions + Events)

cursors(Actions + Events)

mermaid
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)
mermaid
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)

cursors-raw-websocket

cursors-raw-websocket

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

Security Checklist

安全检查清单

Both examples ship without authentication so the presence pattern stays readable. Everything below is recommended hardening for production, not behavior the examples implement.
  • Identity: Bind presence identity to the connection (
    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.
  • Authorization: Authorize label mutations by owner. In the examples,
    updateText
    accepts arbitrary
    id
    and
    userId
    arguments and
    removeText
    accepts an arbitrary
    id
    , so any client can edit or delete any label.
  • Input validation: Clamp
    x
    and
    y
    to canvas bounds, cap text label length, and cap the total
    textLabels
    count so persistent state cannot grow unbounded.
  • Rate limiting: Enforce a per-connection cap on
    updateCursor
    (for example 30Hz) and on label writes, as described in Update Throttling.
  • Protocol strictness (raw variant): Validate message shape before use and close the socket on malformed JSON instead of logging and continuing. Reject duplicate
    sessionId
    values rather than silently overwriting another session's socket entry.
两个示例都未包含身份验证,以便保持在线状态模式的可读性。以下是生产环境推荐的强化措施,示例中并未实现。
  • 身份验证:将在线状态身份绑定到连接(动作变体中的
    c.conn.id
    ,原始变体中的服务器生成会话ID)。永远不要信任客户端提供的
    userId
    ;在示例中它是客户端生成的随机字符串,因此任何客户端都可以冒充或删除任何光标。
  • 授权:按所有者授权标签修改。在示例中,
    updateText
    接受任意
    id
    userId
    参数,
    removeText
    接受任意
    id
    ,因此任何客户端都可以编辑或删除任何标签。
  • 输入验证:将
    x
    y
    限制在画布范围内,限制文本标签长度,并限制
    textLabels
    的总数,以防止持久化状态无限增长。
  • 速率限制:对
    updateCursor
    (例如30Hz)和标签写入实施每个连接的限制,如更新限流中所述。
  • 协议严格性(原始变体):使用前验证消息格式,遇到格式错误的JSON时关闭Socket,而非仅记录并继续。拒绝重复的
    sessionId
    值,而非静默覆盖另一个会话的Socket条目。

Reference Map

参考地图

Actors

Actors

  • Access Control
  • Actions
  • Actor Keys
  • Actor Scheduling
  • Actor Statuses
  • AI and User-Generated Rivet Actors
  • Authentication
  • Communicating Between Actors
  • Connections
  • Custom Inspector Tabs
  • Debugging
  • Design Patterns
  • Destroying Actors
  • Errors
  • Fetch and WebSocket Handler
  • Helper Types
  • Icons & Names
  • Input Parameters
  • Lifecycle
  • Limits
  • Low-Level HTTP Request Handler
  • Low-Level KV Storage
  • Low-Level WebSocket Handler
  • Metadata
  • Next.js Quickstart
  • Node.js & Bun Quickstart
  • Queues & Run Loops
  • React Quickstart
  • Realtime
  • Rust Quickstart (Preview)
  • Sandbox Actor
  • Scaling & Concurrency
  • Sharing and Joining State
  • SQLite
  • SQLite + Drizzle
  • State & Storage
  • Testing
  • Troubleshooting
  • Types
  • Vanilla HTTP API
  • Versions & Upgrades
  • Workflows
  • Access Control
  • Actions
  • Actor Keys
  • Actor Scheduling
  • Actor Statuses
  • AI and User-Generated Rivet Actors
  • Authentication
  • Communicating Between Actors
  • Connections
  • Custom Inspector Tabs
  • Debugging
  • Design Patterns
  • Destroying Actors
  • Errors
  • Fetch and WebSocket Handler
  • Helper Types
  • Icons & Names
  • Input Parameters
  • Lifecycle
  • Limits
  • Low-Level HTTP Request Handler
  • Low-Level KV Storage
  • Low-Level WebSocket Handler
  • Metadata
  • Next.js Quickstart
  • Node.js & Bun Quickstart
  • Queues & Run Loops
  • React Quickstart
  • Realtime
  • Rust Quickstart (Preview)
  • Sandbox Actor
  • Scaling & Concurrency
  • Sharing and Joining State
  • SQLite
  • SQLite + Drizzle
  • State & Storage
  • Testing
  • Troubleshooting
  • Types
  • Vanilla HTTP API
  • Versions & Upgrades
  • Workflows

Agent Os

Agent Os

  • Agent-to-Agent Communication
  • agentOS vs Sandbox
  • Authentication
  • Benchmarks
  • Configuration
  • Core Package
  • Cron Jobs
  • Deployment
  • Embedded LLM Gateway
  • Events
  • Filesystem
  • Limitations
  • LLM Credentials
  • Multiplayer
  • Networking & Previews
  • Overview
  • Permissions
  • Persistence & Sleep
  • Pi
  • Processes & Shell
  • Queues
  • Quickstart
  • Sandbox Mounting
  • Security & Auth
  • Security Model
  • Sessions
  • Software
  • SQLite
  • System Prompt
  • Tools
  • Webhooks
  • Workflow Automation
  • Agent-to-Agent Communication
  • agentOS vs Sandbox
  • Authentication
  • Benchmarks
  • Configuration
  • Core Package
  • Cron Jobs
  • Deployment
  • Embedded LLM Gateway
  • Events
  • Filesystem
  • Limitations
  • LLM Credentials
  • Multiplayer
  • Networking & Previews
  • Overview
  • Permissions
  • Persistence & Sleep
  • Pi
  • Processes & Shell
  • Queues
  • Quickstart
  • Sandbox Mounting
  • Security & Auth
  • Security Model
  • Sessions
  • Software
  • SQLite
  • System Prompt
  • Tools
  • Webhooks
  • Workflow Automation

Clients

Clients

  • Node.js & Bun
  • React
  • Swift
  • SwiftUI
  • Node.js & Bun
  • React
  • Swift
  • SwiftUI

Connect

Connect

  • Deploy To Amazon Web Services Lambda
  • Deploying to AWS ECS
  • Deploying to Cloudflare Workers
  • Deploying to Freestyle
  • Deploying to Google Cloud Run
  • Deploying to Hetzner
  • Deploying to Kubernetes
  • Deploying to Railway
  • Deploying to Rivet Compute
  • Deploying to Supabase Functions
  • Deploying to Vercel
  • Deploying to VMs & Bare Metal
  • Deploy To Amazon Web Services Lambda
  • Deploying to AWS ECS
  • Deploying to Cloudflare Workers
  • Deploying to Freestyle
  • Deploying to Google Cloud Run
  • Deploying to Hetzner
  • Deploying to Kubernetes
  • Deploying to Railway
  • Deploying to Rivet Compute
  • Deploying to Supabase Functions
  • Deploying to Vercel
  • Deploying to VMs & Bare Metal

Cookbook

Cookbook

  • AI Agent
  • AI Agent Workspaces
  • Chat Room
  • Collaborative Text Editor
  • Cron Jobs and Scheduled Tasks
  • Database per Tenant
  • Deploying Rivet in a VPC or Air-Gapped Network
  • Live Cursors and Presence
  • Multiplayer Game
  • AI Agent
  • AI Agent Workspaces
  • Chat Room
  • Collaborative Text Editor
  • Cron Jobs and Scheduled Tasks
  • Database per Tenant
  • Deploying Rivet in a VPC or Air-Gapped Network
  • Live Cursors and Presence
  • Multiplayer Game

General

General

  • Actor Configuration
  • Architecture
  • Cross-Origin Resource Sharing
  • Documentation for LLMs & AI
  • Edge Networking
  • Endpoints
  • Environment Variables
  • HTTP Server
  • Logging
  • Pool Configuration
  • Production Checklist
  • Registry Configuration
  • Runtime Modes
  • Actor Configuration
  • Architecture
  • Cross-Origin Resource Sharing
  • Documentation for LLMs & AI
  • Edge Networking
  • Endpoints
  • Environment Variables
  • HTTP Server
  • Logging
  • Pool Configuration
  • Production Checklist
  • Registry Configuration
  • Runtime Modes

Self Hosting

Self Hosting

  • Configuration
  • Docker Compose
  • Docker Container
  • File System
  • FoundationDB (Enterprise)
  • Installing Rivet Engine
  • Kubernetes
  • Multi-Region
  • PostgreSQL
  • Production Checklist
  • Railway Deployment
  • Render Deployment
  • TLS & Certificates
  • Configuration
  • Docker Compose
  • Docker Container
  • File System
  • FoundationDB (Enterprise)
  • Installing Rivet Engine
  • Kubernetes
  • Multi-Region
  • PostgreSQL
  • Production Checklist
  • Railway Deployment
  • Render Deployment
  • TLS & Certificates