summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authortoasted-nutbread <toasted-nutbread@users.noreply.github.com>2023-12-28 00:48:33 -0500
committerGitHub <noreply@github.com>2023-12-28 05:48:33 +0000
commit76805bc0fc65452ca830623aa810888f9c476a2b (patch)
treed91e257fd335c75dfca1a37784eb12769fbb5a66
parentfc2123a45b3ceacc2ec887d24e5e752dca59bb4f (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.js53
-rw-r--r--ext/js/background/backend.js74
-rw-r--r--ext/js/comm/api.js14
-rw-r--r--ext/js/comm/frame-client.js8
-rw-r--r--ext/js/comm/frame-endpoint.js4
-rw-r--r--ext/js/display/search-display-controller.js52
-rw-r--r--ext/js/yomitan.js56
-rw-r--r--types/ext/api.d.ts7
-rw-r--r--types/ext/application.d.ts148
-rw-r--r--types/ext/extension.d.ts11
-rw-r--r--types/ext/frontend.d.ts8
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;
-};