feat: implement ironclad offline strategy with pre-caching and root fallback
This commit is contained in:
53
public/sw.js
53
public/sw.js
@@ -1,5 +1,13 @@
|
|||||||
const CACHE_NAME = 'whisky-vault-v3'; // Increment version to apply new strategy
|
const CACHE_NAME = 'whisky-vault-v4'; // Increment version for "Ironclad" strategy
|
||||||
const ASSETS_TO_CACHE = [
|
|
||||||
|
// CONFIG: Core pages and assets to pre-cache immediately on install
|
||||||
|
const CORE_PAGES = [
|
||||||
|
'/', // Dashboard / Home
|
||||||
|
'/tasting/new', // Critical: Add Tasting Screen
|
||||||
|
'/scan', // Critical: Scan Screen
|
||||||
|
];
|
||||||
|
|
||||||
|
const STATIC_ASSETS = [
|
||||||
'/manifest.json',
|
'/manifest.json',
|
||||||
'/icon-192.png',
|
'/icon-192.png',
|
||||||
'/icon-512.png',
|
'/icon-512.png',
|
||||||
@@ -9,9 +17,12 @@ const ASSETS_TO_CACHE = [
|
|||||||
self.addEventListener('install', (event) => {
|
self.addEventListener('install', (event) => {
|
||||||
event.waitUntil(
|
event.waitUntil(
|
||||||
caches.open(CACHE_NAME).then((cache) => {
|
caches.open(CACHE_NAME).then((cache) => {
|
||||||
return cache.addAll(ASSETS_TO_CACHE);
|
console.log('⚡ PWA: Pre-caching core pages and assets...');
|
||||||
|
// Combine items to cache
|
||||||
|
return cache.addAll([...CORE_PAGES, ...STATIC_ASSETS]);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
self.skipWaiting();
|
||||||
});
|
});
|
||||||
|
|
||||||
self.addEventListener('activate', (event) => {
|
self.addEventListener('activate', (event) => {
|
||||||
@@ -63,7 +74,24 @@ self.addEventListener('fetch', (event) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. ASSETS & APP SHELL: Stale-While-Revalidate
|
// 1. NEXT.JS DATA (RSC): Stale-While-Revalidate with empty JSON fallback
|
||||||
|
if (url.pathname.startsWith('/_next/data/')) {
|
||||||
|
event.respondWith(
|
||||||
|
fetchWithTimeout(event.request, 2000)
|
||||||
|
.catch(async () => {
|
||||||
|
const cachedResponse = await caches.match(event.request);
|
||||||
|
if (cachedResponse) return cachedResponse;
|
||||||
|
|
||||||
|
// Fallback to empty JSON to prevent "Application Error" screens
|
||||||
|
return new Response(JSON.stringify({}), {
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. ASSETS & APP SHELL: Stale-While-Revalidate
|
||||||
const isAsset = event.request.destination === 'style' ||
|
const isAsset = event.request.destination === 'style' ||
|
||||||
event.request.destination === 'script' ||
|
event.request.destination === 'script' ||
|
||||||
event.request.destination === 'worker' ||
|
event.request.destination === 'worker' ||
|
||||||
@@ -89,18 +117,21 @@ self.addEventListener('fetch', (event) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. NAVIGATION: Network-First with 3s Timeout
|
// 3. NAVIGATION: Ironclad Navigation Fallback
|
||||||
if (event.request.mode === 'navigate') {
|
if (event.request.mode === 'navigate') {
|
||||||
event.respondWith(
|
event.respondWith(
|
||||||
fetchWithTimeout(event.request, 3000)
|
fetchWithTimeout(event.request, 3000)
|
||||||
.catch(() => {
|
.catch(async () => {
|
||||||
console.log('[SW] Navigation network failure or timeout, falling back to cache');
|
console.log('[SW] Navigation network failure, attempting cache fallback');
|
||||||
return caches.match(event.request);
|
const cachedResponse = await caches.match(event.request);
|
||||||
|
if (cachedResponse) return cachedResponse;
|
||||||
|
|
||||||
|
// CRITICAL FALLBACK: Load the Root App Shell ('/')
|
||||||
|
// This allows Next.js to bootstrap and handle the routing client-side
|
||||||
|
console.warn('⚠️ PWA: Route not in cache, fallback to Root App Shell');
|
||||||
|
return caches.match('/');
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default: Network-Only or default fetch
|
|
||||||
return;
|
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user