# 機能設計書 81-共有ユーティリティライブラリ

## 概要

本ドキュメントは、Decap CMSの共有ユーティリティライブラリ（`decap-cms-lib-util`パッケージ）の機能設計について記述する。本パッケージは、CMS全体で使用されるCursorページネーション、localForageラッパー、Blob操作、URLヘルパー、非同期ロック等の共有ユーティリティを提供する。

### 本機能の処理概要

**業務上の目的・背景**：Decap CMSは複数のバックエンド（GitHub、GitLab、Bitbucket等）をサポートしており、各バックエンドで共通して必要となるAPI通信、ファイル操作、ページネーション、エラーハンドリング、Git LFS対応等のユーティリティを一元管理することで、コードの重複を排除し、保守性を向上させる必要がある。本パッケージはこの共通基盤を提供する。

**機能の利用シーン**：バックエンド実装（GitHub、GitLab等）がAPIを呼び出す際のリクエスト構築・レスポンス解析、エントリ一覧のカーソルベースページネーション、ローカルキャッシュの管理、メディアファイルのBlobハッシュ計算、非同期処理のロック制御、Git LFSポインタファイルの操作など、CMS全体のあらゆるバックエンド連携場面で使用される。

**主要な処理内容**：
1. Cursorによるカーソルベースページネーション管理（actions/data/metaの状態管理）
2. localForageラッパーによるローカルストレージ操作（容量チェック付き）
3. unsentRequestによるHTTPリクエストの構築と実行（タイムアウト制御、Immutableマップベース）
4. APIError/EditorialWorkflowError/AccessTokenError等のカスタムエラークラス
5. asyncLockによるセマフォベースの非同期排他ロック制御
6. getBlobSHAによるBlobデータのSHA-256ハッシュ計算
7. API通信ユーティリティ（requestWithBackoffによるリトライ、レート制限対応）
8. パスユーティリティ（basename、fileExtension、isAbsolutePath）
9. バックエンドユーティリティ（filterByExtension、parseLinkHeader、getAllResponses、getPathDepth）
10. Implementation型定義とエントリ取得ヘルパー（entriesByFolder、entriesByFiles、allEntriesByFolder）
11. Git LFSポインタファイルの解析・生成・.gitattributesファイルのパース
12. CMS_BRANCH_PREFIX、generateContentKey等のAPIユーティリティ定数・関数

**関連システム・外部連携**：各Gitバックエンド（GitHub REST/GraphQL API、GitLab API、Bitbucket API）、localForage（IndexedDB/WebSQL/localStorage）、Git LFSサーバー

**権限による制御**：本パッケージ自体は権限制御を行わないが、API通信時にBearerトークンによる認証ヘッダーを付与する機能を持つ。

## 関連画面

本パッケージは直接的にUIを持たないユーティリティライブラリであるため、画面との関連は間接的である。全てのバックエンド操作を通じて全画面に影響する。

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| - | 全画面（間接的） | 基盤機能 | バックエンドAPI通信、キャッシュ管理、ページネーション等の基盤を提供 |

## 機能種別

ユーティリティ / 基盤ライブラリ / API通信 / データ操作

## 入力仕様

### 入力パラメータ

本パッケージは多数のユーティリティ関数を含むため、主要な関数ごとにパラメータを記載する。

#### requestWithBackoff

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| api | API | Yes | rateLimiter、buildRequest、requestFunctionを持つAPIオブジェクト | - |
| req | ApiRequest | Yes | リクエストオブジェクトまたはURL文字列 | - |
| attempt | number | No | リトライ試行回数（デフォルト1） | 5回を超えたらスロー |

#### readFile

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| id | string/null | No | ファイルID（キャッシュキーに使用） | - |
| fetchContent | () => Promise<string/Blob> | Yes | コンテンツ取得関数 | - |
| localForage | LocalForage | Yes | ローカルキャッシュストア | - |
| isText | boolean | Yes | テキストモードかどうか | - |

#### entriesByFolder

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| listFiles | () => Promise<ImplementationFile[]> | Yes | ファイル一覧取得関数 | - |
| readFile | ReadFile | Yes | ファイル読み取り関数 | - |
| readFileMetadata | ReadFileMetadata | Yes | ファイルメタデータ取得関数 | - |
| apiName | string | Yes | API名（エラーメッセージ用） | - |

