mcpField
L
live-cursors

live-cursors

github.com/rivet-dev開発441.1k 回インストール1 日前に更新
リポジトリ

live-cursors について

共同編集インターフェースを作る開発者向けの、RivetKitによるライブカーソルとプレゼンスのサンプルです。型付きアクションとイベントを使う方式に加え、JSONプロトコルを扱う生WebSocket版もあります。

主な機能

  1. 01ルーム単位のカーソル共有
  2. 02接続ごとの一時状態
  3. 03カーソル移動・退出イベント
  4. 04生WebSocketプロトコル対応
  5. 05テキストラベルの永続化

活用シーン

  1. 01共同キャンバスで他の利用者のカーソルを表示
  2. 02複数人ルームでプレゼンスを同期
  3. 03RivetKitクライアントライブラリを使わないWebSocket通信

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/rivet-dev/skills --skill live-cursors

インストール後、Claude Code で/live-cursorsを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

BASE_SKILL.md
index.json
openapi.json
reference/actors/
access-control.md
actions.md
actor-runtime-socket.md
appearance.md
authentication.md
communicating-between-actors.md
connections.md
debugging.md
design-patterns.md
destroy.md
errors.md
events.md
fetch-and-websocket-handler.md
helper-types.md
http-api.md
input.md
inspector-tabs.md
keys.md
kv.md
lifecycle.md
limits.md
metadata.md
queues.md
reference/actors/quickstart/
backend.md
cloudflare.md
effect.md
next-js.md

詳細

ライブカーソルとプレゼンス

重要:何かを始める前に、このスキルのディレクトリにある BASE_SKILL.md を必ずお読みください。デバッグ、エラー処理、状態管理、デプロイ、プロジェクトのセットアップに関する重要なガイダンスが記載されています。これらのルールとパターンは、すべての RivetKit の作業に適用されます。以下は、すでに内容を読んで理解していることを前提としています。

動作するサンプル

参考実装が必要な場合は、以下のテンプレートにある動作するサンプルコードを直接お読みください。

RivetKit を使ってライブカーソル、マルチプレイヤーのプレゼンス、リアルタイムのカーソル共有を構築するためのパターンです。1 つのルームアクターが、アクターキーを使ってルームごとにカーソル位置をすべての接続済みクライアントへ配信します。

スターターコード

GitHub にある、動作する 2 つのバリエーションのいずれかから始めてください。どちらも永続的なテキストラベルを備えた、同じ共同編集カーソルキャンバスを実装しています。異なるのはトランスポートのみです。

バリエーションスターターコードトランスポートプレゼンスの保存先
cursorsGitHubRivetKit 接続経由の型付き アクションとイベント接続ごとの connState
cursors-raw-websocketGitHubカスタム 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 に保持し、共有テキストラベルをアクターの状態に永続化して、カーソルとテキストの更新をすべての接続にブロードキャストします。

  • アクション

    • updateCursor
    • updateText
    • removeText
    • getRoomState
  • イベント

    • cursorMoved
    • cursorRemoved
    • textUpdated
    • textRemoved
  • キュー

    • なし
  • 状態

    • 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 の値を拒否してください。

リファレンスマップ

アクター
CLI
クライアント
クックブック
デプロイ
一般
セルフホスティング

コメント