Runtime workers
Create a worker from a function, with no worker file, and learn the one rule that comes with it.
A function as the worker
Give createWorker a function instead of a file. No extra file, no bundler configuration.
import { createWorker } from 'easy-web-worker/createWorker';
export const math = createWorker(({ onMessage }) => {
const fibonacci = (n: number): number =>
n <= 1 ? n : fibonacci(n - 1) + fibonacci(n - 2);
return {
fibonacci,
countTo: onMessage(async (limit: number, message) => {
for (let step = 1; step <= limit; step++) {
message.reportProgress((step * 100) / limit);
}
return limit;
}),
};
});
// both sides are in the same file, so there is no type to import
export const result = math.fibonacci(40); // CancelablePromise<number>The function receives onMessage, returns the methods, and their types are inferred. Everything else works the
same as with a worker file: cancellation, progress, transferable objects, unwrap, and pools with maxWorkers.
The one rule: nothing from outside
import { createWorker } from 'easy-web-worker/createWorker';
const greeting = 'Hello';
export const broken = () =>
createWorker(() => ({
// `greeting` does not exist inside the worker: this throws when it is called
greet: (name: string) => `${greeting} ${name}`,
}));
export const working = () =>
createWorker(() => {
// defined inside the function, so it travels with it
const greeting = 'Hello';
return {
greet: (name: string) => `${greeting} ${name}`,
};
});Everything the worker needs must be defined inside the function, imported as a script, sent as a payload, or
passed through primitiveParameters.
Because the text is what travels, write the function as an arrow function or a function declaration. A method
written with object shorthand, or a bound function, does not produce source that can be reused.
Parameters and the scope of the worker
The second parameter is the global scope of the worker. Static values arrive in context.primitiveParameters.
import { createWorker } from 'easy-web-worker/createWorker';
export const labels = createWorker(
(_helpers, context) => {
// the second parameter is the global scope of the worker
const [prefix] = context.primitiveParameters;
return {
label: (text: string) => `${prefix} ${text}`,
};
},
{
primitiveParameters: ['Result:'] as [string],
},
);
export const labeled = labels.label('complete'); // 'Result: complete'Import scripts
External libraries load with the scripts option and appear in the scope of the worker.
import { createWorker } from 'easy-web-worker/createWorker';
export const text = createWorker(
(_helpers, context) => {
// the script adds `Diff` to the scope of the worker
const { diffWords } = context.Diff as {
diffWords: (before: string, after: string) => unknown[];
};
return {
compare: ({ before, after }: { before: string; after: string }) => diffWords(before, after),
};
},
{
scripts: ['https://cdn.jsdelivr.net/npm/diff@9.0.0/dist/diff.min.js'],
},
);The script does not become part of your application bundle.
Compose a worker from several functions
Pass an array and every function runs in the same Worker. They share its scope, and the methods they return are merged. When a method is repeated, the last function wins, in behavior and in types.
import { createWorker } from 'easy-web-worker/createWorker';
export const numbers = createWorker([
// a reusable piece: leaves a helper in the scope of the worker
(_helpers, context) => {
context.round = (value: number) => Math.round(value * 100) / 100;
},
() => ({
double: (value: number) => value * 2,
describe: () => 'first',
}),
(_helpers, context) => {
const round = context.round as (value: number) => number;
return {
average: (values: number[]) =>
round(values.reduce((total, value) => total + value, 0) / values.length),
// a repeated method: the last function wins
describe: () => 'last',
};
},
]);Write the array inline, or declare it as const, so each function keeps its own type.
The message API inside the function
The function also receives easyWorker, the worker instance behind the methods. Everything the
message API offers stays available, so existing runtime workers can move over at their own
pace.
import { createWorker } from 'easy-web-worker/createWorker';
// the methods registered through easyWorker are described in the generic
export const worker = createWorker<{
double: (value: number) => number;
triple: (value: number) => number;
}>(({ easyWorker }) => {
// a named handler, message style
easyWorker.onMessage<number, number>('double', (message) => {
message.resolve(message.payload * 2);
});
// the handler for messages sent without a method
easyWorker.onMessage((message) => {
message.resolve();
});
return {
triple: (value: number) => value * 3,
};
});Only the returned methods are inferred. Handlers registered through easyWorker are described in the generic,
as above, or called with unwrap(worker).sendToMethod(...).
When to prefer a file
A worker file can import modules, be split by your bundler and be tested on its own. Runtime workers fit small, self-contained jobs. Both share the same features, so moving from one to the other does not change the code that calls them.
Try it
The worker without a file example analyzes text as you type.