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< K extends keyof RequestParams, P extends RequestParams[K], R extends ResponseResults[K], E extends ResponseErrorDatas[K], >(method: K, params: P): [Promise>, Promise>] { return this.createCall(method, params) as [ Promise>, Promise>, ]; } } interface RequestParams { ping: void; } interface ResponseResults { ping: void; } interface ResponseErrorDatas { [key: string]: void; } export type AnyCall = JSONRPC & { method: K; params: RequestParams[K]; };