### 入力データソース

各バックエンドAPIからのHTTPレスポンス、localForageに格納されたキャッシュデータ、Gitリポジトリのファイルシステム情報

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| Cursor | Cursor | ページネーション状態を管理するカーソルオブジェクト |
| ImplementationEntry | ImplementationEntry | data（文字列）とfile（パス・メタデータ）を持つエントリオブジェクト |
| APIError | APIError | message、status、api名を持つエラーオブジェクト |
| AsyncLock | AsyncLock | acquire/releaseメソッドを持つロックオブジェクト |
| Response | Response | HTTP応答オブジェクト（requestWithBackoff） |
| PointerFile | PointerFile | size、shaを持つGit LFSポインタファイルオブジェクト |

### 出力先

呼び出し元のバックエンド実装、Reduxアクション、UIコンポーネント等。localForageキャッシュへの永続化も行う。

## 処理フロー

### 処理シーケンス

```
1. APIリクエスト処理（requestWithBackoff）
   └─ buildRequestでリクエスト構築 → rateLimiterでレート制限 → fetch実行 → 429/403時にリトライ
2. エントリ一覧取得（entriesByFolder / entriesByFiles）
   └─ ファイルリスト取得 → セマフォ制御（10並列制限）で各ファイルを読み込み → メタデータ併合
3. 差分ベースエントリ取得（allEntriesByFolder）
   └─ ローカルツリー取得 → 差分計算 → 新規/削除ファイルの反映 → ローカルツリー更新・永続化
4. readFile（キャッシュ付きファイル読み取り）
   └─ localForageからキャッシュ確認 → ヒットすれば返却 → ミスならfetchContent実行 → キャッシュ保存
```

### フローチャート

