From b1c84fb83762b57ccb4a333ecd75e5e9cbd2e1ab Mon Sep 17 00:00:00 2001 From: Giancarmine Salucci Date: Mon, 22 Dec 2025 05:29:37 +0100 Subject: [PATCH] feat(pwa): migrate service worker to SvelteKit native Story 3: Migrate Service Worker to SvelteKit Native - Replace workbox imports with SvelteKit $service-worker module - Use build, files, version arrays for manual cache management - Implement manual asset caching and cache cleanup - Replace NavigationRoute with manual fetch handling - Preserve all push notification event handlers exactly - Preserve background sync and message handling functionality - Service worker builds successfully as service-worker.mjs SvelteKit native implementation ready - now need to enable registration Refs: docs/plans/MigrateToNativeSvelteKitPWA.md --- src/service-worker.ts | 165 +++++++++++++++++++++--------------------- 1 file changed, 81 insertions(+), 84 deletions(-) diff --git a/src/service-worker.ts b/src/service-worker.ts index b3b745b..ca9e1ce 100644 --- a/src/service-worker.ts +++ b/src/service-worker.ts @@ -1,13 +1,20 @@ -/// +/// +/// +/// /// -// Standard workbox imports - let the build process handle these -import { cleanupOutdatedCaches, createHandlerBoundToURL, precacheAndRoute } from 'workbox-precaching'; -import { NavigationRoute, registerRoute } from 'workbox-routing'; +import { build, files, version } from '$service-worker'; declare let self: ServiceWorkerGlobalScope; -// Global error handler for service worker +// Create a unique cache name for this deployment +const CACHE = `cache-${version}`; +const ASSETS = [ + ...build, // the app itself + ...files // everything in `static` +]; + +// Global error handlers (preserve existing) self.addEventListener('error', (event) => { console.error('[SW] Global error:', event.error); console.error('[SW] Error details:', { @@ -19,7 +26,6 @@ self.addEventListener('error', (event) => { }); }); -// Unhandled promise rejection handler self.addEventListener('unhandledrejection', (event) => { console.error('[SW] Unhandled promise rejection:', event.reason); event.preventDefault(); // Prevent default browser behavior @@ -27,90 +33,81 @@ self.addEventListener('unhandledrejection', (event) => { console.log('[SW] Service worker script loading...'); -// Get the workbox manifest - this will be injected by the build process -const workboxManifest = self.__WB_MANIFEST; +// Install event - cache all assets +self.addEventListener('install', (event) => { + console.log('[SW] Installing service worker...'); + + async function addFilesToCache() { + const cache = await caches.open(CACHE); + await cache.addAll(ASSETS); + console.log(`[SW] Cached ${ASSETS.length} assets`); + } -// Wrap workbox initialization in try-catch with granular error handling -try { - console.log('[SW] Initializing workbox...'); + event.waitUntil(addFilesToCache()); +}); + +// Activate event - clean up old caches +self.addEventListener('activate', (event) => { + console.log('[SW] Activating service worker...'); - // Check if workbox functions are available - if (typeof precacheAndRoute !== 'function' || typeof cleanupOutdatedCaches !== 'function') { - throw new Error('Workbox functions not available'); - } - - // Detect environment - in production, workbox manifest should be injected - const isDevelopment = !workboxManifest || (workboxManifest && workboxManifest.length === 0); - console.log(`[SW] Running in ${isDevelopment ? 'development' : 'production'} mode`); - - // Enhanced manifest validation with detailed logging - if (!workboxManifest) { - if (isDevelopment) { - console.info('[SW] Workbox manifest not injected - running in development mode, precaching disabled'); - } else { - console.warn('[SW] Workbox manifest not found in production build - this may be a build issue'); + async function deleteOldCaches() { + for (const key of await caches.keys()) { + if (key !== CACHE) { + console.log('[SW] Deleting old cache:', key); + await caches.delete(key); + } } - } else if (!Array.isArray(workboxManifest)) { - console.error('[SW] Workbox manifest exists but is invalid format:', typeof workboxManifest); - } else if (workboxManifest.length === 0) { - console.warn('[SW] Workbox manifest is empty - no assets to precache'); - } else { - console.log(`[SW] Workbox manifest found with ${workboxManifest.length} entries`); - console.debug('[SW] Manifest entries:', workboxManifest.slice(0, 5)); // Log first 5 for debugging - + } + + event.waitUntil(deleteOldCaches()); +}); + +// Fetch event - serve from cache with network fallback +self.addEventListener('fetch', (event) => { + // ignore POST requests etc + if (event.request.method !== 'GET') return; + + async function respond() { + const url = new URL(event.request.url); + const cache = await caches.open(CACHE); + + // `build`/`files` can always be served from the cache + if (ASSETS.includes(url.pathname)) { + const response = await cache.match(url.pathname); + if (response) { + return response; + } + } + + // for everything else, try the network first, but + // fall back to the cache if we're offline try { - precacheAndRoute(workboxManifest); - console.log('[SW] Precaching completed successfully'); - } catch (precacheError) { - console.error('[SW] Error during precaching:', precacheError); + const response = await fetch(event.request); + + // if we're offline, fetch can return a value that is not a Response + // instead of throwing - and we can't pass this non-Response to respondWith + if (!(response instanceof Response)) { + throw new Error('invalid response from fetch'); + } + + if (response.status === 200) { + cache.put(event.request, response.clone()); + } + return response; + } catch (err) { + const response = await cache.match(event.request); + if (response) { + return response; + } + + // if there's no cache, then just error out + // as there is nothing we can do to respond to this request + throw err; } } - - // Always try to cleanup outdated caches - try { - cleanupOutdatedCaches(); - console.log('[SW] Cache cleanup completed'); - } catch (cleanupError) { - console.error('[SW] Error during cache cleanup:', cleanupError); - } - - // Handle navigation requests with additional error handling - try { - console.log('[SW] Setting up navigation routing...'); - if (typeof createHandlerBoundToURL === 'function' && typeof NavigationRoute === 'function' && typeof registerRoute === 'function') { - const handler = createHandlerBoundToURL('/'); - const navigationRoute = new NavigationRoute(handler, { - denylist: [/^\/api/] - }); - registerRoute(navigationRoute); - console.log('[SW] Navigation routing configured successfully'); - } else { - throw new Error('Navigation routing functions not available'); - } - } catch (routingError) { - console.error('[SW] Error setting up navigation routing:', routingError); - // Continue without navigation routing if it fails - } - - console.log('[SW] Workbox initialization completed'); -} catch (error) { - console.error('[SW] Critical error initializing workbox:', error); - console.error('[SW] Error details:', { - name: error.name, - message: error.message, - stack: error.stack - }); - - // In development mode, this is expected behavior - if (!workboxManifest || (Array.isArray(workboxManifest) && workboxManifest.length === 0)) { - console.info('[SW] Continuing with limited functionality in development mode'); - } else { - console.error('[SW] Production build should have workbox manifest - check build configuration'); - } - - // Continue with service worker registration even if workbox fails - // This allows push notifications and other features to still work -} + + event.respondWith(respond()); +}); // Push notification handling self.addEventListener('push', (event) => {