Service Worker v25: releases no longer delete offline map tiles
Tile cache names included CACHE_VERSION, and activation deletes caches whose names are not current, so every release threw away every tile officers had downloaded for field work. Tile caches now carry their own TILE_CACHE_VERSION, frozen at v24 so devices on v24 keep their tiles untouched. Tiles in any older tile cache (including the legacy tiles-v1 bucket) are moved into the current per-provider caches instead of deleted: nothing current is overwritten, limits are respected, and the old cache is removed only after a successful copy. Keyless Carto and Google tiles are not carried over. Old shell/modules/api caches are still deleted, and clients are claimed before the tile move so the page is not held up. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
486480eda6
commit
26c8438e04
124
dist/sw.js
vendored
124
dist/sw.js
vendored
@ -137,7 +137,13 @@
|
||||
// Mercator metres, which overstated them by 1/cos(latitude) — about 3 m on
|
||||
// a 200 m circle at Wa. The Circle and Area analysis popups can be dragged
|
||||
// by their title bar, staying attached to the shape they describe.
|
||||
const CACHE_VERSION = 'v24';
|
||||
// v25: Releases no longer delete offline map tiles. Tile cache names used to
|
||||
// include CACHE_VERSION, and activation deletes caches whose names are not
|
||||
// current, so every release since the tile caches were split per provider
|
||||
// threw away every tile officers had downloaded for field work. Tile caches
|
||||
// now carry their own TILE_CACHE_VERSION, frozen at v24, and tiles in any
|
||||
// older tile cache are moved into the current ones instead of deleted.
|
||||
const CACHE_VERSION = 'v25';
|
||||
const SHELL_CACHE = `shell-${CACHE_VERSION}`;
|
||||
const MODULES_CACHE = `modules-${CACHE_VERSION}`;
|
||||
const API_CACHE = `api-${CACHE_VERSION}`;
|
||||
@ -147,11 +153,24 @@ const API_CACHE = `api-${CACHE_VERSION}`;
|
||||
// Limits are per-cache (not global). 5 000 tiles ≈ ~150 MB at ~30 KB/tile,
|
||||
// which covers a Ghana district at zoom 10–15 (typical field-work range).
|
||||
// ----------------------------------------------------------------------------
|
||||
const TILES_OSM = `tiles-osm-${CACHE_VERSION}`;
|
||||
const TILES_TOPO = `tiles-topo-${CACHE_VERSION}`;
|
||||
const TILES_SATELLITE = `tiles-satellite-${CACHE_VERSION}`;
|
||||
const TILES_CARTO_LIGHT = `tiles-carto-light-${CACHE_VERSION}`;
|
||||
const TILES_CARTO_DARK = `tiles-carto-dark-${CACHE_VERSION}`;
|
||||
//
|
||||
// Tile caches are versioned separately from the app. They used to take their
|
||||
// name from CACHE_VERSION, and the activate handler deletes every tiles-* cache
|
||||
// whose name is not current — so each release silently threw away every tile an
|
||||
// officer had downloaded for offline field work, for every provider. Map tiles
|
||||
// do not change between releases of the app, so their cache names no longer do.
|
||||
//
|
||||
// TILE_CACHE_VERSION is frozen at the value CACHE_VERSION had when that stopped,
|
||||
// so devices already on v24 keep their existing tile caches with nothing to copy.
|
||||
// Change it only if the way tiles are stored changes — and even then, activate
|
||||
// moves tiles from older tile caches into the new ones rather than deleting them
|
||||
// (see migrateTileCache).
|
||||
const TILE_CACHE_VERSION = 'v24';
|
||||
const TILES_OSM = `tiles-osm-${TILE_CACHE_VERSION}`;
|
||||
const TILES_TOPO = `tiles-topo-${TILE_CACHE_VERSION}`;
|
||||
const TILES_SATELLITE = `tiles-satellite-${TILE_CACHE_VERSION}`;
|
||||
const TILES_CARTO_LIGHT = `tiles-carto-light-${TILE_CACHE_VERSION}`;
|
||||
const TILES_CARTO_DARK = `tiles-carto-dark-${TILE_CACHE_VERSION}`;
|
||||
|
||||
// Per-provider tile limits.
|
||||
// • OSM and Topographic are the providers offered for active offline
|
||||
@ -228,36 +247,89 @@ self.addEventListener('install', (event) => {
|
||||
self.addEventListener('activate', (event) => {
|
||||
console.log('[SW] Activating...');
|
||||
|
||||
event.waitUntil(
|
||||
caches.keys()
|
||||
.then((cacheNames) => {
|
||||
// Build the set of caches that should remain
|
||||
event.waitUntil((async () => {
|
||||
const cacheNames = await caches.keys();
|
||||
const keep = new Set([SHELL_CACHE, MODULES_CACHE, API_CACHE, ...ALL_TILE_CACHES]);
|
||||
|
||||
return Promise.all(
|
||||
// App caches from earlier releases hold nothing worth keeping: delete them.
|
||||
await Promise.all(
|
||||
cacheNames
|
||||
// Delete anything that:
|
||||
// • belongs to one of our managed cache prefixes (shell-, tiles-, modules-, api-)
|
||||
// • but is NOT in the current keep set
|
||||
// This includes the legacy "tiles-v1" single bucket.
|
||||
.filter((name) => {
|
||||
const isOurs =
|
||||
name.startsWith('shell-') ||
|
||||
name.startsWith('tiles-') ||
|
||||
name.startsWith('modules-') ||
|
||||
name.startsWith('api-');
|
||||
return isOurs && !keep.has(name);
|
||||
})
|
||||
.filter((name) =>
|
||||
(name.startsWith('shell-') || name.startsWith('modules-') || name.startsWith('api-'))
|
||||
&& !keep.has(name))
|
||||
.map((name) => {
|
||||
console.log('[SW] Deleting old cache:', name);
|
||||
return caches.delete(name);
|
||||
})
|
||||
);
|
||||
})
|
||||
.then(() => self.clients.claim())
|
||||
);
|
||||
|
||||
// Take control before moving tiles, so the page is not held waiting on a
|
||||
// copy that can run to thousands of entries.
|
||||
await self.clients.claim();
|
||||
|
||||
// Tile caches from earlier releases hold what officers downloaded for
|
||||
// offline use: move those tiles into the current caches, then delete. This
|
||||
// includes the legacy single "tiles-v1" bucket, whose tiles are sorted into
|
||||
// the right provider by URL.
|
||||
const oldTileCaches = cacheNames.filter((name) => name.startsWith('tiles-') && !keep.has(name));
|
||||
for (const name of oldTileCaches) {
|
||||
await migrateTileCache(name);
|
||||
}
|
||||
})());
|
||||
});
|
||||
|
||||
/**
|
||||
* Move the tiles in a tile cache from an earlier release into the current
|
||||
* per-provider caches, then delete it.
|
||||
*
|
||||
* Each tile goes where getTileCacheName() would put it today, so the legacy
|
||||
* single bucket is sorted by provider too. Nothing already in the current cache
|
||||
* is overwritten, and no cache is filled past its limit. If the copy fails part
|
||||
* way, the old cache is left in place rather than deleted, so its tiles are not
|
||||
* lost and the next activation carries on from there.
|
||||
*/
|
||||
async function migrateTileCache(oldName) {
|
||||
try {
|
||||
const oldCache = await caches.open(oldName);
|
||||
const requests = await oldCache.keys();
|
||||
const targets = new Map(); // cacheName -> { cache, count }
|
||||
let moved = 0;
|
||||
let skipped = 0;
|
||||
|
||||
for (const request of requests) {
|
||||
const url = new URL(request.url);
|
||||
const targetName = getTileCacheName(url);
|
||||
|
||||
// A provider no longer cached, or a Carto tile requested without a key.
|
||||
// The app has sent the CARTO key on every tile request since August 2026,
|
||||
// so a keyless Carto URL is never requested again — and anything cached
|
||||
// after CARTO began enforcing keys carries its "API KEY REQUIRED" stamp.
|
||||
const deadCarto = url.hostname.endsWith('basemaps.cartocdn.com') && !url.searchParams.has('key');
|
||||
if (!targetName || deadCarto) { skipped++; continue; }
|
||||
|
||||
let target = targets.get(targetName);
|
||||
if (!target) {
|
||||
const cache = await caches.open(targetName);
|
||||
target = { cache, count: (await cache.keys()).length };
|
||||
targets.set(targetName, target);
|
||||
}
|
||||
if (target.count >= (TILE_LIMITS[targetName] || 1500)) { skipped++; continue; }
|
||||
if (await target.cache.match(request)) { skipped++; continue; }
|
||||
|
||||
const response = await oldCache.match(request);
|
||||
if (!response) { skipped++; continue; }
|
||||
await target.cache.put(request, response);
|
||||
target.count++;
|
||||
moved++;
|
||||
}
|
||||
|
||||
await caches.delete(oldName);
|
||||
console.log(`[SW] Moved ${moved} tiles out of ${oldName} (${skipped} not needed) and removed it`);
|
||||
} catch (err) {
|
||||
console.warn(`[SW] Could not move tiles out of ${oldName}; kept it for the next activation:`, err);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// FETCH EVENT
|
||||
// ============================================================================
|
||||
|
||||
124
public/sw.js
124
public/sw.js
@ -137,7 +137,13 @@
|
||||
// Mercator metres, which overstated them by 1/cos(latitude) — about 3 m on
|
||||
// a 200 m circle at Wa. The Circle and Area analysis popups can be dragged
|
||||
// by their title bar, staying attached to the shape they describe.
|
||||
const CACHE_VERSION = 'v24';
|
||||
// v25: Releases no longer delete offline map tiles. Tile cache names used to
|
||||
// include CACHE_VERSION, and activation deletes caches whose names are not
|
||||
// current, so every release since the tile caches were split per provider
|
||||
// threw away every tile officers had downloaded for field work. Tile caches
|
||||
// now carry their own TILE_CACHE_VERSION, frozen at v24, and tiles in any
|
||||
// older tile cache are moved into the current ones instead of deleted.
|
||||
const CACHE_VERSION = 'v25';
|
||||
const SHELL_CACHE = `shell-${CACHE_VERSION}`;
|
||||
const MODULES_CACHE = `modules-${CACHE_VERSION}`;
|
||||
const API_CACHE = `api-${CACHE_VERSION}`;
|
||||
@ -147,11 +153,24 @@ const API_CACHE = `api-${CACHE_VERSION}`;
|
||||
// Limits are per-cache (not global). 5 000 tiles ≈ ~150 MB at ~30 KB/tile,
|
||||
// which covers a Ghana district at zoom 10–15 (typical field-work range).
|
||||
// ----------------------------------------------------------------------------
|
||||
const TILES_OSM = `tiles-osm-${CACHE_VERSION}`;
|
||||
const TILES_TOPO = `tiles-topo-${CACHE_VERSION}`;
|
||||
const TILES_SATELLITE = `tiles-satellite-${CACHE_VERSION}`;
|
||||
const TILES_CARTO_LIGHT = `tiles-carto-light-${CACHE_VERSION}`;
|
||||
const TILES_CARTO_DARK = `tiles-carto-dark-${CACHE_VERSION}`;
|
||||
//
|
||||
// Tile caches are versioned separately from the app. They used to take their
|
||||
// name from CACHE_VERSION, and the activate handler deletes every tiles-* cache
|
||||
// whose name is not current — so each release silently threw away every tile an
|
||||
// officer had downloaded for offline field work, for every provider. Map tiles
|
||||
// do not change between releases of the app, so their cache names no longer do.
|
||||
//
|
||||
// TILE_CACHE_VERSION is frozen at the value CACHE_VERSION had when that stopped,
|
||||
// so devices already on v24 keep their existing tile caches with nothing to copy.
|
||||
// Change it only if the way tiles are stored changes — and even then, activate
|
||||
// moves tiles from older tile caches into the new ones rather than deleting them
|
||||
// (see migrateTileCache).
|
||||
const TILE_CACHE_VERSION = 'v24';
|
||||
const TILES_OSM = `tiles-osm-${TILE_CACHE_VERSION}`;
|
||||
const TILES_TOPO = `tiles-topo-${TILE_CACHE_VERSION}`;
|
||||
const TILES_SATELLITE = `tiles-satellite-${TILE_CACHE_VERSION}`;
|
||||
const TILES_CARTO_LIGHT = `tiles-carto-light-${TILE_CACHE_VERSION}`;
|
||||
const TILES_CARTO_DARK = `tiles-carto-dark-${TILE_CACHE_VERSION}`;
|
||||
|
||||
// Per-provider tile limits.
|
||||
// • OSM and Topographic are the providers offered for active offline
|
||||
@ -228,36 +247,89 @@ self.addEventListener('install', (event) => {
|
||||
self.addEventListener('activate', (event) => {
|
||||
console.log('[SW] Activating...');
|
||||
|
||||
event.waitUntil(
|
||||
caches.keys()
|
||||
.then((cacheNames) => {
|
||||
// Build the set of caches that should remain
|
||||
event.waitUntil((async () => {
|
||||
const cacheNames = await caches.keys();
|
||||
const keep = new Set([SHELL_CACHE, MODULES_CACHE, API_CACHE, ...ALL_TILE_CACHES]);
|
||||
|
||||
return Promise.all(
|
||||
// App caches from earlier releases hold nothing worth keeping: delete them.
|
||||
await Promise.all(
|
||||
cacheNames
|
||||
// Delete anything that:
|
||||
// • belongs to one of our managed cache prefixes (shell-, tiles-, modules-, api-)
|
||||
// • but is NOT in the current keep set
|
||||
// This includes the legacy "tiles-v1" single bucket.
|
||||
.filter((name) => {
|
||||
const isOurs =
|
||||
name.startsWith('shell-') ||
|
||||
name.startsWith('tiles-') ||
|
||||
name.startsWith('modules-') ||
|
||||
name.startsWith('api-');
|
||||
return isOurs && !keep.has(name);
|
||||
})
|
||||
.filter((name) =>
|
||||
(name.startsWith('shell-') || name.startsWith('modules-') || name.startsWith('api-'))
|
||||
&& !keep.has(name))
|
||||
.map((name) => {
|
||||
console.log('[SW] Deleting old cache:', name);
|
||||
return caches.delete(name);
|
||||
})
|
||||
);
|
||||
})
|
||||
.then(() => self.clients.claim())
|
||||
);
|
||||
|
||||
// Take control before moving tiles, so the page is not held waiting on a
|
||||
// copy that can run to thousands of entries.
|
||||
await self.clients.claim();
|
||||
|
||||
// Tile caches from earlier releases hold what officers downloaded for
|
||||
// offline use: move those tiles into the current caches, then delete. This
|
||||
// includes the legacy single "tiles-v1" bucket, whose tiles are sorted into
|
||||
// the right provider by URL.
|
||||
const oldTileCaches = cacheNames.filter((name) => name.startsWith('tiles-') && !keep.has(name));
|
||||
for (const name of oldTileCaches) {
|
||||
await migrateTileCache(name);
|
||||
}
|
||||
})());
|
||||
});
|
||||
|
||||
/**
|
||||
* Move the tiles in a tile cache from an earlier release into the current
|
||||
* per-provider caches, then delete it.
|
||||
*
|
||||
* Each tile goes where getTileCacheName() would put it today, so the legacy
|
||||
* single bucket is sorted by provider too. Nothing already in the current cache
|
||||
* is overwritten, and no cache is filled past its limit. If the copy fails part
|
||||
* way, the old cache is left in place rather than deleted, so its tiles are not
|
||||
* lost and the next activation carries on from there.
|
||||
*/
|
||||
async function migrateTileCache(oldName) {
|
||||
try {
|
||||
const oldCache = await caches.open(oldName);
|
||||
const requests = await oldCache.keys();
|
||||
const targets = new Map(); // cacheName -> { cache, count }
|
||||
let moved = 0;
|
||||
let skipped = 0;
|
||||
|
||||
for (const request of requests) {
|
||||
const url = new URL(request.url);
|
||||
const targetName = getTileCacheName(url);
|
||||
|
||||
// A provider no longer cached, or a Carto tile requested without a key.
|
||||
// The app has sent the CARTO key on every tile request since August 2026,
|
||||
// so a keyless Carto URL is never requested again — and anything cached
|
||||
// after CARTO began enforcing keys carries its "API KEY REQUIRED" stamp.
|
||||
const deadCarto = url.hostname.endsWith('basemaps.cartocdn.com') && !url.searchParams.has('key');
|
||||
if (!targetName || deadCarto) { skipped++; continue; }
|
||||
|
||||
let target = targets.get(targetName);
|
||||
if (!target) {
|
||||
const cache = await caches.open(targetName);
|
||||
target = { cache, count: (await cache.keys()).length };
|
||||
targets.set(targetName, target);
|
||||
}
|
||||
if (target.count >= (TILE_LIMITS[targetName] || 1500)) { skipped++; continue; }
|
||||
if (await target.cache.match(request)) { skipped++; continue; }
|
||||
|
||||
const response = await oldCache.match(request);
|
||||
if (!response) { skipped++; continue; }
|
||||
await target.cache.put(request, response);
|
||||
target.count++;
|
||||
moved++;
|
||||
}
|
||||
|
||||
await caches.delete(oldName);
|
||||
console.log(`[SW] Moved ${moved} tiles out of ${oldName} (${skipped} not needed) and removed it`);
|
||||
} catch (err) {
|
||||
console.warn(`[SW] Could not move tiles out of ${oldName}; kept it for the next activation:`, err);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// FETCH EVENT
|
||||
// ============================================================================
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user