mirror of
https://github.com/screentinker/screentinker.git
synced 2026-05-15 07:32:23 -06:00
Bug 1 (SW): Rewrote service worker fetch handler: - Skip range requests (video seeking) to avoid caching partial responses - Skip non-GET requests entirely - Use ignoreSearch on cache match to avoid query-param misses - Don't cache opaque cross-origin responses - Outer catch on Cache API failures - Don't intercept catch-all requests (let browser handle natively) - Bump cache version to v4 to purge broken cached responses Bug 2 (auth): Playlist refresh register was missing device_token, causing auth rejection every 5 minutes. Fixed by including token in the refresh-register emit. Added diagnostic logging on both client and server for token validation failures. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
85 lines
2.9 KiB
JavaScript
85 lines
2.9 KiB
JavaScript
const CACHE_NAME = 'rd-player-v4';
|
|
const CONTENT_CACHE = 'rd-content-v1';
|
|
|
|
// Install: skip waiting to activate immediately
|
|
self.addEventListener('install', (event) => {
|
|
self.skipWaiting();
|
|
});
|
|
|
|
// Activate: clean old caches, 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))
|
|
)).then(() => self.clients.claim())
|
|
);
|
|
});
|
|
|
|
// Fetch handler
|
|
self.addEventListener('fetch', (event) => {
|
|
const url = new URL(event.request.url);
|
|
|
|
// Only handle GET requests — let POST/PUT/DELETE pass through
|
|
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;
|
|
}
|
|
|
|
// 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: network only, don't intercept failures
|
|
// (Returning without calling event.respondWith lets the browser handle it natively)
|
|
});
|