type CacheEntry = { cachedAt: string; data: T; etag?: string; }; type CacheStore = { version: 1; entries: Record>; }; const STORAGE_KEY = "walkup.offlineCache:v1"; function safeLocalStorage(): Storage | null { if (typeof window === "undefined") { return null; } try { const { localStorage } = window; const probeKey = "__walkup_cache_probe__"; localStorage.setItem(probeKey, "1"); localStorage.removeItem(probeKey); return localStorage; } catch { return null; } } function readStore(): CacheStore { const storage = safeLocalStorage(); if (!storage) { return { version: 1, entries: {} }; } const raw = storage.getItem(STORAGE_KEY); if (!raw) { return { version: 1, entries: {} }; } try { const parsed = JSON.parse(raw) as Partial; if (parsed.version !== 1 || !parsed.entries || typeof parsed.entries !== "object") { return { version: 1, entries: {} }; } return { version: 1, entries: parsed.entries as Record>, }; } catch { return { version: 1, entries: {} }; } } function writeStore(store: CacheStore): void { const storage = safeLocalStorage(); if (!storage) { return; } try { storage.setItem(STORAGE_KEY, JSON.stringify(store)); } catch { // Ignore quota and storage errors. The app can still operate online. } } function cacheKeyFromParts(parts: readonly unknown[]): string { return JSON.stringify(parts); } export function readCachedValue(parts: readonly unknown[]): CacheEntry | null { const store = readStore(); const entry = store.entries[cacheKeyFromParts(parts)]; return entry ? (entry as CacheEntry) : null; } export function writeCachedValue(parts: readonly unknown[], data: T, etag?: string): void { const store = readStore(); store.entries[cacheKeyFromParts(parts)] = { cachedAt: new Date().toISOString(), data, etag, }; writeStore(store); } export function clearOfflineCache(): void { const storage = safeLocalStorage(); if (!storage) { return; } storage.removeItem(STORAGE_KEY); } async function readResponseText(response: Response): Promise { try { return await response.text(); } catch { return ""; } } export async function cachedJsonRequest( parts: readonly unknown[], url: string, init?: RequestInit, options?: { onUnauthorized?: () => T; }, ): Promise { const cached = readCachedValue(parts); const headers = new Headers(init?.headers); if (cached?.etag) { headers.set("If-None-Match", cached.etag); } try { const response = await fetch(url, { ...init, headers, cache: "no-cache", credentials: "include", }); if (response.status === 304) { if (!cached) { throw new Error("Cache validation returned 304 without a cached response."); } return cached.data; } if (!response.ok) { if ((response.status === 401 || response.status === 403) && options?.onUnauthorized) { return options.onUnauthorized(); } throw new Error((await readResponseText(response)) || `Request failed: ${response.status}`); } const data = (await response.json()) as T; writeCachedValue(parts, data, response.headers.get("etag") ?? undefined); return data; } catch (error) { if (cached && error instanceof TypeError) { return cached.data; } throw error; } }