1/** 2 * idbCache.js â a small, fail-safe IndexedDB key/value cache for large 3 * parsed payloads (#275). 4 * 5 * archiveService caches the ~13 MB archive-core.json. The Web Storage cache 6 * it used pays JSON.parse(~13 MB) on every repeat visit (200-500 ms on 7 * phones) and, because the blob exceeds localStorage's ~5 MB cap, it lands in 8 * sessionStorage â which is cleared on tab close, so a returning visitor in a 9 * fresh tab never gets a hit. IndexedDB structured-clones the object graph on 10 * read (no string parse) and persists across sessions with multi-hundred-MB 11 * origin quotas. 12 * 13 * Every operation is fail-safe and resolves to a sentinel (null on read, 14 * false on write/clear) instead of throwing: 15 * - IndexedDB is absent, or throws on open, in some private modes and under 16 * Firefox strict tracking protection. 17 * - idb-keyval loads from the esm.sh import map at runtime; a CDN hiccup 18 * must degrade to the Web Storage cache, not break the archive load. 19 * The caller treats null/false as "not cached" and falls back. A throw from 20 * this module must never reach archiveService â its top comment notes that a 21 * module-load throw would take the whole app down with it. 22 * 23 * Offline note: the dynamic idb-keyval import is cross-origin (esm.sh) and is 24 * not service-worker-precached, so an offline read returns null and the load 25 * falls through to the service worker's stale-while-revalidate data cache. 26 * The win this module targets â skipping the parse on online repeat visits â 27 * is unaffected. 28 */ 29 30import { raceTimeout } from '../utils/raceTimeout.js?v=3.8.36'; 31 32// A dedicated database/store so idbClear() only ever wipes this cache, never 33// some other consumer's idb-keyval default store. 34const DB_NAME = 'jrda-archive-cache'; 35const STORE_NAME = 'kv'; 36 37// idb-keyval is imported lazily and dynamically: this keeps it off the app's 38// boot path (loaded on first cache access, not at module eval) and lets a CDN 39// failure fall through to the catch below instead of aborting module load. 40// 41// The import is cross-origin (esm.sh) and not service-worker-precached, so a 42// reachable-but-stalled CDN could hang every caller that awaits it â 43// fetchCoreData awaits idbGet() before it ever tries same-origin 44// archive-core.json or the Web Storage cache. A plain catch only handles a 45// reject, not a stall, so the import is raced against a timeout: on a
45stall the 46// race rejects fast and the caller degrades to the non-IndexedDB path (#392). 47const IMPORT_TIMEOUT_MS = 3000; 48 49/** 50 * Race a promise-returning thunk against a timeout. Mirrors the thunk if it 51 * settles within `ms`; otherwise rejects with a timeout error. Defers the 52 * thunk to a microtask so a synchronous throw becomes a rejection, then races 53 * it in reject-mode through the shared raceTimeout helper. A settle that 54 * arrives after the timeout is ignored (raceTimeout settles once). Exported 55 * for tests. 56 * @param {() => Promise<T>} thunk 57 * @param {number} ms 58 * @returns {Promise<T>} 59 * @template T 60 */ 61export const withTimeout = (thunk, ms) => 62 raceTimeout(Promise.resolve().then(thunk), ms, { rejectOnTimeout: true }); 63 64let storePromise = null; 65const getKeyval = () => { 66 if (!storePromise) { 67 storePromise = withTimeout(() => import('idb-keyval'), IMPORT_TIMEOUT_MS) 68 .then((kv) => ({ kv, store: kv.createStore(DB_NAME, STORE_NAME) })) 69 .catch((err) => { 70 storePromise = null; // allow a later call to retry the import 71 throw err; 72 }); 73 } 74 return storePromise; 75}; 76 77const hasIndexedDB = () => { 78 try { 79 return typeof indexedDB !== 'undefined' && indexedDB !== null; 80 } catch { 81 // Accessing `indexedDB` can itself throw under some storage policies. 82 return false; 83 } 84}; 85 86/** 87 * Read a value by key. Returns the structured-cloned value, or null on a 88 * miss or any failure (IndexedDB blocked, idb-keyval import failed, etc.). 89 * @param {string} key 90 * @returns {Promise<unknown|null>} 91 */ 92export const idbGet = async (key) => { 93 if (!hasIndexedDB()) return null; 94 try { 95 const { kv, store } = await getKeyval(); 96 const value = await kv.get(key, store); 97 return value === undefined ? null : value; 98 } catch { 99 return null; 100 } 101}; 102 103/**
104 * Write a value by key. Returns true on success, false if the value was not 105 * persisted for any reason (so the caller can fall back to another cache). 106 * @param {string} key 107 * @param {unknown} value 108 * @returns {Promise<boolean>} 109 */ 110export const idbSet = async (key, value) => { 111 if (!hasIndexedDB()) return false; 112 try { 113 const { kv, store } = await getKeyval(); 114 await kv.set(key, value, store); 115 return true; 116 } catch { 117 return false; 118 } 119}; 120 121/** 122 * Remove every entry from this cache's store. Returns true on success, false 123 * if the store could not be cleared. Used by archiveService's deploy-version 124 * invalidation so a new deploy drops stale blobs and IndexedDB doesn't grow 125 * one orphaned copy per past version. 126 * @returns {Promise<boolean>} 127 */ 128export const idbClear = async () => { 129 if (!hasIndexedDB()) return false; 130 try { 131 const { kv, store } = await getKeyval(); 132 await kv.clear(store); 133 return true; 134 } catch { 135 return false; 136 } 137};
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.