mcpField
Z
ziflux-expert

ziflux-expert

github.com/neogenz開発178 回インストール15 週間前に更新
リポジトリ

ziflux-expert について

Angular の `resource()` API で SWR キャッシュを使う開発者向けの、ziflux 専門スキルです。キャッシュのライフサイクル、API、機能構成、よくある実装上の注意点を説明します。

主な機能

  1. 01`cachedResource` と `cachedMutation` の説明
  2. 02SWR キャッシュの既定値設定
  3. 03階層型キャッシュキーの設計
  4. 04楽観的更新とロールバック
  5. 05よくあるキャッシュの誤りの診断

活用シーン

  1. 01API、Store、コンポーネントで Angular 機能を構成
  2. 02Angular のビューにキャッシュ付きリソース読み込みを追加
  3. 03キャッシュ無効化とロールバックで変更を処理
  4. 04古いデータやインジェクションコンテキストのエラーを調査

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/neogenz/ziflux --skill ziflux-expert

インストール後、Claude Code で/ziflux-expertを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

references/
api-reference.md
patterns.md
review-and-debug.md

詳細

ziflux のエキスパート

これから ziflux のエキスパートとして対応します。ziflux は、Angular の resource() API に SWR(stale-while-revalidate)キャッシュを追加する Angular 22+ ライブラリです。依存関係はゼロです。シグナルネイティブです。状態管理ライブラリではありません。Angular のシグナルと resource() が状態レイヤーそのものです。ziflux が補うのは、データのライフサイクル(fresh → stale → expired)という、たった 1 つのギャップです。

API は、ドキュメントを読まなくても、あらゆる Angular 開発者が使い方を推測できるように設計されています。resource() を知っていれば、cachedResource() もわかります。

ドメインパターン

すべての機能は、厳格な 3 ファイル構成に従います。これは必須です。

feature.api.ts — providedIn: 'root'。DataCache を所有し、HTTP メソッドを公開します。

@Injectable({ providedIn: 'root' })
export class OrderApi {
  readonly #http = inject(HttpClient);
  readonly cache = new DataCache({ name: 'orders' });

  getOrders(filters: OrderFilters) {
    return this.#http.get<Order[]>('/api/orders', { params: filters });
  }

  getOrder(id: string) {
    return this.#http.get<Order>(`/api/orders/${id}`);
  }

  createOrder(order: NewOrder) {
    return this.#http.post<Order>('/api/orders', order);
  }
}

feature.store.ts — ルートスコープの @Injectable()。cachedResource と cachedMutation を接続します。

@Injectable()
export class OrderListStore {
  readonly #api = inject(OrderApi);

  readonly orders = cachedResource({
    cache: this.#api.cache,
    cacheKey: ['orders', 'list'],
    loader: () => this.#api.getOrders({}),
  });

  readonly createOrder = cachedMutation({
    cache: this.#api.cache,
    mutationFn: (order: NewOrder) => this.#api.createOrder(order),
    invalidateKeys: () => [['orders']],
  });
}

feature.component.ts — ストアを注入し、テンプレートでシグナルを読み取ります。

@Component({
  providers: [OrderListStore],
  template: `
    @if (store.orders.isInitialLoading()) {
      <spinner />
    } @else {
      @for (order of store.orders.value(); track order.id) {
        <order-card [order]="order" />
      }
    }
  `,
})
export class OrderListComponent {
  readonly store = inject(OrderListStore);
}

厳守するルール:

  • コンポーネントから API サービスを直接注入することは絶対にありません
  • HTTP ロジックは API サービスのみに記述します
  • ストアで DataCache をインスタンス化することは絶対にありません。this.#api.cache を読み取ります
  • DataCache は必ず providedIn: 'root' のサービスに置きます(画面遷移後も維持されます)

API クイックリファレンス

provideZiflux(config?, ...features)

app.config.ts で一度だけ呼び出します。グローバルなデフォルト値を設定します。

provideZiflux({ staleTime: 60_000, expireTime: 300_000 }, withDevtools())
DataCache

メモリ内 SWR キャッシュです。インジェクションコンテキスト内で作成する必要があります。

readonly cache = new DataCache({ name: 'orders', staleTime: 30_000, expireTime: 300_000 });

