188 lines
6.1 KiB
JavaScript
188 lines
6.1 KiB
JavaScript
const TILE_CACHE = 'lageplan-tiles-v3'
|
|
const STATIC_CACHE = 'lageplan-static-v3'
|
|
const APP_CACHE = 'lageplan-app-v3'
|
|
const API_CACHE = 'lageplan-api-v3'
|
|
|
|
// API routes that should be cached for offline use
|
|
const CACHEABLE_API = ['/api/icons', '/api/hose-types', '/api/dictionary']
|
|
|
|
// Pre-cache essential app shell on install
|
|
self.addEventListener('install', (event) => {
|
|
event.waitUntil(
|
|
caches.open(APP_CACHE).then((cache) =>
|
|
cache.addAll([
|
|
'/app',
|
|
'/login',
|
|
'/',
|
|
'/logo.svg',
|
|
'/logo-icon.png',
|
|
'/manifest.json',
|
|
]).catch(() => {})
|
|
)
|
|
)
|
|
self.skipWaiting()
|
|
})
|
|
|
|
self.addEventListener('fetch', (event) => {
|
|
const url = event.request.url
|
|
const { pathname } = new URL(url)
|
|
|
|
// Skip non-GET requests
|
|
if (event.request.method !== 'GET') return
|
|
|
|
// Never intercept Socket.IO — let it pass through directly
|
|
if (pathname.startsWith('/socket.io')) return
|
|
|
|
// Cacheable API routes: Network First with cache fallback (icons, hose-types, dictionary)
|
|
if (CACHEABLE_API.some(p => pathname.startsWith(p))) {
|
|
event.respondWith(
|
|
caches.open(API_CACHE).then((cache) =>
|
|
fetch(event.request).then((response) => {
|
|
if (response.ok) cache.put(event.request, response.clone())
|
|
return response
|
|
}).catch(() =>
|
|
cache.match(event.request).then((cached) => cached || new Response('{"error":"offline"}', {
|
|
status: 503, headers: { 'Content-Type': 'application/json' }
|
|
}))
|
|
)
|
|
)
|
|
)
|
|
return
|
|
}
|
|
|
|
// Projects API: Network First with cache fallback
|
|
if (pathname === '/api/projects' || pathname.match(/^\/api\/projects\/[^/]+$/)) {
|
|
event.respondWith(
|
|
caches.open(API_CACHE).then((cache) =>
|
|
fetch(event.request).then((response) => {
|
|
if (response.ok) cache.put(event.request, response.clone())
|
|
return response
|
|
}).catch(() =>
|
|
cache.match(event.request).then((cached) => cached || new Response('{"error":"offline"}', {
|
|
status: 503, headers: { 'Content-Type': 'application/json' }
|
|
}))
|
|
)
|
|
)
|
|
)
|
|
return
|
|
}
|
|
|
|
// Features API: Network First with cache fallback
|
|
if (pathname.match(/^\/api\/projects\/[^/]+\/features$/)) {
|
|
event.respondWith(
|
|
caches.open(API_CACHE).then((cache) =>
|
|
fetch(event.request).then((response) => {
|
|
if (response.ok) cache.put(event.request, response.clone())
|
|
return response
|
|
}).catch(() =>
|
|
cache.match(event.request).then((cached) => cached || new Response('{"error":"offline"}', {
|
|
status: 503, headers: { 'Content-Type': 'application/json' }
|
|
}))
|
|
)
|
|
)
|
|
)
|
|
return
|
|
}
|
|
|
|
// Other API requests: network only
|
|
if (pathname.startsWith('/api/')) return
|
|
|
|
// Cache map tiles from OSM / MapTiler / ArcGIS / Swisstopo (Cache First — tiles don't change)
|
|
if (url.includes('tile.openstreetmap.org') || url.includes('api.maptiler.com') || url.includes('server.arcgisonline.com') || url.includes('geo.admin.ch')) {
|
|
event.respondWith(
|
|
caches.open(TILE_CACHE).then((cache) =>
|
|
cache.match(event.request).then((cached) => {
|
|
if (cached) return cached
|
|
return fetch(event.request).then((response) => {
|
|
if (response.ok) cache.put(event.request, response.clone())
|
|
return response
|
|
}).catch(() => new Response('', { status: 503 }))
|
|
})
|
|
)
|
|
)
|
|
return
|
|
}
|
|
|
|
// Next.js build chunks (_next/static): Cache First (hashed filenames = immutable)
|
|
if (pathname.startsWith('/_next/static/')) {
|
|
event.respondWith(
|
|
caches.open(STATIC_CACHE).then((cache) =>
|
|
cache.match(event.request).then((cached) => {
|
|
if (cached) return cached
|
|
return fetch(event.request).then((response) => {
|
|
if (response.ok) cache.put(event.request, response.clone())
|
|
return response
|
|
}).catch(() => new Response('', { status: 503 }))
|
|
})
|
|
)
|
|
)
|
|
return
|
|
}
|
|
|
|
// Other static assets (JS, CSS, images, fonts): Stale While Revalidate
|
|
if (pathname.match(/\.(js|css|png|jpg|jpeg|svg|ico|woff2?)$/)) {
|
|
event.respondWith(
|
|
caches.open(STATIC_CACHE).then((cache) =>
|
|
cache.match(event.request).then((cached) => {
|
|
const fetchPromise = fetch(event.request).then((response) => {
|
|
if (response.ok) cache.put(event.request, response.clone())
|
|
return response
|
|
}).catch(() => cached || new Response('', { status: 503 }))
|
|
return cached || fetchPromise
|
|
})
|
|
)
|
|
)
|
|
return
|
|
}
|
|
|
|
// App pages / navigation: Network First with cache fallback
|
|
if (event.request.mode === 'navigate' || pathname === '/app' || pathname === '/' || pathname.startsWith('/app')) {
|
|
event.respondWith(
|
|
fetch(event.request).then((response) => {
|
|
if (response.ok) {
|
|
const clone = response.clone()
|
|
caches.open(APP_CACHE).then((cache) => cache.put(event.request, clone))
|
|
}
|
|
return response
|
|
}).catch(() =>
|
|
caches.match(event.request).then((cached) => cached || caches.match('/app'))
|
|
)
|
|
)
|
|
return
|
|
}
|
|
})
|
|
|
|
// Clean old caches on activation
|
|
self.addEventListener('activate', (event) => {
|
|
const currentCaches = [TILE_CACHE, STATIC_CACHE, APP_CACHE, API_CACHE]
|
|
event.waitUntil(
|
|
caches.keys().then((keys) =>
|
|
Promise.all(
|
|
keys
|
|
.filter((k) => !currentCaches.includes(k))
|
|
.map((k) => caches.delete(k))
|
|
)
|
|
).then(() => self.clients.claim())
|
|
)
|
|
})
|
|
|
|
// Listen for sync events (Background Sync for queued saves)
|
|
self.addEventListener('sync', (event) => {
|
|
if (event.tag === 'sync-saves') {
|
|
event.waitUntil(syncQueuedSaves())
|
|
}
|
|
})
|
|
|
|
// Process queued saves from IndexedDB/localStorage
|
|
async function syncQueuedSaves() {
|
|
try {
|
|
const clients = await self.clients.matchAll()
|
|
clients.forEach(client => client.postMessage({ type: 'SYNC_START' }))
|
|
|
|
// Read queue from a BroadcastChannel or let the main thread handle it
|
|
clients.forEach(client => client.postMessage({ type: 'FLUSH_SYNC_QUEUE' }))
|
|
} catch (e) {
|
|
console.error('[SW] Sync error:', e)
|
|
}
|
|
}
|