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 maps #413

Merged
merged 9 commits into from
Dec 21, 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
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>;