# 機能設計書 65-パッドデータ永続化

## 概要

本ドキュメントは、Etherpadのパッドデータ永続化機能の設計を記載する。この機能は、パッドのテキスト、属性、リビジョン履歴、チャットメッセージなどのデータをueberDB2を介してデータベースに保存する。

### 本機能の処理概要

パッドデータ永続化機能は、Etherpadの中核的なデータ保存機能である。パッドの作成、編集、削除に伴うすべてのデータ変更を、ueberDB2抽象化層を通じて永続的なストレージに保存する。

**業務上の目的・背景**：Etherpadの主要な価値はリアルタイム共同編集であり、すべての編集内容が確実に保存されることが必須である。サーバー再起動後も編集内容が失われないよう、すべてのパッドデータはデータベースに永続化される。また、リビジョン履歴を保持することで、任意の時点への復元やタイムスライダー機能が実現される。

**機能の利用シーン**：
- 新規パッドが作成される時
- パッドにテキストが追加・変更される時
- チャットメッセージが送信される時
- 保存済みリビジョンが作成される時
- パッドが削除される時

**主要な処理内容**：
1. Padオブジェクトのデータベースへの保存（saveToDatabase）
2. リビジョン（変更セット）の保存
3. チャットメッセージの保存
4. 属性プールの管理
5. パッドデータの読み込みと初期化

**関連システム・外部連携**：
- ueberDB2（データベース抽象化層）
- 各種データベースバックエンド（rustydb、SQLite、PostgreSQL、MySQL等）

**権限による制御**：パッドデータへのアクセスは、セキュリティマネージャーおよびセッション管理と連携して制御される。

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 2 | パッド編集画面 | 主画面 | テキスト編集内容の永続化 |
| 3 | タイムスライダー画面 | 参照画面 | リビジョン履歴の読み込み |

## 機能種別

データ永続化 / CRUD操作

## 入力仕様

### 入力パラメータ

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| padId | string | Yes | パッドの一意識別子 | 正規表現パターンに一致 |
| changeset | string | Yes | 変更セット文字列 | Changeset形式 |
| authorId | string | No | 著者ID | a.で始まる文字列 |
| text | string | No | パッドテキスト | 最大100,000文字 |

### 入力データソース

- PadMessageHandler（リアルタイム編集）
- API（RESTful API経由の操作）
- インポート機能（ファイルインポート）

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| pad:{padId} | object | パッド本体データ |
| pad:{padId}:revs:{revNum} | object | 各リビジョンのデータ |
| pad:{padId}:chat:{chatNum} | object | 各チャットメッセージ |

### 出力先

- ueberDB2を介したデータベース

## 処理フロー

### 処理シーケンス

```
1. パッドへの変更要求受信
   └─ appendRevision() 呼び出し
2. 変更セットの適用
   └─ applyToAText() で新しいatextを生成
3. リビジョンデータの保存
   └─ db.set(`pad:${padId}:revs:${newRev}`, {...})
4. パッド本体データの保存
   └─ saveToDatabase() で pad:{padId} を更新
5. 著者情報の更新
   └─ authorManager.addPad() 呼び出し
6. フック呼び出し
   └─ padCreate または padUpdate フック
```

### フローチャート

