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

RFC: WebWorker #212

Open
GrandSchtroumpf opened this issue Jan 24, 2025 · 2 comments
Open

RFC: WebWorker #212

GrandSchtroumpf opened this issue Jan 24, 2025 · 2 comments
Labels
[STAGE-2] incomplete implementation Remove this label when implementation is complete [STAGE-2] not fully covered by tests yet Remove this label when tests are verified to cover the implementation [STAGE-2] unresolved discussions left Remove this label when all critical discussions are resolved on the issue [STAGE-3] docs changes not added yet Remove this label when the necessary documentation for the feature / change is added [STAGE-3] missing 2 reviews for RFC PRs Remove this label when at least 2 core team members reviewed and approved the RFC implementation

Comments

@GrandSchtroumpf
Copy link

GrandSchtroumpf commented Jan 24, 2025

Champion

@GrandSchtroumpf

What's the motivation for this proposal?

Problems you are trying to solve:

  • Improve devX & capability to interact with workers
  • worker$ cannot be terminated
  • Cannot interact with long lasting web worker through postMessage
  • worker$ doesn't support streaming

Goals you are trying to achieve:

  • Create a low level API to interact with workers to create, run, close, terminate, postMessage and
  • Build utils on top of this low level API
  • Support streaming output

Any other context or information you want to share:


Proposed Solution / Feature

What do you propose?

A createWorker$(qrl) that can run the qrl inside a web worker and expose a low level API to interact with it :

const worker = createWorker$(function(params) => {
  this.onmessage(() => {
    // Do something in the worker when you receive a message
  });
  this.cleanup(() => {
    // Do something when worker is closed
  });
  // Post a message to the main thread
  this.postMessage();
  
  // If something is a function, it'll be used as cleanup, else it'll be send to the main thread
  return something;
});

// Open a webworker, run the QRL instead and return a ReadableStream with the value returned by the QRL
const result = await worker.create();

// Start listening on message from the worker
worker.onMessage$(() => );

// Post a message to the worker
await worker.postMessage('With love from the main thread');

// Stop running the current QRL, and trigger all cleanup. But keep the worker alive
await worker.close();

// Terminate the web worker
await worker.terminate();

Code examples

Add support for AbortController to existing worker$

const workerQrl = async (qrl) => {
  const worker = createWorkerQrl(qrl);
  return (params, { signal }) => new Promise(async (res, rej) => {
    const abort = () => { 
      worker.terminate();
      rej(signal.reason);
    }
    signal.addEventListener('abort', abort, { once: true })
    const stream = await worker.create(params);
    const { value } = await stream.getReader().read();
    res(value);
    await worker.terminate();
  })
}

Stream data from a worker:

const worker = createWorker$(async function*() {
  yield 1;
  await new Promise((res) => setTimeout(res, 1000));
  yield 2;
});
export default component$(() => {
  const counter = useSignal();
  useVisibleTask$(async () => {
    const stream = await worker.create();
    for await (const value of stream) counter.value = value;
  })
}) 

Compute inside a worker :

export default component$(() => {
  const counter = useSignal(0);
  const result = useSignal<number>();
  const worker = createWorker$(() => {
    return expensiveCalculation(counter.value);
  });
  // terminate worker when unmounted
  useVisbleTask$(() => worker.terminate);
 
  useVisibleTask$(async ({ track, cleanup }) => {
    track(counter);
    cleanup(worker.close);
    for await (const value of stream) result.value = value;
  })
})

Questions :

  1. Stream
    With this implementation we always return a ReadableStream from the create() method to have a native support of AsyncGenerators.
    This makes this low level API harder to work with, but since it's low level I thought it's ok.

A better solution might be a mix of Typescript & runtime code to know if the is an AsyncGenerator or not :

// createWorker$ knows the QRL returns an AsyncGenerator so create returns a Promise<ReadableStream>
const stream = await createWorker$(function*() {}).create();

