@mapvx/web-kit 0.0.0-stage → 1.2.0-develop.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/LICENSE +17 -0
  2. package/README.md +174 -2
  3. package/dist/cjs/chunks/DRACOLoader-BXWDwLus.cjs +2 -0
  4. package/dist/cjs/chunks/GLTFLoader-Ie8s8u8H.cjs +2 -0
  5. package/dist/cjs/chunks/KTX2Loader-qXr_kxSW.cjs +2 -0
  6. package/dist/cjs/chunks/accessibility--hZFITPu.cjs +2 -0
  7. package/dist/cjs/chunks/back_arrow-D7QonpvS.cjs +2 -0
  8. package/dist/cjs/chunks/debug-hooks-CUiqwo8b.cjs +2 -0
  9. package/dist/cjs/chunks/dist-CiVAEX5i.cjs +2 -0
  10. package/dist/cjs/chunks/dist-DDS4P2yt.cjs +2 -0
  11. package/dist/cjs/chunks/dist-Dh7LsBxq.cjs +2 -0
  12. package/dist/cjs/chunks/expression-CO3CSbYd.cjs +2 -0
  13. package/dist/cjs/chunks/globe-viewport-D1yIuqYX.cjs +2 -0
  14. package/dist/cjs/chunks/icon-origin-point-Bt8oX8FA.cjs +2 -0
  15. package/dist/cjs/chunks/meshopt_decoder.module-Bk6j-o8V.cjs +2 -0
  16. package/dist/cjs/chunks/repeat_icon-BEd6gL1X.cjs +2 -0
  17. package/dist/cjs/chunks/rolldown-runtime-DJKPJnot.cjs +2 -0
  18. package/dist/cjs/chunks/shader-type-decoder-D5AGkPPj.cjs +2 -0
  19. package/dist/cjs/chunks/styles-DrSaht0q.cjs +2 -0
  20. package/dist/cjs/chunks/three.module-Cb7BgQfP.cjs +2 -0
  21. package/dist/cjs/chunks/walking-ChGLFmdv.cjs +2 -0
  22. package/dist/cjs/chunks/webgl-C2jh1WTK.cjs +2 -0
  23. package/dist/cjs/chunks/webgl-device-Dy1ruGCX.cjs +2 -0
  24. package/dist/cjs/web-kit.cjs +425 -0
  25. package/dist/es/chunks/DRACOLoader-883EjV8X.js +2 -0
  26. package/dist/es/chunks/GLTFLoader-HXSQaGKT.js +2 -0
  27. package/dist/es/chunks/KTX2Loader-DVOYJ-4Y.js +2 -0
  28. package/dist/es/chunks/accessibility-D-deEbwV.js +2 -0
  29. package/dist/es/chunks/back_arrow-DjhoWjXs.js +2 -0
  30. package/dist/es/chunks/debug-hooks-DWy-ovrh.js +2 -0
  31. package/dist/es/chunks/dist-47_p4Yxg.js +2 -0
  32. package/dist/es/chunks/dist-DOZVk2yA.js +2 -0
  33. package/dist/es/chunks/dist-DggqP-gQ.js +4 -0
  34. package/dist/es/chunks/expression-XgnkW6Sk.js +2 -0
  35. package/dist/es/chunks/globe-viewport-CwpYcu3c.js +2 -0
  36. package/dist/es/chunks/icon-origin-point-JEEMojyJ.js +2 -0
  37. package/dist/es/chunks/meshopt_decoder.module-CVnL-WFq.js +2 -0
  38. package/dist/es/chunks/repeat_icon-O8PH3s5h.js +2 -0
  39. package/dist/es/chunks/rolldown-runtime-Lq6Z3lw4.js +2 -0
  40. package/dist/es/chunks/shader-type-decoder-CpvFwfxh.js +2 -0
  41. package/dist/es/chunks/styles-CSDlMZd2.js +2 -0
  42. package/dist/es/chunks/three.module-jU3YiRA7.js +22 -0
  43. package/dist/es/chunks/walking-CeG7IOqk.js +2 -0
  44. package/dist/es/chunks/webgl-CW6td6sd.js +2 -0
  45. package/dist/es/chunks/webgl-device-ChYrAvsE.js +2 -0
  46. package/dist/es/web-kit.mjs +428 -0
  47. package/dist/iife/web-kit.iife.js +425 -0
  48. package/dist/sw/mvx-tiles-sw.js +582 -0
  49. package/dist/types/index.d.ts +1765 -0
  50. package/package.json +87 -4
