Files
Lageplan/public/sw.js

185 lines
5.9 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
// 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 OpenStreetMap / MapTiler (Cache First — tiles don't change)
if (url.includes('tile.openstreetmap.org') || url.includes('api.maptiler.com')) {
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)
}
}