diff options
-rw-r--r-- | ext/js/app/frontend.js | 8 | ||||
-rw-r--r-- | ext/js/app/popup-proxy.js | 2 | ||||
-rw-r--r-- | ext/js/app/popup-window.js | 4 | ||||
-rw-r--r-- | ext/js/app/popup.js | 2 | ||||
-rw-r--r-- | ext/js/background/backend.js | 11 | ||||
-rw-r--r-- | ext/js/background/background-main.js | 2 | ||||
-rw-r--r-- | ext/js/background/offscreen-proxy.js | 22 | ||||
-rw-r--r-- | ext/js/comm/api.js | 27 | ||||
-rw-r--r-- | ext/js/display/display.js | 7 | ||||
-rw-r--r-- | ext/js/extension/web-extension.js | 107 | ||||
-rw-r--r-- | ext/js/yomitan.js | 59 | ||||
-rw-r--r-- | types/ext/extension.d.ts | 32 | ||||
-rw-r--r-- | types/ext/web-extension.d.ts | 20 |
13 files changed, 179 insertions, 124 deletions
diff --git a/ext/js/app/frontend.js b/ext/js/app/frontend.js index 13d2d9d8..837364ad 100644 --- a/ext/js/app/frontend.js +++ b/ext/js/app/frontend.js @@ -226,7 +226,7 @@ export class Frontend { try { await this._updateOptionsInternal(); } catch (e) { - if (!yomitan.isExtensionUnloaded) { + if (!yomitan.webExtension.unloaded) { throw e; } } @@ -368,7 +368,7 @@ export class Frontend { const scanningOptions = /** @type {import('settings').ProfileOptions} */ (this._options).scanning; if (error !== null) { - if (yomitan.isExtensionUnloaded) { + if (yomitan.webExtension.unloaded) { if (textSource !== null && !passive) { this._showExtensionUnloaded(textSource); } @@ -655,7 +655,7 @@ export class Frontend { try { return this._popup !== null && await this._popup.containsPoint(x, y); } catch (e) { - if (!yomitan.isExtensionUnloaded) { + if (!yomitan.webExtension.unloaded) { throw e; } return false; @@ -742,7 +742,7 @@ export class Frontend { Promise.resolve() ); this._lastShowPromise.catch((error) => { - if (yomitan.isExtensionUnloaded) { return; } + if (yomitan.webExtension.unloaded) { return; } log.error(error); }); return this._lastShowPromise; diff --git a/ext/js/app/popup-proxy.js b/ext/js/app/popup-proxy.js index fa4a448b..856ec086 100644 --- a/ext/js/app/popup-proxy.js +++ b/ext/js/app/popup-proxy.js @@ -320,7 +320,7 @@ export class PopupProxy extends EventDispatcher { try { return await this._invoke(action, params); } catch (e) { - if (!yomitan.isExtensionUnloaded) { throw e; } + if (!yomitan.webExtension.unloaded) { throw e; } return defaultReturnValue; } } diff --git a/ext/js/app/popup-window.js b/ext/js/app/popup-window.js index 60d99612..7a0b6af4 100644 --- a/ext/js/app/popup-window.js +++ b/ext/js/app/popup-window.js @@ -274,7 +274,7 @@ export class PopupWindow extends EventDispatcher { * @returns {Promise<import('display').DirectApiReturn<TName>|undefined>} */ async _invoke(open, action, params) { - if (yomitan.isExtensionUnloaded) { + if (yomitan.webExtension.unloaded) { return void 0; } @@ -290,7 +290,7 @@ export class PopupWindow extends EventDispatcher { message )); } catch (e) { - if (yomitan.isExtensionUnloaded) { + if (yomitan.webExtension.unloaded) { open = false; } } diff --git a/ext/js/app/popup.js b/ext/js/app/popup.js index 0a84f3f7..c741e8f1 100644 --- a/ext/js/app/popup.js +++ b/ext/js/app/popup.js @@ -714,7 +714,7 @@ export class Popup extends EventDispatcher { try { return await this._invoke(action, params); } catch (e) { - if (!yomitan.isExtensionUnloaded) { throw e; } + if (!yomitan.webExtension.unloaded) { throw e; } return void 0; } } diff --git a/ext/js/background/backend.js b/ext/js/background/backend.js index db7a3c0f..b61f27b1 100644 --- a/ext/js/background/backend.js +++ b/ext/js/background/backend.js @@ -49,9 +49,11 @@ import {injectStylesheet} from './script-manager.js'; */ export class Backend { /** - * Creates a new instance. + * @param {import('../extension/web-extension.js').WebExtension} webExtension */ - constructor() { + constructor(webExtension) { + /** @type {import('../extension/web-extension.js').WebExtension} */ + this._webExtension = webExtension; /** @type {JapaneseUtil} */ this._japaneseUtil = new JapaneseUtil(wanakana); /** @type {Environment} */ @@ -80,7 +82,7 @@ export class Backend { }); } else { /** @type {?OffscreenProxy} */ - this._offscreen = new OffscreenProxy(); + this._offscreen = new OffscreenProxy(webExtension); /** @type {DictionaryDatabase|DictionaryDatabaseProxy} */ this._dictionaryDatabase = new DictionaryDatabaseProxy(this._offscreen); /** @type {Translator|TranslatorProxy} */ @@ -1902,8 +1904,7 @@ export class Backend { * @param {import('application').ApiMessage<TName>} message */ _sendMessageIgnoreResponse(message) { - const callback = () => this._checkLastError(chrome.runtime.lastError); - chrome.runtime.sendMessage(message, callback); + this._webExtension.sendMessageIgnoreResponse(message); } /** diff --git a/ext/js/background/background-main.js b/ext/js/background/background-main.js index 2c19e871..f5871a14 100644 --- a/ext/js/background/background-main.js +++ b/ext/js/background/background-main.js @@ -23,7 +23,7 @@ import {Backend} from './backend.js'; async function main() { yomitan.prepare(true); - const backend = new Backend(); + const backend = new Backend(yomitan.webExtension); await backend.prepare(); } diff --git a/ext/js/background/offscreen-proxy.js b/ext/js/background/offscreen-proxy.js index 77f5448a..555c3abc 100644 --- a/ext/js/background/offscreen-proxy.js +++ b/ext/js/background/offscreen-proxy.js @@ -16,12 +16,17 @@ * along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import {isObject} from '../core/utilities.js'; import {ExtensionError} from '../core/extension-error.js'; +import {isObject} from '../core/utilities.js'; import {ArrayBufferUtil} from '../data/sandbox/array-buffer-util.js'; export class OffscreenProxy { - constructor() { + /** + * @param {import('../extension/web-extension.js').WebExtension} webExtension + */ + constructor(webExtension) { + /** @type {import('../extension/web-extension.js').WebExtension} */ + this._webExtension = webExtension; /** @type {?Promise<void>} */ this._creatingOffscreen = null; } @@ -76,16 +81,9 @@ export class OffscreenProxy { * @param {import('offscreen').ApiMessage<TMessageType>} message * @returns {Promise<import('offscreen').ApiReturn<TMessageType>>} */ - sendMessagePromise(message) { - return new Promise((resolve, reject) => { - chrome.runtime.sendMessage(message, (response) => { - try { - resolve(this._getMessageResponseResult(response)); - } catch (error) { - reject(error); - } - }); - }); + async sendMessagePromise(message) { + const response = await this._webExtension.sendMessagePromise(message); + return this._getMessageResponseResult(/** @type {import('core').Response<import('offscreen').ApiReturn<TMessageType>>} */ (response)); } /** diff --git a/ext/js/comm/api.js b/ext/js/comm/api.js index 50814aa2..2e1e8826 100644 --- a/ext/js/comm/api.js +++ b/ext/js/comm/api.js @@ -20,11 +20,11 @@ import {ExtensionError} from '../core/extension-error.js'; export class API { /** - * @param {import('../yomitan.js').Yomitan} yomitan + * @param {import('../extension/web-extension.js').WebExtension} webExtension */ - constructor(yomitan) { - /** @type {import('../yomitan.js').Yomitan} */ - this._yomitan = yomitan; + constructor(webExtension) { + /** @type {import('../extension/web-extension.js').WebExtension} */ + this._webExtension = webExtension; } /** @@ -375,13 +375,15 @@ export class API { const data = {action, params}; return new Promise((resolve, reject) => { try { - this._yomitan.sendMessage(data, (response) => { - this._checkLastError(chrome.runtime.lastError); + this._webExtension.sendMessage(data, (response) => { + this._webExtension.getLastError(); if (response !== null && typeof response === 'object') { - if (typeof response.error !== 'undefined') { - reject(ExtensionError.deserialize(response.error)); + const {error} = /** @type {import('core').UnknownObject} */ (response); + if (typeof error !== 'undefined') { + reject(ExtensionError.deserialize(/** @type {import('core').SerializedError} */ (error))); } else { - resolve(response.result); + const {result} = /** @type {import('core').UnknownObject} */ (response); + resolve(/** @type {import('api').ApiReturn<TAction>} */ (result)); } } else { const message = response === null ? 'Unexpected null response' : `Unexpected response of type ${typeof response}`; @@ -393,11 +395,4 @@ export class API { } }); } - - /** - * @param {chrome.runtime.LastError|undefined} _ignore - */ - _checkLastError(_ignore) { - // NOP - } } diff --git a/ext/js/display/display.js b/ext/js/display/display.js index 677c7c4b..689481f4 100644 --- a/ext/js/display/display.js +++ b/ext/js/display/display.js @@ -390,7 +390,7 @@ export class Display extends EventDispatcher { * @param {Error} error */ onError(error) { - if (yomitan.isExtensionUnloaded) { return; } + if (yomitan.webExtension.unloaded) { return; } log.error(error); } @@ -727,8 +727,7 @@ export class Display extends EventDispatcher { /** @type {import('display').WindowApiHandler<'displayExtensionUnloaded'>} */ _onMessageExtensionUnloaded() { - if (yomitan.isExtensionUnloaded) { return; } - yomitan.triggerExtensionUnloaded(); + yomitan.webExtension.triggerUnloaded(); } // Private @@ -1894,7 +1893,7 @@ export class Display extends EventDispatcher { * @param {import('text-scanner').SearchedEventDetails} details */ _onContentTextScannerSearched({type, dictionaryEntries, sentence, textSource, optionsContext, error}) { - if (error !== null && !yomitan.isExtensionUnloaded) { + if (error !== null && !yomitan.webExtension.unloaded) { log.error(error); } diff --git a/ext/js/extension/web-extension.js b/ext/js/extension/web-extension.js new file mode 100644 index 00000000..95a61339 --- /dev/null +++ b/ext/js/extension/web-extension.js @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2024 Yomitan Authors + * + * This program 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 + * (at your option) any later version. + * + * This program 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +import {EventDispatcher} from '../core/event-dispatcher.js'; +import {toError} from '../core/to-error.js'; + +/** + * @augments EventDispatcher<import('web-extension').Events> + */ +export class WebExtension extends EventDispatcher { + constructor() { + super(); + /** @type {boolean} */ + this._unloaded = false; + } + + /** @type {boolean} */ + get unloaded() { + return this._unloaded; + } + + /** + * @param {string} path + * @returns {string} + */ + getUrl(path) { + return chrome.runtime.getURL(path); + } + + /** + * @param {unknown} message + * @param {(response: unknown) => void} responseCallback + * @throws {Error} + */ + sendMessage(message, responseCallback) { + try { + chrome.runtime.sendMessage(message, responseCallback); + } catch (error) { + this.triggerUnloaded(); + throw toError(error); + } + } + + /** + * @param {unknown} message + * @returns {Promise<unknown>} + */ + sendMessagePromise(message) { + return new Promise((resolve, reject) => { + try { + this.sendMessage(message, (response) => { + const error = this.getLastError(); + if (error !== null) { + reject(error); + } else { + resolve(response); + } + }); + } catch (error) { + reject(error); + } + }); + } + + /** + * @param {unknown} message + */ + sendMessageIgnoreResponse(message) { + this.sendMessage(message, () => { + // Clear the last error + this.getLastError(); + }); + } + + /** + * @returns {?Error} + */ + getLastError() { + const {lastError} = chrome.runtime; + if (typeof lastError !== 'undefined') { + const {message} = lastError; + return new Error(typeof message === 'string' ? message : 'An unknown web extension error occured'); + } + return null; + } + + /** */ + triggerUnloaded() { + if (this._unloaded) { return; } + this._unloaded = true; + this.trigger('unloaded', {}); + } +} diff --git a/ext/js/yomitan.js b/ext/js/yomitan.js index cd3f65fd..33afac27 100644 --- a/ext/js/yomitan.js +++ b/ext/js/yomitan.js @@ -23,6 +23,7 @@ import {EventDispatcher} from './core/event-dispatcher.js'; import {ExtensionError} from './core/extension-error.js'; import {log} from './core/logger.js'; import {deferPromise} from './core/utilities.js'; +import {WebExtension} from './extension/web-extension.js'; /** * @returns {boolean} @@ -61,6 +62,9 @@ export class Yomitan extends EventDispatcher { constructor() { super(); + /** @type {WebExtension} */ + this._webExtension = new WebExtension(); + /** @type {string} */ this._extensionName = 'Yomitan'; try { @@ -73,7 +77,7 @@ export class Yomitan extends EventDispatcher { /** @type {?string} */ this._extensionUrlBase = null; try { - this._extensionUrlBase = chrome.runtime.getURL('/'); + this._extensionUrlBase = this._webExtension.getUrl('/'); } catch (e) { // NOP } @@ -85,10 +89,6 @@ export class Yomitan extends EventDispatcher { /** @type {?CrossFrameAPI} */ this._crossFrame = null; /** @type {boolean} */ - this._isExtensionUnloaded = false; - /** @type {boolean} */ - this._isTriggeringExtensionUnloaded = false; - /** @type {boolean} */ this._isReady = false; const {promise, resolve} = /** @type {import('core').DeferredPromiseDetails<void>} */ (deferPromise()); @@ -110,6 +110,11 @@ export class Yomitan extends EventDispatcher { /* eslint-enable no-multi-spaces */ } + /** @type {WebExtension} */ + get webExtension() { + return this._webExtension; + } + /** * Whether the current frame is the background page/service worker or not. * @type {boolean} @@ -120,14 +125,6 @@ export class Yomitan extends EventDispatcher { } /** - * Whether or not the extension is unloaded. - * @type {boolean} - */ - get isExtensionUnloaded() { - return this._isExtensionUnloaded; - } - - /** * Gets the API instance for communicating with the backend. * This value will be null on the background page/service worker. * @type {API} @@ -156,9 +153,9 @@ export class Yomitan extends EventDispatcher { chrome.runtime.onMessage.addListener(this._onMessage.bind(this)); if (!isBackground) { - this._api = new API(this); + this._api = new API(this._webExtension); - this.sendMessage({action: 'requestBackendReadySignal'}); + await this._webExtension.sendMessagePromise({action: 'requestBackendReadySignal'}); await this._isBackendReadyPromise; this._crossFrame = new CrossFrameAPI(); @@ -174,7 +171,7 @@ export class Yomitan extends EventDispatcher { */ ready() { this._isReady = true; - this.sendMessage({action: 'applicationReady'}); + this._webExtension.sendMessagePromise({action: 'applicationReady'}); } /** @@ -186,36 +183,6 @@ export class Yomitan extends EventDispatcher { return this._extensionUrlBase !== null && url.startsWith(this._extensionUrlBase); } - // TODO : this function needs type safety - /** - * Runs `chrome.runtime.sendMessage()` with additional exception handling events. - * @param {import('extension').ChromeRuntimeSendMessageArgs} args The arguments to be passed to `chrome.runtime.sendMessage()`. - * @throws {Error} Errors thrown by `chrome.runtime.sendMessage()` are re-thrown. - */ - sendMessage(...args) { - try { - // @ts-expect-error - issue with type conversion, somewhat difficult to resolve in pure JS - chrome.runtime.sendMessage(...args); - } catch (e) { - this.triggerExtensionUnloaded(); - throw e; - } - } - - /** - * Triggers the extensionUnloaded event. - */ - triggerExtensionUnloaded() { - this._isExtensionUnloaded = true; - if (this._isTriggeringExtensionUnloaded) { return; } - try { - this._isTriggeringExtensionUnloaded = true; - this.trigger('extensionUnloaded', {}); - } finally { - this._isTriggeringExtensionUnloaded = false; - } - } - /** */ triggerStorageChanged() { this.trigger('storageChanged', {}); diff --git a/types/ext/extension.d.ts b/types/ext/extension.d.ts index d738936e..dc4657f0 100644 --- a/types/ext/extension.d.ts +++ b/types/ext/extension.d.ts @@ -15,38 +15,6 @@ * along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import type * as Core from './core'; - -export type ChromeRuntimeSendMessageArgs1 = [ - message: Core.SafeAny, -]; - -export type ChromeRuntimeSendMessageArgs2 = [ - message: Core.SafeAny, - responseCallback: (response: Core.SafeAny) => void, -]; - -export type ChromeRuntimeSendMessageArgs3 = [ - message: Core.SafeAny, - options: chrome.runtime.MessageOptions, - responseCallback: (response: Core.SafeAny) => void, -]; - -export type ChromeRuntimeSendMessageArgs4 = [ - extensionId: string | undefined | null, - message: Core.SafeAny, - responseCallback: (response: Core.SafeAny) => void, -]; - -export type ChromeRuntimeSendMessageArgs5 = [ - extensionId: string | undefined | null, - message: Core.SafeAny, - options: chrome.runtime.MessageOptions, - responseCallback: (response: Core.SafeAny) => void, -]; - -export type ChromeRuntimeSendMessageArgs = ChromeRuntimeSendMessageArgs1 | ChromeRuntimeSendMessageArgs2 | ChromeRuntimeSendMessageArgs3 | ChromeRuntimeSendMessageArgs4 | ChromeRuntimeSendMessageArgs5; - export type HtmlElementWithContentWindow = HTMLIFrameElement | HTMLFrameElement | HTMLObjectElement; export type ContentOrigin = { diff --git a/types/ext/web-extension.d.ts b/types/ext/web-extension.d.ts new file mode 100644 index 00000000..287e7a72 --- /dev/null +++ b/types/ext/web-extension.d.ts @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2024 Yomitan Authors + * + * This program 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 + * (at your option) any later version. + * + * This program 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +export type Events = { + unloaded: Record<string, never>; +}; |