```mermaid
flowchart TD
    A[APIリクエスト開始] --> B{rateLimiter存在?}
    B -->|Yes| C[acquire待機]
    B -->|No| D[buildRequest実行]
    C --> D
    D --> E[fetch実行]
    E --> F{レスポンスステータス}
    F -->|200| G[レスポンス返却]
    F -->|429| H{attempt <= 5?}
    F -->|403 rate limit| H
    H -->|Yes| I[rateLimiter作成・待機]
    I --> D
    H -->|No| J[エラースロー]
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-81-01 | 並列ダウンロード制限 | エントリ一覧取得時のファイル読み込みは最大10並列に制限される | entriesByFolder/entriesByFiles呼び出し時 |
| BR-81-02 | レート制限リトライ | API応答が429またはGitHub 403（rate limit exceeded）の場合、最大5回までリトライする | requestWithBackoff呼び出し時 |
| BR-81-03 | 非同期ロックタイムアウト | asyncLockのacquireは15秒でタイムアウトし、ロックをリセットする | asyncLock.acquire呼び出し時 |
| BR-81-04 | ファイルキャッシュ | readFileはlocalForageに読み込み結果をキャッシュし、IDが一致すればキャッシュから返す | readFile呼び出し時（idが存在する場合） |
| BR-81-05 | ローカルツリー差分更新 | allEntriesByFolderはローカルツリーが存在する場合、差分のみを計算して効率的に更新する | allEntriesByFolder呼び出し時 |

### 計算ロジック

- BlobのSHA-256ハッシュ計算：FileReaderでArrayBufferとして読み込み、js-sha256ライブラリで計算
- リトライ待機時間：`attempt * attempt`秒（指数的バックオフ）、またはRateLimitErrorのresetSeconds

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

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

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| キャッシュ読み取り | localForage | SELECT | `gh.{id}`キーでのファイルコンテンツキャッシュ取得 |
| キャッシュ書き込み | localForage | INSERT | ファイルコンテンツ・メタデータのキャッシュ保存 |
| ローカルツリー保存 | localForage | INSERT/UPDATE | `git.local.{branch}.{folder}.{extension}.{depth}`キーでのツリー保存 |
| ローカルツリー取得 | localForage | SELECT | ローカルツリーの読み取り |

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

#### localForage（IndexedDB）

| 操作 | 項目（キー） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| SELECT/INSERT | `gh.{id}` | ファイルコンテンツ（テキスト） | テキストファイルのキャッシュ |
| SELECT/INSERT | `gh.{id}.blob` | ファイルコンテンツ（Blob） | バイナリファイルのキャッシュ |
| SELECT/INSERT | `gh.{id}.meta` | FileMetadata（author, updatedOn） | ファイルメタデータのキャッシュ |
| SELECT/INSERT | `git.local.{branch}.{folder}.{ext}.{depth}` | LocalTree（head, files） | ローカルツリーのキャッシュ |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| API_ERROR | APIError | APIレスポンスがエラー（4xx/5xx） | message、status、api名をエラーオブジェクトに格納しスロー |
| EDITORIAL_WORKFLOW_ERROR | EditorialWorkflowError | エディトリアルワークフロー操作時のエラー | notUnderEditorialWorkflowフラグで区別 |
| ACCESS_TOKEN_ERROR | AccessTokenError | アクセストークンが無効 | 再認証を促す |
| - | RateLimitError | API rate limit exceeded（429/403） | resetSecondsに基づきリトライ |
| - | Error | リクエストタイムアウト（60秒） | AbortControllerで中断、エラーメッセージ表示 |
| - | Error | localForage容量超過（code: 22） | コンソール警告出力 |

### リトライ仕様

- requestWithBackoff: 最大5回リトライ。リトライ間隔はattempt^2秒。RateLimitError時はresetSeconds（最小1秒、最大3600秒）
- 暗黙のAuthではリトライしない（"Can't refresh access token when using implicit auth"メッセージ時）

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

localForageの操作は個別のsetItem/getItem呼び出しで行われ、明示的なトランザクション管理は行わない。asyncLockによる排他制御でデータ整合性を確保する。

## パフォーマンス要件

- 並列ダウンロード数: 最大10（semaphoreで制御）
- HTTPリクエストタイムアウト: 60秒
- asyncLockタイムアウト: 15秒
- getAllResponsesの最大ページ数: 30
- allEntriesByFolderはローカルツリーとの差分計算により、大量エントリの再取得を回避

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

- APIリクエスト時のBearerトークン付与（constructRequestHeaders）
- アクセストークンの検証とAccessTokenErrorによる無効トークンの検出
- Git LFSポインタファイルのSHA-256ハッシュによるデータ整合性検証
- localForageに保存されるキャッシュデータは暗号化されない（ブラウザローカルストレージ）

## 備考

- 本パッケージのバージョンは3.4.0
- immutable（^3.7.6）とlodash（^4.17.11）をpeerDependenciesとして要求
- js-sha256、localforage、semaphoreを直接依存として使用
- Cursorクラスの`CURSOR_COMPATIBILITY_SYMBOL`は旧バックエンドとの互換性のための一時的な仕組み

---

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

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

### 推奨読解順序

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

まず、パッケージ全体で使用される型定義とデータ構造を理解することが重要。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | implementation.ts | `packages/decap-cms-lib-util/src/implementation.ts` | Implementation、ImplementationEntry、ImplementationFile、Entry、PersistOptions等の中核型定義 |
| 1-2 | Cursor.ts | `packages/decap-cms-lib-util/src/Cursor.ts` | CursorStoreObject型（actions/data/meta）、Cursorクラスの構造 |
| 1-3 | API.ts | `packages/decap-cms-lib-util/src/API.ts` | ApiRequestObject型、FileMetadata型、FetchError型 |
| 1-4 | git-lfs.ts | `packages/decap-cms-lib-util/src/git-lfs.ts` | PointerFile型 |

**読解のコツ**: `implementation.ts`のImplementationインターフェースは全バックエンドが実装すべき契約であり、CMSコアとバックエンドの境界を定義する最も重要な型である。

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

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | index.ts | `packages/decap-cms-lib-util/src/index.ts` | 全モジュールのre-export構造と公開API一覧 |

**主要処理フロー**:
1. **1-51行目**: 各モジュールからのimportと型のre-export
2. **103-156行目**: DecapCmsLibUtilオブジェクトとしての名前付きエクスポート
3. **157-213行目**: 個別名前付きエクスポート

#### Step 3: HTTP通信層を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | unsentRequest.js | `packages/decap-cms-lib-util/src/unsentRequest.js` | ImmutableマップベースのHTTPリクエストビルダー |
| 3-2 | API.ts | `packages/decap-cms-lib-util/src/API.ts` | requestWithBackoff関数のリトライロジック |
| 3-3 | backendUtil.ts | `packages/decap-cms-lib-util/src/backendUtil.ts` | parseResponse、parseLinkHeader、getAllResponses |
| 3-4 | APIError.ts | `packages/decap-cms-lib-util/src/APIError.ts` | APIエラークラス |

**主要処理フロー**:
- **unsentRequest.js 15-32行目**: fetchWithTimeoutによる60秒タイムアウト付きfetch
- **unsentRequest.js 85-88行目**: performRequest関数でリクエスト実行
- **API.ts 63-117行目**: requestWithBackoff関数のリトライとレート制限対応
- **API.ts 195-218行目**: apiRequest関数の統合リクエスト処理

#### Step 4: エントリ取得処理を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | implementation.ts | `packages/decap-cms-lib-util/src/implementation.ts` | fetchFiles、entriesByFolder、entriesByFiles、allEntriesByFolder |

**主要処理フロー**:
- **228-259行目**: fetchFiles - セマフォ制御（10並列）でファイル読み込み
- **261-269行目**: entriesByFolder - ファイルリスト取得→fetchFiles呼び出し
- **280-290行目**: unpublishedEntries - 未公開エントリキー一覧取得
- **487-590行目**: allEntriesByFolder - ローカルツリーとの差分ベース更新

#### Step 5: ロック・キャッシュ・その他を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 5-1 | asyncLock.ts | `packages/decap-cms-lib-util/src/asyncLock.ts` | セマフォベースの非同期ロック実装 |
| 5-2 | localForage.ts | `packages/decap-cms-lib-util/src/localForage.ts` | localForageラッパー（容量チェック） |
| 5-3 | getBlobSHA.ts | `packages/decap-cms-lib-util/src/getBlobSHA.ts` | BlobのSHA-256ハッシュ計算 |
| 5-4 | path.ts | `packages/decap-cms-lib-util/src/path.ts` | パスユーティリティ関数 |
| 5-5 | promise.ts | `packages/decap-cms-lib-util/src/promise.ts` | Promiseユーティリティ |
| 5-6 | APIUtils.ts | `packages/decap-cms-lib-util/src/APIUtils.ts` | CMSブランチプレフィックス、コンテンツキー生成 |

**主要処理フロー**:
- **asyncLock.ts 8-24行目**: acquire関数 - 15秒タイムアウト付きセマフォ取得
- **asyncLock.ts 26-39行目**: release関数 - セマフォ解放（leave過多への対応付き）
- **getBlobSHA.ts 3-12行目**: FileReaderでArrayBuffer読み込み→sha256計算

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

```
decap-cms-lib-util/src/index.ts（エントリーポイント）
    |
    +-- Cursor.ts（ページネーション管理）
    |
    +-- localForage.ts（ローカルキャッシュ）
    |
    +-- unsentRequest.js（HTTPリクエストビルダー）
    |      +-- fetchWithTimeout（タイムアウト付きfetch）
    |
    +-- API.ts（API通信）
    |      +-- requestWithBackoff
    |      |      +-- api.buildRequest
    |      |      +-- unsentRequest.performRequest
    |      +-- apiRequest
    |      |      +-- constructRequestHeaders
    |      |      +-- constructUrlWithParams
    |      |      +-- requestWithBackoff
    |      +-- readFile（キャッシュ付きファイル読み取り）
    |      +-- readFileMetadata（キャッシュ付きメタデータ読み取り）
    |      +-- getDefaultBranchName
    |
    +-- backendUtil.ts（レスポンス解析）
    |      +-- parseResponse
    |      +-- parseLinkHeader
    |      +-- getAllResponses
    |      +-- filterByExtension
    |
    +-- implementation.ts（エントリ取得）
    |      +-- fetchFiles（セマフォ制御並列読み込み）
    |      +-- entriesByFolder → fetchFiles
    |      +-- entriesByFiles → fetchFiles
    |      +-- allEntriesByFolder → getDiffFromLocalTree → fetchFiles
    |      +-- getMediaDisplayURL → getMediaAsBlob → readFile
    |
    +-- asyncLock.ts（排他制御）
    +-- getBlobSHA.ts（ハッシュ計算）
    +-- APIError.ts / EditorialWorkflowError.ts / AccessTokenError.ts
    +-- APIUtils.ts（ブランチ・コンテンツキーユーティリティ）
    +-- path.ts（パスユーティリティ）
    +-- promise.ts（Promiseユーティリティ）
    +-- git-lfs.ts（Git LFSサポート）
    +-- loadScript.js（スクリプトローダー）
