-
Notifications
You must be signed in to change notification settings - Fork 8
Open
Labels
bugSomething isn't workingSomething isn't workingdocumentationImprovements or additions to documentationImprovements or additions to documentationenhancementNew feature or requestNew feature or requestgood first issueGood for newcomersGood for newcomershelp wantedExtra attention is neededExtra attention is neededquestionFurther information is requestedFurther information is requested
Description
Summary
Level-up GhostCache from “TTL + storage adapters” to a spec-aware HTTP cache with:
- Conditional revalidation via
ETag/If-None-MatchandLast-Modified/If-Modified-Since(RFC-7234), - SWR (serve-stale-while-revalidate) and stale-if-error policies,
- In-flight request coalescing to prevent thundering herds,
- Cross-context invalidation (browser tabs via
BroadcastChannel, Node via Redis pub/sub), and - Tag-based invalidation (“surrogate keys”) for batch purges.
This keeps GhostCache lightweight but makes it production-grade for real APIs.
Motivation
- Honor real HTTP caching semantics to reduce bandwidth and latency while staying consistent with origin freshness rules.
- Serve instant responses even when entries are slightly stale, then refresh in the background.
- Prevent dozens of simultaneous identical requests from stampeding the origin.
- Invalidate coherently across tabs, workers, and servers.
- Purge whole resource groups (e.g.,
user:42,products) at once.
Proposed API
enableGhostCache({
// Existing:
ttl?: number;
persistent?: boolean;
maxEntries?: number;
storage?: "localStorage" | "sessionStorage" | "indexedDB" | "redis" | IStorageAdapter;
// New:
policy?: "ttl" | "http"; // 'http' uses Cache-Control/Expires + validators
swr?: boolean; // serve stale then revalidate in background
staleIfError?: boolean; // if origin fails, serve stale fallback
respectVary?: boolean; // include Vary headers in cache-key normalization
keyFn?: (req: NormalizedRequest) => string; // custom cache key builder
coalesce?: boolean; // in-flight dedupe
broadcast?: boolean; // cross-tab/node invalidations
});
type SetOptions = { ttl?: number; tags?: string[] };
type InvalidateOptions = { key?: string; tags?: string[] };
export function revalidate(url: string, init?: RequestInit): Promise<Response>;
export function invalidate(opts: InvalidateOptions): Promise<void>;
export function setCache(key: string, value: any, opts?: SetOptions): Promise<void>;
export function getCache<T = any>(key: string): Promise<T | null>;Notes
-
policy: "http"usesCache-Control,Expires, and validators (ETag,Last-Modified) when present; otherwise falls back tottl. -
swrwill serve stale (if withinstale-while-revalidatewindow or TTL) and kick off a background revalidation. -
staleIfErrorserves stale data on 5xx/network errors within a grace window. -
broadcast: trueenables:- Browser:
BroadcastChannel("ghostcache")→{ type: "invalidate", key|tags } - Node/Redis: Pub/Sub channel
ghostcache:eventswith the same payload.
- Browser:
Implementation Outline
1) Request normalization & cache key
- Normalize method, URL, sorted query params, relevant headers.
- If origin responds with
Vary, fold those header values into the key whenrespectVaryis on. - Allow
keyFnoverride.
2) Metadata stored with entries
interface StoredEntry {
body: ArrayBuffer | string; // raw or compressed
status: number;
headers: Record<string, string>;
createdAt: number; // ms
maxAge?: number; // parsed from Cache-Control
staleIfErrorSec?: number; // parsed hint (if present) or config default
etag?: string;
lastModified?: string;
tags?: string[];
}3) Conditional revalidation
-
If entry is stale (per HTTP rules or TTL), send conditional request:
- Prefer
If-None-Match: <etag>; elseIf-Modified-Since: <lastModified>.
- Prefer
-
On
304 Not Modified: bump freshness, update headers, keep body. -
On
200: replace entry.
4) SWR & stale-if-error
- If
swrand entry is stale but within grace, return stale immediately, then revalidate in background. - If origin errors and
staleIfErrorapplies, return latest stale entry.
5) In-flight coalescing
- Keep a
Map<cacheKey, Promise<Response>>of active fetches. - Next identical request awaits the same promise; clear when settled.
6) Cross-context invalidation
- On
invalidate({ key, tags }), remove from current store and broadcast. - Browser:
BroadcastChannel. - Redis: simple
publish/subscribeinRedisAdapter.
7) Tag-based invalidation
- Store
tagsper entry; maintain tag→keys index in storage adapter when feasible (for Redis easily; for browser storages keep a compact index key).
8) Compression (optional, storage-aware)
- Optionally compress large bodies for
localStorage/indexedDBto fit quotas (e.g., browser-nativeCompressionStreamwhen available; Node:brotli).
Axios & fetch integration
- fetch: wrap global fetch; before-hit, check entry; after-response, store entry & validators.
- Axios: request interceptor to check cache; response interceptor to store;
If-None-Match/If-Modified-Sinceheaders on revalidate path.
Edge Cases & Rules
- Do not cache
POST,PUT, etc. by default (opt-in viakeyFnif users want). - Respect
Cache-Control: no-store(never cache). - Respect
Cache-Control: privatein browser contexts; allow caching in app code at your own risk—document clearly. Vary: *→ treat as uncacheable.- Binary bodies: store as
ArrayBufferand reconstructResponse.
Testing Plan (Jest)
-
Use
whatwg-fetchmock /msw(or Nodenock) to simulate:- Initial 200 with
ETag, subsequent 304 path. Last-Modifiedfallback.Cache-Control: max-age,stale-while-revalidate,no-store,Vary.- Network error +
staleIfErrorfallback. - Coalescing: 10 parallel requests result in a single origin hit.
- Cross-tab invalidation (BroadcastChannel shim).
- Redis pub/sub invalidation in Node.
- Initial 200 with
Docs & Examples
- Guide: “Choosing
ttlvshttppolicy” with flowcharts. - Recipe: SWR in React (tiny
useGhosthook example). - Recipe: Tag invalidation after a mutation (
invalidate({ tags: ['user:42'] })). - Recipe: Multi-tab coherence demo (open two tabs, show instant purge).
Backward Compatibility
- Defaults preserve today’s behavior (
policy: 'ttl',swr: false,coalesce: false, no broadcasting). - New features gated by options.
Tasks
Core
- Parse/serialize metadata; cache-key normalization with optional
Vary. - Conditional requests & 304 handling.
- SWR + stale-if-error flow.
- In-flight coalescing map.
- Tag index & invalidation API.
Adapters
- Browser storage: tag index keys + optional compression.
- Redis adapter: pub/sub channel + tag sets (e.g.,
SADD ghost:tag:products <key>).
Cross-Context
- BroadcastChannel integration + graceful fallback (no-op if unsupported).
- Redis pub/sub wire-up in
RedisAdapter.
Axios/Fetch
- Interceptors/patch with conditional headers.
- Unit tests for both paths.
DX
- Docs + migration notes.
- Examples (
examples/react-swr,examples/node-redis). - Add flags to
jesttests in CI.
Nice-to-have (post-merge)
- CLI:
ghost-cache ls/inspect/purge --tag products. - Metrics hooks:
onCacheHit,onRevalidate,onPurgefor plugging into logs.
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
bugSomething isn't workingSomething isn't workingdocumentationImprovements or additions to documentationImprovements or additions to documentationenhancementNew feature or requestNew feature or requestgood first issueGood for newcomersGood for newcomershelp wantedExtra attention is neededExtra attention is neededquestionFurther information is requestedFurther information is requested