# 機能設計書 50-キャッシュハンドラ

## 概要

本ドキュメントは、Next.jsのCacheHandler（キャッシュハンドラ）システムの設計を記述する。`"use cache"`ディレクティブで使用されるキャッシュの永続化レイヤーであり、デフォルトのインメモリ実装とカスタムハンドラ（リモートキャッシュ等）の管理を行う。

### 本機能の処理概要

**業務上の目的・背景**：`"use cache"`によるRSCストリームのキャッシュは、さまざまなバックエンドに保存する必要がある。開発環境ではインメモリキャッシュで十分だが、プロダクション環境ではRedis等のリモートキャッシュや、サーバーレス環境での外部キャッシュサービスとの連携が必要になる。CacheHandlerはこのキャッシュバックエンドを抽象化し、プラガブルなアーキテクチャを提供する。

**機能の利用シーン**：`"use cache"`のキャッシュデータ保存/取得、タグベース再検証のタグマニフェスト管理、カスタムキャッシュバックエンドの接続。

**主要な処理内容**：
1. CacheHandlerインターフェースの定義（get, set, refreshTags, getExpiration, updateTags）
2. デフォルトCacheHandler（インメモリLRU）の実装
3. ハンドラの初期化・管理（default/remote/custom）
4. グローバルシンボルを使ったハンドラインスタンスの共有
5. タグマニフェストによるタグベース再検証の管理

**関連システム・外部連携**：LRUCache、tags-manifest、グローバルシンボル（`@next/cache-handlers`）

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

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| - | - | - | 本機能は画面に直接関連しない。サーバーサイドのキャッシュインフラとして動作する |

## 機能種別

インフラ / キャッシュ制御

## 入力仕様

### CacheHandler.get

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| cacheKey | string | Yes | キャッシュキー | - |
| softTags | string[] | Yes | ソフトタグ（暗黙タグ） | - |

### CacheHandler.set

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| cacheKey | string | Yes | キャッシュキー | - |
| pendingEntry | Promise<CacheEntry> | Yes | 保存するキャッシュエントリ（pending） | - |

### CacheHandler.updateTags

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| tags | string[] | Yes | 更新対象のタグ | - |
| durations | { expire?: number } | No | 有効期限設定 | - |

### CacheHandler.getExpiration

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| tags | string[] | Yes | 有効期限を確認するタグ | - |

### 入力データソース

use-cache-wrapper.tsのcache()関数から呼び出される。

## 出力仕様

### 出力データ

| メソッド | 戻り値型 | 説明 |
|---------|---------|------|
| get | CacheEntry \| undefined | キャッシュエントリ。なければundefined |
| set | void | - |
| refreshTags | void | - |
| getExpiration | Timestamp | タグの最大再検証タイムスタンプ。Infinityの場合はget時にチェック |
| updateTags | void | - |

### 出力先

get()の戻り値はuse-cache-wrapper.tsに返却される。set()はメモリ/リモートキャッシュに保存。

## 処理フロー

### デフォルトCacheHandler処理シーケンス

```
■ get(cacheKey, softTags):
1. pendingSetsにペンディング中のsetがあればawait
2. LRUキャッシュからルックアップ
3. 見つからなければ undefined
4. revalidate時間超過 → undefined（インメモリはexpire前にrevalidateで期限切れ）
5. タグの有効期限チェック（areTagsExpired → undefined）
6. タグのstaleチェック（areTagsStale → revalidate = -1）
7. ストリームをteeして片方を返却、もう片方を保存

■ set(cacheKey, pendingEntry):
1. pendingPromiseを設定（並行getの待機用）
2. pendingEntryをawait
3. ストリームをteeしてサイズを計測
4. LRUキャッシュに保存
5. pendingPromiseを解決・削除

■ updateTags(tags, durations):
1. 現在時刻を取得
2. 各タグのタグマニフェストエントリを更新
3. durations指定時: stale = now, expired = now + expire * 1000
4. durations未指定時: expired = now（即時無効化）

■ getExpiration(tags):
1. 各タグのタグマニフェストエントリを参照
2. expired値の最大値を返却
```

