Files
randy 5caf865fc8
CI / check (push) Successful in 40s
CI / deploy (push) Successful in 12s
Cache radar tile images with a Service Worker
public/sw.js: network-falling-back-to-cache for RainViewer radar tile
requests specifically (nothing else — the app itself is served by the
same local server that's already the offline resource in the van, so
there's no app-shell to precache). Registered from layout.erb, a no-op
where it can't register (insecure context) rather than an error.

Verified: registers and controls the page, tiles get cached on a
normal fetch, cache.match() correctly retrieves them by the same
request. CDP's network-blocking simulation doesn't interact cleanly
with Service Worker fetch interception (a known tooling quirk, not
specific to this), so the actual offline fallback path is the
standard try/catch pattern, verified by code review rather than a
simulated-offline screenshot.
2026-09-09 09:51:29 +02:00

64 lines
2.2 KiB
JavaScript

// Caches RainViewer radar tile images so the last-seen frames stay visible
// once the network is actually gone — nothing else here. No app-shell
// precaching: the app itself is served by the same local server that's
// already the offline resource in the van, so there's no "app unreachable
// but should still work" scenario to solve, unlike the radar tiles (a
// third-party CDN).
const CACHE_VERSION = "v1";
const RADAR_CACHE = `radar-tiles-${CACHE_VERSION}`;
// ponytail: a rough cap, not real LRU/expiry — trims oldest-by-insertion via
// Cache API iteration order (stable in practice, not spec-guaranteed).
// Revisit with timestamp-based expiry if storage actually becomes a
// problem; 400 tiles is at most a few MB either way.
const MAX_RADAR_TILES = 400;
function isRadarTile(url) {
return url.hostname.endsWith("rainviewer.com") && url.pathname.includes("/v2/radar/");
}
self.addEventListener("install", (event) => {
self.skipWaiting();
});
self.addEventListener("activate", (event) => {
event.waitUntil(
caches
.keys()
.then((keys) => Promise.all(keys.filter((key) => key !== RADAR_CACHE).map((key) => caches.delete(key))))
.then(() => self.clients.claim()),
);
});
self.addEventListener("fetch", (event) => {
const url = new URL(event.request.url);
if (!isRadarTile(url)) return;
event.respondWith(networkFallingBackToCache(event.request));
});
// Network first (frame ids rotate as new radar sweeps come in, so a cached
// tile can go stale) — cache is only the answer once the network actually
// fails, not a shortcut when it's merely slow.
async function networkFallingBackToCache(request) {
const cache = await caches.open(RADAR_CACHE);
try {
const response = await fetch(request);
if (response.ok) {
cache.put(request, response.clone());
trimRadarCache(cache);
}
return response;
} catch (err) {
const cached = await cache.match(request);
if (cached) return cached;
throw err;
}
}
async function trimRadarCache(cache) {
const keys = await cache.keys();
if (keys.length <= MAX_RADAR_TILES) return;
await Promise.all(keys.slice(0, keys.length - MAX_RADAR_TILES).map((key) => cache.delete(key)));
}