hoangsonww / hoangsonww/GhostCache-API-Cache
Stale-While-Revalidate (SWR) + Tag-Based Invalidation
- Dominant language
- TypeScript
- Stars
- 11
- Forks
- 7
- PR merge metrics
- No merged PRs in 30d
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
1. Serve stale entries instantly if within a configurable **stale window** while kicking off a background refresh.
2. Expose a **tag API** to associate one or more tags with a cache entry and invalidate by tag.
3. Keep existing API backward-compatible; new behavior is opt-in.
4. Work with both `fetch` interception and registered Axios instances.
5. 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
```ts
// 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;
export function getCache(
key: string,
opts?: { swr?: boolean } // when true, returns stale if available & schedules refresh
): Promise;
export function invalidateTags(tags: string | string[]): Promise; // <-- 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
```ts
// 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.enabled` is false globally, per-request `ghostCache.swr === true` still enables SWR for that call.
## Behavior Details
* **Cache states**
* **Fresh** (`now < createdAt + ttl`): return cached response; no refresh.
* **Stale-eligible** (`now < createdAt + staleTtl` and `swr` enabled): return cached response immediately; trigger **background revalidation**; on success, cache new value and emit `revalidate_success`.
* **Expired**: perform network request, cache, return result.
* **Deduping**: if multiple identical requests arrive while a revalidation is in flight, they should **share** the same network promise (no thundering herd).
* **Tags**: when `setCache` or an intercepted request completes, store any provided `tags`. `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:
```ts
interface IStorageAdapter {
// existing get/set/delete/clear...
// Optional for performance; fallback uses full scan if not implemented.
addTags?(key: string, tags: string[]): Promise;
deleteByTags?(tags: string[]): Promise;
}
```
* **In-memory**: maintain a `Map>` 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: key`), and on invalidate use `SMEMBERS` then `DEL`/`UNLINK`.
Adapters that don’t override `deleteByTags` fall back to scanning keys (OK for small caches).
## Examples
```ts
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 entries
```
## Acceptance Criteria
* [ ] Opt-in SWR works for both `fetch` and 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.
* [ ] Backward compatibility: existing APIs unaffected when `swr.enabled` is 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.
Contributor guide
Assessment
This issue has not been assessed yet.