```

### データフロー図

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

APIリクエスト ──────▶ unsentRequest.performRequest ──────▶ HTTPレスポンス
                           |
                    requestWithBackoff
                    (リトライ・レート制限)
                           |
                    parseResponse ──────────────────▶ 解析済みデータ

ファイルID ────────▶ readFile ──────────────────────▶ ファイルコンテンツ
                    (localForageキャッシュ経由)

ファイルリスト ────▶ fetchFiles ────────────────────▶ ImplementationEntry[]
                    (セマフォ10並列制御)

ローカルツリー ───▶ allEntriesByFolder ─────────────▶ ImplementationEntry[]
+ ブランチ差分       (差分ベース更新)
                    |
                    persistLocalTree ──────────────▶ localForageキャッシュ

Blobデータ ────────▶ getBlobSHA ────────────────────▶ SHA-256ハッシュ文字列

LFSポインタ文字列 ─▶ parsePointerFile ──────────────▶ PointerFile{size,sha}
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| index.ts | `packages/decap-cms-lib-util/src/index.ts` | ソース | エントリーポイント（全モジュールのre-export） |
| Cursor.ts | `packages/decap-cms-lib-util/src/Cursor.ts` | ソース | カーソルベースページネーション管理 |
| localForage.ts | `packages/decap-cms-lib-util/src/localForage.ts` | ソース | localForageラッパー |
| API.ts | `packages/decap-cms-lib-util/src/API.ts` | ソース | API通信ユーティリティ（requestWithBackoff、readFile等） |
| APIError.ts | `packages/decap-cms-lib-util/src/APIError.ts` | ソース | APIエラークラス |
| APIUtils.ts | `packages/decap-cms-lib-util/src/APIUtils.ts` | ソース | ブランチ・コンテンツキーユーティリティ |
| asyncLock.ts | `packages/decap-cms-lib-util/src/asyncLock.ts` | ソース | 非同期排他ロック |
| backendUtil.ts | `packages/decap-cms-lib-util/src/backendUtil.ts` | ソース | バックエンドレスポンス解析ユーティリティ |
| getBlobSHA.ts | `packages/decap-cms-lib-util/src/getBlobSHA.ts` | ソース | BlobのSHA-256ハッシュ計算 |
| git-lfs.ts | `packages/decap-cms-lib-util/src/git-lfs.ts` | ソース | Git LFSポインタファイル操作 |
| implementation.ts | `packages/decap-cms-lib-util/src/implementation.ts` | ソース | バックエンド実装型定義とエントリ取得ヘルパー |
| loadScript.js | `packages/decap-cms-lib-util/src/loadScript.js` | ソース | 外部スクリプトローダー |
| path.ts | `packages/decap-cms-lib-util/src/path.ts` | ソース | パスユーティリティ（basename、fileExtension等） |
| promise.ts | `packages/decap-cms-lib-util/src/promise.ts` | ソース | Promiseユーティリティ（onlySuccessfulPromises等） |
| unsentRequest.js | `packages/decap-cms-lib-util/src/unsentRequest.js` | ソース | HTTPリクエストビルダー（Immutableマップベース） |
| EditorialWorkflowError.ts | `packages/decap-cms-lib-util/src/EditorialWorkflowError.ts` | ソース | エディトリアルワークフローエラークラス |
| AccessTokenError.ts | `packages/decap-cms-lib-util/src/AccessTokenError.ts` | ソース | アクセストークンエラークラス |
| types.ts | `packages/decap-cms-lib-util/src/types.ts` | ソース | Immutable型ガード関数 |
| package.json | `packages/decap-cms-lib-util/package.json` | 設定 | パッケージ依存関係定義 |
