Files
nexus-timer/public/service-worker.js
2025-05-09 17:03:37 +02:00

154 lines
5.7 KiB
JavaScript

const CACHE_VERSION = 'nexus-timer-cache-v8';
const APP_SHELL_URLS = [
// '/', // Let NetworkFirst handle '/'
'/manifest.json',
'/favicon.ico',
'/icons/icon-192x192.png',
'/icons/icon-512x512.png',
'/icons/maskable-icon-192x192.png',
'/icons/maskable-icon-512x512.png',
'/icons/shortcut-setup-96x96.png',
'/icons/shortcut-info-96x96.png',
// Add Vite output paths? Usually hard due to hashing. Rely on dynamic caching.
];
self.addEventListener('install', event => {
console.log(`[SW ${CACHE_VERSION}] Install`);
event.waitUntil(
caches.open(CACHE_VERSION)
.then(cache => {
console.log(`[SW ${CACHE_VERSION}] Caching app shell essentials`);
return cache.addAll(APP_SHELL_URLS); // Cache core static assets
})
.then(() => self.skipWaiting())
);
});
self.addEventListener('activate', event => {
console.log(`[SW ${CACHE_VERSION}] Activate`);
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames.map(cacheName => {
if (cacheName !== CACHE_VERSION) {
console.log(`[SW ${CACHE_VERSION}] Deleting old cache: ${cacheName}`);
return caches.delete(cacheName);
}
})
);
}).then(() => self.clients.claim())
);
});
self.addEventListener('fetch', event => {
const { request } = event;
const url = new URL(request.url);
// Ignore non-GET requests & non-http protocols
if (request.method !== 'GET' || !url.protocol.startsWith('http')) {
return;
}
// --- Strategy 1: Network First for HTML ---
if (request.mode === 'navigate' || (request.destination === 'document' || url.pathname === '/')) {
event.respondWith(
fetch(request)
.then(response => {
// If fetch is successful, cache it and return it
if (response.ok) {
const responseClone = response.clone();
caches.open(CACHE_VERSION).then(cache => cache.put(request, responseClone));
}
return response;
})
.catch(async () => {
// If fetch fails, try cache
const cachedResponse = await caches.match(request);
if (cachedResponse) {
return cachedResponse;
}
// If specific request not in cache, try root '/' as SPA fallback
const rootCache = await caches.match('/');
if (rootCache) {
return rootCache;
}
// Optional: return a proper offline fallback page if available
// return caches.match('/offline.html');
// Or just let the browser show its offline error
return new Response('Network error and no cache found.', { status: 404, statusText: 'Not Found' });
})
);
return;
}
// --- Strategy 2: Stale-While-Revalidate for assets ---
if (request.destination === 'style' ||
request.destination === 'script' ||
request.destination === 'worker' ||
request.destination === 'image' ||
request.destination === 'font') {
event.respondWith(
caches.open(CACHE_VERSION).then(cache => { // Open cache first
return cache.match(request).then(cachedResponse => {
// Fetch in parallel (don't wait for it here)
const fetchPromise = fetch(request).then(networkResponse => {
// Check if fetch was successful
if (networkResponse.ok) {
// Clone before caching
const responseToCache = networkResponse.clone();
// Update the cache with the network response
cache.put(request, responseToCache);
} else {
console.warn(`[SW ${CACHE_VERSION}] Fetch for ${request.url} failed with status ${networkResponse.status}`);
}
// Return the original network response for the fetch promise resolution
// This isn't directly used for the *initial* response unless cache misses.
return networkResponse;
}).catch(err => {
console.warn(`[SW ${CACHE_VERSION}] Fetch error for ${request.url}:`, err);
// If fetch fails, we just rely on the cache if it existed.
// Re-throw error so if cache also missed, browser knows resource failed.
throw err;
});
// Return the cached response immediately if available.
// If not cached, this strategy requires waiting for the fetch.
// The common implementation returns cache THEN fetches, but if cache misses,
// the user waits for the network. Let's return fetchPromise if no cache.
return cachedResponse || fetchPromise;
}).catch(err => {
// Error during cache match or subsequent fetch
console.error(`[SW ${CACHE_VERSION}] Error handling fetch for ${request.url}:`, err);
// Fallback to network just in case cache interaction failed? Or let browser handle.
// Let's try network as a last resort if cache fails.
return fetch(request);
});
})
);
return;
}
// --- Strategy 3: Cache First for others ---
// (e.g., manifest, potentially icons not pre-cached)
event.respondWith(
caches.match(request)
.then(response => {
return response || fetch(request).then(networkResponse => {
if(networkResponse.ok) {
const responseClone = networkResponse.clone();
caches.open(CACHE_VERSION).then(cache => cache.put(request, responseClone));
}
return networkResponse;
});
})
);
});
// Listener for skipWaiting message
self.addEventListener('message', event => {
if (event.data && event.data.action === 'skipWaiting') {
console.log('[SW] Received skipWaiting message, activating new SW.');
self.skipWaiting();
}
});