← ユースケース

共同作業:誰がいるか、ライブカーソル、編集ロック

同じドキュメントやチケットを見ている全員のアバターを表示し、カーソルとフォーカスを共有、保存した変更を配信して、誰かの編集を上書きすることを防ぎます。

2 人が知らずに同じレコードを編集すると、どちらかの作業が消えてしまいます。同じドキュメント、チケット、デザインを誰が見ているか、どこにいるかを表示すれば、それを防げるうえに、プロダクトに「一緒に使っている」感覚が生まれます。プロジェクト管理ツール、CRM、ヘルプデスク、ホワイトボード、フォームビルダーなどで役立ちます。

ユーザーにとっての価値

  • 同じページを見ている全員のアバター。人の出入りに合わせて更新されます。
  • ライブカーソルや、誰かが編集中のフィールドのハイライト。
  • 保存の衝突ではなく、はっきりした「Ann がこのチケットを編集中」のロック表示。
  • 他の人の変更が、再読み込みなしでその場に反映されます。

使う機能

  • プレゼンスチャンネル(presence-)。ドキュメントごとに 1 つ、メンバー一覧はすべてのノードで正しく保たれます。
  • クライアントイベント(client-*)。カーソル位置やフォーカスをブラウザ間で送ります。
  • サーバーイベント。保存した変更を、サーバーが保存してから配信します。
  • ラージプレゼンスチャンネル(presence-large-)。同時に 100 人を超える閲覧者がいるページに。

ステップ 1:誰がいるかを表示する

presence-doc-<id> は、ドキュメント自体へのアクセスと同じ基準で認可し、user_info として名前とアバターを返します(ルートはチャットと入力中表示を参照)。続いて:

const doc = pusher.subscribe('presence-doc-' + docId);

doc.bind('pusher:subscription_succeeded', (members) => avatars.render(members));
doc.bind('pusher:member_added', () => avatars.render(doc.members));
doc.bind('pusher:member_removed', (m) => {
  avatars.render(doc.members);
  cursors.remove(m.id);
});

2 つのタブで開いているユーザーは 1 人として数えます。最初のソケットで追加され、最後のソケットが閉じたときに削除されます。

ステップ 2:カーソルとフォーカスを共有する

アプリの設定で クライアントイベントを許可 をオンにします。位置は変わったときだけ、1 秒に数回までに抑えて送ります:

let pending = null;
document.addEventListener('pointermove', (e) => {
  pending = { x: e.pageX / document.body.scrollWidth, y: e.pageY };
});
setInterval(() => {
  if (!pending) return;
  doc.trigger('client-cursor', pending);
  pending = null;
}, 200);

doc.bind('client-cursor', (pos, meta) => cursors.move(meta.user_id, pos));

// Field locks: who is editing what
field.addEventListener('focus', () => doc.trigger('client-focus', { field: field.name }));
doc.bind('client-focus', ({ field }, meta) => locks.show(field, doc.members.get(meta.user_id)));

プレゼンスチャンネルでは、クライアントイベントのハンドラーの 2 番目の引数に送信者の user_id が入っています。

ステップ 3:保存した変更を配信する

誰かが保存したら、変更を保存してから他の人に配信します:

app.patch('/docs/:id', requireLogin, async (req, res) => {
  const change = await db.docs.apply(req.params.id, req.user.id, req.body.patch);
  await pusher.trigger('presence-doc-' + req.params.id, 'changed', { version: change.version, patch: req.body.patch, by: req.user.id }, { socket_id: req.body.socket_id });
  res.json(change);
});

他の閲覧者はパッチを適用するか、自分のバージョンが古ければレコードを読み込み直します。

100 人を超える閲覧者

プレゼンスチャンネルのメンバーは最大 100 人です。全員が一覧全体を受け取るためです。全社向けのお知らせやライブイベントのように多くの人が見るページでは、presence-large-doc-<id> と @hibira-io/large-presence を使いましょう。最大 100,000 人まで入れられ、参加のたびのイベントではなく人数とサンプルを送ります。そこではクライアントイベントは使えないので、カーソルの共有は小さなルームだけにします。

ヒント

  • クライアントイベントは 1 件ごとに 1 メッセージです。10 人の編集者が 1 秒に 5 回カーソルを送れば毎秒 50 メッセージなので、変化したときだけ送り、タブが非表示のときは頻度を下げましょう。
  • 正となるデータはサーバーに置きます。クライアントイベントはカーソルやフォーカスのようなヒント用で、保存されず、送信者には返りません。
  • 本格的な共同テキスト編集には、Yjs や Automerge などの CRDT ライブラリの操作を、同じようにサーバー経由で送ります。