@mapvx/web-kit 0.0.0-stage → 1.2.0-develop.10
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.
- package/LICENSE +17 -0
- package/README.md +187 -2
- package/dist/cjs/chunks/DRACOLoader-BatVMFjv.cjs +2 -0
- package/dist/cjs/chunks/GLTFLoader-DPzCuhq_.cjs +2 -0
- package/dist/cjs/chunks/KTX2Loader-D3UkVYrQ.cjs +2 -0
- package/dist/cjs/chunks/accessibility-DSrJvXsP.cjs +2 -0
- package/dist/cjs/chunks/back_arrow-DDucL2vy.cjs +2 -0
- package/dist/cjs/chunks/debug-hooks-6E-6XyZt.cjs +2 -0
- package/dist/cjs/chunks/dist-Bn3kS9il.cjs +2 -0
- package/dist/cjs/chunks/dist-C7Xq932f.cjs +2 -0
- package/dist/cjs/chunks/dist-CyU0uhT_.cjs +2 -0
- package/dist/cjs/chunks/expression-YhbojQDk.cjs +2 -0
- package/dist/cjs/chunks/globe-viewport-L8jhK17I.cjs +2 -0
- package/dist/cjs/chunks/icon-origin-point-Fx1ItO6l.cjs +2 -0
- package/dist/cjs/chunks/meshopt_decoder.module-CfZgR5oC.cjs +2 -0
- package/dist/cjs/chunks/repeat_icon-DTL5IJer.cjs +2 -0
- package/dist/cjs/chunks/rolldown-runtime-X-Z9OGZI.cjs +2 -0
- package/dist/cjs/chunks/shader-type-decoder-CM9es9dC.cjs +2 -0
- package/dist/cjs/chunks/styles-DSsNALyl.cjs +2 -0
- package/dist/cjs/chunks/three.module-DrkjviT-.cjs +2 -0
- package/dist/cjs/chunks/walking-DH57xlEw.cjs +2 -0
- package/dist/cjs/chunks/webgl-CmQcn-HM.cjs +2 -0
- package/dist/cjs/chunks/webgl-device-Bjj2DuDc.cjs +2 -0
- package/dist/cjs/web-kit.cjs +425 -0
- package/dist/es/chunks/DRACOLoader-Coaeymvd.js +2 -0
- package/dist/es/chunks/GLTFLoader-Cg9W7uGp.js +2 -0
- package/dist/es/chunks/KTX2Loader-CW7QOwqk.js +2 -0
- package/dist/es/chunks/accessibility-CY2Znzrd.js +2 -0
- package/dist/es/chunks/back_arrow-DXxWd53C.js +2 -0
- package/dist/es/chunks/debug-hooks-CttUbwx0.js +2 -0
- package/dist/es/chunks/dist-DrDbx8fV.js +2 -0
- package/dist/es/chunks/dist-Dzp0YWIR.js +4 -0
- package/dist/es/chunks/dist-TkIGGp-O.js +2 -0
- package/dist/es/chunks/expression-DK4fcsrz.js +2 -0
- package/dist/es/chunks/globe-viewport-CmXSxt1e.js +2 -0
- package/dist/es/chunks/icon-origin-point-CsdwEebb.js +2 -0
- package/dist/es/chunks/meshopt_decoder.module-CymL9NxH.js +2 -0
- package/dist/es/chunks/repeat_icon-VI8TF8gY.js +2 -0
- package/dist/es/chunks/rolldown-runtime-DD4WNi4o.js +2 -0
- package/dist/es/chunks/shader-type-decoder-aS2S_0ef.js +2 -0
- package/dist/es/chunks/styles-CT7CDFlr.js +2 -0
- package/dist/es/chunks/three.module-BeGwQoXi.js +22 -0
- package/dist/es/chunks/walking-DJUq61cT.js +2 -0
- package/dist/es/chunks/webgl-Cm54xPsy.js +2 -0
- package/dist/es/chunks/webgl-device-Cmfm4mkc.js +2 -0
- package/dist/es/web-kit.mjs +428 -0
- package/dist/iife/web-kit.iife.js +425 -0
- package/dist/sw/mvx-tiles-sw.js +582 -0
- package/dist/types/index.d.ts +1765 -0
- 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
|
+
})
|