# 機能設計書 42-unstable_cache

## 概要

本ドキュメントは、Next.jsの`unstable_cache`関数の設計を記述する。関数レベルのキャッシュAPIであり、データベースクエリや高コストな計算の結果をキャッシュし、複数リクエスト間で再利用するための仕組みである。

### 本機能の処理概要

**業務上の目的・背景**：Webアプリケーションでは、データベースクエリやAPIコール等の高コストな処理が頻繁に発生する。`unstable_cache`はこれらの処理結果をIncrementalCacheに保存し、再検証間隔に基づいて自動的に再利用・更新する仕組みを提供する。fetch APIを使わないデータ取得（ORM経由等）でもNext.jsのキャッシュ機構を利用可能にする。

**機能の利用シーン**：データベースクエリの結果キャッシュ、外部サービスのレスポンスキャッシュ、高コストな計算結果の再利用など。

**主要な処理内容**：
1. コールバック関数とキーパーツからキャッシュキーを生成
2. IncrementalCacheからキャッシュエントリを取得
3. フレッシュなエントリがあればそのまま返却
4. staleなエントリがあればstaleデータを返却しつつバックグラウンド再検証
5. ミスの場合はコールバックを実行し結果をキャッシュに保存
6. タグとrevalidate値を親スコープに伝播

**関連システム・外部連携**：IncrementalCache、WorkStore/WorkUnitStore（AsyncLocalStorage）

**権限による制御**：なし。ただしDraft Mode時はキャッシュをバイパスする。

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| - | - | - | 本機能は画面に直接関連しない。サーバーサイドのデータ取得レイヤーで動作する |

## 機能種別

データ連携 / キャッシュ制御

## 入力仕様

### 入力パラメータ

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| cb | (...args: any[]) => Promise<any> | Yes | キャッシュ対象のコールバック関数 | - |
| keyParts | string[] | No | キャッシュキーの固定部分 | - |
| options.revalidate | number \| false | No | キャッシュ再検証間隔（秒）。falseは無期限 | 0は不可（エラー）、正の数値またはfalse |
| options.tags | string[] | No | キャッシュタグ | validateTagsでバリデーション |

### 入力データソース

関数の引数から直接取得。IncrementalCacheはWorkStoreまたはglobalThis.__incrementalCacheから取得。

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| result | T | コールバック関数の戻り値。キャッシュヒット時はキャッシュから復元されたJSON |

### 出力先

呼び出し元の関数。キャッシュデータはIncrementalCacheに`CachedRouteKind.FETCH`形式で保存（bodyはJSON.stringify済み）。

## 処理フロー

### 処理シーケンス

```
1. unstable_cache(cb, keyParts, options) 呼び出し
   └─ options.revalidate === 0 の場合はエラースロー
2. タグのバリデーション（validateTags）
3. revalidateのバリデーション（validateRevalidate）
4. 固定キーの生成（cb.toString() + keyParts）
5. ラップされた関数 cachedCb の返却
6. cachedCb 実行時:
   a. IncrementalCache取得
   b. 呼び出しキー生成（fixedKey + JSON.stringify(args)）
   c. キャッシュキー生成（incrementalCache.generateCacheKey）
   d. キャッシュルックアップ
   e. ヒット&フレッシュ → JSON.parseして返却
   f. ヒット&stale → staleデータ返却 + バックグラウンド再検証
   g. ミス → cb実行 → 結果をキャッシュに保存 → 返却
```

### フローチャート

