- add optional collection_photo field to OrderItem type and backend support for pickup photo uploads - implement in-memory + localStorage cache for order items to eliminate tab-switch loading skeletons - update service worker to use stale-while-revalidate for navigation requests to reduce perceived lag - fix LocationTracker to only activate when logged in and hoist to root layout for persistent tracking - adjust InstallPrompt positioning to avoid overlapping bottom navigation - throttle map cluster recomputation to reduce unnecessary work during pan/zoom - clear cached items on logout to prevent cross-driver data leakage
138 lines
No EOL
4.5 KiB
TypeScript
138 lines
No EOL
4.5 KiB
TypeScript
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<ItemScope, CacheEntry>();
|
|
|
|
function readStorage(): Record<string, CacheEntry> {
|
|
if (typeof window === 'undefined') return {};
|
|
try {
|
|
const raw = window.localStorage.getItem(STORAGE_KEY);
|
|
if (!raw) return {};
|
|
return JSON.parse(raw) as Record<string, CacheEntry>;
|
|
} catch {
|
|
return {};
|
|
}
|
|
}
|
|
|
|
function writeStorage(data: Record<string, CacheEntry>): 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;
|
|
} |