diff --git a/static/service-worker.js b/static/service-worker.js index d49d096..88220a7 100644 --- a/static/service-worker.js +++ b/static/service-worker.js @@ -1,5 +1,6 @@ -const cacheName = 'cache-v1'; -const precacheResources = [ +const CACHE_VERSION = 1; + +const BASE_CACHE_FILES = [ '/', '/posts/', 'index.html', @@ -24,28 +25,303 @@ const precacheResources = [ '/fonts/fa-brands-400.woff2' ]; -self.addEventListener('install', event => { - console.log('Service worker install event!'); - event.waitUntil( - caches.open(cacheName) - .then(cache => { - return cache.addAll(precacheResources); - }) - ); -}); +const CONTENT_CACHE_FILES = [ + 'posts/resolving-selinux-denials-on-android/index.html', + 'posts/i-m-gonna-blog/index.html', + 'posts/how-to-get-involved-in-open-source/index.html', + 'posts/tools-for-effective-rust-development/index.html' +]; -self.addEventListener('activate', event => { - console.log('Service worker activate event!'); -}); +const CACHE_VERSIONS = { + assets: 'assets-v' + CACHE_VERSION, + content: 'content-v' + CACHE_VERSION +}; -self.addEventListener('fetch', event => { - console.log('Fetch intercepted for:', event.request.url); - event.respondWith(caches.match(event.request) - .then(cachedResponse => { - if (cachedResponse) { - return cachedResponse; - } - return fetch(event.request); - }) - ); -}); +// Define MAX_TTL's in SECONDS for specific file extensions +const MAX_TTL = { + '/': 3600, + html: 3600, + json: 86400, + js: 86400, + css: 86400, +}; + +const CACHE_BLACKLIST = [ + (str) => { + return !str.startsWith('http://localhost'); + }, +]; + +const SUPPORTED_METHODS = [ + 'GET', +]; + +/** + * isBlackListed + * @param {string} url + * @returns {boolean} + */ +function isBlacklisted(url) { + return (CACHE_BLACKLIST.length > 0) ? !CACHE_BLACKLIST.filter((rule) => { + if(typeof rule === 'function') { + return !rule(url); + } else { + return false; + } + }).length : false +} + +/** + * getFileExtension + * @param {string} url + * @returns {string} + */ +function getFileExtension(url) { + let extension = url.split('.').reverse()[0].split('?')[0]; + return (extension.endsWith('/')) ? '/' : extension; +} + +/** + * getTTL + * @param {string} url + */ +function getTTL(url) { + if (typeof url === 'string') { + let extension = getFileExtension(url); + if (typeof MAX_TTL[extension] === 'number') { + return MAX_TTL[extension]; + } else { + return null; + } + } else { + return null; + } +} + +/** + * installServiceWorker + * @returns {Promise} + */ +function installServiceWorker() { + return Promise.all( + [ + caches.open(CACHE_VERSION) + .then( + (cache) => { + return cache.addAll(BASE_CACHE_FILES); + } + ), + caches.open(CACHE_VERSIONS.content) + .then( + (cache) => { + return cache.addAll(CONTENT_CACHE_FILES); + } + ) + ] + ); +} + +/** + * cleanupLegacyCache + * @returns {Promise} + */ +function cleanupLegacyCache() { + + let currentCaches = Object.keys(CACHE_VERSIONS) + .map( + (key) => { + return CACHE_VERSIONS[key]; + } + ); + return new Promise( + (resolve, reject) => { + caches.keys() + .then( + (keys) => { + return legacyKeys = keys.filter( + (key) => { + return !~currentCaches.indexOf(key); + } + ); + } + ) + .then( + (legacy) => { + if (legacy.length) { + Promise.all( + legacy.map( + (legacyKey) => { + return caches.delete(legacyKey) + } + ) + ) + .then( + () => { + resolve() + } + ) + .catch( + (err) => { + reject(err); + } + ); + } else { + resolve(); + } + } + ) + .catch( + () => { + reject(); + } + ); + } + ); +} + +self.addEventListener( + 'install', event => { + event.waitUntil(installServiceWorker()); + } +); + +// The activate handler takes care of cleaning up old caches. +self.addEventListener( + 'activate', event => { + event.waitUntil( + Promise.all( + [ + cleanupLegacyCache(), + ] + ) + .catch( + (err) => { + event.skipWaiting(); + } + ) + ); + } +); + +self.addEventListener( + 'fetch', event => { + + event.respondWith( + caches.open(CACHE_VERSIONS.content) + .then( + (cache) => { + + return cache.match(event.request) + .then( + (response) => { + + if (response) { + + let headers = response.headers.entries(); + let date = null; + + for (let pair of headers) { + if (pair[0] === 'date') { + date = new Date(pair[1]); + } + } + + if (date) { + let age = parseInt((new Date().getTime() - date.getTime()) / 1000); + let ttl = getTTL(event.request.url); + + if (ttl && age > ttl) { + + return new Promise( + (resolve) => { + + return fetch(event.request) + .then( + (updatedResponse) => { + if (updatedResponse) { + cache.put(event.request, updatedResponse.clone()); + resolve(updatedResponse); + } else { + resolve(response) + } + } + ) + .catch( + () => { + resolve(response); + } + ); + + } + ) + .catch( + (err) => { + return response; + } + ); + } else { + return response; + } + + } else { + return response; + } + + } else { + return null; + } + } + ) + .then( + (response) => { + if (response) { + return response; + } else { + return fetch(event.request) + .then( + (response) => { + + if(response.status < 400) { + if (~SUPPORTED_METHODS.indexOf(event.request.method) && !isBlacklisted(event.request.url)) { + cache.put(event.request, response.clone()); + } + return response; + } else { + return caches.open(CACHE_VERSIONS.notFound).then((cache) => { + return cache.match(NOT_FOUND_PAGE); + }) + } + } + ) + .then((response) => { + if(response) { + return response; + } + }) + .catch( + () => { + + return caches.open(CACHE_VERSIONS.offline) + .then( + (offlineCache) => { + return offlineCache.match(OFFLINE_PAGE) + } + ) + + } + ); + } + } + ) + .catch( + (error) => { + console.error(' Error in fetch handler:', error); + throw error; + } + ); + } + ) + ); + + } +); diff --git a/themes/hello-friend-ng/layouts/partials/head.html b/themes/hello-friend-ng/layouts/partials/head.html index 0f307e3..1565969 100644 --- a/themes/hello-friend-ng/layouts/partials/head.html +++ b/themes/hello-friend-ng/layouts/partials/head.html @@ -8,15 +8,17 @@