/// /// /// /// import { build, files, version } from '$service-worker'; declare let self: ServiceWorkerGlobalScope; // 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:', { message: event.message, filename: event.filename, lineno: event.lineno, colno: event.colno, error: event.error }); }); self.addEventListener('unhandledrejection', (event) => { console.error('[SW] Unhandled promise rejection:', event.reason); event.preventDefault(); // Prevent default browser behavior }); console.log('[SW] Service worker script loading...'); // 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`); } event.waitUntil(addFilesToCache()); }); // Activate event - clean up old caches self.addEventListener('activate', (event) => { console.log('[SW] Activating service worker...'); async function deleteOldCaches() { for (const key of await caches.keys()) { if (key !== CACHE) { console.log('[SW] Deleting old cache:', key); await caches.delete(key); } } } 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 { 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; } } event.respondWith(respond()); }); // Push notification handling self.addEventListener('push', (event) => { console.log('[SW] Push event received:', event); if (!event.data) { console.log('[SW] Push event but no data'); return; } let data; try { data = event.data.json(); } catch (e) { console.error('[SW] Failed to parse push data:', e); return; } console.log('[SW] Push data:', data); const options: NotificationOptions = { body: data.body || 'Recipe processing update', icon: '/favicon.png', badge: '/favicon.png', data: data, requireInteraction: data.requireInteraction || false, silent: false, tag: data.tag || 'recipe-update', timestamp: Date.now(), actions: [] }; // Add actions based on notification type if (data.type === 'success' && data.itemId) { options.actions = [ { action: 'view', title: 'View Recipe', icon: '/favicon.png' }, { action: 'dismiss', title: 'Dismiss' } ]; } else if (data.type === 'error' && data.itemId) { options.actions = [ { action: 'retry', title: 'Retry', icon: '/favicon.png' }, { action: 'view', title: 'View Details' } ]; } const title = data.title || getNotificationTitle(data.type, data); event.waitUntil( self.registration.showNotification(title, options) ); }); // Handle notification clicks self.addEventListener('notificationclick', (event) => { console.log('[SW] Notification click received:', event); event.notification.close(); const data = event.notification.data; const action = event.action; let url = '/'; if (action === 'view' && data?.itemId) { url = `/?highlight=${data.itemId}`; } else if (action === 'retry' && data?.itemId) { // Navigate to dashboard and trigger retry via postMessage url = `/?highlight=${data.itemId}&action=retry`; } else if (data?.itemId) { url = `/?highlight=${data.itemId}`; } event.waitUntil( clients.matchAll({ type: 'window', includeUncontrolled: true }) .then((clientsList) => { // Check if there's already a window/tab open for (const client of clientsList) { if (client.url.includes(self.location.origin) && 'focus' in client) { return client.focus().then(() => { // Send message to the client about the action return client.postMessage({ type: 'notification-action', action: action, data: data }); }); } } // If no window is open, open a new one if (clients.openWindow) { return clients.openWindow(url); } }) ); }); // Handle notification close self.addEventListener('notificationclose', (event) => { console.log('[SW] Notification closed:', event); // Track notification dismissals if needed const data = event.notification.data; if (data?.analytics) { // Could send analytics event here console.log('[SW] Notification dismissed:', data); } }); // Background sync for retry operations self.addEventListener('sync', (event) => { console.log('[SW] Background sync:', event.tag); if (event.tag === 'retry-queue-item') { event.waitUntil(handleRetrySync()); } }); // Helper functions function getNotificationTitle(type: string, data: any): string { switch (type) { case 'success': return data.recipeName ? `✅ Recipe Ready: ${data.recipeName}` : '✅ Recipe extraction complete'; case 'error': return '❌ Recipe extraction failed'; case 'progress': return `🔄 Processing recipe...`; default: return '📱 InstaRecipe Update'; } } async function handleRetrySync() { try { // Get retry items from IndexedDB or localStorage if needed console.log('[SW] Handling retry sync'); // This could implement background retry logic // For now, we'll let the main app handle retries return Promise.resolve(); } catch (error) { console.error('[SW] Retry sync failed:', error); throw error; } } // Message handling for communication with main app self.addEventListener('message', (event) => { console.log('[SW] Message received:', event.data); const { type, data } = event.data; switch (type) { case 'SKIP_WAITING': self.skipWaiting(); break; case 'GET_VERSION': event.ports[0].postMessage({ version: '1.0.0' }); break; case 'QUEUE_RETRY': // Queue a background sync for retry self.registration.sync.register('retry-queue-item'); break; default: console.log('[SW] Unknown message type:', type); } });