154 lines
5.7 KiB
JavaScript
154 lines
5.7 KiB
JavaScript
const CACHE_VERSION = 'nexus-timer-cache-v6';
|
|
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();
|
|
}
|
|
}); |