Skip to content

RFC: WebWorker #212

@GrandSchtroumpf

Description

@GrandSchtroumpf

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    Status

    In Progress (STAGE 2)

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions