{
  "version": 3,
  "sources": ["datacache.js"],
  "sourcesContent": ["// ─────────────────────────────────────────────────────────────────────────\n// Client-side stale-while-revalidate cache for account API responses.\n//\n// Every account page is a full-page document, so navigating between audits\n// refetches hundreds of KB cold each time. This caches responses in\n// sessionStorage (per-tab, auto-cleared on tab close) so a repeat visit paints\n// instantly. The data is private per-user — clearDataCache() runs on sign-out.\n//\n//   const audit = await swr(`audit:${id}`, () => apiPost(\"/api/audit\", { id }),\n//                           { ttl: 300000, onUpdate: render });\n//\n// Within ttl:  returns the cached value with NO network call (instant).\n// Past ttl:    returns the stale value immediately, revalidates in the\n//              background, and calls onUpdate(fresh) only if the payload changed.\n//              A page that passes no onUpdate still gets the fresh copy cached,\n//              but only on its NEXT load — which is why every page passes one\n//              (see fresh-hint.js for the \"re-render or offer a refresh\" rule).\n// Cold:        awaits the fetch once, caches it, returns it.\n//\n// \"Changed\" is decided on the serialized text, which write() produces anyway:\n// the old copy's text is the slice of the stored record we already read, so a\n// multi-MB payload costs one JSON.stringify, not three, and a string compare.\n// Signed storage links (a fresh 5-minute token on every /api/audit read) are\n// ignored by that compare — a new token on the same document is not new data,\n// and counting it would offer a refresh on every visit to a delivered report.\n//\n// Degrades safely: if sessionStorage is unavailable or full, swr() just falls\n// through to the fetcher (same behaviour as no cache). A write that cannot fit\n// drops the key's older copy rather than leaving it to be served again.\n// ─────────────────────────────────────────────────────────────────────────\nconst PREFIX = \"rl.cache.\";\nconst PREFS_PREFIX = \"rl.prefs.\"; // owned by preferences.js; cleared together on sign-out\nconst k = (key) => PREFIX + key;\n\nfunction read(key) {\n  try {\n    const raw = sessionStorage.getItem(k(key));\n    if (!raw) return null;\n    const e = JSON.parse(raw);\n    if (!e || typeof e.t !== \"number\") return null;\n    return { t: e.t, v: e.v, raw };\n  } catch { return null; }\n}\n\n// The record is written by hand around the value's own text so the caller can\n// keep that text for the compare. Byte-identical to JSON.stringify({ t, v }).\nfunction write(key, v, text = serialize(v)) {\n  const rec = text === undefined ? `{\"t\":${Date.now()}}` : `{\"t\":${Date.now()},\"v\":${text}}`;\n  try { sessionStorage.setItem(k(key), rec); }\n  catch {\n    try { evictOldest(); sessionStorage.setItem(k(key), rec); }\n    catch {\n      // Still no room (one payload bigger than what is left): never serve the\n      // older copy of THIS key again — the next read goes to the network.\n      try { sessionStorage.removeItem(k(key)); } catch {}\n    }\n  }\n}\n\n// Drop the oldest entry to make room when sessionStorage hits its quota.\nfunction evictOldest() {\n  let oldestKey = null, oldestT = Infinity;\n  for (let i = 0; i < sessionStorage.length; i++) {\n    const sk = sessionStorage.key(i);\n    if (!sk || !sk.startsWith(PREFIX)) continue;\n    try { const t = JSON.parse(sessionStorage.getItem(sk)).t; if (t < oldestT) { oldestT = t; oldestKey = sk; } } catch {}\n  }\n  if (oldestKey) sessionStorage.removeItem(oldestKey);\n}\n\nfunction serialize(v) { try { return JSON.stringify(v); } catch { return undefined; } }\n\n// The value's text inside a stored record, without parsing it again.\nconst REC_HEAD = /^\\{\"t\":-?[0-9][0-9.eE+-]*,\"v\":/;\nexport function valueText(raw) {\n  if (typeof raw !== \"string\") return undefined;\n  const m = REC_HEAD.exec(raw);\n  return m && raw.endsWith(\"}\") ? raw.slice(m[0].length, -1) : undefined;\n}\n\n// A Supabase signed storage link: …/object/sign/<bucket>/<path>?token=<jwt>.\n// Minted per request, so two reads of an unchanged document differ here only.\nconst SIGNED_TOKEN = /(\\/object\\/sign\\/[^\"?\\s]*\\?token=)[^\"&\\\\s]*/g;\n\n// Whether two serialized payloads carry the same data. Equal text is the fast\n// path (a length check then a memcmp); only text that differs is scanned for\n// signed-link tokens, and only when it has any.\nexport function sameData(a, b) {\n  if (a === b) return true;\n  if (typeof a !== \"string\" || typeof b !== \"string\") return false;\n  if (a.indexOf(\"/object/sign/\") < 0 || b.indexOf(\"/object/sign/\") < 0) return false;\n  return a.replace(SIGNED_TOKEN, \"$1\") === b.replace(SIGNED_TOKEN, \"$1\");\n}\n\n// Synchronous peek (no network) — undefined if absent.\nexport function cachePeek(key) { const e = read(key); return e ? e.v : undefined; }\n\n// Write/overwrite a cache entry (e.g. after a local mutation keeps data current).\nexport function cacheSet(key, v) { supersede(key); write(key, v); }\n\n// Drop one entry — call after a mutation that makes the cached copy wrong.\nexport function invalidate(key) { supersede(key); try { sessionStorage.removeItem(k(key)); } catch {} }\n\n// Wipe all cached data AND the preferences mirror. Called on sign-out so a shared\n// computer never shows the previous user's audits or saved view state.\nexport function clearDataCache() {\n  try {\n    for (let i = sessionStorage.length - 1; i >= 0; i--) {\n      const sk = sessionStorage.key(i);\n      if (sk && sk.startsWith(PREFIX)) sessionStorage.removeItem(sk);\n    }\n  } catch {}\n  try {\n    for (let i = localStorage.length - 1; i >= 0; i--) {\n      const lk = localStorage.key(i);\n      if (lk && lk.startsWith(PREFS_PREFIX)) localStorage.removeItem(lk);\n    }\n  } catch {}\n}\n\n// The shared crawl payload every deliverable viewer reads (`audit:<id>`).\n//\n// This was 5 minutes, which is the right number for data that sits still and the\n// wrong one for a crawl mid-build: a viewer opened while a stage was landing\n// cached the BEFORE picture, and every reload inside the window re-served it\n// without touching the network — so a section that had already been delivered\n// (and the newer layout that renders it) simply didn't appear. Signing out clears\n// this cache, which is why signing out and back in \"fixed\" it and a refresh\n// didn't. Short enough that any page load after a change is honest; long enough\n// that hub → viewer → back still costs nothing.\nexport const AUDIT_TTL = 20000;\n\n// One network read per key at a time within a page: a prefetch and the page's\n// own read of the same key share the request instead of racing two.\n//\n// A read that was already in flight when the page invalidated or overwrote the\n// key (a mutation) is SUPERSEDED: it may have left before the change, so it\n// neither writes the cache nor reports an update, and the next read starts anew\n// rather than joining it.\nconst INFLIGHT = new Map();\nconst GEN = new Map();\nconst genOf = (key) => GEN.get(key) || 0;\nfunction supersede(key) { GEN.set(key, genOf(key) + 1); INFLIGHT.delete(key); }\nfunction fetchOnce(key, fetcher) {\n  let p = INFLIGHT.get(key);\n  if (!p) {\n    const g = genOf(key);\n    p = Promise.resolve().then(fetcher).then((fresh) => {\n      const text = serialize(fresh);\n      const current = genOf(key) === g;\n      if (current) write(key, fresh, text);\n      return { fresh, text, current };\n    });\n    INFLIGHT.set(key, p);\n    const done = () => { if (INFLIGHT.get(key) === p) INFLIGHT.delete(key); };\n    p.then(done, done);\n  }\n  return p;\n}\n\nexport async function swr(key, fetcher, { ttl = 300000, onUpdate } = {}) {\n  const entry = read(key);\n  if (entry) {\n    if (Date.now() - entry.t <= ttl) return entry.v; // fresh → instant, no network\n    // Stale: serve cached now, revalidate in the background, re-render only on change.\n    fetchOnce(key, fetcher).then(({ fresh, text, current }) => {\n      if (!onUpdate || !current) return;\n      let changed = true;\n      try {\n        const before = valueText(entry.raw);\n        changed = !sameData(text, before === undefined ? serialize(entry.v) : before);\n      } catch {}\n      if (!changed) return;\n      try { onUpdate(fresh); }\n      catch (e) { try { console.warn(`[datacache] onUpdate for ${key} failed:`, e); } catch {} }\n    }).catch(() => {});\n    return entry.v;\n  }\n  const { fresh } = await fetchOnce(key, fetcher); // cold → wait once\n  return fresh;\n}\n\n// Warm a key without waiting on it: nothing if it is fresh, one background read\n// if it is stale or absent. For prefetching the page a reader is about to open.\n// Never throws, never rejects.\nexport function warm(key, fetcher, { ttl = 300000 } = {}) {\n  try {\n    const entry = read(key);\n    if (entry && Date.now() - entry.t <= ttl) return Promise.resolve(false);\n    return fetchOnce(key, fetcher).then(() => true, () => false);\n  } catch { return Promise.resolve(false); }\n}\n"],
  "mappings": "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",
  "names": ["PREFIX", "PREFS_PREFIX", "k", "key", "read", "raw", "e", "write", "v", "text", "serialize", "rec", "evictOldest", "oldestKey", "oldestT", "i", "sk", "t", "REC_HEAD", "m", "SIGNED_TOKEN", "a", "b", "supersede", "lk", "INFLIGHT", "GEN", "genOf", "fetchOnce", "fetcher", "p", "g", "fresh", "current", "done", "ttl", "onUpdate", "entry", "changed", "before"]
}