### フローチャート

```mermaid
flowchart TD
    A[CacheHandler初期化] --> B{RemoteCache設定?}
    B -->|Yes| C[remoteハンドラ設定]
    B -->|No| D[defaultハンドラ使用]
    C --> E{DefaultCache設定?}
    D --> E
    E -->|Yes| F[カスタムdefaultハンドラ]
    E -->|No| G[createDefaultCacheHandler]
    F --> H[ハンドラMap登録]
    G --> H
    H --> I[use-cache-wrapper.tsから利用]
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-50-01 | インメモリexpire | デフォルトCacheHandlerではrevalidate時間超過でエントリを返さない（stale-while-revalidate非適用） | デフォルトCacheHandler |
| BR-50-02 | pendingSet待機 | get時にpendingSetsにエントリがある場合はawaitしてから返却 | set処理中 |
| BR-50-03 | ゼロサイズ最適化 | maxSize=0の場合はLRUキャッシュを作成せずnoop実装を返却 | cacheMaxMemorySize === 0 |
| BR-50-04 | グローバルシンボル共有 | CacheHandlerはグローバルシンボルで管理し、異なるモジュールコピー間で同一インスタンスを共有 | 常時 |
| BR-50-05 | タグマニフェスト | タグのstale/expired状態を管理し、再検証タイムスタンプを保持 | タグベース再検証 |
| BR-50-06 | getExpiration Infinity | getExpirationがInfinityを返す場合、get時にsoftTagsを渡して期限チェックを行う | リモートCacheHandler |

### 計算ロジック

- エントリ有効期限（デフォルト）: `entry.timestamp + entry.revalidate * 1000`
- タグ有効期限: `tagsManifest.get(tag).expired`の最大値
- staleチェック: `areTagsStale(entry.tags, entry.timestamp)` → revalidate = -1

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

本機能はRDBを使用しない。LRUメモリキャッシュとtags-manifestを使用。

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| - | Error | CacheHandler未初期化状態でgetCacheHandler() | 'Cache handlers not initialized'エラー |
| - | SetError | set時のストリーム読み取りエラー | console.debugで警告出力し保存をスキップ |

### リトライ仕様

set時のストリーム読み取りエラーは保存をスキップし、次回get時にキャッシュミスとして処理される。

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

pendingSetsマップにより、同一キーの並行set/getを制御。set完了まで同一キーのgetはawaitで待機。

## パフォーマンス要件

- LRUキャッシュのサイズはcacheMaxMemorySizeで制御（バイト単位）
- set時にストリームサイズを計測してLRUの重みとして使用
- getExpirationがInfinityを返す場合はget時のタグチェックをリモートハンドラに委譲（ネットワーク遅延の最適化）

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

- グローバルシンボルで管理されているため、同一プロセス内の全モジュールからアクセス可能
- カスタムCacheHandlerの実装はnext.config.jsで指定し、ビルド時に検証

## 備考

- `@next/cache-handlers`シンボルでRemoteCache/DefaultCacheが登録可能
- `default`と`remote`の2種類のハンドラが標準で管理される
- `setCacheHandler(kind, handler)`で追加のカスタムハンドラを登録可能
- デフォルトCacheHandlerのデバッグは`NEXT_PRIVATE_DEBUG_CACHE`環境変数で有効化

---

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

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

### 推奨読解順序

#### Step 1: インターフェースを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | types.ts | `packages/next/src/server/lib/cache-handlers/types.ts` | CacheEntry型（6-40行目）とCacheHandler型（42-80行目） |

**読解のコツ**:
- CacheEntry.valueはReadableStream<Uint8Array>であり、ストリームのtee()やReader操作が頻出する
- set()のpendingEntryはPromise<CacheEntry>であり、awaitが必要な点に注意
- getExpiration()がInfinityを返す場合はget()でsoftTagsベースのチェックが行われる

#### Step 2: ハンドラ管理を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | handlers.ts | `packages/next/src/server/use-cache/handlers.ts` | `initializeCacheHandlers()`（34-80行目）、`getCacheHandler()`（88-95行目） |

**主要処理フロー**:
1. **10-12行目**: グローバルシンボル定義（handlersSymbol, handlersMapSymbol, handlersSetSymbol）
2. **34-80行目**: 初期化処理 - RemoteCache/DefaultCacheの検出とMap登録
3. **88-95行目**: kind指定によるハンドラ取得

#### Step 3: デフォルトCacheHandler実装を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | default.ts | `packages/next/src/server/lib/cache-handlers/default.ts` | `createDefaultCacheHandler()`（39-206行目） |

**主要処理フロー**:
- **39-51行目**: maxSize=0時のnoop実装
- **53-57行目**: LRUキャッシュとpendingSetsの初期化
- **64-119行目**: get()実装 - pendingSet待機、revalidate期限チェック、タグ有効期限チェック
- **121-158行目**: set()実装 - pendingPromise管理、ストリームteeしてサイズ計測、LRU保存
- **160-162行目**: refreshTags() - インメモリのためnoop
- **164-177行目**: getExpiration() - タグマニフェストからexpired値の最大値取得
- **179-204行目**: updateTags() - タグマニフェスト更新（stale/expired設定）

#### Step 4: タグマニフェストを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | tags-manifest.external.ts | `packages/next/src/server/lib/incremental-cache/tags-manifest.external.ts` | tagsManifest, areTagsExpired(), areTagsStale()の実装 |

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

```
initializeCacheHandlers(cacheMaxMemorySize)
    │
    ├─ globalThis[handlersSymbol]チェック
    │      ├─ RemoteCache → handlersMap.set('remote', ...)
    │      └─ DefaultCache → handlersMap.set('default', ...)
    ├─ createDefaultCacheHandler(cacheMaxMemorySize)  [フォールバック]
    └─ handlersSet作成

