# 機能設計書 39-Suspense

## 概要

本ドキュメントは、Vue 3の組み込みコンポーネント`<Suspense>`の機能設計を記載する。`<Suspense>`は非同期コンポーネントやasync setup()を含むコンポーネントのローディング状態を管理し、フォールバックコンテンツを表示するコンポーネントである。本機能は実験的機能（experimental）として提供される。

### 本機能の処理概要

`<Suspense>`コンポーネントは、defaultスロットの非同期コンポーネントがすべて解決されるまでfallbackスロットのコンテンツを表示し、解決完了後にdefaultスロットに切り替える。非同期依存関係の追跡、ペンディングブランチの管理、解決時のDOMスワップを自動的に処理する。

**業務上の目的・背景**：非同期データフェッチやコード分割による遅延ロードは現代のWebアプリケーションに不可欠であるが、ローディング状態の管理は複雑になりがちである。Suspenseはこれを宣言的に解決し、開発者はフォールバックUIの定義に集中できる。ネストされたSuspenseのサポートにより、複雑な非同期依存関係の階層的な管理も可能。

**機能の利用シーン**：非同期コンポーネントのローディング表示、async setup()を使用するコンポーネントのローディング制御、データフェッチ完了までのスケルトンスクリーン表示等。

**主要な処理内容**：
1. `SuspenseBoundary`の生成と管理
2. defaultスロット（ssContent）とfallbackスロット（ssFallback）の正規化
3. `pendingBranch`のオフDOMコンテナでのレンダリング
4. 非同期依存関係（deps）のカウントと追跡
5. `resolve`メソッドによるペンディングブランチの本コンテナへの移動
6. `fallback`メソッドによるフォールバック表示
7. `timeout`プロパティによるフォールバック表示のタイミング制御
8. `suspensible`プロパティによる親Suspenseへの依存関係伝播
9. `registerDep`メソッドによる非同期コンポーネントの登録
10. `effects`配列によるバッファリングされたエフェクトの管理

**関連システム・外部連携**：レンダラー（`renderer.ts`）のprocess/unmountメソッドと連携。`defineAsyncComponent`と連携。`KeepAlive`との組み合わせ。スケジューラー（`queuePostFlushCb`）との連携。

**権限による制御**：特になし。

## 関連画面

画面機能マッピングに該当する画面関連情報なし。

## 機能種別

非同期状態管理 / 組み込みコンポーネント

## 入力仕様

### 入力パラメータ

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| onResolve | `() => void` | No | 非同期コンテンツが解決された時のイベント | - |
| onPending | `() => void` | No | ペンディング状態に入った時のイベント | - |
| onFallback | `() => void` | No | フォールバック表示時のイベント | - |
| timeout | `string \| number` | No | フォールバック表示までのタイムアウト（ms）| 数値であること |
| suspensible | `boolean` | No | 親Suspenseの依存関係としてカウントするか（デフォルト: false）| - |

### 入力データソース

- デフォルトスロット: メインコンテンツ（非同期コンポーネントを含む可能性あり）
- フォールバックスロット: ローディング中に表示するコンテンツ

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| activeBranch | `VNode` | 現在表示中のブランチ（default or fallback） |
| pendingBranch | `VNode \| null` | 解決待ちのブランチ |

### 出力先

- DOM（activeBranchの内容をコンテナにレンダリング）
- onResolve/onPending/onFallbackイベント

## 処理フロー

### 処理シーケンス

```
初回マウント:
1. createSuspenseBoundaryで境界オブジェクト生成
2. hiddenContainerにpendingBranchをレンダリング
3. deps > 0の場合:
   ├─ onPending/onFallbackイベント発火
   ├─ fallbackスロットをメインコンテナにレンダリング
   └─ activeBranch = fallback
4. deps === 0の場合:
   └─ 即座にresolve()

resolve():
1. pendingBranchをhiddenContainerからメインコンテナに移動
2. 旧activeBranchをアンマウント
3. activeBranch = pendingBranch
4. バッファリングされたeffectsをフラッシュ
5. 親Suspenseの依存関係をデクリメント
6. onResolveイベント発火

registerDep():
1. deps++
2. asyncDep.then()で解決時にhandleSetupResult + setupRenderEffect
3. 解決後 deps-- → deps === 0ならresolve()
```

### フローチャート

