diff --git a/src/component/data/index.js b/src/component/data/index.js index 760e485e..9ffa42f0 100644 --- a/src/component/data/index.js +++ b/src/component/data/index.js @@ -240,6 +240,26 @@ data.remove = (key) => { window.localStorage.removeItem(key); }; +data.clear_serviceWorker = () => { + if ('serviceWorker' in navigator) { + navigator.serviceWorker.getRegistrations().then((registrations) => { + registrations.forEach((registration) => { + registration.unregister(); + }); + }); + } +}; + +data.clear_cache = () => { + if ('caches' in window) { + caches.keys().then((cacheNames) => { + cacheNames.forEach((cacheName) => { + caches.delete(cacheName); + }); + }); + } +}; + data.backup = (dataToBackup) => { if (dataToBackup) { data.set(APP_NAME + 'Backup', JSON.stringify(dataToBackup)); @@ -315,11 +335,16 @@ data.load = () => { data.wipe = { all: () => { + data.clear_serviceWorker(); + data.clear_cache(); + data.remove(APP_NAME); data.reload.render(); }, partial: () => { + data.clear_serviceWorker(); + data.clear_cache(); bookmark.reset(); data.set(APP_NAME, JSON.stringify({ diff --git a/src/component/version/index.js b/src/component/version/index.js index b77a3d1e..24a2cd09 100644 --- a/src/component/version/index.js +++ b/src/component/version/index.js @@ -1,38 +1,36 @@ -export const version = {}; +export const version = { + number: '7.5.0', + name: 'Delightful Komodo Dragon', + compare: (a, b) => { -version.number = '7.5.0'; + let pa = a.split('.'); -version.name = 'Delightful Komodo Dragon'; + let pb = b.split('.'); -version.compare = (a, b) => { + for (let i = 0; i < 3; i++) { - let pa = a.split('.'); + let na = Number(pa[i]); - let pb = b.split('.'); + let nb = Number(pb[i]); - for (let i = 0; i < 3; i++) { + if (na > nb) { + return 1; + } - let na = Number(pa[i]); + if (nb > na) { + return -1; + } - let nb = Number(pb[i]); + if (!isNaN(na) && isNaN(nb)) { + return 1; + } - if (na > nb) { - return 1; - } - - if (nb > na) { - return -1; - } + if (isNaN(na) && !isNaN(nb)) { + return -1; + } - if (!isNaN(na) && isNaN(nb)) { - return 1; - } - - if (isNaN(na) && !isNaN(nb)) { - return -1; } + return 0; } - - return 0; }; diff --git a/src/initialBackground.js b/src/initialBackground.js index 6d52f422..482e69ee 100644 --- a/src/initialBackground.js +++ b/src/initialBackground.js @@ -13,3 +13,13 @@ if (localStorage.getItem('nightTabStyle')) { } document.querySelector('head').appendChild(style); } + +// check if service worker is available +if ('serviceWorker' in navigator) { + // register service worker + navigator.serviceWorker.register('./service_worker.js').then(() => { + console.log('serviceWorker registered'); + }).catch(error => { + console.log('serviceWorker registration failed', error); + }); +} diff --git a/src/serviceWorker/cachingStrategy.js b/src/serviceWorker/cachingStrategy.js new file mode 100644 index 00000000..d159f628 --- /dev/null +++ b/src/serviceWorker/cachingStrategy.js @@ -0,0 +1,65 @@ +// Caching strategies for service workers +// For more details refer +// https://web.dev/learn/pwa/caching/ + +export const cacheFirst = async function (cacheName, event) { + let cacheResponse = await caches.match(event.request); + let request = event.request; + if (cacheResponse !== undefined) { + // cache hit + return cacheResponse; + } + // miss + else { + const fetchResponse = await fetch(request); + const fetchResponseClone = fetchResponse.clone(); + const cache = await caches.open(cacheName); + await cache.put(request, fetchResponseClone); + return fetchResponse; + } +}; + + +export const networkFirst = async function (cacheName, event) { + let request = event.request; + try { + const fetchResponse = await fetch(request); + const fetchResponseClone = fetchResponse.clone(); + const cache = await caches.open(cacheName); + await cache.put(request, fetchResponseClone); + return fetchResponse; + } + catch (err) { + return caches.match(event.request); + } +}; + +export const networkOnly = async function (cacheName, event) { + return fetch(event.request); +}; + +export const raceCacheNetwork = async function (cacheName, event) { + let request = event.request; + + let cachePromise = caches.match(request).then( + response => { + if (response) { + return response; + } + else + throw Error('cache miss'); + } + ); + + let networkPromise = fetch(request).then( + response => { + let responseClone = response.clone(); + caches.open(cacheName).then(cache => + cache.put(request, responseClone) + ); + return response; + } + ); + + return Promise.any([cachePromise, networkPromise]); +}; diff --git a/src/serviceWorker/policies.js b/src/serviceWorker/policies.js new file mode 100644 index 00000000..10ea404c --- /dev/null +++ b/src/serviceWorker/policies.js @@ -0,0 +1,49 @@ +import { cacheFirst, networkFirst, networkOnly } from './cachingStrategy'; + +/* the various network policies for service workers +these are used to determine how to handle a request +based on the request's url +determine which policy to select is done based on the +first matching pattern */ +export const policies = [ + { + // chrome extension + url: /^chrome-extension:.*$/i, + handle: networkOnly + }, + { + // github pages + url: /^https:\/\/\w*\.github\.io\/nightTab\/.*$/i, + handle: cacheFirst + }, + { + // images + url: /^(ftp|https?):.*\.(jpe?g|png|gif|svg)($|\?.*)/i, + handle: cacheFirst + }, + { + // audios & videos + url: /^(ftp|https?):.*\.(mp\d|webm|ogg|wav|flac)($|\?.*)/i, + handle: cacheFirst + }, + { + // fonts + url: /^(ftp|https?):.*\.(ttf|otf|woff\d?)($|\?.*)/i, + handle: cacheFirst + }, + { + // web content + url: /^(ftp|https?):.*\.([jt]sp?|css|html?)($|\?.*)/i, + handle: networkFirst + }, + { + // web data + url: /^(ftp|https?):.*\.(csv|json|txt|xml)($|\?.*)/i, + handle: networkFirst + }, + // fallback + { + url: /.+/i, + handle: cacheFirst + }, +]; diff --git a/src/serviceWorker/service_worker.js b/src/serviceWorker/service_worker.js new file mode 100644 index 00000000..2653ef02 --- /dev/null +++ b/src/serviceWorker/service_worker.js @@ -0,0 +1,59 @@ +import { policies } from './policies'; +import {APP_NAME} from '../constant'; + +import { version } from '../component/version'; + +const CACHE_NAME = `${APP_NAME}-${version.number}`; + +self.addEventListener('install', event => { // register + console.log('serviceWorker installing...'); + event.waitUntil(async () => { + + await caches.open(CACHE_NAME).then( cache => { + // check if running in chrome extension + if(chrome?.extension) { + console.log('running in chrome extension, nothing to cache'); + } + else + cache.addAll([ + '/', // alias for '/index.html' + '/index.html', + ]); + }); + console.log('serviceWorker installed...'); + }); +}); + +self.addEventListener('fetch', event => { // intercept + + if(event.request.method !== 'GET') + event.respondWith(fetch(event.request)); + + // only cache GET requests + let policy = policies.find( + pattern => pattern.url.test(event.request.url) + ); + event.respondWith(policy.handle(CACHE_NAME, event)); +}); + +// Delete outdated caches +self.addEventListener('activate', function (e) { + e.waitUntil( + caches.keys().then(function (keyList) { + // filter out all caches that are not part of this app + let cacheWhitelist = keyList.filter(function (key) { + return key.indexOf(APP_NAME); + }); + // add current latest cache to whitelist + cacheWhitelist.push(CACHE_NAME); + + return Promise.all(keyList.map(function (key, i) { + if (cacheWhitelist.indexOf(key) === -1) { + console.log('deleting cache : ' + keyList[i]); + return caches.delete(keyList[i]); + } + })); + }) + ); + console.log('serviceWorker version ' + version.number + ' activated'); +}); diff --git a/webpack.common.js b/webpack.common.js index b98656d1..eb51346d 100644 --- a/webpack.common.js +++ b/webpack.common.js @@ -4,10 +4,11 @@ const CopyPlugin = require('copy-webpack-plugin'); module.exports = { entry: { - index: path.resolve(__dirname, 'src', 'index.js') + index: path.resolve(__dirname, 'src', 'index.js'), + service_worker: path.resolve(__dirname, 'src', 'serviceWorker', 'service_worker.js'), }, output: { - filename: '[name].[contenthash].js', + filename: '[name].js', path: path.resolve(__dirname, 'dist/web'), clean: true }, @@ -43,7 +44,8 @@ module.exports = { }, plugins: [ new HtmlWebpackPlugin({ - template: './src/index.html' + template: './src/index.html', + chunks : ['index'] }), new CopyPlugin({ patterns: [{