Matt a279bb6aa9
Some checks failed
Build and Push Docker Images / docker (push) Failing after 44s
Add automatic cache clearing and version management to prevent white screen issues
Implements comprehensive service worker solution with:
- Dynamic versioning using git commit hash or timestamp
- Automatic cache invalidation on new deployments
- Hourly update checks and user notifications
- Network-first caching strategy with 24-hour expiration
- Build automation via prebuild script
- Update notification UI component

This prevents stale cached code from causing white screens by ensuring users always get the latest version after deployment.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-04 15:32:50 +01:00

125 lines
3.6 KiB
JavaScript

// Service Worker with automatic versioning and cache invalidation
// Version is updated on each build to force cache refresh
const BUILD_TIMESTAMP = '__BUILD_TIMESTAMP__'; // Replaced during build
const CACHE_NAME = `puffin-calculator-${BUILD_TIMESTAMP}`;
const MAX_CACHE_AGE = 24 * 60 * 60 * 1000; // 24 hours in milliseconds
const urlsToCache = [
'/',
'/mobile-app',
'/puffinOffset.webp',
'/manifest.json'
];
// Install event - cache resources
self.addEventListener('install', (event) => {
console.log('[SW] Installing service worker version:', BUILD_TIMESTAMP);
event.waitUntil(
caches.open(CACHE_NAME)
.then((cache) => {
console.log('[SW] Caching app shell');
return cache.addAll(urlsToCache);
})
.then(() => {
// Force the waiting service worker to become the active service worker
return self.skipWaiting();
})
.catch((error) => {
console.error('[SW] Cache install failed:', error);
})
);
});
// Activate event - clear old caches and claim clients
self.addEventListener('activate', (event) => {
console.log('[SW] Activating service worker version:', BUILD_TIMESTAMP);
event.waitUntil(
Promise.all([
// Clear old caches
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames.map((cacheName) => {
if (cacheName !== CACHE_NAME) {
console.log('[SW] Clearing old cache:', cacheName);
return caches.delete(cacheName);
}
})
);
}),
// Claim all clients immediately
self.clients.claim()
])
);
});
// Fetch event - Network first, fall back to cache
self.addEventListener('fetch', (event) => {
const { request } = event;
const url = new URL(request.url);
// Skip cross-origin requests
if (url.origin !== location.origin) {
return;
}
event.respondWith(
// Try network first
fetch(request)
.then((response) => {
// Don't cache if not a success response
if (!response || response.status !== 200 || response.type !== 'basic') {
return response;
}
// Clone the response
const responseToCache = response.clone();
// Cache the fetched resource
caches.open(CACHE_NAME).then((cache) => {
cache.put(request, responseToCache);
});
return response;
})
.catch(() => {
// Network failed, try cache
return caches.match(request).then((cachedResponse) => {
if (cachedResponse) {
// Check cache age for HTML documents
if (request.destination === 'document') {
const cacheDate = cachedResponse.headers.get('sw-cache-date');
if (cacheDate) {
const age = Date.now() - parseInt(cacheDate, 10);
if (age > MAX_CACHE_AGE) {
console.log('[SW] Cached HTML is too old, returning without cache');
return new Response('Cache expired. Please connect to the internet.', {
status: 503,
statusText: 'Service Unavailable'
});
}
}
}
return cachedResponse;
}
// Not in cache and network failed
return new Response('Offline and not cached', {
status: 503,
statusText: 'Service Unavailable'
});
});
})
);
});
// Listen for skip waiting message
self.addEventListener('message', (event) => {
if (event.data && event.data.type === 'SKIP_WAITING') {
console.log('[SW] Received SKIP_WAITING message');
self.skipWaiting();
}
});