offline mode

This commit is contained in:
cpu
2025-05-15 00:11:42 +02:00
parent 19fbae06fc
commit 6e11db9fb9

View File

@@ -1,21 +1,18 @@
// This global constant __APP_CACHE_VERSION__ will be replaced by Vite
// during the build process due to the `define` config in vite.config.js.
const CACHE_VERSION = typeof __APP_CACHE_VERSION__ !== 'undefined' const CACHE_VERSION = typeof __APP_CACHE_VERSION__ !== 'undefined'
? __APP_CACHE_VERSION__ ? __APP_CACHE_VERSION__
: 'nexus-timer-cache-fallback-dev-vManual'; // Fallback for dev or if define fails : 'nexus-timer-cache-fallback-dev-vManual';
const APP_SHELL_URLS = [ const APP_SHELL_URLS = [
// Note: '/' (index.html) is handled by NetworkFirst strategy, no need to precache explicitly here. // Precache the root (index.html) explicitly for better offline fallback
'/manifest.json', // Will be served from public, copied to dist root '/',
'/favicon.ico', // Will be served from public, copied to dist root '/manifest.json',
// Icons from public/icons, will be copied to dist/icons '/favicon.ico',
'/icons/icon-192x192.png', '/icons/icon-192x192.png',
'/icons/icon-512x512.png', '/icons/icon-512x512.png',
'/icons/maskable-icon-192x192.png', '/icons/maskable-icon-192x192.png',
'/icons/maskable-icon-512x512.png', '/icons/maskable-icon-512x512.png',
'/icons/shortcut-setup-96x96.png', '/icons/shortcut-setup-96x96.png',
'/icons/shortcut-info-96x96.png', '/icons/shortcut-info-96x96.png',
// Any other critical static assets from the public folder that should be part of the app shell
]; ];
self.addEventListener('install', event => { self.addEventListener('install', event => {
@@ -57,47 +54,68 @@ self.addEventListener('fetch', event => {
const url = new URL(request.url); const url = new URL(request.url);
if (request.method !== 'GET' || !url.protocol.startsWith('http')) { if (request.method !== 'GET' || !url.protocol.startsWith('http')) {
// console.log(`[SW ${CACHE_VERSION}] Ignoring non-GET or non-http(s) request: ${request.url}`);
return; return;
} }
// Strategy 1: Network First for HTML (navigations or direct / request) // Strategy 1: Network First, then Cache for Navigation/HTML requests
if (request.mode === 'navigate' || (request.destination === 'document' || url.pathname === '/')) { if (request.mode === 'navigate' || request.destination === 'document' || url.pathname === '/') {
// console.log(`[SW ${CACHE_VERSION}] NetworkFirst for: ${request.url}`); // console.log(`[SW ${CACHE_VERSION}] NetworkFirst for navigation/document: ${request.url}`);
event.respondWith( event.respondWith(
fetch(request) fetch(request)
.then(response => { .then(networkResponse => {
if (response.ok) { // If successful, cache the response and return it
const responseClone = response.clone(); if (networkResponse.ok) {
caches.open(CACHE_VERSION).then(cache => cache.put(request, responseClone)); const responseToCache = networkResponse.clone();
caches.open(CACHE_VERSION).then(cache => {
// For navigations, it's often best to cache the specific URL requested
// as well as potentially updating the '/' cache if this is the root.
cache.put(request, responseToCache);
if (url.pathname === '/') { // Also update root cache if it's the index
const rootResponseClone = networkResponse.clone(); // Need another clone
cache.put('/', rootResponseClone);
}
});
} }
return response; return networkResponse;
}) })
.catch(async () => { .catch(async () => {
// console.warn(`[SW ${CACHE_VERSION}] Network fetch failed for ${request.url}, trying cache.`); // Network failed. Try to serve from cache.
// console.warn(`[SW ${CACHE_VERSION}] Network fetch failed for ${request.url}. Attempting cache.`);
// 1. Try matching the specific request first (e.g. /info, /game)
const cachedResponse = await caches.match(request); const cachedResponse = await caches.match(request);
if (cachedResponse) return cachedResponse; if (cachedResponse) {
// Fallback to root /index.html from cache if specific page not found offline // console.log(`[SW ${CACHE_VERSION}] Serving from cache (specific request): ${request.url}`);
const rootCache = await caches.match('/'); return cachedResponse;
if (rootCache) return rootCache; }
// console.error(`[SW ${CACHE_VERSION}] Network and cache miss for navigation: ${request.url}`);
return new Response('Network error: You are offline and this page is not cached.', { // 2. If specific request not found, try serving the app shell ('/')
status: 404, // This is crucial for SPAs to work offline.
statusText: 'Not Found', const appShellResponse = await caches.match('/');
headers: { 'Content-Type': 'text/html' } // Important for SPA offline fallback if (appShellResponse) {
}); // console.log(`[SW ${CACHE_VERSION}] Serving app shell ('/') from cache for: ${request.url}`);
return appShellResponse;
}
// 3. If even the app shell is not in cache (shouldn't happen if install was successful)
console.error(`[SW ${CACHE_VERSION}] CRITICAL: Network and cache miss for navigation AND app shell ('/') for: ${request.url}`);
// Return a very basic offline message, but ideally this state is avoided.
return new Response(
`<h1>Offline</h1><p>The application is currently offline and the requested page could not be loaded from the cache. Please check your connection.</p>`,
{ headers: { 'Content-Type': 'text/html' } }
);
}) })
); );
return; return;
} }
// Strategy 2: Stale-While-Revalidate for assets (CSS, JS, images, fonts) // Strategy 2: Stale-While-Revalidate for assets (CSS, JS, images, fonts, workers)
if (request.destination === 'style' || if (request.destination === 'style' ||
request.destination === 'script' || request.destination === 'script' ||
request.destination === 'worker' || request.destination === 'worker' ||
request.destination === 'image' || request.destination === 'image' ||
request.destination === 'font') { request.destination === 'font') {
// console.log(`[SW ${CACHE_VERSION}] StaleWhileRevalidate for: ${request.url}`); // console.log(`[SW ${CACHE_VERSION}] StaleWhileRevalidate for asset: ${request.url}`);
event.respondWith( event.respondWith(
caches.open(CACHE_VERSION).then(cache => { caches.open(CACHE_VERSION).then(cache => {
return cache.match(request).then(cachedResponse => { return cache.match(request).then(cachedResponse => {
@@ -105,21 +123,19 @@ self.addEventListener('fetch', event => {
if (networkResponse.ok) { if (networkResponse.ok) {
const responseToCache = networkResponse.clone(); const responseToCache = networkResponse.clone();
cache.put(request, responseToCache); cache.put(request, responseToCache);
} else {
// console.warn(`[SW ${CACHE_VERSION}] StaleWhileRevalidate: Network fetch for ${request.url} failed with status ${networkResponse.status}`);
} }
return networkResponse; return networkResponse;
}).catch(err => { }).catch(err => {
// console.warn(`[SW ${CACHE_VERSION}] StaleWhileRevalidate: Network fetch error for ${request.url}:`, err); // If fetch fails, and we served from cache, it's fine.
// If fetch fails, and we already served from cache, that's okay. // If cache also missed, this error will propagate.
// If cache also missed (i.e., cachedResponse was null), then this error will propagate. // console.warn(`[SW ${CACHE_VERSION}] SWR: Network fetch error for ${request.url}`, err);
throw err; throw err;
}); });
return cachedResponse || fetchPromise; return cachedResponse || fetchPromise;
}).catch(err => { }).catch(() => {
// This catch block handles errors from cache.match() or if fetchPromise was returned and rejected // Fallback to network if cache.match fails
// console.error(`[SW ${CACHE_VERSION}] StaleWhileRevalidate: Error for ${request.url}. Trying network fallback.`, err); // console.warn(`[SW ${CACHE_VERSION}] SWR: Cache match error for ${request.url}, trying network directly.`);
return fetch(request); // Final fallback to network if cache interactions fail return fetch(request);
}); });
}) })
); );