From d73abc809d5b3b45b7427590573ed727ff4d5b0c Mon Sep 17 00:00:00 2001 From: ScreenTinker Date: Mon, 13 Apr 2026 22:24:15 -0500 Subject: [PATCH] Simplify service worker: stop intercepting content requests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- server/player/index.html | 19 +++++++++++--- server/player/sw.js | 56 +++++++++------------------------------- 2 files changed, 27 insertions(+), 48 deletions(-) diff --git a/server/player/index.html b/server/player/index.html index db367b0..7d6b2b6 100644 --- a/server/player/index.html +++ b/server/player/index.html @@ -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 ==================== diff --git a/server/player/sw.js b/server/player/sw.js index 9db8fbc..4413a0a 100644 --- a/server/player/sw.js +++ b/server/player/sw.js @@ -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. });