real-time-collaboration-engine について
Real-Time Collaboration Engineは、共同編集アプリを開発する人向けのスキルで、WebSocket、競合解決、プレゼンスを扱います。データ形式やオフライン要件に応じて、OT、CRDT、基本的なブロードキャストの選択を案内します。
主な機能
- 01WebSocketの再接続と接続パターン
- 02OTとCRDTによる競合解決
- 03プレゼンスとカーソル共有
- 04オフライン変更のキューイングと同期
- 05変更のバッチ処理と楽観的更新
活用シーン
- 01共同編集テキストドキュメントの構築
- 02共有ホワイトボードやデザインツールの実装
- 03複数人で使う動画編集タイムラインの同期
- 04オフライン編集と再接続への対応
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ curiositech/ some_claude_skills --skill real-time-collaboration-engineインストール後、Claude Code で/real-time-collaboration-engineを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
.claude-plugin/plugin.jsonreferences/ot-vs-crdt.mdpresence-patterns.mdwebsocket-scaling.mdscripts/collaboration_tester.tslatency_simulator.ts詳細
リアルタイムコラボレーションエンジン
WebSocket、競合解決、プレゼンス認識を活用した、Google Docs のような共同編集の構築に精通しています。
使用する場面
✅ 次の場合に使用します:
- 共同編集用のテキスト/コードエディター
- 共有ホワイトボードやデザインツール
- 複数ユーザーで編集する動画編集タイムライン
- リアルタイムのデータダッシュボード
- マルチプレイヤーゲームの状態同期
❌ 次の場合は使用しません:
- シンプルなチャットアプリケーション(基本的な WebSocket を使用します)
- リクエスト/レスポンス API(REST/GraphQL を使用します)
- シングルユーザーアプリケーション
- 読み取り専用のデータストリーミング(Server-Sent Events を使用します)
簡単な判断フローチャート
Need real-time collaboration?
├── Text editing? → Operational Transform (OT)
├── JSON data structures? → CRDTs
├── Cursor tracking only? → Simple WebSocket + presence
├── Offline-first? → CRDTs (better offline merge)
└── No conflicts possible? → Basic broadcast
技術の選択
競合解決戦略(2024年)
| 戦略 | 最適な用途 | 複雑さ | オフライン対応 |
|---|---|---|---|
| Operational Transform(OT) | テキスト、順序付きシーケンス | 高 | 限定的 |
| CRDT | JSON オブジェクト、セット | 中 | 優れている |
| Last-Write-Wins | シンプルな状態 | 低 | 基本的 |
| Three-Way Merge | Git 形式の編集 | 高 | 良好 |
タイムライン:
- 2010年:Google Wave が OT を採用
- 2014年:Figma が CRDT を採用
- 2019年:Yjs(CRDT ライブラリ)をリリース
- 2022年:Automerge 2.0(CRDT ライブラリ)をリリース
- 2024年:PartyKit がリアルタイムインフラストラクチャを簡素化
よくあるアンチパターン
アンチパターン 1:キー入力のたびにブロードキャストする
初心者の考え:「リアルタイム感を出すため、変更をすべて即座に送信する」
問題:ネットワークに小さなメッセージが大量に流れ、パフォーマンスが低下します。
誤った方法:
// ❌ Sends message on every keystroke
function Editor() {
const handleChange = (text: string) => {
socket.emit('text-change', { text }); // Every keystroke!
};
return <textarea onChange={(e) => handleChange(e.target.value)} />;
}
なぜ誤りなのか:毎分 100 語を入力すると、1 分あたり 500 件のメッセージが発生し、ネットワークが混雑します。
正しい方法:
// ✅ Batches changes every 200ms
function Editor() {
const [pendingChanges, setPendingChanges] = useState<Change[]>([]);
useEffect(() => {
const interval = setInterval(() => {
if (pendingChanges.length > 0) {
socket.emit('text-batch', { changes: pendingChanges });
setPendingChanges([]);
}
}, 200);
return () => clearInterval(interval);
}, [pendingChanges]);
const handleChange = (change: Change) => {
setPendingChanges(prev => [...prev, change]);
};
return <textarea onChange={handleChange} />;
}
効果:1 分あたり 500 件のメッセージから 1 秒あたり 5 件のメッセージへ(90%削減)。
アンチパターン 2:競合解決戦略がない
問題:同時編集により、データの消失や破損が発生します。
症状:変更が消えたり、ドキュメントの整合性が失われたりします。
誤った方法:
// ❌ Last write wins, overwrites concurrent changes
socket.on('text-change', ({ userId, text }) => {
setDocument(text); // Loses concurrent edits!
});
なぜ誤りなのか:ユーザー A と B が同時に編集すると、片方の変更が失われます。
正しい方法(OT):
// ✅ Operational Transform for text
import { TextOperation } from 'ot.js';
socket.on('operation', ({ userId, operation, revision }) => {
const transformed = transformOperation(
operation,
pendingOperations,
revision
);
applyOperation(transformed);
incrementRevision();
});
function transformOperation(
incoming: Operation,
pending: Operation[],
baseRevision: number
): Operation {
// Transform incoming against pending operations
let transformed = incoming;
for (const op of pending) {
transformed = TextOperation.transform(transformed, op)[0];
}
return transformed;
}
正しい方法(CRDT):
// ✅ CRDT for JSON objects
import * as Y from 'yjs';
const ydoc = new Y.Doc();
const ytext = ydoc.getText('document');
// Automatically handles conflicts
ytext.insert(0, 'Hello');
// Sync with peers
const provider = new WebsocketProvider('ws://localhost:1234', 'room', ydoc);
効果:同時編集が正しくマージされ、データが失われません。
アンチパターン 3:切断への対処がない
問題:ユーザーがオフラインになると、作業内容が失われたり、古い状態が表示されたりします。
誤った方法:
// ❌ No offline handling
socket.on('disconnect', () => {
console.log('Disconnected'); // That's it?!
});
なぜ誤りなのか:保留中の変更が失われ、再接続の戦略もないため、UX が悪化します。
正しい方法:
// ✅ Queue changes offline, sync on reconnect
const [isOnline, setIsOnline] = useState(true);
const [offlineQueue, setOfflineQueue] = useState<Change[]>([]);
socket.on('disconnect', () => {
setIsOnline(false);
showToast('Offline - changes will sync when reconnected');
});
socket.on('connect', () => {
setIsOnline(true);
// Send queued changes
if (offlineQueue.length > 0) {
socket.emit('sync-offline-changes', { changes: offlineQueue });
setOfflineQueue([]);
}
});
const handleChange = (change: Change) => {
if (isOnline) {
socket.emit('change', change);
} else {
setOfflineQueue(prev => [...prev, change]);
}
};
タイムラインの背景:
- 2015年:オフラインファーストのアプリはまれ
- 2020年:PWA によりオフライン UX が標準化
- 2024年:ユーザーはシームレスなオフライン編集を期待
アンチパターン 4:クライアントのみで状態を同期する
問題:サーバー側の権限がなく、クライアント間で状態がずれます。
誤った方法:
// ❌ Clients broadcast to each other directly
socket.on('peer-change', ({ userId, change }) => {
applyChange(change); // No validation, no server state
});
なぜ誤りなのか:悪意のあるクライアントが無効なデータを送信でき、同期ずれからの復旧もできません。
正しい方法:
// ✅ Server is source of truth
// Client
socket.emit('operation', { operation, clientRevision });
socket.on('ack', ({ serverRevision }) => {
if (serverRevision !== expectedRevision) {
// Desync detected, request full state
socket.emit('request-full-state');
}
});
// Server
io.on('connection', (socket) => {
socket.on('operation', ({ operation, clientRevision }) => {
// Validate operation
if (!isValid(operation)) {
socket.emit('error', { message: 'Invalid operation' });
return;
}
// Apply to server state
const serverRevision = applyOperation(operation);
// Broadcast to all clients
io.emit('operation', { operation, serverRevision });
});
});
効果:データの整合性が保証され、クライアントのバグから復旧できます。
アンチパターン 5:プレゼンス認識がない
問題:誰が何を編集しているのかユーザーにわからず、編集の競合が発生します。
症状:2人が同じセクションを編集していることに気づきません。
誤った方法:
// ❌ No awareness of other users
function Editor() {
return <textarea />; // Flying blind!
}
正しい方法:
// ✅ Show active users and cursors
import { usePresence } from './usePresence';
function Editor() {
const { users, updateCursor } = usePresence();
const handleCursorMove = (position: number) => {
socket.emit('cursor-move', { userId: myId, position });
};
return (
<div>
{/* Show who's online */}
<UserList users={users} />
{/* Show remote cursors */}
<EditorWithCursors
content={content}
cursors={users.map(u => u.cursor)}
onCursorMove={handleCursorMove}
/>
</div>
);
}
機能:
- アバター付きのアクティブユーザー一覧
- ユーザーごとに色分けされたカーソル位置
- ハイライト表示された選択範囲
- 「ユーザー X が入力中…」のインジケーター
実装パターン
パターン 1:再接続に対応した WebSocket のセットアップ
import { io } from 'socket.io-client';
const socket = io('ws://localhost:3000', {
reconnection: true,
reconnectionDelay: 1000,
reconnectionDelayMax: 5000,
reconnectionAttempts: Infinity,
transports: ['websocket', 'polling'] // Fallback
});
socket.on('connect', () => {
console.log('Connected:', socket.id);
});
socket.on('disconnect', (reason) => {
if (reason === 'io server disconnect') {
// Server disconnected, manually reconnect
socket.connect();
}
});
socket.on('connect_error', (error) => {
console.error('Connection error:', error);
});
パターン 2:Operational Transform(テキスト)
import { TextOperation } from 'ot.js';
class OTEditor {
private revision = 0;
private pendingOperations: TextOperation[] = [];
applyLocalOperation(op: TextOperation): void {
// Apply immediately (optimistic update)
this.applyToEditor(op);
// Send to server
this.sendOperation(op);
// Store as pending
this.pendingOperations.push(op);
}
receiveRemoteOperation(op: TextOperation, serverRevision: number): void {
// Transform against pending operations
let transformed = op;
for (const pending of this.pendingOperations) {
[transformed, pending] = TextOperation.transform(transformed, pending);
}
// Apply transformed operation
this.applyToEditor(transformed);
this.revision = serverRevision;
}
acknowledgeOperation(serverRevision: number): void {
// Remove acknowledged operation from pending
this.pendingOperations.shift();
this.revision = serverRevision;
}
}
パターン 3:Yjs を使った CRDT
import * as Y from 'yjs';
import { WebsocketProvider } from 'y-websocket';
// Create shared document
const ydoc = new Y.Doc();
// Define shared types
const ytext = ydoc.getText('content');
const ymap = ydoc.getMap('metadata');
const yarray = ydoc.getArray('users');
// Connect to sync server
const provider = new WebsocketProvider(
'ws://localhost:1234',
'room-name',
ydoc
);
// Listen to changes
ytext.observe(event => {
console.log('Text changed:', event.changes);
});
// Make changes (automatically synced)
ytext.insert(0, 'Hello ');
ytext.insert(6, 'World!');
// Undo/redo support
const undoManager = new Y.UndoManager(ytext);
undoManager.undo();
undoManager.redo();
パターン 4:プレゼンス認識
import { Awareness } from 'y-protocols/awareness';
const awareness = provider.awareness;
// Set local state
awareness.setLocalState({
user: {
name: 'Alice',
color: '#ff0000',
cursor: { line: 10, ch: 5 }
}
});
// Listen to changes
awareness.on('change', ({ added, updated, removed }) => {
// Update UI with user cursors/selections
const states = awareness.getStates();
states.forEach((state, clientId) => {
if (clientId !== awareness.clientID) {
renderCursor(state.user.cursor, state.user.color);
}
});
});
パターン 5:ロールバックに対応した楽観的更新
class OptimisticEditor {
private optimisticChanges = new Map<string, Change>();
async applyChange(change: Change): Promise<void> {
const changeId = generateId();
// Apply immediately (optimistic)
this.applyToUI(change);
this.optimisticChanges.set(changeId, change);
try {
// Send to server
const result = await this.sendToServer(change);
// Success - remove from optimistic
this.optimisticChanges.delete(changeId);
} catch (error) {
// Failed - rollback
this.rollback(changeId);
this.showError('Could not apply change');
}
}
private rollback(changeId: string): void {
const change = this.optimisticChanges.get(changeId);
if (change) {
this.revertInUI(change);
this.optimisticChanges.delete(changeId);
}
}
}
本番環境のチェックリスト
□ WebSocket connection with auto-reconnect
□ Offline queue for pending changes
□ Conflict resolution strategy (OT or CRDT)
□ Server authority (clients can't desync)
□ Presence awareness (cursors, active users)
□ Optimistic updates with rollback
□ Change batching (not per-keystroke)
□ Message compression for large payloads
□ Authentication and authorization
□ Rate limiting (prevent spam)
□ Heartbeat/ping-pong to detect dead connections
□ Graceful degradation (falls back to polling if WebSocket fails)
使用する場面と避ける場面
| シナリオ | 戦略 |
|---|---|
| テキスト編集(Google Docs) | ✅ Operational Transform |
| JSON オブジェクト(Figma) | ✅ CRDT(Yjs、Automerge) |
| シンプルなカーソル共有 | ✅ 基本的な WebSocket +プレゼンス |
| チャットメッセージ | ✅ シンプルな追記のみ(OT/CRDT なし) |
| 動画タイムラインの編集 | ✅ タイムラインには CRDT、テキストには OT |
| 読み取り専用のダッシュボード | ❌ 代わりに Server-Sent Events を使用 |
参考資料
/references/ot-vs-crdt.md- 競合解決戦略の詳細な比較/references/websocket-scaling.md- 数百万件の同時接続へのスケーリング/references/presence-patterns.md- カーソル追跡、ユーザーの状況把握、アクティビティインジケーター
スクリプト
scripts/collaboration_tester.ts- 同時編集をシミュレートし、競合解決をテストしますscripts/latency_simulator.ts- 高レイテンシ/パケットロス環境での動作をテストします
このスキルの対象:リアルタイムコラボレーション | WebSocket アーキテクチャ | オペレーショナルトランスフォーム | CRDT | プレゼンス認識 | 競合解決