webworker-async
v1.0.1
Published
Superset of web workers that can export functions to be called as async
Maintainers
Readme
webworker-async
A superset of standard JavaScript web workers with a mechanism to export functions and call them as asynchronous.
// worker.js
import { worker_export } from "webworker-async/worker"
worker_export({
// a slow way to calculate pi
expensive_function: () => new Promise(resolve => setTimeout(() => resolve(Math.PI), 1000));
});// main.js
import { AsyncWorker } from "webworker-async"
let worker_src = new URL("worker.js", import.meta.url);
let worker = AsyncWorker(worker_src, { type: "module" });
let pi = await worker.expensive_function();
worker.terminate();Async workers extend workers with a super-light asynchronous RPC implementation, which makes worker functions as convenient to use as standard async functions.
Usage
Install from npm:
npm install webworker-asyncThis package depends on the web-worker npm package, so it's also compatible with Node.js. Generator functions are currently not supported.
Create an AsyncWorker with the same syntax and options as standard workers, plus any functions you want to expose to it (but without the new keyword).
You can use async workers exactly like standard
web-workerworkers by not importing or exporting functions, and they will provide extra type safety and parameter documentation. This use-case is supported from typescript withAsyncWorker<{}>.
Exporting and calling worker functions
Workers can expose functions to their owners via the provided worker_export function. The export function may be called multiple types, but will exports with the same name will be overwritten.
Exports with the names of standard worker properties (terminate, addEventListener, ...) will not be callable, as those properties are still accessible from the worker object.
Worker functions may be called with any value supported by the structured clone algorithm (meaning functions and symbols, among some others, cannot be passed as arguments to exposed functions).
When called from the worker's owner, the function will return a Promise exactly like a regular asynchronous function. The promise will reject if the function is not defined or throws an error from the worker.
// calculating pi concurrently with a fallback approximation
// (not a very good one, remember calculating pi is expensive!)
let pi_label = "loading π...";
worker.expensive_function()
.then((result) => pi_label = `π = ${result}`)
.catch(() => pi_label = "π ≈ 3.2");Exposing functions to workers
For full bi-directional communication, workers may be exposed to a set of functions to call from the owner. These are passed alongside the worker options at creation time, but the object may be extended afterwards.
these functions will be available from the worker via the worker_import function.
import { AsyncWorker } from "webworker-async"
const worker_src = new URL(/* ... */);
const events = [];
const worker = AsyncWorker(worker_src, { type: "module", imports: {
append_event: (data) => events.push(data);
}});// worker.js, now with imports!
import { worker_import } from "webworker-async/worker"
const { append_event } = worker_import();
append_event({ type: "test", message: "hello world" })
.catch(error => console.error("Something went wrong"));Type safety
When called from typescript, AsyncWorker and worker_import are optionally generic over the exposed functions. They will automatically wrap the provided functions to be asynchronous.
Additionally, worker_export will return the object passed to it as a convenient way to export its type. Below is an example of these techniques used to achieve type safety:
// calculator.worker.ts
export type Calculator = typeof exports;
const exports = worker_export({
add: (a: number, b: number) => a + b,
subtract: (a: number, b: number) => a - b,
});// main.ts
import type { Calculator } from "./calculator.worker"
let worker = AsyncWorker<Calculator>(/* ... */);
// worker now has autocomplete for add and subtract as well as the standard worker properties, and nothing else!Abstraction leaks
Async workers are fully compatible with standard workers, including the ability to simultaneously use the standard messages and events if ever required.
To ensure your additional event listeners don't respond to worker function events (or to extend specifically worker function calls), you can use the provided is_async_event helper.
// worker.js with exported function and extended functionality
import { worker_export, is_async_message } from "webworker-async/worker";
worker_export({ fn: () => {} });
addEventListener("message", (event) => {
if(is_async_event(event)) {
// Log any calls to exported functions
if(event.data.type === "call") {
console.debug(`Main thread called ${event.data.function_name}`);
}
} else {
// Echo any non-async messages
postMessage(event.data);
}
});