hoangsonww / hoangsonww/GhostIO-Invisible-Data-Prefetch
Stale-While-Revalidate Cache + Persistent Storage & Cross-Tab Coordination
- Dominant language
- TypeScript
- Stars
- 13
- Forks
- 8
- PR merge metrics
- No merged PRs in 30d
Description
### 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)
```ts
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
```ts
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;
// Cache management
getMeta(url: string): { insertedAt: number; ttlMs: number; etag?: string; lastModified?: string } | null;
prune(): void; // LRU + expired
clearPersistent(): Promise;
}
```
#### 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
```ts
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”.
---
Contributor guide
Assessment
This issue has not been assessed yet.