PageSourceSearch

https://linknbio.com/service-worker.js

js linknbio.com collected 2026-09-24 11:27:35 UTC 9,467 bytes, 251 lines download raw bytes

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.