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
@@ -16,7 +16,7 @@ independence from connectivity.
|
|||||||
* [ ] Import Park4Night / iOverlander / Freecampsites data
|
* [ ] Import Park4Night / iOverlander / Freecampsites data
|
||||||
* [ ] Kiwix/ZIM offline Wikipedia linked to POI GPS coordinates
|
* [ ] Kiwix/ZIM offline Wikipedia linked to POI GPS coordinates
|
||||||
* [x] Cache weather forecast when temporarily connected — Open-Meteo 3-day forecast in the Radar flyout, last successful fetch cached in localStorage as a fallback when offline
|
* [x] Cache weather forecast when temporarily connected — Open-Meteo 3-day forecast in the Radar flyout, last successful fetch cached in localStorage as a fallback when offline
|
||||||
** [ ] Cache the radar tile *images* too, not just the forecast text — needs a Service Worker (own bigger task: registration, cache strategy, invalidation); app has HTTPS everywhere (reverse proxy, incl. the in-van local server) so it's viable, just not done yet
|
** [x] Cache the radar tile *images* too — `public/sw.js`, network-falling-back-to-cache for RainViewer tile requests, registered from layout.erb
|
||||||
|
|
||||||
== NPS visual style
|
== NPS visual style
|
||||||
|
|
||||||
|
|||||||
@@ -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)));
|
||||||
|
}
|
||||||
@@ -9,6 +9,16 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<%= yield %>
|
<%= yield %>
|
||||||
|
<script>
|
||||||
|
// Caches radar tile images (see public/sw.js) so the last-seen frames
|
||||||
|
// stay visible once the network is actually gone. Silently a no-op
|
||||||
|
// where it can't register — insecure-context LAN access without a
|
||||||
|
// reverse proxy in front, for instance — same as radar caching simply
|
||||||
|
// not working there; nothing else on the page depends on this.
|
||||||
|
if ("serviceWorker" in navigator) {
|
||||||
|
navigator.serviceWorker.register("/sw.js").catch(() => {});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
<script src="/vendor/pmtiles/pmtiles.js"></script>
|
<script src="/vendor/pmtiles/pmtiles.js"></script>
|
||||||
<script src="/vendor/protomaps-themes-base/protomaps-themes-base.js"></script>
|
<script src="/vendor/protomaps-themes-base/protomaps-themes-base.js"></script>
|
||||||
<%# Vendored libs above don't change between deploys, so they're left
|
<%# Vendored libs above don't change between deploys, so they're left
|
||||||
|
|||||||
Reference in new issue
Block a user