コンテンツにスキップ

Design

Context

DataFetcher クラス(src/services/data-fetcher.js)は、静的サイトホスティングから index.json とセッション JSON を取得するシンプルな HTTP クライアントである。現状はキャッシュ機構がなく、React のページ遷移(Dashboard → MemberDetail → GroupDetail)のたびに同一リソースを再取得している。

現在の実装: - fetchIndex(): data/index.json?v={timestamp} でキャッシュバスター付き取得 - fetchSession(id): data/sessions/{id}.json でキャッシュバスターなし取得 - #fetchJson(url): 共通の fetch ラッパー(Result 型を返却)

Goals / Non-Goals

Goals:

  • ページ遷移時の不要なネットワークリクエストを排除する
  • 同時に発生する同一 URL リクエストを 1 つに統合する
  • DataFetcher の公開 API を変更せずに透過的にキャッシュを導入する
  • 外部ライブラリを使わずカスタム実装する

Non-Goals:

  • React コンポーネント層でのキャッシュ(useSWR, React Query 等の導入)
  • IndexFetcher / BlobStorage 等の他サービスへのキャッシュ適用
  • キャッシュの永続化(localStorage, IndexedDB 等)
  • キャッシュのサイズ制限や LRU 等のエビクション戦略

Decisions

1. キャッシュの保持場所: DataFetcher インスタンス内の Map

選択: DataFetcher クラスのプライベートフィールドとして Map を使用する。

代替案: - モジュールスコープ変数 — テスト時のリセットが困難 - 外部キャッシュライブラリ — Issue の方針で外部ライブラリ不使用が明記されている

理由: インスタンスフィールドにすることで、テスト時に新しいインスタンスを作るだけで状態がリセットされる。Map は URL をキーにした O(1) ルックアップを提供する。

2. キャッシュ戦略の二分化: TTL vs 永続

選択: - index.json → TTL ベースキャッシュ(デフォルト 30 秒) - sessions/*.json → 永続キャッシュ(インスタンスの生存期間中)

理由: index.json はアップロードのたびに更新されるため、古いデータをいつまでも返すのは不適切。一方、セッション JSON は不変リソース(一度書き込まれたら変更されない)であるため、永続キャッシュが安全。

3. キャッシュバスターの扱い: TTL 期間中はスキップ

選択: fetchIndex() は TTL 期間中にキャッシュヒットした場合、ネットワークリクエスト自体を行わない(キャッシュバスター URL も生成しない)。TTL 超過時のみ新しいキャッシュバスター付き URL でリクエストする。

代替案: - 常にキャッシュバスターを付けてリクエストし、レスポンスをキャッシュ — ネットワークリクエスト自体は減らない

理由: キャッシュの目的がネットワークリクエスト削減であるため、TTL 内はリクエスト自体をスキップすべき。

4. 重複排除: inflight Map パターン

選択: #inflight Map にリクエスト中の Promise を URL キーで保持し、同一 URL の同時リクエストは同じ Promise を共有する。Promise が解決したら即座に Map からエントリを削除する。

実装フロー: 1. #fetchJson(url) が呼ばれる 2. #inflight に同一 URL が存在すれば、その Promise を返す 3. 存在しなければ新しい fetch Promise を作成し、#inflight に登録 4. Promise の解決(成功/失敗)後に #inflight から削除

理由: SWR や React Query でも使われている標準的な重複排除パターン。実装がシンプルで、追加の状態管理が不要。

5. キャッシュエントリの構造

// index.json 用(TTL 付き)
{ data: Result, timestamp: number }

// sessions/*.json 用(永続)
{ data: Result }

index.json のキャッシュエントリには timestamp を持たせ、Date.now() - timestamp > ttl で有効期限を判定する。セッションキャッシュは有効期限不要のため data のみ。

6. TTL のデフォルト値とコンストラクタ注入

選択: TTL はコンストラクタの引数で受け取り、デフォルト値 30,000ms(30 秒)を設定する。

理由: テスト時に短い TTL を指定でき、本番コードはデフォルト値を使用。ハードコーディングを避けつつ追加の設定ファイルは不要。

Risks / Trade-offs

  • TTL 内のデータ陳腐化 → 30 秒と短い TTL で許容範囲。管理者が CSV アップロード直後にダッシュボードを確認する場合、最大 30 秒の遅延が発生しうるが、ページリロードで解決可能。
  • メモリ消費の増加 → セッション JSON の永続キャッシュはインスタンスの生存期間中増え続ける。ただし、SPA の 1 セッション内で閲覧するセッション数は限定的(数十件程度)であり、各 JSON は数 KB のため問題ない。
  • エラーレスポンスのキャッシュ → 失敗レスポンス(ok: false)はキャッシュしない。成功時のみキャッシュに保存する。これにより、一時的なネットワークエラー後にリトライが可能。