fix: optimize bunker loading and add progress feedback
This commit is contained in:
51
public/sw.js
51
public/sw.js
@@ -1,4 +1,4 @@
|
|||||||
const CACHE_NAME = 'whisky-vault-v7-bunker'; // Neue Version für stabilen Bunker + SWR
|
const CACHE_NAME = 'whisky-vault-v8-bunker'; // Optimierter Bunker v8
|
||||||
|
|
||||||
// CONFIG: Kern-Seiten und Assets für den Bunker (sofortiges Pre-Caching)
|
// CONFIG: Kern-Seiten und Assets für den Bunker (sofortiges Pre-Caching)
|
||||||
const CORE_PAGES = [
|
const CORE_PAGES = [
|
||||||
@@ -6,13 +6,13 @@ const CORE_PAGES = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const STATIC_ASSETS = [
|
const STATIC_ASSETS = [
|
||||||
'/manifest.webmanifest', // Korrigierter Pfad für Next.js
|
'/manifest.webmanifest',
|
||||||
'/icon-192.png',
|
'/icon-192.png',
|
||||||
'/icon-512.png',
|
'/icon-512.png',
|
||||||
'/favicon.ico',
|
'/favicon.ico',
|
||||||
];
|
];
|
||||||
|
|
||||||
// Helper: Fetch mit Timeout und AbortController (sauberer Abbruch)
|
// Helper: Fetch mit Timeout und Sauberen Abort
|
||||||
async function fetchWithTimeout(request, timeoutMs = 3000) {
|
async function fetchWithTimeout(request, timeoutMs = 3000) {
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
const id = setTimeout(() => controller.abort(), timeoutMs);
|
const id = setTimeout(() => controller.abort(), timeoutMs);
|
||||||
@@ -26,36 +26,47 @@ async function fetchWithTimeout(request, timeoutMs = 3000) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper: Broadcast an alle Clients
|
||||||
|
async function broadcast(message) {
|
||||||
|
const clients = await self.clients.matchAll();
|
||||||
|
clients.forEach(client => client.postMessage(message));
|
||||||
|
}
|
||||||
|
|
||||||
// Install: Lade alles Wichtige einzeln in den Bunker
|
// Install: Lade alles Wichtige einzeln in den Bunker
|
||||||
self.addEventListener('install', (event) => {
|
self.addEventListener('install', (event) => {
|
||||||
self.skipWaiting();
|
self.skipWaiting();
|
||||||
event.waitUntil(
|
event.waitUntil(
|
||||||
caches.open(CACHE_NAME).then(async (cache) => {
|
caches.open(CACHE_NAME).then(async (cache) => {
|
||||||
console.log('🏗️ PWA: Building bunker v7...');
|
console.log('🏗️ PWA: Building bunker v8...');
|
||||||
|
const total = CORE_PAGES.length + STATIC_ASSETS.length;
|
||||||
|
let loaded = 0;
|
||||||
|
|
||||||
const promises = [...CORE_PAGES, ...STATIC_ASSETS].map(async (url) => {
|
const promises = [...CORE_PAGES, ...STATIC_ASSETS].map(async (url) => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(url);
|
// Im Dev-Mode kann / lange dauern (Kompilierung). 10s Timeout.
|
||||||
|
const res = await fetchWithTimeout(url, 10000);
|
||||||
if (!res.ok) throw new Error(`Status ${res.status}`);
|
if (!res.ok) throw new Error(`Status ${res.status}`);
|
||||||
return cache.put(url, res);
|
await cache.put(url, res);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`⚠️ PWA: Pre-cache failed for ${url}:`, error);
|
console.error(`⚠️ PWA: Pre-cache failed for ${url}:`, error);
|
||||||
|
} finally {
|
||||||
|
loaded++;
|
||||||
|
broadcast({
|
||||||
|
type: 'PRECACHE_PROGRESS',
|
||||||
|
progress: Math.round((loaded / total) * 100)
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
await Promise.all(promises);
|
await Promise.all(promises);
|
||||||
console.log('✅ PWA: Bunker build finished');
|
console.log('✅ PWA: Bunker build finished');
|
||||||
|
|
||||||
// Signal to clients that pre-caching is complete
|
// Signal an Clients: Bunker ist bereit
|
||||||
broadcast({ type: 'PRECACHE_COMPLETE', version: CACHE_NAME });
|
broadcast({ type: 'PRECACHE_COMPLETE', version: CACHE_NAME });
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Helper: Broadcast to all clients
|
|
||||||
async function broadcast(message) {
|
|
||||||
const clients = await self.clients.matchAll();
|
|
||||||
clients.forEach(client => client.postMessage(message));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Activate: Alte Bunker räumen
|
// Activate: Alte Bunker räumen
|
||||||
self.addEventListener('activate', (event) => {
|
self.addEventListener('activate', (event) => {
|
||||||
event.waitUntil(
|
event.waitUntil(
|
||||||
@@ -114,7 +125,6 @@ self.addEventListener('fetch', (event) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 2. ASSETS & NAVIGATION: Stale-While-Revalidate (Der "Echte" Bunker Mode)
|
// 2. ASSETS & NAVIGATION: Stale-While-Revalidate (Der "Echte" Bunker Mode)
|
||||||
// Wir liefern SOFORT aus dem Cache, fragen aber im Hintergrund das Netzwerk.
|
|
||||||
const isNavigation = event.request.mode === 'navigate';
|
const isNavigation = event.request.mode === 'navigate';
|
||||||
const isAsset = event.request.destination === 'style' ||
|
const isAsset = event.request.destination === 'style' ||
|
||||||
event.request.destination === 'script' ||
|
event.request.destination === 'script' ||
|
||||||
@@ -126,7 +136,7 @@ self.addEventListener('fetch', (event) => {
|
|||||||
if (isNavigation || isAsset) {
|
if (isNavigation || isAsset) {
|
||||||
event.respondWith(
|
event.respondWith(
|
||||||
caches.match(event.request).then(async (cachedResponse) => {
|
caches.match(event.request).then(async (cachedResponse) => {
|
||||||
// Hintergrund-Update vorbereiten
|
// Hintergrund-Update
|
||||||
const fetchPromise = fetchWithTimeout(event.request, 5000)
|
const fetchPromise = fetchWithTimeout(event.request, 5000)
|
||||||
.then(async (networkResponse) => {
|
.then(async (networkResponse) => {
|
||||||
if (networkResponse && networkResponse.status === 200) {
|
if (networkResponse && networkResponse.status === 200) {
|
||||||
@@ -135,21 +145,14 @@ self.addEventListener('fetch', (event) => {
|
|||||||
}
|
}
|
||||||
return networkResponse;
|
return networkResponse;
|
||||||
})
|
})
|
||||||
.catch(() => { /* Fail silently in background */ });
|
.catch(() => { /* Silent in background */ });
|
||||||
|
|
||||||
// Navigation Fallback Logik
|
|
||||||
if (isNavigation) {
|
if (isNavigation) {
|
||||||
if (cachedResponse) return cachedResponse;
|
if (cachedResponse) return cachedResponse;
|
||||||
|
|
||||||
// Root Fallback für Deep Links (App Shell)
|
|
||||||
const shell = await caches.match('/');
|
const shell = await caches.match('/');
|
||||||
if (shell) {
|
if (shell) return shell;
|
||||||
console.log('[SW] Route not cached, using Root App Shell fallback');
|
|
||||||
return shell;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Assets: Cache oder Netzwerk
|
|
||||||
return cachedResponse || fetchPromise || fetch(event.request);
|
return cachedResponse || fetchPromise || fetch(event.request);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { WifiOff, ShieldCheck } from 'lucide-react';
|
|||||||
export default function OfflineIndicator() {
|
export default function OfflineIndicator() {
|
||||||
const [isOffline, setIsOffline] = useState(false);
|
const [isOffline, setIsOffline] = useState(false);
|
||||||
const [isBunkerReady, setIsBunkerReady] = useState(false);
|
const [isBunkerReady, setIsBunkerReady] = useState(false);
|
||||||
|
const [progress, setProgress] = useState(0);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsOffline(!navigator.onLine);
|
setIsOffline(!navigator.onLine);
|
||||||
@@ -17,6 +18,9 @@ export default function OfflineIndicator() {
|
|||||||
const handleOffline = () => setIsOffline(true);
|
const handleOffline = () => setIsOffline(true);
|
||||||
|
|
||||||
const handleMessage = (event: MessageEvent) => {
|
const handleMessage = (event: MessageEvent) => {
|
||||||
|
if (event.data?.type === 'PRECACHE_PROGRESS') {
|
||||||
|
setProgress(event.data.progress);
|
||||||
|
}
|
||||||
if (event.data?.type === 'PRECACHE_COMPLETE' || event.data?.type === 'BUNKER_STATUS') {
|
if (event.data?.type === 'PRECACHE_COMPLETE' || event.data?.type === 'BUNKER_STATUS') {
|
||||||
console.log('🛡️ PWA: Bunker is ready for offline use!');
|
console.log('🛡️ PWA: Bunker is ready for offline use!');
|
||||||
setIsBunkerReady(true);
|
setIsBunkerReady(true);
|
||||||
@@ -30,10 +34,27 @@ export default function OfflineIndicator() {
|
|||||||
if ('serviceWorker' in navigator) {
|
if ('serviceWorker' in navigator) {
|
||||||
navigator.serviceWorker.addEventListener('message', handleMessage);
|
navigator.serviceWorker.addEventListener('message', handleMessage);
|
||||||
|
|
||||||
// Proactively check status if SW is already active
|
// Proactive check
|
||||||
if (navigator.serviceWorker.controller) {
|
navigator.serviceWorker.ready.then((registration) => {
|
||||||
navigator.serviceWorker.controller.postMessage({ type: 'CHECK_BUNKER_STATUS' });
|
if (registration.active) {
|
||||||
|
registration.active.postMessage({ type: 'CHECK_BUNKER_STATUS' });
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fallback: If after 20s we still think we are loading but SW is active, assume ready
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
const isSwActive = !!navigator.serviceWorker.controller;
|
||||||
|
if (!isBunkerReady && isSwActive) {
|
||||||
|
setIsBunkerReady(true);
|
||||||
|
localStorage.setItem('whisky_bunker_ready', 'true');
|
||||||
|
}
|
||||||
|
}, 20000);
|
||||||
|
return () => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
window.removeEventListener('online', handleOnline);
|
||||||
|
window.removeEventListener('offline', handleOffline);
|
||||||
|
navigator.serviceWorker.removeEventListener('message', handleMessage);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
@@ -43,7 +64,7 @@ export default function OfflineIndicator() {
|
|||||||
navigator.serviceWorker.removeEventListener('message', handleMessage);
|
navigator.serviceWorker.removeEventListener('message', handleMessage);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, []);
|
}, [isBunkerReady]);
|
||||||
|
|
||||||
if (isOffline) {
|
if (isOffline) {
|
||||||
return (
|
return (
|
||||||
@@ -76,7 +97,7 @@ export default function OfflineIndicator() {
|
|||||||
<div className="bg-zinc-900/80 backdrop-blur-md border border-amber-500/30 px-3 py-1.5 rounded-full flex items-center gap-2 shadow-lg shadow-amber-500/10">
|
<div className="bg-zinc-900/80 backdrop-blur-md border border-amber-500/30 px-3 py-1.5 rounded-full flex items-center gap-2 shadow-lg shadow-amber-500/10">
|
||||||
<div className="w-1.5 h-1.5 bg-amber-500 rounded-full animate-pulse" />
|
<div className="w-1.5 h-1.5 bg-amber-500 rounded-full animate-pulse" />
|
||||||
<span className="text-[9px] font-black uppercase tracking-widest text-zinc-300">
|
<span className="text-[9px] font-black uppercase tracking-widest text-zinc-300">
|
||||||
Bunker wird geladen...
|
Bunker wird geladen... {progress > 0 ? `${progress}%` : ''}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user