1const CACHE_VERSION = 'v4'; 2const CACHE_NAME = `linknbio-content-${CACHE_VERSION}`; 3const API_URL = '/api/feed'; 4const NEWS_URL = '/api/news'; 5const TENANTS_API = '/api/tenants'; 6const ADMIN_CACHED_FLAG = 'admin-cached'; 7 8// Minimal install event for debugging 9self.addEventListener('install', (event) => { 10 console.log('Service Worker: install event'); 11 self.skipWaiting(); // Take over immediately without waiting for tabs to close 12 event.waitUntil( 13 // Open cache but do not pre-cache any URLs 14 caches.open(CACHE_NAME).then((cache) => { 15 console.log('Cache opened, but not pre-caching any resources'); 16 }) 17 ); 18}); 19 20// Activate event (clean up old caches) 21self.addEventListener('activate', (event) => { 22 const cacheWhitelist = [CACHE_NAME]; 23 event.waitUntil( 24 caches.keys().then((cacheNames) => { 25 return Promise.all( 26 cacheNames.map((cacheName) => { 27 if (!cacheWhitelist.includes(cacheName)) { 28 console.log(`Deleting old cache: ${cacheName}`); 29 return caches.delete(cacheName); 30 } 31 }) 32 ); 33 }).then(() => { 34 self.clients.claim(); // Take control of all open tabs immediately 35 console.log('Cache cleanup complete'); 36 }) 37 ); 38}); 39 40// Fetch events are not intercepted all requests are handled natively by the browser. 41// This service worker is used only for push notifications and cache management messages. 42 43 44// Cache Management 45self.addEventListener('message', (event) => { 46 const { action } = event.data; 47 if (action === 'CLEAR_CACHE') { 48 caches.keys().then((cacheNames) => { 49 return Promise.all( 50 cacheNames.map((cacheName) => caches.delete(cacheName)) 51 ); 52 }).then(() => { 53 self.clients.claim(); 54 }); 55 } else if (action === 'CACHE_RESOURCE') { 56 const url = event.data.url; 57 caches.open(CACHE_NAME).then((cache) => { 58 fetch(url, { cache: 'reload' }).then((response) => { 59 if (response && response.status === 200) { 60 cache.put(url, response); 61 console.log(`Cached new version of: ${url}`); 62 } 63 }).catch((error) => { 64 console.error(`Error fetching ${url}:`, error); 65 }); 66 }); 67 } else if (action === 'CACHE_RESOURCE_WITH_CONTENT') { 68 const cacheUrl = event.data.cacheUrl; 69 const fetchUrl = event.data.fetchUrl; 70 caches.open(CACHE_NAME).then((cache) => { 71 cache.delete(cacheUrl).then(() => { 72 fetch(fetchUrl, { cache: 'reload' }).then((response) => { 73 if (response && response.status === 200) { 74 cache.put(cacheUrl, response.clone()); 75 console.log(`Cached ${cacheUrl} with content from ${fetchUrl}`); 76 } 77 }).catch((error) => { 78 console.error(`Error fetching ${fetchUrl}:`, error); 79 }); 80 }); 81 }); 82 } else if (action === 'DELETE_CACHE_ENTRY') { 83 const pathToDelete = event.data.path; 84 caches.open(CACHE_NAME).then((cache) => { 85 cache.delete(pathToDelete).then(() => { 86 console.log(`${pathToDelete} cache deleted`); 87 }).catch((error) => { 88 console.error(`Error deleting ${pathToDelete} cache:`, error); 89 }); 90 }); 91 } else { 92 console.log('Unhandled message action:', action); 93 } 94}); 95 96// Push event - Fixed for iOS Stability 97self.addEventListener('push', function (event) { 98 event.waitUntil( 99 (async () => { 100 try { 101 // 1. Guard against empty/malformed payloads to protect the token 102 if (!event.data) { 103 throw new Error("Empty push event data received."); 104 } 105 106 const pushData = event.data.json(); 107 const title = pushData.title || "Notification"; 108 109 const options = { 110 body: pushData.body || "", 111 icon: pushData.icon || "/favicon.png", 112 badge: pushData.badge, 113 unreadCount: pushData.unreadCount, 114 actions: [ 115 { action: 'open', title: 'Open App' }, 116 { action: 'dismiss', title: 'Dismiss' } 117 ] 118 }; 119 120 // 2. Handle app badge cleanly in sequence 121 if (self.navigator && 'setAppBadge' in self.navigator) { 122 try { 123 await self.navigator.setAppBadge(1); 124 } catch (badgeError) { 125 console.error('Failed to set application badge:', badgeError); 126 } 127 } 128 129 // 3. Keep the worker alive until the visual banner displays 130 await self.registration.showNotification(title, options); 131 132 } catch (error) { 133 console.error('iOS Web Push recovery caught an error:', error); 134 135 // 4. Mandatory iOS Fallback: Never let a push execution finish blank 136 await self.registration.showNotification("Linknbio Update", { 137 body: "Tap to open your bio link manager." 138 }); 139 } 140 })() 141 ); 142}); 143 144// Periodic Data Refreshing 145setInterval(async () => { 146 try { 147 const [feedResponse, newsResponse] = await Promise.all([ 148 fetch(API_URL), 149 fetch(NEWS_URL) 150 ]); 151 152 if (feedResponse.ok) { 153 const data = await feedResponse.json(); 154 const cache = await caches.open(CACHE_NAME); 155 await cache.put(API_URL, new Response(JSON.stringify(data))); 156 const clients = await self.clients.matchAll();
157 clients.forEach(client => { 158 client.postMessage({ action: 'NEW_DATA_AVAILABLE' }); 159 }); 160 console.log('Updated cache with fresh API data'); 161 } 162 163 if (newsResponse.ok) { 164 const cache = await caches.open(CACHE_NAME); 165 await cache.put(NEWS_URL, newsResponse.clone()); 166 console.log('Updated cache with fresh News view'); 167 } 168 } catch (error) { 169 console.error('Error during periodic refresh:', error); 170 } 171}, 15 * 60 * 1000); 172 173// Helper function to cache admin pages for a specific tenant 174async function cacheAdminPagesOnce(tenantPath) { 175 const cache = await caches.open(CACHE_NAME); 176 const tenantCacheFlag = `${ADMIN_CACHED_FLAG}-${tenantPath}`; 177 const isTenantCached = await cache.match(tenantCacheFlag); 178 179 if (isTenantCached) { 180 console.log(`Admin pages for tenant '${tenantPath}' are already cached. Skipping...`); 181 return; 182 } 183 184 console.log(`Caching admin pages for tenant '${tenantPath}' for the first time...`); 185 186 const adminPages = [ 187 `${tenantPath}/admin/links`, 188 `/api/feed`, 189 `${tenantPath}/admin/feed`, 190 `${tenantPath}/admin/explore`, 191 `${tenantPath}/admin/sociallinks`, 192 `${tenantPath}/admin/layouts`, 193 `${tenantPath}/admin/insights`, 194 `${tenantPath}/admin/leads`, 195 `${tenantPath}/admin/security`, 196 `${tenantPath}/admin/profile`, 197 `${tenantPath}/admin/create`, 198 `${tenantPath}` 199 ]; 200 201 for (const page of adminPages) { 202 try { 203 const response = await fetch(page, { cache: 'reload' }); 204 if (response && response.status === 200) { 205 await cache.put(page, response.clone()); 206 console.log(`Cached admin page: ${page}`); 207 } 208 } catch (error) { 209 console.error(`Error caching admin page '${page}':`, error); 210 } 211 } 212 213 await cache.put( 214 tenantCacheFlag, 215 new Response('true', { status: 200, statusText: 'OK' }) 216 ); 217} 218 219// Helper function to cache admin pages using API 220async function cacheTenantsAdminPages(originalRequest) { 221 try { 222 console.log('Fetching tenants from:', TENANTS_API); 223 const tenantResponse = await fetch(TENANTS_API, { 224 method: 'GET', 225 credentials: 'include', // Send cookies for authentication 226 headers: originalRequest.headers // Reuse headers from /all 227 }); 228 229 if (!tenantResponse.ok) { 230 console.error('Failed to fetch tenants API:', tenantResponse.status); 231 return; 232 } 233 234 const tenantData = await tenantResponse.json(); 235 console.log('Raw tenant data:', tenantData); 236 237 const tenantList = tenantData.map(tenant => tenant.setting.userName); 238 239 if (!tenantList || tenantList.length === 0) { 240 console.log('No tenants found in API response'); 241 return; 242 } 243 244 console.log(`Found ${tenantList.length} tenants to cache from API:`, tenantList); 245 for (const tenant of tenantList) { 246 await cacheAdminPagesOnce(`/${tenant}`); 247 } 248 } catch (error) { 249 console.error('Error caching tenant admin pages from API:', error); 250 } 251}
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.