Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Web Worker Hangs if Imported File Contains Top-Level Await #635

Open
asharonbaltazar opened this issue Mar 27, 2023 · 6 comments
Open

Web Worker Hangs if Imported File Contains Top-Level Await #635

asharonbaltazar opened this issue Mar 27, 2023 · 6 comments

Comments

@asharonbaltazar
Copy link

asharonbaltazar commented Mar 27, 2023

Whatever your thoughts are on top-level await, I'm initializing a library using await. However, the web worker seems to hang once it encounters the await, preventing the rest of the web worker from executing. Any imported file with a top-level await causes this.

I've made a reproducible example (make sure to use a Chromium browser). If the count is {count} button is clicked, the getNumberPlusOne function in fakeTopLevelAwait won't work. If the await is commented out, getNumberPlusOne executes successfully.

I'd like to know if this a limitation in Web Workers or comlink. I'd really like to get this working. And kudos on such a fantastic lib!

@AhmetKaanGuney
Copy link

I had the exact same issue. I think that's because of how import is implemented for the worker.

@surma
Copy link
Collaborator

surma commented Jun 14, 2023

Right, so currently it’s the user’s responsibility to make sure the worker is ready before using Comlink. Because you create the worker in the click hander (side-note: generally I wouldn’t recommend that!), you call getNumberPlusOne() before the worker is ready. So the message that tells the worker that the function was called just gets lost.

You either need to send a ”ready!” message and wait for that first or — the more fragile, hackier and easier solution — is to move the worker creation to the top of the file. Unless the user clicks the button in under 2 seconds (because of your setTimeout) it works! See here.

This is something I’d like to address in an update to Comlink where wrap() returns a Promise that you have to await where Comlink waits until the worker calls expose().

@ansemjo
Copy link

ansemjo commented Jun 14, 2023

This is something I’d like to address in an update to Comlink where wrap() returns a Promise that you have to await where Comlink waits until the worker calls expose().

I'd love that! I'm just beginning to use comlink-wrapped Workers in a Vue component and was wondering why the very simple ready() { return true; } function simply seemed to hang. Because the top-level <script setup> block in single-file components does not allow asynchronous functions, I've put the calls in the onMounted lifecycle hook. But the Promises only started resolving after I inserted a delay of several hundred milliseconds.

[edit: this only happens when I initialize more than one single Worker in a single component <script setup> though. If there's only one it seems to be quick enough.]

If the Comlink.wrap() function returns a Promise, I won't be able to use it in the <script setup> block but maybe I'm better off putting it all outside a component anyway. At least then I'll have a sure way of knowing the link is ready. 😊

@ansemjo
Copy link

ansemjo commented Jun 20, 2023

I wrote myself thin wrappers with an EventListener that waits for a simple { ready: true } message from the Worker before resolving a Promise with the Comlink-wrapped Remote. This probably isn't ripe for a pull request but might serve as inspiration to those that encounter this issue right now.

The environment consists of Vue 3.3.4 with TypeScript 5.0.4 and Vite 4.3.9.

// @/worker/spawn.ts
import { wrap as comlink, type Remote } from "comlink";

/** An object with both the original Worker and the Comlink-wrapped Remote of it. */
export type RemoteWorker<T> = { worker: Worker, remote: Remote<T> };

/** The message expected by the `readinessListener`. */
export const Ready = { ready: true };

/** Listen for the readiness message from the Worker and call the `callback` once. */
export function readinessListener(worker: Worker, callback: () => void) {
  worker.addEventListener("message", function ready(event: MessageEvent<typeof Ready>) {
    if (!!event.data && event.data.ready === true) {
      worker.removeEventListener("message", ready);
      callback();
    };
  });
};

/** Create a new Comlink-wrapped Worker of type `T` and attach an optional
 * readiness-callback to signal when it's ready to receive requests. **/
export function spawnSync<T>(path: URL | string, ready?: () => void): RemoteWorker<T> {

  // create a new worker using the Vite API
  // https://vitejs.dev/guide/features.html#web-workers
  let worker = new Worker(new URL(path, /* relative to _this_ file */ import.meta.url), { type: "module" });

  // optionally attach the callback using readiness listener
  if (ready != undefined) readinessListener(worker, ready);

  // wrap worker with comlink
  let remote = comlink<T>(worker);

  return { worker, remote };
};

/** Create a new Comlink-wrapped Worker of type `T` asynchronously. The Promise
 * resolves only once the Remote is ready to receive messages. **/
export async function spawn<T>(path: URL | string): Promise<RemoteWorker<T>> {

  // create a new worker using the Vite API
  // https://vitejs.dev/guide/features.html#web-workers
  let worker = new Worker(new URL(path, /* relative to _this_ file */ import.meta.url), { type: "module" });

  // create a promise to wait for the readiness message
  await new Promise<void>(resolve => readinessListener(worker, resolve));

  // now we're ready to wrap the worker with comlink
  let remote = comlink<T>(worker);

  return { worker, remote };
};

Then expose the Worker like this:

// @/worker/simpleworker.ts
import { expose } from "comlink";
import { spawn, Ready } from "./spawn";

export class SimpleWorker {
  // ...
}

expose(new SimpleWorker());
postMessage(Ready); // signal the readinessListener

And instantiate it like this:

import { spawn, spawnSync } from "@/worker/spawn";
import { SimpleWorker } from "@/worker/simpleworker";

// asynchronously with promise
const simple = await spawn<SimpleWorker>("simpleworker");
console.log("SimpleWorker is ready!");

// synchronously with callback
const simple = spawnSync<SimpleWorker>("simpleworker", () => console.log("SimpleWorker is ready!"));

@mccare
Copy link

mccare commented Nov 4, 2023

Just had the same issue with vue and creating and setting up a worker in the setup section of my component. Here is the above solution in Javascript.

async function wrap(worker) {
     await new Promise((resolve, reject) => {
          const controller = new AbortController();
          worker.addEventListener('message', (message) => {
               if (message?.data?.ready) {
                    controller.abort();
                    resolve();
               }
          }, { signal: controller.signal })
     })
     return Comlink.wrap(worker);
}

In your worker:

Comlink.expose(object);
postMessage({ready: true})

And to use it:

import Worker from './worker?worker'

await wrap(new Worker());

@vinayakkulkarni
Copy link

Any repro Vue example?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants