1// Borderless Service Worker 2const CACHE_NAME = 'borderless-v2'; 3const OFFLINE_URL = '/offline.html'; 4 5// Assets to cache on install 6const PRECACHE_ASSETS = ['/', '/offline.html']; 7 8// Install event - cache essential assets 9self.addEventListener('install', (event) => { 10 event.waitUntil( 11 caches 12 .open(CACHE_NAME) 13 .then((cache) => cache.addAll(PRECACHE_ASSETS)) 14 .then(() => self.skipWaiting()), 15 ); 16}); 17 18// Activate event - clean up old caches 19self.addEventListener('activate', (event) => { 20 event.waitUntil( 21 caches 22 .keys() 23 .then((cacheNames) => { 24 return Promise.all( 25 cacheNames 26 .filter((name) => name !== CACHE_NAME) 27 .map((name) => caches.delete(name)), 28 ); 29 }) 30 .then(() => self.clients.claim()), 31 ); 32}); 33 34// Fetch event - network first, then cache 35self.addEventListener('fetch', (event) => { 36 // Skip non-GET requests 37 if (event.request.method !== 'GET') return; 38 39 // Skip API requests 40 if (event.request.url.includes('/api/')) return; 41 42 const requestUrl = new URL(event.request.url); 43 44 // Hashed Vite assets are immutable and should always come from the network. 45 // Caching them here can combine JavaScript chunks from different builds. 46 if (requestUrl.pathname.startsWith('/build/assets/')) return; 47 48 // Let the browser handle online navigations. Only provide the offline page 49 // if the navigation itself fails; never cache authenticated HTML responses. 50 if (event.request.mode === 'navigate') { 51 event.respondWith( 52 fetch(event.request).catch(() => caches.match(OFFLINE_URL)), 53 ); 54 return; 55 } 56 57 event.respondWith( 58 fetch(event.request) 59 .then((response) => { 60 // Clone the response 61 const responseClone = response.clone(); 62 63 // Cache successful responses 64 if (response.status === 200) { 65 caches 66 .open(CACHE_NAME) 67 .then((cache) => 68 cache.put(event.request, responseClone), 69 ); 70 } 71 72 return response; 73 }) 74 .catch(() => { 75 // Return cached version or offline page 76 return caches.match(event.request).then((cachedResponse) => { 77 if (cachedResponse) { 78 return cachedResponse; 79 } 80 81 return new Response('Network error', { 82 status: 408, 83 headers: { 'Content-Type': 'text/plain' }, 84 }); 85 }); 86 }), 87 ); 88}); 89 90// Push event - handle push notifications 91self.addEventListener('push', (event) => { 92 if (!event.data) return; 93 94 let data; 95 try { 96 data = event.data.json(); 97 } catch (e) { 98 data = { 99 title: 'Borderless', 100 body: event.data.text(), 101 }; 102 } 103 104 const options = { 105 body: data.body || '', 106 icon: data.icon || '/images/icon-192x192.png', 107 badge: data.badge || '/images/badge-72x72.png', 108 vibrate: [100, 50, 100], 109 data: { 110 url: data.data?.url || '/', 111 dateOfArrival: Date.now(), 112 }, 113 actions: data.actions || [], 114 requireInteraction: data.requireInteraction || false, 115 tag: data.tag || 'borderless-notification', 116 renotify: true, 117 }; 118 119 event.waitUntil( 120 self.registration.showNotification(data.title || 'Borderless', options), 121 ); 122}); 123 124// Notification click event - open the app 125self.addEventListener('notificationclick', (event) => { 126 event.notification.close(); 127 128 const url = event.notification.data?.url || '/'; 129 130 event.waitUntil( 131 clients 132 .matchAll({ type: 'window', includeUncontrolled: true }) 133 .then((windowClients) => { 134 // Check if there's already a window/tab open 135 for (const client of windowClients) { 136 if ( 137 client.url.includes(self.location.origin) &&
138 'focus' in client 139 ) { 140 client.navigate(url); 141 return client.focus(); 142 } 143 } 144 145 // If no window is open, open a new one 146 if (clients.openWindow) { 147 return clients.openWindow(url); 148 } 149 }), 150 ); 151}); 152 153// Handle notification close 154self.addEventListener('notificationclose', (event) => { 155 // Track notification dismissal if needed 156 console.log('Notification closed:', event.notification.tag); 157}); 158 159// Background sync for offline actions (optional) 160self.addEventListener('sync', (event) => { 161 if (event.tag === 'sync-notifications') { 162 event.waitUntil(syncNotifications()); 163 } 164}); 165 166async function syncNotifications() { 167 // Sync any pending notification actions when back online 168 // try { 169 // const response = await fetch('/api/notifications/unread-count'); 170 // if (response.ok) { 171 // const data = await response.json(); 172 // // Update badge if supported 173 // if ('setAppBadge' in navigator && data.unread_count > 0) { 174 // navigator.setAppBadge(data.unread_count); 175 // } 176 // } 177 // } catch (error) { 178 // console.error('Failed to sync notifications:', error); 179 // } 180}
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.