const CACHE_NAME = 'rd-player-v18'; // Install: skip waiting to activate immediately self.addEventListener('install', (event) => { self.skipWaiting(); }); // Activate: clean old caches (including old content cache), claim clients self.addEventListener('activate', (event) => { event.waitUntil( caches.keys().then(keys => Promise.all( keys.filter(k => k !== CACHE_NAME).map(k => caches.delete(k)) )).then(() => self.clients.claim()) ); }); // Fetch handler — ONLY cache player page and static assets. // Content files (/uploads/content/) are NOT intercepted — the server sets // Cache-Control: public, max-age=2592000, immutable which lets the browser // cache them natively without SW complications (range requests, opaque // responses, video seeking, etc.) self.addEventListener('fetch', (event) => { // Only handle GET requests if (event.request.method !== 'GET') return; const url = new URL(event.request.url); // Widget renders pinned to a revision: cache-FIRST, because those exact bytes cannot change // without the rev changing. This is what lets a widget keep rendering when the network is gone — // previously the server sent no-store for every render, so widgets were the one thing the // player's offline cache could never hold, and a display that lost its uplink lost them. // ignoreSearch is deliberately NOT used here: the query string carries the rev, and ignoring it // would match a different revision's entry, which is the staleness we are trying to remove. if (url.pathname.startsWith('/api/widgets/') && url.pathname.endsWith('/render') && url.searchParams.has('rev')) { event.respondWith( caches.match(event.request).then(cached => { if (cached) return cached; return fetch(event.request).then(response => { if (response.ok && response.type !== 'opaque') { const clone = response.clone(); caches.open(CACHE_NAME).then(cache => cache.put(event.request, clone)); } return response; }).catch(() => new Response( '', { status: 200, headers: { 'Content-Type': 'text/html' } } )); }) ); return; } // Player page and static assets: network-first, fall back to cache if (url.pathname.startsWith('/player') || url.pathname === '/socket.io/socket.io.js') { event.respondWith( fetch(event.request).then(response => { if (response.ok && response.type !== 'opaque') { const clone = response.clone(); caches.open(CACHE_NAME).then(cache => cache.put(event.request, clone)); } return response; }).catch(() => caches.match(event.request, { ignoreSearch: true }).then(cached => cached || new Response('Offline', { status: 503, statusText: 'Service Unavailable', headers: { 'Content-Type': 'text/plain' } }) ) ) ); return; } // Everything else (content files, API calls, etc.): don't intercept. // Returning without event.respondWith lets the browser handle it natively. });