bettertend/frontend/dist/sw.js

403 lines
9.9 KiB
JavaScript

/**
* 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)
}