PageSourceSearch

https://www.hollyhomer.com/sw.js

js hollyhomer.com collected 2026-10-04 03:32:28 UTC 13,516 bytes, 360 lines download raw bytes

1// indexHash: 2484548a7aa5a0126585a2668b208ef1fdf41869
2// use our own cache for /api/config and /api/config/theme
3// since ngsw doesn't allow dynamic cache updates
4const CACHE_NAME = 'ihub-cache';
5const CACHE_ORIGINS = ['https://fonts.googleapis.com', 'https://fonts.gstatic.com', 'https://ih-cdn.ihub.app'];
6const CACHE_ICONS_REGEX =
7  /https:\/\/(az720171\.vo\.msecnd\.net|az601583\.vo\.msecnd\.net|cainspirehub\.azureedge\.net)\/app-content\/icon\/.*/i;
8// add the fetch handler before ngsw so our code can intercept the fetch event before ngsw can
9self.addEventListener('fetch', (event) => {
10  const { request } = event;
11
12  // check if this is a get request to /api/config or /api/config/theme
13  // The (\?.*)? suffix allows versioned URLs like /api/config/theme?v=hash
14  // The version param acts as the cache key discriminator —
15  // a new hash means a cache miss, forcing a fresh network fetch.
16  if (
17    request.method === 'GET' &&
18    request.url.startsWith(self.location.origin) &&
19    request.url.match(/\/api\/(config|config\/theme)(\?.*)?$/i)
20  ) {
21    // handle the request with our own cache
22    event.respondWith(
23      (async function () {
24        // if the request cache directive is set to reload then skip cache and go to network
25        // store the response in cache if it's ok
26        switch (request.cache) {
27          case 'reload':
28            // get the response from the network
29            // add cache busting to the request so we can bypass the browser and cdn cache
30            const newUrl = new URL(request.url);
31            newUrl.searchParams.set('d', Date.now());
32
33            var networkRequest = new Request(newUrl, request);
34            // get the network response
35            const networkResponse = await fetch(networkRequest);
36
37            // open the cache
38            const cache = await caches.open(CACHE_NAME).catch((err) => {
39              console.error('Failed to open cache', err);
40              return null;
41            });
42            // if the cache failed to open then just return the network response
43            if (!cache) {
44              return networkResponse;
45            }
46
47            // modify the request to remove the cache busting (d param) from request.url
48            const cacheRequest = new Request(request.url, request);
49
50            if (networkResponse.ok) {
51              // store the response in cache and use a clone in case cache is empty and the network response was used
52              await cache
53                .put(cacheRequest, networkResponse.clone())
54                .catch((err) => console.error('Failed to put cache', err));
55
56              return networkResponse;
57            }
58            // fallback to cache if network fails
59            const cachedResponse = await cache.match(cacheRequest).catch((err) => {
60              console.error('Failed to match cache', err);
61              return null;
62            });
63            // return the cached value if available otherwise wait for the network response;
64            return cachedResponse || networkResponse;
65
66          case 'no-store':
67            return fetch(request);
68        }
69
70        // Default: stale-while-revalidate. Returns cached response instantly
71        // (zero latency) while revalidating in the background. This is the
72        // hot path for page-load theme fetches — the versioned URL (?v=hash)
73        // ensures a cache miss when the theme actually changes.
74        // open the cache for writing
75        const cache = await caches.open(CACHE_NAME).catch((err) => {
76          console.error('Failed to open cache', err);
77          return null;
78        });
79        // if the cache failed to open then just return the network response
80        if (!cache) {
81          return fetch(request);
82        }
83
84        // try to load the request from cache
85        const cachedResponse = await cache.match(request).catch((err) => {
86          console.error('Failed to match cache', err);
87          return null;
88        });
89        // prep the network response
90        const networkResponsePromise = fetch(request);
91
92        // tell the fetch event to stay open
93        // until the network request is also done since we can respond immediately with the cached value
94        event.waitUntil(
95          (async function () {
96            // get the response from the network
97            const networkResponse = await networkResponsePromise;
98
99            if (networkResponse.ok) {
100              // store the response in cache and use a clone in case cache is empty and the network response was used
101              await cache
102                .put(request, networkResponse.clone())
103                .catch((err) => console.error('Failed to put cache', err));
104            }
105          })()
106        );
107
108        // return the cached value if available otherwise wait for the network response;
109        return cachedResponse || networkResponsePromise;
110      })()
111    );
112
113    return;
114  }
115
116  // check if ngsw should intercept the request
117  if (
118    // if this is a navigation request
119    request.mode === 'navigate' ||
120    // or this is a get request
121    (request.method === 'GET' &&
122      // and the request is not local
123      ((request.url.startsWith(self.location.origin) &&
124        // and the request is for something in the cache
125        request.url.match(
126          /\/(manifest.webmanifest|favicon.ico|home|index.html|[a-z0-9]+\.(css|js|woff|eot|ttf|svg|woff2)|)$/i
127        )) ||
128        // and the origin is enabled for caching
129        CACHE_ORIGINS.indexOf(request.url) === 0)) ||
130    CACHE_ICONS_REGEX.test(request.url)
131  ) {
132    // fetch event will be processed by ngsw as normal
133    return;
134  }
135
136  // if the url is for:
137  // "https://ih-cdn.ihub.app/**/*.js",
138  // "https://ih-cdn.ihub.app/**/*.css",
139  // "https://ih-cdn.ihub.app/**/*.woff",
140  // "https://ih-cdn.ihub.app/**/*.eot",
141  // "https://ih-cdn.ihub.app/**/*.ttf",
142  // "https://ih-cdn.ihub.app/**/*.svg",
143  // "https://ih-cdn.ihub.app/**/*.woff2"
144  // then allow it to be processed by ngsw so it can be cached
145  if (
146    request.url.match(/https:\/\/ih-cdn\.ihub\.app\/.*\.(js|css|woff|eot|ttf|svg|woff2)$/i) ||
147    // or the request is the local origin
148    request.url.startsWith(self.location.origin)
149  ) {
150    return;
151  }
152
153  // don't allow the ngsw to intercept this fetch event
154  event.stopImmediatePropagation();
155});
156
157// set global to serviceworker so emoji-js won't choke on init
158global = self;
159
160// Self-hosted (same-origin) rather than cdnjs.cloudflare.com (IHUB-17205).
161// importScripts() is synchronous and re-runs on every worker (re)start — a
162// routine event, since an idle worker is terminated by the browser after
163// ~30s and respawns on the next fetch. Per spec, if any one script here
164// fails to load, the ENTIRE worker fails to start, not just the
165// firebase/he/emoji-js-dependent push-notification code below — ngsw and
166// all its offline/caching support goes down with it. The four
167// cdnjs.cloudflare.com URLs this used to import were never same-origin and
168// never listed in ngsw-config.json's asset groups, so they depen
168ded
169// entirely on ordinary HTTP cache surviving disk-pressure eviction,
170// private/incognito sessions, and Chrome's per-site cache partitioning —
171// none of which self-hosting has to worry about. Vendored copies live in
172// App/assets/vendor/, copied from node_modules at build time (see
173// App.csproj's CopyServiceWorkerVendorAssets* targets) so bumping a
174// package.json version keeps them current instead of drifting from a
175// hand-pinned CDN version — and are additionally listed in
176// ngsw-config.json's service-worker-vendor lazy asset group so they
177// persist in Cache Storage, not just ordinary HTTP cache, once fetched.
178importScripts(
179  'ngsw-worker.js',
180  '/assets/vendor/firebase-app-compat.js',
181  '/assets/vendor/firebase-messaging-compat.js',
182  '/assets/vendor/he.js',
183  '/assets/vendor/emoji.min.js'
184);
185
186let emoji;
187self.addEventListener('message', function (event) {
188  switch (event.data.action) {
189    case 'reset_cache':
190      return caches
191        .keys()
192        .then(function (keyList) {
193          const precacheKey = 'ihubapp-precache-' + self.registration.scope;
194          return Promise.all(
195            keyList.map(function (key) {
196              if (key === precacheKey || key === 'ihubapp-static-resources') {
197                return caches.delete(key);
198              }
199
200              return Promise.resolve();
201            })
202          );
203        })
204        .then(function () {
205          console.log('Serviceworker cache wiped');
206          event.ports[0].postMessage('complete');
207
208          return self.registration
209            .unregister()
210            .then(() => self.clients.matchAll())
211            .then((clients) => {
212              clients.forEach((client) => client.navigate(client.url));
213            });
214        })
215        .catch(function (err) {
216          console.error('Reset cache failed', err);
217          event.ports[0].postMessage('error', err);
218        });
219  }
220});
221
222if ('PushManager' in this) {
223  // initialize firebase
224  firebase.initializeApp({
225    apiKey: 'AIzaSyCHhrP8cbemEhpGMKE2L8Uigl6PDVde4oE',
226    authDomain: 'quantum-gearbox-557.firebaseapp.com',
227    databaseURL: 'https://quantum-gearbox-557.firebaseio.com',
228    projectId: 'quantum-gearbox-557',
229    storageBucket: 'quantum-gearbox-557.appspot.com',
230    messagingSenderId: '261205718171',
231    appId: '1:261205718171:web:3a57afd49efd33e2e8423e',
232    vapid: 'BI5DtqI3h8Db7NQGHawXFQ1TEpKN8GPizj_QN0U2b_WzhNY39ctC4L_Q4z1u6k6sC709W2hYcDZpt5j1UnB37-g'
233  });
234
235  const messaging = firebase.messaging();
236
237  messaging.onBackgroundMessage(function (payload) {
238    if (!emoji) {
239      emoji = new EmojiConvertor();
240      emoji.replace_mode = 'unified';
241    }
242
243    console.log('Received push notification', payload);
244
245    // replace emoticons but skip URLs by splitting the text into segments and only replacing emoticons in non-URL segments
246    const replaceEmoticonsSkipURLs = (text) => {
247      if (!text) return text;
248
249      // Regex pattern to match URLs
250      const urlPattern = /https?:\/\/[^\s]+/g;
251      // array to store segments
252      const segments = [];
253
254      let lastIndex = 0;
255
256      // Iterate through the text and identify URLs
257      text.replace(urlPattern, (match, index) => {
258        // Add non-URL text from lastIndex to the start of the URL
259        segments.push(text.substring(lastIndex, index));
260
261        // Add URL
262        segments.push(match);
263
264        // Update lastIndex
265        lastIndex = index + match.length;
266      });
267
268      // Add any remaining non-URL text to segments
269      if (lastIndex < text.length) {
270        segments.push(text.substring(lastIndex));
271      }
272
273      // Replace emoticons only in non-URL segments
274      for (let i = 0; i < segments.length; i++) {
275        if (!segments[i].match(urlPattern)) {
276          segments[i] = emoji.replace_emoticons(segments[i]);
277        }
278      }
279
280      // Rejoin segments to get final text
281      return segments.join('');
282    };
283
284    // Replace emoji in body and title but skip URLs
285    payload.data.title = he.decode(replaceEmoticonsSkipURLs(payload.data?.title));
286    payload.data.body = payload.data?.body ? he.decode(replaceEmoticonsSkipURLs(payload.data?.body)) : undefined;
287    if (payload.notification) {
288      payload.notification.title = he.decode(replaceEmoticonsSkipURLs(payload.notification?.title));
289      payload.notification.body = he.decode(replaceEmoticonsSkipURLs(payload.notification?.body));
290
291      payload.data = {
292        ...payload.data,
293        ...payload.notification
294      };
295    }
296
297    const options = {
298      body: payload.data.body,
299      icon: payload.data.icon,
300      image: payload.data.image,
301      tag: payload.data.tag,
302      requireInteraction: true,
303      data: payload.data
304    };
305
306    return self.registration.showNotification(payload.data.title, options);
307  });
308
309  self.addEventListener(
310    'notificationclick',
311    function (event) {
312      const data = event.notification.data;
313
314      event.notification.close();
315
316      if (event.action) {
317        console.error('User attempted an unsupported action', event.action);
318      } else {
319        event.waitUntil(
320          clients
321            .matchAll({
322              includeUncontrolled: true,
323              type: 'window'
324            })
325            .then(function (activeClients) {
326              // Validate click_action to prevent open redirect attacks
327              let appUrl = '/home';
328              const clickAction = data?.click_action?.trim();
329              if (clickAction) {
330                // Allow relative URLs or same-origin absolute URLs only
331                // Explicitly reject protocol-relative URLs (//evil.com/path)
332                if (clickAction.startsWith('/') && !clickAction.startsWith('//')) {
333                  appUrl = clickAction;
334                } else {
335                  try {
336                    const url = new URL(clickAction);
337                    if (url.origin === self.location.origin) {
338                      appUrl = clickAction;
339                    }
340                  } catch (e) {
341                    // Invalid URL, use default
342                  }
343                }
344              }
345
346              if (activeClients.length > 0) {
347                activeClients[0].navigate(appUrl);
348                activeClients[0].focus();
349              } else {
350                clients.openWindow(appUrl);
351              }
352            })
353        );
354      }
355    },
356    false
357  );
358
359  self.addEventListener('notificationclose', (e) => console.log('notification closed', e));
360}

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.