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:
@@ -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...');
|
||||||
|
|
||||||
|
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
|
event.waitUntil(addFilesToCache());
|
||||||
try {
|
});
|
||||||
console.log('[SW] Initializing workbox...');
|
|
||||||
|
// Activate event - clean up old caches
|
||||||
|
self.addEventListener('activate', (event) => {
|
||||||
|
console.log('[SW] Activating service worker...');
|
||||||
|
|
||||||
// Check if workbox functions are available
|
async function deleteOldCaches() {
|
||||||
if (typeof precacheAndRoute !== 'function' || typeof cleanupOutdatedCaches !== 'function') {
|
for (const key of await caches.keys()) {
|
||||||
throw new Error('Workbox functions not available');
|
if (key !== CACHE) {
|
||||||
}
|
console.log('[SW] Deleting old cache:', key);
|
||||||
|
await caches.delete(key);
|
||||||
// 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');
|
|
||||||
}
|
}
|
||||||
} else if (!Array.isArray(workboxManifest)) {
|
}
|
||||||
console.error('[SW] Workbox manifest exists but is invalid format:', typeof workboxManifest);
|
|
||||||
} else if (workboxManifest.length === 0) {
|
event.waitUntil(deleteOldCaches());
|
||||||
console.warn('[SW] Workbox manifest is empty - no assets to precache');
|
});
|
||||||
} else {
|
|
||||||
console.log(`[SW] Workbox manifest found with ${workboxManifest.length} entries`);
|
// Fetch event - serve from cache with network fallback
|
||||||
console.debug('[SW] Manifest entries:', workboxManifest.slice(0, 5)); // Log first 5 for debugging
|
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) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user