# 機能設計書 24-ローカルファースト設計

## 概要

本ドキュメントは、Actual Budget におけるローカルファースト設計の設計仕様を記述する。この設計思想は、オフラインでも完全に動作し、オンライン時に自動的に同期するアーキテクチャを実現することを目的とする。

### 本機能の処理概要

ローカルファースト設計は、CRDT（Conflict-free Replicated Data Type）をベースとした分散データ同期アーキテクチャである。すべてのデータ操作はまずローカルデータベースに適用され、ネットワーク接続時にサーバーと同期される。これにより、ネットワーク障害やオフライン環境でもアプリケーションの完全な動作が保証される。

**業務上の目的・背景**：家計管理アプリは、外出先での支出記録や、ネットワーク環境が不安定な場所での使用が想定される。従来のクラウドベースアプリはオフライン時に機能が制限されるが、ローカルファーストアーキテクチャにより、いつでもどこでもフル機能が利用できる。また、CRDTにより複数デバイスからの同時編集でも競合が発生しない。

**機能の利用シーン**：
- 地下鉄や飛行機内などネットワークが利用できない場所での支出記録
- ネットワーク障害時の継続的な利用
- 複数デバイスからの同時編集（家族共有など）
- サーバー障害時のローカルでの継続利用

**主要な処理内容**：
1. ローカルデータベース操作：SQLiteベースのローカルストレージ
2. 変更メッセージの生成：データ変更をCRDTメッセージとして記録
3. タイムスタンプ管理：HULCによる論理時計の管理
4. Merkle Tree同期：効率的な差分検出と同期
5. 競合解決：Last-Write-Wins（最後の書き込み勝ち）ポリシー

**関連システム・外部連携**：
- SQLiteデータベース：ローカルデータストレージ
- CRDT（@actual-app/crdt）：分散データ構造
- 同期サーバー：変更メッセージの集約・配信

**権限による制御**：特になし。ローカル操作はすべてのユーザーが実行可能。同期時にサーバー側で権限チェック。

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| - | 全画面 | 主機能 | すべての画面操作はローカルファーストで動作 |

## 機能種別

アーキテクチャ設計 / データ同期

## 入力仕様

### 入力パラメータ

CRDTメッセージの構造：

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| timestamp | string | Yes | HULCタイムスタンプ | ISO日時-カウンタ-ノードID形式 |
| table | string | Yes | 操作対象テーブル | 有効なテーブル名 |
| row | string | Yes | 操作対象行ID | UUID形式 |
| column | string | Yes | 操作対象カラム | 有効なカラム名 |
| value | any | Yes | 新しい値 | 型に応じた値 |

### HULCタイムスタンプの構造

```
2015-04-24T22:23:42.123Z-1000-0123456789ABCDEF
|------------------------|-----|--------------|
        ISO日時          カウンタ   ノードID
```

| 項目 | 形式 | 説明 |
|-----|-----|------|
| ISO日時 | ISO 8601 | 物理時刻（ミリ秒精度） |
| カウンタ | 16進数4桁 | 同一ミリ秒内の順序 |
| ノードID | 16文字 | デバイス識別子 |

### 入力データソース

- ユーザー操作（UI経由のデータ変更）
- API呼び出し（外部システムからのデータ操作）
- 同期メッセージ受信（他デバイスからの変更）

## 出力仕様

### 出力データ

ローカル変更のメッセージ化：

| 項目名 | 型 | 説明 |
|--------|-----|------|
| messages | Message[] | 生成された変更メッセージ群 |
| merkle | TrieNode | 更新されたMerkle Tree |
| clock | Clock | 更新された論理時計 |

### 出力先

- ローカルSQLiteデータベース：即時反映
- 同期キュー：サーバーへの送信待ち
- Merkle Tree：同期状態の追跡

## 処理フロー

### 処理シーケンス

```
1. データ操作の実行
   └─ ユーザーまたはAPIからの操作リクエスト受信
   └─ HULCタイムスタンプを生成（Timestamp.send()）
   └─ 変更メッセージを作成
   └─ ローカルDBに即時適用

2. 同期準備
   └─ メッセージをMerkle Treeに登録（insert）
   └─ 論理時計を更新
   └─ 同期キューに追加

3. オンライン同期
   └─ サーバーとのMerkle Tree比較（diff）
   └─ 差分メッセージを送信
   └─ サーバーからの新規メッセージを受信
   └─ 受信メッセージをローカルに適用
   └─ タイムスタンプをマージ（Timestamp.recv()）

4. 競合解決
   └─ 同一セルへの競合はタイムスタンプで解決
   └─ 新しいタイムスタンプの値が勝つ（LWW）
```

### フローチャート

```mermaid
flowchart TD
    A[データ変更] --> B[タイムスタンプ生成]
    B --> C[メッセージ作成]
    C --> D[ローカルDB適用]
    D --> E[Merkle Tree更新]
    E --> F{オンライン?}
    F -->|No| G[同期キューに蓄積]
    F -->|Yes| H[サーバーと同期]
    G --> I[オンライン復帰待ち]
    I --> F
    H --> J{差分あり?}
    J -->|No| K[完了]
    J -->|Yes| L[メッセージ交換]
    L --> M[受信メッセージ適用]
    M --> N[競合解決（LWW）]
    N --> K
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-24-1 | ローカル即時反映 | すべての変更はローカルに即時適用 | 常時 |
| BR-24-2 | タイムスタンプ単調増加 | タイムスタンプは常に増加する | send/recv時 |
| BR-24-3 | 最大ドリフト制限 | 物理時計との差は最大5分まで | タイムスタンプ生成時 |
| BR-24-4 | LWW競合解決 | 同一セルの競合は新しいタイムスタンプが勝つ | 同期時 |
| BR-24-5 | Merkle Tree剪定 | 古いノードは定期的に剪定される | 同期時 |

### 計算ロジック

**タイムスタンプ生成（send）**:
```typescript
static send(): Timestamp {
  const phys = Date.now();
  const lOld = clock.timestamp.millis();
  const cOld = clock.timestamp.counter();

  // 論理時刻は物理時刻以上を維持
  const lNew = Math.max(lOld, phys);
  // 同一ミリ秒ならカウンタをインクリメント
  const cNew = lOld === lNew ? cOld + 1 : 0;

  // ドリフトとオーバーフローのチェック
  if (lNew - phys > config.maxDrift) throw new ClockDriftError();
  if (cNew > MAX_COUNTER) throw new OverflowError();

  return new Timestamp(lNew, cNew, clock.timestamp.node());
}
```

**タイムスタンプ受信（recv）**:
```typescript
static recv(msg: Timestamp): Timestamp {
  const phys = Date.now();
  const lMsg = msg.millis();
  const lOld = clock.timestamp.millis();
  const cOld = clock.timestamp.counter();
  const cMsg = msg.counter();

  // 3つの時刻の最大値を取る
  const lNew = Math.max(Math.max(lOld, phys), lMsg);
  // カウンタの計算
  const cNew = lNew === lOld && lNew === lMsg
    ? Math.max(cOld, cMsg) + 1
    : lNew === lOld ? cOld + 1
    : lNew === lMsg ? cMsg + 1
    : 0;

  return new Timestamp(lNew, cNew, clock.timestamp.node());
}
```

**Merkle Tree差分検出**:
```typescript
function diff(trie1: TrieNode, trie2: TrieNode): number | null {
  if (trie1.hash === trie2.hash) return null;  // 差分なし

  // ハッシュが異なるノードを深さ優先で探索
  // 最初に差分が発生したタイムスタンプを返す
  // ...
}
```

## データベース操作仕様

### 操作別データベース影響一覧

| 操作 | 対象 | 操作種別 | 概要 |
|-----|-----|---------|------|
| メッセージ適用 | 各テーブル | INSERT/UPDATE/DELETE | 変更の適用 |
| メッセージ保存 | messages_crdt | INSERT | 変更履歴の保存 |
| 同期状態保存 | messages_clock | UPDATE | 論理時計の保存 |

### CRDTメッセージの構造

| フィールド | 型 | 説明 |
|-----------|-----|------|
| timestamp | TEXT | HULCタイムスタンプ |
| dataset | TEXT | データセット（テーブル） |
| row | TEXT | 行ID |
| column | TEXT | カラム名 |
| value | BLOB | 値（暗号化可） |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| ClockDriftError | 時刻エラー | 物理時計との差が5分超過 | システム時刻を修正 |
| OverflowError | オーバーフロー | カウンタが0xFFFF超過 | 次のミリ秒まで待機 |
| DuplicateNodeError | 識別子重複 | 同一ノードIDが検出 | ノードIDを再生成 |
| InvalidError | 不正タイムスタンプ | パース不可能な形式 | 入力を検証 |

### リトライ仕様

- ネットワークエラー時：ローカルキューに蓄積し、接続復帰後に自動リトライ
- 同期失敗時：次回同期イベントで再試行

## トランザクション仕様

- ローカル操作は即時コミット（楽観的更新）
- 同期時の受信メッセージはバッチ適用
- ロールバックは不要（CRDTは最終的に整合）

## パフォーマンス要件

- ローカル操作：1ms以内の応答
- 同期処理：メッセージ数に応じて線形時間
- Merkle Tree更新：O(log n)

## セキュリティ考慮事項

- ローカルデータはファイルシステムレベルで保護
- E2E暗号化有効時は同期メッセージも暗号化
- ノードIDは推測困難なUUIDベース

## 備考

- CRDTの特性により、同期順序に依存しない結果整合性が保証される
- Merkle Treeは定期的に剪定され、古いメッセージ履歴は圧縮される
- オフライン期間が長い場合、同期時のメッセージ量が増加する可能性がある

---

## コードリーディングガイド

本機能を理解するために参照すべきファイルと、推奨する読み解き順序を以下に示す。

### 推奨読解順序

#### Step 1: 論理時計（タイムスタンプ）を理解する

CRDTの基盤となるHULC（Hybrid Unique Logical Clock）を理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | timestamp.ts | `packages/crdt/src/crdt/timestamp.ts` | Timestampクラスの構造とsend/recv |

**読解のコツ**:
- **6-27行目**: HULCの概要コメント
  - 物理時刻、カウンタ、ノードIDの組み合わせでグローバルユニークなタイムスタンプを生成
- **83-89行目**: 設定値（maxDrift: 5分、MAX_COUNTER: 0xFFFF）
- **94-127行目**: Timestampクラスの基本構造
  - `_state`オブジェクトにmillis, counter, nodeを保持
- **195-230行目**: `Timestamp.send()` - ローカルでのタイムスタンプ生成
  - 物理時刻と論理時刻の最大値を採用
  - 同一ミリ秒ならカウンタをインクリメント
- **235-292行目**: `Timestamp.recv()` - 受信タイムスタンプとのマージ
  - 3つの時刻（ローカル物理、ローカル論理、受信）の最大値

#### Step 2: Merkle Treeを理解する

効率的な差分検出のためのデータ構造を理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | merkle.ts | `packages/crdt/src/crdt/merkle.ts` | TrieNodeの構造、insert、diff関数 |

**読解のコツ**:
- **15-20行目**: `TrieNode`型定義
  - 3分木（'0', '1', '2'のキー）とhashフィールド
- **48-68行目**: `insert`関数
  - タイムスタンプをbase3文字列に変換しTrieに挿入
  - XORでハッシュを更新
- **78-139行目**: `diff`関数
  - 2つのTrieを比較し、最初の差分タイムスタンプを返す
  - ハッシュが異なるノードを深さ優先で探索
- **141-164行目**: `prune`関数
  - 古いノードを剪定してメモリを節約

#### Step 3: 同期処理を理解する

クライアント側の同期処理を把握する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | index.ts | `packages/crdt/src/crdt/index.ts` | エクスポートされる関数群 |
| 3-2 | sync.ts | `packages/loot-core/src/server/sync/` | 同期処理の実装（推測） |

**主要処理フロー**:
- メッセージの生成とローカル適用
- Merkle Treeへのメッセージ登録
- サーバーとの差分同期

#### Step 4: サーバー側同期を理解する

サーバー側の同期処理を確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | app-sync.ts | `packages/sync-server/src/app-sync.ts` | /sync エンドポイント |
| 4-2 | sync-simple.ts | `packages/sync-server/src/sync-simple.ts` | シンプル同期実装 |

### プログラム呼び出し階層図

```
ローカル操作
    │
    ├─ データ変更リクエスト
    │       │
    │       └─ Timestamp.send()
    │               │
    │               └─ 新しいタイムスタンプ生成
    │
    ├─ メッセージ作成
    │       │
    │       └─ { timestamp, table, row, column, value }
    │
    ├─ ローカルDB適用
    │       │
    │       └─ db.update() / db.insert()
    │
    └─ Merkle Tree更新
            │
            └─ merkle.insert(trie, timestamp)

