はじめに
最近、Notionとか Google Docs みたいなリアルタイムコラボレーション機能を自分のアプリに入れたいと思ったことありませんか。個人的には結構あるんですよね。
ただ、これを自前で実装しようとすると、競合解決とかオフライン対応とか、考えることが山ほどあって正直なところ大変です。そこで注目したいのが Automerge というライブラリ。
Automergeは、CRDT(Conflict-free Replicated Data Type)という技術をベースにした「ローカルファースト同期エンジン」です。GitHubスター数は5.8k超え、Herokuの共同設立者やケンブリッジ大学のMartin Kleppmann教授らが開発に関わっているという、なかなか信頼できるプロジェクトなんですよ。
特徴・メリット
Automergeの良いところを整理してみます。
1. オフライン完全対応
これ、意外と重要なんですよね。ネットワークが切れても普通に編集できて、再接続したら自動的に同期される。電車の中とか飛行機の中でも作業できるのは地味にQOL上がります。
2. 自動マージで競合解決
複数人が同時に編集しても、CRDTの仕組みで一貫性のあるマージを自動実行してくれます。データ損失がないのが安心ポイント。「あ、上書きされた...」みたいな事故がなくなります。
3. バージョン履歴の完全保持
Gitみたいに全ての変更履歴を持っているので、過去の状態に戻したり、ブランチを切って実験したりもできる。ドキュメント管理としては最強クラスですね。
4. マルチ言語対応
JavaScript、Rust がメインですが、Swift、Python、C、Java へのバインディングもあります。コアがRust実装でWebAssemblyに変換されているので、パフォーマンスも良好。
5. 柔軟なネットワーク構成
P2P、クライアント・サーバー、ファイルディスク、なんならメール添付まで対応。どんな環境でも使えるのは嬉しいところです。
インストール方法
npmでサクッと入れられます。
npm install @automerge/automerge
Yarnの場合はこちら。
yarn add @automerge/automerge
バンドルサイズは約219KB(Minified + Gzipped)。まあ許容範囲かなという感じですね。
基本的な使い方
実際のコードを見ていきましょう。
ドキュメントの作成と編集
import * as Automerge from '@automerge/automerge'
// 型定義
interface TaskList {
tasks: Array<{ title: string; done: boolean }>
}
// 新しいドキュメントを作成
let doc = Automerge.init<TaskList>()
// ドキュメントを変更(イミュータブルに新しいドキュメントを返す)
doc = Automerge.change(doc, 'タスクを追加', doc => {
doc.tasks = []
doc.tasks.push({ title: '牛乳を買う', done: false })
doc.tasks.push({ title: 'コードレビュー', done: false })
})
console.log(doc.tasks)
// [{ title: '牛乳を買う', done: false }, { title: 'コードレビュー', done: false }]
複数ユーザーの変更をマージ
// ドキュメントをクローン(別ユーザーをシミュレート)
let doc1 = Automerge.clone(doc)
let doc2 = Automerge.clone(doc)
// ユーザー1がタスクを完了にする
doc1 = Automerge.change(doc1, 'タスク完了', doc => {
doc.tasks[0].done = true
})
// ユーザー2が新しいタスクを追加
doc2 = Automerge.change(doc2, '新タスク追加', doc => {
doc.tasks.push({ title: 'ミーティング準備', done: false })
})
// 両方の変更をマージ
const mergedDoc = Automerge.merge(doc1, doc2)
console.log(mergedDoc.tasks)
// 両方の変更が反映される!
// [
// { title: '牛乳を買う', done: true },
// { title: 'コードレビュー', done: false },
// { title: 'ミーティング準備', done: false }
// ]
変更の同期(ネットワーク越し)
// 変更を取得(バイナリ形式)
const changes = Automerge.getChanges(doc, mergedDoc)
// 別のクライアントで変更を適用
let remoteDoc = Automerge.init<TaskList>()
remoteDoc = Automerge.applyChanges(remoteDoc, changes)[0]
正直、このAPIのシンプルさは良いですね。イミュータブルな設計なのでReactとの相性も抜群です。
実践的なユースケース
1. 共同編集ドキュメント
NotionやGoogle Docsみたいなリアルタイム共同編集。ProsemirrorやCodemirror用のプラグインも公式で提供されているので、テキストエディタとの統合も比較的楽にできます。
2. オフラインファーストなモバイルアプリ
電波が不安定な環境でも動作するメモアプリやTODOアプリ。オフラインで編集 → オンラインで自動同期、という体験がスムーズに作れます。
3. P2Pアプリケーション
サーバーレスで動作する分散アプリ。WebRTCと組み合わせれば、中央サーバーなしでデータ同期ができる。これ、個人開発でサーバー代を節約したい時に良いかもしれません。
4. ゲームのセーブデータ同期
複数デバイス間でのセーブデータ同期。競合が起きても自動解決されるので、「どっちが新しいセーブ?」問題が発生しない。
まとめ
Automergeは、リアルタイムコラボレーションやオフライン対応を実現したいときの有力な選択肢です。
個人的に気に入っているポイントをまとめると:
- 学習コストが低い:JSONライクなAPIで直感的に使える
- オフライン対応が標準装備:追加実装なしでオフラインファースト
- 競合解決を考えなくていい:CRDTが自動でやってくれる
- TypeScript対応:型安全に開発できる
30代になって思うのは、「車輪の再発明をしない」ことの大切さなんですよね。競合解決のアルゴリズムを自前で書くより、こういった検証済みのライブラリを使う方がコスパ的に正解だと思います。
ローカルファーストなアプリを作りたい人は、一度試してみる価値ありですよ。
