# 機能設計書 58-変更セット適用

## 概要

本ドキュメントは、Etherpadにおける変更セット適用機能の設計仕様を記載する。クライアントからの変更セット（Changeset）をパッドに適用してリビジョンを作成する処理について詳細に解説する。

### 本機能の処理概要

変更セット適用機能は、クライアントから送信されたUSER_CHANGESメッセージに含まれる変更セットを検証・変換し、パッドの現在の状態に適用してリビジョンを作成する機能を提供する。これはリアルタイム共同編集の中核をなす処理である。

**業務上の目的・背景**：
リアルタイム共同編集では、複数のユーザーが同時に異なる編集を行う可能性がある。これらの編集を整合性を保ちながらパッドに適用し、永続化する必要がある。変更セット適用機能は、OT（Operational Transformation）アルゴリズムを用いて、異なるクライアントからの変更を適切にマージし、すべてのクライアントで同一の最終状態を保証する。

**機能の利用シーン**：
- ユーザーがテキストを入力・削除した場合
- ユーザーが書式を適用した場合（太字、斜体など）
- ユーザーがリストを作成・編集した場合
- コピー&ペーストを行った場合

**主要な処理内容**：
1. USER_CHANGESメッセージからchangeset、baseRev、apoolを取得
2. 変更セットの構文検証（checkRep）
3. 著者属性の検証（セッション著者との一致確認）
4. 属性プールのマージ（moveOpsToNewPool）
5. リベース処理（baseRevから現在headまでfollow）
6. パッドへの変更適用（appendRevision）
7. マーカー補正とリビジョン作成

**関連システム・外部連携**：
- Changesetライブラリ: 変更セットの操作
- AttributePool: 属性管理
- ueberDB2: リビジョンの永続化

**権限による制御**：
書き込み権限のあるユーザーのみが変更を適用可能。読み取り専用ユーザーの変更は拒否される。

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 2 | パッド編集画面 | 主機能 | USER_CHANGESメッセージを処理してパッドに変更を適用 |

## 機能種別

データ処理 / リビジョン管理

## 入力仕様

### 入力パラメータ

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| data.baseRev | number | Yes | 変更の基準リビジョン番号 | null/undefined不可 |
| data.changeset | string | Yes | 変更セット文字列 | Changeset形式、checkRepで検証 |
| data.apool | object | Yes | 属性プール（Jsonable形式） | AttributePool形式 |

### 入力データソース

- Socket.IOメッセージ（USER_CHANGESメッセージのdata部分）

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| newRev | number | 作成された新リビジョン番号 |

### 出力先

- パッドデータベース（リビジョン保存）
- ACCEPT_COMMITメッセージで変更元クライアントに通知

## 処理フロー

### 処理シーケンス

```
1. メッセージデータ抽出
   └─ baseRev, apool, changeset取得
2. 属性プール復元
   └─ AttributePool().fromJsonable(apool)
3. 変更セット検証
   └─ checkRepで構文・形式検証
4. 著者属性検証
   ├─ deserializeOpsで操作を分解
   ├─ AttributeMap.fromStringで属性取得
   └─ author属性がセッション著者と一致確認
5. 属性プールマージ
   └─ moveOpsToNewPoolでパッドプールに統合
6. リベース処理
   └─ baseRevから現在headまでループ
       ├─ 各リビジョンの変更セット取得
       ├─ 重複検出（同一changeset・同一author）
       └─ followで変更セットを最新に変換
7. 長さ検証
   └─ oldLenがドキュメント長と一致確認
8. リビジョン作成
   └─ pad.appendRevision(rebasedChangeset)
9. マーカー補正
   └─ _correctMarkersInPadで行マーカー位置補正
10. 末尾改行保証
    └─ 必要ならmakeSpliceで改行追加
```

### フローチャート

