Typed workers
How defineWorker and createWorker turn the functions of a worker into typed methods on the main thread.
The worker is an API
Every key you return from defineWorker becomes a method on the main thread.
import { defineWorker } from 'easy-web-worker/defineWorker';
const worker = defineWorker(() => ({
countWords: (text: string) => text.trim().split(/\s+/).length,
findDuplicates: (lines: string[]) =>
lines.filter((line, index) => lines.indexOf(line) !== index),
// no payload: called without arguments
version: () => '1.0.0',
}));
export type TextWorker = typeof worker;import { createWorker } from 'easy-web-worker/createWorker';
import workerUrl from './text.worker?worker&url';
import type { TextWorker } from './text.worker';
const text = createWorker<TextWorker>(workerUrl, {
workerOptions: { type: 'module' },
});
export async function analyze(article: string, lines: string[]) {
const words = await text.countWords(article); // number
const duplicates = await text.findDuplicates(lines); // string[]
const version = await text.version(); // string
return { words, duplicates, version };
}The contract between the threads lives next to the code that implements it. There is no second file of message types to keep in sync: change the worker, and the main thread stops compiling until it matches.
How the types cross the threads
| In the worker | On the main thread |
|---|---|
() => string | () => CancelablePromise<string> |
(value: number) => number | (payload: number, transfer?) => CancelablePromise<number> |
async (ids: string[]) => User[] | (payload: string[], transfer?) => CancelablePromise<User[]> |
(value?: number) => number | (payload?: number, transfer?) => CancelablePromise<number> |
A method receives one payload. To send several values, use an object or a tuple.
Return values and errors
What a method returns resolves the call. What it throws rejects it, and the worker keeps serving the next call.
import { defineWorker } from 'easy-web-worker/defineWorker';
const worker = defineWorker(() => ({
parse: (json: string) => {
if (!json) throw new Error('Empty input');
return JSON.parse(json) as unknown;
},
}));
export type ParserWorker = typeof worker;import { createWorker } from 'easy-web-worker/createWorker';
import workerUrl from './errors.worker?worker&url';
import type { ParserWorker } from './errors.worker';
const parser = createWorker<ParserWorker>(workerUrl, {
workerOptions: { type: 'module' },
});
export async function safeParse(json: string) {
try {
return await parser.parse(json);
} catch (error) {
// the Error thrown inside the worker, rebuilt on this side
console.log((error as Error).message); // 'Empty input'
return null;
}
}Values travel between threads with the
structured clone algorithm.
Objects, arrays, Map, Set, Date, typed arrays and Error cross; functions, DOM nodes and class instances
with methods do not.
State inside a worker
A worker stays alive between calls, so it can own state.
import { defineWorker } from 'easy-web-worker/defineWorker';
type User = { id: string; name: string };
// module scope of the worker: it lives as long as the worker does
const users = new Map<string, User>();
const worker = defineWorker(() => ({
save: (user: User) => {
users.set(user.id, user);
},
get: (id: string) => users.get(id) ?? null,
remove: (id: string) => users.delete(id),
}));
export type UsersWorker = typeof worker;With a pool, each Worker has its own memory. State kept in one is not visible to the others.
Two entry points
Import each side from its own entry, so the worker bundle carries no main-thread code.
| Entry | Use it in | Exports |
|---|---|---|
easy-web-worker/defineWorker | The worker file | defineWorker, unwrap, types |
easy-web-worker/createWorker | The main thread | createWorker, unwrap, types |
Everything is also exported from the root easy-web-worker entry.
What it replaces
The native API gives you messages, not functions. This is the least you write by hand to get one answer back:
const fibonacci = (n) => (n <= 1 ? n : fibonacci(n - 1) + fibonacci(n - 2));
self.onmessage = ({ data: { id, n } }) => {
self.postMessage({ id, result: fibonacci(n) });
};const worker = new Worker(new URL('./fibonacci.worker.js', import.meta.url));
const pending = new Map();
let nextId = 0;
worker.onmessage = ({ data: { id, result } }) => {
pending.get(id)(result);
pending.delete(id);
};
export const fibonacci = (n) =>
new Promise((resolve) => {
const id = nextId++;
pending.set(id, resolve);
worker.postMessage({ id, n });
});And that is before errors, cancellation, progress, types, or a second method.