Cache: day-fresh reads + prefetch every other view (#8)
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 <noreply@anthropic.com>
This commit is contained in:
parent
8ff352e236
commit
521a131646
1 changed files with 54 additions and 9 deletions
|
|
@ -84,40 +84,85 @@ function putCache(store, key, s) {
|
||||||
catch (e) { evictOldestCache(store, 8); try { store.setItem(key, s); } catch (e2) {} }
|
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
|
// `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) {
|
async function getJSON(url, ttlMs = 600000, persist = false) {
|
||||||
const store = persist ? localStorage : sessionStorage;
|
const store = persist ? localStorage : sessionStorage;
|
||||||
const key = "tg:" + url;
|
const key = "tg:" + url;
|
||||||
try {
|
try {
|
||||||
const raw = store.getItem(key);
|
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) {}
|
} catch (e) {}
|
||||||
const res = await fetch(url);
|
const res = await fetch(url);
|
||||||
const d = await res.json();
|
const d = await res.json();
|
||||||
if (!res.ok) { const err = new Error(d.detail || "request failed"); err.status = res.status; throw err; }
|
if (!res.ok) { const err = new Error(d.detail || "request failed"); err.status = res.status; throw err; }
|
||||||
try {
|
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
|
if (s.length < 1500000) putCache(store, key, s); // skip caching very large payloads
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
return d;
|
return d;
|
||||||
}
|
}
|
||||||
|
|
||||||
// After the landing page's own data is up, warm the OTHER two views (server- and
|
// Is there already a usable (fresh, same-day) cache entry for this URL? Lets the
|
||||||
// session-cached, so cheap) so switching to them is instant. Deduped per spot.
|
// 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 = "";
|
let _prefetched = "";
|
||||||
function prefetchViews(lat, lon, skip) {
|
function prefetchViews(lat, lon, skip) {
|
||||||
const tag = `${lat.toFixed(4)},${lon.toFixed(4)},${skip}`;
|
const tag = `${lat.toFixed(4)},${lon.toFixed(4)},${skip}`;
|
||||||
if (_prefetched === tag) return;
|
if (_prefetched === tag) return;
|
||||||
_prefetched = tag;
|
_prefetched = tag;
|
||||||
const q = `lat=${lat}&lon=${lon}`;
|
const q = `lat=${lat}&lon=${lon}`;
|
||||||
|
const fetches = viewFetches(q);
|
||||||
|
const own = new Set(VIEW_OWN[skip] || [skip]);
|
||||||
const warm = () => {
|
const warm = () => {
|
||||||
if (skip !== "grade") getJSON(`api/grade?${q}`, TTL.grade).catch(() => {});
|
for (const name of Object.keys(fetches)) {
|
||||||
if (skip !== "forecast") getJSON(`api/v2/forecast?${q}`, TTL.forecast).catch(() => {});
|
if (own.has(name)) continue; // the current page's own data
|
||||||
if (skip !== "calendar") getJSON(`api/v2/calendar?${q}&months=24`, TTL.calendar, true).catch(() => {});
|
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.
|
// Yield first so the landing view finishes rendering before we fetch the rest.
|
||||||
setTimeout(warm, 350);
|
setTimeout(warm, 350);
|
||||||
}
|
}
|
||||||
|
|
||||||
window.Thermograph = { loadLastLocation, saveLastLocation, getJSON, prefetchViews, TTL };
|
window.Thermograph = { loadLastLocation, saveLastLocation, getJSON, prefetchViews, hasFreshCache, TTL };
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue