PageSourceSearch

https://greatcontroversy.vercel.app/service-worker.js

js greatcontroversy.vercel.app collected 2026-10-03 07:01:58 UTC 7,120 bytes, 268 lines download raw bytes

1// Offline support for app shell + runtime-cached book assets.
2// Kept intentionally dependency-free.
3
4const CACHE_NAME = 'book-reader-v7';
5const APP_SHELL_URL = '/index.html';
6const CORE_ASSETS = [
7  '/',
8  '/index.html',
9  '/manifest.json',
10  '/offline-precache.json',
11  '/service-worker.js',
12  '/gc-icon.png',
13];
14const PRECACHE_MANIFEST_PATH = '/offline-precache.json';
15const OFFLINE_STATUS_URL = '/__offline-status';
16
17let warmupPromise = null;
18let offlineReady = false;
19
20async function getManifestAssets() {
21  try {
22    const res = await fetch(PRECACHE_MANIFEST_PATH, { cache: 'no-store' });
23    if (!res.ok) return [];
24    const json = await res.json();
25    if (!json || !Array.isArray(json.assets)) return [];
26    return json.assets.filter((x) => typeof x === 'string' && x.startsWith('/'));
27  } catch {
28    return [];
29  }
30}
31
32async function addMany(cache, urls) {
33  for (const url of urls) {
34    try {
35      await cache.add(url);
36    } catch {
37      // Continue even if a single asset fails.
38    }
39  }
40}
41
42async function cacheUrlWithRetry(cache, url, attempts = 3) {
43  for (let i = 0; i < attempts; i++) {
44    try {
45      const req = new Request(url, { cache: 'no-store' });
46      const res = await fetch(req);
47      if (res && res.ok) {
48        await cache.put(url, res.clone());
49        return true;
50      }
51    } catch {
52      // retry
53    }
54  }
55  return false;
56}
57
58async function setOfflineStatus(cache, payload) {
59  try {
60    await cache.put(
61      OFFLINE_STATUS_URL,
62      new Response(JSON.stringify(payload), {
63        headers: { 'Content-Type': 'application/json' },
64      })
65    );
66  } catch {
67    // ignore status write issues
68  }
69}
70
71async function warmOfflineCache() {
72  if (warmupPromise) return warmupPromise;
73
74  warmupPromise = (async () => {
75    const cache = await caches.open(CACHE_NAME);
76    const manifestAssets = await getManifestAssets();
77    const dedupedAssets = Array.from(new Set(manifestAssets));
78
79    let cached = 0;
80    let failed = 0;
81
82    for (const asset of dedupedAssets) {
83      const ok = await cacheUrlWithRetry(cache, asset, 3);
84      if (ok) cached += 1;
85      else failed += 1;
86    }
87
88    offlineReady = dedupedAssets.length > 0 && failed === 0;
89
90    await setOfflineStatus(cache, {
91      ready: offlineReady,
92      total: dedupedAssets.length,
93      cached,
94      failed,
95      updatedAt: new Date().toISOString(),
96    });
97
98    const clients = await self.clients.matchAll({ type: 'window', includeUncontrolled: true });
99    clients.forEach((client) => {
100      client.postMessage({
101        type: 'OFFLINE_CACHE_STATUS',
102        ready: offlineReady,
103        total: dedupedAssets.length,
104        cached,
105        failed,
106      });
107    });
108  })()
109    .catch(async () => {
110      offlineReady = false;
111      try {
112        const cache = await caches.open(CACHE_NAME);
113        await setOfflineStatus(cache, {
114          ready: false,
115          total: 0,
116          cached: 0,
117          failed: 1,
118          updatedAt: new Date().toISOString(),
119        });
120      } catch {
121        // no-op
122      }
123    })
124    .finally(() => {
125      warmupPromise = null;
126    });
127
128  return warmupPromise;
129}
130
131self.addEventListener('message', (event) => {
132  const data = event.data;
133  if (!data || typeof data !== 'object') return;
134
135  if (data.type === 'WARM_OFFLINE_CACHE') {
136    event.waitUntil(warmOfflineCache());
137    return;
138  }
139
140  if (data.type === 'GET_OFFLINE_CACHE_STATUS') {
141    const source = event.source;
142    if (source && typeof source.postMessage === 'function') {
143      source.postMessage({ type: 'OFFLINE_CACHE_STATUS', ready: offlineReady });
144    }
145  }
146});
147
148async function putInCache(req, res) {
149  if (!res || !res.ok) return;
150  const cache = await caches.open(CACHE_NAME);
151  await cache.put(req, res.clone());
152}
153
154self.addEventListener('install', (event) => {
155  event.waitUntil(
156    (async () => {
157      const cache = await caches.open(CACHE_NAME);
158      // Keep install fast and reliable (especially on iOS).
159      // Large content pre-caching is done right after activation and can be retried.
160      await addMany(cache, CORE_ASSETS);
161    })()
162  );
163  self.skipWaiting();
164});
165
166self.addEventListener('activate', (event) => {
167  event.waitUntil(
168    (async () => {
169      const keys = await caches.keys();
170      await Promise.all(keys.map((k) => (k !== CACHE_NAME ? caches.delete(k) : Promise.resolve())));
171      await self.clients.claim();
172      await warmOfflineCache();
173    })()
174  );
175});
176
177self.addEventListener('fetch', (event) => {
178  const req = event.request;
179
180  // Only handle same-origin GET requests.
181  if (req.method !== 'GET') return;
182  if (req.headers.has('range')) return;
183  const url = new URL(req.url);
184  if (url.origin !== self.location.origin) return;
185
186  const isBookContent = url.pathname.startsWith('/book-content/');
187  const isDataFile = url.pathname.startsWith('/data/');
188
189  if (url.pathname === OFFLINE_STATUS_URL) {
190    event.respondWith(
191      (async () => {
192        const cached = await caches.match(OFFLINE_STATUS_URL);
193        if (cached) return cached;
194        return new Response(
195          JSON.stringify({ ready: offlineReady, total: 0, cached: 0, failed: 0 }),
196          { headers: { 'Content-Type': 'application/json' } }
197        );
198      })()
199    );
200    return;
201  }
202
203  // Navigations: network-first, fall back to cached app shell.
204  // This avoids serving stale HTML after deploys (which can reference removed bundles).
205  if (req.mode === 'navigate') {
206    event.respondWith(
207      (async () => {
208        try {
209          const res = await fetch(req, { cache: 'no-store' });
210          event.waitUntil(putInCache(APP_SHELL_URL, res.clone()));
211          return res;
212        } catch {
213          return (await caches.match(APP_SHELL_URL)) || (await caches.match('/')) || Response.error();
214        }
215      })()
216    );
217    return;
218  }
219
220  // Static assets: cache-first, update cache in background.
221  const dest = req.destination;
222  const isStatic = dest === 'script' || dest === 'style' || dest === 'image' || dest === 'font';
223
224  // Book/text/data files are fetched by JS with destination="".
225  // Stale-while-revalidate keeps offline reopen reliable and updates when online.
226  if (isBookContent || isDataFile) {
227    event.respondWith(
228      (async () => {
229        const cached = await caches.match(req);
230        const networkFetch = fetch(req)
231          .then((res) => {
232            event.waitUntil(putInCache(req, res.clone()));
233            return res;
234          })
235          .catch(() => null);
236
237        if (cached) {
238          event.waitUntil(networkFetch);
239          return cached;
240        }
241
242        const net = await networkFetch;
243        return net || Response.error();
244      })()
245    );
246    return;
247  }
248
249  if (isStatic) {
250    event.respondWith(
251      (async () => {
252        const cached = await caches.match(req);
253        if (cached) {
254          event.waitUntil(
255            fetch(req)
256              .then((res) => putInCache(req, res))
257              .catch(() => null)
258          );
259          return cached;
260        }
261
262        const res = await fetch(req);
263        event.waitUntil(putInCache(req, res.clone()));
264        return res;
265      })()
266    );
267  }
268});

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.