Skip to content

Commit

Permalink
feat(utils): add web worker rpc (#4)
Browse files Browse the repository at this point in the history
  • Loading branch information
mrcnk authored Nov 24, 2024
1 parent df00082 commit d652e7a
Show file tree
Hide file tree
Showing 16 changed files with 219 additions and 5 deletions.
74 changes: 74 additions & 0 deletions apps/docs/src/pages/utils.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Utils [Don't waste time on implementing these yourself.]

We've implemented some utilities that you might find useful. You can find them in the `@mina-js/utils` package.

## Units

Utils library provides functions for unit conversion.

### formatMina

Formats micro-Mina to human-readable Mina value.

```typescript twoslash
import { formatMina } from '@mina-js/utils'

const mina = formatMina(5_000_000_000n)
// -> "5"
```

### parseMina

Parses human-readable Mina value to micro-Mina.

```typescript twoslash
import { parseMina } from '@mina-js/utils'

const mina = parseMina('5')
// -> 5_000_000_000n
```

### formatUnit

```typescript twoslash
import { formatUnits } from '@mina-js/utils'

const formatted = formatUnits(4200000000000n, 10)
// -> "420"
```

### parseUnits

```typescript twoslash
import { parseUnits } from '@mina-js/utils'

const parsed = parseUnits("420", 10)
// -> 4200000000000n
```

## Web Workers

Proof related computations can be heavy and can block the main thread. To avoid this, you can use Web Workers to run these computations in a separate thread. We've prepared a JSON-RPC protocol to easily connect the dots.

```typescript twoslash
// @filename: worker.ts
import { createRpcHandler } from "@mina-js/utils";

const { messageHandler } = createRpcHandler({
methods: {
ping: async () => 'pong',
}
})

self.onmessage = messageHandler

// @filename: index.ts
import { createRpc } from "@mina-js/utils";

const worker = new Worker(new URL('./worker.ts', import.meta.url))
const rpc = createRpc({ worker })
const response = await rpc.request({
method: 'ping',
params: [],
})
```
4 changes: 4 additions & 0 deletions apps/docs/vocs.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,10 @@ export default defineConfig({
text: "Roadmap",
link: "/roadmap",
},
{
text: "Utils",
link: "/utils",
},
{
text: "MinaJS Connect",
link: "/connect",
Expand Down
Binary file modified bun.lockb
Binary file not shown.
3 changes: 2 additions & 1 deletion packages/utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
"cleanup": "rimraf dist .turbo"
},
"dependencies": {
"mina-signer": "3.0.7"
"mina-signer": "3.0.7",
"superjson": "2.2.1"
},
"devDependencies": {
"zod": "3.23.8"
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
9 changes: 5 additions & 4 deletions packages/utils/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
export * from "./types";
export * from "./validation";
export { formatMina } from "./src/format-mina";
export { formatUnits } from "./src/format-units";
export { parseMina } from "./src/parse-mina";
export { parseUnits } from "./src/parse-units";
export { formatMina } from "./format-mina";
export { formatUnits } from "./format-units";
export { parseMina } from "./parse-mina";
export { parseUnits } from "./parse-units";
export { createRpc, createRpcHandler } from "./worker-rpc";
export * as Test from "./test/constants";
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
9 changes: 9 additions & 0 deletions packages/utils/src/test/worker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { createRpcHandler } from "../worker-rpc";

const { messageHandler } = createRpcHandler({
methods: {
ping: async () => 'pong',
}
})

self.onmessage = messageHandler
32 changes: 32 additions & 0 deletions packages/utils/src/worker-rpc.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { describe, it, expect, mock, beforeAll } from 'bun:test'
import { createRpc, createRpcHandler } from './worker-rpc'

describe("Worker RPC", () => {
let worker: Worker

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("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()
})
})
93 changes: 93 additions & 0 deletions packages/utils/src/worker-rpc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { z } from "zod";
import superjson from "superjson";

const DEFAULT_TIMEOUT = 60000;

export const RequestSchema = z.object({
method: z.string(),
params: z.array(z.string()).optional(),
});

type RequestParams = z.infer<typeof RequestSchema>;

export const ResponseSchema = z
.object({
id: z.string(),
result: z.any().optional(),
error: z.string().optional(),
})
.strict();

type Response = z.infer<typeof ResponseSchema>;

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

export const createRpc = ({
worker,
timeout,
}: {
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,
};
};

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

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

0 comments on commit d652e7a

Please sign in to comment.