Skip to content

Stale-While-Revalidate Cache + Persistent Storage & Cross-Tab Coordination #3

@hoangsonww

Description

@hoangsonww

Summary

Add a first-class SWR (stale-while-revalidate) cache layer to GhostIO with optional persistent storage (IndexedDB/localForage) and cross-tab coordination (BroadcastChannel). This keeps prefetched data instantly available, survives reloads, avoids duplicate network work across tabs, and revalidates in the background to keep responses fresh.


Motivation

  • Instant UX: Serve prefetched data synchronously from cache (no spinners), then refresh in the background.
  • Persistence: Current in-memory cache is lost on reload/navigation; keeping hot endpoints warm across sessions boosts hit-rate.
  • Multi-tab efficiency: Today each tab may re-prefetch the same endpoints; coordinating avoids waste and rate limits.
  • Freshness guarantees: Standardize TTL/ETag handling so devs don’t hand-roll invalidation per endpoint.

Proposed Design

1) New Options (constructor)

type GhostIOConfig = {
  // existing...
  maxCacheSize?: number;
  concurrencyLimit?: number;
  prefetchOnHover?: boolean;
  prefetchOnScroll?: boolean;
  idlePrefetchDelay?: number;

  // new
  persistence?: 'memory' | 'indexeddb';       // default 'memory'
  swr?: boolean;                               // default true
  defaultTTLms?: number;                       // e.g., 5 * 60_000
  revalidateOnFocus?: boolean;                 // background revalidate when tab regains focus
  crossTab?: boolean;                          // broadcast cache hits/misses, locks
  respectValidators?: boolean;                 // use If-None-Match / If-Modified-Since
};

2) API Additions

class GhostIO {
  get(url: string): { data: any; stale: boolean } | null; // indicates staleness
  set(url: string, data: any, meta?: { ttlMs?: number; etag?: string; lastModified?: string }): void;

  // Forces background revalidation; returns fresh value when done.
  revalidate(url: string, opts?: { priority?: 'low'|'normal'|'high' }): Promise<any>;

  // Cache management
  getMeta(url: string): { insertedAt: number; ttlMs: number; etag?: string; lastModified?: string } | null;
  prune(): void;             // LRU + expired
  clearPersistent(): Promise<void>;
}

3) Behavior

  • SWR Flow:

    1. get(url) returns cached value immediately (even if expired → marked stale: true).
    2. In parallel, revalidate(url) runs (deduped by an internal lock).
    3. If server returns 304 Not Modified (via If-None-Match or If-Modified-Since), extend TTL without replacing data.
  • Persistence:

    • Store {data, meta} in IndexedDB with LRU index.
    • Hydrate into memory on first access (no full DB scan).
  • Cross-tab:

    • Use BroadcastChannel('ghost-io') to share: cache set events, in-flight locks, and eviction notices.
    • A tab starting a revalidate broadcasts a lock; others avoid duplicate fetches.
  • Revalidate on Focus:

    • When tab regains focus, revalidate any entries whose TTL expired.
  • Axios/Fetch integration:

    • Existing registerAxios can inject validators (If-None-Match, etc.) when meta exists.
    • Add registerFetch(fetchImpl?: typeof fetch) for native Fetch users (optional).

4) Example Usage

import { GhostIO } from 'ghost-io';

const ghost = new GhostIO({
  persistence: 'indexeddb',
  swr: true,
  defaultTTLms: 300_000,         // 5 minutes
  crossTab: true,
  revalidateOnFocus: true,
  respectValidators: true,
});

// Prefetch somewhere (hover/idle) or manually:
await ghost.prefetch('/api/dashboard');

// Later in UI code:
const cached = ghost.get('/api/dashboard');
if (cached) {
  render(cached.data);                 // instantaneous paint
  if (cached.stale) ghost.revalidate('/api/dashboard');   // fire-and-forget
} else {
  // fallback: fetch normally or show skeleton
}

5) Storage & Eviction

  • LRU + TTL: IndexedDB bucket keyed by URL; secondary index for lastAccessed for O(log n) pruning.
  • Quota handling: Detect QuotaExceededError; fallback to memory mode and log a warning hook.
  • Serialization: Structured clone (no JSON precision loss).

6) Edge Cases & Safety

  • Error policy: If revalidate fails, keep stale data and backoff (exponential, capped).
  • PII/secure endpoints: Opt-out per request: ghost.prefetch(url, { persist: false }).
  • Cache keys: Support a stable key function: cacheKey(url, init) to include query/body if needed.

Performance/Success Criteria

  • 70% cache hit-rate on hot endpoints after reloads (measured across sessions).

  • Cross-tab lock dedupes ≥90% of overlapping revalidations.
  • Revalidate CPU/network overhead bounded: no more than 1 concurrent revalidate per URL across tabs.
  • Cold page load renders with cached data in <16ms on mid-tier devices.

Testing

  • Unit: SWR semantics, TTL expiry, ETag/Last-Modified validator logic.
  • Integration: Multi-tab BroadcastChannel tests (Playwright).
  • Persistence: IndexedDB quota & fallback, LRU eviction correctness.
  • Axios/Fetch: 304 handling and header injection.

Docs

  • New “Caching & SWR” section with diagrams (cache states, cross-tab flow).
  • Recipes: “Realtime dashboards”, “Offline-leaning pages”, “Protecting PII endpoints”.

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingdocumentationImprovements or additions to documentationenhancementNew feature or requestgood first issueGood for newcomershelp wantedExtra attention is neededquestionFurther information is requested

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions