PageSourceSearch

https://secplusmastery.com/sw.js

js secplusmastery.com collected 2026-10-02 01:43:50 UTC 6,819 bytes, 157 lines download raw bytes

1/*
2 * SecPlus Mastery offline service worker.
3 *
4 * Hand-written and dependency-free. Strategy by request type:
5 *   - cross-origin (Supabase, Stripe, Plausible, Sentry, fonts) ......... untouched
6 *   - same-origin API / functions (/api/*, /.netlify/*) ................. untouched
7 *   - non-GET (POST/PUT/...) ........................................... untouched
8 *   - HTML navigations ................... network-first, then offline fallback
9 *   - A+ practice exam scripts (/aplus-practice-exam/*) ..... network-first, then cache
10 *   - immutable hashed build assets (/assets/*, /aplus/core-N/assets/*) .. cache-first
11 *   - other same-origin GET (icons, manifest, images, pdfs) .... stale-while-revalidate
12 *
13 * Online users therefore always get fresh HTML, fresh exam code, and live data;
14 * the cache only serves when the network is unavailable. Authenticated API and
15 * payment traffic is never cached.
16 *
17 * Update policy: NO skipWaiting / NO clientsClaim. A new worker waits and takes
18 * over only once existing tabs are gone, so a deploy can never swap assets out
19 * from under an in-progress action such as Stripe checkout. Bump VERSION to roll
20 * a fresh cache generation; older generations are deleted on activate.
21 */
22// Bumped to v3 with the A+ exam routing fix below: installs that are already out
23// there hold a STATIC_CACHE full of the previous build's exam scripts, and the
24// new rule alone would never evict them. A new generation drops that cache on
25// activate. Existing tabs keep the old worker until they are closed (no
26// skipWaiting), which is fine: the worst they do is what they already do.
27const VERSION = 'v3';
28const STATIC_CACHE = `secplus-static-${VERSION}`;
29const PAGE_CACHE = `secplus-pages-${VERSION}`;
30const OFFLINE_URL = '/offline.html';
31
32// App-shell essentials worth having before the first offline navigation.
33const PRECACHE_URLS = [
34  OFFLINE_URL,
35  '/favicon.svg?v=2',
36  '/apple-touch-icon.png?v=2',
37  '/icon-192.png?v=2',
38  '/icon-512.png?v=2',
39  '/site.webmanifest',
40];
41
42self.addEventListener('install', (event) => {
43  event.waitUntil(
44    caches.open(STATIC_CACHE).then((cache) => cache.addAll(PRECACHE_URLS)),
45  );
46});
47
48self.addEventListener('activate', (event) => {
49  // Drop caches from previous versions. Without clientsClaim, pages already open
50  // keep their current controller; new loads pick up this worker.
51  event.waitUntil(
52    caches.keys().then((keys) =>
53      Promise.all(
54        keys
55          .filter((key) => key !== STATIC_CACHE && key !== PAGE_CACHE)
56          .map((key) => caches.delete(key)),
57      ),
58    ),
59  );
60});
61
62// True for anything the worker must not touch: it falls through to the network
63// exactly as if no worker were installed.
64function isBypassed(url, request) {
65  if (request.method !== 'GET') return true;
66  if (url.origin !== self.location.origin) return true; // Supabase/Stripe/Plausible/fonts
67  if (url.pathname.startsWith('/.netlify/')) return true; // serverless functions
68  if (url.pathname.startsWith('/api/')) return true; // function proxy (netlify.toml)
69  return false;
70}
71
72// Only ever cache a same-origin 200. Skips opaque/cross-origin and partial (206)
73// range responses, which throw on Cache.put or would poison the cache.
74async function cachePut(cacheName, request, response) {
75  if (response && response.status === 200 && response.type === 'basic') {
76    const cache = await caches.open(cacheName);
77    await cache.put(request, response);
78  }
79}
80
81async function networkFirst(event, request) {
82  try {
83    const response = await fetch(request);
84    event.waitUntil(cachePut(PAGE_CACHE, request, response.clone()));
85    return response;
86  } catch {
87    const cached = await caches.match(request);
88    if (cached) return cached;
89    // The offline page answers a NAVIGATION. This path also serves the A+ exam's
90    // scripts, and handing an HTML document back for a .js request does not
91    // degrade gracefully, it fails confusingly: under nosniff the browser
92    // refuses to execute a text/html script, and without it the page is parsed
93    // as JavaScript. A network error is the honest answer for a subresource.
94    if (request.mode === 'navigate') {
95      return (await caches.match(OFFLINE_URL)) || Response.error();
96    }
97    return Response.error();
98  }
99}
100
101async function cacheFirst(event, request) {
102  const cached = await caches.match(request);
103  if (cached) return cached;
104  const response = await fetch(request);
105  event.waitUntil(cachePut(STATIC_CACHE, request, response.clone()));
106  return response;
107}
108
109async function staleWhileRevalidate(event, request) {
110  const cached = await caches.match(request);
111  const network = fetch(request)
112    .then(async (response) => {
113      await cachePut(STATIC_CACHE, request, response.clone());
114      return response;
115    })
116    .catch(() => cached || Response.error());
117  // waitUntil on the whole revalidation, and from here rather than from inside
118  // the .then above. A fetch event stays active only until the promise given to
119  // respondWith settles, and on a cache hit that is immediate, so the old call
120  // ran after the event was over and threw InvalidStateError straight into the
121  // catch below, where it was swallowed. The write had already started, but
122  // nothing was holding the worker alive for it, so a worker terminated
123  // mid-write left the stale copy in place and a redeployed icon or manifest
124  // kept serving the old one.
125  event.waitUntil(network);
126  return cached || network;
127}
128
129self.addEventListener('fetch', (event) => {
130  const { request } = event;
131  const url = new URL(request.url);
132  if (isBypassed(url, request)) return;
133
134  if (request.mode === 'navigate') {
135    event.respondWith(networkFirst(event, request));
136    return;
137  }
138  // The A+ practice exam is a standalone page under public/, so its three
139  // scripts (the two question banks and the grader) are copied through with
140  // fixed names and no content hash. The default below would answer them from
141  // the cache and pair a freshly fetched page with the previous build's answer
142  // key, marking someone wrong on the exact question a deploy just corrected.
143  // Network-first keeps the page and its code from the same build, and still
144  // falls back to the cached copy when the network is gone.
145  if (url.pathname.startsWith('/aplus-practice-exam/')) {
146    event.respondWith(networkFirst(event, request));
147    return;
148  }
149  // The A+ course builds keep the same kind of hashed chunks under their own base
150  // paths; they used to fall through to stale-while-revalidate and be fetched
151  // again in the background on every page load.
152  if (url.pathname.startsWith('/assets/') || /^\/aplus\/core-\d\/assets\//.test(url.pathname)) {
153    event.respondWith(cacheFirst(event, request));
154    return;
155  }
156  event.respondWith(staleWhileRevalidate(event, request));
157});

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.