diff options
-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; -}; |