tractatus/public/service-worker.js
TheFlow 9838efb593 fix: force cache invalidation - bump to v0.1.4
CRITICAL: Force all clients to dump old cached content

- Increment service worker CACHE_VERSION from 0.1.3 to 0.1.4
- Update version.json to 0.1.4
- Forces immediate cache invalidation for all users
- Ensures Phase 2 content (Agent Lightning, Discord) visible immediately

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 13:02:29 +13:00

231 lines
7 KiB
JavaScript

/**
* Tractatus Service Worker
* - Version management and update notifications
* - Cache management for offline support
* - PWA functionality
*/
const CACHE_VERSION = '0.1.4';
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 AGGRESSIVELY
self.addEventListener('activate', (event) => {
console.log('[Service Worker] Activating new version:', CACHE_VERSION);
event.waitUntil(
caches.keys().then((cacheNames) => {
console.log('[Service Worker] Deleting ALL caches:', 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(() => {
console.log('[Service Worker] Taking control of all clients immediately');
// Take control of all clients immediately (don't wait for page reload)
return self.clients.claim();
}).then(() => {
// Notify all clients that cache has been cleared
return self.clients.matchAll().then((clients) => {
clients.forEach((client) => {
console.log('[Service Worker] Notifying client to reload:', client.url);
client.postMessage({
type: 'CACHE_CLEARED',
version: CACHE_VERSION,
message: 'Service worker updated - reload for latest content'
});
});
});
})
);
});
// 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, {
cache: 'no-store', // Force fresh fetch, bypass all caches
headers: {
'Cache-Control': 'no-cache, no-store, must-revalidate',
'Pragma': 'no-cache'
}
})
.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
});
});
});
}
})
);
}
});