mcpField
R
real-time-collaboration-engine

real-time-collaboration-engine

github.com/curiositech開発257185 回インストール2 日前に更新
リポジトリ

real-time-collaboration-engine について

Real-Time Collaboration Engineは、共同編集アプリを開発する人向けのスキルで、WebSocket、競合解決、プレゼンスを扱います。データ形式やオフライン要件に応じて、OT、CRDT、基本的なブロードキャストの選択を案内します。

主な機能

  1. 01WebSocketの再接続と接続パターン
  2. 02OTとCRDTによる競合解決
  3. 03プレゼンスとカーソル共有
  4. 04オフライン変更のキューイングと同期
  5. 05変更のバッチ処理と楽観的更新

活用シーン

  1. 01共同編集テキストドキュメントの構築
  2. 02共有ホワイトボードやデザインツールの実装
  3. 03複数人で使う動画編集タイムラインの同期
  4. 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.json
references/
ot-vs-crdt.md
presence-patterns.md
websocket-scaling.md
scripts/
collaboration_tester.ts
latency_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)テキスト、順序付きシーケンス高限定的
CRDTJSON オブジェクト、セット中優れている
Last-Write-Winsシンプルな状態低基本的
Three-Way MergeGit 形式の編集高良好

タイムライン:

  • 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 | プレゼンス認識 | 競合解決

コメント