@@ -0,0 +1,582 @@
1
+ /*
2
+ MapVX Tiles Service Worker (Workbox)
3
+ - Progressive bounded tile prefetch (INITIAL_TILE_LOAD / REFRESH_TILES)
4
+ - Cache-first for PBF tiles; stale-while-revalidate / network-first for MapVX APIs
5
+ - Restricted to lazarillo.app and mapvx.com (any subdomain), plus the 3D model
6
+ bucket; glyph ranges are matched by path, whatever host the style points at
7
+
8
+ How to use:
9
+ 1) Copy this file to your site root as "/mvx-tiles-sw.js"
10
+ (shipped at dist/sw/mvx-tiles-sw.js — see src/utils/mvx-tile-sw.ts)
11
+ 2) Ensure the site is served over HTTPS with Cache-Control: no-cache on this file
12
+ 3) Web Components register it and postMessage INITIAL_TILE_LOAD when ready
13
+ */
14
+
15
+ /* global workbox */
16
+ importScripts('https://storage.googleapis.com/workbox-cdn/releases/7.4.0/workbox-sw.js')
17
+
18
+ const CACHE_TILES = 'tiles-pbf'
19
+ const CACHE_API_PLACES = 'api-places-cache'
20
+ const CACHE_API_CONFIG = 'api-config-cache'
21
+ const CACHE_API_ROUTES = 'api-routes-cache'
22
+ const CACHE_API_FALLBACK = 'api-cache'
23
+ const CACHE_MAP_ASSETS = 'map-assets'
24
+ const CACHE_GLYPHS = 'map-glyphs'
25
+ const CACHE_VENDOR = 'map-vendor'
26
+
27
+ const KNOWN_RUNTIME_CACHES = new Set([
28
+ CACHE_TILES,
29
+ CACHE_API_PLACES,
30
+ CACHE_API_CONFIG,
31
+ CACHE_API_ROUTES,
32
+ CACHE_API_FALLBACK,
33
+ CACHE_MAP_ASSETS,
34
+ CACHE_GLYPHS,
35
+ CACHE_VENDOR,
36
+ ])
37
+
38
+ const DEFAULT_VECTOR_TILES_BASE_URL = 'https://tiles.mapvx.com/tiles'
39
+
40
+ /** Abort any in-flight prefetch when a new INITIAL_TILE_LOAD / REFRESH_TILES runs. */
41
+ let activeTilePrefetch = null
42
+
43
+ const TILE_PREFETCH = {
44
+ maxConcurrent: 4,
45
+ batchGapMs: 100,
46
+ zoomLevelGapMs: 5000,
47
+ initialRadiusTiles: 2,
48
+ progressiveRadiusTiles: 1,
49
+ maxTilesPerRun: 220,
50
+ }
51
+
52
+ function delay(ms) {
53
+ return new Promise((resolve) => {
54
+ self.setTimeout(resolve, ms)
55
+ })
56
+ }
57
+
58
+ function normalizeVectorTilesBaseUrl(url) {
59
+ if (url == null || typeof url !== 'string' || url.trim() === '') {
60
+ return DEFAULT_VECTOR_TILES_BASE_URL
61
+ }
62
+ const trimmed = url.trim().replace(/\/+$/, '')
63
+ try {
64
+ const parsed = new URL(trimmed)
65
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
66
+ throw new Error('invalid protocol')
67
+ }
68
+ return trimmed
69
+ } catch {
70
+ console.warn(
71
+ '[MVX-SW] Invalid vectorTilesBaseUrl, using default:',
72
+ url,
73
+ DEFAULT_VECTOR_TILES_BASE_URL,
74
+ )
75
+ return DEFAULT_VECTOR_TILES_BASE_URL
76
+ }
77
+ }
78
+
79
+ function buildVectorTileUrl(vectorTilesBase, z, x, y, apiKey) {
80
+ const base = vectorTilesBase.replace(/\/+$/, '')
81
+ const params = new URLSearchParams({ key: apiKey })
82
+ return `${base}/${z}/${x}/${y}.pbf?${params.toString()}`
83
+ }
84
+
85
+ function webMercatorXfromLng(lng) {
86
+ return (180 + lng) / 360
87
+ }
88
+
89
+ function webMercatorYfromLat(lat) {
90
+ return (180 - (180 / Math.PI) * Math.log(Math.tan(Math.PI / 4 + (lat * Math.PI) / 360))) / 360
91
+ }
92
+
93
+ function xTileFromMercator(x, zoom) {
94
+ const n = Math.pow(2, zoom)
95
+ return Math.floor(x * n)
96
+ }
97
+
98
+ function yTileFromMercator(y, zoom) {
99
+ const n = Math.pow(2, zoom)
100
+ return Math.floor(y * n)
101
+ }
102
+
103
+ async function getConfiguration(apiKey, placeId) {
104
+ const url = `https://public-api.mapvx.com/api/configuration?token=${apiKey}&place_id=${placeId}`
105
+ try {
106
+ const response = await fetch(url)
107
+ if (response.ok) {
108
+ return response.json()
109
+ }
110
+ console.error('[MVX-SW] Failed to get configuration', url, response.statusText)
111
+ } catch (err) {
112
+ console.error('[MVX-SW] getConfiguration network error', url, err)
113
+ }
114
+ return null
115
+ }
116
+
117
+ function centroidFromPositions(configuration, positions) {
118
+ if (configuration.initialCenter) {
119
+ return configuration.initialCenter
120
+ }
121
+ let lng = 0
122
+ let lat = 0
123
+ for (const p of positions) {
124
+ lng += p.lng
125
+ lat += p.lat
126
+ }
127
+ return { lng: lng / positions.length, lat: lat / positions.length }
128
+ }
129
+
130
+ function tilesAroundCenter(center, zoom, radius) {
131
+ const mx = webMercatorXfromLng(center.lng)
132
+ const my = webMercatorYfromLat(center.lat)
133
+ const cx = xTileFromMercator(mx, zoom)
134
+ const cy = yTileFromMercator(my, zoom)
135
+ const out = []
136
+ for (let dx = -radius; dx <= radius; dx++) {
137
+ for (let dy = -radius; dy <= radius; dy++) {
138
+ out.push({ z: zoom, x: cx + dx, y: cy + dy })
139
+ }
140
+ }
141
+ return out
142
+ }
143
+
144
+ async function fetchAndCacheTile(cache, url, signal) {
145
+ if (signal.aborted) {
146
+ return
147
+ }
148
+ try {
149
+ const response = await fetch(url, { signal })
150
+ if (response.status === 304) {
151
+ return
152
+ }
153
+ if (response.ok) {
154
+ await cache.put(url, response.clone())
155
+ } else {
156
+ console.warn('[MVX-SW] Tile prefetch non-OK', response.status, url.slice(0, 140))
157
+ }
158
+ } catch (err) {
159
+ if (err.name === 'AbortError') {
160
+ return
161
+ }
162
+ console.warn('[MVX-SW] Tile prefetch fetch error', url.slice(0, 100), err)
163
+ }
164
+ }
165
+
166
+ async function prefetchUrlsInBatches(cache, urls, signal) {
167
+ const { maxConcurrent, batchGapMs } = TILE_PREFETCH
168
+ for (let i = 0; i < urls.length; i += maxConcurrent) {
169
+ if (signal.aborted) {
170
+ return
171
+ }
172
+ const slice = urls.slice(i, i + maxConcurrent)
173
+ await Promise.all(slice.map((url) => fetchAndCacheTile(cache, url, signal)))
174
+ if (i + maxConcurrent < urls.length && batchGapMs > 0) {
175
+ await delay(batchGapMs)
176
+ }
177
+ }
178
+ }
179
+
180
+ async function progressivePrefetchTiles(apiKey, placeId, tilesBaseUrl) {
181
+ const trimmedKey = typeof apiKey === 'string' ? apiKey.trim() : ''
182
+ if (trimmedKey === '') {
183
+ console.warn('[MVX-SW] Tile prefetch skipped: empty apiKey')
184
+ return
185
+ }
186
+
187
+ if (activeTilePrefetch) {
188
+ activeTilePrefetch.abort()
189
+ }
190
+ activeTilePrefetch = new AbortController()
191
+ const signal = activeTilePrefetch.signal
192
+
193
+ const vectorTilesBase = normalizeVectorTilesBaseUrl(tilesBaseUrl)
194
+
195
+ console.log('[MVX-SW] Progressive tile prefetch starting', {
196
+ placeId,
197
+ tilesBase: vectorTilesBase,
198
+ })
199
+
200
+ try {
201
+ const configuration = await getConfiguration(trimmedKey, placeId)
202
+ if (!configuration || signal.aborted) {
203
+ return
204
+ }
205
+
206
+ const positions = []
207
+ if (configuration.pointsOfInterest) {
208
+ positions.push(...configuration.pointsOfInterest)
209
+ }
210
+ if (configuration.initialCenter) {
211
+ positions.push(configuration.initialCenter)
212
+ }
213
+ if (positions.length === 0) {
214
+ console.error('[MVX-SW] Tile prefetch: no positions in configuration')
215
+ return
216
+ }
217
+
218
+ const center = centroidFromPositions(configuration, positions)
219
+ const initialZoom = Math.round(configuration.initialZoom ?? 18)
220
+ const minZoom = Math.floor(configuration.limitZoomOut ?? initialZoom - 2)
221
+ const maxZoom = Math.ceil(configuration.limitZoomIn ?? initialZoom + 2)
222
+
223
+ const cache = await caches.open(CACHE_TILES)
224
+ let budget = TILE_PREFETCH.maxTilesPerRun
225
+
226
+ const phaseOne = tilesAroundCenter(center, initialZoom, TILE_PREFETCH.initialRadiusTiles)
227
+ const urlsPhaseOne = phaseOne
228
+ .slice(0, budget)
229
+ .map(({ z, x, y }) => buildVectorTileUrl(vectorTilesBase, z, x, y, trimmedKey))
230
+ budget -= urlsPhaseOne.length
231
+
232
+ console.log('[MVX-SW] Tile prefetch phase 1 (initial zoom only)', {
233
+ initialZoom,
234
+ tiles: urlsPhaseOne.length,
235
+ })
236
+ await prefetchUrlsInBatches(cache, urlsPhaseOne, signal)
237
+
238
+ if (signal.aborted || budget <= 0) {
239
+ console.log('[MVX-SW] Tile prefetch stopped after phase 1')
240
+ return
241
+ }
242
+
243
+ const otherZoomLevels = []
244
+ for (let z = minZoom; z <= maxZoom; z++) {
245
+ if (z !== initialZoom) {
246
+ otherZoomLevels.push(z)
247
+ }
248
+ }
249
+ otherZoomLevels.sort((a, b) => Math.abs(a - initialZoom) - Math.abs(b - initialZoom))
250
+
251
+ for (const zoom of otherZoomLevels) {
252
+ if (signal.aborted || budget <= 0) {
253
+ break
254
+ }
255
+ await delay(TILE_PREFETCH.zoomLevelGapMs)
256
+ if (signal.aborted) {
257
+ break
258
+ }
259
+
260
+ const ring = tilesAroundCenter(center, zoom, TILE_PREFETCH.progressiveRadiusTiles)
261
+ const urls = ring
262
+ .slice(0, budget)
263
+ .map(({ z: zz, x, y }) => buildVectorTileUrl(vectorTilesBase, zz, x, y, trimmedKey))
264
+ budget -= urls.length
265
+
266
+ console.log('[MVX-SW] Tile prefetch background zoom', {
267
+ zoom,
268
+ tiles: urls.length,
269
+ })
270
+ await prefetchUrlsInBatches(cache, urls, signal)
271
+ }
272
+
273
+ console.log('[MVX-SW] Progressive tile prefetch finished')
274
+ } catch (err) {
275
+ if (err.name === 'AbortError') {
276
+ return
277
+ }
278
+ console.error('[MVX-SW] Progressive tile prefetch failed', err)
279
+ }
280
+ }
281
+
282
+ function isValidMessage(data) {
283
+ return (
284
+ data !== null &&
285
+ typeof data === 'object' &&
286
+ typeof data.type === 'string' &&
287
+ data.type.length > 0
288
+ )
289
+ }
290
+
291
+ async function clearAllCaches() {
292
+ const cacheNames = await caches.keys()
293
+ await Promise.all(cacheNames.map((name) => caches.delete(name)))
294
+ }
295
+
296
+ async function resetAndUnregister() {
297
+ try {
298
+ await clearAllCaches()
299
+ } catch (err) {
300
+ console.warn('[MVX-SW] resetAndUnregister: clear caches failed', err)
301
+ }
302
+ try {
303
+ await self.registration.unregister()
304
+ } catch (err) {
305
+ console.warn('[MVX-SW] resetAndUnregister: unregister failed', err)
306
+ }
307
+ try {
308
+ const clients = await self.clients.matchAll({ type: 'window' })
309
+ for (const client of clients) {
310
+ try {
311
+ client.postMessage({ type: 'SW_RESET_DONE' })
312
+ } catch (postErr) {
313
+ console.warn('[MVX-SW] resetAndUnregister: postMessage failed', postErr)
314
+ }
315
+ }
316
+ } catch (err) {
317
+ console.warn('[MVX-SW] resetAndUnregister: matchAll failed', err)
318
+ }
319
+ }
320
+
321
+ function notifyClients(message) {
322
+ self.clients
323
+ .matchAll({ includeUncontrolled: true })
324
+ .then((clients) => {
325
+ clients.forEach((client) => {
326
+ try {
327
+ client.postMessage(message)
328
+ } catch (err) {
329
+ console.warn('[MVX-SW] notifyClients postMessage failed', err)
330
+ }
331
+ })
332
+ })
333
+ .catch((err) => console.warn('[MVX-SW] notifyClients matchAll failed', err))
334
+ }
335
+
336
+ const ALLOWED_HOSTS = [
337
+ 'mapvx.com',
338
+ 'lazarillo.app',
339
+ // GLB models for the 3D scene. The bucket is pinned in
340
+ // `src/config/scene3d.config.ts` (GLB_BASE), so this stays as narrow as the
341
+ // host it actually serves — an `amazonaws.com` suffix would match any bucket.
342
+ 'mapvx-glb-assets.s3.us-east-1.amazonaws.com',
343
+ ]
344
+ const isAllowedHost = (hostname) => ALLOWED_HOSTS.some((host) => hostname.endsWith(host))
345
+
346
+ self.skipWaiting()
347
+ workbox.core.clientsClaim()
348
+ workbox.core.setCacheNameDetails({ prefix: 'mvx' })
349
+ workbox.precaching.cleanupOutdatedCaches()
350
+
351
+ workbox.loadModule('workbox-routing')
352
+ workbox.loadModule('workbox-strategies')
353
+ workbox.loadModule('workbox-cacheable-response')
354
+ workbox.loadModule('workbox-expiration')
355
+
356
+ const { registerRoute } = workbox.routing
357
+ const { CacheFirst, StaleWhileRevalidate, NetworkFirst } = workbox.strategies
358
+ const { CacheableResponsePlugin } = workbox.cacheableResponse
359
+ const { ExpirationPlugin } = workbox.expiration
360
+
361
+ registerRoute(
362
+ ({ url }) => url.hostname === 'public-api.mapvx.com' && url.pathname.includes('/place'),
363
+ new StaleWhileRevalidate({
364
+ cacheName: CACHE_API_PLACES,
365
+ plugins: [
366
+ new CacheableResponsePlugin({ statuses: [0, 200, 204] }),
367
+ new ExpirationPlugin({
368
+ maxEntries: 500,
369
+ maxAgeSeconds: 60 * 60 * 24 * 7,
370
+ purgeOnQuotaError: true,
371
+ }),
372
+ ],
373
+ }),
374
+ )
375
+
376
+ registerRoute(
377
+ ({ url }) => url.hostname === 'public-api.mapvx.com' && url.pathname.includes('/configuration'),
378
+ new StaleWhileRevalidate({
379
+ cacheName: CACHE_API_CONFIG,
380
+ plugins: [
381
+ new CacheableResponsePlugin({ statuses: [0, 200, 204] }),
382
+ new ExpirationPlugin({
383
+ maxEntries: 100,
384
+ maxAgeSeconds: 60 * 60 * 24,
385
+ purgeOnQuotaError: true,
386
+ }),
387
+ ],
388
+ }),
389
+ )
390
+
391
+ registerRoute(
392
+ ({ url }) => url.hostname === 'api.mapvx.com' && url.pathname.includes('/route'),
393
+ new NetworkFirst({
394
+ cacheName: CACHE_API_ROUTES,
395
+ networkTimeoutSeconds: 10,
396
+ plugins: [
397
+ new CacheableResponsePlugin({ statuses: [0, 200, 204] }),
398
+ new ExpirationPlugin({
399
+ maxEntries: 200,
400
+ maxAgeSeconds: 60 * 60 * 24,
401
+ purgeOnQuotaError: true,
402
+ }),
403
+ ],
404
+ }),
405
+ )
406
+
407
+ registerRoute(
408
+ ({ url }) => url.hostname === 'api.mapvx.com' || url.hostname === 'public-api.mapvx.com',
409
+ new NetworkFirst({
410
+ cacheName: CACHE_API_FALLBACK,
411
+ networkTimeoutSeconds: 10,
412
+ plugins: [
413
+ new CacheableResponsePlugin({ statuses: [0, 200, 204] }),
414
+ new ExpirationPlugin({
415
+ maxEntries: 500,
416
+ maxAgeSeconds: 60 * 60,
417
+ purgeOnQuotaError: true,
418
+ }),
419
+ ],
420
+ }),
421
+ )
422
+
423
+ // Third-party code the map pulls from unpkg at runtime. Neither entry is ours to
424
+ // remove from here — that belongs upstream — but both URLs are version-pinned and
425
+ // immutable, so caching them keeps a totem that has been online once working
426
+ // after it loses connectivity.
427
+ //
428
+ // `@loaders.gl` is deck.gl's MVT worker, loaded when the 3D scene is on. Verified
429
+ // intercepted: a `new Worker()` script load from a controlled page goes through
430
+ // the service worker.
431
+ //
432
+ // `mapbox-gl-rtl-text` is best-effort. The SDK registers it through maplibre,
433
+ // which fetches it from inside the maplibre worker, and a fetch issued there is
434
+ // not guaranteed to reach us. It was never observed in the page's network log
435
+ // during testing, so treat this entry as a no-op until proven otherwise.
436
+ const VENDOR_CDN_PATHS = ['/@loaders.gl/', '/@mapbox/mapbox-gl-rtl-text']
437
+
438
+ registerRoute(
439
+ ({ url }) =>
440
+ url.hostname === 'unpkg.com' && VENDOR_CDN_PATHS.some((path) => url.pathname.includes(path)),
441
+ new CacheFirst({
442
+ cacheName: CACHE_VENDOR,
443
+ plugins: [
444
+ new CacheableResponsePlugin({ statuses: [0, 200, 204] }),
445
+ new ExpirationPlugin({
446
+ maxEntries: 20,
447
+ maxAgeSeconds: 60 * 60 * 24 * 30,
448
+ purgeOnQuotaError: true,
449
+ }),
450
+ ],
451
+ }),
452
+ )
453
+
454
+ // Glyph ranges for map labels. The basemap style decides the host (MapTiler
455
+ // today), so match on the path the way the tile route does instead of
456
+ // allowlisting one provider. A range is immutable for a given font stack, hence
457
+ // CacheFirst and a long TTL. Without this every label disappears as soon as a
458
+ // totem loses connectivity, because `/fonts/` matches neither the tile route
459
+ // (no `/tiles/` in the path) nor the asset route (host not allowlisted).
460
+ registerRoute(
461
+ ({ url }) => url.pathname.includes('/fonts/') && /\.pbf($|\?)/.test(url.pathname),
462
+ new CacheFirst({
463
+ cacheName: CACHE_GLYPHS,
464
+ plugins: [
465
+ new CacheableResponsePlugin({ statuses: [0, 200, 204] }),
466
+ new ExpirationPlugin({
467
+ maxEntries: 500,
468
+ maxAgeSeconds: 60 * 60 * 24 * 30,
469
+ purgeOnQuotaError: true,
470
+ }),
471
+ ],
472
+ }),
473
+ )
474
+
475
+ registerRoute(
476
+ ({ url }) =>
477
+ (isAllowedHost(url.hostname) && /\.pbf($|\?)/.test(url.pathname)) ||
478
+ url.pathname.includes('/tiles/'),
479
+ new CacheFirst({
480
+ cacheName: CACHE_TILES,
481
+ plugins: [
482
+ new CacheableResponsePlugin({ statuses: [0, 200, 204] }),
483
+ new ExpirationPlugin({
484
+ maxEntries: 10000,
485
+ maxAgeSeconds: 60 * 60 * 24,
486
+ purgeOnQuotaError: true,
487
+ }),
488
+ ],
489
+ }),
490
+ )
491
+
492
+ registerRoute(
493
+ ({ url }) => isAllowedHost(url.hostname) && !/\.pbf($|\?)/.test(url.pathname),
494
+ new StaleWhileRevalidate({
495
+ cacheName: CACHE_MAP_ASSETS,
496
+ plugins: [
497
+ new CacheableResponsePlugin({ statuses: [0, 200, 204] }),
498
+ new ExpirationPlugin({
499
+ maxEntries: 1000,
500
+ maxAgeSeconds: 60 * 60 * 24 * 7,
501
+ purgeOnQuotaError: true,
502
+ }),
503
+ ],
504
+ }),
505
+ )
506
+
507
+ self.addEventListener('activate', (event) => {
508
+ event.waitUntil(
509
+ (async () => {
510
+ try {
511
+ const cacheNames = await caches.keys()
512
+ await Promise.all(
513
+ cacheNames.map((name) => {
514
+ if (
515
+ !KNOWN_RUNTIME_CACHES.has(name) &&
516
+ !name.startsWith('workbox-precache') &&
517
+ !name.startsWith('mvx-precache') &&
518
+ !name.startsWith('mvx-')
519
+ ) {
520
+ console.log('[MVX-SW] Dropping legacy cache', name)
521
+ return caches.delete(name)
522
+ }
523
+ return Promise.resolve(false)
524
+ }),
525
+ )
526
+ } catch (err) {
527
+ console.warn('[MVX-SW] Activate cleanup failed', err)
528
+ }
529
+ })(),
530
+ )
531
+ })
532
+
533
+ self.addEventListener('message', (event) => {
534
+ if (!isValidMessage(event.data)) return
535
+
536
+ switch (event.data.type) {
537
+ case 'INITIAL_TILE_LOAD':
538
+ case 'REFRESH_TILES': {
539
+ const { apiKey, placeId, tilesBaseUrl } = event.data
540
+ if (typeof apiKey !== 'string' || typeof placeId !== 'string') {
541
+ console.warn('[MVX-SW]', event.data.type, 'missing apiKey or placeId')
542
+ return
543
+ }
544
+ event.waitUntil(
545
+ progressivePrefetchTiles(apiKey, placeId, tilesBaseUrl).catch((err) =>
546
+ console.error('[MVX-SW]', event.data.type, 'failed', err),
547
+ ),
548
+ )
549
+ break
550
+ }
551
+
552
+ case 'CLEAR_TILE_CACHE':
553
+ event.waitUntil(
554
+ caches
555
+ .delete(CACHE_TILES)
556
+ .then(() => {
557
+ console.log('[MVX-SW] Tile cache cleared')
558
+ notifyClients({ type: 'TILE_CACHE_CLEARED' })
559
+ })
560
+ .catch((err) => console.error('[MVX-SW] CLEAR_TILE_CACHE failed', err)),
561
+ )
562
+ break
563
+
564
+ case 'CLEAR_ALL_CACHES':
565
+ event.waitUntil(
566
+ clearAllCaches()
567
+ .then(() => {
568
+ console.log('[MVX-SW] All caches cleared')
569
+ notifyClients({ type: 'ALL_CACHES_CLEARED' })
570
+ })
571
+ .catch((err) => console.error('[MVX-SW] CLEAR_ALL_CACHES failed', err)),
572
+ )
573
+ break
574
+
575
+ case 'RESET_AND_UNREGISTER':
576
+ event.waitUntil(resetAndUnregister())
577
+ break
578
+
579
+ default:
580
+ break
581
+ }
582
+ })