Linear's Sync Engine解説
Linear Sync Engine 詳解レポート
0. 概要
Linear Sync Engine(LSE)は、Linear社が提供するコラボレーション機能の中核であり、リアルタイム同期・部分同期・オフライン動作・権限制御・Undo/Redoを任意データモデルで実現する基盤です。
特徴は以下の通りです。
- 中央集権型 + 総順序同期(lastSyncId)
- ORMライクなモデル定義 & MobXによるリアクティブ化
- IndexedDBローカル永続化 & 部分インデックスによる効率的な遅延水和
- サーバ承認前にローカルDBを書き換えない整合性モデル
- シンプルなLWW(Last-Writer-Wins)競合解決
1. モデル定義とメタデータ管理
1.1 ModelRegistry
-
役割: 全モデルのメタデータを集中管理する辞書。
-
登録情報:
- モデル名→コンストラクタ
- プロパティ定義(型, serializer, indexed, nullable, lazy 等)
- 参照定義(
reference,referenceCollectionなど) - ロード戦略(
instant,lazy,partial,explicitlyRequested,local)
-
登録方法: TypeScriptデコレータ(
@ClientModel,@Property,@Referenceなど)
1.2 プロパティ種別
-
property: モデル自身が持つ値(永続化対象) -
ephemeralProperty: 永続化しない一時値 -
reference: 他モデルのIDを持つ(永続化対象) -
referenceModel:referenceのIDをモデルインスタンスに解決するgetter/setter -
referenceCollection: 1:Nの配列参照 -
backReference: 他モデルに所有される逆参照 -
referenceArray: 多対多関係
1.3 ロード戦略
- instant: 起動時に全件ロード
- lazy: 必要時にまとめてロード
- partial: 必要な部分のみロード(部分インデックス使用)
- explicitlyRequested: 明示要求時のみロード
- local: IndexedDBのみ保持(実験的機能)
2. データフロー概要
2.1 全体図
3. 起動〜初回同期(Bootstrap)
3.1 ブートストラップ種別
- full: サーバから全モデルを取得
- partial: モデルの一部を取得(loadStrategy=partialのみ)
- local: IndexedDBのみ読み込み
3.2 full bootstrap フロー
-
ObjectStore生成
- モデルごとに
FullObjectStoreまたはPartialObjectStoreを生成 - partialの場合は部分インデックス用DBも作成
- モデルごとに
-
IndexedDB構築
-
_metaテーブル(lastSyncId, firstSyncId, userSyncGroups等) -
_transactionsテーブル(未送信Tx) - モデルごとのテーブル
-
-
サーバ要求
/sync/bootstrap?type=full&onlyModels=...- JSONストリームでモデルデータを受信
-
保存 & 水和
- IndexedDBに保存
-
instantモデルのみhydrateしてObject Poolに登録
-
WebSocket接続
-
lastSyncId確認 - 欠落デルタがあれば再取得
-
4. 遅延水和(Lazy Hydration)と部分インデックス
4.1 概要
必要な時だけサーバ/IndexedDBからデータをロードし、モデルをhydrateする。
**部分インデックス(Partial Index)**を使って「何を取るか」を決定する。
4.2 フロー図
5. トランザクション同期
5.1 生成
- setterが
markPropertyChanged()を呼び、変更前値と新値を記録 -
save()でUpdateTransaction生成 -
TransactionQueueに投入、同一tick内のTxをバッチ化 -
_transactionsに永続化
5.2 送信
- GraphQLミューテーションとして送信
- レスポンスで
lastSyncId受信 → そのデルタ受信まで完了待ち
6. デルタパケット受信と適用
6.1 Delta Packet構造
-
id(syncId) -
modelName,modelId -
action(I/U/A/D/C/G/S/V) -
data(更新後モデルデータ)
6.2 適用フロー
-
SyncGroup追加/削除確認
-
依存モデルの事前取得
-
IndexedDB更新
-
メモリ上のモデル更新
-
ローカル未確定Txのrebase
- LWWで競合解決
-
lastSyncId更新 & 完了待ちTx解放
7. Undo/Redo
- トランザクション単位で
undoTransaction()を実装 - 実行時に逆方向のトランザクションを生成して送信
- Undo/RedoスタックはUI層で管理し、Txキューと連動
8. 設計上のキーポイント
-
中央集権型 + 総順序
- CRDTのメタデータ肥大や権限制御困難を回避
- OTの複雑な変換関数不要
-
IndexedDB取得済みインデックス管理
- 遅延水和時にネットワーク呼び出しを最小化
-
サーバ承認前にローカルDB更新しない
- 整合性を保ち、ロールバックを容易にする
-
LWW競合解決
- 意図保存よりも単純さと性能を優先
9. 詳細シーケンス図
9.1 CreationTransaction + サーバ副作用履歴生成
このシーケンスは、新規モデル作成時にローカル保存 → サーバ送信 → サーバ側副作用(履歴生成など)が行われる流れを示します。
ポイント
- CreationTransactionは全プロパティを持ち、サーバ側でモデルインスタンスを完全構築可能
- サーバは副作用モデル(履歴等)も同時生成し、同一Delta Packに含めて配信
- 副作用の受信順は保証される(lastSyncId単位で総順序)
9.2 部分Bootstrap再取得(Partial Bootstrap Refresh)
IndexedDBに存在しない依存モデルを参照した場合の部分再同期の流れです。
ポイント
- 部分インデックスキャッシュは取得済み判定のキー
- サーバクエリには対象モデル + フィルタ条件を含む
- partial bootstrapは
instantやlazyには影響を与えない
10. トラブルシューティング指針
10.1 lastSyncIdの不一致
症状
- 同期が止まる
- 未確定Txが解放されない
原因
- Delta Packet欠落(WebSocket切断・サーバリトライ失敗)
- _metaテーブルのlastSyncId破損
対処
-
/sync/bootstrap?type=partial&fromSyncId=<lastSyncId>で再同期 - IndexedDBの_metaを最新サーバ値に更新
10.2 部分インデックス破損
症状
- 本来取得済みのデータが再取得される
- 無限partial bootstrapループ
原因
- Partial Index Cache消失(IndexedDB削除)
- PIdxとモデル表の不整合
対処
- PIdx再構築(現行モデル表からインデックス作り直し)
- 必要に応じてfull bootstrapを実行
10.3 Undo/Redoの競合
症状
- Undo後に他ユーザーの編集が上書きされる
- Redoで不正な状態になる
原因
- LWWの仕様によりサーバ側更新が優先される
- Undo対象Txの同期前に別ユーザーが同じフィールドを編集
対処
- Undo/Redo時に
_conflictフラグを付与しUI警告 - 必要に応じて手動マージ
11. 運用のベストプラクティス
-
IndexedDBスキーマ管理
バージョンアップ時は_metaにスキーマバージョンを保持し、互換性がない場合は自動full bootstrap。 -
部分Bootstrapの制御
頻発するとネットワーク負荷が高まるため、UI要求を短時間バッチ化してまとめて取得する。 -
トランザクション粒度の最適化
高頻度UIイベント(ドラッグなど)は1Txにまとめ、debounce送信。 -
SyncGroupの適切な利用
プロジェクト単位でSyncGroupを分けると、不要なDelta受信を削減可能。