Open3

Linear's Sync Engine解説

catatsuycatatsuy

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 フロー

  1. ObjectStore生成

    • モデルごとにFullObjectStoreまたはPartialObjectStoreを生成
    • partialの場合は部分インデックス用DBも作成
  2. IndexedDB構築

    • _metaテーブル(lastSyncId, firstSyncId, userSyncGroups等)
    • _transactionsテーブル(未送信Tx)
    • モデルごとのテーブル
  3. サーバ要求

    • /sync/bootstrap?type=full&onlyModels=...
    • JSONストリームでモデルデータを受信
  4. 保存 & 水和

    • IndexedDBに保存
    • instantモデルのみhydrateしてObject Poolに登録
  5. 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 適用フロー

  1. SyncGroup追加/削除確認

  2. 依存モデルの事前取得

  3. IndexedDB更新

  4. メモリ上のモデル更新

  5. ローカル未確定Txのrebase

    • LWWで競合解決
  6. lastSyncId更新 & 完了待ちTx解放


7. Undo/Redo

  • トランザクション単位でundoTransaction()を実装
  • 実行時に逆方向のトランザクションを生成して送信
  • Undo/RedoスタックはUI層で管理し、Txキューと連動

8. 設計上のキーポイント

  • 中央集権型 + 総順序

    • CRDTのメタデータ肥大や権限制御困難を回避
    • OTの複雑な変換関数不要
  • IndexedDB取得済みインデックス管理

    • 遅延水和時にネットワーク呼び出しを最小化
  • サーバ承認前にローカルDB更新しない

    • 整合性を保ち、ロールバックを容易にする
  • LWW競合解決

    • 意図保存よりも単純さと性能を優先
catatsuycatatsuy

9. 詳細シーケンス図

9.1 CreationTransaction + サーバ副作用履歴生成

このシーケンスは、新規モデル作成時にローカル保存 → サーバ送信 → サーバ側副作用(履歴生成など)が行われる流れを示します。

ポイント

  • CreationTransactionは全プロパティを持ち、サーバ側でモデルインスタンスを完全構築可能
  • サーバは副作用モデル(履歴等)も同時生成し、同一Delta Packに含めて配信
  • 副作用の受信順は保証される(lastSyncId単位で総順序)

9.2 部分Bootstrap再取得(Partial Bootstrap Refresh)

IndexedDBに存在しない依存モデルを参照した場合の部分再同期の流れです。

ポイント

  • 部分インデックスキャッシュは取得済み判定のキー
  • サーバクエリには対象モデル + フィルタ条件を含む
  • partial bootstrapはinstantlazyには影響を与えない

10. トラブルシューティング指針

10.1 lastSyncIdの不一致

症状

  • 同期が止まる
  • 未確定Txが解放されない

原因

  • Delta Packet欠落(WebSocket切断・サーバリトライ失敗)
  • _metaテーブルのlastSyncId破損

対処

  1. /sync/bootstrap?type=partial&fromSyncId=<lastSyncId>で再同期
  2. IndexedDBの_metaを最新サーバ値に更新

10.2 部分インデックス破損

症状

  • 本来取得済みのデータが再取得される
  • 無限partial bootstrapループ

原因

  • Partial Index Cache消失(IndexedDB削除)
  • PIdxとモデル表の不整合

対処

  1. PIdx再構築(現行モデル表からインデックス作り直し)
  2. 必要に応じてfull bootstrapを実行

10.3 Undo/Redoの競合

症状

  • Undo後に他ユーザーの編集が上書きされる
  • Redoで不正な状態になる

原因

  • LWWの仕様によりサーバ側更新が優先される
  • Undo対象Txの同期前に別ユーザーが同じフィールドを編集

対処

  • Undo/Redo時に_conflictフラグを付与しUI警告
  • 必要に応じて手動マージ

11. 運用のベストプラクティス

  1. IndexedDBスキーマ管理
    バージョンアップ時は_metaにスキーマバージョンを保持し、互換性がない場合は自動full bootstrap。

  2. 部分Bootstrapの制御
    頻発するとネットワーク負荷が高まるため、UI要求を短時間バッチ化してまとめて取得する。

  3. トランザクション粒度の最適化
    高頻度UIイベント(ドラッグなど)は1Txにまとめ、debounce送信。

  4. SyncGroupの適切な利用
    プロジェクト単位でSyncGroupを分けると、不要なDelta受信を削減可能。