oneuptime/Dashboard/sw.js.template

386 lines
12 KiB
Text

/* eslint-disable no-restricted-globals */
// OneUptime Progressive Web App Service Worker
// Handles push notifications and caching for PWA functionality
console.log('[ServiceWorker] OneUptime PWA Service Worker Loaded');
// Cache configuration - Updated dynamically during build
// Version format: oneuptime-v{APP_VERSION}-{GIT_SHA}
// This ensures cache invalidation on every deployment
const CACHE_VERSION = 'oneuptime-v{{APP_VERSION}}-{{GIT_SHA}}'; // Auto-generated version
const STATIC_CACHE = `${CACHE_VERSION}-static`;
const DYNAMIC_CACHE = `${CACHE_VERSION}-dynamic`;
// Cache duration configuration (in milliseconds)
const CACHE_DURATIONS = {
static: 7 * 24 * 60 * 60 * 1000, // 7 days for static assets
dynamic: 24 * 60 * 60 * 1000, // 1 day for dynamic content
};
// Assets to cache immediately during install
const STATIC_ASSETS = [
'/dashboard/',
'/dashboard/manifest.json',
'/dashboard/offline.html',
'/dashboard/assets/img/favicons/favicon.ico',
'/dashboard/assets/img/favicons/android-chrome-192x192.png',
'/dashboard/assets/img/favicons/android-chrome-512x512.png',
// Add other critical assets as needed
];
// Install event - cache static assets
self.addEventListener('install', function(event) {
console.log('[ServiceWorker] Installing...');
event.waitUntil(
Promise.all([
// Cache static assets
caches.open(STATIC_CACHE).then(function(cache) {
console.log('[ServiceWorker] Pre-caching static assets');
return cache.addAll(STATIC_ASSETS.filter(url => url !== '/dashboard/'));
}),
// Skip waiting to activate immediately
self.skipWaiting()
])
);
});
// Activate event - clean up old caches
self.addEventListener('activate', function(event) {
console.log('[ServiceWorker] Activating...');
event.waitUntil(
Promise.all([
// Clean up old caches
caches.keys().then(function(cacheNames) {
return Promise.all(
cacheNames.map(function(cacheName) {
if (cacheName.startsWith('oneuptime-') &&
!cacheName.startsWith(CACHE_VERSION)) {
console.log('[ServiceWorker] Deleting old cache:', cacheName);
return caches.delete(cacheName);
}
})
);
}),
// Claim all clients
self.clients.claim()
])
);
});
// Fetch event - implement caching strategies
self.addEventListener('fetch', function(event) {
const request = event.request;
const url = new URL(request.url);
// Skip non-GET requests
if (request.method !== 'GET') {
return;
}
// Skip chrome-extension and other non-http(s) requests
if (!url.protocol.startsWith('http')) {
return;
}
event.respondWith(handleRequest(request));
});
// Request handling with different caching strategies
async function handleRequest(request) {
const url = new URL(request.url);
const pathname = url.pathname;
try {
// Strategy 1: Network First for HTML pages (with fallback)
if (pathname.endsWith('/') || pathname.endsWith('.html') ||
pathname === '/dashboard' || pathname.startsWith('/dashboard/') && !pathname.includes('.')) {
return await networkFirstWithFallback(request, DYNAMIC_CACHE);
}
// Strategy 2: Cache First for JavaScript, CSS, and other static assets
if (pathname.includes('/dist/') || pathname.match(/\.(js|css|woff|woff2|ttf|otf|eot)$/)) {
return await cacheFirstWithUpdate(request, STATIC_CACHE);
}
// Strategy 3: Cache First for images and other media
if (pathname.match(/\.(png|jpe?g|gif|svg|ico|webp|avif)$/)) {
return await cacheFirstWithUpdate(request, STATIC_CACHE);
}
// Strategy 5: Network First for everything else
return await networkFirstWithFallback(request, DYNAMIC_CACHE);
} catch (error) {
console.error('[ServiceWorker] Request handling error:', error);
// Return offline page for navigation requests
if (request.mode === 'navigate') {
const offlineResponse = await caches.match('/dashboard/offline.html');
if (offlineResponse) {
return offlineResponse;
}
}
// Return a basic offline response
return new Response('Offline - Please check your internet connection', {
status: 503,
statusText: 'Service Unavailable',
headers: { 'Content-Type': 'text/plain' }
});
}
}
// Caching Strategy 1: Network First with Fallback (for HTML)
async function networkFirstWithFallback(request, cacheName) {
try {
const networkResponse = await fetch(request);
if (networkResponse.ok) {
// Cache successful responses
const cache = await caches.open(cacheName);
cache.put(request, networkResponse.clone());
}
return networkResponse;
} catch (error) {
console.log('[ServiceWorker] Network failed, trying cache:', request.url);
const cachedResponse = await caches.match(request);
if (cachedResponse) {
return cachedResponse;
}
throw error;
}
}
// Caching Strategy 2: Cache First with Background Update (for static assets)
async function cacheFirstWithUpdate(request, cacheName) {
const cachedResponse = await caches.match(request);
if (cachedResponse) {
// Return cached version immediately
// Background update if cache is old
const cacheDate = new Date(cachedResponse.headers.get('date') || 0);
const now = new Date();
const age = now.getTime() - cacheDate.getTime();
if (age > CACHE_DURATIONS.static) {
// Background update - don't await
updateCacheInBackground(request, cacheName);
}
return cachedResponse;
}
// Not in cache, fetch from network
try {
const networkResponse = await fetch(request);
if (networkResponse.ok) {
const cache = await caches.open(cacheName);
cache.put(request, networkResponse.clone());
}
return networkResponse;
} catch (error) {
console.error('[ServiceWorker] Failed to fetch asset:', request.url, error);
throw error;
}
}
// Background cache update
async function updateCacheInBackground(request, cacheName) {
try {
const networkResponse = await fetch(request);
if (networkResponse.ok) {
const cache = await caches.open(cacheName);
await cache.put(request, networkResponse);
console.log('[ServiceWorker] Background cache update:', request.url);
}
} catch (error) {
console.log('[ServiceWorker] Background update failed:', request.url, error);
}
}
// Handle push subscription changes
self.addEventListener('pushsubscriptionchange', function(event) {
console.log('[ServiceWorker] Push subscription changed:', event);
// Re-subscribe to push notifications
event.waitUntil(
self.registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: null // This should be set by your application
})
.then(function(newSubscription) {
console.log('[ServiceWorker] New push subscription:', newSubscription);
// Send new subscription to your server
return updatePushSubscription(newSubscription);
})
.catch(function(error) {
console.error('[ServiceWorker] Failed to resubscribe to push:', error);
})
);
});
// Handle push notifications
self.addEventListener('push', function(event) {
console.log('[ServiceWorker] Push received:', event);
console.log('[ServiceWorker] Event data available:', !!event.data);
if (event.data) {
try {
const dataText = event.data.text();
console.log('[ServiceWorker] Raw push data:', dataText);
const data = event.data.json();
console.log('[ServiceWorker] Push data (parsed):', data);
const options = {
body: data.body,
icon: data.icon || '/dashboard/assets/img/favicons/android-chrome-192x192.png',
badge: data.badge || '/dashboard/assets/img/favicons/favicon-32x32.png',
tag: data.tag || 'oneuptime-notification',
requireInteraction: data.requireInteraction || false,
actions: data.actions || [],
data: data.data || {},
silent: false,
renotify: true,
vibrate: [100, 50, 100],
timestamp: Date.now()
};
console.log('[ServiceWorker] Showing notification with options:', options);
event.waitUntil(
self.registration.showNotification(data.title, options)
.then(() => {
console.log('[ServiceWorker] Notification shown successfully');
})
.catch((error) => {
console.error('[ServiceWorker] Error showing notification:', error);
})
);
} catch (error) {
console.error('[ServiceWorker] Error parsing push data:', error);
const rawData = event.data ? event.data.text() : 'No data';
console.log('[ServiceWorker] Raw event data:', rawData);
// Show fallback notification
event.waitUntil(
self.registration.showNotification('OneUptime Notification', {
body: 'You have a new notification from OneUptime',
icon: '/dashboard/assets/img/favicons/android-chrome-192x192.png',
tag: 'oneuptime-fallback',
data: { url: '/dashboard' }
})
);
}
} else {
console.log('[ServiceWorker] Push event received but no data');
// Show default notification
event.waitUntil(
self.registration.showNotification('OneUptime', {
body: 'You have a new notification',
icon: '/dashboard/assets/img/favicons/android-chrome-192x192.png',
tag: 'oneuptime-default',
data: { url: '/dashboard' }
})
);
}
});
// Handle notification clicks
self.addEventListener('notificationclick', function(event) {
console.log('[ServiceWorker] Notification clicked:', event.notification.tag);
event.notification.close();
const clickAction = event.action;
const notificationData = event.notification.data || {};
let targetUrl = '/dashboard';
if (clickAction && notificationData[clickAction]) {
// Handle action button clicks
targetUrl = notificationData[clickAction].url || targetUrl;
} else {
// Handle main notification click
targetUrl = notificationData.url ||
notificationData.clickAction ||
notificationData.link ||
targetUrl;
}
event.waitUntil(
clients.matchAll({ type: 'window', includeUncontrolled: true })
.then(function(clientList) {
// Check if there's already a OneUptime window open
for (let i = 0; i < clientList.length; i++) {
const client = clientList[i];
const clientUrl = new URL(client.url);
if (clientUrl.origin === self.location.origin && 'focus' in client) {
// Navigate to the target URL and focus the window
return client.navigate(targetUrl)
.then(() => client.focus());
}
}
// If no OneUptime window is open, open a new one
if (clients.openWindow) {
return clients.openWindow(targetUrl);
}
})
.catch(function(error) {
console.error('[ServiceWorker] Error handling notification click:', error);
})
);
});
// Handle notification close events
self.addEventListener('notificationclose', function(event) {
console.log('[ServiceWorker] Notification closed:', event.notification.tag);
});
// Handle background sync - removed offline functionality
self.addEventListener('sync', function(event) {
console.log('[ServiceWorker] Background sync:', event.tag);
// Background sync events can still be handled but no offline caching
});
// Handle messages from the main thread
self.addEventListener('message', function(event) {
console.log('[ServiceWorker] Message received:', event.data);
if (event.data && event.data.type === 'SKIP_WAITING') {
self.skipWaiting();
} else if (event.data && event.data.type === 'GET_VERSION') {
event.ports[0].postMessage({ version: CACHE_VERSION });
}
});
// Helper function to update push subscription
function updatePushSubscription(subscription) {
return fetch('/api/push-subscription', {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(subscription)
})
.catch(function(error) {
console.error('[ServiceWorker] Error updating push subscription:', error);
});
}
console.log('[ServiceWorker] OneUptime PWA Service Worker Ready');