cache.get<T>(key: string[], opts?): { data: T; fresh: boolean } | null
cache.set<T>(key: string[], data: T): void
cache.invalidate(prefix: string[]): void      // marks stale, never deletes
cache.deduplicate<T>(key: string[], fn): Promise<T>  // one in-flight per key
cache.prefetch<T>(key: string[], fn): Promise<void>
cache.wrap<T>(key: string[], obs$): Observable<T>     // tap → set
cache.clear(): void
cache.cleanup(): number                        // evict expired entries
cache.inspect(): CacheInspection<unknown>
cache.version: Signal<number>                  // bumps on invalidate/clear
cachedResource<T, P>(options)

SWR 対応の Angular resource() です。インジェクションコンテキスト内で呼び出す必要があります。

cachedResource({
  cache: this.#api.cache,
  cacheKey: params => ['orders', 'details', params.id],
  params: () => ({ id: this.orderId() }),     // undefined suspends (status: 'idle')
  loader: ({ params, abortSignal }) => this.#api.getOrder(params.id),
  defaultValue: [],                           // optional; makes value() never undefined
  staleTime: 10_000,                          // optional per-resource override
  retry: { maxRetries: 3, baseDelay: 1000 },  // optional
  refetchInterval: 30_000,                    // optional polling, ignores staleTime
  refetchOnWindowFocus: true,                 // optional; respects staleTime, browser-only
  refetchOnReconnect: true,                   // optional; respects staleTime, browser-only
  id: 'orders',                               // optional; reuse the SSR value via TransferState
})

CachedResourceRef<T> を返します。

  • value: Signal<T | undefined> — SWR 対応です。再検証中も古いデータを表示します
  • status: Signal<ResourceStatus> — 'idle'|'loading'|'reloading'|'resolved'|'error'|'local'
  • isLoading: Signal<boolean> — フェッチ中は true です
  • isStale: Signal<boolean> — バックグラウンドで再取得中に古いデータを表示している場合は true です
  • isInitialLoading: Signal<boolean> — キャッシュが空の場合のみ true です(スピナーにはこれを使用します)
  • error: Signal<Error | undefined> — Angular の resource() が公開するものと同じ型です
  • hasValue(): this is Omit<CachedResourceRef<T>, 'value'> & { value: Signal<T> } — 型ガードです。value() の型を絞り込みます
  • reload(): boolean — staleTime の期間内でも、常にネットワークにアクセスします
  • set(value: T): void — 楽観的更新です。DataCache にも書き込みます(ステータスは 'local' になります)
  • update(updater: (T | undefined) => T): void — 楽観的更新です。DataCache にも書き込みます
  • destroy(): void
cachedMutation<A, R, C>(options)

ミューテーションのラッパーです。インジェクションコンテキストは必要ありません。

cachedMutation({
  cache: this.#api.cache,
  mutationFn: (order: NewOrder) => this.#api.createOrder(order),
  invalidateKeys: (args, result) => [['orders']],
  onMutate: (args) => { /* optimistic update; return context */ },
  onSuccess: (result, args) => { /* after invalidation */ },
  onError: (error, args, context) => { /* rollback with context */ },
})

CachedMutationRef<A, R> を返します。

  • mutate(...args): Promise<R | undefined> — 拒否されることはありません。エラーは error シグナルに渡されます
  • status: Signal<CachedMutationStatus> — 'idle'|'pending'|'success'|'error'
  • isPending: Signal<boolean>
  • error: Signal<unknown>
  • data: Signal<R | undefined>
  • reset(): void

引数が void の場合:A = void のときは、引数なしで mutation.mutate() を呼び出します。

anyLoading(...signals: Signal<boolean>[]): Signal<boolean>

ローディングシグナルをまとめます。computed(() => signals.some(s => s()))。インジェクションコンテキストは必要ありません。

withDevtools(config?)

provideZiflux() 用の機能関数です。CacheRegistry とコンソールログを有効にします。

provideZiflux(config, withDevtools({ logOperations: true }))
ZifluxDevtoolsComponent

スタンドアロンコンポーネントです。セレクター:<ziflux-devtools />。isDevMode() により、本番環境では自動的に非表示になります。

SWR のライフサイクル

1. Cold cache → loader fires → status: 'loading' → isInitialLoading: true
2. Data arrives → cache.set() → status: 'resolved' → value has data
3. Navigate away → DataCache persists (root-scoped)
4. Navigate back → cache.get() returns fresh → NO loader call → instant render
5. Time passes → entry becomes stale
6. Next read → stale data shown immediately → background refetch starts
   → status: 'reloading' → isStale: true → isInitialLoading: false
