justin.deal/public/service-worker.js

226 lines
6.1 KiB
JavaScript
Raw Normal View History

2025-05-03 14:20:57 -07:00
/**
* Service Worker for justin.deal
* Provides caching and offline support
*/
const CACHE_NAME = 'justin-deal-v1';
// Assets to cache immediately on service worker install
const PRECACHE_ASSETS = [
'/',
'/index.html',
'/favicon.svg',
'/site.webmanifest',
'/favicons/favicon.png',
'/favicons/apple-touch-icon.png',
'/favicons/favicon-16x16.png',
'/favicons/favicon-32x32.png'
];
// Cache strategies
const CACHE_STRATEGIES = {
// Cache first, falling back to network
CACHE_FIRST: 'cache-first',
// Network first, falling back to cache
NETWORK_FIRST: 'network-first',
// Cache only (no network request)
CACHE_ONLY: 'cache-only',
// Network only (no cache lookup)
NETWORK_ONLY: 'network-only',
// Stale while revalidate (serve from cache, update cache in background)
STALE_WHILE_REVALIDATE: 'stale-while-revalidate'
};
// Route patterns and their corresponding cache strategies
const ROUTE_STRATEGIES = [
// HTML pages - network first
{
pattern: /\.html$|\/$/,
strategy: CACHE_STRATEGIES.NETWORK_FIRST
},
// CSS and JS - stale while revalidate
{
pattern: /\.(css|js)$/,
strategy: CACHE_STRATEGIES.STALE_WHILE_REVALIDATE
},
// Images - cache first
{
pattern: /\.(jpe?g|png|gif|svg|webp|avif)$/,
strategy: CACHE_STRATEGIES.CACHE_FIRST
},
// Fonts - cache first
{
pattern: /\.(woff2?|ttf|otf|eot)$/,
strategy: CACHE_STRATEGIES.CACHE_FIRST
},
// API requests - network first
{
pattern: /\/api\//,
strategy: CACHE_STRATEGIES.NETWORK_FIRST
}
];
// Determine cache strategy for a given URL
function getStrategyForUrl(url) {
const matchedRoute = ROUTE_STRATEGIES.find(route => route.pattern.test(url));
return matchedRoute ? matchedRoute.strategy : CACHE_STRATEGIES.NETWORK_FIRST;
}
// Install event - precache critical assets
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => cache.addAll(PRECACHE_ASSETS))
.then(() => self.skipWaiting())
);
});
// Activate event - clean up old caches
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames
.filter(cacheName => cacheName !== CACHE_NAME)
.map(cacheName => caches.delete(cacheName))
);
}).then(() => self.clients.claim())
);
});
// Helper function to handle network-first strategy
async function networkFirstStrategy(request) {
try {
// Try network first
const networkResponse = await fetch(request);
// If successful, clone and cache the response
if (networkResponse.ok) {
const cache = await caches.open(CACHE_NAME);
cache.put(request, networkResponse.clone());
}
return networkResponse;
} catch (error) {
// If network fails, try cache
const cachedResponse = await caches.match(request);
if (cachedResponse) {
return cachedResponse;
}
// If no cache, throw error
throw error;
}
}
// Helper function to handle cache-first strategy
async function cacheFirstStrategy(request) {
// Try cache first
const cachedResponse = await caches.match(request);
if (cachedResponse) {
return cachedResponse;
}
// If not in cache, get from network
const networkResponse = await fetch(request);
// Cache the response for future
if (networkResponse.ok) {
const cache = await caches.open(CACHE_NAME);
cache.put(request, networkResponse.clone());
}
return networkResponse;
}
// Helper function to handle stale-while-revalidate strategy
async function staleWhileRevalidateStrategy(request) {
// Try to get from cache
const cachedResponse = await caches.match(request);
// Fetch from network to update cache (don't await)
const fetchPromise = fetch(request)
.then(networkResponse => {
if (networkResponse.ok) {
const cache = caches.open(CACHE_NAME);
cache.then(cache => cache.put(request, networkResponse.clone()));
}
return networkResponse;
})
.catch(error => {
console.error('Failed to fetch and update cache:', error);
});
// Return cached response immediately if available
return cachedResponse || fetchPromise;
}
// Fetch event - handle all fetch requests
self.addEventListener('fetch', event => {
// Skip non-GET requests and browser extensions
if (event.request.method !== 'GET' ||
event.request.url.startsWith('chrome-extension://') ||
event.request.url.includes('extension://')) {
return;
}
// Get the appropriate strategy for this URL
const strategy = getStrategyForUrl(event.request.url);
// Apply the selected strategy
switch (strategy) {
case CACHE_STRATEGIES.NETWORK_FIRST:
event.respondWith(networkFirstStrategy(event.request));
break;
case CACHE_STRATEGIES.CACHE_FIRST:
event.respondWith(cacheFirstStrategy(event.request));
break;
case CACHE_STRATEGIES.STALE_WHILE_REVALIDATE:
event.respondWith(staleWhileRevalidateStrategy(event.request));
break;
case CACHE_STRATEGIES.CACHE_ONLY:
event.respondWith(caches.match(event.request));
break;
case CACHE_STRATEGIES.NETWORK_ONLY:
event.respondWith(fetch(event.request));
break;
default:
// Default to network first
event.respondWith(networkFirstStrategy(event.request));
}
});
// Handle offline fallback
self.addEventListener('fetch', event => {
// Only handle HTML navigation requests that fail
if (event.request.mode === 'navigate') {
event.respondWith(
fetch(event.request)
.catch(() => {
// If fetch fails, return the offline page
return caches.match('/offline.html')
.then(response => {
return response || new Response(
'<html><body><h1>Offline</h1><p>You are currently offline.</p></body></html>',
{
headers: { 'Content-Type': 'text/html' }
}
);
});
})
);
}
});
// Listen for messages from the client
self.addEventListener('message', event => {
if (event.data && event.data.type === 'SKIP_WAITING') {
self.skipWaiting();
}
});