// Bambuddy Service Worker const CACHE_NAME = 'bambuddy-v4'; const STATIC_CACHE = 'bambuddy-static-v4'; // Static assets to cache on install const STATIC_ASSETS = [ '/', '/manifest.json', '/img/favicon.png', '/img/favicon-16x16.png', '/img/favicon-32x32.png', '/img/android-chrome-192x192.png', '/img/android-chrome-512x512.png', '/img/apple-touch-icon.png', '/img/bambuddy_logo_dark.png', ]; // Install event - cache static assets self.addEventListener('install', (event) => { console.log('[SW] Installing service worker...'); event.waitUntil( caches.open(STATIC_CACHE).then((cache) => { console.log('[SW] Caching static assets'); return cache.addAll(STATIC_ASSETS); }) ); // Activate immediately self.skipWaiting(); }); // Activate event - clean up old caches self.addEventListener('activate', (event) => { console.log('[SW] Activating service worker...'); event.waitUntil( caches.keys().then((cacheNames) => { return Promise.all( cacheNames .filter((name) => name !== CACHE_NAME && name !== STATIC_CACHE) .map((name) => { console.log('[SW] Deleting old cache:', name); return caches.delete(name); }) ); }) ); // Take control immediately self.clients.claim(); }); // Fetch event - network-first for API, cache-first for static self.addEventListener('fetch', (event) => { const { request } = event; const url = new URL(request.url); // Skip non-GET requests if (request.method !== 'GET') { return; } // Skip WebSocket connections if (url.protocol === 'ws:' || url.protocol === 'wss:') { return; } // API requests - network first, no cache (real-time data is critical) if (url.pathname.startsWith('/api/')) { event.respondWith( fetch(request).catch(() => { // Return offline response for API failures return new Response( JSON.stringify({ error: 'offline', message: 'You are currently offline' }), { status: 503, headers: { 'Content-Type': 'application/json' }, } ); }) ); return; } // Static assets - cache first, then network if ( url.pathname.startsWith('/img/') || url.pathname.startsWith('/icons/') || url.pathname.endsWith('.png') || url.pathname.endsWith('.jpg') || url.pathname.endsWith('.svg') || url.pathname.endsWith('.ico') ) { event.respondWith( caches.match(request).then((cached) => { if (cached) { return cached; } return fetch(request).then((response) => { // Cache successful responses if (response.ok) { const clone = response.clone(); caches.open(STATIC_CACHE).then((cache) => { cache.put(request, clone); }); } return response; }); }) ); return; } // JS/CSS assets - stale-while-revalidate if ( url.pathname.startsWith('/assets/') || url.pathname.endsWith('.js') || url.pathname.endsWith('.css') ) { event.respondWith( caches.match(request).then((cached) => { const fetchPromise = fetch(request).then((response) => { if (response.ok) { const clone = response.clone(); caches.open(CACHE_NAME).then((cache) => { cache.put(request, clone); }); } return response; }); return cached || fetchPromise; }) ); return; } // HTML pages - network first, fall back to cache event.respondWith( fetch(request) .then((response) => { if (response.ok) { const clone = response.clone(); caches.open(CACHE_NAME).then((cache) => { cache.put(request, clone); }); } return response; }) .catch(() => { return caches.match(request).then((cached) => { if (cached) { return cached; } // Return cached index for SPA navigation return caches.match('/'); }); }) ); }); // Handle push notifications (for future use) self.addEventListener('push', (event) => { if (!event.data) return; const data = event.data.json(); const options = { body: data.body || 'New notification from Bambuddy', icon: '/img/android-chrome-192x192.png', badge: '/img/favicon-32x32.png', vibrate: [100, 50, 100], data: { url: data.url || '/', }, }; event.waitUntil( self.registration.showNotification(data.title || 'Bambuddy', options) ); }); // Handle notification clicks self.addEventListener('notificationclick', (event) => { event.notification.close(); const url = event.notification.data?.url || '/'; event.waitUntil( clients.matchAll({ type: 'window', includeUncontrolled: true }).then((windowClients) => { // Check if there's already a window open for (const client of windowClients) { if (client.url.includes(self.location.origin) && 'focus' in client) { client.navigate(url); return client.focus(); } } // Open a new window if none exists if (clients.openWindow) { return clients.openWindow(url); } }) ); });