diff --git a/ext/js/application.js b/ext/js/application.js index c7fa4753bd..cfd6497bbc 100644 --- a/ext/js/application.js +++ b/ext/js/application.js @@ -220,6 +220,9 @@ export class Application extends EventDispatcher { if (mediaDrawingWorker !== null) { api.connectToDatabaseWorker(mediaDrawingWorkerToBackendChannel.port1); } + setInterval(() => { + void api.heartbeat(); + }, 20 * 1000); const {tabId, frameId} = await api.frameInformationGet(); const crossFrameApi = new CrossFrameAPI(api, tabId, frameId); diff --git a/ext/js/background/backend.js b/ext/js/background/backend.js index 6a8e863fc6..9f2588c230 100644 --- a/ext/js/background/backend.js +++ b/ext/js/background/backend.js @@ -184,6 +184,7 @@ export class Backend { ['findAnkiNotes', this._onApiFindAnkiNotes.bind(this)], ['openCrossFramePort', this._onApiOpenCrossFramePort.bind(this)], ['getLanguageSummaries', this._onApiGetLanguageSummaries.bind(this)], + ['heartbeat', this._onApiHeartbeat.bind(this)], ]); /** @type {import('api').PmApiMap} */ @@ -247,6 +248,7 @@ export class Backend { // On Chrome, this is for receiving messages sent with navigator.serviceWorker, which has the benefit of being able to transfer objects, but doesn't accept callbacks (/** @type {ServiceWorkerGlobalScope & typeof globalThis} */ (globalThis)).addEventListener('message', this._onPmMessage.bind(this)); + (/** @type {ServiceWorkerGlobalScope & typeof globalThis} */ (globalThis)).addEventListener('messageerror', this._onPmMessageError.bind(this)); if (this._canObservePermissionsChanges()) { const onPermissionsChanged = this._onWebExtensionEventWrapper(this._onPermissionsChanged.bind(this)); @@ -259,6 +261,7 @@ export class Backend { /** @type {import('api').PmApiHandler<'connectToDatabaseWorker'>} */ async _onPmConnectToDatabaseWorker(_params, ports) { + console.log('Backend: _onPmConnectToDatabaseWorker'); if (ports !== null && ports.length > 0) { await this._dictionaryDatabase.connectToDatabaseWorker(ports[0]); } @@ -303,6 +306,7 @@ export class Backend { // connectToBackend2 e.ports[0].onmessage = this._onPmMessage.bind(this); }); + sharedWorkerBridge.port.addEventListener('messageerror', this._onPmMessageError.bind(this)); sharedWorkerBridge.port.start(); } try { @@ -449,6 +453,16 @@ export class Backend { return invokeApiMapHandler(this._pmApiMap, action, params, [event.ports], () => {}); } + /** + * @param {MessageEvent} event + */ + _onPmMessageError(event) { + const error = new ExtensionError('Backend: Error receiving message via postMessage'); + error.data = event; + log.error(error); + } + + /** * @param {chrome.tabs.ZoomChangeInfo} event */ @@ -527,6 +541,7 @@ export class Backend { /** @type {import('api').ApiHandler<'termsFind'>} */ async _onApiTermsFind({text, details, optionsContext}) { + console.log('Backend: _onApiTermsFind'); const options = this._getProfileOptions(optionsContext, false); const {general: {resultOutputMode: mode, maxResults}} = options; const findTermsOptions = this._getTranslatorFindTermsOptions(mode, details, options); @@ -1031,6 +1046,11 @@ export class Backend { return getLanguageSummaries(); } + /** @type {import('api').ApiHandler<'heartbeat'>} */ + _onApiHeartbeat() { + return void 0; + } + // Command handlers /** diff --git a/ext/js/background/offscreen.js b/ext/js/background/offscreen.js index cbf943f49c..33c46024f8 100644 --- a/ext/js/background/offscreen.js +++ b/ext/js/background/offscreen.js @@ -19,6 +19,8 @@ import {API} from '../comm/api.js'; import {ClipboardReader} from '../comm/clipboard-reader.js'; import {createApiMap, invokeApiMapHandler} from '../core/api-map.js'; +import {ExtensionError} from '../core/extension-error.js'; +import {log} from '../core/log.js'; import {arrayBufferToBase64} from '../data/array-buffer-util.js'; import {DictionaryDatabase} from '../dictionary/dictionary-database.js'; import {WebExtension} from '../extension/web-extension.js'; @@ -187,6 +189,7 @@ export class Offscreen { _createAndRegisterPort() { const mc = new MessageChannel(); mc.port1.onmessage = this._onMcMessage.bind(this); + mc.port1.onmessageerror = this._onMcMessageError.bind(this); this._api.registerOffscreenPort([mc.port2]); } @@ -202,4 +205,13 @@ export class Offscreen { const {action, params} = event.data; invokeApiMapHandler(this._mcApiMap, action, params, [event.ports], () => {}); } + + /** + * @param {MessageEvent} event + */ + _onMcMessageError(event) { + const error = new ExtensionError('Offscreen: Error receiving message via postMessage'); + error.data = event; + log.error(error); + } } diff --git a/ext/js/comm/api.js b/ext/js/comm/api.js index 758bbbf9b1..9e36d68f3a 100644 --- a/ext/js/comm/api.js +++ b/ext/js/comm/api.js @@ -381,6 +381,23 @@ export class API { return this._invoke('openCrossFramePort', {targetTabId, targetFrameId}); } + /** + * This is used to keep the background page alive on Firefox MV3, as it does not support offscreen. + * The reason that backend persistency is required on FF is actually different from the reason it's required on Chromium -- + * on Chromium, persistency (which we achieve via the offscreen page, not via this heartbeat) is required because the load time + * for the IndexedDB is incredibly long, which makes the first lookup after the extension sleeps take one minute+, which is + * not acceptable. However, on Firefox, the database is backed by sqlite and starts very fast. Instead, the problem is that the + * media-drawing-worker on the frontend holds a MessagePort to the database-worker on the backend, which closes when the extension + * sleeps, because the database-worker is killed and currently there is no way to detect a closed port due to + * https://github.com/whatwg/html/issues/1766 / https://github.com/whatwg/html/issues/10201 + * + * So this is our only choice. We can remove this once there is a way to gracefully detect the closed MessagePort and rebuild it. + * @returns {Promise>} + */ + heartbeat() { + return this._invoke('heartbeat', void 0); + } + /** * @param {Transferable[]} transferables */ @@ -392,6 +409,7 @@ export class API { * @param {MessagePort} port */ connectToDatabaseWorker(port) { + console.log('pmInvoke connectToDatabaseWorker'); this._pmInvoke('connectToDatabaseWorker', void 0, [port]); } diff --git a/ext/js/comm/shared-worker-bridge.js b/ext/js/comm/shared-worker-bridge.js index d18f537715..8fda52d88c 100644 --- a/ext/js/comm/shared-worker-bridge.js +++ b/ext/js/comm/shared-worker-bridge.js @@ -16,6 +16,7 @@ */ import {createApiMap, invokeApiMapHandler} from '../core/api-map.js'; +import {ExtensionError} from '../core/extension-error.js'; import {log} from '../core/log.js'; /** @@ -64,12 +65,18 @@ export class SharedWorkerBridge { const {action, params} = event.data; return invokeApiMapHandler(this._apiMap, action, params, [interlocutorPort, event.ports], () => {}); }); + interlocutorPort.addEventListener('messageerror', (/** @type {MessageEvent} */ event) => { + const error = new ExtensionError('SharedWorkerBridge: Error receiving message from interlocutor port when establishing connection'); + error.data = event; + log.error(error); + }); interlocutorPort.start(); }); } /** @type {import('shared-worker').ApiHandler<'registerBackendPort'>} */ _onRegisterBackendPort(_params, interlocutorPort, _ports) { + console.log('SharedWorkerBridge: backend port registered'); this._backendPort = interlocutorPort; } @@ -78,7 +85,7 @@ export class SharedWorkerBridge { if (this._backendPort !== null) { this._backendPort.postMessage(void 0, [ports[0]]); // connectToBackend2 } else { - log.error('SharedWorkerBridge: backend port is not registered'); + log.warn('SharedWorkerBridge: backend port is not registered; this can happen if one of the content scripts loads faster than the backend when extension is reloading'); } } } diff --git a/ext/js/dictionary/dictionary-database-worker-handler.js b/ext/js/dictionary/dictionary-database-worker-handler.js index 2d622b8284..e410c2d4ac 100644 --- a/ext/js/dictionary/dictionary-database-worker-handler.js +++ b/ext/js/dictionary/dictionary-database-worker-handler.js @@ -15,6 +15,7 @@ * along with this program. If not, see . */ +import {ExtensionError} from '../core/extension-error.js'; import {log} from '../core/log.js'; import {DictionaryDatabase} from './dictionary-database.js'; @@ -35,6 +36,11 @@ export class DictionaryDatabaseWorkerHandler { log.error(e); } self.addEventListener('message', this._onMessage.bind(this), false); + self.addEventListener('messageerror', (event) => { + const error = new ExtensionError('DictionaryDatabaseWorkerHandler: Error receiving message from main thread'); + error.data = event; + log.error(error); + }); } // Private diff --git a/ext/js/dictionary/dictionary-database.js b/ext/js/dictionary/dictionary-database.js index 2212279e93..585d977c15 100644 --- a/ext/js/dictionary/dictionary-database.js +++ b/ext/js/dictionary/dictionary-database.js @@ -18,6 +18,7 @@ import {initWasm, Resvg} from '../../lib/resvg-wasm.js'; import {createApiMap, invokeApiMapHandler} from '../core/api-map.js'; +import {ExtensionError} from '../core/extension-error.js'; import {log} from '../core/log.js'; import {safePerformance} from '../core/safe-performance.js'; import {stringReverse} from '../core/utilities.js'; @@ -402,9 +403,11 @@ export class DictionaryDatabase { */ async drawMedia(items, source) { if (this._worker !== null) { // if a worker is available, offload the work to it + console.log('offloading to worker'); this._worker.postMessage({action: 'drawMedia', params: {items}}, [source]); return; } + console.log('doing work in worker'); // otherwise, you are the worker, so do the work safePerformance.mark('drawMedia:start'); @@ -838,14 +841,21 @@ export class DictionaryDatabase { async connectToDatabaseWorker(port) { if (this._worker !== null) { // executes outside of worker + console.log('connecting to worker'); this._worker.postMessage({action: 'connectToDatabaseWorker'}, [port]); return; } // executes inside worker + console.log('connected to worker'); port.onmessage = (/** @type {MessageEvent} */event) => { const {action, params} = event.data; return invokeApiMapHandler(this._apiMap, action, params, [port], () => {}); }; + port.onmessageerror = (event) => { + const error = new ExtensionError('DictionaryDatabase: Error receiving message from main thread'); + error.data = event; + log.error(error); + }; } /** @type {import('dictionary-database').ApiHandler<'drawMedia'>} */ diff --git a/ext/js/display/media-drawing-worker.js b/ext/js/display/media-drawing-worker.js index 6138e5ecc1..d259559c2c 100644 --- a/ext/js/display/media-drawing-worker.js +++ b/ext/js/display/media-drawing-worker.js @@ -17,6 +17,7 @@ import {API} from '../comm/api.js'; import {createApiMap, invokeApiMapHandler} from '../core/api-map.js'; +import {ExtensionError} from '../core/extension-error.js'; import {log} from '../core/log.js'; import {WebExtension} from '../extension/web-extension.js'; @@ -59,15 +60,22 @@ export class MediaDrawingWorker { const message = event.data; return invokeApiMapHandler(this._fromApplicationApiMap, message.action, message.params, [event.ports], () => {}); }); + addEventListener('messageerror', (event) => { + const error = new ExtensionError('MediaDrawingWorker: Error receiving message from application'); + error.data = event; + log.error(error); + }); } /** @type {import('api').PmApiHandler<'drawMedia'>} */ async _onDrawMedia({requests}) { + console.log('MediaDrawingWorker: drawMedia', requests); this._generation++; this._canvasesByGeneration.set(this._generation, requests.map((request) => request.canvas)); this._cleanOldGenerations(); const newRequests = requests.map((request, index) => ({...request, canvas: null, generation: this._generation, canvasIndex: index, canvasWidth: request.canvas.width, canvasHeight: request.canvas.height})); if (this._dbPort !== null) { + console.log('MediaDrawingWorker: sending drawMedia to database worker', newRequests, this._dbPort); this._dbPort.postMessage({action: 'drawMedia', params: {requests: newRequests}}); } else { log.error('no database port available'); @@ -115,10 +123,17 @@ export class MediaDrawingWorker { const dbPort = ports[0]; this._dbPort = dbPort; dbPort.addEventListener('message', (/** @type {MessageEvent} */ event) => { + console.log('MediaDrawingWorker: message from database worker', event.data); const message = event.data; return invokeApiMapHandler(this._fromDatabaseApiMap, message.action, message.params, [event.ports], () => {}); }); + dbPort.addEventListener('messageerror', (event) => { + const error = new ExtensionError('MediaDrawingWorker: Error receiving message from database worker'); + error.data = event; + log.error(error); + }); dbPort.start(); + console.log('MediaDrawingWorker: connected to database worker'); } /** diff --git a/types/ext/api.d.ts b/types/ext/api.d.ts index 7ff5b1d32f..7f0d57d1f8 100644 --- a/types/ext/api.d.ts +++ b/types/ext/api.d.ts @@ -390,6 +390,10 @@ type ApiSurface = { params: void; return: Language.LanguageSummary[]; }; + heartbeat: { + params: void; + return: void; + }; }; type ApiExtraArgs = [sender: chrome.runtime.MessageSender];