PageSourceSearch

https://geoview.info/sw.js

js geoview.info collected 2026-10-02 07:41:47 UTC 7,439 bytes, 145 lines download raw bytes

1/* GeoView PWA. Public page snapshots only; no API, search, tile or third-party cache. */
2'use strict';
3const RELEASE = '20261001-1';
4const CORE = 'geoview-core-' + RELEASE;
5const PAGES = 'geoview-pages-v1';
6const MAX_PAGES = 12;
7const MAX_BYTES = 1024 * 1024;
8const MAX_AGE = 7 * 24 * 60 * 60 * 1000;
9// Icons stay out of the pre-cache: they are 200 of the 278 KB an install used to
10// cost, and the browser fetches them from the manifest only when an install prompt
11// or home-screen entry actually needs one. Clients that discard storage between
12// page views re-ran this install on ~1.4M page views a day, so the icons alone
13// accounted for roughly 5.7M requests and 286 GB of daily egress.
14const ASSETS = ['/offline.html', '/pwa.css', '/pwa.js', '/style.css', '/map-view.css', '/map-view.js'];
15let pageWrites = Promise.resolve();
16
17self.addEventListener('install', event => {
18    // A failed pre-cache leaves the preceding worker in control.
19    event.waitUntil(caches.open(CORE).then(cache => cache.addAll(ASSETS.map(url => new Request(url, {cache: 'reload'})))));
20});
21self.addEventListener('activate', event => {
22    event.waitUntil((async () => {
23        const names = await caches.keys();
24        await Promise.all(names.filter(name => name.startsWith('geoview-core-') && name !== CORE).map(name => caches.delete(name)));
25        await prunePages();
26        await self.clients.claim();
27    })());
28});
29
30function publicPage(url) {
31    return url.origin === self.location.origin && !url.search && !/\.php(?:\/|$)/i.test(url.pathname)
32        && !/^\/(?:admin|captcha|api|tiles|errors|vendor|scripts|data|alkis|\.well-known)(?:[/.]|$)/i.test(url.pathname)
33        && url.pathname !== '/search' && url.pathname !== '/offline.html';
34}
35async function prunePages() {
36    const cache = await caches.open(PAGES);
37    const requests = await cache.keys();
38    const live = [];
39    for (const request of requests) {
40        const response = await cache.match(request);
41        const saved = Number(response?.headers.get('X-GeoView-Saved-At'));
42        if (!saved || Date.now() - saved > MAX_AGE) await cache.delete(request);
43        else live.push(request);
44    }
45    while (live.length > MAX_PAGES) await cache.delete(live.shift());
46}
47function queueWrite(job) {
48    pageWrites = pageWrites.then(job).catch(() => {}); // Storage denial/quota must never break browsing.
49    return pageWrites;
50}
51async function savePage(request, response) {
52    const url = new URL(request.url);
53    if (!publicPage(url) || response.status !== 200 || response.redirected
54        || !response.headers.get('Content-Type')?.includes('text/html')
55        || /no-store|private/i.test(response.headers.get('Cache-Control') || '')
56        || /noindex/i.test(response.headers.get('X-Robots-Tag') || '')
57        || Number(response.headers.get('Content-Length')) > MAX_BYTES) return;
58    const html = await response.text();
59    if (new TextEncoder().encode(html).byteLength > MAX_BYTES
60        || !html.includes('<meta name="geoview-page" content="public">')
61        || /<meta\b[^>]*name=["']robots["'][^>]*content=["'][^"']*noindex/i.test(html)) return;
62    const saved = Date.now();
63    const date = new Date(saved).toISOString().slice(0, 16).replace('T', ' ') + ' UTC';
64    const banner = '<aside class="gv-pwa-snapshot" role="status"><span>Saved copy · ' + date
65        + ' · Maps and search need internet.</span><button type="button" data-gv-reconnect>Reload online</button></aside>';
66    const headers = new Headers(response.headers);
67    headers.delete('Content-Length'); headers.delete('Content-Encoding');
68    headers.set('X-GeoView-Saved-At', String(saved));
69    headers.set('X-GeoView-Title', encodeURIComponent((html.match(/<title>([^<]{0,400})<\/title>/i) || [])[1] || url.pathname));
70    const snapshot = new Response(html.replace(/<body\b[^>]*>/i, body => body + banner), {status: 200, headers});
71    const cache = await caches.open(PAGES);
72    await cache.delete(url.href);
73    await cache.put(url.href, snapshot);
74    await prunePages();
75}
76async function savedPage(request) {
77    const cache = await caches.open(PAGES);
78    const response = await cache.match(request);
79    if (!response) return null;
80    if (Date.now() - Number(response.headers.get('X-GeoView-Saved-At')) > MAX_AGE) {
81        await cache.delete(request); return null;
82    }
83    return response;
84}
85async function fetchWithTimeout(request, ms) {
86    const controller = new AbortController();
87    const timer = setTimeout(() => controller.abort(), ms);
88    try { return await fetch(request, {signal: controller.signal}); }
89    finally { clearTimeout(timer); }
90}
91self.addEventListener('fetch', event => {
92    const request = event.request;
93    const url = new URL(request.url);
94    if (request.method !== 'GET' || url.origin !== self.location.origin) return;
95    if (request.mode === 'navigate') {
96        // Security challenges, errors and HTTP responses always retain their original status.
97        if (!publicPage(url) && url.pathname !== '/search') return;
98        const network = fetchWithTimeout(request, 7000);
99        event.waitUntil(network.then(response => {
100            const copy = response.clone();
101            return queueWrite(() => savePage(request, copy));
102        }).catch(() => {}));
103        event.respondWith(network.catch(async () => {
104            return (publicPage(url) && await savedPage(request))
105                || await (await caches.open(CORE)).match('/offline.html')
106                || new Response('GeoView is offline. Please reconnect and reload.', {status: 503, headers: {'Content-Type': 'text/plain'}});
107        }));
108    } else if (ASSETS.includes(url.pathname)) {
109        event.respondWith(fetchWithTimeout(request, 4000).catch(async () => {
110            return await (await caches.open(CORE)).match(url.pathname) || Response.error();
111        }));
112    }
113});
114self.addEventListener('message', event => {
115    if (!event.source || new URL(event.source.url).origin !== self.location.origin) return;
116    if (event.data?.type === 'ACTIVATE_UPDATE') { event.waitUntil(self.skipWaiting()); return; }
117    if (event.data?.type === 'SAVE_CURRENT_PAGE') {
118        const url = new URL(event.source.url); url.hash = '';
119        if (!publicPage(url)) return;
120        event.waitUntil(queueWrite(async () => {
121            if (await savedPage(url.href)) return;
122            const request = new Request(url.href, {credentials: 'same-origin', cache: 'no-cache'});
123            await savePage(request, await fetchWithTimeout(request, 7000));
124        }));
125        return;
126    }
127    const port = event.ports[0];
128    if (!port) return;
129    if (event.data?.type === 'LIST_PAGES') {
130        event.waitUntil((async () => {
131            await pageWrites;
132            await prunePages();
133            const cache = await caches.open(PAGES);
134            const pages = [];
135            for (const request of await cache.keys()) {
136                const response = await cache.match(request);
137                pages.push({url: request.url, title: response.headers.get('X-GeoView-Title') || '',
138                    saved: Number(response.headers.get('X-GeoView-Saved-At'))});
139            }
140            port.postMessage({pages: pages.reverse()});
141        })().catch(() => port.postMessage({error: 'Storage is unavailable.'})));
142    } else if (event.data?.type === 'CLEAR_PAGES') {
143        event.waitUntil(queueWrite(() => caches.delete(PAGES)).then(() => port.postMessage({ok: true})));
144    }
145});

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.