offline mode
This commit is contained in:
94
src/sw.js
94
src/sw.js
@@ -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'
|
||||
? __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 = [
|
||||
// Note: '/' (index.html) is handled by NetworkFirst strategy, no need to precache explicitly here.
|
||||
'/manifest.json', // Will be served from public, copied to dist root
|
||||
'/favicon.ico', // Will be served from public, copied to dist root
|
||||
// Icons from public/icons, will be copied to dist/icons
|
||||
// Precache the root (index.html) explicitly for better offline fallback
|
||||
'/',
|
||||
'/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',
|
||||
// Any other critical static assets from the public folder that should be part of the app shell
|
||||
];
|
||||
|
||||
self.addEventListener('install', event => {
|
||||
@@ -57,47 +54,68 @@ self.addEventListener('fetch', event => {
|
||||
const url = new URL(request.url);
|
||||
|
||||
if (request.method !== 'GET' || !url.protocol.startsWith('http')) {
|
||||
// console.log(`[SW ${CACHE_VERSION}] Ignoring non-GET or non-http(s) request: ${request.url}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Strategy 1: Network First for HTML (navigations or direct / request)
|
||||
if (request.mode === 'navigate' || (request.destination === 'document' || url.pathname === '/')) {
|
||||
// console.log(`[SW ${CACHE_VERSION}] NetworkFirst for: ${request.url}`);
|
||||
// Strategy 1: Network First, then Cache for Navigation/HTML requests
|
||||
if (request.mode === 'navigate' || request.destination === 'document' || url.pathname === '/') {
|
||||
// console.log(`[SW ${CACHE_VERSION}] NetworkFirst for navigation/document: ${request.url}`);
|
||||
event.respondWith(
|
||||
fetch(request)
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
const responseClone = response.clone();
|
||||
caches.open(CACHE_VERSION).then(cache => cache.put(request, responseClone));
|
||||
.then(networkResponse => {
|
||||
// If successful, cache the response and return it
|
||||
if (networkResponse.ok) {
|
||||
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 () => {
|
||||
// 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);
|
||||
if (cachedResponse) return cachedResponse;
|
||||
// Fallback to root /index.html from cache if specific page not found offline
|
||||
const rootCache = await caches.match('/');
|
||||
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.', {
|
||||
status: 404,
|
||||
statusText: 'Not Found',
|
||||
headers: { 'Content-Type': 'text/html' } // Important for SPA offline fallback
|
||||
});
|
||||
if (cachedResponse) {
|
||||
// console.log(`[SW ${CACHE_VERSION}] Serving from cache (specific request): ${request.url}`);
|
||||
return cachedResponse;
|
||||
}
|
||||
|
||||
// 2. If specific request not found, try serving the app shell ('/')
|
||||
// This is crucial for SPAs to work offline.
|
||||
const appShellResponse = await caches.match('/');
|
||||
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;
|
||||
}
|
||||
|
||||
// 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' ||
|
||||
request.destination === 'script' ||
|
||||
request.destination === 'worker' ||
|
||||
request.destination === 'image' ||
|
||||
request.destination === 'font') {
|
||||
// console.log(`[SW ${CACHE_VERSION}] StaleWhileRevalidate for: ${request.url}`);
|
||||
// console.log(`[SW ${CACHE_VERSION}] StaleWhileRevalidate for asset: ${request.url}`);
|
||||
event.respondWith(
|
||||
caches.open(CACHE_VERSION).then(cache => {
|
||||
return cache.match(request).then(cachedResponse => {
|
||||
@@ -105,21 +123,19 @@ self.addEventListener('fetch', event => {
|
||||
if (networkResponse.ok) {
|
||||
const responseToCache = networkResponse.clone();
|
||||
cache.put(request, responseToCache);
|
||||
} else {
|
||||
// console.warn(`[SW ${CACHE_VERSION}] StaleWhileRevalidate: Network fetch for ${request.url} failed with status ${networkResponse.status}`);
|
||||
}
|
||||
return networkResponse;
|
||||
}).catch(err => {
|
||||
// console.warn(`[SW ${CACHE_VERSION}] StaleWhileRevalidate: Network fetch error for ${request.url}:`, err);
|
||||
// If fetch fails, and we already served from cache, that's okay.
|
||||
// If cache also missed (i.e., cachedResponse was null), then this error will propagate.
|
||||
// If fetch fails, and we served from cache, it's fine.
|
||||
// If cache also missed, this error will propagate.
|
||||
// console.warn(`[SW ${CACHE_VERSION}] SWR: Network fetch error for ${request.url}`, err);
|
||||
throw err;
|
||||
});
|
||||
return cachedResponse || fetchPromise;
|
||||
}).catch(err => {
|
||||
// This catch block handles errors from cache.match() or if fetchPromise was returned and rejected
|
||||
// console.error(`[SW ${CACHE_VERSION}] StaleWhileRevalidate: Error for ${request.url}. Trying network fallback.`, err);
|
||||
return fetch(request); // Final fallback to network if cache interactions fail
|
||||
}).catch(() => {
|
||||
// Fallback to network if cache.match fails
|
||||
// console.warn(`[SW ${CACHE_VERSION}] SWR: Cache match error for ${request.url}, trying network directly.`);
|
||||
return fetch(request);
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user