live-cursors について
共同編集インターフェースを作る開発者向けの、RivetKitによるライブカーソルとプレゼンスのサンプルです。型付きアクションとイベントを使う方式に加え、JSONプロトコルを扱う生WebSocket版もあります。
主な機能
- 01ルーム単位のカーソル共有
- 02接続ごとの一時状態
- 03カーソル移動・退出イベント
- 04生WebSocketプロトコル対応
- 05テキストラベルの永続化
活用シーン
- 01共同キャンバスで他の利用者のカーソルを表示
- 02複数人ルームでプレゼンスを同期
- 03RivetKitクライアントライブラリを使わないWebSocket通信
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ rivet-dev/ skills --skill live-cursorsインストール後、Claude Code で/live-cursorsを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
BASE_SKILL.mdindex.jsonopenapi.jsonreference/actors/access-control.mdactions.mdactor-runtime-socket.mdappearance.mdauthentication.mdcommunicating-between-actors.mdconnections.mddebugging.mddesign-patterns.mddestroy.mderrors.mdevents.mdfetch-and-websocket-handler.mdhelper-types.mdhttp-api.mdinput.mdinspector-tabs.mdkeys.mdkv.mdlifecycle.mdlimits.mdmetadata.mdqueues.mdreference/actors/quickstart/backend.mdcloudflare.mdeffect.mdnext-js.md詳細
ライブカーソルとプレゼンス
重要:何かを始める前に、このスキルのディレクトリにある BASE_SKILL.md を必ずお読みください。デバッグ、エラー処理、状態管理、デプロイ、プロジェクトのセットアップに関する重要なガイダンスが記載されています。これらのルールとパターンは、すべての RivetKit の作業に適用されます。以下は、すでに内容を読んで理解していることを前提としています。
動作するサンプル
参考実装が必要な場合は、以下のテンプレートにある動作するサンプルコードを直接お読みください。
RivetKit を使ってライブカーソル、マルチプレイヤーのプレゼンス、リアルタイムのカーソル共有を構築するためのパターンです。1 つのルームアクターが、アクターキーを使ってルームごとにカーソル位置をすべての接続済みクライアントへ配信します。
スターターコード
GitHub にある、動作する 2 つのバリエーションのいずれかから始めてください。どちらも永続的なテキストラベルを備えた、同じ共同編集カーソルキャンバスを実装しています。異なるのはトランスポートのみです。
| バリエーション | スターターコード | トランスポート | プレゼンスの保存先 |
|---|---|---|---|
cursors | GitHub | RivetKit 接続経由の型付き アクションとイベント | 接続ごとの connState |
cursors-raw-websocket | GitHub | カスタム JSON メッセージプロトコルを使う、生の onWebSocket ハンドラー | createVars 内のソケットマップ |
デフォルトでは cursors を使用してください。型付きアクション、型付きイベント、自動接続追跡により、ほとんどのアプリをより少ないコードで実装できます。カスタム JSON やバイナリプロトコルなど、ワイヤ形式を完全に制御する必要がある場合や、RivetKit クライアントライブラリを使用しないクライアントを使う場合は、cursors-raw-websocket を使用してください。
接続状態と永続状態
プレゼンスは定義上、一時的なものです。カーソル位置が意味を持つのは接続が有効な間だけなので、永続的なアクター状態ではなく、接続ごとのストレージに保存します。永続状態は、切断やアクターの再起動後も保持する必要があるデータのために使います。
| データ | 保存場所 | 理由 |
|---|---|---|
| カーソル位置 | connState(cursors)または createVars のソケットマップ(cursors-raw-websocket) | 1 つの接続に紐付き、その接続とともに破棄されます。古いプレゼンスがストレージに蓄積することはありません。 |
テキストラベル(textLabels) | どちらのバリエーションでも、永続的なアクターの state | キャンバスの内容は、切断やアクターの再起動後も保持する必要があります。 |
cursors のバリエーションでは、updateCursor が c.state.cursor に書き込み、getRoomState が c.conns.values() を反復処理してプレゼンスのスナップショットを再構築するため、カーソルマップは常に保存データではなく、現在有効な接続から導出されます。connState については接続を、永続化の動作については状態を参照してください。
プレゼンスのライフサイクル
- 参加:
cursors-raw-websocketのバリエーションでは、ソケットが接続するとすぐに、現在の{ cursors, textLabels }スナップショットを含むinitメッセージが送信されます。cursorsのバリエーションには明示的な参加ブロードキャストがありません。クライアントは接続後にgetRoomStateアクションを 1 回呼び出してローカルマップを初期化し、他の参加者が新しいユーザーを最初に確認するのは、そのユーザーの最初のcursorMovedブロードキャスト時です。 - 移動:
updateCursorが呼び出されるたびに、接続のプレゼンスエントリーを書き換えた後、送信元を含むすべての接続にcursorMovedをブロードキャストします。 - 退出:
cursorsのバリエーションでは、onDisconnectで退出を処理し、接続の最後のカーソル位置とともにcursorRemovedをブロードキャストします。Raw バリエーションでも同様の処理をソケットのcloseリスナーから行い、その後vars.websocketsマップからセッションを削除します。クライアントはそのユーザーをローカルのカーソルマップから削除するため、タブが閉じた時点で古いカーソルは消えます。
onDisconnect と createVars についてはライフサイクルを参照してください。
更新のスロットリング
どちらの例にもスロットリングはありません。どちらのフロントエンドも、デバウンスや間隔の上限を設けず、生の mousemove イベントごとにカーソルの更新を送信します。デモでは問題ありませんが、高リフレッシュレートのディスプレイでマウスをすばやく動かすと、ユーザーごとに毎秒数百件のイベントが発生することがあります。以下のパターンは、スターターコードに追加する本番環境向けの強化策として推奨するもので、サンプルに実装されているものではありません。
| レイヤー | パターン | ガイダンス |
|---|---|---|
| クライアント(滑らかさ) | 20~30Hzにスロットリング | 33~50msごとに最新のポインター位置をサンプリングし、その位置だけを送信します。中間の移動は破棄しますが、カーソルが正しい位置に落ち着くよう、最後の位置は必ずフラッシュします。描画側では、受信した位置の間を補間します。 |
| サーバー(適用) | 接続ごとのレート制限 | 接続ごとに最後に受け付けた更新のタイムスタンプを追跡し、上限より速く到着した更新を破棄するか、まとめます。クライアント側のスロットリングは協調的なものです。適用の境界となるのはアクターです。 |
アクター
-
キー:
cursorRoom[roomId](フロントエンドではroomIdのデフォルト値は"general"です) -
責務:接続ごとのカーソルのプレゼンスを
connStateに保持し、共有テキストラベルをアクターの状態に永続化して、カーソルとテキストの更新をすべての接続にブロードキャストします。 -
アクション
updateCursorupdateTextremoveTextgetRoomState
-
イベント
cursorMovedcursorRemovedtextUpdatedtextRemoved
-
キュー
- なし
-
状態
- JSON
textLabels(永続)- 接続ごとの
connState.cursor(エフェメラル)
-
キー:
cursorRoom[roomId](client.cursorRoom.getOrCreate(roomId)を通じて解決されます) -
責務:生の WebSocket エンドポイントを公開し、
sessionIdクエリパラメーターをキーとするcreateVarsマップでライブソケットとそのカーソルを追跡し、テキストラベルを永続化して、すべてのソケットに JSON フレームを手動でファンアウトします。 -
アクション
getOrCreate({ status: "ok" }を返すスタブです。フロントエンドは、アクションをディスパッチせずにアクターを作成するgetOrCreate(roomId).resolve()をクライアントハンドルで呼び出して、アクター ID を解決します)getRoomState
-
キュー
- なし
-
状態
- JSON
textLabels(永続)sessionIdをソケットとカーソルに対応付けるvars.websocketsマップ(メモリー内にあり、再起動すると失われます)
生のバリアントでは、RivetKit イベントは定義されていません。メッセージ名は、生の JSON フレームの type フィールドです。
| 方向 | メッセージの type | ペイロード |
|---|---|---|
| クライアントからサーバー | updateCursor | { userId, x, y } |
| クライアントからサーバー | updateText | { id, userId, text, x, y } |
| クライアントからサーバー | removeText | { id } |
| サーバーからクライアント | init | 接続時の { cursors, textLabels } スナップショット |
| サーバーからクライアント | cursorMoved, textUpdated, textRemoved, cursorRemoved | 対応するカーソル、ラベル、または ID のペイロード |
ライフサイクル
cursors(アクション + イベント)
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
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
セキュリティチェックリスト
どちらの例も認証なしで提供されており、プレゼンスのパターンを読みやすく保っています。以下は本番環境で推奨されるセキュリティ強化策であり、例に実装されている動作ではありません。
- ID:プレゼンスの ID を接続に紐付けてください(actions 版では
c.conn.id、raw 版ではサーバー生成のセッション ID)。クライアントが指定したuserIdを決して信頼しないでください。例ではランダムなクライアント生成文字列を使っているため、どのクライアントでも任意のカーソルになりすましたり、削除したりできます。 - 認可:所有者に基づいてラベルの変更を認可してください。例では、
updateTextは任意のidとuserId引数を受け入れ、removeTextは任意のidを受け入れるため、どのクライアントでも任意のラベルを編集または削除できます。 - 入力検証:
xとyをキャンバスの範囲内に制限し、テキストラベルの長さに上限を設け、textLabelsの合計数にも上限を設けて、永続状態が際限なく増加しないようにしてください。 - レート制限:更新のスロットリングで説明されているように、接続ごとに
updateCursor(たとえば 30Hz)とラベル書き込みに上限を設けてください。 - プロトコルの厳格化(raw 版):使用前にメッセージの形式を検証し、不正な JSON をログに記録して処理を続けるのではなく、ソケットを閉じてください。別のセッションのソケットエントリを黙って上書きせず、重複する
sessionIdの値を拒否してください。
リファレンスマップ
アクター
- アクセス制御
- アクション
- アクターキー
- アクターランタイムソケット
- アクターのステータス
- 認証
- Cloudflare Workers クイックスタート
- アクター間の通信
- 接続
- カスタムインスペクタータブ
- デバッグ
- 設計パターン
- アクターの破棄
- Effect.ts クイックスタート(ベータ)
- エラー
- Fetch および WebSocket ハンドラー
- ヘルパー型
- アイコンと名前
- メモリ内状態
- 入力パラメーター
- ライフサイクル
- 制限
- 低レベル HTTP リクエストハンドラー
- 低レベル KV ストレージ
- 低レベル WebSocket ハンドラー
- メタデータ
- Next.js クイックスタート
- Node.js と Bun のクイックスタート
- キューと実行ループ
- React クイックスタート
- リアルタイム
- Rust クイックスタート(ベータ)
- スケーリングと並行処理
- スケジュールと Cron
- 状態の共有と参加
- SQLite
- SQLite と Drizzle
- Supabase Functions クイックスタート
- テスト
- トラブルシューティング
- 型
- 標準 HTTP API
- バージョンとアップグレード
- ワークフロー
CLI
クライアント
クックブック
- AI エージェント
- チャットルーム
- 共同編集テキストエディター
- Cron ジョブとスケジュールされたタスク
- テナントごとのデータベース
- VPC またはエアギャップネットワークへの Rivet のデプロイ
- ライブカーソルとプレゼンス
- マルチプレイヤーゲーム
デプロイ
- コンテナーランナー
- Amazon Web Services Lambda へのデプロイ
- AWS ECS へのデプロイ
- Cloudflare Workers へのデプロイ
- Freestyle へのデプロイ
- Google Cloud Run へのデプロイ
- Hetzner へのデプロイ
- Kubernetes へのデプロイ
- Railway へのデプロイ
- Rivet Compute へのデプロイ
- Supabase Functions へのデプロイ
- Vercel へのデプロイ
- VM とベアメタルへのデプロイ
一般
- アクターの設定
- クロスオリジンリソース共有
- LLM と AI 向けドキュメント
- エッジネットワーキング
- エンドポイント
- 環境変数
- HTTP サーバー
- ログ記録
- プールの設定
- 本番環境チェックリスト
- レジストリの設定
- ランタイムモード
- WASM とネイティブ SDK