From fe8bd3e46d176fc074854e79ad7bcada307adc60 Mon Sep 17 00:00:00 2001 From: Jaifroid Date: Thu, 22 Aug 2019 11:34:29 +0100 Subject: [PATCH 01/26] Add native asset caching to Service Worker --- service-worker.js | 219 +++++++++++++++++++++++++++++----------------- 1 file changed, 137 insertions(+), 82 deletions(-) diff --git a/service-worker.js b/service-worker.js index 678d225ae..0004061c3 100644 --- a/service-worker.js +++ b/service-worker.js @@ -23,38 +23,58 @@ */ 'use strict'; -self.addEventListener('install', function(event) { +var CACHE = 'kiwixjs-cache'; + +self.addEventListener('install', function (event) { event.waitUntil(self.skipWaiting()); }); -self.addEventListener('activate', function(event) { +self.addEventListener('activate', function (event) { // "Claiming" the ServiceWorker is necessary to make it work right away, // without the need to reload the page. // See https://developer.mozilla.org/en-US/docs/Web/API/Clients/claim event.waitUntil(self.clients.claim()); }); -var regexpRemoveUrlParameters = new RegExp(/([^?#]+)[?#].*$/); +var outgoingMessagePort = null; +var fetchCaptureEnabled = false; -// This function is duplicated from uiUtil.js -// because using requirejs would force to add the 'fetch' event listener -// after the initial evaluation of this script, which is not supported any more -// in recent versions of the browsers. -// Cf https://bugzilla.mozilla.org/show_bug.cgi?id=1181127 -// TODO : find a way to avoid this duplication +self.addEventListener('fetch', function (event) { + if (fetchCaptureEnabled && + regexpZIMUrlWithNamespace.test(event.request.url) && + event.request.method === "GET") { -/** - * Removes parameters and anchors from a URL - * @param {type} url - * @returns {String} same URL without its parameters and anchors - */ -function removeUrlParameters(url) { - return url.replace(regexpRemoveUrlParameters, "$1"); -} + // The ServiceWorker will handle this request either from CACHE or from app.js -var outgoingMessagePort = null; -var fetchCaptureEnabled = false; -self.addEventListener('fetch', fetchEventListener); + event.respondWith( + // First see if the content is in the cache + fromCache(event.request).then( + function (response) { + // The response was found in the cache so we respond with it + console.log('[SW] Supplying ' + event.request.url + ' from CACHE...'); + return response; + }, + function () { + // The response was not found in the cache so we look for it in the ZIM + // and add it to the cache if it is an asset type (css or js) + return fetchRequestFromZIM(event).then(function(response) { + // Add css or js assets to CACHE (or update their cache entries) + if (/(text|application)\/(css|javascript)/i.test(response.headers.get('Content-Type'))) { + console.log('[SW] Adding ' + event.request.url + ' to CACHE'); + event.waitUntil(updateCache(event.request, response.clone())); + } + return response; + }).catch(function(msgPortData, title) { + console.error('Invalid message received from app.js for ' + title, msgPortData); + return msgPortData; + }); + } + ) + ); + } + // If event.respondWith() isn't called because this wasn't a request that we want to handle, + // then the default request/response behavior will automatically be used. +}); self.addEventListener('message', function (event) { if (event.data.action === 'init') { @@ -73,67 +93,102 @@ self.addEventListener('message', function (event) { // In our case, there is also the ZIM file name, used as a prefix in the URL var regexpZIMUrlWithNamespace = /(?:^|\/)([^\/]+\/)([-ABIJMUVWX])\/(.+)/; -function fetchEventListener(event) { - if (fetchCaptureEnabled) { - if (regexpZIMUrlWithNamespace.test(event.request.url)) { - // The ServiceWorker will handle this request - // Let's ask app.js for that content - event.respondWith(new Promise(function(resolve, reject) { - var nameSpace; - var title; - var titleWithNameSpace; - var regexpResult = regexpZIMUrlWithNamespace.exec(event.request.url); - var prefix = regexpResult[1]; - nameSpace = regexpResult[2]; - title = regexpResult[3]; - - // We need to remove the potential parameters in the URL - title = removeUrlParameters(decodeURIComponent(title)); - - titleWithNameSpace = nameSpace + '/' + title; - - // Let's instanciate a new messageChannel, to allow app.s to give us the content - var messageChannel = new MessageChannel(); - messageChannel.port1.onmessage = function(event) { - if (event.data.action === 'giveContent') { - // Content received from app.js - var contentLength = event.data.content ? event.data.content.byteLength : null; - var contentType = event.data.mimetype; - var headers = new Headers (); - if (contentLength) headers.set('Content-Length', contentLength); - if (contentType) headers.set('Content-Type', contentType); - // Test if the content is a video or audio file - // See kiwix-js #519 and openzim/zimwriterfs #113 for why we test for invalid types like "mp4" or "webm" (without "video/") - // The full list of types produced by zimwriterfs is in https://github.com/openzim/zimwriterfs/blob/master/src/tools.cpp - if (contentLength >= 1 && /^(video|audio)|(^|\/)(mp4|webm|og[gmv]|mpeg)$/i.test(contentType)) { - // In case of a video (at least), Chrome and Edge need these HTTP headers else seeking doesn't work - // (even if we always send all the video content, not the requested range, until the backend supports it) - headers.set('Accept-Ranges', 'bytes'); - headers.set('Content-Range', 'bytes 0-' + (contentLength-1) + '/' + contentLength); - } - var responseInit = { - status: 200, - statusText: 'OK', - headers: headers - }; - - var httpResponse = new Response(event.data.content, responseInit); - - // Let's send the content back from the ServiceWorker - resolve(httpResponse); - } - else if (event.data.action === 'sendRedirect') { - resolve(Response.redirect(prefix + event.data.redirectUrl)); - } - else { - console.error('Invalid message received from app.js for ' + titleWithNameSpace, event.data); - reject(event.data); - } +/** + * Handles fetch events that need to be extracted from the ZIM + * + * @param {Event} fetchEvent The fetch event to be processed + * @returns {Promise} A Promise for the Response or the rejected invalid message port data + */ +function fetchRequestFromZIM(fetchEvent) { + return new Promise(function (resolve, reject) { + var nameSpace; + var title; + var titleWithNameSpace; + var regexpResult = regexpZIMUrlWithNamespace.exec(fetchEvent.request.url); + var prefix = regexpResult[1]; + nameSpace = regexpResult[2]; + title = regexpResult[3]; + + // We need to remove the potential parameters in the URL + title = removeUrlParameters(decodeURIComponent(title)); + + titleWithNameSpace = nameSpace + '/' + title; + + // Let's instantiate a new messageChannel, to allow app.js to give us the content + var messageChannel = new MessageChannel(); + messageChannel.port1.onmessage = function (msgPortEvent) { + if (msgPortEvent.data.action === 'giveContent') { + // Content received from app.js + var contentLength = msgPortEvent.data.content ? msgPortEvent.data.content.byteLength : null; + var contentType = msgPortEvent.data.mimetype; + var headers = new Headers(); + if (contentLength) headers.set('Content-Length', contentLength); + if (contentType) headers.set('Content-Type', contentType); + // Test if the content is a video or audio file + // See kiwix-js #519 and openzim/zimwriterfs #113 for why we test for invalid types like "mp4" or "webm" (without "video/") + // The full list of types produced by zimwriterfs is in https://github.com/openzim/zimwriterfs/blob/master/src/tools.cpp + if (contentLength >= 1 && /^(video|audio)|(^|\/)(mp4|webm|og[gmv]|mpeg)$/i.test(contentType)) { + // In case of a video (at least), Chrome and Edge need these HTTP headers else seeking doesn't work + // (even if we always send all the video content, not the requested range, until the backend supports it) + headers.set('Accept-Ranges', 'bytes'); + headers.set('Content-Range', 'bytes 0-' + (contentLength - 1) + '/' + contentLength); + } + var responseInit = { + status: 200, + statusText: 'OK', + headers: headers }; - outgoingMessagePort.postMessage({'action': 'askForContent', 'title': titleWithNameSpace}, [messageChannel.port2]); - })); - } - // If event.respondWith() isn't called because this wasn't a request that we want to handle, - // then the default request/response behavior will automatically be used. - } + + var httpResponse = new Response(msgPortEvent.data.content, responseInit); + + // Let's send the content back from the ServiceWorker + resolve(httpResponse); + } else if (msgPortEvent.data.action === 'sendRedirect') { + resolve(Response.redirect(prefix + msgPortEvent.data.redirectUrl)); + } else { + reject(msgPortEvent.data, titleWithNameSpace); + } + }; + outgoingMessagePort.postMessage({ + 'action': 'askForContent', + 'title': titleWithNameSpace + }, [messageChannel.port2]); + }); +} + +/** + * Removes parameters and anchors from a URL + * @param {type} url The URL to be processed + * @returns {String} The same URL without its parameters and anchors + */ +function removeUrlParameters(url) { + return url.replace(/([^?#]+)[?#].*$/, '$1'); } + +/** + * Looks up a Request in CACHE and returns a Promise for the matched Response + * @param {Request} request The Request to fulfill from CACHE + * @returns {Response} The cached Response (as a Promise) + */ +function fromCache(request) { + return caches.open(CACHE).then(function (cache) { + return cache.match(request).then(function (matching) { + if (!matching || matching.status === 404) { + return Promise.reject("no-match"); + } + return matching; + }); + }); +} + +/** + * Stores or updates in CACHE the given Request/Response pair + * @param {Request} request The original Request object + * @param {Response} response The Response received from the server/ZIM + * @returns {Promise} A Promise for the update action + */ +function updateCache(request, response) { + return caches.open(CACHE).then(function (cache) { + return cache.put(request, response); + }); +} \ No newline at end of file From bcb29a6c22e257a1039a85b9fc0dd4decc37ef3f Mon Sep 17 00:00:00 2001 From: Jaifroid Date: Thu, 22 Aug 2019 12:49:10 +0100 Subject: [PATCH 02/26] Place remaining regExp at head of file --- service-worker.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/service-worker.js b/service-worker.js index 0004061c3..09f91a42d 100644 --- a/service-worker.js +++ b/service-worker.js @@ -25,6 +25,10 @@ var CACHE = 'kiwixjs-cache'; +// Pattern for ZIM file namespace - see https://wiki.openzim.org/wiki/ZIM_file_format#Namespaces +// In our case, there is also the ZIM file name, used as a prefix in the URL +var regexpZIMUrlWithNamespace = /(?:^|\/)([^\/]+\/)([-ABIJMUVWX])\/(.+)/; + self.addEventListener('install', function (event) { event.waitUntil(self.skipWaiting()); }); @@ -89,10 +93,6 @@ self.addEventListener('message', function (event) { } }); -// Pattern for ZIM file namespace - see https://wiki.openzim.org/wiki/ZIM_file_format#Namespaces -// In our case, there is also the ZIM file name, used as a prefix in the URL -var regexpZIMUrlWithNamespace = /(?:^|\/)([^\/]+\/)([-ABIJMUVWX])\/(.+)/; - /** * Handles fetch events that need to be extracted from the ZIM * From 00adfa19e7095f834444a4bd009d46a19333ebcb Mon Sep 17 00:00:00 2001 From: Jaifroid Date: Fri, 23 Aug 2019 19:09:17 +0100 Subject: [PATCH 03/26] Add memory cache for excluded extensions --- service-worker.js | 46 +++++++++++++++++++++++++++++++++++----------- 1 file changed, 35 insertions(+), 11 deletions(-) diff --git a/service-worker.js b/service-worker.js index 09f91a42d..ef06f00d2 100644 --- a/service-worker.js +++ b/service-worker.js @@ -24,6 +24,15 @@ 'use strict'; var CACHE = 'kiwixjs-cache'; +// DEV: add any Content-Types you wish to cache to the regexp below, separated by '|' +var cachedContentTypesRegexp = /text\/css|text\/javascript|application\/javascript/i; +// DEV: add any URL schemata that should be excluded from caching with the Cache API to the regex below +// As of 08-2019 the chrome-extension: schema is incompatible with the Cache API +// 'example-extension' is included to show how to add another schema if necessary +// You can test this code by temporarily changing 'example-extension' to 'http' and running on localhost +var excludedURLSchema = /^(?:chrome-extension|example-extension):/i; +// This Map will be used as a fallback volatile cache for the URL schemata not supported above +var assetsCache = new Map(); // Pattern for ZIM file namespace - see https://wiki.openzim.org/wiki/ZIM_file_format#Namespaces // In our case, there is also the ZIM file name, used as a prefix in the URL @@ -61,14 +70,15 @@ self.addEventListener('fetch', function (event) { function () { // The response was not found in the cache so we look for it in the ZIM // and add it to the cache if it is an asset type (css or js) - return fetchRequestFromZIM(event).then(function(response) { - // Add css or js assets to CACHE (or update their cache entries) - if (/(text|application)\/(css|javascript)/i.test(response.headers.get('Content-Type'))) { + return fetchRequestFromZIM(event).then(function (response) { + // Add css or js assets to CACHE (or update their cache entries) unless the URL schema is not supported + if (cachedContentTypesRegexp.test(response.headers.get('Content-Type')) && + !excludedURLSchema.test(event.request.url)) { console.log('[SW] Adding ' + event.request.url + ' to CACHE'); event.waitUntil(updateCache(event.request, response.clone())); } return response; - }).catch(function(msgPortData, title) { + }).catch(function (msgPortData, title) { console.error('Invalid message received from app.js for ' + title, msgPortData); return msgPortData; }); @@ -139,6 +149,13 @@ function fetchRequestFromZIM(fetchEvent) { headers: headers }; + // If we are dealing with an excluded schema, store the response in assetsCache instead of Cache + // NB we have to store the data in its constitutent format, otherwise the Response is expired by the system + if (excludedURLSchema.test(fetchEvent.request.url) && cachedContentTypesRegexp.test(contentType)) { + console.log('[SW] Adding EXCLUDED schema URL ' + fetchEvent.request.url + ' to assetsCache'); + assetsCache.set(fetchEvent.request.url, [msgPortEvent.data.content, responseInit]); + } + var httpResponse = new Response(msgPortEvent.data.content, responseInit); // Let's send the content back from the ServiceWorker @@ -171,14 +188,21 @@ function removeUrlParameters(url) { * @returns {Response} The cached Response (as a Promise) */ function fromCache(request) { - return caches.open(CACHE).then(function (cache) { - return cache.match(request).then(function (matching) { - if (!matching || matching.status === 404) { - return Promise.reject("no-match"); - } - return matching; + // If the response has been stored in assetsCache, it is an excluded URL schema + if (assetsCache.has(request.url)) { + var data = assetsCache.get(request.url); + var response = new Response(data[0], data[1]); + return Promise.resolve(response); + } else { + return caches.open(CACHE).then(function (cache) { + return cache.match(request).then(function (matching) { + if (!matching || matching.status === 404) { + return Promise.reject("no-match"); + } + return matching; + }); }); - }); + } } /** From d3152f731e66188e3ea04adf021811eab4519606 Mon Sep 17 00:00:00 2001 From: Jaifroid Date: Sat, 24 Aug 2019 08:35:48 +0100 Subject: [PATCH 04/26] Remove memory cache --- service-worker.js | 33 ++++++++------------------------- 1 file changed, 8 insertions(+), 25 deletions(-) diff --git a/service-worker.js b/service-worker.js index ef06f00d2..5105fb328 100644 --- a/service-worker.js +++ b/service-worker.js @@ -29,10 +29,7 @@ var cachedContentTypesRegexp = /text\/css|text\/javascript|application\/javascri // DEV: add any URL schemata that should be excluded from caching with the Cache API to the regex below // As of 08-2019 the chrome-extension: schema is incompatible with the Cache API // 'example-extension' is included to show how to add another schema if necessary -// You can test this code by temporarily changing 'example-extension' to 'http' and running on localhost var excludedURLSchema = /^(?:chrome-extension|example-extension):/i; -// This Map will be used as a fallback volatile cache for the URL schemata not supported above -var assetsCache = new Map(); // Pattern for ZIM file namespace - see https://wiki.openzim.org/wiki/ZIM_file_format#Namespaces // In our case, there is also the ZIM file name, used as a prefix in the URL @@ -73,7 +70,7 @@ self.addEventListener('fetch', function (event) { return fetchRequestFromZIM(event).then(function (response) { // Add css or js assets to CACHE (or update their cache entries) unless the URL schema is not supported if (cachedContentTypesRegexp.test(response.headers.get('Content-Type')) && - !excludedURLSchema.test(event.request.url)) { + !excludedURLSchema.test(event.request.url)) { console.log('[SW] Adding ' + event.request.url + ' to CACHE'); event.waitUntil(updateCache(event.request, response.clone())); } @@ -149,13 +146,6 @@ function fetchRequestFromZIM(fetchEvent) { headers: headers }; - // If we are dealing with an excluded schema, store the response in assetsCache instead of Cache - // NB we have to store the data in its constitutent format, otherwise the Response is expired by the system - if (excludedURLSchema.test(fetchEvent.request.url) && cachedContentTypesRegexp.test(contentType)) { - console.log('[SW] Adding EXCLUDED schema URL ' + fetchEvent.request.url + ' to assetsCache'); - assetsCache.set(fetchEvent.request.url, [msgPortEvent.data.content, responseInit]); - } - var httpResponse = new Response(msgPortEvent.data.content, responseInit); // Let's send the content back from the ServiceWorker @@ -188,21 +178,14 @@ function removeUrlParameters(url) { * @returns {Response} The cached Response (as a Promise) */ function fromCache(request) { - // If the response has been stored in assetsCache, it is an excluded URL schema - if (assetsCache.has(request.url)) { - var data = assetsCache.get(request.url); - var response = new Response(data[0], data[1]); - return Promise.resolve(response); - } else { - return caches.open(CACHE).then(function (cache) { - return cache.match(request).then(function (matching) { - if (!matching || matching.status === 404) { - return Promise.reject("no-match"); - } - return matching; - }); + return caches.open(CACHE).then(function (cache) { + return cache.match(request).then(function (matching) { + if (!matching || matching.status === 404) { + return Promise.reject("no-match"); + } + return matching; }); - } + }); } /** From 3c5edc052e35a06b5eed6ebaf2a70f7c72c0a90c Mon Sep 17 00:00:00 2001 From: Jaifroid Date: Wed, 28 Aug 2019 08:13:56 +0100 Subject: [PATCH 05/26] Add infrastructure and UI for Cache evacuation --- service-worker.js | 31 +++++++++----- www/index.html | 66 +++++++++++++++++++++++++++++ www/js/app.js | 106 ++++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 189 insertions(+), 14 deletions(-) diff --git a/service-worker.js b/service-worker.js index 5105fb328..470cb60e8 100644 --- a/service-worker.js +++ b/service-worker.js @@ -24,6 +24,7 @@ 'use strict'; var CACHE = 'kiwixjs-cache'; +var useCache = true; // DEV: add any Content-Types you wish to cache to the regexp below, separated by '|' var cachedContentTypesRegexp = /text\/css|text\/javascript|application\/javascript/i; // DEV: add any URL schemata that should be excluded from caching with the Cache API to the regex below @@ -61,7 +62,6 @@ self.addEventListener('fetch', function (event) { fromCache(event.request).then( function (response) { // The response was found in the cache so we respond with it - console.log('[SW] Supplying ' + event.request.url + ' from CACHE...'); return response; }, function () { @@ -71,7 +71,6 @@ self.addEventListener('fetch', function (event) { // Add css or js assets to CACHE (or update their cache entries) unless the URL schema is not supported if (cachedContentTypesRegexp.test(response.headers.get('Content-Type')) && !excludedURLSchema.test(event.request.url)) { - console.log('[SW] Adding ' + event.request.url + ' to CACHE'); event.waitUntil(updateCache(event.request, response.clone())); } return response; @@ -88,15 +87,21 @@ self.addEventListener('fetch', function (event) { }); self.addEventListener('message', function (event) { - if (event.data.action === 'init') { - // On 'init' message, we initialize the outgoingMessagePort and enable the fetchEventListener - outgoingMessagePort = event.ports[0]; - fetchCaptureEnabled = true; + if (event.data.action) { + if (event.data.action === 'init') { + // On 'init' message, we initialize the outgoingMessagePort and enable the fetchEventListener + outgoingMessagePort = event.ports[0]; + fetchCaptureEnabled = true; + } else if (event.data.action === 'disable') { + // On 'disable' message, we delete the outgoingMessagePort and disable the fetchEventListener + outgoingMessagePort = null; + fetchCaptureEnabled = false; + } } - if (event.data.action === 'disable') { - // On 'disable' message, we delete the outgoingMessagePort and disable the fetchEventListener - outgoingMessagePort = null; - fetchCaptureEnabled = false; + if (event.data.useCache) { + // Turns caching on or off (a string value of 'on' turns it on, any other string turns it off) + useCache = event.data.useCache === 'on'; + console.log('[SW] Caching was turned ' + event.data.useCache); } }); @@ -178,11 +183,14 @@ function removeUrlParameters(url) { * @returns {Response} The cached Response (as a Promise) */ function fromCache(request) { + // Prevents use of Cache API if user has disabled it + if (!useCache) return Promise.reject('no-match'); return caches.open(CACHE).then(function (cache) { return cache.match(request).then(function (matching) { if (!matching || matching.status === 404) { return Promise.reject("no-match"); } + console.log('[SW] Supplying ' + request.url + ' from CACHE...'); return matching; }); }); @@ -195,7 +203,10 @@ function fromCache(request) { * @returns {Promise} A Promise for the update action */ function updateCache(request, response) { + // Prevents use of Cache API if user has disabled it + if (!useCache) return Promise.resolve(); return caches.open(CACHE).then(function (cache) { + console.log('[SW] Adding ' + request.url + ' to CACHE'); return cache.put(request, response); }); } \ No newline at end of file diff --git a/www/index.html b/www/index.html index 138ddae60..7bc271cd2 100644 --- a/www/index.html +++ b/www/index.html @@ -232,6 +232,72 @@

Display settings

+ +
+
+

Performance and privacy settings

+
+
+
Speed up archive access
+
+
+
+
+

Kiwix JS can speed up the display of articles by caching assets:

+ +
+
+ +
+
+
+
+ +
+ +
+
+
+
+
+
+
+ +
+
+

Expert Settings

+
+
+
Content injection mode
+
+
+ +
+
+ +
+
+

Expert settings

diff --git a/www/js/app.js b/www/js/app.js index 4a59cc016..3855982e8 100644 --- a/www/js/app.js +++ b/www/js/app.js @@ -44,6 +44,12 @@ define(['jquery', 'zimArchiveLoader', 'util', 'uiUtil', 'cookies','abstractFiles */ var DELAY_BETWEEN_KEEPALIVE_SERVICEWORKER = 30000; + /** + * The name of the Cache API cache to use for caching Service Worker requests and responses + * DEV: Make sure the same name is used at the head of service-worker.js + */ + var CACHE = 'kiwixjs-cache'; + /** * @type ZIMArchive */ @@ -60,6 +66,8 @@ define(['jquery', 'zimArchiveLoader', 'util', 'uiUtil', 'cookies','abstractFiles // Set parameters and associated UI elements from cookie params['hideActiveContentWarning'] = cookies.getItem('hideActiveContentWarning') === 'true'; document.getElementById('hideActiveContentWarningCheck').checked = params.hideActiveContentWarning; + // A global parameter that turns caching on or off and deletes the cache (it defaults to true unless explicitly turned off in UI) + params['useCache'] = cookies.getItem('useCache') !== 'false'; // Define globalDropZone (universal drop area) and configDropZone (highlighting area on Config page) var globalDropZone = document.getElementById('search-article'); @@ -245,6 +253,7 @@ define(['jquery', 'zimArchiveLoader', 'util', 'uiUtil', 'cookies','abstractFiles $('#articleContent').hide(); $('.alert').hide(); refreshAPIStatus(); + refreshCacheStatus(); return false; }); $('#btnAbout').on('click', function(e) { @@ -273,9 +282,42 @@ define(['jquery', 'zimArchiveLoader', 'util', 'uiUtil', 'cookies','abstractFiles params.hideActiveContentWarning = this.checked ? true : false; cookies.setItem('hideActiveContentWarning', params.hideActiveContentWarning, Infinity); }); + document.getElementById('cachedAssetsModeRadioTrue').addEventListener('change', function (e) { + if (e.target.checked) { + cookies.setItem('useCache', true, Infinity); + params.useCache = true; + document.getElementById('clearCacheResult').innerHTML = ''; + refreshCacheStatus(); + } + }); + document.getElementById('cachedAssetsModeRadioFalse').addEventListener('change', function (e) { + if (e.target.checked) { + cookies.setItem('useCache', false, Infinity); + params.useCache = false; + var result; + refreshCacheStatus().then(function (itemsCount) { + result = itemsCount[0] + itemsCount[1]; + cssCache = new Map(); + if ('caches' in window) caches.delete(CACHE); + refreshCacheStatus().then(function () { + document.getElementById('clearCacheResult').innerHTML = 'Items cleared: ' + result + ''; + }); + }); + } + }); + document.getElementById('rememberLastVisitedPageCheck').addEventListener('change', function(e) { + var rememberLastPage = e.target.checked ? true : false; + cookies.setItem('rememberLastPage', rememberLastPage, Infinity); + if (!rememberLastPage) { + cache.clear('lastpages', refreshCacheStatus); + } else { + refreshCacheStatus(); + } + }); + /** - * Displays of refreshes the API status shown to the user + * Displays or refreshes the API status shown to the user */ function refreshAPIStatus() { var apiStatusPanel = document.getElementById('apiStatusDiv'); @@ -311,7 +353,59 @@ define(['jquery', 'zimArchiveLoader', 'util', 'uiUtil', 'cookies','abstractFiles apiStatusPanel.classList.add(apiPanelClass); } + + // Determines cache capability and returns and array with the number of items in cssCache and CACHE + function checkCacheStatus() { + params.cacheCapability = 'memory'; + var memCacheSize = cssCache ? cssCache.size : 0; + if ('caches' in window && /https?:/i.test(window.location.protocol)) + params.cacheCapability = 'cacheAPI'; + if (params.cacheCapability === 'memory') { + // Use Q because browsers without Cache might not have Promise API + return q.resolve([memCacheSize, 0]); + } else { + return caches.open(CACHE).then(function (cache) { + return cache.keys().then(function (keys) { + if (contentInjectionMode === 'jquery') params.cacheCapability = 'memory'; + return [memCacheSize, keys.length]; + }); + }); + } + } + // Refreshes the cache information displayed on the Configuration page and returns the assetsCount + function refreshCacheStatus() { + return checkCacheStatus().then(function(assetsCount) { + var cacheInUse = params.cacheCapability === 'cacheAPI' ? 'CacheAPI' : 'Memory'; + cacheInUse = params.useCache ? cacheInUse : 'None'; + document.getElementById('cacheStatus').innerHTML = '

Cache used: ' + + cacheInUse + '

Assets: ' + + (params.cacheCapability === 'memory' ? assetsCount[0] : assetsCount[1]) + '

'; + var cacheSettings = document.getElementById('cacheSettingsDiv'); + var cacheStatusPanel = document.getElementById('cacheStatusPanel'); + [cacheSettings, cacheStatusPanel].forEach(function(panel) { + // IE11 cannot remove more than one class from a list at a time + panel.classList.remove('panel-success'); + panel.classList.remove('panel-warning'); + if (params.useCache) { + panel.classList.add('panel-success'); + } else { + panel.classList.add('panel-warning'); + } + }); + // Clear count of deleted assets + document.getElementById('clearCacheResult').innerHTML = ''; + // Update radio buttons and checkbox + params.rememberLastPage = true; + document.getElementById('rememberLastVisitedPageCheck').checked = params.rememberLastPage; + document.getElementById('cachedAssetsModeRadio' + (params.useCache ? 'True' : 'False')).checked = true; + // Send a message to Service Worker to turn caching on or off + if (contentInjectionMode === 'serviceworker') + navigator.serviceWorker.controller.postMessage({ 'useCache': params.useCache ? 'on' : 'off' }); + return assetsCount; + }); + } + var contentInjectionMode; var keepAliveServiceWorkerHandle; @@ -419,6 +513,7 @@ define(['jquery', 'zimArchiveLoader', 'util', 'uiUtil', 'cookies','abstractFiles contentInjectionMode = value; // Save the value in a cookie, so that to be able to keep it after a reload/restart cookies.setItem('lastContentInjectionMode', value, Infinity); + refreshCacheStatus(); } // At launch, we try to set the last content injection mode (stored in a cookie) @@ -432,6 +527,9 @@ define(['jquery', 'zimArchiveLoader', 'util', 'uiUtil', 'cookies','abstractFiles var serviceWorkerRegistration = null; + // We need to establish the caching capabilities before first page launch + refreshCacheStatus(); + /** * Tells if the ServiceWorker API is available * https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorker @@ -1201,13 +1299,13 @@ define(['jquery', 'zimArchiveLoader', 'util', 'uiUtil', 'cookies','abstractFiles uiUtil.replaceCSSLinkWithInlineCSS(link, cssContent); cssFulfilled++; } else { - $('#cachingCSS').show(); + if (params.useCache) $('#cachingCSS').show(); selectedArchive.getDirEntryByTitle(title) .then(function (dirEntry) { return selectedArchive.readUtf8File(dirEntry, function (fileDirEntry, content) { var fullUrl = fileDirEntry.namespace + "/" + fileDirEntry.url; - cssCache.set(fullUrl, content); + if (params.useCache) cssCache.set(fullUrl, content); uiUtil.replaceCSSLinkWithInlineCSS(link, content); cssFulfilled++; renderIfCSSFulfilled(fileDirEntry.url); @@ -1234,7 +1332,7 @@ define(['jquery', 'zimArchiveLoader', 'util', 'uiUtil', 'cookies','abstractFiles resizeIFrame(); } else if (title) { title = title.replace(/[^/]+\//g, '').substring(0,18); - $('#cachingCSS').html('Caching ' + title + '...'); + if (params.useCache) $('#cachingCSS').html('Caching ' + title + '...'); } } } From 09cbf00ec483ed1de655ebd057c7d18f0c14fa5e Mon Sep 17 00:00:00 2001 From: Jaifroid Date: Thu, 29 Aug 2019 06:56:54 +0100 Subject: [PATCH 06/26] Remove remember last pages checkbox and controls --- www/index.html | 11 ++--------- www/js/app.js | 12 ------------ 2 files changed, 2 insertions(+), 21 deletions(-) diff --git a/www/index.html b/www/index.html index 7bc271cd2..30405feea 100644 --- a/www/index.html +++ b/www/index.html @@ -235,7 +235,7 @@

Display settings

-

Performance and privacy settings

+

Performance settings

Speed up archive access
@@ -257,13 +257,6 @@

Performance and privacy settings

-
- -