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

深掘りしたいトピック

関連

抽出された概念

この記事から以下の一般概念をnotes/に抽出した: