Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

API type safety updates #457

Merged
merged 11 commits into from
Dec 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 22 additions & 31 deletions ext/js/app/frontend.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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)],
Expand Down Expand Up @@ -239,9 +240,7 @@ export class Frontend {

// Message handlers

/**
* @param {import('frontend').FrontendRequestReadyBroadcastParams} params
*/
/** @type {import('application').ApiHandler<'frontendRequestReadyBroadcast'>} */
_onMessageRequestFrontendReadyBroadcast({frameId}) {
this._signalFrontendReady(frameId);
}
Expand Down Expand Up @@ -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) {
Expand All @@ -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);
}
Expand All @@ -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);
}

/**
Expand Down Expand Up @@ -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);
}
}

Expand All @@ -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();
Expand All @@ -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}});
});
}

Expand Down
74 changes: 38 additions & 36 deletions ext/js/background/backend.js
Original file line number Diff line number Diff line change
Expand Up @@ -301,8 +301,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;
Expand Down Expand Up @@ -406,7 +406,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}}, {});
}

/**
Expand All @@ -429,7 +429,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;
Expand Down Expand Up @@ -611,30 +612,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;
}

Expand Down Expand Up @@ -1096,7 +1097,7 @@ export class Backend {

await this._sendMessageTabPromise(
id,
{action: 'SearchDisplayController.setMode', params: {mode: 'popup'}},
{action: 'searchDisplayControllerSetMode', params: {mode: 'popup'}},
{frameId: 0}
);

Expand All @@ -1116,7 +1117,7 @@ export class Backend {
try {
const mode = await this._sendMessageTabPromise(
id,
{action: 'SearchDisplayController.getMode', params: {}},
{action: 'searchDisplayControllerGetMode'},
{frameId: 0}
);
return mode === 'popup';
Expand Down Expand Up @@ -1196,7 +1197,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}
);
}
Expand Down Expand Up @@ -1227,7 +1228,7 @@ export class Backend {

this._accessibilityController.update(this._getOptionsFull(false));

this._sendMessageAllTabsIgnoreResponse('Yomitan.optionsUpdated', {source});
this._sendMessageAllTabsIgnoreResponse({action: 'applicationOptionsUpdated', params: {source}});
}

/**
Expand Down Expand Up @@ -1635,7 +1636,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;
Expand Down Expand Up @@ -1806,14 +1807,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;
}
Expand All @@ -1834,7 +1835,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; }
Expand Down Expand Up @@ -1893,7 +1894,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);
Expand All @@ -1902,7 +1904,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) {
Expand All @@ -1911,25 +1913,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) => {
Expand All @@ -1938,7 +1940,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);
}
Expand All @@ -1961,11 +1963,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;
}

/**
Expand Down Expand Up @@ -2000,7 +2002,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});
}
Expand All @@ -2017,7 +2019,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});
Expand Down Expand Up @@ -2382,7 +2384,7 @@ export class Backend {
*/
_triggerDatabaseUpdated(type, cause) {
this._translator.clearDatabaseCaches();
this._sendMessageAllTabsIgnoreResponse('Yomitan.databaseUpdated', {type, cause});
this._sendMessageAllTabsIgnoreResponse({action: 'applicationDatabaseUpdated', params: {type, cause}});
}

/**
Expand Down
14 changes: 6 additions & 8 deletions ext/js/comm/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -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});
}

/**
Expand Down
Loading