diff options
| author | toasted-nutbread <toasted-nutbread@users.noreply.github.com> | 2024-01-20 23:13:17 -0500 | 
|---|---|---|
| committer | GitHub <noreply@github.com> | 2024-01-21 04:13:17 +0000 | 
| commit | 6ba1ffe74558dd174e3308d48885fb068fa37d55 (patch) | |
| tree | 5519bbf972096e0e3370171d2b62a26d6164d671 /ext/js | |
| parent | ebdde1ee612a262256ad0384131e53bc29b1e10f (diff) | |
WebExtension class (#551)
* Add WebExtension class
* Use WebExtension class
* Use WebExtension instance for all runtime message sending
* Use getUrl
* Add a sendMessage variant which ignores the response and error
Diffstat (limited to 'ext/js')
| -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 | 
11 files changed, 159 insertions, 92 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', {}); |