Skip to content

Commit c15809e

Browse files
authored
chore(worker rpc): improve error throwing (#6)
1 parent 67c9205 commit c15809e

File tree

5 files changed

+107
-98
lines changed

5 files changed

+107
-98
lines changed

bun.lockb

736 Bytes
Binary file not shown.

packages/utils/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
},
1919
"dependencies": {
2020
"mina-signer": "3.0.7",
21+
"serialize-error": "^11.0.3",
2122
"superjson": "2.2.1"
2223
},
2324
"devDependencies": {

packages/utils/src/test/worker.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { createRpcHandler } from "../worker-rpc";
22

33
const { messageHandler } = createRpcHandler({
4-
methods: {
5-
ping: async () => 'pong',
6-
}
7-
})
4+
methods: {
5+
ping: async () => "pong",
6+
},
7+
});
88

9-
self.onmessage = messageHandler
9+
self.onmessage = messageHandler;

packages/utils/src/worker-rpc.spec.ts

Lines changed: 28 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,34 @@
1-
import { describe, it, expect, mock, beforeAll } from 'bun:test'
2-
import { createRpc, createRpcHandler } from './worker-rpc'
1+
import { beforeAll, describe, expect, it, mock } from "bun:test";
2+
import { createRpc, createRpcHandler } from "./worker-rpc";
33

44
describe("Worker RPC", () => {
5-
let worker: Worker
5+
let worker: Worker;
66

7-
beforeAll(() => {
8-
worker = new Worker(new URL('./test/worker.ts', import.meta.url))
9-
})
7+
beforeAll(() => {
8+
worker = new Worker(new URL("./test/worker.ts", import.meta.url));
9+
});
1010

11-
it("creates RPC handler", async () => {
12-
const mockedHandler = mock(async () => "pong")
13-
const { messageHandler } = createRpcHandler({
14-
methods: {
15-
ping: mockedHandler,
16-
}
17-
})
18-
await messageHandler(new MessageEvent('message', { data: { method: 'ping', params: [] } }))
19-
expect(mockedHandler).toHaveBeenCalled()
20-
})
11+
it("creates RPC handler", async () => {
12+
const mockedHandler = mock(async () => "pong");
13+
const { messageHandler } = createRpcHandler({
14+
methods: {
15+
ping: mockedHandler,
16+
},
17+
});
18+
await messageHandler(
19+
new MessageEvent("message", { data: { method: "ping", params: [] } }),
20+
);
21+
expect(mockedHandler).toHaveBeenCalled();
22+
});
2123

22-
it("exchanges messages with Web Worker", async () => {
23-
const rpc = createRpc({ worker })
24-
const response = await rpc.request({ method: 'ping', params: [] })
25-
expect(response.result).toBe('pong')
26-
})
24+
it("exchanges messages with Web Worker", async () => {
25+
const rpc = createRpc({ worker });
26+
const response = await rpc.request({ method: "ping", params: [] });
27+
expect(response.result).toBe("pong");
28+
});
2729

28-
it("calls non-existing method", async () => {
29-
const rpc = createRpc({ worker })
30-
expect(rpc.request({ method: 'pang', params: [] })).rejects.toThrow()
31-
})
32-
})
30+
it("calls non-existing method", async () => {
31+
const rpc = createRpc({ worker });
32+
expect(rpc.request({ method: "pang", params: [] })).rejects.toThrow();
33+
});
34+
});

packages/utils/src/worker-rpc.ts

Lines changed: 73 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,93 +1,99 @@
1-
import { z } from "zod";
1+
import { deserializeError, serializeError } from "serialize-error";
22
import superjson from "superjson";
3+
import { z } from "zod";
34

45
const DEFAULT_TIMEOUT = 60000;
56

67
export const RequestSchema = z.object({
7-
method: z.string(),
8-
params: z.array(z.string()).optional(),
8+
method: z.string(),
9+
params: z.array(z.string()).optional(),
910
});
1011

1112
type RequestParams = z.infer<typeof RequestSchema>;
1213

1314
export const ResponseSchema = z
14-
.object({
15-
id: z.string(),
16-
result: z.any().optional(),
17-
error: z.string().optional(),
18-
})
19-
.strict();
15+
.object({
16+
id: z.string(),
17+
result: z.any().optional(),
18+
error: z.string().optional(),
19+
})
20+
.strict();
2021

2122
type Response = z.infer<typeof ResponseSchema>;
2223

2324
export type RequestFn = (params: RequestParams) => Promise<Response>;
2425

