From 521a131646fd1f77418fe76d0d6ec1e6205e0ec2 Mon Sep 17 00:00:00 2001 From: Emi Griffith Date: Fri, 10 Jul 2026 19:28:34 -0700 Subject: [PATCH] Cache: day-fresh reads + prefetch every other view (#8) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two related client-cache improvements in nav.js: - Day-of-fetch freshness: every cached API response is now tagged with the viewer's local calendar day, and a cache hit requires both the TTL AND a same-day tag. A cache stored yesterday is refetched today, so a new day's data (and the calendar's "days shown") can't stay hidden behind an otherwise-fresh cache across a midnight rollover. - Cross-view prefetch now covers the Day page (api/v2/day), which it previously never warmed, and skips any view that already has a fresh same-day cache — so it only fetches the pages actually missing one. Claude-Session: https://claude.ai/code/session_019VP23wKmjS2ozk1g5a9g1Z Co-authored-by: Claude Opus 4.8 --- static/nav.js | 63 +++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 54 insertions(+), 9 deletions(-) diff --git a/static/nav.js b/static/nav.js index cef0f40..e20ac8a 100644 --- a/static/nav.js +++ b/static/nav.js @@ -84,40 +84,85 @@ function putCache(store, key, s) { catch (e) { evictOldestCache(store, 8); try { store.setItem(key, s); } catch (e2) {} } } +// The viewer's own calendar day ("YYYY-MM-DD", local clock). Every cached response +// is tagged with the day it was stored, so a cache made yesterday is treated as +// stale today — the newest available day (and the day counts derived from it) can't +// be hidden behind an otherwise-fresh cache across a midnight rollover. +function localDay() { + const d = new Date(); + return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`; +} + // `persist` picks localStorage (survives tab close / navigating back to a prior -// spot); otherwise sessionStorage (per-tab). The read TTL governs freshness. +// spot); otherwise sessionStorage (per-tab). A cache hit needs BOTH freshness +// checks: within its TTL, and stored on today's local date. async function getJSON(url, ttlMs = 600000, persist = false) { const store = persist ? localStorage : sessionStorage; const key = "tg:" + url; try { const raw = store.getItem(key); - if (raw) { const o = JSON.parse(raw); if (Date.now() - o.t < ttlMs) return o.d; } + if (raw) { + const o = JSON.parse(raw); + if (o.day === localDay() && Date.now() - o.t < ttlMs) return o.d; + } } catch (e) {} const res = await fetch(url); const d = await res.json(); if (!res.ok) { const err = new Error(d.detail || "request failed"); err.status = res.status; throw err; } try { - const s = JSON.stringify({ t: Date.now(), d }); + const s = JSON.stringify({ t: Date.now(), day: localDay(), d }); if (s.length < 1500000) putCache(store, key, s); // skip caching very large payloads } catch (e) {} return d; } -// After the landing page's own data is up, warm the OTHER two views (server- and -// session-cached, so cheap) so switching to them is instant. Deduped per spot. +// Is there already a usable (fresh, same-day) cache entry for this URL? Lets the +// prefetch skip views that are already warm and only reach out for the ones missing. +function hasFreshCache(url, ttlMs, persist) { + const store = persist ? localStorage : sessionStorage; + try { + const raw = store.getItem("tg:" + url); + if (!raw) return false; + const o = JSON.parse(raw); + return o.day === localDay() && Date.now() - o.t < ttlMs; + } catch (e) { return false; } +} + +// The primary API each view loads first. Weekly (the map page) owns two panels — +// its grade and its forecast — so leaving that page prefetches neither. +function viewFetches(q) { + return { + grade: [`api/grade?${q}`, TTL.grade, false], + forecast: [`api/v2/forecast?${q}`, TTL.forecast, false], + calendar: [`api/v2/calendar?${q}&months=24`, TTL.calendar, true], + day: [`api/v2/day?${q}`, TTL.day, false], + }; +} +// The fetch(es) that belong to the page prefetch is called from, so they're skipped +// (that data is already loading in the foreground). +const VIEW_OWN = { grade: ["grade", "forecast"], calendar: ["calendar"], day: ["day"] }; + +// After the landing page's own data is up, warm the OTHER views so switching to them +// is instant. Each view with no fresh same-day cache is fetched once in the +// background (already-cached views are left alone). Deduped per spot. let _prefetched = ""; function prefetchViews(lat, lon, skip) { const tag = `${lat.toFixed(4)},${lon.toFixed(4)},${skip}`; if (_prefetched === tag) return; _prefetched = tag; const q = `lat=${lat}&lon=${lon}`; + const fetches = viewFetches(q); + const own = new Set(VIEW_OWN[skip] || [skip]); const warm = () => { - if (skip !== "grade") getJSON(`api/grade?${q}`, TTL.grade).catch(() => {}); - if (skip !== "forecast") getJSON(`api/v2/forecast?${q}`, TTL.forecast).catch(() => {}); - if (skip !== "calendar") getJSON(`api/v2/calendar?${q}&months=24`, TTL.calendar, true).catch(() => {}); + for (const name of Object.keys(fetches)) { + if (own.has(name)) continue; // the current page's own data + const [url, ttl, persist] = fetches[name]; + if (hasFreshCache(url, ttl, persist)) continue; // already warm — no refetch + getJSON(url, ttl, persist).catch(() => {}); + } }; // Yield first so the landing view finishes rendering before we fetch the rest. setTimeout(warm, 350); } -window.Thermograph = { loadLastLocation, saveLastLocation, getJSON, prefetchViews, TTL }; +window.Thermograph = { loadLastLocation, saveLastLocation, getJSON, prefetchViews, hasFreshCache, TTL };