import { OrderItem } from './types'; /** * Tiny in-memory + localStorage cache for the three list scopes * (`assigned`, `collection_pool`, `problem`). * * Why this exists * --------------- * The driver app's three list pages each mount → fetch → render. * That made every bottom-nav tab click show a loading skeleton * before any data appeared, even when the user had just been * looking at that exact list two seconds ago. Perceived * "sluggishness on PC tab switching" was almost entirely the * skeleton flash, not a slow API. * * Strategy: stale-while-revalidate at the React level. * 1. The page reads `getCached(scope)` synchronously on mount * and renders immediately. No loading spinner if we have * anything cached. * 2. The page still fires the real fetch in `useEffect`. When * it resolves, we `setCached(scope, items)` and the page * re-renders with fresh data. * 3. On the next tab switch, the new page reads the (now * fresh) cache and renders without a skeleton. * * Backing store * ------------- * Two layers, in order: * - Module-level `Map` for synchronous reads on the same tab * session (instant). * - `localStorage` so the cache survives a hard refresh and a * tab close+reopen. * * Cache shape * ----------- * One key per scope. Stored as `{ items, ts }` where `ts` is the * fetch time (ms since epoch). The `ts` lets callers decide to * force-refresh if the data is stale (we currently don't — the * in-flight re-fetch handles that). * * Limits / non-goals * ------------------ * - NOT a TTL cache. Stale data is fine because we always * re-fetch in the background. * - NOT shared across drivers. localStorage is per-origin per- * browser, which on a shared cab tablet would mean driver A * sees driver B's cached list until the re-fetch lands. * For the single-driver-per-device PWA this is fine. * - NOT encrypted. Items include PO numbers and quantities — * same data the app already puts in localStorage via auth, * so we're not expanding the threat surface. */ const STORAGE_KEY = '1driver_items_cache_v1'; const ALL_SCOPES = ['assigned', 'collection_pool', 'problem'] as const; export type ItemScope = typeof ALL_SCOPES[number]; type CacheEntry = { items: OrderItem[]; ts: number }; // Module-level map is the hot path — synchronous, free, lives // only as long as the page is loaded. const memory = new Map(); function readStorage(): Record { if (typeof window === 'undefined') return {}; try { const raw = window.localStorage.getItem(STORAGE_KEY); if (!raw) return {}; return JSON.parse(raw) as Record; } catch { return {}; } } function writeStorage(data: Record): void { if (typeof window === 'undefined') return; try { window.localStorage.setItem(STORAGE_KEY, JSON.stringify(data)); } catch { /* quota / private mode — fine, memory layer still works */ } } function hydrateMemoryFromStorage(): void { const data = readStorage(); for (const scope of ALL_SCOPES) { const entry = data[scope]; if (entry) memory.set(scope, entry); } } // One-time per page load — pulls whatever localStorage had into // the module-level map so subsequent reads are sync. let hydrated = false; function ensureHydrated(): void { if (hydrated) return; hydrateMemoryFromStorage(); hydrated = true; } /** Synchronous read. Returns null if nothing cached for this scope. */ export function getCached(scope: ItemScope): OrderItem[] | null { ensureHydrated(); const entry = memory.get(scope); return entry ? entry.items : null; } /** Persist a successful fetch. */ export function setCached(scope: ItemScope, items: OrderItem[]): void { ensureHydrated(); const entry: CacheEntry = { items, ts: Date.now() }; memory.set(scope, entry); // Mirror to localStorage. We re-read-modify-write (rather than // use memory directly) so other scopes survive this call. const data = readStorage(); data[scope] = entry; writeStorage(data); } /** Drop everything — used on logout so the next driver doesn't * briefly see the previous driver's items. */ export function clearCache(): void { memory.clear(); if (typeof window !== 'undefined') { try { window.localStorage.removeItem(STORAGE_KEY); } catch { /* fine */ } } } /** Best-effort timestamp from the last successful fetch, or null. */ export function getCachedAt(scope: ItemScope): number | null { ensureHydrated(); return memory.get(scope)?.ts ?? null; }