skills/ziflux-expert/SKILL.md
Deep expertise on the ziflux Angular library — SWR caching for resource(). Use this skill whenever implementing cachedResource, cachedMutation, DataCache, or provideZiflux in Angular code. Also use when reviewing, debugging, testing, or cleaning up code that uses ziflux, or when the user asks about SWR caching patterns in Angular, data freshness lifecycle, or cache invalidation strategies. Triggers on: ziflux, cachedResource, cachedMutation, DataCache, SWR cache Angular, stale-while-revalidate Angular, cache invalidation Angular, optimistic updates Angular signals.
npx skillsauth add neogenz/ziflux ziflux-expertInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
You are now a ziflux expert. ziflux is an Angular 21+ library that adds SWR (stale-while-revalidate) caching to Angular's resource() API. Zero dependencies. Signal-native. Not a state manager — Angular signals + resource() IS the state layer. ziflux fills exactly one gap: the data lifecycle (fresh → stale → expired).
The API is designed so that any Angular developer can guess it without reading docs. If you know resource(), you know cachedResource().
Every feature follows a strict 3-file architecture. This is non-negotiable:
feature.api.ts — providedIn: 'root', owns the DataCache, exposes HTTP methods:
@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 — route-scoped @Injectable(), wires 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 — injects the store, reads signals in the template:
@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);
}
Hard rules:
DataCache — it reads this.#api.cacheDataCache MUST live in a providedIn: 'root' service (survives navigation)provideZiflux(config?, ...features)Called once in app.config.ts. Sets global defaults.
provideZiflux({ staleTime: 60_000, expireTime: 300_000 }, withDevtools())
DataCacheIn-memory SWR cache. Must be created inside an injection context.
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)Angular resource() with SWR. Must be called inside an injection context.
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),
staleTime: 10_000, // optional per-resource override
retry: { maxRetries: 3, baseDelay: 1000 }, // optional
refetchInterval: 30_000, // optional polling
})
Returns CachedResourceRef<T>:
value: Signal<T | undefined> — SWR-aware: shows stale data during revalidationstatus: Signal<ResourceStatus> — 'idle'|'loading'|'reloading'|'resolved'|'error'|'local'isLoading: Signal<boolean> — true during any fetchisStale: Signal<boolean> — true when showing stale data during background refetchisInitialLoading: Signal<boolean> — true only on cold cache (use this for spinners)error: Signal<unknown>hasValue(): booleanreload(): booleanset(value: T): void — optimistic update, writes through to DataCache (status becomes 'local')update(updater: (T | undefined) => T): void — optimistic update, writes through to DataCachedestroy(): voidcachedMutation<A, R, C>(options)Mutation wrapper. No injection context needed.
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 */ },
})
Returns CachedMutationRef<A, R>:
mutate(...args): Promise<R | undefined> — never rejects, errors go to error signalstatus: Signal<CachedMutationStatus> — 'idle'|'pending'|'success'|'error'isPending: Signal<boolean>error: Signal<unknown>data: Signal<R | undefined>reset(): voidVoid args: when A = void, call mutation.mutate() with no argument.
anyLoading(...signals: Signal<boolean>[]): Signal<boolean>Combines loading signals. computed(() => signals.some(s => s())). No injection context.
withDevtools(config?)Feature function for provideZiflux(). Enables CacheRegistry and console logging.
provideZiflux(config, withDevtools({ logOperations: true }))
ZifluxDevtoolsComponentStandalone component. Selector: <ziflux-devtools />. Auto-hides in production via isDevMode().
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
The key UX insight: isInitialLoading controls spinners (cold cache only). isStale is informational — stale data is still shown, the user sees content immediately.
Keys are string[] serialized via JSON.stringify(). Prefix matching on invalidate() uses 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
Gotcha: invalidate(['order']) does NOT match ['orders'] — the JSON prefix ["order" does not match ["orders". This is by design to prevent accidental cross-invalidation.
Empty prefix invalidate([]) is a no-op. Use cache.clear() for full wipe.
runInInjectionContext.providedIn: 'root'.staleTime > expireTime → constructor throws.invalidate() to delete → it marks stale. get() still returns { data, fresh: false }.invalidate([]) for full wipe → no-op. Use clear().ref.value.set() → value is a read-only Signal, not WritableSignal. Use ref.set() or ref.update().await mutate() then checking success → mutate() never rejects. Check mutation.error() signal or capture return value (undefined = error).params: () => undefined suspends → resource stays 'idle', loader never fires.The onMutate → onError pattern with context:
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
},
});
Latest-wins by call order, not resolution order. If two mutate() calls overlap:
mutationFnonSuccessConstructor arg > provideZiflux() global config > hardcoded defaults (staleTime: 30s, expireTime: 5min).
For deeper information, read these reference files:
references/api-reference.md — Read when you need complete type signatures, all configuration options, DataCacheOptions validation rules, RetryConfig details, CacheInspection shape, or edge-case behaviors.references/patterns.md — Read when implementing a new feature with ziflux, setting up optimistic updates, designing cache key hierarchies, configuring polling/retry, or writing the provideZiflux setup.references/review-and-debug.md — Read when reviewing code that uses ziflux, debugging cache issues (stale data, missing invalidation, NG0203 errors), writing tests for ziflux code, or cleaning up ziflux usage.development
Maintainer-only workflow for handling GitHub Secret Scanning alerts on OpenClaw. Use when Codex needs to triage, redact, clean up, and resolve secret leakage found in issue comments, issue bodies, PR comments, or other GitHub content.
development
Maintainer workflow for OpenClaw releases, prereleases, changelog release notes, and publish validation. Use when Codex needs to prepare or verify stable or beta release steps, align version naming, assemble release notes, check release auth requirements, or validate publish-time commands and artifacts.
development
Run, watch, debug, and extend OpenClaw QA testing with qa-lab and qa-channel. Use when Codex needs to execute the repo-backed QA suite, inspect live QA artifacts, debug failing scenarios, add new QA scenarios, or explain the OpenClaw QA workflow. Prefer the live OpenAI lane with regular openai/gpt-5.4 in fast mode; do not use gpt-5.4-pro or gpt-5.4-mini unless the user explicitly overrides that policy.
development
End-to-end Parallels smoke, upgrade, and rerun workflow for OpenClaw across macOS, Windows, and Linux guests. Use when Codex needs to run, rerun, debug, or interpret VM-based install, onboarding, gateway smoke tests, latest-release-to-main upgrade checks, fresh snapshot retests, or optional Discord roundtrip verification under Parallels.