// Service Worker for offline support and performance optimization const CACHE_NAME = 'marketing-agent-v1' const STATIC_CACHE_NAME = 'marketing-agent-static-v1' const DYNAMIC_CACHE_NAME = 'marketing-agent-dynamic-v1' const API_CACHE_NAME = 'marketing-agent-api-v1' // Files to cache immediately const STATIC_ASSETS = [ '/', '/index.html', '/manifest.json', '/images/logo.png', '/images/placeholder.png' ] // API endpoints to cache const CACHEABLE_API_PATTERNS = [ /\/api\/v1\/users\/profile$/, /\/api\/v1\/campaigns\/templates$/, /\/api\/v1\/settings$/ ] // Install event - cache static assets self.addEventListener('install', (event) => { event.waitUntil( caches.open(STATIC_CACHE_NAME).then((cache) => { return cache.addAll(STATIC_ASSETS) }) ) self.skipWaiting() }) // Activate event - clean up old caches self.addEventListener('activate', (event) => { event.waitUntil( caches.keys().then((cacheNames) => { return Promise.all( cacheNames .filter((cacheName) => { return cacheName.startsWith('marketing-agent-') && cacheName !== CACHE_NAME && cacheName !== STATIC_CACHE_NAME && cacheName !== DYNAMIC_CACHE_NAME && cacheName !== API_CACHE_NAME }) .map((cacheName) => caches.delete(cacheName)) ) }) ) self.clients.claim() }) // Fetch event - implement caching strategies self.addEventListener('fetch', (event) => { const { request } = event const url = new URL(request.url) // Skip non-GET requests if (request.method !== 'GET') { return } // Handle API requests if (url.pathname.startsWith('/api/')) { event.respondWith(handleApiRequest(request)) return } // Handle static assets if (isStaticAsset(url.pathname)) { event.respondWith(handleStaticAsset(request)) return } // Handle dynamic content event.respondWith(handleDynamicContent(request)) }) // Cache-first strategy for static assets async function handleStaticAsset(request) { const cache = await caches.open(STATIC_CACHE_NAME) const cached = await cache.match(request) if (cached) { return cached } try { const response = await fetch(request) if (response.ok) { cache.put(request, response.clone()) } return response } catch (error) { return new Response('Offline - Asset not available', { status: 503, statusText: 'Service Unavailable' }) } } // Network-first strategy for API requests with fallback async function handleApiRequest(request) { const cache = await caches.open(API_CACHE_NAME) try { const response = await fetch(request) // Cache successful responses for cacheable endpoints if (response.ok && isCacheableApi(request.url)) { cache.put(request, response.clone()) } return response } catch (error) { // Try cache on network failure const cached = await cache.match(request) if (cached) { // Add header to indicate cached response const headers = new Headers(cached.headers) headers.set('X-From-Cache', 'true') return new Response(cached.body, { status: cached.status, statusText: cached.statusText, headers }) } // Return offline response return new Response( JSON.stringify({ error: 'Network unavailable', offline: true }), { status: 503, statusText: 'Service Unavailable', headers: { 'Content-Type': 'application/json' } } ) } } // Stale-while-revalidate strategy for dynamic content async function handleDynamicContent(request) { const cache = await caches.open(DYNAMIC_CACHE_NAME) const cached = await cache.match(request) const fetchPromise = fetch(request).then((response) => { if (response.ok) { cache.put(request, response.clone()) } return response }) return cached || fetchPromise } // Helper functions function isStaticAsset(pathname) { return /\.(js|css|png|jpg|jpeg|gif|svg|woff|woff2|ttf|eot)$/.test(pathname) } function isCacheableApi(url) { return CACHEABLE_API_PATTERNS.some(pattern => pattern.test(url)) } // Background sync for offline actions self.addEventListener('sync', (event) => { if (event.tag === 'sync-campaigns') { event.waitUntil(syncCampaigns()) } }) async function syncCampaigns() { const cache = await caches.open('offline-campaigns') const requests = await cache.keys() for (const request of requests) { try { const response = await fetch(request) if (response.ok) { await cache.delete(request) } } catch (error) { console.error('Failed to sync:', error) } } } // Push notifications self.addEventListener('push', (event) => { const options = { body: event.data ? event.data.text() : 'New notification', icon: '/images/logo.png', badge: '/images/badge.png', vibrate: [100, 50, 100], data: { dateOfArrival: Date.now(), primaryKey: 1 }, actions: [ { action: 'view', title: 'View', icon: '/images/checkmark.png' }, { action: 'close', title: 'Close', icon: '/images/xmark.png' } ] } event.waitUntil( self.registration.showNotification('Marketing Agent', options) ) }) self.addEventListener('notificationclick', (event) => { event.notification.close() if (event.action === 'view') { event.waitUntil( clients.openWindow('/') ) } }) // Message handling for cache control self.addEventListener('message', (event) => { if (event.data.type === 'SKIP_WAITING') { self.skipWaiting() } else if (event.data.type === 'CLEAR_CACHE') { event.waitUntil( caches.keys().then((cacheNames) => { return Promise.all( cacheNames.map((cacheName) => caches.delete(cacheName)) ) }) ) } })