'use strict'; importScripts('vendor/localforage.min.js'); var CACHE = 'calc'; /** * get path from URL. * drop schema, host, query string. */ const getUrlPath = function (url) { var path = '/' + url.split('/').slice(3).join('/'); var paramIndex = path.indexOf('?'); if (paramIndex !== -1) { return path.substring(0, paramIndex); } return path; }; const cachePathList = ['/', '/calc.html']; const cacheFileExtList = ['.js', '.css', '.png', '.jpg', '.gif']; /** * return true if this URL should be cached by service worker. */ const shouldCache = function (url) { const path = getUrlPath(url); for (let cachePath of cachePathList) { // since sw.js is scoped at ./, this should work. if (path.endsWith(cachePath)) { return true; } } return cacheFileExtList.some(function (ext, index, ar) { return path.endsWith(ext); }); }; /** * return true if this URL is a vendor asset. */ const isVendorAsset = function (url) { const path = getUrlPath(url); return path.includes('/vendor/'); }; /** * return true if this URL is static asset. see cacheFileExtList. * files ending in those ext are considered to be static asset. */ const isStaticAsset = function (url) { const path = getUrlPath(url); if (path.endsWith('/sw.js')) { return false; // do not cache service worker js file. } return cacheFileExtList.some(function (ext, index, ar) { return path.endsWith(ext); }); }; /** * cache timestamp key used in indexeddb. * each cached url can have a cache timestamp with it. */ const cacheTimestampKey = function (url) { return "timestamp:" + url; }; // =============== // event handler // =============== self.addEventListener('fetch', function(event) { /** * used as fetch(event.request)'s then function. * cache the resp in service worker's req/resp cache, then return the resp. * * if response is not 2xx, do not cache. */ const cacheRespAndReturn = function (resp) { const respClone = resp.clone(); if (! resp.ok) { return resp; } caches.open(CACHE).then(function (cache) { cache.put(event.request, respClone); console.log("cached " + event.request.url); }); return resp; }; /** * use cache if it exists. Otherwise, fetch and cache it. */ const useServiceWorkerCache = function () { return event.respondWith( caches.match(event.request).then(function (resp) { if (resp) { return resp; } else { return fetch(event.request).then(cacheRespAndReturn); } }) ); }; /** * use cached Response if it's not expired or when network is down. * try renew cache if it's expired. */ const useServiceWorkerCacheIfNotExpired = function () { // console.log("use service worker cache if not expired"); const url = event.request.url; const tsKey = cacheTimestampKey(url); const expireSeconds = 24 * 3600; /** * try update service worker req/resp cache and the ts cache. */ const tryUpdate = function () { return fetch(event.request).then(function (resp) { const respClone = resp.clone(); if (! resp.ok) { console.log("HTTP ERROR: " + resp.status); return resp; // just return, don't cache. } caches.open(CACHE).then(function (cache) { cache.put(event.request, respClone); console.log("cached " + event.request.url); const ts = new Date().toJSON(); localforage.setItem(tsKey, ts).then(function () { console.log("cache ts updated to " + ts + ", url=" + event.request.url); }).catch(function (err) { console.log("cache ts update failed: " + err); }); }); return resp; }).catch(function (_err) { console.log("fetch failed. try use cached resp."); return caches.match(event.request); // nothing to do if // match failed. }); }; return event.respondWith( localforage.getItem(tsKey).then(function (tsString) { if (tsString !== null) { const now = new Date(); const cacheDate = new Date(tsString); const diffSeconds = (now - cacheDate) / 1000; if (diffSeconds > expireSeconds) { console.log("cache expired"); return tryUpdate(); } console.log("cache is valid according to cache ts, url=", event.request.url); return caches.match(event.request).then(function (resp) { if (resp) { return resp; } return fetch(event.request); }); } else { console.log("no ts cache"); return tryUpdate(); } }).catch(function (_err) { console.log("no ts cache"); return tryUpdate(); }) ); }; var url = event.request.url; if (! shouldCache(url)) { console.log("not caching " + url); return fetch(event.request).catch(function (err) { console.log(err); }); } // console.log("will use sw cache on " + url); if (isVendorAsset(url)) { return useServiceWorkerCache(); } else { return useServiceWorkerCacheIfNotExpired(); } });