diff --git a/src/App.tsx b/src/App.tsx index 6a73749..9721c84 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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("ping"); worker.postMessage(await call); const result = await ret; console.assert(!result.error, result); diff --git a/src/serviceworker/main.ts b/src/serviceworker/main.ts index 457875e..71a4f37 100644 --- a/src/serviceworker/main.ts +++ b/src/serviceworker/main.ts @@ -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) => { - 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) => { + const payload = event.data; + if (typeof payload !== "object") return; + if (isJSONRPCCall(payload as Record)) { + dispatchCall(Service, event as MessageEvent>); } -}) +}); diff --git a/src/serviceworker/services.ts b/src/serviceworker/services.ts index 61f70ec..2db3d4a 100644 --- a/src/serviceworker/services.ts +++ b/src/serviceworker/services.ts @@ -1,177 +1,3 @@ -export type JSONRPC = { - jsonrpc: "2.0"; - id?: string | number; +export type Service = { + ping(): void }; - -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]; - }; diff --git a/src/serviceworker/workerrpc.ts b/src/serviceworker/workerrpc.ts new file mode 100644 index 0000000..84bb39f --- /dev/null +++ b/src/serviceworker/workerrpc.ts @@ -0,0 +1,214 @@ +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); + } +}