214 lines
4.9 KiB
TypeScript
214 lines
4.9 KiB
TypeScript
export type JSONRPC = {
|
|
jsonrpc: "2.0";
|
|
id?: string | number;
|
|
};
|
|
|
|
export type Call<T = undefined> = JSONRPC & {
|
|
method: string;
|
|
params: T;
|
|
};
|
|
|
|
export type RemoteError<E = undefined> = { 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<T, E> = JSONRPC & { id: string | number } & (
|
|
| {
|
|
result: T;
|
|
error: undefined;
|
|
}
|
|
| { error: RemoteError<E>; result: undefined }
|
|
);
|
|
|
|
export function isJSONRPCResult(
|
|
object: Record<string, unknown>,
|
|
): object is Result<unknown, unknown> {
|
|
return object["jsonrpc"] === "2.0" && object["id"] && !object["method"];
|
|
}
|
|
|
|
export function isJSONRPCCall(
|
|
object: Record<string, unknown>,
|
|
): object is Call<unknown> {
|
|
return object["jsonrpc"] === "2.0" && !!object["method"];
|
|
}
|
|
|
|
export class ResultDispatcher {
|
|
private map: Map<
|
|
number | string,
|
|
((value: Result<unknown, unknown>) => 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<T>(
|
|
method: string,
|
|
params: T,
|
|
): [Promise<Call<T>>, Promise<Result<unknown, unknown>>] {
|
|
const id = this.rollId();
|
|
const p = new Promise<Result<unknown, unknown>>((resolve) =>
|
|
this.map.set(id, resolve),
|
|
);
|
|
this.map.set(id, true);
|
|
const call: Call<T> = {
|
|
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<unknown, unknown>) {
|
|
{
|
|
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<void>((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<S[K]> = Parameters<S[K]>,
|
|
R extends ReturnType<S[K]> = ReturnType<S[K]>,
|
|
>(method: K, ...params: P): [Promise<Call<P>>, Promise<Result<R, E>>] {
|
|
return this.createCall(method as string, params) as [
|
|
Promise<Call<P>>,
|
|
Promise<Result<R, E>>,
|
|
];
|
|
}
|
|
}
|
|
|
|
type AnyService = Record<string, ((...args: unknown[]) => unknown) | undefined>
|
|
|
|
export async function dispatchCall<
|
|
S extends AnyService,
|
|
>(service: S, event: MessageEvent<Call<unknown>>) {
|
|
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<void, void>);
|
|
}
|
|
|
|
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<unknown, void>);
|
|
} catch (reason) {
|
|
event.source.postMessage({
|
|
jsonrpc: "2.0",
|
|
id: event.data.id,
|
|
error: {
|
|
code: 0,
|
|
message: String(reason),
|
|
data: reason,
|
|
},
|
|
} as Result<unknown, unknown>);
|
|
}
|
|
} 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<void, unknown>);
|
|
}
|
|
}
|