```mermaid
flowchart TD
    A[Suspense mount] --> B[createSuspenseBoundary]
    B --> C[hiddenContainerでpendingBranch patch]
    C --> D{deps > 0?}
    D -->|Yes| E[onPending + onFallback発火]
    E --> F[fallbackをメインコンテナにpatch]
    F --> G[非同期dep解決を待機]
    G --> H{全deps解決?}
    H -->|Yes| I[resolve]
    D -->|No| I
    I --> J[pendingBranchをメインコンテナに移動]
    J --> K[旧activeBranchアンマウント]
    K --> L[effectsフラッシュ]
    L --> M[onResolve発火]
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-39-01 | 単一ルートノード | Suspenseの各スロットは単一のルートノードを期待する | 常時（開発モードで警告） |
| BR-39-02 | 実験的機能 | 初回生成時にconsole.infoで実験的機能であることを通知 | 開発モード、初回のみ |
| BR-39-03 | timeout制御 | timeout > 0の場合はsetTimeoutでfallback表示を遅延、timeout === 0の場合は即座にfallback表示 | ペンディング状態かつtimeout指定時 |
| BR-39-04 | suspensible | suspensible=trueの場合、親Suspenseのdepsをインクリメントし依存関係として登録 | suspensible prop |
| BR-39-05 | effectsバッファリング | ペンディング中のeffectsはバッファリングされ、resolveまたは親Suspenseに転送 | ペンディング中 |
| BR-39-06 | ペンディングID | suspenseIdを用いて非同期コールバックの無効化を管理 | 常時 |
| BR-39-07 | ハイドレーション連携 | SSRコンテンツのハイドレーション時にSuspenseBoundaryを構築し非同期deps追跡 | SSRハイドレーション時 |
| BR-39-08 | out-inモード連携 | Transitionのout-inモード使用時、activeBranchのleave完了後にpendingBranchを移動 | Transition連携時 |

### 計算ロジック

`normalizeSuspenseSlot`: スロット関数を実行し、配列の場合は`filterSingleRoot`で単一ルートを抽出、`normalizeVNode`で正規化。

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

該当なし。

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| - | 実行時エラー | resumeなしでresolve()がpendingBranchなしで呼ばれた | `suspense.resolve() is called without a pending branch.` |
| - | 実行時エラー | アンマウント済みSuspenseでresolve()が呼ばれた | `suspense.resolve() is called on an already unmounted suspense boundary.` |
| - | 開発時警告 | Suspenseスロットに複数のルートノード | `<Suspense> slots expect a single root node.` |
| - | エラーハンドリング | asyncDepのPromiseがrejectされた | `handleError(err, instance, ErrorCodes.SETUP_FUNCTION)` |

### リトライ仕様

該当なし。非同期コンポーネントのリトライはdefineAsyncComponent側で管理。

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

該当なし。

## パフォーマンス要件

- `__FEATURE_SUSPENSE__`フラグによりtree-shaking対応（使用しない場合はバンドルから除外）
- hiddenContainerでのオフDOMレンダリングにより、ペンディング中のDOMフラッシュを防止
- effectsのバッファリングにより、解決前の不要なエフェクト実行を抑制

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

特記事項なし。

## 備考

- `__FEATURE_SUSPENSE__`フラグがfalseの場合、`Suspense`はnullとしてエクスポートされる
- `SuspenseBoundary`はコンポーネントとして扱われるが、内部的にはレンダラーに直接フックするspecial built-in type
- `queueEffectWithSuspense`: Suspenseがペンディング中の場合はeffects配列にバッファリングし、それ以外の場合は直接`queuePostFlushCb`に渡す
- `setActiveBranch`: HOC（Higher Order Component）の場合、branch.componentを辿ってelを取得

---

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

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

### 推奨読解順序

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

SuspensePropsとSuspenseBoundaryインターフェースを理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | Suspense.ts | `packages/runtime-core/src/components/Suspense.ts` | `SuspenseProps`（36-47行目）、`SuspenseBoundary`インターフェース（414-444行目） |

**読解のコツ**: `SuspenseBoundary`は`resolve`, `fallback`, `move`, `next`, `registerDep`, `unmount`の6つのメソッドを持つ境界オブジェクト。`deps`カウンターが0になるとresolveが呼ばれる。

#### Step 2: process関数を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | Suspense.ts | `packages/runtime-core/src/components/Suspense.ts` | `SuspenseImpl.process()`（69-125行目） |
| 2-2 | Suspense.ts | `packages/runtime-core/src/components/Suspense.ts` | `mountSuspense()`（154-217行目） |
| 2-3 | Suspense.ts | `packages/runtime-core/src/components/Suspense.ts` | `patchSuspense()`（219-412行目） |

#### Step 3: SuspenseBoundaryの生成とresolveを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | Suspense.ts | `packages/runtime-core/src/components/Suspense.ts` | `createSuspenseBoundary()`（448-778行目） |

**主要処理フロー**:
1. **496-511行目**: 境界オブジェクトの初期化
2. **513-630行目**: `resolve`メソッド - pendingBranchの移動、effectsフラッシュ
3. **632-681行目**: `fallback`メソッド - フォールバック表示
4. **693-754行目**: `registerDep`メソッド - 非同期コンポーネント登録

#### Step 4: スロット正規化を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | Suspense.ts | `packages/runtime-core/src/components/Suspense.ts` | `normalizeSuspenseChildren`（832-841行目）、`normalizeSuspenseSlot`（843-877行目） |

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

```
<Suspense>
    │
    ├─ SuspenseImpl.process(n1, n2, ...)
    │      ├─ mountSuspense() [初回]
    │      │      ├─ createSuspenseBoundary()
    │      │      ├─ patch(pendingBranch → hiddenContainer)
    │      │      ├─ triggerEvent('onPending'/'onFallback')
    │      │      └─ patch(fallback → container)
    │      └─ patchSuspense() [更新]
    │             └─ patch(pendingBranch/activeBranch)
    │
    ├─ SuspenseBoundary
    │      ├─ resolve()
    │      │      ├─ move(pendingBranch → container)
    │      │      ├─ unmount(activeBranch)
    │      │      ├─ queuePostFlushCb(effects)
    │      │      └─ triggerEvent('onResolve')
    │      ├─ fallback()
    │      │      ├─ unmount(activeBranch)
    │      │      └─ patch(fallbackVNode → container)
    │      ├─ registerDep()
    │      │      ├─ deps++
    │      │      ├─ asyncDep.then(handleSetupResult)
    │      │      └─ deps-- → resolve()
    │      └─ unmount()
    │
    └─ normalizeSuspenseChildren()
           ├─ normalizeSuspenseSlot(default) → ssContent
           └─ normalizeSuspenseSlot(fallback) → ssFallback
