diff --git a/bun.lockb b/bun.lockb index 7984ec7..9971bc8 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/packages/utils/package.json b/packages/utils/package.json index e328a9d..aa55899 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -18,6 +18,7 @@ }, "dependencies": { "mina-signer": "3.0.7", + "serialize-error": "^11.0.3", "superjson": "2.2.1" }, "devDependencies": { diff --git a/packages/utils/src/test/worker.ts b/packages/utils/src/test/worker.ts index 05663c6..1048622 100644 --- a/packages/utils/src/test/worker.ts +++ b/packages/utils/src/test/worker.ts @@ -1,9 +1,9 @@ import { createRpcHandler } from "../worker-rpc"; const { messageHandler } = createRpcHandler({ - methods: { - ping: async () => 'pong', - } -}) + methods: { + ping: async () => "pong", + }, +}); -self.onmessage = messageHandler +self.onmessage = messageHandler; diff --git a/packages/utils/src/worker-rpc.spec.ts b/packages/utils/src/worker-rpc.spec.ts index 19dbdf7..6aee4e8 100644 --- a/packages/utils/src/worker-rpc.spec.ts +++ b/packages/utils/src/worker-rpc.spec.ts @@ -1,32 +1,34 @@ -import { describe, it, expect, mock, beforeAll } from 'bun:test' -import { createRpc, createRpcHandler } from './worker-rpc' +import { beforeAll, describe, expect, it, mock } from "bun:test"; +import { createRpc, createRpcHandler } from "./worker-rpc"; describe("Worker RPC", () => { - let worker: Worker + let worker: Worker; - beforeAll(() => { - worker = new Worker(new URL('./test/worker.ts', import.meta.url)) - }) + beforeAll(() => { + worker = new Worker(new URL("./test/worker.ts", import.meta.url)); + }); - it("creates RPC handler", async () => { - const mockedHandler = mock(async () => "pong") - const { messageHandler } = createRpcHandler({ - methods: { - ping: mockedHandler, - } - }) - await messageHandler(new MessageEvent('message', { data: { method: 'ping', params: [] } })) - expect(mockedHandler).toHaveBeenCalled() - }) + it("creates RPC handler", async () => { + const mockedHandler = mock(async () => "pong"); + const { messageHandler } = createRpcHandler({ + methods: { + ping: mockedHandler, + }, + }); + await messageHandler( + new MessageEvent("message", { data: { method: "ping", params: [] } }), + ); + expect(mockedHandler).toHaveBeenCalled(); + }); - it("exchanges messages with Web Worker", async () => { - const rpc = createRpc({ worker }) - const response = await rpc.request({ method: 'ping', params: [] }) - expect(response.result).toBe('pong') - }) + it("exchanges messages with Web Worker", async () => { + const rpc = createRpc({ worker }); + const response = await rpc.request({ method: "ping", params: [] }); + expect(response.result).toBe("pong"); + }); - it("calls non-existing method", async () => { - const rpc = createRpc({ worker }) - expect(rpc.request({ method: 'pang', params: [] })).rejects.toThrow() - }) -}) + it("calls non-existing method", async () => { + const rpc = createRpc({ worker }); + expect(rpc.request({ method: "pang", params: [] })).rejects.toThrow(); + }); +}); diff --git a/packages/utils/src/worker-rpc.ts b/packages/utils/src/worker-rpc.ts index 1da9e2b..7bdeaa7 100644 --- a/packages/utils/src/worker-rpc.ts +++ b/packages/utils/src/worker-rpc.ts @@ -1,93 +1,99 @@ -import { z } from "zod"; +import { deserializeError, serializeError } from "serialize-error"; import superjson from "superjson"; +import { z } from "zod"; const DEFAULT_TIMEOUT = 60000; export const RequestSchema = z.object({ - method: z.string(), - params: z.array(z.string()).optional(), + method: z.string(), + params: z.array(z.string()).optional(), }); type RequestParams = z.infer; export const ResponseSchema = z - .object({ - id: z.string(), - result: z.any().optional(), - error: z.string().optional(), - }) - .strict(); + .object({ + id: z.string(), + result: z.any().optional(), + error: z.string().optional(), + }) + .strict(); type Response = z.infer; export type RequestFn = (params: RequestParams) => Promise; export const createRpc = ({ - worker, - timeout, + worker, + timeout, }: { - worker: Worker; - timeout?: number; + worker: Worker; + timeout?: number; }) => { - const request: RequestFn = async ({ method, params }) => { - let resolved = false; - return new Promise((resolve, reject) => { - console.log('>>>M', method, params) - setTimeout(() => { - if (resolved) return; - return reject(new Error("[WorkerRPC] Timeout reached.")); - }, timeout ?? DEFAULT_TIMEOUT); - const responseListener = (event: MessageEvent) => { - resolved = true; - worker.removeEventListener("message", responseListener); - const data = superjson.parse(event.data); - const response = ResponseSchema.parse(data); - if (response.error) - return reject(new Error(`[WorkerRPC] ${response.error}`)); - return resolve(response); - }; - worker.addEventListener("message", responseListener); - worker.postMessage({ method, params }); - }); - }; - return { - request, - }; + const request: RequestFn = async ({ method, params }) => { + let resolved = false; + return new Promise((resolve, reject) => { + setTimeout(() => { + if (resolved) return; + return reject(new Error("[WorkerRPC] Timeout reached.")); + }, timeout ?? DEFAULT_TIMEOUT); + const responseListener = (event: MessageEvent) => { + resolved = true; + worker.removeEventListener("message", responseListener); + const data = superjson.parse(event.data); + const response = ResponseSchema.parse(data); + if (response.error) { + const errorObject = superjson.parse(response.error); + const deserializedError = deserializeError(errorObject); + return reject(deserializedError); + } + return resolve(response); + }; + worker.addEventListener("message", responseListener); + worker.postMessage({ method, params }); + }); + }; + return { + request, + }; }; type Method = (params: string[]) => Promise; type MethodsMap = Record; -const respond = (data: unknown) => postMessage(superjson.stringify(data)) +const respond = (data: unknown) => postMessage(superjson.stringify(data)); export const createRpcHandler = ({ methods }: { methods: MethodsMap }) => { - const methodKeys = Object.keys(methods); - if (methodKeys.length === 0) throw new Error("No methods provided."); - const MethodEnum = z.enum(['error', ...methodKeys]); - const ExtendedRequestSchema = RequestSchema.extend({ - method: MethodEnum, - }).strict(); - const ExtendedResponseSchema = ResponseSchema.extend({ - id: MethodEnum, - }).strict(); - const messageHandler = async (event: MessageEvent) => { - try { - const action = ExtendedRequestSchema.parse(event.data) - const callable = methods[action.method] - if (!callable) throw new Error(`Method "${action.method}" not found.`); - const result = await callable(action.params ?? []); - const parsedResult = ExtendedResponseSchema.parse({ - id: action.method, - result, - }); - return respond(parsedResult); - // biome-ignore lint/suspicious/noExplicitAny: Error handling - } catch (error: any) { - return respond(ExtendedResponseSchema.parse({ - id: 'error', - error: `[WorkerRPC] ${error.message}`, - })); - } - }; - return { messageHandler }; + const methodKeys = Object.keys(methods); + if (methodKeys.length === 0) throw new Error("No methods provided."); + const MethodEnum = z.enum(["error", ...methodKeys]); + const ExtendedRequestSchema = RequestSchema.extend({ + method: MethodEnum, + }).strict(); + const ExtendedResponseSchema = ResponseSchema.extend({ + id: MethodEnum, + }).strict(); + const messageHandler = async (event: MessageEvent) => { + try { + const action = ExtendedRequestSchema.parse(event.data); + const callable = methods[action.method]; + if (!callable) throw new Error(`Method "${action.method}" not found.`); + const result = await callable(action.params ?? []); + const parsedResult = ExtendedResponseSchema.parse({ + id: action.method, + result, + }); + return respond(parsedResult); + // biome-ignore lint/suspicious/noExplicitAny: Error handling + } catch (error: any) { + const serializedError = superjson.stringify(serializeError(error)); + return respond( + ExtendedResponseSchema.parse({ + id: "error", + error: serializedError, + }), + ); + } + }; + return { messageHandler }; };