diff options
Diffstat (limited to 'ext/js/yomitan.js')
-rw-r--r-- | ext/js/yomitan.js | 76 |
1 files changed, 60 insertions, 16 deletions
diff --git a/ext/js/yomitan.js b/ext/js/yomitan.js index 5535aeb6..7cf67aec 100644 --- a/ext/js/yomitan.js +++ b/ext/js/yomitan.js @@ -18,7 +18,8 @@ import {API} from './comm/api.js'; import {CrossFrameAPI} from './comm/cross-frame-api.js'; -import {EventDispatcher, deferPromise, invokeMessageHandler, log, serializeError} from './core.js'; +import {EventDispatcher, deferPromise, invokeMessageHandler, log} from './core.js'; +import {ExtensionError} from './core/extension-error.js'; // Set up chrome alias if it's not available (Edge Legacy) if ((() => { @@ -36,51 +37,66 @@ if ((() => { } return (hasBrowser && !hasChrome); })()) { + // @ts-expect-error - objects should have roughly the same interface chrome = browser; } /** * The Yomitan class is a core component through which various APIs are handled and invoked. + * @augments EventDispatcher<import('extension').ExtensionEventType> */ -class Yomitan extends EventDispatcher { +export class Yomitan extends EventDispatcher { /** * Creates a new instance. The instance should not be used until it has been fully prepare()'d. */ constructor() { super(); + /** @type {string} */ + this._extensionName = 'Yomitan'; try { const manifest = chrome.runtime.getManifest(); this._extensionName = `${manifest.name} v${manifest.version}`; } catch (e) { - this._extensionName = 'Yomitan'; + // NOP } + /** @type {?string} */ + this._extensionUrlBase = null; try { this._extensionUrlBase = chrome.runtime.getURL('/'); } catch (e) { - this._extensionUrlBase = null; + // NOP } + /** @type {?boolean} */ this._isBackground = null; + /** @type {?API} */ this._api = null; + /** @type {?CrossFrameAPI} */ this._crossFrame = null; + /** @type {boolean} */ this._isExtensionUnloaded = false; + /** @type {boolean} */ this._isTriggeringExtensionUnloaded = false; + /** @type {boolean} */ this._isReady = false; - const {promise, resolve} = deferPromise(); + const {promise, resolve} = /** @type {import('core').DeferredPromiseDetails<void>} */ (deferPromise()); + /** @type {Promise<void>} */ this._isBackendReadyPromise = promise; + /** @type {?(() => void)} */ this._isBackendReadyPromiseResolve = resolve; - this._messageHandlers = new Map([ + /** @type {import('core').MessageHandlerMap} */ + this._messageHandlers = new Map(/** @type {import('core').MessageHandlerArray} */ ([ ['Yomitan.isReady', {async: false, handler: this._onMessageIsReady.bind(this)}], ['Yomitan.backendReady', {async: false, handler: this._onMessageBackendReady.bind(this)}], ['Yomitan.getUrl', {async: false, handler: this._onMessageGetUrl.bind(this)}], ['Yomitan.optionsUpdated', {async: false, handler: this._onMessageOptionsUpdated.bind(this)}], ['Yomitan.databaseUpdated', {async: false, handler: this._onMessageDatabaseUpdated.bind(this)}], ['Yomitan.zoomChanged', {async: false, handler: this._onMessageZoomChanged.bind(this)}] - ]); + ])); } /** @@ -88,7 +104,8 @@ class Yomitan extends EventDispatcher { * @type {boolean} */ get isBackground() { - return this._isBackground; + if (this._isBackground === null) { throw new Error('Not prepared'); } + return /** @type {boolean} */ (this._isBackground); } /** @@ -105,6 +122,7 @@ class Yomitan extends EventDispatcher { * @type {API} */ get api() { + if (this._api === null) { throw new Error('Not prepared'); } return this._api; } @@ -114,6 +132,7 @@ class Yomitan extends EventDispatcher { * @type {CrossFrameAPI} */ get crossFrame() { + if (this._crossFrame === null) { throw new Error('Not prepared'); } return this._crossFrame; } @@ -158,19 +177,22 @@ class Yomitan extends EventDispatcher { /** * Runs `chrome.runtime.sendMessage()` with additional exception handling events. - * @param {...*} args The arguments to be passed to `chrome.runtime.sendMessage()`. - * @returns {void} The result of the `chrome.runtime.sendMessage()` call. + * @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 { - return chrome.runtime.sendMessage(...args); + // @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; } @@ -184,51 +206,73 @@ class Yomitan extends EventDispatcher { // Private + /** + * @returns {string} + */ _getUrl() { return location.href; } - _getLogContext() { - return {url: this._getUrl()}; - } - + /** @type {import('extension').ChromeRuntimeOnMessageCallback} */ _onMessage({action, params}, sender, callback) { const messageHandler = this._messageHandlers.get(action); if (typeof messageHandler === 'undefined') { return false; } return invokeMessageHandler(messageHandler, params, callback, sender); } + /** + * @returns {boolean} + */ _onMessageIsReady() { return this._isReady; } + /** + * @returns {void} + */ _onMessageBackendReady() { if (this._isBackendReadyPromiseResolve === null) { return; } this._isBackendReadyPromiseResolve(); this._isBackendReadyPromiseResolve = null; } + /** + * @returns {{url: string}} + */ _onMessageGetUrl() { return {url: this._getUrl()}; } + /** + * @param {{source: string}} params + */ _onMessageOptionsUpdated({source}) { if (source !== 'background') { this.trigger('optionsUpdated', {source}); } } + /** + * @param {{type: string, cause: string}} params + */ _onMessageDatabaseUpdated({type, cause}) { this.trigger('databaseUpdated', {type, cause}); } + /** + * @param {{oldZoomFactor: number, newZoomFactor: number}} params + */ _onMessageZoomChanged({oldZoomFactor, newZoomFactor}) { this.trigger('zoomChanged', {oldZoomFactor, newZoomFactor}); } + /** + * @param {{error: unknown, level: import('log').LogLevel, context?: import('log').LogContext}} params + */ async _onForwardLog({error, level, context}) { try { - await this._api.log(serializeError(error), level, context); + const api = /** @type {API} */ (this._api); + await api.log(ExtensionError.serialize(error), level, context); } catch (e) { // NOP } |