From ab8f0fe3ef82982c4489042fdc74da5d1cc174a0 Mon Sep 17 00:00:00 2001 From: robin Date: Sun, 21 Dec 2025 00:21:01 +0100 Subject: [PATCH] fix: robust SW progress sync and startup delay --- public/sw.js | 83 ++++++++++++++--------------- src/components/OfflineIndicator.tsx | 39 +++++++++----- 2 files changed, 68 insertions(+), 54 deletions(-) diff --git a/public/sw.js b/public/sw.js index be32ea2..f8b6858 100644 --- a/public/sw.js +++ b/public/sw.js @@ -1,10 +1,6 @@ -const CACHE_NAME = 'whisky-vault-v11-offline'; // Professional Offline-Modus v11 - -// CONFIG: Core Pages & Static Assets -const CORE_PAGES = [ - '/', -]; +const CACHE_NAME = 'whisky-vault-v12-offline'; +// CONFIG: Assets const STATIC_ASSETS = [ '/manifest.webmanifest', '/icon-192.png', @@ -12,24 +8,30 @@ const STATIC_ASSETS = [ '/favicon.ico', ]; -// Helper: Broadcast to all clients (including those not yet controlled) +const CORE_PAGES = [ + '/', +]; + +// Global state to track progress even when UI is not listening +let currentProgress = 0; +let isPrecacheFinished = false; + +// Helper: Broadcast to all clients async function broadcast(message) { try { const clients = await self.clients.matchAll({ includeUncontrolled: true, type: 'window' }); clients.forEach(client => client.postMessage(message)); - } catch (e) { - // Silently fail if no clients found - } + } catch (e) { } } // Helper: Fetch with Timeout & AbortController -async function fetchWithTimeout(url, timeoutMs = 25000) { +async function fetchWithTimeout(url, timeoutMs = 30000) { const controller = new AbortController(); const id = setTimeout(() => controller.abort(), timeoutMs); try { const response = await fetch(url, { signal: controller.signal, - cache: 'no-store' // Ensure we get fresh bits + cache: 'no-store' }); clearTimeout(id); return response; @@ -41,20 +43,23 @@ async function fetchWithTimeout(url, timeoutMs = 25000) { // 🏗️ INSTALL: Build the Offline-Modus self.addEventListener('install', (event) => { - // Immediate takeover self.skipWaiting(); event.waitUntil( caches.open(CACHE_NAME).then(async (cache) => { - console.log('🏗️ PWA: Building Offline-Modus v11...'); - // Load small static assets first for instant progress bar feedback + console.log('🏗️ PWA: Building Offline-Modus v12...'); + + // 💡 WAIT A MOMENT: Give the UI time to mount and register listeners + // In dev mode, the app takes a second to boot up. + await new Promise(resolve => setTimeout(resolve, 1500)); + const items = [...STATIC_ASSETS, ...CORE_PAGES]; const total = items.length; let loaded = 0; - // Sequential loading to avoid concurrent fetch limits on mobile for (const url of items) { try { + // Start with manifest and icons (fast) then the app shell (slow in dev) const res = await fetchWithTimeout(url); if (res && res.ok) { await cache.put(url, res); @@ -63,27 +68,28 @@ self.addEventListener('install', (event) => { console.error(`⚠️ PWA: Pre-cache failed for ${url}:`, error); } finally { loaded++; + currentProgress = Math.round((loaded / total) * 100); broadcast({ - type: 'PRECACHE_PROGRESS', - progress: Math.round((loaded / total) * 100) + type: 'OFFLINE_PROGRESS', + progress: currentProgress }); } } - console.log('✅ PWA: Bunker build finished'); - broadcast({ type: 'PRECACHE_COMPLETE', version: CACHE_NAME }); + console.log('✅ PWA: Offline-Modus is ready'); + isPrecacheFinished = true; + broadcast({ type: 'OFFLINE_READY', version: CACHE_NAME }); }) ); }); -// 🧹 ACTIVATE: Cleanup old bunkers +// 🧹 ACTIVATE: Cleanup old caches 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 bunker', cacheName); return caches.delete(cacheName); } }) @@ -93,66 +99,59 @@ self.addEventListener('activate', (event) => { self.clients.claim(); }); -// 💬 MESSAGE: Handle status checks +// 💬 MESSAGE: Handle status queries self.addEventListener('message', (event) => { if (event.data?.type === 'CHECK_OFFLINE_STATUS') { event.source.postMessage({ - type: 'OFFLINE_STATUS', - isReady: true, + type: 'OFFLINE_STATUS_RESPONSE', + isReady: isPrecacheFinished, + progress: currentProgress, version: CACHE_NAME }); } }); -// 🚀 FETCH: Bunker Strategy +// 🚀 FETCH: Offline-First Strategy self.addEventListener('fetch', (event) => { if (event.request.method !== 'GET') return; const url = new URL(event.request.url); - // 0. BYPASS for Auth/API/Supabase - if (url.pathname.includes('/auth/') || - url.pathname.includes('/api/') || - url.hostname.includes('supabase.co')) { + // Bypass Auth/API + if (url.pathname.includes('/auth/') || url.pathname.includes('/api/') || url.hostname.includes('supabase.co')) { return; } - // 1. RSC DATA (_next/data): Bunker with JSON Fallback + // RSC Data if (url.pathname.startsWith('/_next/data/')) { event.respondWith( caches.match(event.request).then((cached) => { if (cached) return cached; - return fetchWithTimeout(event.request, 3000) - .catch(() => new Response(JSON.stringify({}), { - headers: { 'Content-Type': 'application/json' } - })); + return fetchWithTimeout(event.request, 4000) + .catch(() => new Response(JSON.stringify({}), { headers: { 'Content-Type': 'application/json' } })); }) ); return; } - // 2. NAVIGATION & ASSETS: Bunker + SWR + // Navigation & Assets const isNavigation = event.request.mode === 'navigate'; const isAsset = event.request.destination === 'style' || event.request.destination === 'script' || - event.request.destination === 'worker' || - event.request.destination === 'font' || event.request.destination === 'image' || url.pathname.startsWith('/_next/static'); if (isNavigation || isAsset) { event.respondWith( caches.match(event.request).then(async (cachedResponse) => { - // Background update - const fetchPromise = fetchWithTimeout(event.request, 8000) + const fetchPromise = fetchWithTimeout(event.request, 10000) .then(async (networkResponse) => { if (networkResponse && networkResponse.status === 200) { const cache = await caches.open(CACHE_NAME); cache.put(event.request, networkResponse.clone()); } return networkResponse; - }) - .catch(() => { /* Background fail silent */ }); + }).catch(() => { }); if (isNavigation) { if (cachedResponse) return cachedResponse; diff --git a/src/components/OfflineIndicator.tsx b/src/components/OfflineIndicator.tsx index b0ffd12..6248d56 100644 --- a/src/components/OfflineIndicator.tsx +++ b/src/components/OfflineIndicator.tsx @@ -10,19 +10,27 @@ export default function OfflineIndicator() { useEffect(() => { setIsOffline(!navigator.onLine); - const savedReady = localStorage.getItem('whisky_bunker_ready') === 'true'; + const savedReady = localStorage.getItem('whisky_offline_ready') === 'true'; setIsReady(savedReady); const handleOnline = () => setIsOffline(false); const handleOffline = () => setIsOffline(true); const handleMessage = (event: MessageEvent) => { - if (event.data?.type === 'PRECACHE_PROGRESS') { + if (event.data?.type === 'OFFLINE_PROGRESS') { setProgress(event.data.progress); } - if (event.data?.type === 'PRECACHE_COMPLETE' || event.data?.type === 'OFFLINE_STATUS') { - setIsReady(true); - localStorage.setItem('whisky_bunker_ready', 'true'); + if (event.data?.type === 'OFFLINE_READY' || event.data?.type === 'OFFLINE_STATUS_RESPONSE') { + if (event.data?.type === 'OFFLINE_STATUS_RESPONSE') { + setProgress(event.data.progress || 0); + if (event.data.isReady) { + setIsReady(true); + localStorage.setItem('whisky_offline_ready', 'true'); + } + } else { + setIsReady(true); + localStorage.setItem('whisky_offline_ready', 'true'); + } } }; @@ -31,9 +39,18 @@ export default function OfflineIndicator() { if ('serviceWorker' in navigator) { navigator.serviceWorker.addEventListener('message', handleMessage); - if (navigator.serviceWorker.controller) { - navigator.serviceWorker.controller.postMessage({ type: 'CHECK_OFFLINE_STATUS' }); - } + + // Proactive status check + navigator.serviceWorker.ready.then(() => { + // If there's an active or installing worker, ask for status + const sw = navigator.serviceWorker.controller || + (navigator.serviceWorker as any).installing || + (navigator.serviceWorker as any).waiting; + + if (sw) { + sw.postMessage({ type: 'CHECK_OFFLINE_STATUS' }); + } + }); } return () => { @@ -56,11 +73,9 @@ export default function OfflineIndicator() { if (isReady) { return ( -
+
Offline-Modus aktiv - - {/* Tooltip for desktop */}
Alle Funktionen sind vollständig offline verfügbar.
@@ -72,7 +87,7 @@ export default function OfflineIndicator() {
- {progress > 0 ? `Lade Offline-Daten... ${progress}%` : 'Offline-Modus wird vorbereitet...'} + {progress > 0 ? `Lade Offline-Daten... ${progress}%` : 'Vorbereiten...'}
);