CRDTとYjsによる協調編集
この記事は概要レベルの整理である。詳細な仕組みは別途深掘りする予定。
協調編集の課題
複数のユーザーが同時に同じドキュメントを編集するとき、以下の問題が発生する:
User A: "Hello" → "Hello World" (位置5に" World"を挿入)
User B: "Hello" → "Hi" (位置0-4を"Hi"に置換)
同時に実行されたら、最終結果は?
ネットワーク遅延がある環境では、操作の順序が保証されない。単純に「後勝ち」にすると、一方の編集が失われる。
2つのアプローチ
OT(Operational Transformation)
Google Docsが採用している方式。操作を変換して競合を解決する。
User Aの操作: insert(5, " World")
User Bの操作: replace(0, 4, "Hi")
→ サーバーが操作を変換
→ User Aの操作を調整: insert(2, " World")
中央サーバーが必要で、変換ロジックが複雑になりやすい。
CRDT(Conflict-free Replicated Data Type)
数学的に「競合が起きない」ことを保証するデータ構造。各操作に一意のIDを付与し、どの順序で適用しても同じ結果になるよう設計されている。
User Aの操作: { id: "A1", type: "insert", after: "char-5", text: " World" }
User Bの操作: { id: "B1", type: "delete", target: "char-0-4" }
{ id: "B2", type: "insert", after: null, text: "Hi" }
→ 順序に関係なく、同じ最終状態に収束
サーバーレスでも動作可能で、オフライン編集→後から同期にも対応できる。
Yjsとは
YjsはCRDTの実装ライブラリで、リッチテキストエディタとの連携に特化している。
import * as Y from 'yjs'
import { WebsocketProvider } from 'y-websocket'
// 共有ドキュメントを作成
const ydoc = new Y.Doc()
// WebSocketで他のクライアントと同期
const provider = new WebsocketProvider(
'wss://your-server',
'document-room-id',
ydoc
)
// 共有テキストを取得
const ytext = ydoc.getText('content')
// 変更を監視
ytext.observe(event => {
console.log('変更検知:', event.changes)
})
エディタライブラリとの連携
┌─────────────────┐ ┌─────────────────┐
│ ProseMirror │ │ Yjs │
│ TipTap │ ←→ │ (CRDT実装) │
│ Slate │ │ │
└─────────────────┘ └─────────────────┘
↓ ↓
ローカル編集 ネットワーク同期
TipTapの場合:
import { Editor } from '@tiptap/core'
import Collaboration from '@tiptap/extension-collaboration'
import CollaborationCursor from '@tiptap/extension-collaboration-cursor'
import * as Y from 'yjs'
import { WebsocketProvider } from 'y-websocket'
const ydoc = new Y.Doc()
const provider = new WebsocketProvider('wss://server', 'doc-id', ydoc)
const editor = new Editor({
extensions: [
StarterKit,
Collaboration.configure({ document: ydoc }),
CollaborationCursor.configure({
provider,
user: { name: 'User A', color: '#f783ac' }
})
]
})
Undo/Redo
Yjsは協調編集環境でのUndo/Redoもサポートする。「自分の操作だけを戻す」ことが可能。
import { UndoManager } from 'yjs'
const undoManager = new UndoManager(ytext)
// 自分の操作だけをUndo
undoManager.undo()
undoManager.redo()
深掘りしたいトピック
- CRDTの数学的背景(半束、結合律など)
- YjsのYXmlFragment構造
- オフラインファースト設計
- パフォーマンスとメモリ消費
- 認証・認可との統合
関連
抽出された概念
この記事から以下の一般概念をnotes/に抽出した:
- Operational Transformation - OT(操作変換)アルゴリズムの概要とCRDTとの比較
- 協調編集 - リアルタイム協調編集の課題・アプローチ・代表実装