tutu/src/serviceworker/workerrpc.ts

215 lines
4.9 KiB
TypeScript
Raw Normal View History

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>);
}
}