1'use strict'; 2 3const TTL_MS = 60*1000; 4// The pages will live in RAM so keep it tight 5const MAX_ENTRIES = 5; 6// Like normal map, but: 7// 1. Entries have a TTL 8// 2. Can only hold MAX_ENTRIES elements. 9class FixedSizeTTLMap { 10 #map = new Map(); // key -> {value, cachedAt} 11 12 get(url) { 13 const entry = this.#map.get(url); 14 return (entry && !this.#expired(entry)) ? entry.value : null; 15 } 16 17 #expired(entry) { 18 return Date.now() - entry.cachedAt >= TTL_MS; 19 } 20 21 22 set(url, value) { 23 this.#map.set(url, {value, cachedAt: Date.now()}); 24 this.#cleanup(); 25 } 26 27 clear() { 28 this.#map.clear(); 29 } 30 31 #cleanup() { 32 // Remove expired 33 for (const [url, entry] of this.#map) { 34 if (this.#expired(entry)) { 35 this.#map.delete(url); 36 } 37 } 38 39 if (this.#map.size <= MAX_ENTRIES) return; // Fast return if below capacity 40 41 // Evict oldest by cachedAt 42 const sorted = [...this.#map.entries()].sort((a, b) => a[1].cachedAt - b[1].cachedAt); 43 while (sorted.length > MAX_ENTRIES) this.#map.delete(sorted.shift()[0]); 44 } 45} 46 47 48// The offline page lives in it's own (permanent) cache storage. 49const OFFLINE_PAGE_CACHE_NAME = 'cache3'; 50const OFFLINE_PAGE_URL = 'https://cdn.medino.com/html/offline_v3.html'; 51const offlinePageCacheReady = caches.open(OFFLINE_PAGE_CACHE_NAME); 52 53// The preload caches doesn't need to persist across SW restarts, 54// so it can live in memory. 55const preloadCache = new FixedSizeTTLMap(); 56 57// Used to abort in-progress preload requests 58let fetchController = new AbortController(); 59 60 61self.addEventListener('install', event => { 62 // Add the offline page to the cache 63 offlinePageCacheReady.then(cache => cache.add(OFFLINE_PAGE_URL)); 64 self.skipWaiting(); // Take over from any earlier SW ASAP. 65}); 66 67self.addEventListener('activate', () => { 68 // Take over fetch handling right away, as opposed to after next page load 69 self.clients.claim().then(() => { console.log('Preloading SW now in control.'); }); 70}); 71 72self.addEventListener('message', event => { 73 // The only messages involved at the moment are preload request messages. 74 const url = event.data; 75 76 if (preloadCache.get(url)) return; // already cached 77 78 const referrer = event.source.url; 79 80 const fetchAndCache = async () => { 81 let response; 82 try { 83 response = await fetch(url, {credentials: 'same-origin', // include cookies 84 referrer: referrer, 85 // We can cancel the request 86 signal: fetchController.signal, 87 // You can't serve redirected responses in a fetch handler 88 redirect: 'manual'}); 89 } catch (e) { 90 // AbortError is expected, log any other errors 91 if (e.name !== 'AbortError') console.error(e); 92 return null; 93 } 94 // Here we need to do some tricky stuff to allow the cached response to be re-used multiple times. 95 // As the response.body is a stream, it can otherwise be consumed only once. 96 // 97 // Opaque redirects happen when a redirect request is attempted in redirect:manual mode. 98 // They hide headers & status code from JS, but the browser can use them just fine. 99 // Opaque redirects have a null body so the response can be reused directly. 100 if (response.type === 'opaqueredirect') return () => response.clone(); 101 // Eagerly consume the body to save ~10ms when serving the preloaded response 102 const body = await response.arrayBuffer(); 103 // Each request needs to get a new Response, because the body can only be consumed once, as mentioned above. 104 // Thus we create a new response in a factory function from the body we captured before. 105 return () => new Response(body, {status: response.status, headers: response.headers}); 106 }; 107 preloadCache.set(url, fetchAndCache()); 108}); 109 110 111 112async function serveNavigation(request) { 113 const fetchAndCache = preloadCache.get(request.url); 114 const makeResponse = fetchAndCache ? await fetchAndCache : null; 115 // Return cached response if it exists 116 if (makeResponse) return makeResponse(); 117 118 try { 119 // Otherwise falls back to a normal request 120 return await fetch(request); 121 } catch (error) { 122 // If that fails too, show the offline page 123 console.debug("Network request Failed. Serving offline page " + error); 124 const cache = await offlinePageCacheReady; 125 // The fallback is there in case the offline page hasn't loaded yet 126 return (await cache.match(OFFLINE_PAGE_URL)) || Response.error(); 127 } 128} 129 130 131self.addEventListener('fetch', function(event) { 132 // Use built in fetch handler if not medino domain 133 if (!event.request.url.startsWith(self.location.origin)) return; 134 135 if (['POST','PUT','PATCH','DELETE'].includes(event.request.method)) { 136 fetchController.abort(); // Cancel in-flight preloads 137 fetchController = new AbortController(); // Replace the spent controller 138 preloadCache.clear(); 139 return; 140 } 141 142 // only if it's a proper load of a new page or refresh 143 if (event.request.mode === 'navigate' && 144 // the referrer = url thing is needed for the reload on the offline page, as this one does not get the .destination set 145 (event.request.destination === 'document' || event.request.referrer === event.request.url)) { 146 event.respondWith(serveNavigation(event.request)); 147 } 148});
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.