diff --git a/static/cache.js b/static/cache.js index 592f10b..a8b0df2 100644 --- a/static/cache.js +++ b/static/cache.js @@ -162,12 +162,6 @@ export async function chunkedFetch(urls, { ttl, concurrency = 4, isCurrent = () }); } -// 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. -export async function hasFreshCache(url, ttlMs) { - return isFresh(await cacheGet(url), ttlMs); -} - // Seed the cache for a URL from data that arrived by other means (a bundle // slice), with the etag its own endpoint would have sent — so the entry is // later revalidated per-view exactly as if it had been fetched directly. @@ -191,78 +185,41 @@ function viewFetches(q, today) { // them is instant — with ONE /api/v2/cell bundle request instead of one per view. // Its slices are the exact per-view payloads (each carrying the etag its endpoint // would emit), seeded under the per-view URLs the views actually request. The -// bundle call itself is conditional via a remembered etag, so a still-valid spot -// costs an empty 304. Deduped per spot; afterwards the 8 surrounding grid cells -// are warmed server-side (see prefetchNeighbors). +// request is conditional via a remembered etag — a still-valid spot costs an +// empty 304 — and neighbors=1 has the server warm the 8 surrounding grid cells +// itself: it owns the snapping math (grid.py) and the Nominatim pacing, so the +// old client-side clone of both is gone. Deduped per spot per page load. // // `ownViews` lists the view names the calling page already loads in the -// foreground (so they're skipped) — the page declares what it owns instead of -// this module knowing every page. +// foreground (so they're skipped when seeding) — the page declares what it owns +// instead of this module knowing every page. let _prefetched = ""; export function prefetchViews(lat, lon, ownViews) { const tag = `${lat.toFixed(4)},${lon.toFixed(4)}`; if (_prefetched === tag) return; _prefetched = tag; const q = `lat=${lat}&lon=${lon}`; + const own = new Set(ownViews); // Yield first so the landing view finishes rendering before we fetch the rest. setTimeout(async () => { - const fetches = viewFetches(q, localDay()); - const own = new Set(ownViews); - let missing = false; - for (const name of Object.keys(fetches)) { - if (own.has(name)) continue; - if (!(await hasFreshCache(...fetches[name]))) { missing = true; break; } - } - if (missing) { - try { - const ek = "tg-cell-etag:" + q; - let prev = null; - try { prev = sessionStorage.getItem(ek); } catch (e) {} - const res = await fetch(`api/v2/cell?${q}`, prev ? { headers: { "If-None-Match": prev } } : {}); - if (res.ok && res.status !== 304) { - const bundle = await res.json(); - try { sessionStorage.setItem(ek, res.headers.get("ETag") || ""); } catch (e) {} - for (const [name, slice] of Object.entries(bundle.slices || {})) { - if (!slice || !slice.data) continue; - const url = name === "day" - ? `api/v2/day?${q}&date=${bundle.today}` // the day slice targets today - : (fetches[name] || [])[0]; - if (url) seedCache(url, slice.data, slice.etag); - } + try { + const ek = "tg-cell-etag:" + q; + let prev = null; + try { prev = sessionStorage.getItem(ek); } catch (e) {} + const res = await fetch(`api/v2/cell?${q}&neighbors=1`, + prev ? { headers: { "If-None-Match": prev } } : {}); + if (res.ok && res.status !== 304) { + const bundle = await res.json(); + try { sessionStorage.setItem(ek, res.headers.get("ETag") || ""); } catch (e) {} + const fetches = viewFetches(q, localDay()); + for (const [name, slice] of Object.entries(bundle.slices || {})) { + if (own.has(name) || !slice || !slice.data) continue; + const url = name === "day" + ? `api/v2/day?${q}&date=${bundle.today}` // the day slice targets today + : (fetches[name] || [])[0]; + if (url) seedCache(url, slice.data, slice.etag); } - } catch (e) {} // prefetch is a bonus; every view still fetches for itself - } - prefetchNeighbors(lat, lon); + } + } catch (e) {} // prefetch is a bonus; every view still fetches for itself }, 350); } - -// Warm the server for the 8 grid cells around the selected one, so tapping a -// nearby spot lands on already-graded data. prefetch=1 is warm-only: the server -// never spends weather-API quota for it (a cold cell answers 204), and at most -// one reverse-geocode lookup happens per brand-new cell — hence the ≥1.1 s -// stagger, respecting Nominatim's 1-request/second policy. Responses aren't -// stored client-side (cache keys are coordinate-based); the value is server-side -// warmth. Mirrors backend/grid.py's snapping math. -const _warmedCells = new Set(); -function prefetchNeighbors(lat, lon) { - const latStep = 1 / 34.5; - const i = Math.floor(lat / latStep); - const centerLat = (i + 0.5) * latStep; - const lonStep = latStep / Math.max(Math.cos((centerLat * Math.PI) / 180), 0.05); - const j = Math.floor(lon / lonStep); - const centerLon = (j + 0.5) * lonStep; - let k = 0; - for (const di of [-1, 0, 1]) { - for (const dj of [-1, 0, 1]) { - if (!di && !dj) continue; - const nLat = centerLat + di * latStep; - if (Math.abs(nLat) > 90) continue; // no rows past the poles - const nLon = ((centerLon + dj * lonStep + 540) % 360) - 180; // wrap across the antimeridian - const cellTag = `${i + di}_${j + dj}`; - if (_warmedCells.has(cellTag)) continue; - _warmedCells.add(cellTag); - const nq = `lat=${nLat.toFixed(5)}&lon=${nLon.toFixed(5)}`; - setTimeout(() => { fetch(`api/v2/cell?${nq}&prefetch=1`).catch(() => {}); }, 1500 + 1100 * k++); - } - } -}