# 機能設計書 65-元に戻す・やり直し（Undo/Redo）

## 概要

本ドキュメントは、Actualアプリケーションにおける元に戻す・やり直し（Undo/Redo）機能の設計仕様を記載したものである。操作の取り消しとやり直し機能を提供する。

### 本機能の処理概要

**業務上の目的・背景**：家計簿の入力作業では、誤入力や意図しない変更が発生することがある。Undo/Redo機能は、そのような場合にデータベースの変更を元に戻したり、元に戻した操作をやり直したりすることを可能にする。CRDTベースの同期システムと統合されており、メッセージ履歴を管理することで実現している。

**機能の利用シーン**：
- 取引の金額を誤って入力した場合
- カテゴリを間違えて変更した場合
- 複数の取引を一括で変更した後に元に戻したい場合
- 元に戻しすぎた操作をやり直したい場合

**主要な処理内容**：
1. 操作履歴（MESSAGE_HISTORY）の管理
2. Undoマーカーの挿入
3. Undo操作（過去の状態への復帰）
4. Redo操作（Undoした操作のやり直し）
5. 履歴のトリミング（HISTORY_SIZE制限）
6. Undo対応関数のラッピング（undoable）

**関連システム・外部連携**：
- CRDTベースの同期システム（@actual-app/crdt）
- mutatorコンテキスト管理
- クライアント-サーバー間の接続（connection.send）

**権限による制御**：特別な権限制御はなく、予算ファイルにアクセス可能なユーザーが利用可能。

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 12 | 口座一覧画面（デスクトップ） | 補助機能 | 取引操作の取り消し・やり直し |

## 機能種別

データ操作 / 履歴管理

## 入力仕様

### 入力パラメータ

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| messages | Message[] | Yes | 変更メッセージの配列 | - |
| oldData | object | Yes | 変更前のデータ | - |
| undoTag | string | No | Undo操作のグループ化タグ | - |
| meta | unknown | No | メタデータ | - |

### 入力データソース

- mutatorコンテキスト（getMutatorContext）
- データベース変更メッセージ

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| undo-event | object | Undo/Redo完了時のイベント |

### 出力先

- クライアントへのイベント通知（connection.send）
- データベース（sendMessages）

## 処理フロー

### 処理シーケンス

```
1. 操作の開始（withUndo）
   └─ マーカーをMESSAGE_HISTORYに追加

2. 変更の記録（appendMessages）
   └─ 変更メッセージとoldDataを履歴に追加

3. Undo実行（undo関数）
   └─ マーカーまで遡り、変更を逆転

4. Redo実行（redo関数）
   └─ 次のマーカーまで進み、変更を再適用

5. イベント通知
   └─ undo-eventをクライアントに送信
```

### フローチャート