2526
export const createRpc = ({
26-
worker,
27-
timeout,
27+
worker,
28+
timeout,
2829
}: {
29-
worker: Worker;
30-
timeout?: number;
30+
worker: Worker;
31+
timeout?: number;
3132
}) => {
32-
const request: RequestFn = async ({ method, params }) => {
33-
let resolved = false;
34-
return new Promise((resolve, reject) => {
35-
console.log('>>>M', method, params)
36-
setTimeout(() => {
37-
if (resolved) return;
38-
return reject(new Error("[WorkerRPC] Timeout reached."));
39-
}, timeout ?? DEFAULT_TIMEOUT);
40-
const responseListener = (event: MessageEvent) => {
41-
resolved = true;
42-
worker.removeEventListener("message", responseListener);
43-
const data = superjson.parse(event.data);
44-
const response = ResponseSchema.parse(data);
45-
if (response.error)
46-
return reject(new Error(`[WorkerRPC] ${response.error}`));
47-
return resolve(response);
48-
};
49-
worker.addEventListener("message", responseListener);
50-
worker.postMessage({ method, params });
51-
});
52-
};
53-
return {
54-
request,
55-
};
33+
const request: RequestFn = async ({ method, params }) => {
34+
let resolved = false;
35+
return new Promise((resolve, reject) => {
36+
setTimeout(() => {
37+
if (resolved) return;
38+
return reject(new Error("[WorkerRPC] Timeout reached."));
39+
}, timeout ?? DEFAULT_TIMEOUT);
40+
const responseListener = (event: MessageEvent) => {
41+
resolved = true;
42+
worker.removeEventListener("message", responseListener);
43+
const data = superjson.parse(event.data);
44+
const response = ResponseSchema.parse(data);
45+
if (response.error) {
46+
const errorObject = superjson.parse(response.error);
47+
const deserializedError = deserializeError(errorObject);
48+
return reject(deserializedError);
49+
}
50+
return resolve(response);
51+
};
52+
worker.addEventListener("message", responseListener);
53+
worker.postMessage({ method, params });
54+
});
55+
};
56+
return {
57+
request,
58+
};
5659
};
5760

5861
type Method = (params: string[]) => Promise<unknown>;
5962
type MethodsMap = Record<string, Method>;
6063

61-
const respond = (data: unknown) => postMessage(superjson.stringify(data))
64+
const respond = (data: unknown) => postMessage(superjson.stringify(data));
6265

6366
export const createRpcHandler = ({ methods }: { methods: MethodsMap }) => {
64-
const methodKeys = Object.keys(methods);
65-
if (methodKeys.length === 0) throw new Error("No methods provided.");
66-
const MethodEnum = z.enum(['error', ...methodKeys]);
67-
const ExtendedRequestSchema = RequestSchema.extend({
68-
method: MethodEnum,
69-
}).strict();
70-
const ExtendedResponseSchema = ResponseSchema.extend({
71-
id: MethodEnum,
72-
}).strict();
73-
const messageHandler = async (event: MessageEvent) => {
74-
try {
75-
const action = ExtendedRequestSchema.parse(event.data)
76-
const callable = methods[action.method]
77-
if (!callable) throw new Error(`Method "${action.method}" not found.`);
78-
const result = await callable(action.params ?? []);
79-
const parsedResult = ExtendedResponseSchema.parse({
80-
id: action.method,
81-
result,
82-
});
83-
return respond(parsedResult);
84-
// biome-ignore lint/suspicious/noExplicitAny: Error handling
85-
} catch (error: any) {
86-
return respond(ExtendedResponseSchema.parse({
87-
id: 'error',
88-
error: `[WorkerRPC] ${error.message}`,
89-
}));
90-
}
91-
};
92-
return { messageHandler };
67+
const methodKeys = Object.keys(methods);
68+
if (methodKeys.length === 0) throw new Error("No methods provided.");
69+
const MethodEnum = z.enum(["error", ...methodKeys]);
70+
const ExtendedRequestSchema = RequestSchema.extend({
71+
method: MethodEnum,
72+
}).strict();
73+
const ExtendedResponseSchema = ResponseSchema.extend({
74+
id: MethodEnum,
75+
}).strict();
76+
const messageHandler = async (event: MessageEvent) => {
77+
try {
78+
const action = ExtendedRequestSchema.parse(event.data);
79+
const callable = methods[action.method];
80+
if (!callable) throw new Error(`Method "${action.method}" not found.`);
81+
const result = await callable(action.params ?? []);
82+
const parsedResult = ExtendedResponseSchema.parse({
83+
id: action.method,
84+
result,
85+
});
86+
return respond(parsedResult);
87+
// biome-ignore lint/suspicious/noExplicitAny: Error handling
88+
} catch (error: any) {
89+
const serializedError = superjson.stringify(serializeError(error));
90+
return respond(
91+
ExtendedResponseSchema.parse({
92+
id: "error",
93+
error: serializedError,
94+
}),
95+
);
96+
}
97+
};
98+
return { messageHandler };
9399
};

0 commit comments

Comments
 (0)