class Pwa { constructor(self) { this.scope = self; const Version = new URL(location).searchParams.get("v"); this.CACHE_VERSION = Version; //this.BASE_CACHE_FILES=['/js/theme.min.js','/js/theme_light.min.js','/abridge.css','/js/abridge.min.js','/','/404.html','/offline/','/manifest.json']; this.BASE_CACHE_FILES = ['/js/theme.min.js','/js/theme_light.min.js','/abridge.css','/js/abridge.min.js','/','/404.html','/offline/','/manifest.json']; this.host = `${self.location.protocol}//${self.location.host}`; console.info(`Host: ${this.host}`); this.OFFLINE_PAGE = '/offline/'; this.NOT_FOUND_PAGE = '/404.html'; this.CACHE_NAME = `content-v${this.CACHE_VERSION}`; // 3600=1hour, 28800=8hours, 86400=1day, 604800=1week, 1209600=2weeks this.NORM_TTL = 86400;// html, json, xml, anything else undefined this.LONG_TTL = 604800;// js, css // rarely change, may be a good idea to periodically refresh, incase I change these and forget to increment service worker version: this.TTL_LONG = ["js", "css"]; // never change, cache forever unless service worker version is incremented: this.TTL_EXEMPT = ["jpg", "jpeg", "png", "gif", "webp", "avif", "ico", "svg", "otf", "eot", "ttf", "woff", "woff2", "xml", "xsl", "txt", "mp4", "webm", "mp3", "ogg"]; // skip these extensions so they expire same time as html: st,wasm,json(search), xml(sitemap,atom,rss) } canCache(url) { if (url.startsWith("http://localhost")) { return false; } const result = url.toString().startsWith(this.host); return result; } getFileExtension(url) { const extension = url.split('.').reverse()[0].split('?')[0]; return (extension.endsWith('/')) ? '/' : extension; } getTTL(url) { if (typeof url === 'string') { const extension = this.getFileExtension(url); if (this.TTL_LONG.indexOf(extension) > -1) { //console.info(url + ' contains a TTL_LONG extension'); return this.LONG_TTL; } if (this.TTL_EXEMPT.indexOf(extension) > -1) { //console.info(url + ' contains a TTL_EXEMPT extension'); return null; } return this.NORM_TTL; } return null; } async installServiceWorker() { try { await caches.open(this.CACHE_NAME).then((cache) => { return cache.addAll(this.BASE_CACHE_FILES); }, err => console.error(`Error with ${this.CACHE_NAME}`, err)); return this.scope.skipWaiting(); } catch (err) { return console.error("Error with installation: ", err); } } cleanupLegacyCache() { const currentCaches = [this.CACHE_NAME]; return new Promise( (resolve, reject) => { caches.keys() .then((keys) => keys.filter((key) => !~currentCaches.indexOf(key))) .then((legacy) => { if (legacy.length) { Promise.all(legacy.map((legacyKey) => caches.delete(legacyKey)) ).then(() => resolve()).catch((err) => { console.error("Error in legacy cleanup: ", err); reject(err); }); } else { resolve(); } }).catch((err) => { console.error("Error in legacy cleanup: ", err); reject(err); }); }); } async preCacheUrl(url) { const cache = await caches.open(this.CACHE_NAME); const response = await cache.match(url); if (!response) { return fetch(url).then(resp => cache.put(url, resp.clone())); } return null; } register() { this.scope.addEventListener('install', event => { event.waitUntil( Promise.all([ this.installServiceWorker(), this.scope.skipWaiting(), ])); console.info('SW Installed'); }); this.scope.addEventListener('activate', event => { event.waitUntil(Promise.all( [this.cleanupLegacyCache(), this.scope.clients.claim(), this.scope.skipWaiting()]).catch((err) => { console.error("Activation error: ", err); event.skipWaiting(); })); }); this.scope.addEventListener('fetch', event => { event.respondWith( caches.open(this.CACHE_NAME).then(async cache => { // check if this is NOT a resource we allow cacheing (some other domain), if so fetch it instead of cache. if (!this.canCache(event.request.url)) { return fetch(event.request); } // check the cache for the requested resource const response = await cache.match(event.request); if (response) { const headers = response.headers.entries(); let date = null; for (let pair of headers) { if (pair[0] === 'date') { date = new Date(pair[1]); break; } } // date is not working, so ignore TTL and just serve the cached resource. if (!date) { return response; } const age = parseInt(((new Date().getTime() - date.getTime()) / 1000).toString()); const ttl = this.getTTL(event.request.url); if (ttl === null || (ttl && age < ttl)) { // return the resource if it is not beyond the TTL return response; } } // if we made it here then we either did not have the cache, or the TTL was expired. return fetch(event.request.clone()).then(resp => { if (resp.status < 400) { if (this.canCache(event.request.url)) { cache.put(event.request, resp.clone()); } return resp; } else { return cache.match(this.NOT_FOUND_PAGE); } }).catch(err => { // if we made it here then we were unable to fetch the resource. // maybe we were only fetching because of expired TTL, so use the cache regardless of TTL: if (response) { return response; } // if we made it here then we were unable to fetch the resource and do not have it cached. console.error(`Error fetching ${event.request.url} resulted in offline`, err); return cache.match(this.OFFLINE_PAGE); }) })); }); } } const pwa = new Pwa(self); pwa.register();