403 lines
9.9 KiB
JavaScript
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)
|
|
} |