```mermaid
flowchart TD
    A[開始] --> B[withUndo呼び出し]
    B --> C{undoDisabled?}
    C -->|Yes| D[関数を直接実行]
    C -->|No| E[マーカー挿入]
    E --> F[withMutatorContext実行]
    F --> G[関数実行]
    G --> H[appendMessages呼び出し]
    H --> I{undoListening?}
    I -->|Yes| J[履歴に追加]
    I -->|No| K[スキップ]
    J --> L[CURSOR更新]
    K --> M[完了]
    L --> M
    D --> M

    N[Undo呼び出し] --> O[CURSORをマーカーまで戻す]
    O --> P[履歴からエントリ取得]
    P --> Q[undoMessage生成]
    Q --> R[applyUndoAction実行]
    R --> S[sendMessages]
    S --> T[undo-event送信]

    U[Redo呼び出し] --> V[CURSORを次のマーカーまで進める]
    V --> W[履歴からエントリ取得]
    W --> X[resurrections生成]
    X --> Y[applyUndoAction実行]
    Y --> Z[sendMessages]
    Z --> AA[undo-event送信]
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-65-001 | 履歴サイズ制限 | MESSAGE_HISTORYは最大20マーカー分を保持 | trimHistory時 |
| BR-65-002 | マーカーベース管理 | 操作の境界はマーカーで管理 | Undo/Redo時 |
| BR-65-003 | undoDisabled制御 | mutatorコンテキストでUndoを無効化可能 | withUndo時 |
| BR-65-004 | undoListening制御 | Undo中のネスト実行を防止 | appendMessages時 |
| BR-65-005 | tombstone復活 | Redo時に削除されたレコードを復活 | redoResurrections |
| BR-65-006 | スプレッドシート特別処理 | spreadsheet_cellsはcachedValue列を使用 | undoMessage時 |

### 計算ロジック

**undoMessage生成**:
- 変更前のデータ（oldData）から元の値を取得
- spreadsheet_cellsの場合はcachedValueを使用
- レコードが存在しない場合はtombstone=1を設定
- 予算関連テーブルの場合は値を0にリセット
- notesの場合はnullを設定

**redoResurrections生成**:
- 新規作成されたレコードのtombstoneを0に設定
- 特定のテーブル（予算、notes、mapping）は除外

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

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

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| Undo/Redo | 全テーブル | UPDATE | メッセージに基づく値の更新 |
| Undo（削除の取消） | 対象テーブル | UPDATE | tombstone=0に設定 |
| Undo（作成の取消） | 対象テーブル | UPDATE | tombstone=1に設定 |

### テーブル別操作詳細

#### 全テーブル共通

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| UPDATE | 各カラム | oldData[dataset][row][column] | Undo時 |
| UPDATE | tombstone | 0 | Redo時の復活 |
| UPDATE | tombstone | 1 | Undo時の削除復帰 |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| - | エラーログ | テンプレートタイプ不一致 | console.error出力 |

### リトライ仕様

特になし。Undo/Redo操作は即時実行される。

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

- sendMessagesで一括でメッセージを適用
- withMutatorContextでコンテキストを管理
- undoListening=falseでUndo操作自体はUndo対象外

## パフォーマンス要件

- Undo/Redo操作: 200ms以内
- 履歴トリミング: 50ms以内

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

- Undo/Redoはローカルデータのみに作用
- クラウド同期時はCRDTメッセージとして送信

## 備考

- HISTORY_SIZE = 20
- MESSAGE_HISTORY配列で履歴管理
- CURSORで現在位置を管理
- マーカータイプとメッセージタイプの2種類のエントリ

---

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

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

### 推奨読解順序

#### Step 1: データ構造を理解する

まず、履歴管理のデータ構造を理解することが重要。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | undo.ts | `packages/loot-core/src/server/undo.ts` | MarkerMessage型、MessagesMessage型、UndoState型の定義 |

**読解のコツ**: MESSAGE_HISTORY配列は2種類のエントリ（marker/messages）を持つ。markerは操作の境界を示し、messagesは実際の変更データを保持する。CURSORは現在の位置を示すインデックス。

#### Step 2: エントリーポイントを理解する

処理の起点となる関数を特定。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | undo.ts | `packages/loot-core/src/server/undo.ts` | withUndo関数、undoable関数 |

**主要処理フロー（withUndo）**:
1. **68-75行目**: undoDisabledまたはundoListeningの場合は直接実行
2. **77行目**: 履歴をCURSOR位置でスライス
3. **79-86行目**: マーカーを挿入（末尾がマーカーの場合は上書き）
4. **88-92行目**: withMutatorContextでundoListening=trueとして関数実行

**主要処理フロー（undoable）**:
- **94-106行目**: 関数をundoable化するラッパー関数

#### Step 3: 履歴記録を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | undo.ts | `packages/loot-core/src/server/undo.ts` | appendMessages関数、trimHistory関数 |

**主要処理フロー（appendMessages）**:
- **45-61行目**: undoListening中かつmessages.length > 0の場合のみ履歴追加
- **49行目**: trimHistoryで古い履歴を削除
- **53-59行目**: messagesエントリを追加してCURSOR更新

**主要処理フロー（trimHistory）**:
- **33-43行目**: HISTORY_SIZEを超えるマーカーを削除

#### Step 4: Undo/Redo処理を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | undo.ts | `packages/loot-core/src/server/undo.ts` | undo関数、redo関数、undoMessage関数、redoResurrections関数 |

**主要処理フロー（undo）**:
- **130-132行目**: CURSORを1つ戻す
- **134-137行目**: マーカーまで遡る
- **139-143行目**: エントリをフィルタ
- **145-157行目**: undoMessageで逆転メッセージ生成、applyUndoAction実行

**主要処理フロー（redo）**:
- **205-220行目**: CURSORを次のマーカーまで進める
- **223-235行目**: メッセージとresurrectionsを結合してapplyUndoAction実行

**主要処理フロー（undoMessage）**:
- **160-203行目**: 変更前の値を取得して逆転メッセージを生成
- **164-169行目**: spreadsheet_cellsの特別処理
- **178-200行目**: テーブル別の処理（予算テーブル、notes、通常テーブル）

**主要処理フロー（redoResurrections）**:
- **238-270行目**: 新規作成されたレコードのtombstoneを0に設定

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

```
undoable(func) [ラッパー関数]
    │
    └─ withUndo(func, meta)
           │
           ├─ getMutatorContext()
           │      └─ undoDisabled? undoListening? チェック
           │
           ├─ trimHistory()
           │      └─ HISTORY_SIZE超過分を削除
           │
           ├─ マーカー挿入
           │      └─ MESSAGE_HISTORY.push({ type: 'marker' })
           │
           └─ withMutatorContext({ undoListening: true }, func)
                  │
                  └─ func() 実行
                         │
                         └─ appendMessages(messages, oldData)
                                │
                                └─ MESSAGE_HISTORY.push({ type: 'messages' })

