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.
This commit is contained in:
1 parent
a98b8dbd47
commit
5caf865fc8
3 files changed
+74
-1
No files matched your search
@@ -0,0 +1,63 @@
|
||||
// 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)));
|
||||
}
|
||||
Reference in new issue
Block a user