Skip to content
easy-web-worker

Cancellation and progress

Report progress from a worker, cancel a call from either thread, and stop work that never pauses.

GuideTypeScriptTry an example

Progress on the same call

Every call returns a CancelablePromise. Progress and cancellation belong to that promise, not to a second channel.

main.ts
import { createWorker } from 'easy-web-worker/createWorker';
import workerUrl from './primes.worker?worker&url';
import type { PrimesWorker } from './primes.worker';

export const math = createWorker<PrimesWorker>(workerUrl, {
  workerOptions: { type: 'module' },
});

export function search(progressBar: HTMLProgressElement, cancelButton: HTMLButtonElement) {
  const task = math.findPrimes(50_000_000).onProgress((percentage) => {
    progressBar.value = percentage;
  });

  cancelButton.onclick = () => task.cancel('Canceled by user');

  return task;
}

Inside the worker, the message of the call reports the progress:

math.worker.ts
import { defineWorker } from 'easy-web-worker/defineWorker';

const isPrime = (candidate: number) => {
  for (let divisor = 2; divisor * divisor <= candidate; divisor++) {
    if (candidate % divisor === 0) return false;
  }

  return candidate > 1;
};

const worker = defineWorker(({ onMessage }) => ({
  findPrimes: onMessage(async (limit: number, message) => {
    const primes: number[] = [];

    for (let candidate = 2; candidate <= limit; candidate++) {
      if (isPrime(candidate)) primes.push(candidate);

      if (candidate % 100_000 !== 0) continue;

      // give the worker a moment to receive a cancellation
      await new Promise((resolve) => setTimeout(resolve));

      if (!message.isPending()) break;

      message.reportProgress((candidate / limit) * 100);
    }

    return primes;
  }),
}));

export type PrimesWorker = typeof worker;

reportProgress(percentage, payload?) can also carry a value. It arrives as the second argument of the onProgress callback.

Cancellation reaches the worker

task.cancel(reason) sends the cancellation to the worker, and the promise rejects with that reason once the worker confirms. Inside the worker there are two ways to notice it:

  • message.isPending() becomes false. A loop can check it between chunks, as above.
  • message.onCancel(callback) runs the callback. Use it to abort requests and release resources.
search.worker.ts
import { defineWorker } from 'easy-web-worker/defineWorker';

const worker = defineWorker(({ onMessage }) => ({
  search: onMessage(async (query: string, message) => {
    const controller = new AbortController();

    // called when the main thread cancels the call
    message.onCancel(() => controller.abort());

    const response = await fetch(`/api/search?q=${encodeURIComponent(query)}`, {
      signal: controller.signal,
    });

    return (await response.json()) as string[];
  }),
}));

export type SearchWorker = typeof worker;

Cancellation is idempotent: canceling twice sends one request to the worker, and canceling a call that already finished does nothing.

A worker can cancel its own call

validator.worker.ts
import { defineWorker } from 'easy-web-worker/defineWorker';

const worker = defineWorker(({ onMessage }) => ({
  validate: onMessage((input: string, message) => {
    if (!input) {
      // the call is rejected on the main thread with this reason
      message.cancel('Nothing to validate');

      return false;
    }

    return input.length > 3;
  }),
}));

export type ValidatorWorker = typeof worker;

The main thread sees a rejection with the reason, the same as when it cancels the call itself.

Stop work that never pauses

For work that cannot pause, restart the worker instead:

controls.ts
import { unwrap } from 'easy-web-worker/createWorker';
import { math } from './primes-main';

export async function stopEverything() {
  // asks every pending call to cancel, and waits for the worker to confirm
  await unwrap(math).cancelAll('Leaving the page');
}

export async function restart() {
  // terminates the Workers, rejects the pending calls, starts fresh ones
  await unwrap(math).cancelAll('Taking too long', { force: true });
}

force: true terminates the Workers, rejects every pending call and starts fresh ones. A worker created from an existing native Worker cannot be restarted by the library; see Creating workers.

Try it

The progress and cancellation example runs this code with a real Worker.

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

Real Web Workers. Typed methods.
Cancelable promises.