diff options
author | toasted-nutbread <toasted-nutbread@users.noreply.github.com> | 2023-12-29 19:17:46 -0500 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-12-30 00:17:46 +0000 |
commit | 7303da3991814a0ce220bf2fff3e51b968913b86 (patch) | |
tree | 809c289d824ec2a08c5ff54579766b7f5c5e09e1 /ext/js | |
parent | 1b0e0c551d1505ed4242c04ebac224e5fff81f04 (diff) |
Cross frame API safety (#491)
* Require error type
* Add TODOs
* Fix init
* Updates
* More type safety
* Fix incorrect API map
* Update type safety
* Updates
* Add API
* Update types
* Update types
* Updates
* Remove unused
* Restore types
* Update frame ancestry handler
* Simplify names
* Fix
* Remove old message handlers
Diffstat (limited to 'ext/js')
-rw-r--r-- | ext/js/app/frontend.js | 42 | ||||
-rw-r--r-- | ext/js/app/popup-factory.js | 105 | ||||
-rw-r--r-- | ext/js/app/popup-proxy.js | 48 | ||||
-rw-r--r-- | ext/js/app/popup-window.js | 34 | ||||
-rw-r--r-- | ext/js/app/popup.js | 32 | ||||
-rw-r--r-- | ext/js/comm/cross-frame-api.js | 99 | ||||
-rw-r--r-- | ext/js/comm/frame-ancestry-handler.js | 48 | ||||
-rw-r--r-- | ext/js/comm/frame-offset-forwarder.js | 9 | ||||
-rw-r--r-- | ext/js/core.js | 32 | ||||
-rw-r--r-- | ext/js/display/display-resizer.js | 4 | ||||
-rw-r--r-- | ext/js/display/display.js | 74 | ||||
-rw-r--r-- | ext/js/input/hotkey-handler.js | 7 |
12 files changed, 217 insertions, 317 deletions
diff --git a/ext/js/app/frontend.js b/ext/js/app/frontend.js index e386bf64..9dafde7a 100644 --- a/ext/js/app/frontend.js +++ b/ext/js/app/frontend.js @@ -178,11 +178,11 @@ export class Frontend { /* eslint-disable no-multi-spaces */ yomitan.crossFrame.registerHandlers([ - ['Frontend.closePopup', this._onApiClosePopup.bind(this)], - ['Frontend.copySelection', this._onApiCopySelection.bind(this)], - ['Frontend.getSelectionText', this._onApiGetSelectionText.bind(this)], - ['Frontend.getPopupInfo', this._onApiGetPopupInfo.bind(this)], - ['Frontend.getPageInfo', this._onApiGetPageInfo.bind(this)] + ['frontendClosePopup', this._onApiClosePopup.bind(this)], + ['frontendCopySelection', this._onApiCopySelection.bind(this)], + ['frontendGetSelectionText', this._onApiGetSelectionText.bind(this)], + ['frontendGetPopupInfo', this._onApiGetPopupInfo.bind(this)], + ['frontendGetPageInfo', this._onApiGetPageInfo.bind(this)] ]); /* eslint-enable no-multi-spaces */ @@ -263,48 +263,31 @@ export class Frontend { // API message handlers - /** - * @returns {string} - */ - _onApiGetUrl() { - return window.location.href; - } - - /** - * @returns {void} - */ + /** @type {import('cross-frame-api').ApiHandler<'frontendClosePopup'>} */ _onApiClosePopup() { this._clearSelection(false); } - /** - * @returns {void} - */ + /** @type {import('cross-frame-api').ApiHandler<'frontendCopySelection'>} */ _onApiCopySelection() { // This will not work on Firefox if a popup has focus, which is usually the case when this function is called. document.execCommand('copy'); } - /** - * @returns {string} - */ + /** @type {import('cross-frame-api').ApiHandler<'frontendGetSelectionText'>} */ _onApiGetSelectionText() { const selection = document.getSelection(); return selection !== null ? selection.toString() : ''; } - /** - * @returns {import('frontend').GetPopupInfoResult} - */ + /** @type {import('cross-frame-api').ApiHandler<'frontendGetPopupInfo'>} */ _onApiGetPopupInfo() { return { popupId: (this._popup !== null ? this._popup.id : null) }; } - /** - * @returns {{url: string, documentTitle: string}} - */ + /** @type {import('cross-frame-api').ApiHandler<'frontendGetPageInfo'>} */ _onApiGetPageInfo() { return { url: window.location.href, @@ -620,8 +603,7 @@ export class Frontend { return await this._getDefaultPopup(); } - /** @type {import('frontend').GetPopupInfoResult} */ - const {popupId} = await yomitan.crossFrame.invoke(targetFrameId, 'Frontend.getPopupInfo', {}); + const {popupId} = await yomitan.crossFrame.invoke(targetFrameId, 'frontendGetPopupInfo', void 0); if (popupId === null) { return null; } @@ -905,7 +887,7 @@ export class Frontend { let documentTitle = document.title; if (this._useProxyPopup && this._parentFrameId !== null) { try { - ({url, documentTitle} = await yomitan.crossFrame.invoke(this._parentFrameId, 'Frontend.getPageInfo', {})); + ({url, documentTitle} = await yomitan.crossFrame.invoke(this._parentFrameId, 'frontendGetPageInfo', void 0)); } catch (e) { // NOP } diff --git a/ext/js/app/popup-factory.js b/ext/js/app/popup-factory.js index 184a55ca..0d8cabd4 100644 --- a/ext/js/app/popup-factory.js +++ b/ext/js/app/popup-factory.js @@ -49,21 +49,21 @@ export class PopupFactory { this._frameOffsetForwarder.prepare(); /* eslint-disable no-multi-spaces */ yomitan.crossFrame.registerHandlers([ - ['PopupFactory.getOrCreatePopup', this._onApiGetOrCreatePopup.bind(this)], - ['PopupFactory.setOptionsContext', this._onApiSetOptionsContext.bind(this)], - ['PopupFactory.hide', this._onApiHide.bind(this)], - ['PopupFactory.isVisible', this._onApiIsVisibleAsync.bind(this)], - ['PopupFactory.setVisibleOverride', this._onApiSetVisibleOverride.bind(this)], - ['PopupFactory.clearVisibleOverride', this._onApiClearVisibleOverride.bind(this)], - ['PopupFactory.containsPoint', this._onApiContainsPoint.bind(this)], - ['PopupFactory.showContent', this._onApiShowContent.bind(this)], - ['PopupFactory.setCustomCss', this._onApiSetCustomCss.bind(this)], - ['PopupFactory.clearAutoPlayTimer', this._onApiClearAutoPlayTimer.bind(this)], - ['PopupFactory.setContentScale', this._onApiSetContentScale.bind(this)], - ['PopupFactory.updateTheme', this._onApiUpdateTheme.bind(this)], - ['PopupFactory.setCustomOuterCss', this._onApiSetCustomOuterCss.bind(this)], - ['PopupFactory.getFrameSize', this._onApiGetFrameSize.bind(this)], - ['PopupFactory.setFrameSize', this._onApiSetFrameSize.bind(this)] + ['popupFactoryGetOrCreatePopup', this._onApiGetOrCreatePopup.bind(this)], + ['popupFactorySetOptionsContext', this._onApiSetOptionsContext.bind(this)], + ['popupFactoryHide', this._onApiHide.bind(this)], + ['popupFactoryIsVisible', this._onApiIsVisibleAsync.bind(this)], + ['popupFactorySetVisibleOverride', this._onApiSetVisibleOverride.bind(this)], + ['popupFactoryClearVisibleOverride', this._onApiClearVisibleOverride.bind(this)], + ['popupFactoryContainsPoint', this._onApiContainsPoint.bind(this)], + ['popupFactoryShowContent', this._onApiShowContent.bind(this)], + ['popupFactorySetCustomCss', this._onApiSetCustomCss.bind(this)], + ['popupFactoryClearAutoPlayTimer', this._onApiClearAutoPlayTimer.bind(this)], + ['popupFactorySetContentScale', this._onApiSetContentScale.bind(this)], + ['popupFactoryUpdateTheme', this._onApiUpdateTheme.bind(this)], + ['popupFactorySetCustomOuterCss', this._onApiSetCustomOuterCss.bind(this)], + ['popupFactoryGetFrameSize', this._onApiGetFrameSize.bind(this)], + ['popupFactorySetFrameSize', this._onApiSetFrameSize.bind(this)] ]); /* eslint-enable no-multi-spaces */ } @@ -152,7 +152,7 @@ export class PopupFactory { } const useFrameOffsetForwarder = (parentPopupId === null); /** @type {{id: string, depth: number, frameId: number}} */ - const info = await yomitan.crossFrame.invoke(frameId, 'PopupFactory.getOrCreatePopup', /** @type {import('popup-factory').GetOrCreatePopupDetails} */ ({ + const info = await yomitan.crossFrame.invoke(frameId, 'popupFactoryGetOrCreatePopup', /** @type {import('popup-factory').GetOrCreatePopupDetails} */ ({ id, parentPopupId, frameId, @@ -239,10 +239,7 @@ export class PopupFactory { // API message handlers - /** - * @param {import('popup-factory').GetOrCreatePopupDetails} details - * @returns {Promise<{id: string, depth: number, frameId: number}>} - */ + /** @type {import('cross-frame-api').ApiHandler<'popupFactoryGetOrCreatePopup'>} */ async _onApiGetOrCreatePopup(details) { const popup = await this.getOrCreatePopup(details); return { @@ -252,53 +249,37 @@ export class PopupFactory { }; } - /** - * @param {{id: string, optionsContext: import('settings').OptionsContext}} params - */ + /** @type {import('cross-frame-api').ApiHandler<'popupFactorySetOptionsContext'>} */ async _onApiSetOptionsContext({id, optionsContext}) { const popup = this._getPopup(id); await popup.setOptionsContext(optionsContext); } - /** - * @param {{id: string, changeFocus: boolean}} params - */ + /** @type {import('cross-frame-api').ApiHandler<'popupFactoryHide'>} */ async _onApiHide({id, changeFocus}) { const popup = this._getPopup(id); await popup.hide(changeFocus); } - /** - * @param {{id: string}} params - * @returns {Promise<boolean>} - */ + /** @type {import('cross-frame-api').ApiHandler<'popupFactoryIsVisible'>} */ async _onApiIsVisibleAsync({id}) { const popup = this._getPopup(id); return await popup.isVisible(); } - /** - * @param {{id: string, value: boolean, priority: number}} params - * @returns {Promise<?import('core').TokenString>} - */ + /** @type {import('cross-frame-api').ApiHandler<'popupFactorySetVisibleOverride'>} */ async _onApiSetVisibleOverride({id, value, priority}) { const popup = this._getPopup(id); return await popup.setVisibleOverride(value, priority); } - /** - * @param {{id: string, token: import('core').TokenString}} params - * @returns {Promise<boolean>} - */ + /** @type {import('cross-frame-api').ApiHandler<'popupFactoryClearVisibleOverride'>} */ async _onApiClearVisibleOverride({id, token}) { const popup = this._getPopup(id); return await popup.clearVisibleOverride(token); } - /** - * @param {{id: string, x: number, y: number}} params - * @returns {Promise<boolean>} - */ + /** @type {import('cross-frame-api').ApiHandler<'popupFactoryContainsPoint'>} */ async _onApiContainsPoint({id, x, y}) { const popup = this._getPopup(id); const offset = this._getPopupOffset(popup); @@ -307,10 +288,7 @@ export class PopupFactory { return await popup.containsPoint(x, y); } - /** - * @param {{id: string, details: import('popup').ContentDetails, displayDetails: ?import('display').ContentDetails}} params - * @returns {Promise<void>} - */ + /** @type {import('cross-frame-api').ApiHandler<'popupFactoryShowContent'>} */ async _onApiShowContent({id, details, displayDetails}) { const popup = this._getPopup(id); if (!this._popupCanShow(popup)) { return; } @@ -327,64 +305,43 @@ export class PopupFactory { return await popup.showContent(details, displayDetails); } - /** - * @param {{id: string, css: string}} params - * @returns {Promise<void>} - */ + /** @type {import('cross-frame-api').ApiHandler<'popupFactorySetCustomCss'>} */ async _onApiSetCustomCss({id, css}) { const popup = this._getPopup(id); await popup.setCustomCss(css); } - /** - * @param {{id: string}} params - * @returns {Promise<void>} - */ + /** @type {import('cross-frame-api').ApiHandler<'popupFactoryClearAutoPlayTimer'>} */ async _onApiClearAutoPlayTimer({id}) { const popup = this._getPopup(id); await popup.clearAutoPlayTimer(); } - /** - * @param {{id: string, scale: number}} params - * @returns {Promise<void>} - */ + /** @type {import('cross-frame-api').ApiHandler<'popupFactorySetContentScale'>} */ async _onApiSetContentScale({id, scale}) { const popup = this._getPopup(id); await popup.setContentScale(scale); } - /** - * @param {{id: string}} params - * @returns {Promise<void>} - */ + /** @type {import('cross-frame-api').ApiHandler<'popupFactoryUpdateTheme'>} */ async _onApiUpdateTheme({id}) { const popup = this._getPopup(id); await popup.updateTheme(); } - /** - * @param {{id: string, css: string, useWebExtensionApi: boolean}} params - * @returns {Promise<void>} - */ + /** @type {import('cross-frame-api').ApiHandler<'popupFactorySetCustomOuterCss'>} */ async _onApiSetCustomOuterCss({id, css, useWebExtensionApi}) { const popup = this._getPopup(id); await popup.setCustomOuterCss(css, useWebExtensionApi); } - /** - * @param {{id: string}} params - * @returns {Promise<import('popup').ValidSize>} - */ + /** @type {import('cross-frame-api').ApiHandler<'popupFactoryGetFrameSize'>} */ async _onApiGetFrameSize({id}) { const popup = this._getPopup(id); return await popup.getFrameSize(); } - /** - * @param {{id: string, width: number, height: number}} params - * @returns {Promise<boolean>} - */ + /** @type {import('cross-frame-api').ApiHandler<'popupFactorySetFrameSize'>} */ async _onApiSetFrameSize({id, width, height}) { const popup = this._getPopup(id); return await popup.setFrameSize(width, height); diff --git a/ext/js/app/popup-proxy.js b/ext/js/app/popup-proxy.js index 2821d774..e581be82 100644 --- a/ext/js/app/popup-proxy.js +++ b/ext/js/app/popup-proxy.js @@ -140,7 +140,7 @@ export class PopupProxy extends EventDispatcher { * @returns {Promise<void>} */ async setOptionsContext(optionsContext) { - await this._invokeSafe('PopupFactory.setOptionsContext', {id: this._id, optionsContext}, void 0); + await this._invokeSafe('popupFactorySetOptionsContext', {id: this._id, optionsContext}, void 0); } /** @@ -149,7 +149,7 @@ export class PopupProxy extends EventDispatcher { * @returns {Promise<void>} */ async hide(changeFocus) { - await this._invokeSafe('PopupFactory.hide', {id: this._id, changeFocus}, void 0); + await this._invokeSafe('popupFactoryHide', {id: this._id, changeFocus}, void 0); } /** @@ -157,7 +157,7 @@ export class PopupProxy extends EventDispatcher { * @returns {Promise<boolean>} `true` if the popup is visible, `false` otherwise. */ isVisible() { - return this._invokeSafe('PopupFactory.isVisible', {id: this._id}, false); + return this._invokeSafe('popupFactoryIsVisible', {id: this._id}, false); } /** @@ -168,7 +168,7 @@ export class PopupProxy extends EventDispatcher { * or null if the override wasn't assigned. */ setVisibleOverride(value, priority) { - return this._invokeSafe('PopupFactory.setVisibleOverride', {id: this._id, value, priority}, null); + return this._invokeSafe('popupFactorySetVisibleOverride', {id: this._id, value, priority}, null); } /** @@ -177,7 +177,7 @@ export class PopupProxy extends EventDispatcher { * @returns {Promise<boolean>} `true` if the override existed and was removed, `false` otherwise. */ clearVisibleOverride(token) { - return this._invokeSafe('PopupFactory.clearVisibleOverride', {id: this._id, token}, false); + return this._invokeSafe('popupFactoryClearVisibleOverride', {id: this._id, token}, false); } /** @@ -192,7 +192,7 @@ export class PopupProxy extends EventDispatcher { x += this._frameOffsetX; y += this._frameOffsetY; } - return await this._invokeSafe('PopupFactory.containsPoint', {id: this._id, x, y}, false); + return await this._invokeSafe('popupFactoryContainsPoint', {id: this._id, x, y}, false); } /** @@ -212,7 +212,7 @@ export class PopupProxy extends EventDispatcher { sourceRect.bottom += this._frameOffsetY; } } - await this._invokeSafe('PopupFactory.showContent', {id: this._id, details, displayDetails}, void 0); + await this._invokeSafe('popupFactoryShowContent', {id: this._id, details, displayDetails}, void 0); } /** @@ -221,7 +221,7 @@ export class PopupProxy extends EventDispatcher { * @returns {Promise<void>} */ async setCustomCss(css) { - await this._invokeSafe('PopupFactory.setCustomCss', {id: this._id, css}, void 0); + await this._invokeSafe('popupFactorySetCustomCss', {id: this._id, css}, void 0); } /** @@ -229,7 +229,7 @@ export class PopupProxy extends EventDispatcher { * @returns {Promise<void>} */ async clearAutoPlayTimer() { - await this._invokeSafe('PopupFactory.clearAutoPlayTimer', {id: this._id}, void 0); + await this._invokeSafe('popupFactoryClearAutoPlayTimer', {id: this._id}, void 0); } /** @@ -238,7 +238,7 @@ export class PopupProxy extends EventDispatcher { * @returns {Promise<void>} */ async setContentScale(scale) { - await this._invokeSafe('PopupFactory.setContentScale', {id: this._id, scale}, void 0); + await this._invokeSafe('popupFactorySetContentScale', {id: this._id, scale}, void 0); } /** @@ -254,7 +254,7 @@ export class PopupProxy extends EventDispatcher { * @returns {Promise<void>} */ async updateTheme() { - await this._invokeSafe('PopupFactory.updateTheme', {id: this._id}, void 0); + await this._invokeSafe('popupFactoryUpdateTheme', {id: this._id}, void 0); } /** @@ -265,7 +265,7 @@ export class PopupProxy extends EventDispatcher { * @returns {Promise<void>} */ async setCustomOuterCss(css, useWebExtensionApi) { - await this._invokeSafe('PopupFactory.setCustomOuterCss', {id: this._id, css, useWebExtensionApi}, void 0); + await this._invokeSafe('popupFactorySetCustomOuterCss', {id: this._id, css, useWebExtensionApi}, void 0); } /** @@ -282,7 +282,7 @@ export class PopupProxy extends EventDispatcher { * @returns {Promise<import('popup').ValidSize>} The size and whether or not it is valid. */ getFrameSize() { - return this._invokeSafe('PopupFactory.getFrameSize', {id: this._id}, {width: 0, height: 0, valid: false}); + return this._invokeSafe('popupFactoryGetFrameSize', {id: this._id}, {width: 0, height: 0, valid: false}); } /** @@ -292,32 +292,28 @@ export class PopupProxy extends EventDispatcher { * @returns {Promise<boolean>} `true` if the size assignment was successful, `false` otherwise. */ setFrameSize(width, height) { - return this._invokeSafe('PopupFactory.setFrameSize', {id: this._id, width, height}, false); + return this._invokeSafe('popupFactorySetFrameSize', {id: this._id, width, height}, false); } // Private - // TODO : Type safety /** - * @template {import('core').SerializableObject} TParams - * @template [TReturn=unknown] - * @param {string} action - * @param {TParams} params - * @returns {Promise<TReturn>} + * @template {import('cross-frame-api').ApiNames} TName + * @param {TName} action + * @param {import('cross-frame-api').ApiParams<TName>} params + * @returns {Promise<import('cross-frame-api').ApiReturn<TName>>} */ _invoke(action, params) { return yomitan.crossFrame.invoke(this._frameId, action, params); } - // TODO : Type safety /** - * @template {import('core').SerializableObject} TParams - * @template [TReturn=unknown] + * @template {import('cross-frame-api').ApiNames} TName * @template [TReturnDefault=unknown] - * @param {string} action - * @param {TParams} params + * @param {TName} action + * @param {import('cross-frame-api').ApiParams<TName>} params * @param {TReturnDefault} defaultReturnValue - * @returns {Promise<TReturn|TReturnDefault>} + * @returns {Promise<import('cross-frame-api').ApiReturn<TName>|TReturnDefault>} */ async _invokeSafe(action, params, defaultReturnValue) { try { diff --git a/ext/js/app/popup-window.js b/ext/js/app/popup-window.js index 801afb3f..a696885a 100644 --- a/ext/js/app/popup-window.js +++ b/ext/js/app/popup-window.js @@ -126,7 +126,7 @@ export class PopupWindow extends EventDispatcher { * @returns {Promise<void>} */ async setOptionsContext(optionsContext) { - await this._invoke(false, 'displaySetOptionsContext', {id: this._id, optionsContext}); + await this._invoke(false, 'displaySetOptionsContext', {optionsContext}); } /** @@ -183,7 +183,7 @@ export class PopupWindow extends EventDispatcher { */ async showContent(_details, displayDetails) { if (displayDetails === null) { return; } - await this._invoke(true, 'displaySetContent', {id: this._id, details: displayDetails}); + await this._invoke(true, 'displaySetContent', {details: displayDetails}); } /** @@ -192,7 +192,7 @@ export class PopupWindow extends EventDispatcher { * @returns {Promise<void>} */ async setCustomCss(css) { - await this._invoke(false, 'displaySetCustomCss', {id: this._id, css}); + await this._invoke(false, 'displaySetCustomCss', {css}); } /** @@ -200,7 +200,7 @@ export class PopupWindow extends EventDispatcher { * @returns {Promise<void>} */ async clearAutoPlayTimer() { - await this._invoke(false, 'displayAudioClearAutoPlayTimer', {id: this._id}); + await this._invoke(false, 'displayAudioClearAutoPlayTimer', void 0); } /** @@ -266,24 +266,29 @@ export class PopupWindow extends EventDispatcher { // Private - // TODO : Type safety /** - * @template {import('core').SerializableObject} TParams - * @template [TReturn=unknown] + * @template {import('display').DirectApiNames} TName * @param {boolean} open - * @param {string} action - * @param {TParams} params - * @returns {Promise<TReturn|undefined>} + * @param {TName} action + * @param {import('display').DirectApiParams<TName>} params + * @returns {Promise<import('display').DirectApiReturn<TName>|undefined>} */ async _invoke(open, action, params) { if (yomitan.isExtensionUnloaded) { return void 0; } + const message = /** @type {import('display').DirectApiMessageAny} */ ({action, params}); + const frameId = 0; if (this._popupTabId !== null) { try { - return await yomitan.crossFrame.invokeTab(this._popupTabId, frameId, 'popupMessage', {action, params}); + return /** @type {import('display').DirectApiReturn<TName>} */ (await yomitan.crossFrame.invokeTab( + this._popupTabId, + frameId, + 'displayPopupMessage2', + message + )); } catch (e) { if (yomitan.isExtensionUnloaded) { open = false; @@ -299,6 +304,11 @@ export class PopupWindow extends EventDispatcher { const {tabId} = await yomitan.api.getOrCreateSearchPopup({focus: 'ifCreated'}); this._popupTabId = tabId; - return await yomitan.crossFrame.invokeTab(this._popupTabId, frameId, 'popupMessage', {action, params}); + return /** @type {import('display').DirectApiReturn<TName>} */ (await yomitan.crossFrame.invokeTab( + this._popupTabId, + frameId, + 'displayPopupMessage2', + message + )); } } diff --git a/ext/js/app/popup.js b/ext/js/app/popup.js index a8cdf1a6..0f7fbd87 100644 --- a/ext/js/app/popup.js +++ b/ext/js/app/popup.js @@ -316,7 +316,7 @@ export class Popup extends EventDispatcher { */ async clearAutoPlayTimer() { if (this._frameConnected) { - await this._invokeSafe('displayAudioClearAutoPlayTimer', {}); + await this._invokeSafe('displayAudioClearAutoPlayTimer', void 0); } } @@ -679,13 +679,11 @@ export class Popup extends EventDispatcher { } } - // TODO : Type safety /** - * @template {import('core').SerializableObject} TParams - * @template [TReturn=unknown] - * @param {string} action - * @param {TParams} params - * @returns {Promise<TReturn>} + * @template {import('display').DirectApiNames} TName + * @param {TName} action + * @param {import('display').DirectApiParams<TName>} params + * @returns {Promise<import('display').DirectApiReturn<TName>>} */ async _invoke(action, params) { const contentWindow = this._frame.contentWindow; @@ -693,17 +691,21 @@ export class Popup extends EventDispatcher { throw new Error(`Failed to invoke action ${action}: frame state invalid`); } - const message = this._frameClient.createMessage({action, params}); - return await yomitan.crossFrame.invoke(this._frameClient.frameId, 'popupMessage', message); + /** @type {import('display').DirectApiMessage<TName>} */ + const message = {action, params}; + const wrappedMessage = this._frameClient.createMessage(message); + return /** @type {import('display').DirectApiReturn<TName>} */ (await yomitan.crossFrame.invoke( + this._frameClient.frameId, + 'displayPopupMessage1', + /** @type {import('display').DirectApiFrameClientMessageAny} */ (wrappedMessage) + )); } - // TODO : Type safety /** - * @template {import('core').SerializableObject} TParams - * @template [TReturn=unknown] - * @param {string} action - * @param {TParams} params - * @returns {Promise<TReturn|undefined>} + * @template {import('display').DirectApiNames} TName + * @param {TName} action + * @param {import('display').DirectApiParams<TName>} params + * @returns {Promise<import('display').DirectApiReturn<TName>|undefined>} */ async _invokeSafe(action, params) { try { diff --git a/ext/js/comm/cross-frame-api.js b/ext/js/comm/cross-frame-api.js index 14e410a8..3569c037 100644 --- a/ext/js/comm/cross-frame-api.js +++ b/ext/js/comm/cross-frame-api.js @@ -16,7 +16,8 @@ * along with this program. If not, see <https://www.gnu.org/licenses/>. */ -import {EventDispatcher, EventListenerCollection, invokeMessageHandler, log} from '../core.js'; +import {EventDispatcher, EventListenerCollection, log} from '../core.js'; +import {extendApiMap, invokeApiMapHandler} from '../core/api-map.js'; import {ExtensionError} from '../core/extension-error.js'; import {parseJson} from '../core/json.js'; import {yomitan} from '../yomitan.js'; @@ -29,9 +30,9 @@ export class CrossFrameAPIPort extends EventDispatcher { * @param {number} otherTabId * @param {number} otherFrameId * @param {chrome.runtime.Port} port - * @param {import('core').MessageHandlerMap} messageHandlers + * @param {import('cross-frame-api').ApiMap} apiMap */ - constructor(otherTabId, otherFrameId, port, messageHandlers) { + constructor(otherTabId, otherFrameId, port, apiMap) { super(); /** @type {number} */ this._otherTabId = otherTabId; @@ -39,8 +40,8 @@ export class CrossFrameAPIPort extends EventDispatcher { this._otherFrameId = otherFrameId; /** @type {?chrome.runtime.Port} */ this._port = port; - /** @type {import('core').MessageHandlerMap} */ - this._messageHandlers = messageHandlers; + /** @type {import('cross-frame-api').ApiMap} */ + this._apiMap = apiMap; /** @type {Map<number, import('cross-frame-api').Invocation>} */ this._activeInvocations = new Map(); /** @type {number} */ @@ -69,13 +70,12 @@ export class CrossFrameAPIPort extends EventDispatcher { } /** - * @template [TParams=import('core').SerializableObject] - * @template [TReturn=unknown] - * @param {string} action - * @param {TParams} params + * @template {import('cross-frame-api').ApiNames} TName + * @param {TName} action + * @param {import('cross-frame-api').ApiParams<TName>} params * @param {number} ackTimeout * @param {number} responseTimeout - * @returns {Promise<TReturn>} + * @returns {Promise<import('cross-frame-api').ApiReturn<TName>>} */ invoke(action, params, ackTimeout, responseTimeout) { return new Promise((resolve, reject) => { @@ -186,7 +186,7 @@ export class CrossFrameAPIPort extends EventDispatcher { /** * @param {number} id - * @param {import('core').Response<unknown>} data + * @param {import('core').Response<import('cross-frame-api').ApiReturnAny>} data */ _onResult(id, data) { const invocation = this._activeInvocations.get(id); @@ -217,15 +217,13 @@ export class CrossFrameAPIPort extends EventDispatcher { /** * @param {number} id - * @param {unknown} error + * @param {unknown} errorOrMessage */ - _onError(id, error) { + _onError(id, errorOrMessage) { const invocation = this._activeInvocations.get(id); if (typeof invocation === 'undefined') { return; } - if (!(error instanceof Error)) { - error = new Error(`${error} (${invocation.action})`); - } + const error = errorOrMessage instanceof Error ? errorOrMessage : new Error(`${errorOrMessage} (${invocation.action})`); this._activeInvocations.delete(id); if (invocation.timer !== null) { @@ -239,23 +237,18 @@ export class CrossFrameAPIPort extends EventDispatcher { /** * @param {number} id - * @param {import('cross-frame-api').InvocationData} details - * @returns {boolean} + * @param {import('cross-frame-api').ApiMessageAny} details */ _onInvoke(id, {action, params}) { - const messageHandler = this._messageHandlers.get(action); this._sendAck(id); - if (typeof messageHandler === 'undefined') { - this._sendError(id, new Error(`Unknown action: ${action}`)); - return false; - } - - /** - * @param {import('core').Response<unknown>} data - * @returns {void} - */ - const callback = (data) => this._sendResult(id, data); - return invokeMessageHandler(messageHandler, params, callback); + invokeApiMapHandler( + this._apiMap, + action, + params, + [], + (data) => this._sendResult(id, data), + () => this._sendError(id, new Error(`Unknown action: ${action}`)) + ); } /** @@ -279,7 +272,7 @@ export class CrossFrameAPIPort extends EventDispatcher { /** * @param {number} id - * @param {import('core').Response<unknown>} data + * @param {import('core').Response<import('cross-frame-api').ApiReturnAny>} data */ _sendResult(id, data) { this._sendResponse({type: 'result', id, data}); @@ -302,8 +295,8 @@ export class CrossFrameAPI { this._responseTimeout = 10000; // 10 seconds /** @type {Map<number, Map<number, CrossFrameAPIPort>>} */ this._commPorts = new Map(); - /** @type {import('core').MessageHandlerMap} */ - this._messageHandlers = new Map(); + /** @type {import('cross-frame-api').ApiMap} */ + this._apiMap = new Map(); /** @type {(port: CrossFrameAPIPort) => void} */ this._onDisconnectBind = this._onDisconnect.bind(this); /** @type {?number} */ @@ -319,25 +312,23 @@ export class CrossFrameAPI { } /** - * @template [TParams=import('core').SerializableObject] - * @template [TReturn=unknown] + * @template {import('cross-frame-api').ApiNames} TName * @param {number} targetFrameId - * @param {string} action - * @param {TParams} params - * @returns {Promise<TReturn>} + * @param {TName} action + * @param {import('cross-frame-api').ApiParams<TName>} params + * @returns {Promise<import('cross-frame-api').ApiReturn<TName>>} */ invoke(targetFrameId, action, params) { return this.invokeTab(null, targetFrameId, action, params); } /** - * @template [TParams=import('core').SerializableObject] - * @template [TReturn=unknown] + * @template {import('cross-frame-api').ApiNames} TName * @param {?number} targetTabId * @param {number} targetFrameId - * @param {string} action - * @param {TParams} params - * @returns {Promise<TReturn>} + * @param {TName} action + * @param {import('cross-frame-api').ApiParams<TName>} params + * @returns {Promise<import('cross-frame-api').ApiReturn<TName>>} */ async invokeTab(targetTabId, targetFrameId, action, params) { if (typeof targetTabId !== 'number') { @@ -351,24 +342,10 @@ export class CrossFrameAPI { } /** - * @param {import('core').MessageHandlerMapInit} messageHandlers - * @throws {Error} - */ - registerHandlers(messageHandlers) { - for (const [key, value] of messageHandlers) { - if (this._messageHandlers.has(key)) { - throw new Error(`Handler ${key} is already registered`); - } - this._messageHandlers.set(key, value); - } - } - - /** - * @param {string} key - * @returns {boolean} + * @param {import('cross-frame-api').ApiMapInit} handlers */ - unregisterHandler(key) { - return this._messageHandlers.delete(key); + registerHandlers(handlers) { + extendApiMap(this._apiMap, handlers); } // Private @@ -451,7 +428,7 @@ export class CrossFrameAPI { * @returns {CrossFrameAPIPort} */ _setupCommPort(otherTabId, otherFrameId, port) { - const commPort = new CrossFrameAPIPort(otherTabId, otherFrameId, port, this._messageHandlers); + const commPort = new CrossFrameAPIPort(otherTabId, otherFrameId, port, this._apiMap); let tabPorts = this._commPorts.get(otherTabId); if (typeof tabPorts === 'undefined') { tabPorts = new Map(); diff --git a/ext/js/comm/frame-ancestry-handler.js b/ext/js/comm/frame-ancestry-handler.js index 261ea943..e715cedc 100644 --- a/ext/js/comm/frame-ancestry-handler.js +++ b/ext/js/comm/frame-ancestry-handler.js @@ -37,12 +37,12 @@ export class FrameAncestryHandler { this._isPrepared = false; /** @type {string} */ this._requestMessageId = 'FrameAncestryHandler.requestFrameInfo'; - /** @type {string} */ - this._responseMessageIdBase = `${this._requestMessageId}.response.`; /** @type {?Promise<number[]>} */ this._getFrameAncestryInfoPromise = null; /** @type {Map<number, {window: Window, frameElement: ?(undefined|Element)}>} */ this._childFrameMap = new Map(); + /** @type {Map<string, import('frame-ancestry-handler').ResponseHandler>} */ + this._responseHandlers = new Map(); } /** @@ -59,6 +59,9 @@ export class FrameAncestryHandler { prepare() { if (this._isPrepared) { return; } window.addEventListener('message', this._onWindowMessage.bind(this), false); + yomitan.crossFrame.registerHandlers([ + ['frameAncestryHandlerRequestFrameInfoResponse', this._onFrameAncestryHandlerRequestFrameInfoResponse.bind(this)] + ]); this._isPrepared = true; } @@ -119,7 +122,6 @@ export class FrameAncestryHandler { const uniqueId = generateId(16); let nonce = generateId(16); - const responseMessageId = `${this._responseMessageIdBase}${uniqueId}`; /** @type {number[]} */ const results = []; /** @type {?import('core').Timeout} */ @@ -130,12 +132,9 @@ export class FrameAncestryHandler { clearTimeout(timer); timer = null; } - yomitan.crossFrame.unregisterHandler(responseMessageId); + this._removeResponseHandler(uniqueId); }; - /** - * @param {import('frame-ancestry-handler').RequestFrameInfoResponseParams} params - * @returns {?import('frame-ancestry-handler').RequestFrameInfoResponseReturn} - */ + /** @type {import('frame-ancestry-handler').ResponseHandler} */ const onMessage = (params) => { if (params.nonce !== nonce) { return null; } @@ -164,9 +163,7 @@ export class FrameAncestryHandler { }; // Start - yomitan.crossFrame.registerHandlers([ - [responseMessageId, onMessage] - ]); + this._addResponseHandler(uniqueId, onMessage); resetTimeout(); const frameId = this._frameId; this._requestFrameInfo(targetWindow, frameId, frameId, uniqueId, nonce); @@ -212,13 +209,9 @@ export class FrameAncestryHandler { const frameId = this._frameId; const {parent} = window; const more = (window !== parent); - /** @type {import('frame-ancestry-handler').RequestFrameInfoResponseParams} */ - const responseParams = {frameId, nonce, more}; - const responseMessageId = `${this._responseMessageIdBase}${uniqueId}`; try { - /** @type {?import('frame-ancestry-handler').RequestFrameInfoResponseReturn} */ - const response = await yomitan.crossFrame.invoke(originFrameId, responseMessageId, responseParams); + const response = await yomitan.crossFrame.invoke(originFrameId, 'frameAncestryHandlerRequestFrameInfoResponse', {uniqueId, frameId, nonce, more}); if (response === null) { return; } const nonce2 = response.nonce; if (typeof nonce2 !== 'string') { return; } @@ -317,4 +310,27 @@ export class FrameAncestryHandler { // Not found return null; } + + /** + * @param {string} id + * @param {import('frame-ancestry-handler').ResponseHandler} handler + * @throws {Error} + */ + _addResponseHandler(id, handler) { + if (this._responseHandlers.has(id)) { throw new Error('Identifier already used'); } + this._responseHandlers.set(id, handler); + } + + /** + * @param {string} id + */ + _removeResponseHandler(id) { + this._responseHandlers.delete(id); + } + + /** @type {import('cross-frame-api').ApiHandler<'frameAncestryHandlerRequestFrameInfoResponse'>} */ + _onFrameAncestryHandlerRequestFrameInfoResponse(params) { + const handler = this._responseHandlers.get(params.uniqueId); + return typeof handler !== 'undefined' ? handler(params) : null; + } } diff --git a/ext/js/comm/frame-offset-forwarder.js b/ext/js/comm/frame-offset-forwarder.js index afa6a5e6..570f3e88 100644 --- a/ext/js/comm/frame-offset-forwarder.js +++ b/ext/js/comm/frame-offset-forwarder.js @@ -36,7 +36,7 @@ export class FrameOffsetForwarder { prepare() { this._frameAncestryHandler.prepare(); yomitan.crossFrame.registerHandlers([ - ['FrameOffsetForwarder.getChildFrameRect', this._onMessageGetChildFrameRect.bind(this)] + ['frameOffsetForwarderGetChildFrameRect', this._onMessageGetChildFrameRect.bind(this)] ]); } @@ -55,7 +55,7 @@ export class FrameOffsetForwarder { /** @type {Promise<?import('frame-offset-forwarder').ChildFrameRect>[]} */ const promises = []; for (const frameId of ancestorFrameIds) { - promises.push(yomitan.crossFrame.invoke(frameId, 'FrameOffsetForwarder.getChildFrameRect', {frameId: childFrameId})); + promises.push(yomitan.crossFrame.invoke(frameId, 'frameOffsetForwarderGetChildFrameRect', {frameId: childFrameId})); childFrameId = frameId; } @@ -76,10 +76,7 @@ export class FrameOffsetForwarder { // Private - /** - * @param {{frameId: number}} event - * @returns {?import('frame-offset-forwarder').ChildFrameRect} - */ + /** @type {import('cross-frame-api').ApiHandler<'frameOffsetForwarderGetChildFrameRect'>} */ _onMessageGetChildFrameRect({frameId}) { const frameElement = this._frameAncestryHandler.getChildFrameElement(frameId); if (frameElement === null) { return null; } diff --git a/ext/js/core.js b/ext/js/core.js index c9c989ac..726b037c 100644 --- a/ext/js/core.js +++ b/ext/js/core.js @@ -321,38 +321,6 @@ export function promiseAnimationFrame(timeout) { } /** - * Invokes a standard message handler. This function is used to react and respond - * to communication messages within the extension. - * @template {import('core').SafeAny} TParams - * @param {import('core').MessageHandler} handler The message handler function. - * @param {TParams} params Information which was passed with the original message. - * @param {(response: import('core').Response) => void} callback A callback function which is invoked after the handler has completed. The value passed - * to the function is in the format: - * - `{result: unknown}` if the handler invoked successfully. - * - `{error: object}` if the handler thew an error. The error is serialized. - * @param {...*} extraArgs Additional arguments which are passed to the `handler` function. - * @returns {boolean} `true` if the function is invoked asynchronously, `false` otherwise. - */ -export function invokeMessageHandler(handler, params, callback, ...extraArgs) { - try { - const promiseOrResult = handler(params, ...extraArgs); - if (promiseOrResult instanceof Promise) { - /** @type {Promise<any>} */ (promiseOrResult).then( - (result) => { callback({result}); }, - (error) => { callback({error: ExtensionError.serialize(error)}); } - ); - return true; - } else { - callback({result: promiseOrResult}); - return false; - } - } catch (error) { - callback({error: ExtensionError.serialize(error)}); - return false; - } -} - -/** * The following typedef is required because the JSDoc `implements` tag doesn't work with `import()`. * https://github.com/microsoft/TypeScript/issues/49905 * @typedef {import('core').EventDispatcherOffGeneric} EventDispatcherOffGeneric diff --git a/ext/js/display/display-resizer.js b/ext/js/display/display-resizer.js index 794398b8..8245e0bb 100644 --- a/ext/js/display/display-resizer.js +++ b/ext/js/display/display-resizer.js @@ -174,7 +174,7 @@ export class DisplayResizer { if (parentPopupId === null) { return; } /** @type {import('popup').ValidSize} */ - const size = await this._display.invokeParentFrame('PopupFactory.getFrameSize', {id: parentPopupId}); + const size = await this._display.invokeParentFrame('popupFactoryGetFrameSize', {id: parentPopupId}); if (this._token !== token) { return; } const {width, height} = size; this._startSize = {width, height}; @@ -210,7 +210,7 @@ export class DisplayResizer { height += y - this._startOffset.y; width = Math.max(Math.max(0, handleSize.width), width); height = Math.max(Math.max(0, handleSize.height), height); - await this._display.invokeParentFrame('PopupFactory.setFrameSize', {id: parentPopupId, width, height}); + await this._display.invokeParentFrame('popupFactorySetFrameSize', {id: parentPopupId, width, height}); } /** diff --git a/ext/js/display/display.js b/ext/js/display/display.js index e3c92ee2..cae394f8 100644 --- a/ext/js/display/display.js +++ b/ext/js/display/display.js @@ -19,7 +19,7 @@ import {ThemeController} from '../app/theme-controller.js'; import {FrameEndpoint} from '../comm/frame-endpoint.js'; import {DynamicProperty, EventDispatcher, EventListenerCollection, clone, deepEqual, log, promiseTimeout} from '../core.js'; -import {invokeApiMapHandler} from '../core/api-map.js'; +import {extendApiMap, invokeApiMapHandler} from '../core/api-map.js'; import {ExtensionError} from '../core/extension-error.js'; import {PopupMenu} from '../dom/popup-menu.js'; import {querySelectorNotNull} from '../dom/query-selector.js'; @@ -91,7 +91,7 @@ export class Display extends EventDispatcher { }); /** @type {import('display').DirectApiMap} */ this._directApiMap = new Map(); - /** @type {import('display').WindowApiMap} */ + /** @type {import('api-map').ApiMap<import('display').WindowApiSurface>} */ // import('display').WindowApiMap this._windowApiMap = new Map(); /** @type {DisplayHistory} */ this._history = new DisplayHistory({clearable: true, useBrowserHistory: false}); @@ -328,7 +328,8 @@ export class Display extends EventDispatcher { this._progressIndicatorVisible.on('change', this._onProgressIndicatorVisibleChanged.bind(this)); yomitan.on('extensionUnloaded', this._onExtensionUnloaded.bind(this)); yomitan.crossFrame.registerHandlers([ - ['popupMessage', this._onDirectMessage.bind(this)] + ['displayPopupMessage1', this._onDisplayPopupMessage1.bind(this)], + ['displayPopupMessage2', this._onDisplayPopupMessage2.bind(this)] ]); window.addEventListener('message', this._onWindowMessage.bind(this), false); @@ -507,25 +508,21 @@ export class Display extends EventDispatcher { * @param {import('display').DirectApiMapInit} handlers */ registerDirectMessageHandlers(handlers) { - for (const [name, handlerInfo] of handlers) { - this._directApiMap.set(name, handlerInfo); - } + extendApiMap(this._directApiMap, handlers); } /** * @param {import('display').WindowApiMapInit} handlers */ registerWindowMessageHandlers(handlers) { - for (const [name, handlerInfo] of handlers) { - this._windowApiMap.set(name, handlerInfo); - } + extendApiMap(this._windowApiMap, handlers); } /** */ close() { switch (this._pageType) { case 'popup': - this.invokeContentOrigin('Frontend.closePopup'); + this.invokeContentOrigin('frontendClosePopup', void 0); break; case 'search': this._closeTab(); @@ -578,12 +575,12 @@ export class Display extends EventDispatcher { } /** - * @template [TReturn=unknown] - * @param {string} action - * @param {import('core').SerializableObject} [params] - * @returns {Promise<TReturn>} + * @template {import('cross-frame-api').ApiNames} TName + * @param {TName} action + * @param {import('cross-frame-api').ApiParams<TName>} params + * @returns {Promise<import('cross-frame-api').ApiReturn<TName>>} */ - async invokeContentOrigin(action, params = {}) { + async invokeContentOrigin(action, params) { if (this._contentOriginTabId === this._tabId && this._contentOriginFrameId === this._frameId) { throw new Error('Content origin is same page'); } @@ -594,12 +591,12 @@ export class Display extends EventDispatcher { } /** - * @template [TReturn=unknown] - * @param {string} action - * @param {import('core').SerializableObject} [params] - * @returns {Promise<TReturn>} + * @template {import('cross-frame-api').ApiNames} TName + * @param {TName} action + * @param {import('cross-frame-api').ApiParams<TName>} params + * @returns {Promise<import('cross-frame-api').ApiReturn<TName>>} */ - async invokeParentFrame(action, params = {}) { + async invokeParentFrame(action, params) { if (this._parentFrameId === null || this._parentFrameId === this._frameId) { throw new Error('Invalid parent frame'); } @@ -634,14 +631,17 @@ export class Display extends EventDispatcher { // Message handlers - /** - * @param {import('frame-client').Message<import('display').DirectApiMessageAny>} data - * @returns {Promise<import('display').DirectApiReturnAny>} - * @throws {Error} - */ - _onDirectMessage(data) { + /** @type {import('cross-frame-api').ApiHandler<'displayPopupMessage1'>} */ + async _onDisplayPopupMessage1(message) { + /** @type {import('display').DirectApiMessageAny} */ + const messageInner = this._authenticateMessageData(message); + return await this._onDisplayPopupMessage2(messageInner); + } + + /** @type {import('cross-frame-api').ApiHandler<'displayPopupMessage2'>} */ + _onDisplayPopupMessage2(message) { return new Promise((resolve, reject) => { - const {action, params} = this._authenticateMessageData(data); + const {action, params} = message; invokeApiMapHandler( this._directApiMap, action, @@ -663,9 +663,10 @@ export class Display extends EventDispatcher { } /** - * @param {MessageEvent<import('frame-client').Message<import('display').WindowApiMessageAny>>} details + * @param {MessageEvent<import('display').WindowApiFrameClientMessageAny>} details */ _onWindowMessage({data}) { + /** @type {import('display').WindowApiMessageAny} */ let data2; try { data2 = this._authenticateMessageData(data); @@ -676,7 +677,7 @@ export class Display extends EventDispatcher { try { const {action, params} = data2; const callback = () => {}; // NOP - invokeApiMapHandler(this._directApiMap, action, params, [], callback); + invokeApiMapHandler(this._windowApiMap, action, params, [], callback); } catch (e) { // NOP } @@ -729,18 +730,15 @@ export class Display extends EventDispatcher { /** * @template [T=unknown] - * @param {T|import('frame-client').Message<T>} data + * @param {import('frame-client').Message<unknown>} message * @returns {T} * @throws {Error} */ - _authenticateMessageData(data) { - if (this._frameEndpoint === null) { - return /** @type {T} */ (data); - } - if (!this._frameEndpoint.authenticate(data)) { + _authenticateMessageData(message) { + if (this._frameEndpoint !== null && !this._frameEndpoint.authenticate(message)) { throw new Error('Invalid authentication'); } - return /** @type {import('frame-client').Message<T>} */ (data).data; + return /** @type {import('frame-client').Message<T>} */ (message).data; } /** */ @@ -1767,7 +1765,7 @@ export class Display extends EventDispatcher { /** @type {string} */ let text; try { - text = await this.invokeContentOrigin('Frontend.getSelectionText'); + text = await this.invokeContentOrigin('frontendGetSelectionText', void 0); } catch (e) { break; } @@ -1775,7 +1773,7 @@ export class Display extends EventDispatcher { } break; default: - await this.invokeContentOrigin('Frontend.copySelection'); + await this.invokeContentOrigin('frontendCopySelection', void 0); break; } } diff --git a/ext/js/input/hotkey-handler.js b/ext/js/input/hotkey-handler.js index 48c2de57..5969af05 100644 --- a/ext/js/input/hotkey-handler.js +++ b/ext/js/input/hotkey-handler.js @@ -51,7 +51,7 @@ export class HotkeyHandler extends EventDispatcher { this._isPrepared = true; this._updateEventHandlers(); yomitan.crossFrame.registerHandlers([ - ['HotkeyHandler.forwardHotkey', this._onMessageForwardHotkey.bind(this)] + ['hotkeyHandlerForwardHotkey', this._onMessageForwardHotkey.bind(this)] ]); } @@ -159,10 +159,7 @@ export class HotkeyHandler extends EventDispatcher { // Message handlers - /** - * @param {{key: string, modifiers: import('input').ModifierKey[]}} details - * @returns {boolean} - */ + /** @type {import('cross-frame-api').ApiHandler<'hotkeyHandlerForwardHotkey'>} */ _onMessageForwardHotkey({key, modifiers}) { return this.simulate(key, modifiers); } |