GoogleChromeLabs / GoogleChromeLabs/comlink
documentation on (deeply?) proxying objects like Request/Response
- Dominant language
- TypeScript
- Stars
- 12.8k
- Forks
- 435
- PR merge metrics
- No merged PRs in 30d
Description
Here's the code I think I want to be able to write:
**main.ts**
```ts
import { serve } from "https://deno.land/std@0.133.0/http/server.ts";
import * as Comlink from "https://cdn.skypack.dev/comlink@4.3.1?dts";
serve(async function handler(request) {
const worker = new Worker(new URL("./worker.ts", import.meta.url).href, {
type: "module",
});
const handler = Comlink.wrap<(request: Request) => Promise>(worker);
return await handler(Comlink.proxy(request));
});
```
**worker.ts**
```ts
///
///
import * as Comlink from "https://cdn.skypack.dev/comlink@4.3.1?dts";
Comlink.expose(async (request: Request): Promise => {
const body = await request.text();
return Comlink.proxy(
new Response(`Hello to ${request.url}\n\nReceived:\n\n${body}\n`)
);
});
```
but when I run my server and send it a request (e.g. `curl -X POST --data '{"answer":42}' http://localhost:8000/foo/bar`) I get an error:
```console
% deno run --allow-net --allow-read main.ts
Check file:///path/to/main.ts
Check file:///path/to/worker.ts
error: Uncaught (in worker "") (in promise) TypeError: Cannot read properties of undefined (reading 'apply')
TypeError: Cannot convert object to primitive value
at AsyncFunction. (file:///path/to/worker.ts:9:38)
error: Uncaught (in promise) Error: Unhandled error event in child worker.
at Worker.#pollControl (deno:runtime/js/11_workers.js:168:21)
```
I tried implementing a transfer handler based on some async-iterable examples but I didn't get it working and ended up implementing in a less than ideal way. I figure there's bound to be an easier way. I think some additional documentation on how to implement transfer handlers would be helpful. Could such be added in Comlink's README? I think it would be very beneficial to include an example that reuses the existing `proxy` transfer handler to transfer/proxy a more complex object than an `Event` (e.g. a `Request` and/or `Response` object).
For reference, the code I ended up writing to get things to work is included below and is what is in my current [answer](https://stackoverflow.com/a/71744944/3255152) to [multithreading - Deno on multi-core machines - Stack Overflow](https://stackoverflow.com/questions/71736369/deno-on-multi-core-machines).
> ```typescript
> import { serve } from "https://deno.land/std@0.133.0/http/server.ts";
>
> import ComlinkRequestHandler from "./ComlinkRequestHandler.ts";
>
> serve(async function handler(request) {
> const worker = new Worker(new URL("./worker.ts", import.meta.url).> href, {
> type: "module",
> });
>
> const handler = ComlinkRequestHandler.wrap(worker);
>
> return await handler(request);
> });
> ```
>
> **worker.ts**
>
> ```typescript
> ///
> ///
>
> import ComlinkRequestHandler from "./ComlinkRequestHandler.ts";
>
> ComlinkRequestHandler.expose(async (request) => {
> const body = await request.text();
> return new Response(`Hello to ${request.url}\n\nReceived:\n\n${body}> \n`);
> });
> ```
>
> **ComlinkRequestHandler.ts**
>
> ```typescript
> import * as Comlink from "https://cdn.skypack.dev/comlink@4.3.1?dts";
>
> interface RequestMessage extends Omit > {
> url: string;
> headers: Record;
> hasBody: boolean;
> }
>
> interface ResponseMessage extends ResponseInit {
> headers: Record;
> hasBody: boolean;
> }
>
> export default class ComlinkRequestHandler {
> #handler: (request: Request) => Promise;
> #responseBodyReader: ReadableStreamDefaultReader | > undefined;
>
> static expose(handler: (request: Request) => Promise) {
> Comlink.expose(new ComlinkRequestHandler(handler));
> }
>
> static wrap(worker: Worker) {
> const { handleRequest, nextResponseBodyChunk } =
> Comlink.wrap(worker);
>
> return async (request: Request): Promise => {
> const requestBodyReader = request.body?.getReader();
>
> const requestMessage: RequestMessage = {
> url: request.url,
> hasBody: requestBodyReader !== undefined,
> cache: request.cache,
> credentials: request.credentials,
> headers: Object.fromEntries(request.headers.entries()),
> integrity: request.integrity,
> keepalive: request.keepalive,
> method: request.method,
> mode: request.mode,
> redirect: request.redirect,
> referrer: request.referrer,
> referrerPolicy: request.referrerPolicy,
> };
>
> const nextRequestBodyChunk = Comlink.proxy(async () => {
> if (requestBodyReader === undefined) return undefined;
> const { value } = await requestBodyReader.read();
> return value;
> });
>
> const { hasBody: responseHasBody, ...responseInit } = await > handleRequest(
> requestMessage,
> nextRequestBodyChunk
> );
>
> const responseBodyInit: BodyInit | null = responseHasBody
> ? new ReadableStream({
> start(controller) {
> async function push() {
> const value = await nextResponseBodyChunk();
> if (value === undefined) {
> controller.close();
> return;
> }
> controller.enqueue(value);
> push();
> }
>
> push();
> },
> })
> : null;
>
> return new Response(responseBodyInit, responseInit);
> };
> }
>
> constructor(handler: (request: Request) => Promise) {
> this.#handler = handler;
> }
>
> async handleRequest(
> { url, hasBody, ...init }: RequestMessage,
> nextRequestBodyChunk: () => Promise
> ): Promise {
> const request = new Request(
> url,
> hasBody
> ? {
> ...init,
> body: new ReadableStream({
> start(controller) {
> async function push() {
> const value = await nextRequestBodyChunk();
> if (value === undefined) {
> controller.close();
> return;
> }
> controller.enqueue(value);
> push();
> }
>
> push();
> },
> }),
> }
> : init
> );
> const response = await this.#handler(request);
> this.#responseBodyReader = response.body?.getReader();
> return {
> hasBody: this.#responseBodyReader !== undefined,
> headers: Object.fromEntries(response.headers.entries()),
> status: response.status,
> statusText: response.statusText,
> };
> }
>
> async nextResponseBodyChunk(): Promise {
> if (this.#responseBodyReader === undefined) return undefined;
> const { value } = await this.#responseBodyReader.read();
> return value;
> }
> }
> ```
>
> Example usage:
>
> ```shell
> % deno run --allow-net --allow-read main.ts
> ```
>
> ```shell
> % curl -X POST --data '{"answer":42}' http://localhost:8000/foo/bar
> Hello to http://localhost:8000/foo/bar
>
> Received:
>
> {"answer":42}
> ```
Contributor guide
Assessment
This issue has not been assessed yet.