mcpField
C
collaborative-text-editor

collaborative-text-editor

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

collaborative-text-editor について

RivetKitとYjsを使った共同編集テキストエディターのサンプルで、共有ドキュメントを作る開発者向けです。文書ごとのActorがCRDT更新を中継してスナップショットを保存し、カーソルとプレゼンスも配信します。

主な機能

  1. 01Yjs CRDT文書同期
  2. 02文書ごとのActor
  3. 03バイナリスナップショット保存
  4. 04カーソルとプレゼンスの中継
  5. 05ワークスペース文書インデックス

活用シーン

  1. 01共有ノートや文書の構築
  2. 02再接続後のローカル編集のマージ
  3. 03文書へのリモートカーソルとプレゼンス追加

インストール

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

$npx skills add https://github.com/rivet-dev/skills --skill collaborative-text-editor

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

同梱ファイル

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 で Yjs サーバーを構築するためのパターンです。CRDT ドキュメントの同期、プレゼンスとカーソル、スナップショットの永続化を扱い、ドキュメントごとに 1 つの Rivet Actor が中継役を担います。

スターターコード

GitHub にある動作するサンプルから始め、エディターに合わせて調整してください。シンプルなテキストエリア、リモートカーソルのオーバーレイ、ワークスペースのドキュメントインデックスを備えた React フロントエンドが含まれています。

ユースケーススターターコードよくある例
ドキュメントの共同編集GitHubNotion 風ドキュメント、共有メモ、ペアライティングツール、フォームの共同編集

CRDT と OT の比較

同時テキスト編集を解決するアルゴリズムには、2 つの系統があります。どちらを選ぶかによって、サーバーに必要な処理が決まります。

項目CRDT(Yjs)Operational Transformation
競合解決モデル可換なマージです。更新はどのピアでも任意の順序で適用でき、同じ結果に収束します。サーバーは各操作を、並行して行われたすべての操作に対して変換します。正しく動作するには、一元化された順序制御が必要です。
オフライン対応優れています。クライアントはローカルで編集を続け、再接続時にバッファーされた更新をマージします。限定的です。長期にわたる不一致によって、変換チェーンが複雑で壊れやすくなります。
サーバーの役割中継と永続化です。サーバーは内容を解釈しない更新を適用して再配信します。ドキュメントの意味を理解する必要はありません。権威ある変換処理です。サーバーは、すべての操作タイプに対する変換ロジックを実装する必要があります。
ライブラリーの成熟度Yjs は成熟して広く導入されており、ProseMirror、CodeMirror、Monaco などのバインディングがあります。本番品質の実装は、ほとんどが独自実装(Google Docs)か、古くなっています(ShareDB)。

このサンプルでは CRDT を使用しています。CRDT を使うと、サーバーを中継型の Rivet Actor のままにできるためです。Actor は受信した各更新をサーバー側の Y.Doc に適用し、マージ後の状態を永続化して後から参加したユーザーに提供しますが、操作の変換や競合の調整は行いません。Yjs のマージは可換であるため、順序は関係ありません。

ドキュメントアクターモデル

トピック概要
トポロジードキュメントごとに document[workspaceId, documentId] アクターを1つ、ワークスペースごとに documentList[workspaceId] コーディネーターを1つ配置します。
同期モデル各クライアントはローカルの Y.Doc を保持します。ドキュメントアクターは、増分 Yjs 更新をブロードキャスト イベント として中継し、サーバー側にマージ済みのコピーを vars に保持します。
永続化同期更新ごとに、マージ済みの Yjs スナップショット全体を1つのバイナリ アクター KV キー(yjs:doc)に上書き保存します。ドキュメントのメタデータは JSON ステート に保存されます。
キューありません。このサンプルは アクション とブロードキャストイベントのみで構成されています。
プレゼンスYjs Awareness は同じ applyUpdate アクションを通じて中継されます。接続ごとの connState では、切断時のクリーンアップのために、その接続が申告した Awareness の clientId を追跡します。

2つのアクターに分ける構成は、設計パターン のコーディネーターパターンに沿っています。コーディネーターは検索と作成を担当し、各ドキュメントアクターは1つのドキュメントのリアルタイム状態を管理します。複数要素の キー によって、両方のアクターをワークスペースのスコープ内に配置します。