```mermaid
flowchart TD
    A[USER_CHANGESメッセージ受信] --> B[baseRev/apool/changeset取得]
    B --> C{パラメータ検証}
    C -->|不正| Z1[エラー: missing parameter]
    C -->|OK| D[wireApoolからAttributePool復元]
    D --> E[checkRep実行]
    E --> F{構文正しい?}
    F -->|No| Z2[エラー: badChangeset]
    F -->|Yes| G[deserializeOpsで操作分解]
    G --> H{著者属性一致?}
    H -->|No| Z3[エラー: Author mismatch]
    H -->|Yes| I[moveOpsToNewPool]
    I --> J{baseRev < head?}
    J -->|Yes| K[リビジョン取得]
    K --> L{重複検出?}
    L -->|Yes| M[identity変換]
    L -->|No| N[follow変換]
    M --> O{次のリビジョン?}
    N --> O
    O -->|Yes| K
    O -->|No| P{oldLen一致?}
    J -->|No| P
    P -->|No| Z4[エラー: length mismatch]
    P -->|Yes| Q[pad.appendRevision]
    Q --> R[_correctMarkersInPad]
    R --> S{末尾改行あり?}
    S -->|No| T[makeSpliceで改行追加]
    S -->|Yes| U[リビジョン番号返却]
    T --> U
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-58-01 | 変更セット検証 | checkRepで構文・正規形式を検証 | 常時 |
| BR-58-02 | 著者属性制限 | 変更内のauthor属性はセッション著者と一致必須 | 常時 |
| BR-58-03 | リベース必須 | baseRevが現在headより古い場合はfollow変換必須 | baseRev < head |
| BR-58-04 | 重複変更処理 | 同一changeset・同一authorの場合はidentity変換 | 重複検出時 |
| BR-58-05 | 長さ整合性 | oldLenはパッドのテキスト長と一致必須 | 常時 |
| BR-58-06 | 末尾改行必須 | パッドは常に改行で終わる必要あり | 常時 |

### 計算ロジック

#### follow関数（OTアルゴリズム）
2つの変更セットA, Bが同一ベースに対する変更の場合、Aを適用後のドキュメントにBを適用できるよう変換する。

```
follow(A, B, false, pool) => B'
- A: 先に適用された変更
- B: 後から来た変更（リベース対象）
- B': Aが適用された後のドキュメントに適用可能なB
```

#### 重複検出ロジック
```
if (changeset === c && thisSession.author === authorId):
    // 再送信と判断し、恒等変換を返す
    rebasedChangeset = identity(unpack(changeset).oldLen)
