Make web workers fun!
This tiny library allows you use web workers via asynchronous proxy functions! No more struggling with mesages and event handlers - all this is done automatically. Exposed worker functions return promises resolving with their results.
- Full typescript support
- Bundler independent
- Perfect with async/await
- Tiny size
This was written as an alternative to workerize-loader which currently lacks support for webpack 5. The code is tiny and contained in a single file, I encourage you to check it out!
Using this library is simple, all you need is two functions!
-
Use
expose()
at the end of your worker to make functions callable from the master thread. This takes in an object whose entries are functions, these can be async. -
Load the worker in your script however you like, e.g. worker-loader
-
Create a new instance of the worker and pass it into
wrap()
to create the wrapper object -
You can now call worker functions "directly" using the wrapper! E.g.
const result = await wrapper.someFunction()
. They will all return promises which resolve with the result or reject with an error if one occurs. You can also usewrapper.terminate()
to terminate the worker
See the example below:
worker.ts
import { expose } from './promise-worker';
function add(a: number, b: number) {
return a + b;
}
function multiply(a: number, b: number) {
return a * b;
}
expose({ add, multiply });
// Export the type for type checking
const workerFunctions = { add, multiply };
type WorkerFunctions = typeof workerFunctions;
export type { WorkerFunctions };
index.ts
import { wrap } from './promise-worker';
// You may load the worker as you like, I use worker-loader as it requires no configuration
import DemoWorker from 'worker-loader!./worker';
import type { WorkerFunctions } from './worker';
async function run() {
// Create the worker & wrapper
const wrapper = await wrap<WorkerFunctions>(new DemoWorker());
// Use the worker
const a = 2,
b = 5;
const added = await wrapper.add(2, 5);
const multiplied = await wrapper.multiply(2, 5);
console.log(`${a} + ${b} = ${added}`);
console.log(`${a} * ${b} = ${multiplied}`);
// Terminate the worker
wrapper.terminate();
}
void run();
The code is small so understanding it with a look through should be enough. I'm also not great at explaining :/ But here's an explanation none the less:
The basic idea is: You can access properties of an object by name house["price"]
so you can call functions within an object by name. Our requests to the worker can contain a function name action
to call and array of arguments to supply payload
. The worker can then call the requested function with the the given arguments, and post the result back. We can add a try catch statement (in the worker) to detect errors, so our response has a type type = "success" | "error"
and value payload
. Finally we add a unique id
property to the requests and responses to be able to match the two.
expose()
adds an onmessage event listener to the worker which does just this. But how do we create the proxy functions for the wrapper? Firstly using the id feature, the wrapper can keep track of the requests as promises - create Promise and post request with id, when response with matching id is found resolve (or reject) the corresponding promise with the result. createJob()
(line 130) creates and returns these promises. The wrapper must know what functions are available in the worker, so expose()
also adds a getFunctionality
function which returns a list of all exposed function names. The wrap()
factory function requests this, then for each entry creates a corresponding proxy function in the wrapper which simply calls createJob()
(line 179). Finally we add a terminate function to the wrapper and return it.
Now we can just call the proxy functions and get a promise back, leaving the wrapper to handle the actual interaction with the worker.
@gone-skiing @TobbeLundberg
Shoot, sorry I missed that first comment. The licence for this is the unlicense license. I've added
UNLICENSE.txt
to the gist. Hope that helps!