/** * Atlas CMMS Service Worker * Provides offline capabilities, background sync, and caching strategies */ const CACHE_NAME = 'atlas-cmms-v1' const CACHE_ASSETS = [ '/', '/index.html', '/manifest.json', '/icons/icon-192x192.png', '/icons/icon-512x512.png' ] // URLs that require network-first strategy const NETWORK_FIRST_URLS = [ '/api/', '/auth/' ] // URLs that can be cached for offline use const CACHE_FIRST_URLS = [ '/assets/', '/icons/', '/screenshots/', '.js', '.css', '.woff', '.woff2', '.ttf', '.eot' ] // Background sync tag for offline actions const BACKGROUND_SYNC_TAG = 'atlas-cmms-sync' // IndexedDB configuration for offline data const DB_NAME = 'atlas-cmms-offline' const DB_VERSION = 1 const STORES = { pendingActions: 'pending-actions', offlineData: 'offline-data', syncQueue: 'sync-queue' } /** * Service Worker Installation */ self.addEventListener('install', event => { console.log('[SW] Installing service worker...') event.waitUntil( caches.open(CACHE_NAME) .then(cache => { console.log('[SW] Caching app shell') return cache.addAll(CACHE_ASSETS) }) .then(() => { console.log('[SW] App shell cached successfully') return self.skipWaiting() }) .catch(error => { console.error('[SW] Cache installation failed:', error) }) ) }) /** * Service Worker Activation */ self.addEventListener('activate', event => { console.log('[SW] Activating service worker...') event.waitUntil( Promise.all([ // Clean up old caches caches.keys().then(cacheNames => { return Promise.all( cacheNames.map(cacheName => { if (cacheName !== CACHE_NAME) { console.log('[SW] Deleting old cache:', cacheName) return caches.delete(cacheName) } }) ) }), // Initialize IndexedDB initializeDatabase(), // Take control of all clients self.clients.claim() ]) ) }) /** * Fetch Event Handler - Implements caching strategies */ self.addEventListener('fetch', event => { const { request } = event const url = new URL(request.url) // Skip non-GET requests and chrome-extension requests if (request.method !== 'GET' || url.protocol === 'chrome-extension:') { return } // Determine caching strategy based on URL if (isNetworkFirstUrl(url.pathname)) { event.respondWith(networkFirstStrategy(request)) } else if (isCacheFirstUrl(url.pathname)) { event.respondWith(cacheFirstStrategy(request)) } else { event.respondWith(staleWhileRevalidateStrategy(request)) } }) /** * Background Sync Event Handler */ self.addEventListener('sync', event => { console.log('[SW] Background sync triggered:', event.tag) if (event.tag === BACKGROUND_SYNC_TAG) { event.waitUntil(processPendingActions()) } }) /** * Push Event Handler for notifications */ self.addEventListener('push', event => { console.log('[SW] Push message received') const options = { body: event.data ? event.data.text() : 'New notification from Atlas CMMS', icon: '/icons/icon-192x192.png', badge: '/icons/badge-72x72.png', vibrate: [100, 50, 100], data: { timestamp: Date.now() }, actions: [ { action: 'view', title: 'View', icon: '/icons/action-view.png' }, { action: 'dismiss', title: 'Dismiss', icon: '/icons/action-dismiss.png' } ] } event.waitUntil( self.registration.showNotification('Atlas CMMS', options) ) }) /** * Notification Click Handler */ self.addEventListener('notificationclick', event => { console.log('[SW] Notification clicked:', event.action) event.notification.close() if (event.action === 'view') { event.waitUntil( clients.openWindow('/') ) } }) /** * Message Handler for communication with main thread */ self.addEventListener('message', event => { console.log('[SW] Message received:', event.data) if (event.data && event.data.type) { switch (event.data.type) { case 'SKIP_WAITING': self.skipWaiting() break case 'CACHE_URLS': cacheUrls(event.data.urls) break case 'CLEAR_CACHE': clearCache() break case 'SYNC_DATA': event.waitUntil(processPendingActions()) break } } }) /** * Network First Strategy - For dynamic content */ async function networkFirstStrategy(request) { try { const networkResponse = await fetch(request) // Cache successful responses if (networkResponse.ok) { const cache = await caches.open(CACHE_NAME) cache.put(request, networkResponse.clone()) } return networkResponse } catch (error) { console.log('[SW] Network failed, falling back to cache:', request.url) const cachedResponse = await caches.match(request) if (cachedResponse) { return cachedResponse } // Return offline page for navigation requests if (request.destination === 'document') { return caches.match('/offline.html') } throw error } } /** * Cache First Strategy - For static assets */ async function cacheFirstStrategy(request) { const cachedResponse = await caches.match(request) if (cachedResponse) { return cachedResponse } try { const networkResponse = await fetch(request) if (networkResponse.ok) { const cache = await caches.open(CACHE_NAME) cache.put(request, networkResponse.clone()) } return networkResponse } catch (error) { console.error('[SW] Cache and network both failed:', request.url) throw error } } /** * Stale While Revalidate Strategy - For general content */ async function staleWhileRevalidateStrategy(request) { const cache = await caches.open(CACHE_NAME) const cachedResponse = await cache.match(request) const fetchPromise = fetch(request).then(networkResponse => { if (networkResponse.ok) { cache.put(request, networkResponse.clone()) } return networkResponse }).catch(error => { console.log('[SW] Network request failed:', request.url) return cachedResponse }) return cachedResponse || fetchPromise } /** * Helper Functions */ function isNetworkFirstUrl(pathname) { return NETWORK_FIRST_URLS.some(pattern => pathname.includes(pattern)) } function isCacheFirstUrl(pathname) { return CACHE_FIRST_URLS.some(pattern => pathname.includes(pattern)) } async function initializeDatabase() { return new Promise((resolve, reject) => { const request = indexedDB.open(DB_NAME, DB_VERSION) request.onerror = () => reject(request.error) request.onsuccess = () => resolve(request.result) request.onupgradeneeded = event => { const db = event.target.result // Create object stores for offline functionality if (!db.objectStoreNames.contains(STORES.pendingActions)) { const store = db.createObjectStore(STORES.pendingActions, { keyPath: 'id', autoIncrement: true }) store.createIndex('timestamp', 'timestamp', { unique: false }) store.createIndex('type', 'type', { unique: false }) } if (!db.objectStoreNames.contains(STORES.offlineData)) { const store = db.createObjectStore(STORES.offlineData, { keyPath: 'id' }) store.createIndex('type', 'type', { unique: false }) store.createIndex('lastModified', 'lastModified', { unique: false }) } if (!db.objectStoreNames.contains(STORES.syncQueue)) { const store = db.createObjectStore(STORES.syncQueue, { keyPath: 'id', autoIncrement: true }) store.createIndex('priority', 'priority', { unique: false }) store.createIndex('retryCount', 'retryCount', { unique: false }) } } }) } async function processPendingActions() { console.log('[SW] Processing pending actions...') try { const db = await initializeDatabase() const transaction = db.transaction([STORES.pendingActions], 'readonly') const store = transaction.objectStore(STORES.pendingActions) const actions = await getAllFromStore(store) for (const action of actions) { try { await processAction(action) await removeAction(action.id) } catch (error) { console.error('[SW] Failed to process action:', action, error) await incrementRetryCount(action.id) } } } catch (error) { console.error('[SW] Failed to process pending actions:', error) } } async function processAction(action) { const { type, data, url, method = 'POST' } = action const response = await fetch(url, { method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }) if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`) } return response.json() } async function removeAction(actionId) { const db = await initializeDatabase() const transaction = db.transaction([STORES.pendingActions], 'readwrite') const store = transaction.objectStore(STORES.pendingActions) await store.delete(actionId) } async function incrementRetryCount(actionId) { const db = await initializeDatabase() const transaction = db.transaction([STORES.pendingActions], 'readwrite') const store = transaction.objectStore(STORES.pendingActions) const action = await store.get(actionId) if (action) { action.retryCount = (action.retryCount || 0) + 1 action.lastRetry = Date.now() await store.put(action) } } function getAllFromStore(store) { return new Promise((resolve, reject) => { const request = store.getAll() request.onsuccess = () => resolve(request.result) request.onerror = () => reject(request.error) }) } async function cacheUrls(urls) { const cache = await caches.open(CACHE_NAME) return cache.addAll(urls) } async function clearCache() { return caches.delete(CACHE_NAME) }