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
This commit is contained in:
Giancarmine Salucci
2025-12-22 05:29:37 +01:00
parent c9b53e0dbe
commit b1c84fb837

View File

@@ -1,13 +1,20 @@
/// <reference types="vite/client" /> /// <reference types="@sveltejs/kit" />
/// <reference no-default-lib="true"/>
/// <reference lib="esnext" />
/// <reference lib="webworker" /> /// <reference lib="webworker" />
// Standard workbox imports - let the build process handle these import { build, files, version } from '$service-worker';
import { cleanupOutdatedCaches, createHandlerBoundToURL, precacheAndRoute } from 'workbox-precaching';
import { NavigationRoute, registerRoute } from 'workbox-routing';
declare let self: ServiceWorkerGlobalScope; 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) => { self.addEventListener('error', (event) => {
console.error('[SW] Global error:', event.error); console.error('[SW] Global error:', event.error);
console.error('[SW] Error details:', { console.error('[SW] Error details:', {
@@ -19,7 +26,6 @@ self.addEventListener('error', (event) => {
}); });
}); });
// Unhandled promise rejection handler
self.addEventListener('unhandledrejection', (event) => { self.addEventListener('unhandledrejection', (event) => {
console.error('[SW] Unhandled promise rejection:', event.reason); console.error('[SW] Unhandled promise rejection:', event.reason);
event.preventDefault(); // Prevent default browser behavior event.preventDefault(); // Prevent default browser behavior
@@ -27,90 +33,81 @@ self.addEventListener('unhandledrejection', (event) => {
console.log('[SW] Service worker script loading...'); console.log('[SW] Service worker script loading...');
// Get the workbox manifest - this will be injected by the build process // Install event - cache all assets
const workboxManifest = self.__WB_MANIFEST; self.addEventListener('install', (event) => {
console.log('[SW] Installing service worker...');
// Wrap workbox initialization in try-catch with granular error handling async function addFilesToCache() {
try { const cache = await caches.open(CACHE);
console.log('[SW] Initializing workbox...'); await cache.addAll(ASSETS);
console.log(`[SW] Cached ${ASSETS.length} assets`);
// 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 event.waitUntil(addFilesToCache());
const isDevelopment = !workboxManifest || (workboxManifest && workboxManifest.length === 0); });
console.log(`[SW] Running in ${isDevelopment ? 'development' : 'production'} mode`);
// Enhanced manifest validation with detailed logging // Activate event - clean up old caches
if (!workboxManifest) { self.addEventListener('activate', (event) => {
if (isDevelopment) { console.log('[SW] Activating service worker...');
console.info('[SW] Workbox manifest not injected - running in development mode, precaching disabled');
} else { async function deleteOldCaches() {
console.warn('[SW] Workbox manifest not found in production build - this may be a build issue'); 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 { try {
precacheAndRoute(workboxManifest); const response = await fetch(event.request);
console.log('[SW] Precaching completed successfully');
} catch (precacheError) { // if we're offline, fetch can return a value that is not a Response
console.error('[SW] Error during precaching:', precacheError); // 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 event.respondWith(respond());
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
}
// Push notification handling // Push notification handling
self.addEventListener('push', (event) => { self.addEventListener('push', (event) => {