```mermaid
flowchart TD
    A[unstable_cache呼び出し] --> B[バリデーション]
    B --> C[fixedKey生成]
    C --> D[cachedCb返却]
    D --> E[cachedCb実行]
    E --> F{IncrementalCache取得可能?}
    F -->|No| G[エラースロー]
    F -->|Yes| H[キャッシュキー生成]
    H --> I{ネスト/force-no-store/draft?}
    I -->|Yes| J[cb直接実行]
    I -->|No| K[IncrementalCache.get]
    K --> L{キャッシュヒット?}
    L -->|Fresh| M[JSON.parseして返却]
    L -->|Stale| N[stale返却 + バックグラウンド再検証]
    L -->|Miss| O[cb実行]
    O --> P[結果をキャッシュに保存]
    P --> Q[結果を返却]
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-42-01 | revalidate: 0禁止 | revalidateに0を指定するとエラー。falseまたは正の数値のみ | 常時 |
| BR-42-02 | ネスト回避 | unstable_cache内のunstable_cacheはキャッシュをバイパス | isNestedUnstableCache判定 |
| BR-42-03 | タグ・revalidate伝播 | 親スコープ（prerender, cache等）にタグとrevalidate値を伝播 | workUnitStore存在時 |
| BR-42-04 | ドラフトモードバイパス | Draft Mode時はキャッシュルックアップをスキップ | workStore.isDraftMode |
| BR-42-05 | 静的生成時のstale | 静的生成中にstaleエントリを検出した場合はブロッキング再検証 | workStore.isStaticGeneration && entry.isStale |

### 計算ロジック

- キャッシュキー: `cb.toString()` + `keyParts.join(',')` + `JSON.stringify(args)` をIncrementalCacheでハッシュ化
- キャッシュ保存時のrevalidate: 数値ならそのまま、それ以外は`CACHE_ONE_YEAR`

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

本機能はRDBを直接操作しない。IncrementalCache（ファイルシステム/メモリ）を使用してキャッシュデータを保存する。

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| - | InvariantError | revalidateに0が指定された | エラーメッセージでfalseまたは正の数値を指定するよう案内 |
| - | InvariantError | IncrementalCacheが見つからない | エラーをスローし呼び出し元に通知 |
| - | CacheEntryInvalid | キャッシュエントリのkindがFETCHでない | console.errorで警告しキャッシュミスとして処理 |

### リトライ仕様

staleエントリの再検証でエラーが発生した場合、エラーをconsole.errorで出力しstaleデータを返却する（静的生成時）。

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

トランザクション管理は不要。キャッシュ操作はIncrementalCacheのlock機構で並行制御される。

## パフォーマンス要件

- キャッシュヒット時はコールバック関数の再実行を回避
- stale-while-revalidateパターンで古いデータを即座に返却しつつバックグラウンドで更新

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

- コールバック関数の実行は`workUnitAsyncStorage.run(innerCacheStore, cb, ...args)`でクリーンなコンテキストで実行
- innerCacheStoreの`type: 'unstable-cache'`により、内部のfetchは`force-no-store`として扱われる

## 備考

- `unstable_`プレフィックスが示す通り実験的APIであり、将来的に`use cache`ディレクティブに置き換えられる可能性がある
- Pages Router環境でもworkStore外で動作可能だが、その場合はstaleエントリの返却を行わない（常に最新データを取得）

---

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

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

### 推奨読解順序

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

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | types.ts | `packages/next/src/server/response-cache/types.ts` | CachedFetchData型（41-46行目）とCachedFetchValue型（57-64行目） |
| 1-2 | unstable-cache.ts | `packages/next/src/server/web/spec-extension/unstable-cache.ts` | UnstableCacheStore型のインポート（20-22行目） |

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

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | unstable-cache.ts | `packages/next/src/server/web/spec-extension/unstable-cache.ts` | `unstable_cache()`関数（60-392行目） |

**主要処理フロー**:
1. **71-75行目**: revalidate === 0のバリデーション
2. **78-86行目**: タグ・revalidateのバリデーション
3. **95-97行目**: fixedKey生成
4. **99-389行目**: cachedCb関数の本体

#### Step 3: キャッシュルックアップと保存を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | unstable-cache.ts | `packages/next/src/server/web/spec-extension/unstable-cache.ts` | キャッシュルックアップ（214-296行目）、キャッシュ保存（300-322行目） |

**主要処理フロー**:
- **133-134行目**: invocationKeyとcacheKeyの生成
- **214-221行目**: IncrementalCache.get()呼び出し
- **237-240行目**: キャッシュヒット時のJSON.parse
- **242-291行目**: stale時のバックグラウンド再検証
- **300-304行目**: ミス時のcb実行
- **314-322行目**: 結果のキャッシュ保存

#### Step 4: キャッシュ保存ヘルパーを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | unstable-cache.ts | `packages/next/src/server/web/spec-extension/unstable-cache.ts` | `cacheNewResult()`関数（28-53行目） |

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

```
unstable_cache(cb, keyParts, options)
    │
    ├─ validateTags()
    ├─ validateRevalidate()
    │
    └─ cachedCb(...args)   [返却される関数]
           │
           ├─ workAsyncStorage.getStore()
           ├─ incrementalCache.generateCacheKey()
           ├─ incrementalCache.get()
           │      └─ entry判定 (fresh/stale/miss)
           ├─ workUnitAsyncStorage.run(innerCacheStore, cb, ...args)
           └─ cacheNewResult()
                  └─ incrementalCache.set()
```

### データフロー図

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

cb + keyParts ───▶ fixedKey生成                         ───▶ cachedCb関数
    + options         │
                      └─ cachedCb(args)
                           │
                           ├─▶ cacheKey生成
                           ├─▶ IncrementalCache.get()
                           │      │
                           │      ├─ Fresh ──▶ JSON.parse(body)
                           │      ├─ Stale ──▶ stale + 再検証
                           │      └─ Miss ──▶ cb() → cacheNewResult()
                           └─▶ 結果返却
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| unstable-cache.ts | `packages/next/src/server/web/spec-extension/unstable-cache.ts` | ソース | unstable_cacheの本体実装 |
| patch-fetch.ts | `packages/next/src/server/lib/patch-fetch.ts` | ソース | validateRevalidate/validateTags関数の提供元 |
| types.ts | `packages/next/src/server/response-cache/types.ts` | ソース | CachedFetchData/CachedFetchValue型定義 |
| work-async-storage.external.ts | `packages/next/src/server/app-render/work-async-storage.external.ts` | ソース | WorkStore |
| work-unit-async-storage.external.ts | `packages/next/src/server/app-render/work-unit-async-storage.external.ts` | ソース | WorkUnitStore/UnstableCacheStore |
