-
Notifications
You must be signed in to change notification settings - Fork 20
Description
Summary
Add first-class support for Stale-While-Revalidate (serve cached data instantly while refreshing in the background) and tag-based invalidation (purge groups of related entries at once). This will improve perceived performance and give developers fine-grained control over cache consistency across fetch and Axios.
Motivation
- Instant UX + Freshness: Many apps prefer returning slightly stale data immediately, then updating silently. SWR is a proven pattern for this.
- Simple cache busting: Today you can clear the whole cache, or set/get individual keys. Real apps often need to invalidate sets of keys (e.g., “all
pokemon/*” or “all user profile data”) when a mutation occurs. - Parity across adapters: Support should work for in-memory,
localStorage,sessionStorage, IndexedDB, and Redis adapters via a minimal, portable API.
Goals
- Serve stale entries instantly if within a configurable stale window while kicking off a background refresh.
- Expose a tag API to associate one or more tags with a cache entry and invalidate by tag.
- Keep existing API backward-compatible; new behavior is opt-in.
- Work with both
fetchinterception and registered Axios instances. - Provide deterministic behavior in Node and browser environments.
Non-Goals
- Reactive UI bindings (React hooks, etc.)—app code can subscribe to events we emit.
- Complex query invalidation logic (regex on keys is out of scope for v1; tag sets cover most needs).
Proposed API
// New options on enableGhostCache
type GhostCacheOptions = {
ttl?: number; // existing
persistent?: boolean; // existing
maxEntries?: number; // existing
storage?: StorageOption; // existing
swr?: {
enabled?: boolean; // default false
staleTtl?: number; // ms window during which stale can be served (<= ttl)
revalidateOnFocus?: boolean; // browser only; triggers background refresh on window focus
revalidateOnReconnect?: boolean; // triggers on online event
};
};
// New manual cache APIs
export function setCache(
key: string,
value: any,
opts?: { ttl?: number; tags?: string[] } // <-- NEW tags
): Promise<void>;
export function getCache<T = any>(
key: string,
opts?: { swr?: boolean } // when true, returns stale if available & schedules refresh
): Promise<T | null>;
export function invalidateTags(tags: string | string[]): Promise<void>; // <-- NEW
// Optional: events for UI layers/devtools
export type GhostCacheEvent =
| { type: 'cache_hit'; key: string; stale: boolean }
| { type: 'cache_miss'; key: string }
| { type: 'revalidate_start'; key: string }
| { type: 'revalidate_success'; key: string }
| { type: 'revalidate_error'; key: string; error: unknown };
export function onGhostCacheEvent(listener: (e: GhostCacheEvent) => void): () => void;Request-Level Overrides
// fetch: opt-in via Request init
fetch(url, {
ghostCache: {
swr: true, // enable SWR for this request only
tags: ['pokemon', 'list']
}
});
// axios: opt-in via config
api.get('/pokemon/ditto', {
ghostCache: { swr: true, tags: ['pokemon', 'detail:ditto'] }
});If
swr.enabledis false globally, per-requestghostCache.swr === truestill enables SWR for that call.
Behavior Details
-
Cache states
- Fresh (
now < createdAt + ttl): return cached response; no refresh. - Stale-eligible (
now < createdAt + staleTtlandswrenabled): return cached response immediately; trigger background revalidation; on success, cache new value and emitrevalidate_success. - Expired: perform network request, cache, return result.
- Fresh (
-
Deduping: if multiple identical requests arrive while a revalidation is in flight, they should share the same network promise (no thundering herd).
-
Tags: when
setCacheor an intercepted request completes, store any providedtags.invalidateTags(['pokemon'])removes all entries associated with those tags across adapters.
Storage Adapter Contract Changes
All existing adapters continue to work. To support tags efficiently, add two optional methods:
interface IStorageAdapter {
// existing get/set/delete/clear...
// Optional for performance; fallback uses full scan if not implemented.
addTags?(key: string, tags: string[]): Promise<void>;
deleteByTags?(tags: string[]): Promise<void>;
}- In-memory: maintain a
Map<string, Set<string>>for tag index. - localStorage/sessionStorage: maintain a serialized tag index key (e.g.,
__ghostcache_tag_index__). - IndexedDB: add an index on
tags. - Redis: store per-tag sets (
SADD tag:<name> key), and on invalidate useSMEMBERSthenDEL/UNLINK.
Adapters that don’t override deleteByTags fall back to scanning keys (OK for small caches).
Examples
enableGhostCache({
ttl: 60_000,
swr: { enabled: true, staleTtl: 30_000, revalidateOnFocus: true }
});
// Serve stale instantly, refresh in background
await fetch('https://pokeapi.co/api/v2/pokemon/ditto', {
ghostCache: { swr: true, tags: ['pokemon', 'detail:ditto'] }
});
// After a mutation:
await invalidateTags(['pokemon']); // bust all pokemon-related entriesAcceptance Criteria
-
Opt-in SWR works for both
fetchand registered Axios instances. -
Background revalidation returns stale data immediately and updates cache on success.
-
Request deduplication prevents duplicate network calls during revalidation.
-
Tagging works via:
- per-request
ghostCache.tags - manual
setCache(..., { tags }) -
invalidateTags([...])purges matching entries across adapters.
- per-request
-
Backward compatibility: existing APIs unaffected when
swr.enabledis not used. -
Comprehensive tests:
- In-memory adapter (SWR + tags)
- Browser-like storage (localStorage/sessionStorage) via JSDOM/mocks
- IndexedDB (using fake-indexeddb)
- Redis adapter (integration test behind env var; otherwise mocked)
- Deduping under concurrency
- Event emission on hits/misses/revalidate
-
Updated README with examples and adapter notes.
Potential Follow-Ups (separate issues)
- DevTools overlay (small panel that logs cache hits/misses in dev).
- Cache hydration/serialization for SSR frameworks.
- ETag/Last-Modified awareness: use conditional requests during revalidation.
Additional Context
This feature aligns with modern caching patterns (e.g., SWR/RTK Query) and complements GhostCache’s multi-adapter design. It should significantly improve perceived performance while offering safe, explicit invalidation.