この記事はAtrae Advent Calendar 2024の6日目の記事です。前回は@motonosuke_devによる「改善余地のあるコードから考える学びの重要性」でした。
今日はリアルタイム共同編集が実現できるライブラリのLoroを試してみようと思います。似たようなライブラリには、Yjsがありますが、筆者の個人的な感想としてはYjsよりドキュメントがわかりやすく、かつタイムトラベル機能が容易に実装できるのが魅力的です。
Examples – LoroThe examples of basic usage in Loroloro.dev
Loroとは
LoroはCRDT(Conflict-free Replicated Data Types)というデータ構造を活用したライブラリで、リアルタイムの共同編集やバージョン管理機能をアプリケーションに組み込むことができます。
Loro – Reimagine state management with CRDTsLoro - Reimagine state management with CRDTs | Built for local-first software.loro.dev
簡単に言うと、複数人で同時にデータを操作しても競合を起こさないでいい感じにマージしてくれるライブラリです。
同時編集中に接続が切れオフラインで操作をしても、オンライン復帰時に他の人の操作も含めていい感じにマージしてくれます。
CRDTについてはCRDT (Conflict-free Replicated Data Type)を15分で説明してみるが参考になるのでぜひご覧ください。
CRDT (Conflict-free Replicated Data Type)を15分で説明してみる - QiitaCRDTについて勉強したので纏めてみました。15分くらいでざっとわかったつもりになれる感じで纏めてみたつもりです。全体スライドSlideshareのスライドが埋め込めなかったので、↓からアクセス…qiita.com
使い方
使い方は意外と簡単です。LoroDocと呼ばれるものを初期化して、好みのデータを追加したり削除したりしていきます。
サポートしてるデータ形式はList,Text,Mapなどです。詳しくは公式ドキュメントを参照してください。
共同で編集する場合は複数のLoroDocを同期させる必要がありますが、exportメソッドとimportメソッドが用意されているのでそれを使います。変更を共有したいLoroDocの内容をexportして、変更を取り込みたいLoroDocでimportするといった具合です。
exportメソッドは、用途に応じてエンコードモードを使い分けます。
以下は同時に同じmapを操作したときの例です。Loroのmapは最後に編集したものが反映されるLWW(Last Writer Wins)というポリシーを使用しています。また同時編集でコンフリクトが起きた際には、Lamport timestampの順序決定アルゴリズムを使用して競合を解消しているらしいです。
実際にアプリケーションの組み込んでみる
今回はホワイトボードライブラリの tldraw を使って、共同編集可能なホワイトボードを作ってみます。今回はサーバーでは状態を持たず、ひたすら差分を垂れ流すだけにして、各クライアントがLoroDocを管理する形にしてみます。
サンプルを用意しましたのでぜひ参考にしてみてください。
GitHub - mr04vv/tldraw-loro: loroを使ったtldrawのサンプルリポジトリloroを使ったtldrawのサンプルリポジトリ. Contribute to mr04vv/tldraw-loro development by creating an account on GitHub.github.com
受け取ったメッセージを他のクライアントに送信するWebsocketサーバーを作る
まずはメッセージを垂れ流すだけのWebsocketサーバーを作ります。
実態はほぼ33行目と38行目で、来たデータをそのまま他のクライアントに送り返してるだけです。
tldrawを使ったクライアントアプリを作る
続いてクライアントです。
tldraw-loro/apps/client at main · mr04vv/tldraw-loroloroを使ったtldrawのサンプルリポジトリ. Contribute to mr04vv/tldraw-loro development by creating an account on GitHub.github.com
クライアントの処理の流れは大体以下のような感じです。
- tldrawの状態の変化を検知し、LoroDocを更新
- LoroDocの更新を検知してWebsocketに変更差分を送信
- (Websocketが他のクライアントに差分を垂れ流す)
- Websocketから送られてきた差分をLoroDocに適用
- LoroDocの更新を検知してtldrawに反映
1. tldrawの状態の変化を検知し、LoroDocを更新
tldrawのボード上にオブジェクトが追加されたり、更新・削除された場合、tldrawのstoreのイベントハンドラで変更を検知できます。イベントハンドラ内で、取得した変更を元にローカルのLoroDocを更新します。
2. LoroDocの更新を検知してWebsocketに変更差分を送信
ステップ1の13行目でのLoroDocの更新によって、Mapのイベントハンドラが発火します。Mapのイベントハンドラ内では、ローカルの変更をWebsocketサーバーに送信する処理を追加します。
versionRefはステップ4で説明します。
3. (Websocketが他のクライアントに差分を垂れ流す)
4. Websocketから送られてきた差分をLoroDocに適用
ステップ2で変更差分が送信元以外のクライアントに送信されます。差分を受け取ったクライアントは、LoroDocに適用します。
この時LoroDocを更新したタイミングのバージョンをrefで保持しておきます。そうすることで、状態更新時に前のバージョンとの差分のみを取り出して送信できます。
5. LoroDocの更新を検知してtldrawに反映
Loroではsubscribeメソッドが用意されていて、Docの変更をトリガーとしたイベントハンドラをセットできます。
イベントハンドラ内では、ローカルでの更新(map.set("hoge")等の直接の更新)とリモートの更新(importメソッドを使用した更新)を区別できるので、以下のようにそれぞれ処理を分けることができます。
自分の操作(→Websocketに送信)と他のクライアントから送信されたもの(→tldrawに反映)を区別するために、先程のMapのイベントハンドラに条件分岐を追加します。
うまくいくとこんな感じでリアルタイムに同期されたホワイトボードができるはずです。
また、ドキュメントには書いてないのですが、Awarenessの実装もあるのでちょっと工夫すればユーザーカーソルの同期のように永続化しなくても良い情報の同期もよしなにできるようになります。
AwarenessについてはYjsのドキュメントを読んだほうが分かりやすいかもしれません。概念はおそらくほぼ同じです。
Websocketサーバーに一手間加えて永続化できるようにしてあげればリロードしても他のクライアントと同期できるようにもなります。
まとめ
今回はLoroを使ってリアルタイムに共同編集できるホワイトボードを作ってみました。Yjsと比較してもドキュメントがわかりやすく操作も直感的な気がします。
履歴機能も割と簡単に作れちゃうのでぜひ皆さんも試してみてください。
CloudflareのDurable Objectsと組み合わせて使えたらかなり面白そうということで、後日Honoのアドベントカレンダーでチャレンジしようと思います。
それではまた次回お会いしましょう。
宣伝
我が家の猫のLGTM画像サイトです。ぜひご自由に使ってください。
Lgtlatte - らてのLGTM画像飼い猫らてのLGTM画像を集めましたlgtlatte.mooriii.com
次回の7日目は @muttsu_623による「Androidアプリエンジニアからフルスタックエンジニアになった話 a.k.a. バックエンドの学び方【後編】」です。