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

Asynchronous module loading via top-level await breaks SharedWorkers "connect" event #37

Open
pvh opened this issue Aug 9, 2023 · 6 comments

Comments

@pvh
Copy link

pvh commented Aug 9, 2023

Chrome has support for a feature called SharedWorkers. This allows you to run a process in the background shared between multiple tabs with the same origin. It's essentially a small extension over a webworker. The key difference between the two is that SharedWorkers have a "connect" event which fires when a new tab creates a reference to the shared worker.

Unfortunately, this event ("connect") is sent after the SharedWorker's code is evaluated but before the async function's promise resolves. Fixing this upstream in Chrome is the right solution... but I suspect not really an option in the mid-term.

I've been trying to think of what a good workaround would be... Possibly in a SharedWorker the "connect" event could be intercepted and forwarded on after async evaluation is resolved. I'm not entirely sure.

I am at least sure of this: without a solution, you can't really use SharedWorkers with WASM. (Except through some pretty gnarly hacks.)

@Menci
Copy link
Owner

Menci commented Aug 9, 2023

Could you provide a minimal reproduce? Let me see what can I do on my side...

@pvh
Copy link
Author

pvh commented Aug 9, 2023

Thanks for the offer. I saw the README recommending an approach for other webworkers but I don't see any example/test code in the repo. Do you have something in another repo I should use as a model, or should I just write something based on my own code?

@pvh
Copy link
Author

pvh commented Aug 9, 2023

I spent a little time pulling on the thread. I'd hoped removing the top-level-await plugin would make things better but it turns out that the browser runs the async code

However! All is not lost. The less-hacky but still hacky solution I found was to use dynamic imports to load the offending module inside an async method. This could probably be improved further but looks something like this:

https://github.com/automerge/automerge-repo/blob/main/examples/automerge-repo-demo-counter/src/shared-worker.ts

I confirmed the order of operations via log-lines here, though I've removed them now.

@Menci
Copy link
Owner

Menci commented Aug 12, 2023

I just understand your problem. The top-level statements executes earlier than imported async modules' loading. So your event handler depending on imported async modules won't work.

I don't think it's a issue of my plugin. In the ESM top-level await specifications, the top-level statements is executed after synchronous sub-modules loading but before asynchronous sub-modules loading. Without my plugin in a standard browser environment with TLA support the result will be the same.

@pvh
Copy link
Author

pvh commented Aug 15, 2023

Let me see if we're understanding each other:

import("module-which-might-depend-on-wasm")
self.on("connect", () => { console.log("connected") })

This code will behave differently depending on whether some module in its dependency tree loads WASM, but if there is WASM it will fail to log for both cases of native-TLA / vite-plugin-top-level-await.

But if we consider this case:

self.on("connect", () => { console.log("connected") })
await import("module-which-uses-wasm")

I believe you're saying that a browser with TLA support will console.log() successfully, but vite-plugin-top-level-await will not log successfully. (Because it wraps the entire file in one large async function().)

@Menci
Copy link
Owner

Menci commented Dec 10, 2023

I still don't know what you mean. Could you please attach an example project that my plugin and native TLA have different behavior?

I tested this:

// main.mjs
const sw = new SharedWorker(new URL("./worker.mjs", import.meta.url), { type: "module" });
sw.port.addEventListener("message", e => console.log(e));
sw.port.start();

// worker.mjs
self.addEventListener("connect", e => {
  console.log(e);
  const port = e.ports[0];
  setInterval(() => port.postMessage({ qwq: "qwq" }), 1000);
});

await new Promise(r => setTimeout(r, 10000));
console.log("resolved");

The compiled worker is:

(function () {
    'use strict';

    (async ()=>{
        self.addEventListener("connect", (e)=>{
            console.log(e);
            const port = e.ports[0];
            setInterval(()=>port.postMessage({
                    qwq: "qwq"
                }), 1000);
        });
        await new Promise((r)=>setTimeout(r, 10000));
        console.log("resolved");
    })();

})();

Its behavior is expected: "connect" event listener is attached immediately and message event is printed, before the long await resolves. 10 seconds after then, the long await resolves and it prints resolved.

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

No branches or pull requests

2 participants