export type JSONRPC = { jsonrpc: "2.0"; id?: string | number; }; export type Call = JSONRPC & { method: string; params: T; }; export type RemoteError = { data: E } & ( | { code: number; message: string; } | { code: -32700; message: "Parse Error"; } | { code: -32600; message: "Invalid Request"; } | { code: -32601; message: "Method not found"; } | { code: -32602; message: "Invalid params"; } | { code: -32603; message: "Internal error"; } ); export type Result = JSONRPC & { id: string | number } & ( | { result: T; error: undefined; } | { error: RemoteError; result: undefined } ); export function isJSONRPCResult( object: Record, ): object is Result { return object["jsonrpc"] === "2.0" && object["id"] && !object["method"]; } export function isJSONRPCCall( object: Record, ): object is Call { return object["jsonrpc"] === "2.0" && !!object["method"]; } export class ResultDispatcher { private map: Map< number | string, ((value: Result) => void) | true // `true` = a call is generated, but the promise not created >; private nextId: number = Number.MIN_SAFE_INTEGER; constructor() { this.map = new Map(); } private rollId() { let id = 0; while (this.map.get((id = this.nextId++))) { if (this.nextId >= Number.MAX_SAFE_INTEGER) { this.nextId = Number.MIN_SAFE_INTEGER; } } return id; } createCall( method: string, params: T, ): [Promise>, Promise>] { const id = this.rollId(); const p = new Promise>((resolve) => this.map.set(id, resolve), ); this.map.set(id, true); const call: Call = { jsonrpc: "2.0", id, method, params, }; return [ new Promise((resolve) => { const waitUntilTheIdSet = () => { // We must do this check to make sure the id is set before the call made. // or the dispatching may lost the callback if (this.map.get(id)) { resolve(call); } else { setTimeout(waitUntilTheIdSet, 0); } }; waitUntilTheIdSet(); }), p, ]; } dispatch(id: string | number, message: Result) { { const callback = this.map.get(id); if (!callback) return; if (typeof callback !== "boolean") { callback(message); this.map.delete(id); return Promise.resolve(); } } return new Promise((resolve) => { let retried = 0; const checkAndDispatch = () => { const callback = this.map.get(id); if (typeof callback !== "boolean") { callback(message); this.map.delete(id); resolve(); return; } setTimeout(checkAndDispatch, 0); if (++retried > 3) { console.warn( `retried ${retried} time(s) but the callback is still disappeared, id is "${id}"`, ); } }; // start the loop checkAndDispatch(); }); } createTypedCall< S extends AnyService, E = unknown, K extends keyof S = keyof S, P extends Parameters = Parameters, R extends ReturnType = ReturnType, >(method: K, ...params: P): [Promise>, Promise>] { return this.createCall(method as string, params) as [ Promise>, Promise>, ]; } } type AnyService = Record unknown) | undefined> export async function dispatchCall< S extends AnyService, >(service: S, event: MessageEvent>) { try { const fn = service[event.data.method]; if (!fn) { if (event.data.id) return event.source.postMessage({ jsonrpc: "2.0", id: event.data.id, error: { code: -30601, message: "Method not found", }, } as Result); } try { const result = await fn(...event.data.params as unknown[]); if (!event.data.id) return; event.source.postMessage({ jsonrpc: "2.0", id: event.data.id, result: result, } as Result); } catch (reason) { event.source.postMessage({ jsonrpc: "2.0", id: event.data.id, error: { code: 0, message: String(reason), data: reason, }, } as Result); } } catch (reason) { if (event.data.id) event.source.postMessage({ jsonrpc: "2.0", id: event.data.id, error: { code: -32603, message: "Internal error", data: reason, }, } as Result); } }