getCacheHandler(kind) → handlersMap.get(kind)

[デフォルトCacheHandler]
    │
    ├─ get(cacheKey, softTags)
    │      ├─ pendingSets.get(cacheKey)
    │      ├─ memoryCache.get(cacheKey)
    │      ├─ areTagsExpired(tags, timestamp)
    │      ├─ areTagsStale(tags, timestamp)
    │      └─ entry.value.tee()
    │
    ├─ set(cacheKey, pendingEntry)
    │      ├─ pendingSets.set(cacheKey, pendingPromise)
    │      ├─ await pendingEntry
    │      ├─ entry.value.tee() → サイズ計測
    │      └─ memoryCache.set(cacheKey, {entry, size})
    │
    ├─ getExpiration(tags)
    │      └─ tagsManifest.get(tag).expired
    │
    └─ updateTags(tags, durations)
           └─ tagsManifest.set(tag, {stale, expired})
```

### データフロー図

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

cacheKey ───▶ CacheHandler.get()
+ softTags       │
                 ├─▶ LRUCache.get()
                 ├─▶ revalidate期限チェック
                 ├─▶ タグ有効期限チェック
                 └─▶ stream.tee() → CacheEntry

pendingEntry ───▶ CacheHandler.set()
                      │
                      ├─▶ await pendingEntry
                      ├─▶ stream.tee() → サイズ計測
                      └─▶ LRUCache.set()

tags ───▶ CacheHandler.updateTags()
+ durations      │
                 └─▶ tagsManifest.set({stale, expired})
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| types.ts | `packages/next/src/server/lib/cache-handlers/types.ts` | ソース | CacheEntry/CacheHandlerインターフェース |
| default.ts | `packages/next/src/server/lib/cache-handlers/default.ts` | ソース | デフォルトCacheHandler実装（インメモリLRU） |
| default.external.ts | `packages/next/src/server/lib/cache-handlers/default.external.ts` | ソース | 外部公開用のデフォルトCacheHandler |
| handlers.ts | `packages/next/src/server/use-cache/handlers.ts` | ソース | ハンドラの初期化・管理 |
| tags-manifest.external.ts | `packages/next/src/server/lib/incremental-cache/tags-manifest.external.ts` | ソース | タグマニフェスト管理 |
| lru-cache.ts | `packages/next/src/server/lib/lru-cache.ts` | ソース | LRUキャッシュ実装 |
