hoangsonww / hoangsonww/GhostIO-Invisible-Data-Prefetch

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

Open
#3 0 comments 0 reactions 1 assignee Claimed by @hoangsonww View on GitHub
bug documentation enhancement good first issue help wanted question
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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.