共同編集
ツールレール 👥 共有 で表示名を入れて「参加」を押すと、同じルームの人と 視点・選択範囲・非破壊編集をリアルタイムに共有します。
招待する
ルーム ID は既定で UUID(推測できないランダム値)です。 共有はパネルの「招待リンク」ボタン、またはコンソールの oniyanma.inviteLink() で作ります。
https://demo.oniyanma.jp/?collab=wss://…&room=<uuid>&name=<あなた>このリンクを開いた人は自動で同じルームに参加します。 ID は localStorage に残るので、リロードしても同じルームに戻ります。
認証はまだありません
サーバ側に認証がなく、ルーム ID を知っている人は誰でも相席できます。 だからこそ既定 ID を推測できない値にして、共有経路を招待リンクに一本化しています。 「既定ルームが推測できない」ことが、そのままいまのアクセス制御です (この性質が破れても画面上は正常に見え、黙って相席されるので、 ルーム ID の決定ロジックは回帰テストで固定してあります)。 identity と権限は、要件が固まってから上載せする予定です。
何が共有されるか
視点。 他の参加者のカメラが視錐台として表示されます。 プレゼンス一覧で名前をクリックすると、その人の視点を追従します (自分が操作すると追従は解除されます)。
選択範囲。 「ここ見て」がそのまま渡せます。 共有されるのは点の一覧ではなく述語なので、 プレゼンス一覧の「◻ 選択 n 点」を押すと、自分の画面でも厳密に同じ点が選ばれます。
座標フレームが双方判明していて一致しない場合は、枠を描かずに理由を出します (「◻ 選択あり(座標フレームが違うため表示できません)」)。 黙ってずれた枠を出すほうが有害だからです。
非破壊編集。 編集ログが Yjs の共有配列で同期されます。Undo / Redo / クリアも伝播します。
所見。 所見と型定義も同期されます。ただし編集ログとは別のルーム(<room>:entries)で、 アプリが所有する Doc に provider を後付けする形なので、退室しても所見は手元に残ります。 参加前に起票した分は接続時に合流します → 協働での共有
ユーザーごとの Undo
⌘Z が戻すのは自分が出した編集だけです。他人の編集は残ります。 ログの途中にある自分の 1 件だけが消え、Redo は元の位置へ戻すので、 後勝ちの関係も壊れません。
「編集をクリア」だけは全員分を消すので、協働中は件数付きの確認が出ます。
同期サーバ
本番の同期サーバは Cloudflare Durable Objects です(packages/collab)。 ルーム = DO インスタンス(idFromName)なので、ルーム分離と接続管理をランタイムが持ちます。
WebSocket の受け口は Pages Functions で、そこから DO へ転送します。 これは配信レイテンシの都合です——無料ゾーンの独自ドメインと per-account の *.workers.dev は 米西海岸の PoP に回されますが(実測 min 103〜138 ms)、*.pages.dev は東京から返ります (実測 min 9.5 ms / colo=NRT)。WS の接続先はアプリの配信元と別ホストでよいので、 同期だけ wss://<pages>/collab に寄せています。
プロトコル実装(src/room.ts)はランタイム非依存で、 ローカル開発用の Node 版(packages/server/src/collab.ts)と同じコードを共有します。 二重実装にすると「ローカルでは動くが本番で壊れる」が起きるためです。
ローカルで動かす:
pnpm dev:all # アプリ + 中継 + 協働(Node版, ws://8788)
pnpm collab:dev # Durable Objects 版(wrangler dev / 本番と同じ実装)
pnpm collab:smoke # Worker のワイヤ疎通テスト接続先を明示するなら、パネルの「詳細設定 → 同期サーバ」か ?collab= です。 ビルド時に焼き込むなら VITE_COLLAB_URL。
永続化はされない
サーバ側に永続化はありません。全員が退出するとルームは破棄されます。 編集はローカルに自動保存されるので手元には残りますが、 成果物として残すならサイドカーに保存してください。
コンソールから
oniyanma.joinRoom('wss://…', '<room-id>', '表示名')
oniyanma.inviteLink() // 招待 URL を返す
oniyanma.showPeerMarkers(false) // ピアの視点・選択マーカーを消す
oniyanma.leaveRoom()