455 lines
12 KiB
JavaScript
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
|
|
});
|
|
}
|
|
} |