Skip to content
easy-web-worker

Runtime workers

Create a worker from a function, with no worker file, and learn the one rule that comes with it.

GuideTypeScriptTry an example

A function as the worker

Give createWorker a function instead of a file. No extra file, no bundler configuration.

main.ts
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

scope.ts
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.

labels.ts
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.

text.ts
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.

numbers.ts
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.

worker.ts
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.

Documentation
↑↓ Navigate ↵ Open Esc Closeeasy-web-worker docs
Get started

Real Web Workers. Typed methods.
Cancelable promises.