アクター

  • キー:document[workspaceId, documentId]

  • 責務:受信した同期更新と Awareness 更新をサーバー側の Y.Doc および Awareness に適用し、マージ済みの Yjs スナップショットをアクター KV に永続化して、接続中のすべての共同作業者に更新をブロードキャストします。

  • アクション

    • getContent
    • applyUpdate
    • getAwareness
  • キュー

    • ありません
  • ステート

    • JSON メタデータのみ:title、createdAt、updatedAt
    • マージ済みの Yjs スナップショット全体を保持するバイナリ KV キー yjs:doc
    • 一時的な vars:createVars で作成され、アクターの起動時に KV から再水和される、稼働中の Y.Doc と Awareness
    • 接続ごとの connState:その接続が申告した Awareness クライアントの clientIds
  • キー:documentList[workspaceId]

  • 責務:1つのワークスペースのコーディネーターです。アクター間クライアントを通じてドキュメントアクターを作成し、ドキュメント概要のインデックスを管理します。

  • アクション

    • createDocument
    • listDocuments
    • deleteDocument
  • キュー

    • ありません
  • ステート

    • JSON
    • DocumentSummary エントリー(id、title、createdAt、updatedAt)の配列 documents

コーディネーターの createDocument は UUID を生成し、続いて c.client<typeof registry>() を使用してドキュメントアクターを明示的に作成し、作成時の 入力 として { title, createdAt } を渡します。これはドキュメントアクターの createState によって使用されます。アクター間クライアントについては、アクター間の通信 を参照してください。

更新リレー

単一の applyUpdate(update, kind, clientId?) アクションで、両方の更新種別を処理します。更新はアクションの境界を number[] のバイト配列として通過し、各側で Uint8Array に変換されます。

種別サーバーでの適用先永続化ブロードキャスト
"sync"Y.applyUpdate を使い、オリジン "client" で c.vars.doc に適用マージ済みのスナップショット全体を KV キー yjs:doc に保存し、その後 updatedAt を更新します差分更新を含む sync イベント
"awareness"オリジン "client" で applyAwarenessUpdate を使い、c.vars.awareness に適用なし。プレゼンスは一時的です。更新を含む awareness イベント

同期ブランチには非対称な点があります。ブロードキャストには小さな差分更新のみが含まれますが、KV への書き込みでは Y.encodeStateAsUpdate で再エンコードした、マージ済みドキュメント全体が保存されます。

Yjs のオリジンタグは、リレーのループを防ぐエコーガードです。

オリジンタグ設定場所効果
"local"クライアントでの編集時に doc.transact(..., "local") 内で設定クライアントの更新リスナーが実行され、applyUpdate がアクターに送信されます。
"client"サーバーが受信した更新を Y.Doc または Awareness に適用する際に設定サーバー側のコピーで、変更がクライアント起点であることを示します。
"remote"クライアントがブロードキャストイベントまたは初期同期データを適用する際に設定更新リスナーは "remote" の場合に早期リターンするため、クライアントが自身のエコーを再送信することはありません。

接続時または再接続時に、クライアントは getContent と getAwareness を呼び出し、両方の結果をオリジン "remote" でローカルの Y.Doc と Awareness に適用します。その後、すべての変更は applyUpdate とブロードキャストイベントを介して伝播します。

Awareness とプレゼンス

プレゼンス(ユーザー名、色、カーソル位置)は、アクターの状態ではなく Yjs Awareness プロトコルを介して伝達されます。

  • クライアントは awareness.setLocalStateField を使って、user フィールドと cursor フィールドにプレゼンスを設定します。Awareness の更新リスナーは変更をエンコードし、applyUpdate(update, "awareness", awareness.clientID) を送信します。
  • アクターは、接続ごとの connState.clientIds に通知された各 clientId を記録し、その更新をサーバー側の Awareness に適用して、すべてのピアに awareness イベントをブロードキャストします。接続ごとの状態については、接続を参照してください。
  • onDisconnect は接続の clientIds を読み取り、サーバー側の Awareness で removeAwarenessStates を呼び出して、エンコードした削除情報をブロードキャストします。これにより、残りのすべてのクライアントで、切断したユーザーのカーソルが削除されます。このフックについては、ライフサイクルを参照してください。

アクターは、どの Awareness の clientId がどの接続に属するかを追跡するため、クライアント側での協力がなくても、切断時にプレゼンスが自動的にクリーンアップされます。

