diff options
| author | toasted-nutbread <toasted-nutbread@users.noreply.github.com> | 2023-12-28 00:48:33 -0500 | 
|---|---|---|
| committer | GitHub <noreply@github.com> | 2023-12-28 05:48:33 +0000 | 
| commit | 76805bc0fc65452ca830623aa810888f9c476a2b (patch) | |
| tree | d91e257fd335c75dfca1a37784eb12769fbb5a66 | |
| parent | fc2123a45b3ceacc2ec887d24e5e752dca59bb4f (diff) | |
API type safety updates (#457)
* Update message handlers in SearchDisplayController
* Update types
* Updates
* Updates
* Simplify
* Updates
* Updates
* Rename
* Improve types
* Improve types
* Resolve TODOs
| -rw-r--r-- | ext/js/app/frontend.js | 53 | ||||
| -rw-r--r-- | ext/js/background/backend.js | 74 | ||||
| -rw-r--r-- | ext/js/comm/api.js | 14 | ||||
| -rw-r--r-- | ext/js/comm/frame-client.js | 8 | ||||
| -rw-r--r-- | ext/js/comm/frame-endpoint.js | 4 | ||||
| -rw-r--r-- | ext/js/display/search-display-controller.js | 52 | ||||
| -rw-r--r-- | ext/js/yomitan.js | 56 | ||||
| -rw-r--r-- | types/ext/api.d.ts | 7 | ||||
| -rw-r--r-- | types/ext/application.d.ts | 148 | ||||
| -rw-r--r-- | types/ext/extension.d.ts | 11 | ||||
| -rw-r--r-- | types/ext/frontend.d.ts | 8 | 
11 files changed, 260 insertions, 175 deletions
| diff --git a/ext/js/app/frontend.js b/ext/js/app/frontend.js index b68b55f3..b093ec33 100644 --- a/ext/js/app/frontend.js +++ b/ext/js/app/frontend.js @@ -16,7 +16,8 @@   * along with this program.  If not, see <https://www.gnu.org/licenses/>.   */ -import {EventListenerCollection, invokeMessageHandler, log, promiseAnimationFrame} from '../core.js'; +import {EventListenerCollection, log, promiseAnimationFrame} from '../core.js'; +import {createApiMap, invokeApiMapHandler} from '../core/api-map.js';  import {DocumentUtil} from '../dom/document-util.js';  import {TextSourceElement} from '../dom/text-source-element.js';  import {TextSourceRange} from '../dom/text-source-range.js'; @@ -106,12 +107,12 @@ export class Frontend {          this._optionsContextOverride = null;          /* eslint-disable no-multi-spaces */ -        /** @type {import('core').MessageHandlerMap} */ -        this._runtimeMessageHandlers = new Map(/** @type {import('core').MessageHandlerMapInit} */ ([ -            ['Frontend.requestReadyBroadcast',   this._onMessageRequestFrontendReadyBroadcast.bind(this)], -            ['Frontend.setAllVisibleOverride',   this._onApiSetAllVisibleOverride.bind(this)], -            ['Frontend.clearAllVisibleOverride', this._onApiClearAllVisibleOverride.bind(this)] -        ])); +        /** @type {import('application').ApiMap} */ +        this._runtimeApiMap = createApiMap([ +            ['frontendRequestReadyBroadcast',   this._onMessageRequestFrontendReadyBroadcast.bind(this)], +            ['frontendSetAllVisibleOverride',   this._onApiSetAllVisibleOverride.bind(this)], +            ['frontendClearAllVisibleOverride', this._onApiClearAllVisibleOverride.bind(this)] +        ]);          this._hotkeyHandler.registerActions([              ['scanSelectedText', this._onActionScanSelectedText.bind(this)], @@ -239,9 +240,7 @@ export class Frontend {      // Message handlers -    /** -     * @param {import('frontend').FrontendRequestReadyBroadcastParams} params -     */ +    /** @type {import('application').ApiHandler<'frontendRequestReadyBroadcast'>} */      _onMessageRequestFrontendReadyBroadcast({frameId}) {          this._signalFrontendReady(frameId);      } @@ -313,10 +312,7 @@ export class Frontend {          };      } -    /** -     * @param {{value: boolean, priority: number, awaitFrame: boolean}} params -     * @returns {Promise<import('core').TokenString>} -     */ +    /** @type {import('application').ApiHandler<'frontendSetAllVisibleOverride'>} */      async _onApiSetAllVisibleOverride({value, priority, awaitFrame}) {          const result = await this._popupFactory.setAllVisibleOverride(value, priority);          if (awaitFrame) { @@ -325,10 +321,7 @@ export class Frontend {          return result;      } -    /** -     * @param {{token: import('core').TokenString}} params -     * @returns {Promise<boolean>} -     */ +    /** @type {import('application').ApiHandler<'frontendClearAllVisibleOverride'>} */      async _onApiClearAllVisibleOverride({token}) {          return await this._popupFactory.clearAllVisibleOverride(token);      } @@ -342,11 +335,9 @@ export class Frontend {          this._updatePopupPosition();      } -    /** @type {import('extension').ChromeRuntimeOnMessageCallback} */ -    _onRuntimeMessage({action, params}, sender, callback) { -        const messageHandler = this._runtimeMessageHandlers.get(action); -        if (typeof messageHandler === 'undefined') { return false; } -        return invokeMessageHandler(messageHandler, params, callback, sender); +    /** @type {import('extension').ChromeRuntimeOnMessageCallback<import('application').ApiMessageAny>} */ +    _onRuntimeMessage({action, params}, _sender, callback) { +        return invokeApiMapHandler(this._runtimeApiMap, action, params, [], callback);      }      /** @@ -827,12 +818,12 @@ export class Frontend {       * @param {?number} targetFrameId       */      _signalFrontendReady(targetFrameId) { -        /** @type {import('frontend').FrontendReadyDetails} */ -        const params = {frameId: this._frameId}; +        /** @type {import('application').ApiMessageNoFrameId<'frontendReady'>} */ +        const message = {action: 'frontendReady', params: {frameId: this._frameId}};          if (targetFrameId === null) { -            yomitan.api.broadcastTab('frontendReady', params); +            yomitan.api.broadcastTab(message);          } else { -            yomitan.api.sendMessageToFrame(targetFrameId, 'frontendReady', params); +            yomitan.api.sendMessageToFrame(targetFrameId, message);          }      } @@ -853,11 +844,11 @@ export class Frontend {                  }                  chrome.runtime.onMessage.removeListener(onMessage);              }; -            /** @type {import('extension').ChromeRuntimeOnMessageCallback} */ +            /** @type {import('extension').ChromeRuntimeOnMessageCallback<import('application').ApiMessageAny>} */              const onMessage = (message, _sender, sendResponse) => {                  try { -                    const {action, params} = message; -                    if (action === 'frontendReady' && /** @type {import('frontend').FrontendReadyDetails} */ (params).frameId === frameId) { +                    const {action} = message; +                    if (action === 'frontendReady' && message.params.frameId === frameId) {                          cleanup();                          resolve();                          sendResponse(); @@ -876,7 +867,7 @@ export class Frontend {              }              chrome.runtime.onMessage.addListener(onMessage); -            yomitan.api.broadcastTab('Frontend.requestReadyBroadcast', {frameId: this._frameId}); +            yomitan.api.broadcastTab({action: 'frontendRequestReadyBroadcast', params: {frameId: this._frameId}});          });      } diff --git a/ext/js/background/backend.js b/ext/js/background/backend.js index 0604fe8b..ae78a97b 100644 --- a/ext/js/background/backend.js +++ b/ext/js/background/backend.js @@ -299,8 +299,8 @@ export class Backend {              this._clipboardMonitor.on('change', this._onClipboardTextChange.bind(this)); -            this._sendMessageAllTabsIgnoreResponse('Yomitan.backendReady', {}); -            this._sendMessageIgnoreResponse({action: 'Yomitan.backendReady', params: {}}); +            this._sendMessageAllTabsIgnoreResponse({action: 'applicationBackendReady'}); +            this._sendMessageIgnoreResponse({action: 'applicationBackendReady'});          } catch (e) {              log.error(e);              throw e; @@ -404,7 +404,7 @@ export class Backend {       * @param {chrome.tabs.ZoomChangeInfo} event       */      _onZoomChange({tabId, oldZoomFactor, newZoomFactor}) { -        this._sendMessageTabIgnoreResponse(tabId, {action: 'Yomitan.zoomChanged', params: {oldZoomFactor, newZoomFactor}}, {}); +        this._sendMessageTabIgnoreResponse(tabId, {action: 'applicationZoomChanged', params: {oldZoomFactor, newZoomFactor}}, {});      }      /** @@ -427,7 +427,8 @@ export class Backend {      /** @type {import('api').ApiHandler<'requestBackendReadySignal'>} */      _onApiRequestBackendReadySignal(_params, sender) {          // tab ID isn't set in background (e.g. browser_action) -        const data = {action: 'Yomitan.backendReady', params: {}}; +        /** @type {import('application').ApiMessage<'applicationBackendReady'>} */ +        const data = {action: 'applicationBackendReady'};          if (typeof sender.tab === 'undefined') {              this._sendMessageIgnoreResponse(data);              return false; @@ -609,30 +610,30 @@ export class Backend {      }      /** @type {import('api').ApiHandler<'sendMessageToFrame'>} */ -    _onApiSendMessageToFrame({frameId: targetFrameId, action, params}, sender) { +    _onApiSendMessageToFrame({frameId: targetFrameId, message}, sender) {          if (!sender) { return false; }          const {tab} = sender;          if (!tab) { return false; }          const {id} = tab;          if (typeof id !== 'number') { return false; } -        const frameId = sender.frameId; -        /** @type {import('extension').ChromeRuntimeMessageWithFrameId} */ -        const message = {action, params, frameId}; -        this._sendMessageTabIgnoreResponse(id, message, {frameId: targetFrameId}); +        const {frameId} = sender; +        /** @type {import('application').ApiMessageAny} */ +        const message2 = {...message, frameId}; +        this._sendMessageTabIgnoreResponse(id, message2, {frameId: targetFrameId});          return true;      }      /** @type {import('api').ApiHandler<'broadcastTab'>} */ -    _onApiBroadcastTab({action, params}, sender) { +    _onApiBroadcastTab({message}, sender) {          if (!sender) { return false; }          const {tab} = sender;          if (!tab) { return false; }          const {id} = tab;          if (typeof id !== 'number') { return false; } -        const frameId = sender.frameId; -        /** @type {import('extension').ChromeRuntimeMessageWithFrameId} */ -        const message = {action, params, frameId}; -        this._sendMessageTabIgnoreResponse(id, message, {}); +        const {frameId} = sender; +        /** @type {import('application').ApiMessageAny} */ +        const message2 = {...message, frameId}; +        this._sendMessageTabIgnoreResponse(id, message2, {});          return true;      } @@ -1094,7 +1095,7 @@ export class Backend {          await this._sendMessageTabPromise(              id, -            {action: 'SearchDisplayController.setMode', params: {mode: 'popup'}}, +            {action: 'searchDisplayControllerSetMode', params: {mode: 'popup'}},              {frameId: 0}          ); @@ -1114,7 +1115,7 @@ export class Backend {              try {                  const mode = await this._sendMessageTabPromise(                      id, -                    {action: 'SearchDisplayController.getMode', params: {}}, +                    {action: 'searchDisplayControllerGetMode'},                      {frameId: 0}                  );                  return mode === 'popup'; @@ -1194,7 +1195,7 @@ export class Backend {      async _updateSearchQuery(tabId, text, animate) {          await this._sendMessageTabPromise(              tabId, -            {action: 'SearchDisplayController.updateSearchQuery', params: {text, animate}}, +            {action: 'searchDisplayControllerUpdateSearchQuery', params: {text, animate}},              {frameId: 0}          );      } @@ -1225,7 +1226,7 @@ export class Backend {          this._accessibilityController.update(this._getOptionsFull(false)); -        this._sendMessageAllTabsIgnoreResponse('Yomitan.optionsUpdated', {source}); +        this._sendMessageAllTabsIgnoreResponse({action: 'applicationOptionsUpdated', params: {source}});      }      /** @@ -1633,7 +1634,7 @@ export class Backend {          try {              const response = await this._sendMessageTabPromise(                  tabId, -                {action: 'Yomitan.getUrl', params: {}}, +                {action: 'applicationGetUrl'},                  {frameId: 0}              );              const url = typeof response === 'object' && response !== null ? /** @type {import('core').SerializableObject} */ (response).url : void 0; @@ -1804,14 +1805,14 @@ export class Backend {          return new Promise((resolve, reject) => {              /** @type {?import('core').Timeout} */              let timer = null; -            /** @type {?import('extension').ChromeRuntimeOnMessageCallback} */ +            /** @type {?import('extension').ChromeRuntimeOnMessageCallback<import('application').ApiMessageAny>} */              let onMessage = (message, sender) => {                  if (                      !sender.tab ||                      sender.tab.id !== tabId ||                      sender.frameId !== frameId ||                      !(typeof message === 'object' && message !== null) || -                    /** @type {import('core').SerializableObject} */ (message).action !== 'yomitanReady' +                    message.action !== 'applicationReady'                  ) {                      return;                  } @@ -1832,7 +1833,7 @@ export class Backend {              chrome.runtime.onMessage.addListener(onMessage); -            this._sendMessageTabPromise(tabId, {action: 'Yomitan.isReady'}, {frameId}) +            this._sendMessageTabPromise(tabId, {action: 'applicationIsReady'}, {frameId})                  .then(                      (value) => {                          if (!value) { return; } @@ -1891,7 +1892,8 @@ export class Backend {      }      /** -     * @param {{action: string, params: import('core').SerializableObject}} message +     * @template {import('application').ApiNames} TName +     * @param {import('application').ApiMessage<TName>} message       */      _sendMessageIgnoreResponse(message) {          const callback = () => this._checkLastError(chrome.runtime.lastError); @@ -1900,7 +1902,7 @@ export class Backend {      /**       * @param {number} tabId -     * @param {{action: string, params?: import('core').SerializableObject, frameId?: number}} message +     * @param {import('application').ApiMessageAny} message       * @param {chrome.tabs.MessageSendOptions} options       */      _sendMessageTabIgnoreResponse(tabId, message, options) { @@ -1909,25 +1911,25 @@ export class Backend {      }      /** -     * @param {string} action -     * @param {import('core').SerializableObject} params +     * @param {import('application').ApiMessageAny} message       */ -    _sendMessageAllTabsIgnoreResponse(action, params) { +    _sendMessageAllTabsIgnoreResponse(message) {          const callback = () => this._checkLastError(chrome.runtime.lastError);          chrome.tabs.query({}, (tabs) => {              for (const tab of tabs) {                  const {id} = tab;                  if (typeof id !== 'number') { continue; } -                chrome.tabs.sendMessage(id, {action, params}, callback); +                chrome.tabs.sendMessage(id, message, callback);              }          });      }      /** +     * @template {import('application').ApiNames} TName       * @param {number} tabId -     * @param {{action: string, params?: import('core').SerializableObject}} message +     * @param {import('application').ApiMessage<TName>} message       * @param {chrome.tabs.MessageSendOptions} options -     * @returns {Promise<unknown>} +     * @returns {Promise<import('application').ApiReturn<TName>>}       */      _sendMessageTabPromise(tabId, message, options) {          return new Promise((resolve, reject) => { @@ -1936,7 +1938,7 @@ export class Backend {               */              const callback = (response) => {                  try { -                    resolve(this._getMessageResponseResult(response)); +                    resolve(/** @type {import('application').ApiReturn<TName>} */ (this._getMessageResponseResult(response)));                  } catch (error) {                      reject(error);                  } @@ -1959,11 +1961,11 @@ export class Backend {          if (typeof response !== 'object' || response === null) {              throw new Error('Tab did not respond');          } -        const responseError = /** @type {import('core').SerializedError|undefined} */ (/** @type {import('core').SerializableObject} */ (response).error); +        const responseError = /** @type {import('core').Response<unknown>} */ (response).error;          if (typeof responseError === 'object' && responseError !== null) {              throw ExtensionError.deserialize(responseError);          } -        return /** @type {import('core').SerializableObject} */ (response).result; +        return /** @type {import('core').Response<unknown>} */ (response).result;      }      /** @@ -1998,7 +2000,7 @@ export class Backend {          let token = null;          try {              if (typeof tabId === 'number' && typeof frameId === 'number') { -                const action = 'Frontend.setAllVisibleOverride'; +                const action = 'frontendSetAllVisibleOverride';                  const params = {value: false, priority: 0, awaitFrame: true};                  token = await this._sendMessageTabPromise(tabId, {action, params}, {frameId});              } @@ -2015,7 +2017,7 @@ export class Backend {              });          } finally {              if (token !== null) { -                const action = 'Frontend.clearAllVisibleOverride'; +                const action = 'frontendClearAllVisibleOverride';                  const params = {token};                  try {                      await this._sendMessageTabPromise(tabId, {action, params}, {frameId}); @@ -2380,7 +2382,7 @@ export class Backend {       */      _triggerDatabaseUpdated(type, cause) {          this._translator.clearDatabaseCaches(); -        this._sendMessageAllTabsIgnoreResponse('Yomitan.databaseUpdated', {type, cause}); +        this._sendMessageAllTabsIgnoreResponse({action: 'applicationDatabaseUpdated', params: {type, cause}});      }      /** diff --git a/ext/js/comm/api.js b/ext/js/comm/api.js index c2351538..423115f1 100644 --- a/ext/js/comm/api.js +++ b/ext/js/comm/api.js @@ -156,21 +156,19 @@ export class API {      /**       * @param {import('api').ApiParam<'sendMessageToFrame', 'frameId'>} frameId -     * @param {import('api').ApiParam<'sendMessageToFrame', 'action'>} action -     * @param {import('api').ApiParam<'sendMessageToFrame', 'params'>} [params] +     * @param {import('api').ApiParam<'sendMessageToFrame', 'message'>} message       * @returns {Promise<import('api').ApiReturn<'sendMessageToFrame'>>}       */ -    sendMessageToFrame(frameId, action, params) { -        return this._invoke('sendMessageToFrame', {frameId, action, params}); +    sendMessageToFrame(frameId, message) { +        return this._invoke('sendMessageToFrame', {frameId, message});      }      /** -     * @param {import('api').ApiParam<'broadcastTab', 'action'>} action -     * @param {import('api').ApiParam<'broadcastTab', 'params'>} params +     * @param {import('api').ApiParam<'broadcastTab', 'message'>} message       * @returns {Promise<import('api').ApiReturn<'broadcastTab'>>}       */ -    broadcastTab(action, params) { -        return this._invoke('broadcastTab', {action, params}); +    broadcastTab(message) { +        return this._invoke('broadcastTab', {message});      }      /** diff --git a/ext/js/comm/frame-client.js b/ext/js/comm/frame-client.js index 5e997622..cb591ca9 100644 --- a/ext/js/comm/frame-client.js +++ b/ext/js/comm/frame-client.js @@ -110,14 +110,14 @@ export class FrameClient {                  contentWindow.postMessage({action, params}, targetOrigin);              }; -            /** @type {import('extension').ChromeRuntimeOnMessageCallback<import('extension').ChromeRuntimeMessageWithFrameId>} */ +            /** @type {import('extension').ChromeRuntimeOnMessageCallback<import('application').ApiMessageAny>} */              const onMessage = (message) => {                  onMessageInner(message);                  return false;              };              /** -             * @param {import('extension').ChromeRuntimeMessageWithFrameId} message +             * @param {import('application').ApiMessageAny} message               */              const onMessageInner = async (message) => {                  try { @@ -130,7 +130,7 @@ export class FrameClient {                      switch (action) {                          case 'frameEndpointReady':                              { -                                const {secret} = /** @type {import('frame-client').FrameEndpointReadyDetails} */ (params); +                                const {secret} = params;                                  const token = generateId(16);                                  tokenMap.set(secret, token);                                  postMessage('frameEndpointConnect', {secret, token, hostFrameId}); @@ -138,7 +138,7 @@ export class FrameClient {                              break;                          case 'frameEndpointConnected':                              { -                                const {secret, token} = /** @type {import('frame-client').FrameEndpointConnectedDetails} */ (params); +                                const {secret, token} = params;                                  const frameId = message.frameId;                                  const token2 = tokenMap.get(secret);                                  if (typeof token2 !== 'undefined' && token === token2 && typeof frameId === 'number') { diff --git a/ext/js/comm/frame-endpoint.js b/ext/js/comm/frame-endpoint.js index c338e143..4c5f58c1 100644 --- a/ext/js/comm/frame-endpoint.js +++ b/ext/js/comm/frame-endpoint.js @@ -41,7 +41,7 @@ export class FrameEndpoint {          }          /** @type {import('frame-client').FrameEndpointReadyDetails} */          const details = {secret: this._secret}; -        yomitan.api.broadcastTab('frameEndpointReady', details); +        yomitan.api.broadcastTab({action: 'frameEndpointReady', params: details});      }      /** @@ -83,6 +83,6 @@ export class FrameEndpoint {          this._eventListeners.removeAllEventListeners();          /** @type {import('frame-client').FrameEndpointConnectedDetails} */          const details = {secret, token}; -        yomitan.api.sendMessageToFrame(hostFrameId, 'frameEndpointConnected', details); +        yomitan.api.sendMessageToFrame(hostFrameId, {action: 'frameEndpointConnected', params: details});      }  } diff --git a/ext/js/display/search-display-controller.js b/ext/js/display/search-display-controller.js index 482afd56..6767d201 100644 --- a/ext/js/display/search-display-controller.js +++ b/ext/js/display/search-display-controller.js @@ -18,7 +18,8 @@  import * as wanakana from '../../lib/wanakana.js';  import {ClipboardMonitor} from '../comm/clipboard-monitor.js'; -import {EventListenerCollection, invokeMessageHandler} from '../core.js'; +import {EventListenerCollection} from '../core.js'; +import {createApiMap, invokeApiMapHandler} from '../core/api-map.js';  import {querySelectorNotNull} from '../dom/query-selector.js';  import {yomitan} from '../yomitan.js'; @@ -75,8 +76,12 @@ export class SearchDisplayController {                  getText: yomitan.api.clipboardGet.bind(yomitan.api)              }          }); -        /** @type {import('core').MessageHandlerMap} */ -        this._messageHandlers = new Map(); +        /** @type {import('application').ApiMap} */ +        this._apiMap = createApiMap([ +            ['searchDisplayControllerGetMode', this._onMessageGetMode.bind(this)], +            ['searchDisplayControllerSetMode', this._onMessageSetMode.bind(this)], +            ['searchDisplayControllerUpdateSearchQuery', this._onExternalSearchUpdate.bind(this)] +        ]);      }      /** */ @@ -94,13 +99,6 @@ export class SearchDisplayController {          this._display.hotkeyHandler.registerActions([              ['focusSearchBox', this._onActionFocusSearchBox.bind(this)]          ]); -        /* eslint-disable no-multi-spaces */ -        this._registerMessageHandlers([ -            ['SearchDisplayController.getMode',           this._onMessageGetMode.bind(this)], -            ['SearchDisplayController.setMode',           this._onMessageSetMode.bind(this)], -            ['SearchDisplayController.updateSearchQuery', this._onExternalSearchUpdate.bind(this)] -        ]); -        /* eslint-enable no-multi-spaces */          this._updateClipboardMonitorEnabled(); @@ -140,32 +138,21 @@ export class SearchDisplayController {      // Messages -    /** -     * @param {{mode: import('display').SearchMode}} details -     */ +    /** @type {import('application').ApiHandler<'searchDisplayControllerSetMode'>} */      _onMessageSetMode({mode}) {          this.setMode(mode);      } -    /** -     * @returns {import('display').SearchMode} -     */ +    /** @type {import('application').ApiHandler<'searchDisplayControllerGetMode'>} */      _onMessageGetMode() {          return this._searchPersistentStateController.mode;      }      // Private -    /** -     * @param {{action: string, params?: import('core').SerializableObject}} message -     * @param {chrome.runtime.MessageSender} sender -     * @param {(response?: unknown) => void} callback -     * @returns {boolean} -     */ -    _onMessage({action, params}, sender, callback) { -        const messageHandler = this._messageHandlers.get(action); -        if (typeof messageHandler === 'undefined') { return false; } -        return invokeMessageHandler(messageHandler, params, callback, sender); +    /** @type {import('extension').ChromeRuntimeOnMessageCallback<import('application').ApiMessageAny>} */ +    _onMessage({action, params}, _sender, callback) { +        return invokeApiMapHandler(this._apiMap, action, params, [], callback);      }      /** @@ -284,9 +271,7 @@ export class SearchDisplayController {          this._clipboardMonitor.setPreviousText(selection !== null ? selection.toString().trim() : '');      } -    /** -     * @param {{text: string, animate?: boolean}} details -     */ +    /** @type {import('application').ApiHandler<'searchDisplayControllerUpdateSearchQuery'>} */      _onExternalSearchUpdate({text, animate = true}) {          const options = this._display.getOptions();          if (options === null) { return; } @@ -549,15 +534,6 @@ export class SearchDisplayController {      }      /** -     * @param {import('core').MessageHandlerMapInit} handlers -     */ -    _registerMessageHandlers(handlers) { -        for (const [name, handlerInfo] of handlers) { -            this._messageHandlers.set(name, handlerInfo); -        } -    } - -    /**       * @param {?Element} element       * @returns {boolean}       */ diff --git a/ext/js/yomitan.js b/ext/js/yomitan.js index 7505c0ca..621e9cf0 100644 --- a/ext/js/yomitan.js +++ b/ext/js/yomitan.js @@ -18,7 +18,8 @@  import {API} from './comm/api.js';  import {CrossFrameAPI} from './comm/cross-frame-api.js'; -import {EventDispatcher, deferPromise, invokeMessageHandler, log} from './core.js'; +import {EventDispatcher, deferPromise, log} from './core.js'; +import {createApiMap, invokeApiMapHandler} from './core/api-map.js';  import {ExtensionError} from './core/extension-error.js';  /** @@ -95,15 +96,15 @@ export class Yomitan extends EventDispatcher {          this._isBackendReadyPromiseResolve = resolve;          /* eslint-disable no-multi-spaces */ -        /** @type {import('core').MessageHandlerMap} */ -        this._messageHandlers = new Map(/** @type {import('core').MessageHandlerMapInit} */ ([ -            ['Yomitan.isReady',         this._onMessageIsReady.bind(this)], -            ['Yomitan.backendReady',    this._onMessageBackendReady.bind(this)], -            ['Yomitan.getUrl',          this._onMessageGetUrl.bind(this)], -            ['Yomitan.optionsUpdated',  this._onMessageOptionsUpdated.bind(this)], -            ['Yomitan.databaseUpdated', this._onMessageDatabaseUpdated.bind(this)], -            ['Yomitan.zoomChanged',     this._onMessageZoomChanged.bind(this)] -        ])); +        /** @type {import('application').ApiMap} */ +        this._apiMap = createApiMap([ +            ['applicationIsReady',         this._onMessageIsReady.bind(this)], +            ['applicationBackendReady',    this._onMessageBackendReady.bind(this)], +            ['applicationGetUrl',          this._onMessageGetUrl.bind(this)], +            ['applicationOptionsUpdated',  this._onMessageOptionsUpdated.bind(this)], +            ['applicationDatabaseUpdated', this._onMessageDatabaseUpdated.bind(this)], +            ['applicationZoomChanged',     this._onMessageZoomChanged.bind(this)] +        ]);          /* eslint-enable no-multi-spaces */      } @@ -171,7 +172,7 @@ export class Yomitan extends EventDispatcher {       */      ready() {          this._isReady = true; -        this.sendMessage({action: 'yomitanReady'}); +        this.sendMessage({action: 'applicationReady'});      }      /** @@ -183,6 +184,7 @@ export class Yomitan extends EventDispatcher {          return this._extensionUrlBase !== null && url.startsWith(this._extensionUrlBase);      } +    // TODO : this function needs type safety      /**       * Runs `chrome.runtime.sendMessage()` with additional exception handling events.       * @param {import('extension').ChromeRuntimeSendMessageArgs} args The arguments to be passed to `chrome.runtime.sendMessage()`. @@ -221,55 +223,41 @@ export class Yomitan extends EventDispatcher {          return location.href;      } -    /** @type {import('extension').ChromeRuntimeOnMessageCallback} */ -    _onMessage({action, params}, sender, callback) { -        const messageHandler = this._messageHandlers.get(action); -        if (typeof messageHandler === 'undefined') { return false; } -        return invokeMessageHandler(messageHandler, params, callback, sender); +    /** @type {import('extension').ChromeRuntimeOnMessageCallback<import('application').ApiMessageAny>} */ +    _onMessage({action, params}, _sender, callback) { +        return invokeApiMapHandler(this._apiMap, action, params, [], callback);      } -    /** -     * @returns {boolean} -     */ +    /** @type {import('application').ApiHandler<'applicationIsReady'>} */      _onMessageIsReady() {          return this._isReady;      } -    /** -     * @returns {void} -     */ +    /** @type {import('application').ApiHandler<'applicationBackendReady'>} */      _onMessageBackendReady() {          if (this._isBackendReadyPromiseResolve === null) { return; }          this._isBackendReadyPromiseResolve();          this._isBackendReadyPromiseResolve = null;      } -    /** -     * @returns {{url: string}} -     */ +    /** @type {import('application').ApiHandler<'applicationGetUrl'>} */      _onMessageGetUrl() {          return {url: this._getUrl()};      } -    /** -     * @param {{source: string}} params -     */ +    /** @type {import('application').ApiHandler<'applicationOptionsUpdated'>} */      _onMessageOptionsUpdated({source}) {          if (source !== 'background') {              this.trigger('optionsUpdated', {source});          }      } -    /** -     * @param {{type: string, cause: string}} params -     */ +    /** @type {import('application').ApiHandler<'applicationDatabaseUpdated'>} */      _onMessageDatabaseUpdated({type, cause}) {          this.trigger('databaseUpdated', {type, cause});      } -    /** -     * @param {{oldZoomFactor: number, newZoomFactor: number}} params -     */ +    /** @type {import('application').ApiHandler<'applicationZoomChanged'>} */      _onMessageZoomChanged({oldZoomFactor, newZoomFactor}) {          this.trigger('zoomChanged', {oldZoomFactor, newZoomFactor});      } diff --git a/types/ext/api.d.ts b/types/ext/api.d.ts index ad3aa22c..46dfbdc2 100644 --- a/types/ext/api.d.ts +++ b/types/ext/api.d.ts @@ -31,6 +31,7 @@ import type * as Settings from './settings';  import type * as SettingsModifications from './settings-modifications';  import type * as Translation from './translation';  import type * as Translator from './translator'; +import type {ApiMessageNoFrameIdAny as ApplicationApiMessageNoFrameIdAny} from './application';  import type {      ApiMap as BaseApiMap,      ApiMapInit as BaseApiMapInit, @@ -220,15 +221,13 @@ type ApiSurface = {      sendMessageToFrame: {          params: {              frameId: number; -            action: string; -            params?: Core.SerializableObject; +            message: ApplicationApiMessageNoFrameIdAny;          };          return: boolean;      };      broadcastTab: {          params: { -            action: string; -            params?: Core.SerializableObject; +            message: ApplicationApiMessageNoFrameIdAny;          };          return: boolean;      }; diff --git a/types/ext/application.d.ts b/types/ext/application.d.ts new file mode 100644 index 00000000..ac594abc --- /dev/null +++ b/types/ext/application.d.ts @@ -0,0 +1,148 @@ +/* + * Copyright (C) 2023  Yomitan Authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program.  If not, see <https://www.gnu.org/licenses/>. + */ + +import type {TokenString} from './core'; +import type {SearchMode} from './display'; +import type {FrameEndpointReadyDetails, FrameEndpointConnectedDetails} from './frame-client'; +import type {DatabaseUpdateType, DatabaseUpdateCause} from './backend'; +import type { +    ApiMap as BaseApiMap, +    ApiHandler as BaseApiHandler, +    ApiParams as BaseApiParams, +    ApiNames as BaseApiNames, +    ApiReturn as BaseApiReturn, +} from './api-map'; + +export type ApiSurface = { +    searchDisplayControllerGetMode: { +        params: void; +        return: SearchMode; +    }; +    searchDisplayControllerSetMode: { +        params: { +            mode: SearchMode; +        }; +        return: void; +    }; +    searchDisplayControllerUpdateSearchQuery: { +        params: { +            text: string; +            animate?: boolean; +        }; +        return: void; +    }; +    applicationReady: { +        params: void; +        return: void; +    }; +    applicationIsReady: { +        params: void; +        return: boolean; +    }; +    applicationBackendReady: { +        params: void; +        return: void; +    }; +    applicationGetUrl: { +        params: void; +        return: { +            url: string; +        }; +    }; +    applicationOptionsUpdated: { +        params: { +            source: string; +        }; +        return: void; +    }; +    applicationDatabaseUpdated: { +        params: { +            type: DatabaseUpdateType; +            cause: DatabaseUpdateCause; +        }; +        return: void; +    }; +    applicationZoomChanged: { +        params: { +            oldZoomFactor: number; +            newZoomFactor: number; +        }; +        return: void; +    }; +    frontendRequestReadyBroadcast: { +        params: { +            frameId: number; +        }; +        return: void; +    }; +    frontendSetAllVisibleOverride: { +        params: { +            value: boolean; +            priority: number; +            awaitFrame: boolean; +        }; +        return: TokenString; +    }; +    frontendClearAllVisibleOverride: { +        params: { +            token: TokenString; +        }; +        return: boolean; +    }; +    frontendReady: { +        params: { +            frameId: number; +        }; +        return: void; +    }; +    frameEndpointReady: { +        params: FrameEndpointReadyDetails; +        return: void; +    }; +    frameEndpointConnected: { +        params: FrameEndpointConnectedDetails; +        return: void; +    }; +}; + +export type ApiParams<TName extends ApiNames> = BaseApiParams<ApiSurface[TName]>; + +export type ApiNames = BaseApiNames<ApiSurface>; + +export type ApiMessageNoFrameId<TName extends ApiNames> = ( +    ApiParams<TName> extends void ? +        {action: TName, params?: never} : +        {action: TName, params: ApiParams<TName>} +); + +export type ApiMessage<TName extends ApiNames> = ApiMessageNoFrameId<TName> & { +    /** +     * The origin frameId that sent this message. +     * If sent from the backend, this value will be undefined. +     */ +    frameId?: number; +}; + +export type ApiMessageNoFrameIdAny = {[name in ApiNames]: ApiMessageNoFrameId<name>}[ApiNames]; + +export type ApiMessageAny = {[name in ApiNames]: ApiMessage<name>}[ApiNames]; + +export type ApiMap = BaseApiMap<ApiSurface>; + +export type ApiHandler<TName extends ApiNames> = BaseApiHandler<ApiSurface[TName]>; + +export type ApiReturn<TName extends ApiNames> = BaseApiReturn<ApiSurface[TName]>; diff --git a/types/ext/extension.d.ts b/types/ext/extension.d.ts index 1c86a4ca..5a244566 100644 --- a/types/ext/extension.d.ts +++ b/types/ext/extension.d.ts @@ -56,16 +56,7 @@ export type ContentOrigin = {      frameId?: number;  }; -export type ChromeRuntimeMessage = { -    action: string; -    params?: Core.SerializableObject; -}; - -export type ChromeRuntimeMessageWithFrameId = ChromeRuntimeMessage & { -    frameId?: number; -}; - -export type ChromeRuntimeOnMessageCallback<TMessage = ChromeRuntimeMessage> = ( +export type ChromeRuntimeOnMessageCallback<TMessage = unknown> = (      message: TMessage,      sender: chrome.runtime.MessageSender,      sendResponse: ChromeRuntimeMessageSendResponseFunction, diff --git a/types/ext/frontend.d.ts b/types/ext/frontend.d.ts index 73b24dc3..4cc8d03b 100644 --- a/types/ext/frontend.d.ts +++ b/types/ext/frontend.d.ts @@ -48,14 +48,6 @@ export type ConstructorDetails = {  export type PageType = 'web' | 'popup' | 'search'; -export type FrontendRequestReadyBroadcastParams = { -    frameId: number; -}; -  export type GetPopupInfoResult = {      popupId: string | null;  }; - -export type FrontendReadyDetails = { -    frameId: number; -}; |