Files
ukaiautomation/sw.js

455 lines
12 KiB
JavaScript

// UK Data Services - Service Worker for PWA Functionality
// Version 1.0 - Advanced caching and offline support
const CACHE_NAME = 'ukds-pwa-v1.0.0';
const STATIC_CACHE = 'ukds-static-v1.0.0';
const DYNAMIC_CACHE = 'ukds-dynamic-v1.0.0';
const IMAGE_CACHE = 'ukds-images-v1.0.0';
// Files to cache immediately (critical resources)
const STATIC_ASSETS = [
'/',
'/index.php',
'/assets/css/main.css',
'/assets/js/main.js',
'/assets/images/ukds-main-logo.png',
'/assets/images/logo-white.svg',
'/assets/images/favicon.svg',
'/manifest.json',
'/offline.html'
];
// Network-first resources (always try network first)
const NETWORK_FIRST = [
'/quote.php',
'/contact-handler.php',
'/quote-handler.php'
];
// Cache-first resources (images, fonts, static assets)
const CACHE_FIRST = [
'/assets/images/',
'/assets/fonts/',
'https://fonts.googleapis.com/',
'https://fonts.gstatic.com/'
];
// Install event - cache critical resources
self.addEventListener('install', (event) => {
console.log('[SW] Installing service worker...');
event.waitUntil(
Promise.all([
// Cache static assets
caches.open(STATIC_CACHE).then((cache) => {
console.log('[SW] Caching static assets');
return cache.addAll(STATIC_ASSETS);
}),
// Skip waiting to activate immediately
self.skipWaiting()
])
);
});
// Activate event - clean up old caches
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 (!isCurrentCache(cacheName)) {
console.log('[SW] Deleting old cache:', cacheName);
return caches.delete(cacheName);
}
})
);
}),
// Take control of all clients
self.clients.claim()
])
);
});
// Fetch event - handle all network requests
self.addEventListener('fetch', (event) => {
const { request } = event;
const url = new URL(request.url);
// Skip non-GET requests
if (request.method !== 'GET') {
return;
}
// Skip external analytics and third-party requests
if (url.origin !== location.origin && !isCacheableExternal(url.href)) {
return;
}
event.respondWith(handleRequest(request));
});
// Handle different types of requests with appropriate strategies
async function handleRequest(request) {
const url = new URL(request.url);
try {
// Network-first strategy for forms and dynamic content
if (isNetworkFirst(request.url)) {
return await networkFirst(request);
}
// Cache-first strategy for images and static assets
if (isCacheFirst(request.url)) {
return await cacheFirst(request);
}
// Stale-while-revalidate for HTML pages
if (isHTML(request)) {
return await staleWhileRevalidate(request);
}
// Default to network-first
return await networkFirst(request);
} catch (error) {
console.log('[SW] Request failed:', error);
return await handleOffline(request);
}
}
// Network-first strategy
async function networkFirst(request) {
try {
const networkResponse = await fetch(request);
// Cache successful responses
if (networkResponse.ok) {
const cache = await caches.open(DYNAMIC_CACHE);
cache.put(request, networkResponse.clone());
}
return networkResponse;
} catch (error) {
// Fallback to cache
const cachedResponse = await caches.match(request);
if (cachedResponse) {
return cachedResponse;
}
throw error;
}
}
// Cache-first strategy
async function cacheFirst(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(getAppropriateCache(request.url));
cache.put(request, networkResponse.clone());
}
return networkResponse;
} catch (error) {
throw error;
}
}
// Stale-while-revalidate strategy
async function staleWhileRevalidate(request) {
const cachedResponse = await caches.match(request);
// Always try to update in background
const networkPromise = fetch(request).then(async (networkResponse) => {
if (networkResponse.ok) {
const cache = await caches.open(DYNAMIC_CACHE);
cache.put(request, networkResponse.clone());
}
return networkResponse;
}).catch(() => {
// Ignore network errors for background updates
});
// Return cached version immediately if available
if (cachedResponse) {
return cachedResponse;
}
// Wait for network if no cache
return await networkPromise;
}
// Handle offline scenarios
async function handleOffline(request) {
const url = new URL(request.url);
// Try to find cached version
const cachedResponse = await caches.match(request);
if (cachedResponse) {
return cachedResponse;
}
// Return offline page for HTML requests
if (isHTML(request)) {
const offlinePage = await caches.match('/offline.html');
if (offlinePage) {
return offlinePage;
}
}
// Return generic offline response
return new Response(
JSON.stringify({
error: 'Offline',
message: 'This content is not available offline'
}),
{
status: 503,
statusText: 'Service Unavailable',
headers: {
'Content-Type': 'application/json'
}
}
);
}
// Utility functions
function isCurrentCache(cacheName) {
return [CACHE_NAME, STATIC_CACHE, DYNAMIC_CACHE, IMAGE_CACHE].includes(cacheName);
}
function isNetworkFirst(url) {
return NETWORK_FIRST.some(pattern => url.includes(pattern));
}
function isCacheFirst(url) {
return CACHE_FIRST.some(pattern => url.includes(pattern));
}
function isHTML(request) {
return request.headers.get('accept')?.includes('text/html');
}
function isCacheableExternal(url) {
return url.includes('fonts.googleapis.com') ||
url.includes('fonts.gstatic.com') ||
url.includes('cdnjs.cloudflare.com');
}
function getAppropriateCache(url) {
if (url.includes('/assets/images/')) {
return IMAGE_CACHE;
}
if (url.includes('/assets/')) {
return STATIC_CACHE;
}
return DYNAMIC_CACHE;
}
// Background sync for form submissions
self.addEventListener('sync', (event) => {
if (event.tag === 'quote-submission') {
event.waitUntil(syncQuoteSubmissions());
}
if (event.tag === 'contact-submission') {
event.waitUntil(syncContactSubmissions());
}
});
// Handle quote submissions when back online
async function syncQuoteSubmissions() {
try {
const submissions = await getStoredSubmissions('quote');
for (const submission of submissions) {
try {
const response = await fetch('/quote-handler.php', {
method: 'POST',
body: submission.data
});
if (response.ok) {
await removeStoredSubmission('quote', submission.id);
console.log('[SW] Quote submission synced:', submission.id);
}
} catch (error) {
console.log('[SW] Failed to sync quote submission:', error);
}
}
} catch (error) {
console.log('[SW] Sync failed:', error);
}
}
// Handle contact submissions when back online
async function syncContactSubmissions() {
try {
const submissions = await getStoredSubmissions('contact');
for (const submission of submissions) {
try {
const response = await fetch('/contact-handler.php', {
method: 'POST',
body: submission.data
});
if (response.ok) {
await removeStoredSubmission('contact', submission.id);
console.log('[SW] Contact submission synced:', submission.id);
}
} catch (error) {
console.log('[SW] Failed to sync contact submission:', error);
}
}
} catch (error) {
console.log('[SW] Sync failed:', error);
}
}
// IndexedDB helpers for storing offline form submissions
async function getStoredSubmissions(type) {
// Simplified version - in production use IndexedDB
return [];
}
async function removeStoredSubmission(type, id) {
// Simplified version - in production use IndexedDB
return true;
}
// Push notification handling
self.addEventListener('push', (event) => {
console.log('[SW] Push received');
const options = {
body: event.data ? event.data.text() : 'New notification from UK Data Services',
icon: '/assets/images/favicon-192x192.png',
badge: '/assets/images/badge-72x72.png',
vibrate: [200, 100, 200],
tag: 'ukds-notification',
actions: [
{
action: 'view',
title: 'View',
icon: '/assets/images/icon-view.png'
},
{
action: 'dismiss',
title: 'Dismiss',
icon: '/assets/images/icon-dismiss.png'
}
]
};
event.waitUntil(
self.registration.showNotification('UK Data Services', options)
);
});
// Handle notification clicks
self.addEventListener('notificationclick', (event) => {
console.log('[SW] Notification clicked');
event.notification.close();
if (event.action === 'view') {
event.waitUntil(
clients.openWindow('/')
);
}
});
// Periodic background sync (if supported)
self.addEventListener('periodicsync', (event) => {
if (event.tag === 'update-cache') {
event.waitUntil(updateCriticalResources());
}
});
// Update critical resources in background
async function updateCriticalResources() {
try {
const cache = await caches.open(STATIC_CACHE);
const updatePromises = [
'/',
'/assets/css/main.css',
'/assets/js/main.js'
].map(url =>
fetch(url).then(response => {
if (response.ok) {
return cache.put(url, response);
}
}).catch(() => {
// Ignore failed updates
})
);
await Promise.all(updatePromises);
console.log('[SW] Critical resources updated');
} catch (error) {
console.log('[SW] Failed to update critical resources:', error);
}
}
// Message handling from main thread
self.addEventListener('message', (event) => {
if (event.data && event.data.type === 'SKIP_WAITING') {
self.skipWaiting();
}
if (event.data && event.data.type === 'CACHE_URLS') {
event.waitUntil(
cacheUrls(event.data.urls)
);
}
});
// Cache specific URLs on demand
async function cacheUrls(urls) {
const cache = await caches.open(DYNAMIC_CACHE);
const cachePromises = urls.map(url =>
fetch(url).then(response => {
if (response.ok) {
return cache.put(url, response);
}
}).catch(() => {
// Ignore failed caching
})
);
await Promise.all(cachePromises);
}
// Analytics for service worker performance
function trackSWEvent(eventType, details = {}) {
// Send analytics data when online
if (navigator.onLine) {
fetch('/analytics/sw-events', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
type: eventType,
timestamp: Date.now(),
...details
})
}).catch(() => {
// Ignore analytics failures
});
}
}