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 | |
| 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
| -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 | ||||
| -rw-r--r-- | types/ext/core.d.ts | 13 | ||||
| -rw-r--r-- | types/ext/cross-frame-api.d.ts | 215 | ||||
| -rw-r--r-- | types/ext/display.d.ts | 14 | ||||
| -rw-r--r-- | types/ext/frame-ancestry-handler.d.ts | 7 | ||||
| -rw-r--r-- | types/ext/frontend.d.ts | 4 | 
17 files changed, 436 insertions, 351 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);      } diff --git a/types/ext/core.d.ts b/types/ext/core.d.ts index b94b649b..48cb68d3 100644 --- a/types/ext/core.d.ts +++ b/types/ext/core.d.ts @@ -74,19 +74,6 @@ export type ResponseError = {  export type Response<T = unknown> = ResponseSuccess<T> | ResponseError; -export type MessageHandler = (params: SafeAny, ...extraArgs: SafeAny[]) => ( -    MessageHandlerResult | -    Promise<MessageHandlerResult> -); - -export type MessageHandlerResult = SafeAny; - -export type MessageHandlerMap = Map<string, MessageHandler>; - -export type MessageHandlerMapInit = MessageHandlerMapInitItem[]; - -export type MessageHandlerMapInitItem = [key: string, handlerDetails: MessageHandler]; -  export type Timeout = number | NodeJS.Timeout;  export type EventSurface = {[name: string]: unknown}; diff --git a/types/ext/cross-frame-api.d.ts b/types/ext/cross-frame-api.d.ts index 8ddca6e7..148bfe00 100644 --- a/types/ext/cross-frame-api.d.ts +++ b/types/ext/cross-frame-api.d.ts @@ -16,7 +16,28 @@   */  import type {CrossFrameAPIPort} from '../../ext/js/comm/cross-frame-api.js'; -import type * as Core from './core'; +import type {Response, Timeout, TokenString} from './core'; +import type {ModifierKey} from './input'; +import type {ContentDetails as PopupContentDetails, ValidSize} from './popup'; +import type {GetOrCreatePopupDetails} from './popup-factory'; +import type {OptionsContext} from './settings'; +import type { +    ApiMap as BaseApiMap, +    ApiParams as BaseApiParams, +    ApiNames as BaseApiNames, +    ApiMapInit as BaseApiMapInit, +    ApiHandler as BaseApiHandler, +    ApiReturn as BaseApiReturn, +    ApiReturnAny as BaseApiReturnAny, +} from './api-map'; +import type { +    DirectApiFrameClientMessageAny as DisplayDirectApiFrameClientMessageAny, +    DirectApiMessageAny as DisplayDirectApiMessageAny, +    DirectApiReturnAny as DisplayDirectApiReturnAny, +    ContentDetails as DisplayContentDetails, +} from './display'; +import type {ChildFrameRect} from 'frame-offset-forwarder'; +import type {RequestFrameInfoResponseParams, RequestFrameInfoResponseReturn} from './frame-ancestry-handler';  export type CrossFrameAPIPortEvents = {      disconnect: CrossFrameAPIPort; @@ -30,30 +51,25 @@ export type AcknowledgeMessage = {  export type ResultMessage = {      type: 'result';      id: number; -    data: Core.Response<unknown>; +    data: Response<ApiReturnAny>;  };  export type InvokeMessage = {      type: 'invoke';      id: number; -    data: InvocationData; -}; - -export type InvocationData = { -    action: string; -    params: Core.SerializableObject; +    data: ApiMessageAny;  };  export type Message = AcknowledgeMessage | ResultMessage | InvokeMessage;  export type Invocation = {      id: number; -    resolve: (value: Core.SafeAny) => void; -    reject: (reason?: unknown) => void; +    resolve: (value: ApiReturnAny) => void; +    reject: (reason: Error) => void;      responseTimeout: number;      action: string;      ack: boolean; -    timer: Core.Timeout | null; +    timer: Timeout | null;  };  export type PortDetails = CrossFrameCommunicationPortDetails; @@ -63,3 +79,180 @@ export type CrossFrameCommunicationPortDetails = {      otherTabId: number;      otherFrameId: number;  }; + +type ApiSurface = { +    displayPopupMessage1: { +        params: DisplayDirectApiFrameClientMessageAny; +        return: DisplayDirectApiReturnAny; +    }; +    displayPopupMessage2: { +        params: DisplayDirectApiMessageAny; +        return: DisplayDirectApiReturnAny; +    }; +    frontendClosePopup: { +        params: void; +        return: void; +    }; +    frontendCopySelection: { +        params: void; +        return: void; +    }; +    frontendGetSelectionText: { +        params: void; +        return: string; +    }; +    frontendGetPopupInfo: { +        params: void; +        return: { +            popupId: string | null; +        }; +    }; +    frontendGetPageInfo: { +        params: void; +        return: { +            url: string; +            documentTitle: string; +        }; +    }; +    frameOffsetForwarderGetChildFrameRect: { +        params: { +            frameId: number; +        }; +        return: ChildFrameRect | null; +    }; +    hotkeyHandlerForwardHotkey: { +        params: { +            key: string; +            modifiers: ModifierKey[]; +        }; +        return: boolean; +    }; +    popupFactoryGetOrCreatePopup: { +        params: GetOrCreatePopupDetails; +        return: {id: string, depth: number, frameId: number}; +    }; +    popupFactorySetOptionsContext: { +        params: { +            id: string; +            optionsContext: OptionsContext; +        }; +        return: void; +    }; +    popupFactoryHide: { +        params: { +            id: string; +            changeFocus: boolean; +        }; +        return: void; +    }; +    popupFactoryIsVisible: { +        params: { +            id: string; +        }; +        return: boolean; +    }; +    popupFactorySetVisibleOverride: { +        params: { +            id: string; +            value: boolean; +            priority: number; +        }; +        return: TokenString | null; +    }; +    popupFactoryClearVisibleOverride: { +        params: { +            id: string; +            token: TokenString; +        }; +        return: boolean; +    }; +    popupFactoryContainsPoint: { +        params: { +            id: string; +            x: number; +            y: number; +        }; +        return: boolean; +    }; +    popupFactoryShowContent: { +        params: { +            id: string; +            details: PopupContentDetails; +            displayDetails: DisplayContentDetails | null; +        }; +        return: void; +    }; +    popupFactorySetCustomCss: { +        params: { +            id: string; +            css: string; +        }; +        return: void; +    }; +    popupFactoryClearAutoPlayTimer: { +        params: { +            id: string; +        }; +        return: void; +    }; +    popupFactorySetContentScale: { +        params: { +            id: string; +            scale: number; +        }; +        return: void; +    }; +    popupFactoryUpdateTheme: { +        params: { +            id: string; +        }; +        return: void; +    }; +    popupFactorySetCustomOuterCss: { +        params: { +            id: string; +            css: string; +            useWebExtensionApi: boolean; +        }; +        return: void; +    }; +    popupFactoryGetFrameSize: { +        params: { +            id: string; +        }; +        return: ValidSize; +    }; +    popupFactorySetFrameSize: { +        params: { +            id: string; +            width: number; +            height: number; +        }; +        return: boolean; +    }; +    frameAncestryHandlerRequestFrameInfoResponse: { +        params: RequestFrameInfoResponseParams; +        return: RequestFrameInfoResponseReturn; +    }; +}; + +export type ApiNames = BaseApiNames<ApiSurface>; + +export type ApiMapInit = BaseApiMapInit<ApiSurface>; + +export type ApiMap = BaseApiMap<ApiSurface, []>; + +export type ApiHandler<TName extends ApiNames> = BaseApiHandler<ApiSurface[TName]>; + +export type ApiParams<TName extends ApiNames> = BaseApiParams<ApiSurface[TName]>; + +export type ApiReturn<TName extends ApiNames> = BaseApiReturn<ApiSurface[TName]>; + +export type ApiReturnAny = BaseApiReturnAny<ApiSurface>; + +export type ApiMessageAny = {[name in ApiNames]: ApiMessage<name>}[ApiNames]; + +type ApiMessage<TName extends ApiNames> = { +    action: TName; +    params: ApiParams<TName>; +}; diff --git a/types/ext/display.d.ts b/types/ext/display.d.ts index f7c45b02..127823d2 100644 --- a/types/ext/display.d.ts +++ b/types/ext/display.d.ts @@ -23,6 +23,7 @@ import type * as Extension from './extension';  import type * as Settings from './settings';  import type * as TextScannerTypes from './text-scanner';  import type {EventNames, EventArgument as BaseEventArgument} from './core'; +import type {Message as FrameClientMessage} from './frame-client';  import type {      ApiMap as BaseApiMap,      ApiParams as BaseApiParams, @@ -30,6 +31,7 @@ import type {      ApiMapInit as BaseApiMapInit,      ApiParamsAny as BaseApiParamsAny,      ApiHandler as BaseApiHandler, +    ApiReturn as BaseApiReturn,  } from './api-map';  export type HistoryMode = 'clear' | 'overwrite' | 'new'; @@ -222,7 +224,7 @@ export type DirectApiSurface = {      };  }; -type DirectApiNames = BaseApiNames<DirectApiSurface>; +export type DirectApiNames = BaseApiNames<DirectApiSurface>;  export type DirectApiMapInit = BaseApiMapInit<DirectApiSurface>; @@ -230,7 +232,9 @@ export type DirectApiMap = BaseApiMap<DirectApiSurface, []>;  export type DirectApiHandler<TName extends DirectApiNames> = BaseApiHandler<DirectApiSurface[TName]>; -type DirectApiParams<TName extends DirectApiNames> = BaseApiParams<DirectApiSurface[TName]>; +export type DirectApiParams<TName extends DirectApiNames> = BaseApiParams<DirectApiSurface[TName]>; + +export type DirectApiReturn<TName extends DirectApiNames> = BaseApiReturn<DirectApiSurface[TName]>;  export type DirectApiMessageAny = {[name in DirectApiNames]: DirectApiMessage<name>}[DirectApiNames]; @@ -241,10 +245,12 @@ type DirectApiMessage<TName extends DirectApiNames> = {      params: DirectApiParams<TName>;  }; +export type DirectApiFrameClientMessageAny = {[name in DirectApiNames]: FrameClientMessage<DirectApiMessage<name>>}[DirectApiNames]; +  // Window API  export type WindowApiSurface = { -    'displayExtensionUnloaded': { +    displayExtensionUnloaded: {          params: void;          return: void;      }; @@ -266,3 +272,5 @@ type WindowApiMessage<TName extends WindowApiNames> = {      action: TName;      params: WindowApiParams<TName>;  }; + +export type WindowApiFrameClientMessageAny = {[name in WindowApiNames]: FrameClientMessage<WindowApiMessage<name>>}[WindowApiNames];
\ No newline at end of file diff --git a/types/ext/frame-ancestry-handler.d.ts b/types/ext/frame-ancestry-handler.d.ts index 3c9e32bf..4eb7f861 100644 --- a/types/ext/frame-ancestry-handler.d.ts +++ b/types/ext/frame-ancestry-handler.d.ts @@ -16,11 +16,12 @@   */  export type RequestFrameInfoResponseParams = { +    uniqueId: string;      frameId: number;      nonce: string;      more: boolean;  }; -export type RequestFrameInfoResponseReturn = { -    nonce: string; -}; +export type RequestFrameInfoResponseReturn = {nonce: string} | null; + +export type ResponseHandler = (params: RequestFrameInfoResponseParams) => RequestFrameInfoResponseReturn; diff --git a/types/ext/frontend.d.ts b/types/ext/frontend.d.ts index 4cc8d03b..b06e6040 100644 --- a/types/ext/frontend.d.ts +++ b/types/ext/frontend.d.ts @@ -47,7 +47,3 @@ export type ConstructorDetails = {  };  export type PageType = 'web' | 'popup' | 'search'; - -export type GetPopupInfoResult = { -    popupId: string | null; -}; |