```

### データフロー図

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

defaultスロット       ┌──────────────────────────┐
  (async components)─▶│ mountSuspense             │
                    │  ├ hiddenContainerでpatch   │
                    │  ├ deps追跡                 │
                    │  └ deps > 0 → fallback表示  │──────▶ fallbackスロットのDOM

非同期dep解決        ┌──────────────────────────┐
  (Promise resolve)──▶│ registerDep callback      │
                    │  ├ handleSetupResult        │
                    │  ├ setupRenderEffect        │
                    │  └ deps-- → resolve()       │
                    └──────────┬───────────────┘
                               │
                    ┌──────────▼──────────────┐
                    │ resolve()                  │
                    │  ├ move(pending→container) │──────▶ defaultスロットのDOM
                    │  ├ unmount(fallback)       │
                    │  └ flush effects           │──────▶ onResolveイベント
                    └──────────────────────────┘
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| Suspense.ts | `packages/runtime-core/src/components/Suspense.ts` | ソース | Suspenseコンポーネントのメイン実装 |
| renderer.ts | `packages/runtime-core/src/renderer.ts` | ソース | process/unmountの呼び出し元、queuePostRenderEffect |
| component.ts | `packages/runtime-core/src/component.ts` | ソース | handleSetupResultの提供 |
| apiAsyncComponent.ts | `packages/runtime-core/src/apiAsyncComponent.ts` | ソース | defineAsyncComponentとの連携 |
| scheduler.ts | `packages/runtime-core/src/scheduler.ts` | ソース | queuePostFlushCbの提供 |
| componentRenderUtils.ts | `packages/runtime-core/src/componentRenderUtils.ts` | ソース | filterSingleRoot、updateHOCHostEl |
| index.ts | `packages/runtime-core/src/index.ts` | ソース | Suspenseのエクスポート |