同期処理
    │
    ├─ merkle.diff(localTrie, serverTrie)
    │       │
    │       └─ 差分タイムスタンプ取得
    │
    ├─ メッセージ送受信
    │       │
    │       ├─ 送信：since以降のローカルメッセージ
    │       │
    │       └─ 受信：サーバーからの新規メッセージ
    │
    └─ 受信メッセージ適用
            │
            ├─ Timestamp.recv() でタイムスタンプマージ
            │
            └─ ローカルDB更新（LWWで競合解決）
```

### データフロー図

```
[ローカル操作]              [同期処理]                  [サーバー]

ユーザー入力           ┌─────────────────┐
    │                 │                 │
    ▼                 │   オンライン?    │
┌─────────┐           │                 │
│Timestamp│ ◀─────────┤                 │
│ .send() │           └────────┬────────┘
└─────────┘                    │
    │                          │ Yes
    ▼                          ▼
┌─────────┐           ┌─────────────────┐
│メッセージ │────────▶│   merkle.diff   │────────▶ POST /sync
│  作成   │           └─────────────────┘              │
└─────────┘                    ▲                       │
    │                          │                       │
    ▼                          │                       ▼
┌─────────┐           ┌─────────────────┐        ┌─────────┐
│ローカルDB│           │受信メッセージ適用│ ◀────── │simpleSync│
│  適用   │           └─────────────────┘        │ .sync() │
└─────────┘                    │                 └─────────┘
    │                          │
    ▼                          ▼
┌─────────┐           ┌─────────────────┐
│Merkle   │ ◀─────────│  Timestamp.recv │
│ Tree    │           │  (タイムスタンプ  │
│ 更新    │           │   マージ)       │
└─────────┘           └─────────────────┘
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| timestamp.ts | `packages/crdt/src/crdt/timestamp.ts` | ソース | HULCタイムスタンプ実装 |
| merkle.ts | `packages/crdt/src/crdt/merkle.ts` | ソース | Merkle Tree実装 |
| index.ts | `packages/crdt/src/crdt/index.ts` | ソース | CRDTモジュールエクスポート |
| sync_pb.ts | `packages/crdt/src/proto/sync_pb.ts` | ソース | Protocol Buffers定義 |
| app-sync.ts | `packages/sync-server/src/app-sync.ts` | ソース | サーバー同期API |
| sync-simple.ts | `packages/sync-server/src/sync-simple.ts` | ソース | シンプル同期ロジック |
| db.ts | `packages/loot-core/src/server/db/` | ソース | ローカルDBアクセス |
