sw.js fixed
This commit is contained in:
117
public/sw.js
117
public/sw.js
@@ -1,9 +1,12 @@
|
||||
const CACHE_NAME = 'whisky-vault-v5'; // Increment version for refined strategy
|
||||
const CACHE_NAME = 'whisky-vault-v6-bunker'; // Neue Version erzwingen!
|
||||
|
||||
// CONFIG: Core pages and assets to pre-cache immediately on install
|
||||
// WICHTIG: Hier muss die URL für "Neues Tasting" rein, damit sie offline da ist!
|
||||
const CORE_PAGES = [
|
||||
'/', // Dashboard / Home (Includes Scan and Tasting features)
|
||||
'/sessions', // Tasting Sessions overview
|
||||
'/', // Dashboard / App Shell
|
||||
'/sessions', // Tasting Sessions
|
||||
'/scan', // Scan Page (falls URL existiert)
|
||||
// '/tasting/new' // <--- FÜGE HIER DEINE TASTING URL HINZU, falls sie existiert!
|
||||
];
|
||||
|
||||
const STATIC_ASSETS = [
|
||||
@@ -13,48 +16,41 @@ const STATIC_ASSETS = [
|
||||
'/favicon.ico',
|
||||
];
|
||||
|
||||
// Install: Lade alles Wichtige sofort in den Keller
|
||||
self.addEventListener('install', (event) => {
|
||||
self.skipWaiting(); // Sofortiger Wechsel zum neuen SW
|
||||
event.waitUntil(
|
||||
caches.open(CACHE_NAME).then((cache) => {
|
||||
console.log('⚡ PWA: Pre-caching core pages and assets...');
|
||||
// Combine items to cache
|
||||
return cache.addAll([...CORE_PAGES, ...STATIC_ASSETS]);
|
||||
})
|
||||
);
|
||||
self.skipWaiting();
|
||||
});
|
||||
|
||||
// Activate: Alten Müll rauswerfen
|
||||
self.addEventListener('activate', (event) => {
|
||||
event.waitUntil(
|
||||
caches.keys().then((cacheNames) => {
|
||||
return Promise.all(
|
||||
cacheNames.map((cacheName) => {
|
||||
if (cacheName !== CACHE_NAME) {
|
||||
console.log('🧹 PWA: Clearing old cache', cacheName);
|
||||
return caches.delete(cacheName);
|
||||
}
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
self.clients.claim();
|
||||
self.clients.claim(); // Sofort Kontrolle übernehmen
|
||||
});
|
||||
|
||||
// Helper for Network-First with Timeout
|
||||
// Helper: Network mit Timeout (für den Notfall)
|
||||
function fetchWithTimeout(request, timeoutMs = 3000) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeoutId = setTimeout(() => {
|
||||
reject(new Error('Network timeout'));
|
||||
}, timeoutMs);
|
||||
|
||||
const timeoutId = setTimeout(() => reject(new Error('Network timeout')), timeoutMs);
|
||||
fetch(request).then(
|
||||
(response) => {
|
||||
clearTimeout(timeoutId);
|
||||
resolve(response);
|
||||
},
|
||||
(err) => {
|
||||
clearTimeout(timeoutId);
|
||||
reject(err);
|
||||
}
|
||||
(res) => { clearTimeout(timeoutId); resolve(res); },
|
||||
(err) => { clearTimeout(timeoutId); reject(err); }
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -62,35 +58,41 @@ function fetchWithTimeout(request, timeoutMs = 3000) {
|
||||
self.addEventListener('fetch', (event) => {
|
||||
const url = new URL(event.request.url);
|
||||
|
||||
// CRITICAL: Always bypass cache for auth, api, and supabase requests
|
||||
const isAuthRequest = url.pathname.includes('/auth/') ||
|
||||
url.pathname.includes('/v1/auth/') ||
|
||||
url.pathname.includes('/v1/token');
|
||||
const isApiRequest = url.pathname.includes('/api/');
|
||||
const isSupabaseRequest = url.hostname.includes('supabase.co');
|
||||
|
||||
if (isAuthRequest || isApiRequest || isSupabaseRequest) {
|
||||
// 0. BYPASS: Auth & API immer durchlassen (werden von Dexie/App handled)
|
||||
if (url.pathname.includes('/auth/') ||
|
||||
url.pathname.includes('/api/') ||
|
||||
url.hostname.includes('supabase.co')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. NEXT.JS DATA (RSC): Stale-While-Revalidate with empty JSON fallback
|
||||
// 1. NEXT.JS DATA (RSC): Swallow Errors
|
||||
// Verhindert "Application Error" wenn JSON fehlt
|
||||
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;
|
||||
caches.match(event.request).then((cached) => {
|
||||
// Wenn im Cache (selten bei RSC), nimm es
|
||||
if (cached) return cached;
|
||||
|
||||
// Fallback to empty JSON to prevent "Application Error" screens
|
||||
return new Response(JSON.stringify({}), {
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
// Sonst Netzwerk mit Timeout
|
||||
return fetchWithTimeout(event.request, 2000)
|
||||
.then(res => {
|
||||
// Optional: RSC cachen für später?
|
||||
// Vorsicht: RSC Daten veralten schnell. Hier cachen wir NICHT.
|
||||
return res;
|
||||
})
|
||||
.catch(() => {
|
||||
// Fallback: Leeres JSON, damit React nicht crasht
|
||||
return new Response(JSON.stringify({}), {
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
});
|
||||
})
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. ASSETS & APP SHELL: Stale-While-Revalidate
|
||||
// 2. ASSETS (JS/CSS/Images): CACHE FIRST (Der "Bunker" Fix)
|
||||
// Wenn wir die Datei haben, nutzen wir sie. Keine Diskussion mit dem Netzwerk.
|
||||
const isAsset = event.request.destination === 'style' ||
|
||||
event.request.destination === 'script' ||
|
||||
event.request.destination === 'worker' ||
|
||||
@@ -101,7 +103,13 @@ self.addEventListener('fetch', (event) => {
|
||||
if (isAsset) {
|
||||
event.respondWith(
|
||||
caches.match(event.request).then((cachedResponse) => {
|
||||
const fetchPromise = fetch(event.request).then((networkResponse) => {
|
||||
if (cachedResponse) {
|
||||
// TREFFER: Sofort zurückgeben. Kein Fetch!
|
||||
// Das verhindert den "Network Changed" Crash.
|
||||
return cachedResponse;
|
||||
}
|
||||
// MISS: Nur dann zum Netzwerk gehen
|
||||
return fetch(event.request).then((networkResponse) => {
|
||||
if (networkResponse && networkResponse.status === 200) {
|
||||
const responseClone = networkResponse.clone();
|
||||
caches.open(CACHE_NAME).then((cache) => {
|
||||
@@ -110,27 +118,36 @@ self.addEventListener('fetch', (event) => {
|
||||
}
|
||||
return networkResponse;
|
||||
});
|
||||
return cachedResponse || fetchPromise;
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. NAVIGATION: Ironclad Navigation Fallback
|
||||
// 3. NAVIGATION (HTML): App Shell / Cache First
|
||||
// Wenn offline, gib die Startseite zurück und lass Next.js routen.
|
||||
if (event.request.mode === 'navigate') {
|
||||
event.respondWith(
|
||||
fetchWithTimeout(event.request, 3000)
|
||||
.catch(async () => {
|
||||
console.log('[SW] Navigation network failure, attempting cache fallback');
|
||||
(async () => {
|
||||
try {
|
||||
// A. Suche exakte Seite im Cache
|
||||
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('/');
|
||||
})
|
||||
// B. Suche Startseite (App Shell) im Cache
|
||||
// Das ist der wichtigste Fallback für SPAs!
|
||||
const shellResponse = await caches.match('/');
|
||||
if (shellResponse) return shellResponse;
|
||||
|
||||
// C. Wenn nichts im Cache ist (ganz neuer User): Netzwerk
|
||||
return await fetchWithTimeout(event.request, 3000);
|
||||
} catch (error) {
|
||||
console.log('[SW] Offline fallback failed:', error);
|
||||
// D. Letzter Ausweg (Text anzeigen)
|
||||
return new Response('<h1>Offline</h1><p>Bitte App neu laden.</p>', {
|
||||
headers: { 'Content-Type': 'text/html' }
|
||||
});
|
||||
}
|
||||
})()
|
||||
);
|
||||
return;
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user