undo()
    │
    ├─ CURSOR位置調整（マーカーまで戻る）
    │
    ├─ エントリ取得（start〜end）
    │
    ├─ undoMessage(message, oldData) × N
    │      │
    │      └─ 逆転メッセージ生成
    │             ├─ spreadsheet_cells → cachedValue
    │             ├─ 予算テーブル → 0
    │             ├─ notes → null
    │             └─ その他 → tombstone: 1
    │
    └─ applyUndoAction(messages, meta, undoTag)
           │
           ├─ withMutatorContext({ undoListening: false })
           │      └─ sendMessages(messages)
           │
           └─ connection.send('undo-event', {...})

redo()
    │
    ├─ CURSOR位置調整（次のマーカーまで進む）
    │
    ├─ エントリ取得（start〜end）
    │
    ├─ redoResurrections(messages, oldData)
    │      └─ tombstone: 0 メッセージ生成
    │
    └─ applyUndoAction(messages + resurrections, meta, undoTag)
```

### データフロー図

```
[入力]                          [処理]                           [出力]

データベース変更 ─────┐
(mutator経由)        │
                     ▼
              ┌─────────────────┐
              │ undoable(func)  │
              │ withUndo        │
              └────────┬────────┘
                       │
                       ▼
              ┌─────────────────┐
              │ マーカー挿入    │
              │ MESSAGE_HISTORY │
              └────────┬────────┘
                       │
                       ▼
              ┌─────────────────┐
              │ func() 実行     │
              │ DB変更          │
              └────────┬────────┘
                       │
                       ▼
              ┌─────────────────┐
              │ appendMessages  │
              │ - messages      │
              │ - oldData       │
              └────────┬────────┘
                       │
                       ▼
              ┌─────────────────┐
              │ MESSAGE_HISTORY │
              │ 更新            │
              │ CURSOR++        │
              └─────────────────┘

Undo操作 ─────────────┐
                     ▼
              ┌─────────────────┐
              │ undo()          │
              │ CURSOR調整      │
              └────────┬────────┘
                       │
                       ▼
              ┌─────────────────┐
              │ undoMessage     │
              │ 逆転メッセージ  │
              └────────┬────────┘
                       │
                       ▼
              ┌─────────────────┐
              │ applyUndoAction │
              │ sendMessages    │
              └────────┬────────┘
                       │
    ┌──────────────────┴──────────────────┐
    ▼                                      ▼
┌──────────────────┐              ┌──────────────────┐
│ データベース更新 │              │ undo-event       │
│                  │              │ クライアント通知 │
└──────────────────┘              └──────────────────┘
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| undo.ts | `packages/loot-core/src/server/undo.ts` | ソース | Undo/Redo機能のメイン実装 |
| mutators.ts | `packages/loot-core/src/server/mutators.ts` | ソース | Mutatorコンテキスト管理 |
| sync.ts | `packages/loot-core/src/server/sync.ts` | ソース | sendMessages関数（CRDTメッセージ送信） |
| connection.ts | `packages/loot-core/src/platform/server/connection.ts` | ソース | クライアント接続・イベント送信 |
| crdt | `packages/crdt/` | ソース | Timestampクラス |
