Skip to content

Commit

Permalink
API maps (#413)
Browse files Browse the repository at this point in the history
* Add API map type descriptions

* Remove unused ApiMapInitLax

* Add createApiMap function

* Add extendApiMap

* Support promises

* Update Offscreen to use API map

* Add ApiNames<> template

* Add getApiMapHandler

* Use getApiMapHandler in offscreen
  • Loading branch information
toasted-nutbread authored Dec 21, 2023
1 parent b83ca2f commit ab847b1
Show file tree
Hide file tree
Showing 5 changed files with 191 additions and 68 deletions.
2 changes: 1 addition & 1 deletion ext/js/background/offscreen-proxy.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ export class OffscreenProxy {
/**
* @template {import('offscreen').MessageType} TMessageType
* @param {import('offscreen').Message<TMessageType>} message
* @returns {Promise<import('offscreen').MessageReturn<TMessageType>>}
* @returns {Promise<import('offscreen').OffscreenApiReturn<TMessageType>>}
*/
sendMessagePromise(message) {
return new Promise((resolve, reject) => {
Expand Down
38 changes: 21 additions & 17 deletions ext/js/background/offscreen.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import * as wanakana from '../../lib/wanakana.js';
import {ClipboardReader} from '../comm/clipboard-reader.js';
import {invokeMessageHandler} from '../core.js';
import {createApiMap, getApiMapHandler} from '../core/api-map.js';
import {ArrayBufferUtil} from '../data/sandbox/array-buffer-util.js';
import {DictionaryDatabase} from '../language/dictionary-database.js';
import {JapaneseUtil} from '../language/sandbox/japanese-util.js';
Expand Down Expand Up @@ -50,9 +51,10 @@ export class Offscreen {
richContentPasteTargetSelector: '#clipboard-rich-content-paste-target'
});


/* eslint-disable no-multi-spaces */
/** @type {import('offscreen').MessageHandlerMap} */
this._messageHandlers = new Map(/** @type {import('offscreen').MessageHandlerMapInit} */ ([
/** @type {import('offscreen').OffscreenApiMapInit} */
const messageHandlersInit = [
['clipboardGetTextOffscreen', this._getTextHandler.bind(this)],
['clipboardGetImageOffscreen', this._getImageHandler.bind(this)],
['clipboardSetBrowserOffscreen', this._setClipboardBrowser.bind(this)],
Expand All @@ -65,8 +67,10 @@ export class Offscreen {
['findTermsOffscreen', this._findTermsHandler.bind(this)],
['getTermFrequenciesOffscreen', this._getTermFrequenciesHandler.bind(this)],
['clearDatabaseCachesOffscreen', this._clearDatabaseCachesHandler.bind(this)]
]));
/* eslint-enable no-multi-spaces */
];

/** @type {import('offscreen').OffscreenApiMap} */
this._messageHandlers = createApiMap(messageHandlersInit);

/** @type {?Promise<void>} */
this._prepareDatabasePromise = null;
Expand All @@ -77,22 +81,22 @@ export class Offscreen {
chrome.runtime.onMessage.addListener(this._onMessage.bind(this));
}

/** @type {import('offscreen').MessageHandler<'clipboardGetTextOffscreen', true>} */
/** @type {import('offscreen').OffscreenApiHandler<'clipboardGetTextOffscreen'>} */
async _getTextHandler({useRichText}) {
return await this._clipboardReader.getText(useRichText);
}

/** @type {import('offscreen').MessageHandler<'clipboardGetImageOffscreen', true>} */
/** @type {import('offscreen').OffscreenApiHandler<'clipboardGetImageOffscreen'>} */
async _getImageHandler() {
return await this._clipboardReader.getImage();
}

/** @type {import('offscreen').MessageHandler<'clipboardSetBrowserOffscreen', false>} */
/** @type {import('offscreen').OffscreenApiHandler<'clipboardSetBrowserOffscreen'>} */
_setClipboardBrowser({value}) {
this._clipboardReader.browser = value;
}

/** @type {import('offscreen').MessageHandler<'databasePrepareOffscreen', true>} */
/** @type {import('offscreen').OffscreenApiHandler<'databasePrepareOffscreen'>} */
_prepareDatabaseHandler() {
if (this._prepareDatabasePromise !== null) {
return this._prepareDatabasePromise;
Expand All @@ -101,29 +105,29 @@ export class Offscreen {
return this._prepareDatabasePromise;
}

/** @type {import('offscreen').MessageHandler<'getDictionaryInfoOffscreen', true>} */
/** @type {import('offscreen').OffscreenApiHandler<'getDictionaryInfoOffscreen'>} */
async _getDictionaryInfoHandler() {
return await this._dictionaryDatabase.getDictionaryInfo();
}

/** @type {import('offscreen').MessageHandler<'databasePurgeOffscreen', true>} */
/** @type {import('offscreen').OffscreenApiHandler<'databasePurgeOffscreen'>} */
async _purgeDatabaseHandler() {
return await this._dictionaryDatabase.purge();
}

/** @type {import('offscreen').MessageHandler<'databaseGetMediaOffscreen', true>} */
/** @type {import('offscreen').OffscreenApiHandler<'databaseGetMediaOffscreen'>} */
async _getMediaHandler({targets}) {
const media = await this._dictionaryDatabase.getMedia(targets);
const serializedMedia = media.map((m) => ({...m, content: ArrayBufferUtil.arrayBufferToBase64(m.content)}));
return serializedMedia;
}

/** @type {import('offscreen').MessageHandler<'translatorPrepareOffscreen', false>} */
/** @type {import('offscreen').OffscreenApiHandler<'translatorPrepareOffscreen'>} */
_prepareTranslatorHandler({deinflectionReasons}) {
this._translator.prepare(deinflectionReasons);
}

/** @type {import('offscreen').MessageHandler<'findKanjiOffscreen', true>} */
/** @type {import('offscreen').OffscreenApiHandler<'findKanjiOffscreen'>} */
async _findKanjiHandler({text, options}) {
/** @type {import('translation').FindKanjiOptions} */
const modifiedOptions = {
Expand All @@ -133,7 +137,7 @@ export class Offscreen {
return await this._translator.findKanji(text, modifiedOptions);
}

/** @type {import('offscreen').MessageHandler<'findTermsOffscreen', true>} */
/** @type {import('offscreen').OffscreenApiHandler<'findTermsOffscreen'>} */
_findTermsHandler({mode, text, options}) {
const enabledDictionaryMap = new Map(options.enabledDictionaryMap);
const excludeDictionaryDefinitions = (
Expand All @@ -160,19 +164,19 @@ export class Offscreen {
return this._translator.findTerms(mode, text, modifiedOptions);
}

/** @type {import('offscreen').MessageHandler<'getTermFrequenciesOffscreen', true>} */
/** @type {import('offscreen').OffscreenApiHandler<'getTermFrequenciesOffscreen'>} */
_getTermFrequenciesHandler({termReadingList, dictionaries}) {
return this._translator.getTermFrequencies(termReadingList, dictionaries);
}

/** @type {import('offscreen').MessageHandler<'clearDatabaseCachesOffscreen', false>} */
/** @type {import('offscreen').OffscreenApiHandler<'clearDatabaseCachesOffscreen'>} */
_clearDatabaseCachesHandler() {
this._translator.clearDatabaseCaches();
}

/** @type {import('extension').ChromeRuntimeOnMessageCallback} */
_onMessage({action, params}, sender, callback) {
const messageHandler = this._messageHandlers.get(/** @type {import('offscreen').MessageType} */ (action));
const messageHandler = getApiMapHandler(this._messageHandlers, action);
if (typeof messageHandler === 'undefined') { return false; }
return invokeMessageHandler(messageHandler, params, callback, sender);
}
Expand Down
48 changes: 48 additions & 0 deletions ext/js/core/api-map.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* 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/>.
*/

/**
* @template {import('api-map').ApiSurface} [TApiSurface=never]
* @param {import('api-map').ApiMapInit<TApiSurface>} init
* @returns {import('api-map').ApiMap<TApiSurface>}
*/
export function createApiMap(init) {
return new Map(init);
}

/**
* @template {import('api-map').ApiSurface} [TApiSurface=never]
* @param {import('api-map').ApiMap<TApiSurface>} map
* @param {import('api-map').ApiMapInit<TApiSurface>} init
* @throws {Error}
*/
export function extendApiMap(map, init) {
for (const [key, value] of init) {
if (map.has(key)) { throw new Error(`The handler for ${String(key)} has already been registered`); }
map.set(key, value);
}
}

/**
* @template {import('api-map').ApiSurface} [TApiSurface=never]
* @param {import('api-map').ApiMap<TApiSurface>} map
* @param {string} name
* @returns {import('api-map').ApiHandlerAny<TApiSurface>|undefined}
*/
export function getApiMapHandler(map, name) {
return map.get(/** @type {import('api-map').ApiNames<TApiSurface>} */ (name));
}
55 changes: 55 additions & 0 deletions types/ext/api-map.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* 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/>.
*/

type ApiSurface = {
[name: string]: ApiItem;
};

type ApiItem = {
params: void | {[name: string]: unknown};
return: unknown;
};

export type ApiHandler<TApiItem extends ApiItem> = (params: TApiItem['params']) => TApiItem['return'] | Promise<TApiItem['return']>;

type ApiHandlerSurface<TApiSurface extends ApiSurface> = {[name in ApiNames<TApiSurface>]: ApiHandler<TApiSurface[name]>};

export type ApiHandlerAny<TApiSurface extends ApiSurface> = ApiHandlerSurface<TApiSurface>[ApiNames<TApiSurface>];

export type ApiNames<TApiSurface extends ApiSurface> = keyof TApiSurface;

export type ApiParams<TApiSurface extends ApiSurface, TName extends ApiNames<TApiSurface>> = TApiSurface[TName]['params'];

export type ApiReturn<TApiSurface extends ApiSurface, TName extends ApiNames<TApiSurface>> = TApiSurface[TName]['return'];

export type ApiMap<TApiSurface extends ApiSurface> = Map<ApiNames<TApiSurface>, ApiHandlerAny<TApiSurface>>;

export type ApiMapInit<TApiSurface extends ApiSurface> = ApiMapInitItemAny<TApiSurface>[];

export type ApiMapInitLax<TApiSurface extends ApiSurface> = ApiMapInitLaxItem<TApiSurface>[];

export type ApiMapInitLaxItem<TApiSurface extends ApiSurface> = [
name: ApiNames<TApiSurface>,
handler: ApiHandlerAny<TApiSurface>,
];

type ApiMapInitItem<TApiSurface extends ApiSurface, TName extends ApiNames<TApiSurface>> = [
name: TName,
handler: ApiHandler<TApiSurface[TName]>,
];

type ApiMapInitItemAny<TApiSurface extends ApiSurface> = {[key in ApiNames<TApiSurface>]: ApiMapInitItem<TApiSurface, key>}[ApiNames<TApiSurface>];
116 changes: 66 additions & 50 deletions types/ext/offscreen.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,72 +15,91 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

import type * as Core from './core';
import type * as Deinflector from './deinflector';
import type * as Dictionary from './dictionary';
import type * as DictionaryDatabase from './dictionary-database';
import type * as DictionaryImporter from './dictionary-importer';
import type * as Environment from './environment';
import type * as Translation from './translation';
import type * as Translator from './translator';
import type {ApiMap, ApiMapInit, ApiHandler, ApiParams, ApiReturn} from './api-map';

export type Message<T extends MessageType> = (
MessageDetailsMap[T] extends undefined ?
{action: T} :
{action: T, params: MessageDetailsMap[T]}
);

export type MessageReturn<T extends MessageType> = MessageReturnMap[T];

type MessageDetailsMap = {
databasePrepareOffscreen: undefined;
getDictionaryInfoOffscreen: undefined;
databasePurgeOffscreen: undefined;
type OffscreenApiSurface = {
databasePrepareOffscreen: {
params: void;
return: void;
};
getDictionaryInfoOffscreen: {
params: void;
return: DictionaryImporter.Summary[];
};
databasePurgeOffscreen: {
params: void;
return: boolean;
};
databaseGetMediaOffscreen: {
targets: DictionaryDatabase.MediaRequest[];
params: {
targets: DictionaryDatabase.MediaRequest[];
};
return: DictionaryDatabase.Media<string>[];
};
translatorPrepareOffscreen: {
deinflectionReasons: Deinflector.ReasonsRaw;
params: {
deinflectionReasons: Deinflector.ReasonsRaw;
};
return: void;
};
findKanjiOffscreen: {
text: string;
options: FindKanjiOptionsOffscreen;
params: {
text: string;
options: FindKanjiOptionsOffscreen;
};
return: Dictionary.KanjiDictionaryEntry[];
};
findTermsOffscreen: {
mode: Translator.FindTermsMode;
text: string;
options: FindTermsOptionsOffscreen;
params: {
mode: Translator.FindTermsMode;
text: string;
options: FindTermsOptionsOffscreen;
};
return: Translator.FindTermsResult;
};
getTermFrequenciesOffscreen: {
termReadingList: Translator.TermReadingList;
dictionaries: string[];
params: {
termReadingList: Translator.TermReadingList;
dictionaries: string[];
};
return: Translator.TermFrequencySimple[];
};
clearDatabaseCachesOffscreen: {
params: void;
return: void;
};
clearDatabaseCachesOffscreen: undefined;
clipboardSetBrowserOffscreen: {
value: Environment.Browser | null;
params: {
value: Environment.Browser | null;
};
return: void;
};
clipboardGetTextOffscreen: {
useRichText: boolean;
params: {
useRichText: boolean;
};
return: string;
};
clipboardGetImageOffscreen: {
params: void;
return: string | null;
};
clipboardGetImageOffscreen: undefined;
};

type MessageReturnMap = {
databasePrepareOffscreen: void;
getDictionaryInfoOffscreen: DictionaryImporter.Summary[];
databasePurgeOffscreen: boolean;
databaseGetMediaOffscreen: DictionaryDatabase.Media<string>[];
translatorPrepareOffscreen: void;
findKanjiOffscreen: Dictionary.KanjiDictionaryEntry[];
findTermsOffscreen: Translator.FindTermsResult;
getTermFrequenciesOffscreen: Translator.TermFrequencySimple[];
clearDatabaseCachesOffscreen: void;
clipboardSetBrowserOffscreen: void;
clipboardGetTextOffscreen: string;
clipboardGetImageOffscreen: string | null;
};
export type Message<TName extends MessageType> = (
OffscreenApiParams<TName> extends void ?
{action: TName} :
{action: TName, params: OffscreenApiParams<TName>}
);

export type MessageType = keyof MessageDetailsMap;
export type MessageType = keyof OffscreenApiSurface;

export type FindKanjiOptionsOffscreen = Omit<Translation.FindKanjiOptions, 'enabledDictionaryMap'> & {
enabledDictionaryMap: [
Expand All @@ -103,15 +122,12 @@ export type FindTermsTextReplacementOffscreen = Omit<Translation.FindTermsTextRe
pattern: string;
};

export type MessageHandler<
TMessage extends MessageType,
TIsAsync extends boolean,
> = (
details: MessageDetailsMap[TMessage],
) => (TIsAsync extends true ? Promise<MessageReturn<TMessage>> : MessageReturn<TMessage>);
export type OffscreenApiMap = ApiMap<OffscreenApiSurface>;

export type OffscreenApiMapInit = ApiMapInit<OffscreenApiSurface>;

export type MessageHandlerMap = Map<MessageType, Core.MessageHandler>;
export type OffscreenApiHandler<TName extends keyof OffscreenApiSurface> = ApiHandler<OffscreenApiSurface[TName]>;

export type MessageHandlerMapInit = MessageHandlerMapInitItem[];
export type OffscreenApiParams<TName extends keyof OffscreenApiSurface> = ApiParams<OffscreenApiSurface, TName>;

export type MessageHandlerMapInitItem = [messageType: MessageType, handler: Core.MessageHandler];
export type OffscreenApiReturn<TName extends keyof OffscreenApiSurface> = ApiReturn<OffscreenApiSurface, TName>;

0 comments on commit ab847b1

Please sign in to comment.