Simplify service worker: stop intercepting content requests

The SW was causing "unexpected error" on video/image fetches due to
range request handling, opaque response caching, and stale SW races.

Fix: SW now ONLY caches player page + socket.io JS for offline boot.
Content files are left to browser native HTTP cache (server already
sets Cache-Control: public, max-age=2592000, immutable).

Also: auto-reload player when new SW activates so deploys take effect
immediately without manual hard refresh.

Bumped cache to v5 — activate purges all old caches (including the
broken rd-content-v1 content cache).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
ScreenTinker 2026-04-13 22:24:15 -05:00
parent b4ac2fb821
commit d73abc809d
2 changed files with 27 additions and 48 deletions

View file

@ -952,10 +952,21 @@
// Register service worker for offline content caching
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/player/sw.js').then(
() => console.log('Service Worker registered'),
(err) => console.warn('SW registration failed:', err)
);
navigator.serviceWorker.register('/player/sw.js').then(reg => {
console.log('Service Worker registered');
// When a new SW activates, reload so the fresh code takes effect immediately
reg.addEventListener('updatefound', () => {
const newWorker = reg.installing;
if (newWorker) {
newWorker.addEventListener('statechange', () => {
if (newWorker.state === 'activated' && navigator.serviceWorker.controller) {
console.log('New Service Worker activated — reloading for fresh code');
location.reload();
}
});
}
});
}, (err) => console.warn('SW registration failed:', err));
}
// ==================== Keyboard shortcuts ====================

View file

@ -1,61 +1,29 @@
const CACHE_NAME = 'rd-player-v4';
const CONTENT_CACHE = 'rd-content-v1';
const CACHE_NAME = 'rd-player-v5';
// Install: skip waiting to activate immediately
self.addEventListener('install', (event) => {
self.skipWaiting();
});
// Activate: clean old caches, claim clients
// 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 && k !== CONTENT_CACHE).map(k => caches.delete(k))
keys.filter(k => k !== CACHE_NAME).map(k => caches.delete(k))
)).then(() => self.clients.claim())
);
});
// Fetch handler
// 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) => {
const url = new URL(event.request.url);
// Only handle GET requests — let POST/PUT/DELETE pass through
// Only handle GET requests
if (event.request.method !== 'GET') return;
// Content files (videos, images): cache-first for offline playback
if (url.pathname.startsWith('/uploads/content/')) {
// Skip range requests (video seeking) — serve from network, don't cache partial responses
if (event.request.headers.get('range')) {
return; // Let the browser handle range requests directly
}
event.respondWith(
caches.open(CONTENT_CACHE).then(cache =>
cache.match(event.request, { ignoreSearch: true }).then(cached => {
if (cached) return cached;
return fetch(event.request).then(response => {
// Only cache successful, complete (non-opaque) responses
if (response.ok && response.status === 200 && response.type !== 'opaque') {
cache.put(event.request, response.clone());
}
return response;
}).catch(() => {
return new Response('Content unavailable offline', {
status: 503,
statusText: 'Service Unavailable',
headers: { 'Content-Type': 'text/plain' }
});
});
})
).catch(() => {
// Cache API itself failed — fall through to network
return fetch(event.request).catch(() =>
new Response('Offline', { status: 503, headers: { 'Content-Type': 'text/plain' } })
);
})
);
return;
}
const url = new URL(event.request.url);
// Player page and static assets: network-first, fall back to cache
if (url.pathname.startsWith('/player') || url.pathname === '/socket.io/socket.io.js') {
@ -79,6 +47,6 @@ self.addEventListener('fetch', (event) => {
return;
}
// Everything else: network only, don't intercept failures
// (Returning without calling event.respondWith lets the browser handle it natively)
// Everything else (content files, API calls, etc.): don't intercept.
// Returning without event.respondWith lets the browser handle it natively.
});