Port SW initialization solutions from Kiwix JS (#495)

This commit is contained in:
Jaifroid 2023-11-18 10:36:31 +00:00 committed by GitHub
parent f43f0b5b6e
commit 82c3c27d9d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 83 additions and 72 deletions

View File

@ -386,6 +386,12 @@ self.addEventListener('message', function (event) {
if (event.data.action === 'init') { if (event.data.action === 'init') {
// On 'init' message, we enable the fetchEventListener // On 'init' message, we enable the fetchEventListener
fetchCaptureEnabled = true; fetchCaptureEnabled = true;
// Acdknowledge the init message to all clients
self.clients.matchAll().then(function (clientList) {
clientList.forEach(function (client) {
client.postMessage({ action: 'acknowledge' });
});
});
} else if (event.data.action === 'disable') { } else if (event.data.action === 'disable') {
// On 'disable' message, we disable the fetchEventListener // On 'disable' message, we disable the fetchEventListener
// Note that this code doesn't currently run because the app currently never sends a 'disable' message // Note that this code doesn't currently run because the app currently never sends a 'disable' message

View File

@ -3,21 +3,21 @@
* This file handles the interaction between the Kiwix JS back end and the user * This file handles the interaction between the Kiwix JS back end and the user
* *
* Copyright 2013-2023 Jaifroid, Mossroy and contributors * Copyright 2013-2023 Jaifroid, Mossroy and contributors
* License GPL v3: * Licence GPL v3:
* *
* This file is part of Kiwix. * This file is part of Kiwix.
* *
* Kiwix is free software: you can redistribute it and/or modify * Kiwix is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public Licence as published by
* the Free Software Foundation, either version 3 of the License, or * the Free Software Foundation, either version 3 of the Licence, or
* (at your option) any later version. * (at your option) any later version.
* *
* Kiwix is distributed in the hope that it will be useful, * Kiwix is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of * but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details. * GNU General Public Licence for more details.
* *
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public Licence
* along with Kiwix (file LICENSE-GPLv3.txt). If not, see <http://www.gnu.org/licenses/> * along with Kiwix (file LICENSE-GPLv3.txt). If not, see <http://www.gnu.org/licenses/>
*/ */
@ -57,11 +57,13 @@ const DELAY_BETWEEN_KEEPALIVE_SERVICEWORKER = 30000;
// The global parameter and app state objects are defined in init.js // The global parameter and app state objects are defined in init.js
/* global params, appstate, nw, electronAPI, Windows, webpMachine, dialog, LaunchParams, launchQueue, abstractFilesystemAccess, MSApp */ /* global params, appstate, nw, electronAPI, Windows, webpMachine, dialog, LaunchParams, launchQueue, abstractFilesystemAccess, MSApp */
// Placeholders for the article container, the article window and the article DOM // Placeholders for the article container, the article window, the article DOM and some UI elements
var articleContainer = document.getElementById('articleContent'); var articleContainer = document.getElementById('articleContent');
articleContainer.kiwixType = 'iframe'; articleContainer.kiwixType = 'iframe';
var articleWindow = articleContainer.contentWindow; var articleWindow = articleContainer.contentWindow;
var articleDocument; var articleDocument;
var scrollbox = document.getElementById('scrollbox');
var prefix = document.getElementById('prefix');
// The following variables are used to store the current article and its state // The following variables are used to store the current article and its state
@ -104,8 +106,12 @@ if (typeof Windows !== 'undefined' && Windows.UI && Windows.UI.WebUI && Windows.
}, false); }, false);
} }
// At launch, we set the correct content injection mode
setContentInjectionMode(params.contentInjectionMode);
// Test caching capability // Test caching capability
cache.test(function () {}); cache.test(function () {});
// Unique identifier of the article expected to be displayed // Unique identifier of the article expected to be displayed
appstate.expectedArticleURLToBeDisplayed = ''; appstate.expectedArticleURLToBeDisplayed = '';
// Check if we have managed to switch to PWA mode (if running UWP app) // Check if we have managed to switch to PWA mode (if running UWP app)
@ -216,8 +222,6 @@ function onPointerUp (e) {
if (/UWP/.test(params.appType)) document.body.addEventListener('pointerup', onPointerUp); if (/UWP/.test(params.appType)) document.body.addEventListener('pointerup', onPointerUp);
var prefix = document.getElementById('prefix');
var scrollbox = document.getElementById('scrollbox');
var searchArticlesFocused = false; var searchArticlesFocused = false;
document.getElementById('searchArticles').addEventListener('click', function () { document.getElementById('searchArticles').addEventListener('click', function () {
@ -2663,61 +2667,71 @@ function refreshCacheStatus () {
} }
} }
var initServiceWorkerHandle = null;
var serviceWorkerRegistration = null; var serviceWorkerRegistration = null;
/** /**
* Sends an 'init' message to the ServiceWorker and inititalizes the onmessage event * Sends an 'init' message to the ServiceWorker and inititalizes the onmessage event
* When the event is received, it will provide a MessageChannel port to respond to the ServiceWorker * It is called when the Service Worker is first activated, and also when a new archive is loaded
* When a message is received, it will provide a MessageChannel port to respond to the ServiceWorker
*/ */
function initServiceWorkerMessaging () { function initServiceWorkerMessaging () {
// If no ZIM archive is loaded, return (it will be called when one is loaded) if (!(isServiceWorkerAvailable() && isMessageChannelAvailable())) {
if (!appstate.selectedArchive) return; console.warn('Cannot initiate ServiceWorker messaging, because one or more API is unavailable!');
if (params.contentInjectionMode === 'serviceworker') { return;
// Create a message listener };
navigator.serviceWorker.onmessage = function (event) { // Create a message listener
navigator.serviceWorker.onmessage = function (event) {
if (event.data.error) {
console.error('Error in MessageChannel', event.data.error);
throw event.data.error;
} else if (event.data.action === 'acknowledge') {
// The Service Worker is acknowledging receipt of init message
console.log('SW acknowledged init message');
serviceWorkerRegistration = true;
refreshAPIStatus();
} else if (event.data.action === 'askForContent') {
// The Service Worker is asking for content. Check we have a loaded ZIM in this instance.
// DEV: This can happen if there are various instances of the app open in different tabs or windows, and no archive has been selected in this instance.
if (!appstate.selectedArchive) { if (!appstate.selectedArchive) {
console.warn('Message from SW received, but no archive is selected!'); console.warn('Message from SW received, but no archive is selected!');
return; return;
} }
if (event.data.error) { // See below for explanation of this exception
console.error('Error in MessageChannel', event.data.error); const videoException = appstate.selectedArchive.zimType === 'zimit' && /\/\/youtubei.*player/.test(event.data.title);
throw event.data.error; // Check that the zimFileId in the messageChannel event data is the same as the one in the currently open archive
} // Because the SW broadcasts its request to all open tabs or windows, we need to check that the request is for this instance
if (event.data.action === 'askForContent') { if (event.data.zimFileName !== appstate.selectedArchive.file.name && !videoException) {
// Check that the zimFileId in the messageChannel event data is the same as the one in the currently open archive // Do nothing if the request is not for this instance
// Because the SW broadcasts its request to all open tabs or windows, we need to check that the request is for this instance // console.debug('SW request does not match this instance', '[zimFileName:' + event.data.zimFileName + ' !== ' + appstate.selectedArchive.file.name + ']');
if (event.data.zimFileName !== appstate.selectedArchive.file.name) {
console.warn('SW request does not match this insstance', '[zimFileName:' + event.data.zimFileName + ' !== ' + appstate.selectedArchive.file.name + ']');
if (appstate.selectedArchive.zimType === 'zimit' && /\/\/youtubei.*player/.test(event.data.title)) {
// DEV: This is a hack to allow YouTube videos to play in Zimit archives:
// Because links are embedded in a nested iframe, the SW cannot identify the top-level window from which to request the ZIM content
// Until we find a way to tell where it is coming from, we allow the request through and try to load the content
console.warn('>>> Allowing passthrough to process YouTube video <<<');
} else {
return;
}
}
handleMessageChannelMessage(event)
} else { } else {
console.error('Invalid message received', event.data); if (videoException) {
// DEV: This is a hack to allow YouTube videos to play in Zimit archives:
// Because links are embedded in a nested iframe, the SW cannot identify the top-level window from which to request the ZIM content
// Until we find a way to tell where it is coming from, we allow the request through on all controlled clients and try to load the content
console.warn('>>> Allowing passthrough of SW request to process Zimit video <<<');
}
handleMessageChannelMessage(event);
} }
};
// Send the init message to the ServiceWorker
if (navigator.serviceWorker.controller) {
navigator.serviceWorker.controller.postMessage({
action: 'init'
});
} else if (initServiceWorkerHandle) {
console.error('The Service Worker is active but is not controlling the current page! We have to reload.');
// Turn off failsafe, as this is a controlled reboot
settingsStore.setItem('lastPageLoad', 'rebooting', Infinity);
window.location.reload();
} else { } else {
// If this is the first time we are initiating the SW, allow Promises to complete by delaying potential reload till next tick console.error('Invalid message received', event.data);
console.debug('The Service Worker needs more time to load...');
initServiceWorkerHandle = setTimeout(initServiceWorkerMessaging, 0);
} }
};
// Send the init message to the ServiceWorker
if (navigator.serviceWorker.controller) {
console.log('Initializing SW messaging...');
navigator.serviceWorker.controller.postMessage({
action: 'init'
});
} else if (serviceWorkerRegistration) {
// If this is the first time we are initiating the SW, allow Promises to complete by delaying potential reload till next tick
console.warn('The Service Worker needs more time to load, or else the app was force-refrshed...');
serviceWorkerRegistration = null;
setTimeout(initServiceWorkerMessaging, 1200);
} else {
console.error('The Service Worker is not controlling the current page! We have to reload.');
// Turn off failsafe, as this is a controlled reboot
settingsStore.setItem('lastPageLoad', 'rebooting', Infinity);
window.location.reload();
} }
} }
@ -2736,17 +2750,16 @@ function setContentInjectionMode (value) {
if ('serviceWorker' in navigator) { if ('serviceWorker' in navigator) {
serviceWorkerRegistration = null; serviceWorkerRegistration = null;
} }
// User has switched to jQuery mode, so no longer needs ASSETS_CACHE // User has switched to jQuery mode, so no longer needs ASSETS_CACHE on SW side (it will still be used app-side)
// We should empty it and turn it off to prevent unnecessary space usage // if ('caches' in window && isMessageChannelAvailable()) {
if ('caches' in window && isMessageChannelAvailable()) { // if (isServiceWorkerAvailable() && navigator.serviceWorker.controller) {
if (isServiceWorkerAvailable() && navigator.serviceWorker.controller) { // var channel = new MessageChannel();
var channel = new MessageChannel(); // navigator.serviceWorker.controller.postMessage({
navigator.serviceWorker.controller.postMessage({ // action: { assetsCache: 'disable' }
action: { assetsCache: 'disable' } // }, [channel.port2]);
}, [channel.port2]); // }
} // caches.delete(cache.ASSETS_CACHE);
caches.delete(cache.ASSETS_CACHE); // }
}
refreshAPIStatus(); refreshAPIStatus();
} else if (value === 'serviceworker') { } else if (value === 'serviceworker') {
if (!isServiceWorkerAvailable()) { if (!isServiceWorkerAvailable()) {
@ -2839,21 +2852,13 @@ function setContentInjectionMode (value) {
} }
$('input:radio[name=contentInjectionMode]').prop('checked', false); $('input:radio[name=contentInjectionMode]').prop('checked', false);
$('input:radio[name=contentInjectionMode]').filter('[value="' + value + '"]').prop('checked', true); $('input:radio[name=contentInjectionMode]').filter('[value="' + value + '"]').prop('checked', true);
params.contentInjectionMode = value; // Save the value in the Settings Store, so that to be able to keep it after a reload/restart
// Save the value in a cookie, so that to be able to keep it after a reload/restart
settingsStore.setItem('contentInjectionMode', value, Infinity); settingsStore.setItem('contentInjectionMode', value, Infinity);
setWindowOpenerUI(); setWindowOpenerUI();
// Even in JQuery mode, the PWA needs to be able to serve the app in offline mode
setTimeout(initServiceWorkerMessaging, 600);
} }
// At launch, we try to set the last content injection mode (stored in Settings Store)
setContentInjectionMode(params.contentInjectionMode);
// var contentInjectionMode = settingsStore.getItem('contentInjectionMode');
// if (contentInjectionMode) {
// setContentInjectionMode(contentInjectionMode);
// } else {
// setContentInjectionMode('jquery');
// }
/** /**
* Detects whether the ServiceWorker API is available * Detects whether the ServiceWorker API is available
* https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorker * https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorker

View File

@ -139,7 +139,7 @@ function count (callback) {
var channel = new MessageChannel(); var channel = new MessageChannel();
navigator.serviceWorker.controller.postMessage({ navigator.serviceWorker.controller.postMessage({
action: { action: {
assetsCache: params.assetsCache ? 'enable' : 'disable', assetsCache: params.assetsCache && params.contentInjectionMode === 'serviceworker' ? 'enable' : 'disable',
appCache: params.appCache ? 'enable' : 'disable', appCache: params.appCache ? 'enable' : 'disable',
checkCache: window.location.href checkCache: window.location.href
} }