```mermaid
flowchart TD
    A[変更要求] --> B[appendRevision]
    B --> C[applyToAText]
    C --> D{変更あり?}
    D -->|No| E[現在のheadを返却]
    D -->|Yes| F[headをインクリメント]
    F --> G[リビジョンデータ作成]
    G --> H{キーリビジョン?}
    H -->|Yes| I[pool/atextも保存]
    H -->|No| J[changesetのみ保存]
    I --> K[db.set revs]
    J --> K
    K --> L[saveToDatabase]
    L --> M[authorManager.addPad]
    M --> N[フック呼び出し]
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-65-01 | キーリビジョン | 100リビジョンごとにpool/atextを完全保存 | head % 100 === 0 |
| BR-65-02 | 重複変更スキップ | 変更がない場合はリビジョンを作成しない | text/attribsが同一 |
| BR-65-03 | 末尾改行保証 | パッドテキストは常に改行で終わる | 保存時 |

### 計算ロジック

キーリビジョン番号の計算:
```typescript
getKeyRevisionNumber(revNum: number) {
  return Math.floor(revNum / 100) * 100;
}
```

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

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

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| パッド作成 | pad:{padId} | INSERT | パッド本体の作成 |
| リビジョン保存 | pad:{padId}:revs:{revNum} | INSERT | 変更セットの保存 |
| チャット保存 | pad:{padId}:chat:{chatNum} | INSERT | チャットメッセージの保存 |
| パッド更新 | pad:{padId} | UPDATE | head/chatHead等の更新 |
| パッド削除 | pad:{padId}* | DELETE | パッドと関連データの削除 |

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

#### pad:{padId}

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| INSERT/UPDATE | atext | {text, attribs} | 現在のテキストと属性 |
| INSERT/UPDATE | pool | AttributePool JSON | 属性プール |
| INSERT/UPDATE | head | number | 最新リビジョン番号 |
| INSERT/UPDATE | chatHead | number | 最新チャット番号 |
| INSERT/UPDATE | publicStatus | boolean | 公開ステータス |
| INSERT/UPDATE | savedRevisions | array | 保存済みリビジョン |

#### pad:{padId}:revs:{revNum}

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| INSERT | changeset | string | 変更セット文字列 |
| INSERT | meta.author | string | 著者ID |
| INSERT | meta.timestamp | number | タイムスタンプ |
| INSERT | meta.pool | object | キーリビジョン時のみ |
| INSERT | meta.atext | object | キーリビジョン時のみ |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| apierror | 無効なパッドID | パッドID形式不正 | エラーメッセージを返却 |
| apierror | テキスト長超過 | 100,000文字超 | エラーメッセージを返却 |
| - | データベースエラー | DB接続失敗等 | 例外を上位に伝播 |

### リトライ仕様

ueberDB2の設定による。書き込みキューによりバッファリングされる。

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

ueberDB2は書き込みをキューイングし、バッチ処理で効率的にデータベースに書き込む。明示的なトランザクション制御はueberDB2が管理。

## パフォーマンス要件

- リビジョン保存: リアルタイム編集に影響しない速度
- キーリビジョン間隔: 100リビジョン（読み込み時のパフォーマンスとのバランス）

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

- パッドIDのバリデーションでパストラバーサル攻撃を防止
- データベースアクセスはueberDB2経由で抽象化

## 備考

- ueberDB2は多様なデータベースバックエンドをサポート
- デフォルトはrustydb（ファイルベース）

---

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

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

### 推奨読解順序

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

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | PadType.ts | `src/node/types/PadType.ts` | AText、APool、AChangeSet型を確認 |
| 1-2 | Pad.ts | `src/node/db/Pad.ts` | Padクラスのプロパティ定義（41-66行目） |

**読解のコツ**: atextはテキストと属性情報を保持する中心的なデータ構造。poolは属性の辞書。

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

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | PadManager.ts | `src/node/db/PadManager.ts` | getPad関数でパッド取得・作成 |

**主要処理フロー**:
1. **109-144行目**: getPad関数 - パッドの取得または作成
2. **146-150行目**: listAllPads関数 - 全パッド一覧
3. **156-160行目**: doesPadExist関数 - パッド存在確認

#### Step 3: パッドクラスの永続化処理を理解する

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

**主要処理フロー**:
- **97-144行目**: appendRevision関数 - リビジョン追加の中核ロジック
- **155-159行目**: saveToDatabase関数 - パッド本体の保存
- **382-401行目**: init関数 - パッド初期化・読み込み
- **334-345行目**: appendChatMessage関数 - チャット保存

#### Step 4: データベースアクセス層を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | DB.ts | `src/node/db/DB.ts` | ueberDB2の初期化と操作 |

**主要処理フロー**:
- **39-54行目**: init関数 - データベース初期化
- **48-53行目**: get/set/findKeys等の関数エクスポート

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

```
PadMessageHandler（リアルタイム編集）
    │
    └─ Pad.appendRevision(changeset, authorId)
           │
           ├─ Changeset.applyToAText()
           │
           ├─ db.set(`pad:${padId}:revs:${newRev}`)
           │      └─ ueberDB2.set()
           │
           ├─ Pad.saveToDatabase()
           │      └─ db.set(`pad:${padId}`)
           │             └─ ueberDB2.set()
           │
           ├─ authorManager.addPad()
           │      └─ db.set(`globalAuthor:${authorId}`)
           │
           └─ hooks.aCallAll('padCreate'/'padUpdate')
```

### データフロー図

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

変更セット ───▶ Pad.appendRevision()
                         │
                         ▼
               Changeset.applyToAText()
                         │
                         ▼
               新しいatext生成
                         │
         ┌───────────────┼───────────────┐
         │               │               │
         ▼               ▼               ▼
    revs保存       pad保存      author更新
         │               │               │
         └───────────────┼───────────────┘
                         │
                         ▼
                    ueberDB2
                         │
                         ▼
                   データベース
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| Pad.ts | `src/node/db/Pad.ts` | ソース | パッドクラス本体 |
| PadManager.ts | `src/node/db/PadManager.ts` | ソース | パッド管理 |
| DB.ts | `src/node/db/DB.ts` | ソース | DB抽象化 |
| PadType.ts | `src/node/types/PadType.ts` | ソース | 型定義 |
| Changeset.ts | `src/static/js/Changeset.ts` | ソース | 変更セット処理 |
| AttributePool.ts | `src/static/js/AttributePool.ts` | ソース | 属性プール |
| AuthorManager.ts | `src/node/db/AuthorManager.ts` | ソース | 著者管理連携 |