永続化とコンパクション

この例では、完全なスナップショットで上書きして永続化します。"sync" の更新ごとに、アクターは Y.encodeStateAsUpdate でマージ済みドキュメント全体を再エンコードし、単一のバイナリ KV キー yjs:doc を上書きします。追記専用の更新ログも、個別のコンパクションジョブもありません。Y.encodeStateAsUpdate がドキュメントをマージしたコンパクトな表現を出力するため、Yjs のマージセマンティクスによって、保存された Blob は自動的にコンパクトに保たれます。

プロパティ完全なスナップショットでの上書き(この例)
書き込みコスト同期更新ごとにドキュメント全体を KV に書き込むため、キー入力のたびに Blob 全体を書き換えます。
読み取りコストアクターの起動時に createVars でバイナリ KV を 1 回読み取り、ドキュメントを復元します。
クラッシュ時の安全性最後に完了した applyUpdate は永続化されています。ログの再生は不要です。
最適な用途書き込みの増幅よりシンプルさを優先する、小規模から中規模のドキュメントです。

推奨される拡張(この例には含まれません):大規模なドキュメントや非常に頻繁な編集では、差分更新を KV 更新ログに追記し、定期的(たとえば N 回の更新ごと)にマージ済みスナップショットを書き込む方式に切り替えてください。起動時にはスナップショットにログの再生を組み合わせ、スナップショットの書き込みをログを切り詰める明示的なコンパクション手順とします。完全なスナップショットの書き込みが実測でボトルネックになった場合、または Blob が KV の値サイズ上限に近づいた場合にのみ、この方法を採用してください。

ライフサイクル

sequenceDiagram
	participant A as Client A
	participant B as Client B
	participant DL as documentList
	participant D as document

	A->>DL: listDocuments()
	A->>DL: createDocument(title)
	DL->>D: create([workspaceId, documentId], input)
	DL-->>A: DocumentSummary
	A->>D: connect
	B->>D: connect
	Note over D: createVars rehydrates Y.Doc from KV "yjs:doc"
	A->>D: getContent() + getAwareness()
	D-->>A: encoded doc + awareness state
	Note over A: local edit with origin "local"
	A->>D: applyUpdate(update, "sync")
	Note over D: apply with origin "client", overwrite KV snapshot
	D-->>B: sync event (incremental update)
	Note over B: apply with origin "remote", no echo
	A->>D: applyUpdate(update, "awareness", clientId)
	D-->>B: awareness event
	B-->>D: disconnect
	Note over D: onDisconnect removes B's awareness clientIds
	D-->>A: awareness event (removal)

セキュリティチェックリスト

この例には、認証も認可も実装されていません。本番環境で使用する前に、以下を基本としてセキュリティを強化してください。これらはいずれも、この例では実装されていません。

  • 接続前に認証する:ワークスペース ID を知っている、または推測できる人なら誰でも接続できます。また、useActor は暗黙的に getOrCreates するため、存在しないワークスペース ID で接続すると、空の documentList コーディネーターが黙って作成されます。認証されていないクライアントがアクターに到達しないよう、接続時の認証を追加してください。認証を参照してください。
  • ドキュメントごとのアクセス制御:認証済みユーザーが任意のドキュメントではなく、特定の [workspaceId, documentId] キーにアクセスする権限を持つことを検証してください。
  • applyUpdate に上限を設け、レート制限する:更新ペイロードはサイズ制限のない未検証の number[] 配列です。また、この例のクライアントは、スロットリングを一切行わず、キー入力やカーソル移動のたびにアクションを送信します。サーバーでペイロードサイズの上限と接続ごとのレート制限を適用し、クライアントではデバウンスしてください。
  • クライアントが申告する awareness の clientIds を信頼しない:applyUpdate の clientId 引数はクライアントから送信され、そのまま信頼されています。接続スコープのサーバー状態からプレゼンス ID を導出するか、検証してください。
  • 削除時にアクターと KV を破棄する:deleteDocument はコーディネーターのインデックスからエントリーを除外するだけです。ドキュメントアクターとその KV スナップショットは孤立したままになります。削除時には、削除権限のチェックを行ったうえで、ドキュメントアクターとそのストレージも破棄してください。

リファレンスマップ

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

コメント