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
138
dist/sw.js
vendored
138
dist/sw.js
vendored
@ -137,7 +137,13 @@
|
|||||||
// Mercator metres, which overstated them by 1/cos(latitude) — about 3 m on
|
// 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
|
// 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.
|
// 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 SHELL_CACHE = `shell-${CACHE_VERSION}`;
|
||||||
const MODULES_CACHE = `modules-${CACHE_VERSION}`;
|
const MODULES_CACHE = `modules-${CACHE_VERSION}`;
|
||||||
const API_CACHE = `api-${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,
|
// 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).
|
// 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}`;
|
// Tile caches are versioned separately from the app. They used to take their
|
||||||
const TILES_SATELLITE = `tiles-satellite-${CACHE_VERSION}`;
|
// name from CACHE_VERSION, and the activate handler deletes every tiles-* cache
|
||||||
const TILES_CARTO_LIGHT = `tiles-carto-light-${CACHE_VERSION}`;
|
// whose name is not current — so each release silently threw away every tile an
|
||||||
const TILES_CARTO_DARK = `tiles-carto-dark-${CACHE_VERSION}`;
|
// 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.
|
// Per-provider tile limits.
|
||||||
// • OSM and Topographic are the providers offered for active offline
|
// • OSM and Topographic are the providers offered for active offline
|
||||||
@ -228,36 +247,89 @@ self.addEventListener('install', (event) => {
|
|||||||
self.addEventListener('activate', (event) => {
|
self.addEventListener('activate', (event) => {
|
||||||
console.log('[SW] Activating...');
|
console.log('[SW] Activating...');
|
||||||
|
|
||||||
event.waitUntil(
|
event.waitUntil((async () => {
|
||||||
caches.keys()
|
const cacheNames = await caches.keys();
|
||||||
.then((cacheNames) => {
|
const keep = new Set([SHELL_CACHE, MODULES_CACHE, API_CACHE, ...ALL_TILE_CACHES]);
|
||||||
// Build the set of caches that should remain
|
|
||||||
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.
|
||||||
cacheNames
|
await Promise.all(
|
||||||
// Delete anything that:
|
cacheNames
|
||||||
// • belongs to one of our managed cache prefixes (shell-, tiles-, modules-, api-)
|
.filter((name) =>
|
||||||
// • but is NOT in the current keep set
|
(name.startsWith('shell-') || name.startsWith('modules-') || name.startsWith('api-'))
|
||||||
// This includes the legacy "tiles-v1" single bucket.
|
&& !keep.has(name))
|
||||||
.filter((name) => {
|
.map((name) => {
|
||||||
const isOurs =
|
console.log('[SW] Deleting old cache:', name);
|
||||||
name.startsWith('shell-') ||
|
return caches.delete(name);
|
||||||
name.startsWith('tiles-') ||
|
})
|
||||||
name.startsWith('modules-') ||
|
);
|
||||||
name.startsWith('api-');
|
|
||||||
return isOurs && !keep.has(name);
|
// Take control before moving tiles, so the page is not held waiting on a
|
||||||
})
|
// copy that can run to thousands of entries.
|
||||||
.map((name) => {
|
await self.clients.claim();
|
||||||
console.log('[SW] Deleting old cache:', name);
|
|
||||||
return caches.delete(name);
|
// 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.
|
||||||
.then(() => self.clients.claim())
|
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
|
// FETCH EVENT
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|||||||
138
public/sw.js
138
public/sw.js
@ -137,7 +137,13 @@
|
|||||||
// Mercator metres, which overstated them by 1/cos(latitude) — about 3 m on
|
// 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
|
// 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.
|
// 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 SHELL_CACHE = `shell-${CACHE_VERSION}`;
|
||||||
const MODULES_CACHE = `modules-${CACHE_VERSION}`;
|
const MODULES_CACHE = `modules-${CACHE_VERSION}`;
|
||||||
const API_CACHE = `api-${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,
|
// 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).
|
// 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}`;
|
// Tile caches are versioned separately from the app. They used to take their
|
||||||
const TILES_SATELLITE = `tiles-satellite-${CACHE_VERSION}`;
|
// name from CACHE_VERSION, and the activate handler deletes every tiles-* cache
|
||||||
const TILES_CARTO_LIGHT = `tiles-carto-light-${CACHE_VERSION}`;
|
// whose name is not current — so each release silently threw away every tile an
|
||||||
const TILES_CARTO_DARK = `tiles-carto-dark-${CACHE_VERSION}`;
|
// 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.
|
// Per-provider tile limits.
|
||||||
// • OSM and Topographic are the providers offered for active offline
|
// • OSM and Topographic are the providers offered for active offline
|
||||||
@ -228,36 +247,89 @@ self.addEventListener('install', (event) => {
|
|||||||
self.addEventListener('activate', (event) => {
|
self.addEventListener('activate', (event) => {
|
||||||
console.log('[SW] Activating...');
|
console.log('[SW] Activating...');
|
||||||
|
|
||||||
event.waitUntil(
|
event.waitUntil((async () => {
|
||||||
caches.keys()
|
const cacheNames = await caches.keys();
|
||||||
.then((cacheNames) => {
|
const keep = new Set([SHELL_CACHE, MODULES_CACHE, API_CACHE, ...ALL_TILE_CACHES]);
|
||||||
// Build the set of caches that should remain
|
|
||||||
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.
|
||||||
cacheNames
|
await Promise.all(
|
||||||
// Delete anything that:
|
cacheNames
|
||||||
// • belongs to one of our managed cache prefixes (shell-, tiles-, modules-, api-)
|
.filter((name) =>
|
||||||
// • but is NOT in the current keep set
|
(name.startsWith('shell-') || name.startsWith('modules-') || name.startsWith('api-'))
|
||||||
// This includes the legacy "tiles-v1" single bucket.
|
&& !keep.has(name))
|
||||||
.filter((name) => {
|
.map((name) => {
|
||||||
const isOurs =
|
console.log('[SW] Deleting old cache:', name);
|
||||||
name.startsWith('shell-') ||
|
return caches.delete(name);
|
||||||
name.startsWith('tiles-') ||
|
})
|
||||||
name.startsWith('modules-') ||
|
);
|
||||||
name.startsWith('api-');
|
|
||||||
return isOurs && !keep.has(name);
|
// Take control before moving tiles, so the page is not held waiting on a
|
||||||
})
|
// copy that can run to thousands of entries.
|
||||||
.map((name) => {
|
await self.clients.claim();
|
||||||
console.log('[SW] Deleting old cache:', name);
|
|
||||||
return caches.delete(name);
|
// 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.
|
||||||
.then(() => self.clients.claim())
|
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
|
// FETCH EVENT
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user