1// NoteTube Service Worker 2const CACHE_NAME = 'notetube-v3'; 3const OFFLINE_URL = '/offline.html'; 4 5// Assets to cache on install. 6// Do NOT precache '/' â its HTML references content-hashed JS/CSS chunks that 7// rotate every deploy, so a precached copy goes stale immediately. Navigation 8// requests are served network-first with /offline.html as the only fallback. 9const STATIC_ASSETS = [ 10 '/manifest.json', 11 '/offline.html', 12]; 13 14// Install event - cache static assets 15self.addEventListener('install', (event) => { 16 event.waitUntil( 17 caches.open(CACHE_NAME).then((cache) => { 18 // Cache static assets, but don't fail if some aren't available 19 return cache.addAll(STATIC_ASSETS).catch((error) => { 20 console.warn('Failed to cache some static assets:', error); 21 }); 22 }) 23 ); 24 // Activate immediately 25 self.skipWaiting(); 26}); 27 28// Activate event - clean up old caches 29self.addEventListener('activate', (event) => { 30 event.waitUntil( 31 caches.keys().then((cacheNames) => { 32 return Promise.all( 33 cacheNames 34 .filter((name) => name !== CACHE_NAME) 35 .map((name) => caches.delete(name)) 36 ); 37 }) 38 ); 39 // Take control of all pages immediately 40 self.clients.claim(); 41}); 42 43// Fetch event - network first, fallback to cache 44self.addEventListener('fetch', (event) => { 45 const { request } = event; 46 const url = new URL(request.url); 47 48 // Skip non-GET requests 49 if (request.method !== 'GET') { 50 return; 51 } 52 53 // Skip cross-origin requests 54 if (url.origin !== self.location.origin) { 55 return; 56 } 57 58 // Skip API routes - always go to network 59 if (url.pathname.startsWith('/api/')) { 60 return; 61 } 62 63 // Skip auth-related routes 64 if (url.pathname.startsWith('/auth/')) { 65 return; 66 } 67 68 // For navigation requests (HTML pages): always network, never cache HTML. 69 // HTML references content-hashed JS/CSS chunks; caching it long-term causes 70 // drift across deploys (stale HTML points at dead hashes â MIME errors â 71 // hydration never runs). Only fall back to offline page when network fails. 72 if (request.mode === 'navigate') { 73 event.respondWith( 74 fetch(request).catch(async () => { 75 // Network failed. Fall back to the cached offline page. If install's 76 // best-effort addAll never cached it, caches.match resolves to 77 // undefined â respondWith(undefined) throws "Failed to convert value 78 // to 'Response'", so always hand back a real Response. 79 const offline = await caches.match(OFFLINE_URL); 80 return ( 81 offline || 82 new Response( 83 '<!doctype html><title>Offline</title><h1>You are offline</h1>', 84 { status: 503, headers: { 'Content-Type': 'text/html; charset=utf-8' } } 85 ) 86 ); 87 }) 88 ); 89 return; 90 } 91 92 // For other requests (assets, images, etc.) 93 event.respondWith( 94 fetch(request) 95 .then((response) => { 96 // Cache successful responses 97 if (response.ok) { 98 const responseClone = response.clone(); 99 caches.open(CACHE_NAME).then((cache) => { 100 cache.put(request, responseClone); 101 }); 102 } 103 return response; 104 }) 105 .catch(async () => { 106 // Network failed. Serve the cached copy if we have one; otherwise 107 // return a network-error Response (NOT undefined, which would throw 108 // "Failed to convert value to 'Response'"). Response.error() reads to 109 // the page as a genuine failed fetch â the correct outcome for an 110 // uncached asset whose network request died. 111 const cached = await caches.match(request); 112 return cached || Response.error(); 113 }) 114 ); 115}); 116 117// Handle push notifications (for future use) 118self.addEventListener('push', (event) => { 119 if (event.data) { 120 const data = event.data.json(); 121 const options = { 122 body: data.body || 'New notification from NoteTube', 123 icon: '/icon-192x192.png', 124 badge: '/icon-72x72.png', 125 vibrate: [100, 50, 100], 126 data: { 127 url: data.url || '/', 128 }, 129 }; 130 event.waitUntil( 131 self.registration.showNotification(data.title || 'NoteTube', options) 132 ); 133 } 134}); 135 136// Handle notification click 137self.addEventListener('notificationclick', (event) => { 138 event.notification.close(); 139 const url = event.notification.data?.url || '/'; 140 event.waitUntil( 141 clients.matchAll({ type: 'window' }).then((clientList) => { 142 // Focus existing window if available 143 for (const client of clientList) {
144 if (client.url === url && 'focus' in client) { 145 return client.focus(); 146 } 147 } 148 // Open new window 149 if (clients.openWindow) { 150 return clients.openWindow(url); 151 } 152 }) 153 ); 154});
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.