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') {
// On 'init' message, we enable the fetchEventListener
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') {
// On 'disable' message, we disable the fetchEventListener
// 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
*
* Copyright 2013-2023 Jaifroid, Mossroy and contributors
* License GPL v3:
* Licence GPL v3:
*
* This file is part of Kiwix.
*
* Kiwix is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* it under the terms of the GNU General Public Licence as published by
* the Free Software Foundation, either version 3 of the Licence, or
* (at your option) any later version.
*
* Kiwix is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* 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/>
*/
@ -57,11 +57,13 @@ const DELAY_BETWEEN_KEEPALIVE_SERVICEWORKER = 30000;
// The global parameter and app state objects are defined in init.js
/* 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');
articleContainer.kiwixType = 'iframe';
var articleWindow = articleContainer.contentWindow;
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
@ -104,8 +106,12 @@ if (typeof Windows !== 'undefined' && Windows.UI && Windows.UI.WebUI && Windows.
}, false);
}
// At launch, we set the correct content injection mode
setContentInjectionMode(params.contentInjectionMode);
// Test caching capability
cache.test(function () {});
// Unique identifier of the article expected to be displayed
appstate.expectedArticleURLToBeDisplayed = '';
// 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);
var prefix = document.getElementById('prefix');
var scrollbox = document.getElementById('scrollbox');
var searchArticlesFocused = false;
document.getElementById('searchArticles').addEventListener('click', function () {
@ -2663,61 +2667,71 @@ function refreshCacheStatus () {
}
}
var initServiceWorkerHandle = null;
var serviceWorkerRegistration = null;
/**
* 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 () {
// If no ZIM archive is loaded, return (it will be called when one is loaded)
if (!appstate.selectedArchive) return;
if (params.contentInjectionMode === 'serviceworker') {
if (!(isServiceWorkerAvailable() && isMessageChannelAvailable())) {
console.warn('Cannot initiate ServiceWorker messaging, because one or more API is unavailable!');
return;
};
// 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) {
console.warn('Message from SW received, but no archive is selected!');
return;
}
if (event.data.error) {
console.error('Error in MessageChannel', event.data.error);
throw event.data.error;
}
if (event.data.action === 'askForContent') {
// See below for explanation of this exception
const videoException = appstate.selectedArchive.zimType === 'zimit' && /\/\/youtubei.*player/.test(event.data.title);
// 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.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)) {
if (event.data.zimFileName !== appstate.selectedArchive.file.name && !videoException) {
// Do nothing if the request is not for this instance
// console.debug('SW request does not match this instance', '[zimFileName:' + event.data.zimFileName + ' !== ' + appstate.selectedArchive.file.name + ']');
} else {
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 and try to load the content
console.warn('>>> Allowing passthrough to process YouTube video <<<');
} else {
return;
// 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);
}
handleMessageChannelMessage(event)
} else {
console.error('Invalid message received', event.data);
}
};
// Send the init message to the ServiceWorker
if (navigator.serviceWorker.controller) {
console.log('Initializing SW messaging...');
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.');
} 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();
} else {
// If this is the first time we are initiating the SW, allow Promises to complete by delaying potential reload till next tick
console.debug('The Service Worker needs more time to load...');
initServiceWorkerHandle = setTimeout(initServiceWorkerMessaging, 0);
}
}
}
@ -2736,17 +2750,16 @@ function setContentInjectionMode (value) {
if ('serviceWorker' in navigator) {
serviceWorkerRegistration = null;
}
// User has switched to jQuery mode, so no longer needs ASSETS_CACHE
// We should empty it and turn it off to prevent unnecessary space usage
if ('caches' in window && isMessageChannelAvailable()) {
if (isServiceWorkerAvailable() && navigator.serviceWorker.controller) {
var channel = new MessageChannel();
navigator.serviceWorker.controller.postMessage({
action: { assetsCache: 'disable' }
}, [channel.port2]);
}
caches.delete(cache.ASSETS_CACHE);
}
// User has switched to jQuery mode, so no longer needs ASSETS_CACHE on SW side (it will still be used app-side)
// if ('caches' in window && isMessageChannelAvailable()) {
// if (isServiceWorkerAvailable() && navigator.serviceWorker.controller) {
// var channel = new MessageChannel();
// navigator.serviceWorker.controller.postMessage({
// action: { assetsCache: 'disable' }
// }, [channel.port2]);
// }
// caches.delete(cache.ASSETS_CACHE);
// }
refreshAPIStatus();
} else if (value === 'serviceworker') {
if (!isServiceWorkerAvailable()) {
@ -2839,21 +2852,13 @@ function setContentInjectionMode (value) {
}
$('input:radio[name=contentInjectionMode]').prop('checked', false);
$('input:radio[name=contentInjectionMode]').filter('[value="' + value + '"]').prop('checked', true);
params.contentInjectionMode = value;
// Save the value in a cookie, so that to be able to keep it after a reload/restart
// Save the value in the Settings Store, so that to be able to keep it after a reload/restart
settingsStore.setItem('contentInjectionMode', value, Infinity);
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
* https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorker

View File

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