```

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

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

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| 過去リビジョン取得 | pad:{padId}:revs:{n} | SELECT | リベース用にbaseRev+1〜headを取得 |
| パッドデータ更新 | pad:{padId} | UPDATE | atext、pool、head更新 |
| リビジョン作成 | pad:{padId}:revs:{n} | INSERT | 新リビジョンの保存 |

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

#### pad:{padId}:revs:{n}（読み取り）

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| SELECT | changeset | リベース用 | |
| SELECT | meta.author | 重複検出用 | |

#### pad:{padId}（更新）

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| UPDATE | atext.text | 変更適用後のテキスト | |
| UPDATE | atext.attribs | 変更適用後の属性 | |
| UPDATE | pool | マージ後の属性プール | |
| UPDATE | head | 新リビジョン番号 | |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| missing baseRev | パラメータ不正 | baseRevがnull/undefined | クライアント再接続 |
| missing apool | パラメータ不正 | apoolがnull/undefined | クライアント再接続 |
| missing changeset | パラメータ不正 | changesetがnull/undefined | クライアント再接続 |
| badChangeset | 変更セット不正 | checkRepで検証失敗 | クライアント切断 |
| Author mismatch | 著者不正 | 著者属性がセッションと不一致 | クライアント切断 |
| length mismatch | 長さ不正 | oldLenがドキュメント長と不一致 | クライアント切断 |

### リトライ仕様

サーバー側での自動リトライは実装されていない。エラー発生時はクライアントを切断し、再接続を促す。

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

pad.appendRevisionでアトミックに変更が適用される。リビジョン作成とパッドデータ更新は一貫性を持って実行。

## パフォーマンス要件

- リベースループは各リビジョンを順次処理
- 大量のリビジョンギャップがある場合は処理時間増加
- stats.timer('edits')で処理時間を計測

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

- 著者属性の検証による偽装防止
- 変更セットの構文検証による不正データ防止
- 読み取り専用ユーザーの変更をブロック

## 備考

- Changesetは元のEtherpad（Google Wave由来）から継承
- OTアルゴリズムにより最終一貫性を保証

---

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

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

### 推奨読解順序

#### Step 1: Changesetの構造を理解する

変更セットのフォーマットと操作を理解することが最重要。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | Changeset.ts | `src/static/js/Changeset.ts` | Changesetフォーマットと主要関数 |

**読解のコツ**:
- Changesetは「Z:oldLen>deltaLen*attrib=keep+insert-delete$text」形式
- unpack()でヘッダーと操作文字列を分解
- deserializeOps()で各操作（+:挿入、-:削除、=:保持）を取得

主要関数:
- **checkRep**: 構文検証
- **follow**: リベース変換
- **compose**: 変更セット合成
- **moveOpsToNewPool**: 属性プール間の変換

#### Step 2: handleUserChanges関数を詳細に理解する

変更適用の核心処理を確認。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | PadMessageHandler.ts | `src/node/handler/PadMessageHandler.ts` | handleUserChanges関数（627-736行目） |

**主要処理フロー**:
- **645-648行目**: パラメータ検証
- **649行目**: wireApoolからAttributePool復元
- **653行目**: checkRepで変更セット検証
- **656-670行目**: 著者属性検証ループ
- **675行目**: moveOpsToNewPoolで属性プールマージ
- **678行目**: baseRev初期化
- **683-695行目**: リベースループ
  - **685行目**: リビジョンデータ取得
  - **686-688行目**: 重複検出とidentity変換
  - **694行目**: follow変換
- **699-703行目**: oldLen検証
- **705行目**: pad.appendRevision呼び出し
- **708行目**: アサーション（rev増分確認）

#### Step 3: pad.appendRevisionを理解する

実際のリビジョン作成処理を確認。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | Pad.ts | `src/node/db/Pad.ts` | appendRevision関数 |

**読解のコツ**: appendRevisionはchangesetをatextに適用し、新しいリビジョンをDBに保存する。

#### Step 4: マーカー補正を理解する

行マーカー（リスト等）の位置補正を確認。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | PadMessageHandler.ts | `src/node/handler/PadMessageHandler.ts` | _correctMarkersInPad関数（800-838行目） |

**主要処理フロー**:
- **807-820行目**: 行頭以外のマーカーを検出
- **829-836行目**: 不正なマーカーを削除するchangeset生成

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

```
handleUserChanges (PadMessageHandler.ts:627)
    │
    ├─ [パラメータ取得]
    │      ├─ data.baseRev
    │      ├─ data.apool
    │      └─ data.changeset
    │
    ├─ AttributePool.fromJsonable (649)
    │
    ├─ checkRep (653)
    │      └─ Changeset構文検証
    │
    ├─ [著者属性検証] (656-670)
    │      ├─ deserializeOps
    │      ├─ AttributeMap.fromString
    │      └─ author属性確認
    │
    ├─ moveOpsToNewPool (675)
    │      └─ 属性プールマージ
    │
    ├─ [リベースループ] (683-695)
    │      │
    │      ├─ pad.getRevision(r)
    │      │
    │      ├─ [重複検出] identity
    │      │
    │      └─ follow
    │             └─ OT変換
    │
    ├─ [長さ検証] (699-703)
    │
    ├─ pad.appendRevision (705)
    │      │
    │      └─ [内部処理]
    │             ├─ applyToAText
    │             ├─ db.set(revs:{n})
    │             └─ head更新
    │
    ├─ _correctMarkersInPad (710-713)
    │      └─ マーカー位置補正changeset
    │
    └─ [末尾改行保証] (716-719)
           └─ makeSplice
```

### データフロー図

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

USER_CHANGES         ┌─────────────────┐
(baseRev=10,        │  パラメータ     │
 changeset,         │  抽出・検証     │
 apool) ───────────▶│                 │
                    └────────┬────────┘
                             │
                             ▼
                    ┌─────────────────┐
                    │  checkRep       │
                    │  構文検証       │
                    └────────┬────────┘
                             │
                             ▼
                    ┌─────────────────┐
                    │  著者属性検証   │
                    │  (author確認)   │
                    └────────┬────────┘
                             │
                             ▼
                    ┌─────────────────┐
パッド ────────────▶│  リベース処理   │
(head=12,           │  follow×2回    │
revs:11, revs:12)   │  (10→12)       │
                    └────────┬────────┘
                             │
                             ▼
                    ┌─────────────────┐
                    │  appendRevision │
                    │  (rev=13作成)   │
                    └────────┬────────┘
                             │
                             ▼
                    ┌─────────────────┐
                    │  マーカー補正   │
                    │  末尾改行保証   │ ───────▶ Database
                    └────────┬────────┘          (revs:13)
                             │
                             ▼
                         newRev=13
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| PadMessageHandler.ts | `src/node/handler/PadMessageHandler.ts` | ソース | 変更セット処理 |
| Changeset.ts | `src/static/js/Changeset.ts` | ソース | 変更セット操作 |
| AttributePool.ts | `src/static/js/AttributePool.ts` | ソース | 属性プール管理 |
| AttributeMap.ts | `src/static/js/AttributeMap.ts` | ソース | 属性マップ操作 |
| Builder.ts | `src/static/js/Builder.ts` | ソース | Changeset構築 |
| Pad.ts | `src/node/db/Pad.ts` | ソース | リビジョン管理 |