// createWorker$ knows the QRL returns a value so `create` returns a Promise<T>
const result = await createWorker$(() => {}).create();

// createWorker$ knows the QRL returns a cleanup function so `create` returns a Promise<void>
await createWorker$(() => () => /* cleanup */).create();

Do you think we should always return Promise, or it should depends on the QRL output ?

  1. create
    Currently create open the webworker, run the qrl inside and returns the value if any.
    Do you think we should split the create that would open the webworker and apply that would run the QRL ?

PRs/ Links / References

No response

@github-project-automation github-project-automation bot moved this to In Progress (STAGE 2) in Qwik Evolution Jan 24, 2025
@github-actions github-actions bot added [STAGE-2] incomplete implementation Remove this label when implementation is complete [STAGE-2] not fully covered by tests yet Remove this label when tests are verified to cover the implementation [STAGE-2] unresolved discussions left Remove this label when all critical discussions are resolved on the issue [STAGE-3] docs changes not added yet Remove this label when the necessary documentation for the feature / change is added [STAGE-3] missing 2 reviews for RFC PRs Remove this label when at least 2 core team members reviewed and approved the RFC implementation labels Jan 24, 2025
@GrandSchtroumpf GrandSchtroumpf changed the title RFC: RFC: WebWorker Jan 24, 2025
@DustinJSilk
Copy link

Is there a way to achieve these goals but having Qwik do more of the heavy lifting in more of a "Qwik" kind of way?

What if there were 2 functions:

export default component(() => {
  const foo = useSignal(10)
  
  const computed = useComputedWorker$(() => foo.value * 100)
  
  useWorker$(({ track, cleanup }) => {
    track(computed)
  
    const id = setTimeout(() => foo.value++, 1000)

    cleanup(() => clearTimeout(id))
  })

  return <>{ foo.value }</>
})

Is this possible and are there any limitations with this approach?

@GrandSchtroumpf
Copy link
Author

Thanks for the feedback.
The goal behind this low-level API is to be able to build this kind of utils.
In an ideal world it would look like that

useWorker$

export const useWorkerQrl = (qrlFn) => {
  const worker = createWorkerQrl(qrlFn);
  useVisibleTask$(async (task)  => {
    task.cleanup(() => worker.terminate());
    await worker.create(task);
  });
}
export const useWorker$ = implicit$FirstArg(useWorkerQrl);

The technical limitations for this are:

  • task (track & cleanup) is neither serializable by Qwik not transferable by the browser. So it cannot be transfered and executed in the WebWorker
  • The cleanup function doesn't await. So the worker terminate after the next create happens (I've open a discussion for that Async cleanup in Task #209)

useComputedWorker$

export const useComputedWorkerQrl = (qrlFn) => {
  const worker = createWorkerQrl(qrlFn);
  useVisibleTask$(() => worker.terminate); // terminate when component is unmounted
  return useComputed$(() => worker.create());
}

The technical limitation for this is:

  • In v2 useComputed$ won't support async operation. Since worker/mainthread communication is async (postmessage/onmessage), we won't be able to run it

The closest API I was able to build are :

const worker = useWorker$(function() {
  const id = setTimeout(() => foo.value++, 1000);
  this.cleanup(() => clearTimeout(id)); // <-- cleanup in the webworker
}, {
  track: [foo], // <-- need to specify the track here to run in mainthread
});

and

const signal = useWorkerComputed$(() => foo.value * 100, { track: [foo] })

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
[STAGE-2] incomplete implementation Remove this label when implementation is complete [STAGE-2] not fully covered by tests yet Remove this label when tests are verified to cover the implementation [STAGE-2] unresolved discussions left Remove this label when all critical discussions are resolved on the issue [STAGE-3] docs changes not added yet Remove this label when the necessary documentation for the feature / change is added [STAGE-3] missing 2 reviews for RFC PRs Remove this label when at least 2 core team members reviewed and approved the RFC implementation
Projects
Status: In Progress (STAGE 2)
Development

No branches or pull requests

2 participants