serviceworker: move rpc-relateds to workerrpc
All checks were successful
/ depoly (push) Successful in 56s

This commit is contained in:
thislight 2024-10-16 22:42:25 +08:00
parent 46e7f1aaea
commit 5b72160cdb
No known key found for this signature in database
GPG key ID: A50F9451AC56A63E
4 changed files with 235 additions and 193 deletions

View file

@ -23,7 +23,10 @@ import {
isJSONRPCResult,
ResultDispatcher,
type JSONRPC,
} from "./serviceworker/services.js";
} from "./serviceworker/workerrpc.js";
import {
Service
} from "./serviceworker/services.js"
import { makeEventListener } from "@solid-primitives/event-listener";
import { ServiceWorkerProvider } from "./platform/host.js";
@ -75,7 +78,7 @@ const App: Component = () => {
worker: ServiceWorker,
expectedAge: number,
) => {
const [call, ret] = dispatcher.createTypedCall("ping", undefined);
const [call, ret] = dispatcher.createTypedCall<Service>("ping");
worker.postMessage(await call);
const result = await ret;
console.assert(!result.error, result);

View file

@ -1,17 +1,17 @@
import { cleanupOutdatedCaches, precacheAndRoute } from "workbox-precaching";
import { clientsClaim } from "workbox-core";
import type { AnyCall } from "./services";
import { dispatchCall, isJSONRPCCall, type Call } from "./workerrpc";
function checkServiceWorker(
function isServiceWorker(
self: WorkerGlobalScope,
): self is ServiceWorkerGlobalScope {
return !!(self as unknown as ServiceWorkerGlobalScope).registration;
}
if (checkServiceWorker(self)) {
if (isServiceWorker(self)) {
cleanupOutdatedCaches();
precacheAndRoute(self.__WB_MANIFEST, {
cleanURLs: false
cleanURLs: false,
});
// auto update
@ -21,15 +21,14 @@ if (checkServiceWorker(self)) {
throw new TypeError("This entry point must be run in a service worker");
}
self.addEventListener("message", (event: MessageEvent<AnyCall>) => {
if (event.data.method === "ping") {
event.source.postMessage({id: event.data.id, jsonrpc: "2.0", result: undefined})
} else {
event.source.postMessage({
error: {
code: -32601,
message: "Method not found"
}
})
export const Service = {
ping() {},
};
self.addEventListener("message", (event: MessageEvent<unknown>) => {
const payload = event.data;
if (typeof payload !== "object") return;
if (isJSONRPCCall(payload as Record<string, unknown>)) {
dispatchCall(Service, event as MessageEvent<Call<unknown>>);
}
})
});

View file

@ -1,177 +1,3 @@
export type JSONRPC = {
jsonrpc: "2.0";
id?: string | number;
export type Service = {
ping(): void
};
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<
K extends keyof RequestParams,
P extends RequestParams[K],
R extends ResponseResults[K],
E extends ResponseErrorDatas[K],
>(method: K, params: P): [Promise<Call<P>>, Promise<Result<R, E>>] {
return this.createCall(method, params) as [
Promise<Call<P>>,
Promise<Result<R, E>>,
];
}
}
interface RequestParams {
ping: void;
}
interface ResponseResults {
ping: void;
}
interface ResponseErrorDatas {
[key: string]: void;
}
export type AnyCall<K extends keyof RequestParams = keyof RequestParams> =
JSONRPC & {
method: K;
params: RequestParams[K];
};

View file

@ -0,0 +1,214 @@
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>);
}
}