Skip to content

Commit

Permalink
Type safety for TemplateRendererProxy (#428)
Browse files Browse the repository at this point in the history
* Type safety for TemplateRendererProxy

* Simplify
  • Loading branch information
toasted-nutbread authored Dec 23, 2023
1 parent cb4499f commit a72b53d
Show file tree
Hide file tree
Showing 4 changed files with 138 additions and 75 deletions.
51 changes: 15 additions & 36 deletions ext/js/templates/sandbox/template-renderer-frame-api.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

import {ExtensionError} from '../../core/extension-error.js';
import {createApiMap, invokeApiMapHandler} from '../../core/api-map.js';
import {parseJson} from '../../core/json.js';

export class TemplateRendererFrameApi {
Expand All @@ -26,56 +26,32 @@ export class TemplateRendererFrameApi {
constructor(templateRenderer) {
/** @type {import('./template-renderer.js').TemplateRenderer} */
this._templateRenderer = templateRenderer;
/** @type {import('core').MessageHandlerMap} */
this._windowMessageHandlers = new Map(/** @type {import('core').MessageHandlerMapInit} */ ([
/** @type {import('template-renderer-proxy').FrontendApiMap} */
this._windowMessageHandlers = createApiMap([
['render', this._onRender.bind(this)],
['renderMulti', this._onRenderMulti.bind(this)],
['getModifiedData', this._onGetModifiedData.bind(this)]
]));
]);
}

/**
* @returns {void}
*/
prepare() {
window.addEventListener('message', this._onWindowMessage.bind(this), false);
this._postMessage(window.parent, 'ready', {}, null);
this._postMessage(window.parent, 'ready', void 0, null);
}

// Private

/**
* @param {MessageEvent<import('template-renderer-frame-api').MessageData>} e
* @param {MessageEvent<import('template-renderer-proxy').FrontendMessageAny>} e
*/
_onWindowMessage(e) {
const {source, data: {action, params, id}} = e;
const messageHandler = this._windowMessageHandlers.get(action);
if (typeof messageHandler === 'undefined') { return; }

this._onWindowMessageInner(messageHandler, action, params, /** @type {Window} */ (source), id);
}

/**
* @param {import('core').MessageHandler} handler
* @param {string} action
* @param {import('core').SerializableObject} params
* @param {Window} source
* @param {?string} id
*/
async _onWindowMessageInner(handler, action, params, source, id) {
let response;
try {
let result = handler(params);
if (result instanceof Promise) {
result = await result;
}
response = {result};
} catch (error) {
response = {error: ExtensionError.serialize(error)};
}

if (typeof id === 'undefined') { return; }
this._postMessage(source, `${action}.response`, response, id);
invokeApiMapHandler(this._windowMessageHandlers, action, params, [], (response) => {
this._postMessage(/** @type {Window} */ (source), 'response', response, id);
});
}

/**
Expand Down Expand Up @@ -113,12 +89,15 @@ export class TemplateRendererFrameApi {
}

/**
* @template {import('template-renderer-proxy').BackendApiNames} TName
* @param {Window} target
* @param {string} action
* @param {import('core').SerializableObject} params
* @param {TName} action
* @param {import('template-renderer-proxy').BackendApiParams<TName>} params
* @param {?string} id
*/
_postMessage(target, action, params, id) {
target.postMessage(/** @type {import('template-renderer-frame-api').MessageData} */ ({action, params, id}), '*');
/** @type {import('template-renderer-proxy').BackendMessageAny} */
const data = {action, params, id};
target.postMessage(data, '*');
}
}
36 changes: 21 additions & 15 deletions ext/js/templates/template-renderer-proxy.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export class TemplateRendererProxy {
*/
async render(template, data, type) {
await this._prepareFrame();
return /** @type {import('template-renderer').RenderResult} */ (await this._invoke('render', {template, data, type}));
return await this._invoke('render', {template, data, type});
}

/**
Expand All @@ -52,7 +52,7 @@ export class TemplateRendererProxy {
*/
async renderMulti(items) {
await this._prepareFrame();
return /** @type {import('core').Response<import('template-renderer').RenderResult>[]} */ (await this._invoke('renderMulti', {items}));
return await this._invoke('renderMulti', {items});
}

/**
Expand All @@ -62,7 +62,7 @@ export class TemplateRendererProxy {
*/
async getModifiedData(data, type) {
await this._prepareFrame();
return /** @type {import('anki-templates').NoteData} */ (await this._invoke('getModifiedData', {data, type}));
return await this._invoke('getModifiedData', {data, type});
}

// Private
Expand Down Expand Up @@ -124,14 +124,14 @@ export class TemplateRendererProxy {
updateState(0x2);
};
/**
* @param {MessageEvent<unknown>} e
* @param {MessageEvent<import('template-renderer-proxy').BackendMessageAny>} e
*/
const onWindowMessage = (e) => {
if ((state & 0x5) !== 0x1) { return; }
const frameWindow = frame.contentWindow;
if (frameWindow === null || frameWindow !== e.source) { return; }
const {data} = e;
if (!(typeof data === 'object' && data !== null && /** @type {import('core').SerializableObject} */ (data).action === 'ready')) { return; }
if (!(typeof data === 'object' && data !== null && data.action === 'ready')) { return; }
updateState(0x4);
};

Expand Down Expand Up @@ -160,10 +160,11 @@ export class TemplateRendererProxy {
}

/**
* @param {string} action
* @param {import('core').SerializableObject} params
* @template {import('template-renderer-proxy').FrontendApiNames} TName
* @param {TName} action
* @param {import('template-renderer-proxy').FrontendApiParams<TName>} params
* @param {?number} [timeout]
* @returns {Promise<unknown>}
* @returns {Promise<import('template-renderer-proxy').FrontendApiReturn<TName>>}
*/
_invoke(action, params, timeout = null) {
return new Promise((resolve, reject) => {
Expand All @@ -189,30 +190,33 @@ export class TemplateRendererProxy {
timer = null;
}
};

/**
* @param {MessageEvent<unknown>} event
* @param {MessageEvent<import('template-renderer-proxy').BackendMessageAny>} event
*/
const onMessage = (event) => {
if (event.source !== frameWindow) { return; }
const {data} = event;
if (
typeof data !== 'object' ||
data === null ||
/** @type {import('core').SerializableObject} */ (data).id !== id ||
/** @type {import('core').SerializableObject} */ (data).action !== `${action}.response`
data.id !== id ||
data.action !== 'response'
) {
return;
}

const response = /** @type {import('core').SerializableObject} */ (data).params;
// This type should probably be able to be inferred without a cast, but for some reason it isn't.
const responseData = /** @type {import('template-renderer-proxy').BackendMessage<'response'>} */ (data);
const response = responseData.params;
if (typeof response !== 'object' || response === null) { return; }

cleanup();
const {error} = /** @type {import('core').Response} */ (response);
const {error} = response;
if (error) {
reject(ExtensionError.deserialize(error));
} else {
resolve(/** @type {import('core').Response} */ (response).result);
resolve(/** @type {import('template-renderer-proxy').FrontendApiReturn<TName>} */ (response.result));
}
};

Expand All @@ -224,7 +228,9 @@ export class TemplateRendererProxy {
this._invocations.add(invocation);

window.addEventListener('message', onMessage, false);
frameWindow.postMessage({action, params, id}, '*');
/** @type {import('template-renderer-proxy').FrontendMessage<TName>} */
const requestMessage = {action, params, id};
frameWindow.postMessage(requestMessage, '*');
});
}

Expand Down
24 changes: 0 additions & 24 deletions types/ext/template-renderer-frame-api.d.ts

This file was deleted.

102 changes: 102 additions & 0 deletions types/ext/template-renderer-proxy.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/*
* 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 {Response} from 'core';
import type {RenderMode, NoteData} from 'anki-templates';
import type {CompositeRenderData, PartialOrCompositeRenderData, RenderMultiItem, RenderResult} from 'template-renderer';
import type {
ApiMap as BaseApiMap,
ApiMapInit as BaseApiMapInit,
ApiHandler as BaseApiHandler,
ApiParams as BaseApiParams,
ApiNames as BaseApiNames,
ApiReturn as BaseApiReturn,
ApiReturnAny as BaseApiReturnAny,
} from './api-map';

// Frontend API

type FrontendApiSurface = {
render: {
params: {
template: string;
data: PartialOrCompositeRenderData;
type: RenderMode;
};
return: RenderResult;
};
renderMulti: {
params: {
items: RenderMultiItem[];
};
return: Response<RenderResult>[];
};
getModifiedData: {
params: {
data: CompositeRenderData;
type: RenderMode;
};
return: NoteData;
};
};

type FrontendApiParams<TName extends FrontendApiNames> = BaseApiParams<FrontendApiSurface[TName]>;

type FrontendApiNames = BaseApiNames<FrontendApiSurface>;

type FrontendApiReturnAny = BaseApiReturnAny<FrontendApiSurface>;

export type FrontendMessage<TName extends FrontendApiNames> = {
action: TName;
params: FrontendApiParams<TName>;
id: string;
};

export type FrontendMessageAny = FrontendMessage<FrontendApiNames>;

export type FrontendApiReturn<TName extends FrontendApiNames> = BaseApiReturn<FrontendApiSurface[TName]>;

export type FrontendApiMap = BaseApiMap<FrontendApiSurface>;

export type FrontendApiMapInit = BaseApiMapInit<FrontendApiSurface>;

export type FrontendApiHandler<TName extends FrontendApiNames> = BaseApiHandler<FrontendApiSurface[TName]>;

// Backend API

export type BackendApiSurface = {
ready: {
params: void;
return: void;
};
response: {
params: Response<FrontendApiReturnAny>;
return: void;
};
};

type BackendApiNames = BaseApiNames<BackendApiSurface>;

type BackendApiParams<TName extends BackendApiNames> = BaseApiParams<BackendApiSurface[TName]>;

export type BackendMessage<TName extends BackendApiNames> = {
action: TName;
params: BackendApiParams<TName>;
id: string | null;
};

export type BackendMessageAny = BackendMessage<BackendApiNames>;

0 comments on commit a72b53d

Please sign in to comment.