PageSourceSearch

https://pressthink.org/j/rosen-archive/frontend/services/idbCache.js?v=3.8.36

js pressthink.org collected 2026-10-02 04:27:01 UTC 5,204 bytes, 137 lines download raw bytes

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.