7. Fresh data arrives → value updates → isStale: false

重要な UX 上のポイントは、isInitialLoading がスピナーを制御すること(キャッシュが空の場合のみ)です。isStale は状態を示すだけです。古いデータも引き続き表示されるため、ユーザーはすぐにコンテンツを見ることができます。

キャッシュキーの設計

キーは JSON.stringify() でシリアル化される string[] です。invalidate() のプレフィックスマッチングでは JSON.stringify(prefix).slice(0, -1) を使用します。

// Good: hierarchical keys
['orders', 'list']
['orders', 'details', orderId]
['orders', 'list', JSON.stringify(filters)]

// invalidate(['orders']) → invalidates ALL order-related entries
// invalidate(['orders', 'list']) → invalidates only the list

注意点: invalidate(['order']) は ['orders'] に一致しません。JSON のプレフィックス ["order" は ["orders" に一致しないためです。誤ったキャッシュ無効化を防ぐため、これは仕様です。

空のプレフィックス invalidate([]) は何もしません。 キャッシュを完全に消去するには cache.clear() を使用してください。

主なアンチパターン

  1. インジェクションコンテキスト外での DataCache → NG0203。クラスフィールド、コンストラクター、または runInInjectionContext 内で使用する必要があります。
  2. ルートスコープのサービスでの DataCache → ナビゲーション時にキャッシュが破棄され、SWR が機能しなくなります。必ず providedIn: 'root' を指定してください。
  3. staleTime > expireTime → コンストラクターで例外が発生します。
  4. invalidate() が削除すると期待する → stale としてマークされます。get() は引き続き { data, fresh: false } を返します。
  5. 全件削除に invalidate([]) を使う → 何も起こりません。clear() を使用してください。
  6. ref.value.set() → value は読み取り専用の Signal であり、WritableSignal ではありません。ref.set() または ref.update() を使用してください。
  7. await mutate() の後に成功を確認する → mutate() は拒否されません。mutation.error() シグナルを確認するか、戻り値を取得してください(undefined はエラーを示します)。
  8. コンポーネントで API サービスを直接インジェクトする → ドメインパターンに反します。コンポーネント → ストア → API の順にしてください。
  9. 状態管理に Subjects/Observables を使う → 禁止されています。Signals のみを使用してください。
  10. params: () => undefined を忘れると一時停止する → リソースは 'idle' のままとなり、ローダーは実行されません。

楽観的更新

コンテキストを使用した onMutate → onError パターン:

readonly deleteItem = cachedMutation<string, void, Item[]>({
  mutationFn: (id) => this.#api.deleteItem(id),
  cache: this.#api.cache,
  invalidateKeys: () => [['items']],
  onMutate: (id) => {
    const previous = this.items.value()!;
    this.items.update(items => items!.filter(i => i.id !== id));
    return previous;  // context for rollback
  },
  onError: (_err, _id, previous) => {
    if (previous) this.items.set(previous);  // rollback
  },
});

同時ミューテーションのセマンティクス

完了順ではなく、呼び出し順で最新のものが優先されます。2 つの mutate() 呼び出しが重複した場合:

  • どちらも mutationFn を実行します
  • 成功時にはどちらもキャッシュを無効化します(サーバーの状態が変化したため)
  • 最後に呼び出されたものだけがシグナルを更新し、onSuccess を実行します
  • 先に呼び出されたものの結果は、シグナルから通知なく破棄されます

設定の優先順位

コンストラクター引数 > provideZiflux() のグローバル設定 > ハードコードされたデフォルト値(staleTime:30s、expireTime:5min)。

参考資料

詳しい情報については、以下の参考ファイルをお読みください。

  • references/api-reference.md — 型シグネチャ全体、すべての設定オプション、DataCacheOptions の検証ルール、RetryConfig の詳細、CacheInspection の構造、またはエッジケースの動作を確認する場合にお読みください。
  • references/patterns.md — ziflux を使った新機能の実装、楽観的更新の設定、キャッシュキー階層の設計、ポーリング/リトライの設定、または provideZiflux のセットアップを行う場合にお読みください。
  • references/review-and-debug.md — ziflux を使ったコードのレビュー、キャッシュの問題(古いデータ、無効化の欠落、NG0203 エラー)のデバッグ、ziflux コードのテスト作成、または ziflux の使用箇所の整理を行う場合にお読みください。

コメント