tractatus/public/service-worker.js
TheFlow 5c902324a1 fix: restore cache version 0.1.2 (reverted by e0e4b5f)
The automated cache update in e0e4b5f accidentally reverted service worker
cache version from 0.1.2 back to 0.1.1. Restoring to 0.1.2 to ensure homepage
cultural DNA updates are served to visitors.
2025-10-28 09:12:22 +13:00

210 lines
6 KiB
JavaScript

/**
* Tractatus Service Worker
* - Version management and update notifications
* - Cache management for offline support
* - PWA functionality
*/
const CACHE_VERSION = '0.1.2';
const CACHE_NAME = `tractatus-v${CACHE_VERSION}`;
const VERSION_CHECK_INTERVAL = 3600000; // 1 hour in milliseconds
// Paths that should NEVER be cached (always fetch fresh from network)
const NEVER_CACHE_PATHS = [
'/js/admin/', // Admin JavaScript - always fresh
'/api/', // API calls
'/admin/', // Admin pages
'/locales/' // Translation files - always fresh
];
// Assets to cache immediately on install
const CRITICAL_ASSETS = [
'/',
'/index.html',
'/css/tailwind.css',
'/js/components/navbar.js',
'/images/tractatus-icon.svg',
'/favicon.svg'
];
// Install event - cache critical assets
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => {
console.log('[Service Worker] Caching critical assets');
return cache.addAll(CRITICAL_ASSETS);
}).then(() => {
// Force activation of new service worker
return self.skipWaiting();
})
);
});
// Activate event - clean up old caches
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
// Delete ALL caches (including current) to force fresh fetch
cacheNames.map((name) => {
console.log('[Service Worker] Deleting cache:', name);
return caches.delete(name);
})
);
}).then(() => {
// Take control of all clients immediately
return self.clients.claim();
})
);
});
// Fetch event - network-first strategy with cache fallback
self.addEventListener('fetch', (event) => {
const { request } = event;
const url = new URL(request.url);
// Skip chrome-extension and other non-http requests
if (!url.protocol.startsWith('http')) {
return;
}
// NEVER CACHE: Admin files, API calls, translations - bypass service worker entirely
if (NEVER_CACHE_PATHS.some(path => url.pathname.startsWith(path))) {
// Don't intercept at all - let browser handle it directly
return;
}
// HTML files: Network-ONLY (never cache, always fetch fresh)
// This ensures users always get the latest content without cache refresh
if (request.destination === 'document' || url.pathname.endsWith('.html')) {
event.respondWith(
fetch(request)
.catch(() => {
// Only for offline fallback: serve cached index.html
if (url.pathname === '/' || url.pathname === '/index.html') {
return caches.match('/index.html');
}
// All other HTML: network only, fail if offline
throw new Error('Network required for HTML pages');
})
);
return;
}
// Static assets (CSS, JS, images): Network-first for versioned URLs, cache-first for others
if (
request.destination === 'style' ||
request.destination === 'script' ||
request.destination === 'image' ||
request.destination === 'font'
) {
// If URL has version parameter, always fetch fresh (network-first)
const hasVersionParam = url.searchParams.has('v');
if (hasVersionParam) {
// Network-first for versioned assets (ensures cache-busting works)
event.respondWith(
fetch(request).then((response) => {
// Cache the response for offline use
const responseClone = response.clone();
caches.open(CACHE_NAME).then((cache) => {
cache.put(request, responseClone);
});
return response;
}).catch(() => {
// Fallback to cache if offline
return caches.match(request);
})
);
} else {
// Cache-first for non-versioned assets
event.respondWith(
caches.match(request).then((cachedResponse) => {
if (cachedResponse) {
return cachedResponse;
}
return fetch(request).then((response) => {
const responseClone = response.clone();
caches.open(CACHE_NAME).then((cache) => {
cache.put(request, responseClone);
});
return response;
});
})
);
}
return;
}
// API calls and other requests: Network-first
event.respondWith(
fetch(request)
.then((response) => {
return response;
})
.catch(() => {
return caches.match(request);
})
);
});
// Message event - handle version checks from clients
self.addEventListener('message', (event) => {
if (event.data.type === 'CHECK_VERSION') {
checkVersion().then((versionInfo) => {
event.ports[0].postMessage({
type: 'VERSION_INFO',
...versionInfo
});
});
}
if (event.data.type === 'SKIP_WAITING') {
self.skipWaiting();
}
});
// Check for version updates
async function checkVersion() {
try {
const response = await fetch('/version.json', { cache: 'no-store' });
const serverVersion = await response.json();
return {
currentVersion: CACHE_VERSION,
serverVersion: serverVersion.version,
updateAvailable: CACHE_VERSION !== serverVersion.version,
forceUpdate: serverVersion.forceUpdate,
changelog: serverVersion.changelog
};
} catch (error) {
console.error('[Service Worker] Version check failed:', error);
return {
currentVersion: CACHE_VERSION,
serverVersion: null,
updateAvailable: false,
error: true
};
}
}
// Periodic background sync for version checks (if supported)
self.addEventListener('periodicsync', (event) => {
if (event.tag === 'version-check') {
event.waitUntil(
checkVersion().then((versionInfo) => {
if (versionInfo.updateAvailable) {
// Notify all clients about update
self.clients.matchAll().then((clients) => {
clients.forEach((client) => {
client.postMessage({
type: 'UPDATE_AVAILABLE',
...versionInfo
});
});
});
}
})
);
}
});