Skip to content
easy-web-worker
Interactive example · Lifecycle

Watch the work, and stop it when you change your mind

A long prime search that reports how far it is, and stops inside the Worker the moment the call is canceled.

Try it hereLive example
Application preview

Loading the example…

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

/** How many numbers are checked before the worker pauses to listen for a cancellation. */
const CHUNK = 20_000;

const worker = defineWorker(({ onMessage }) => ({
  countPrimes: onMessage(async (limit: number, message) => {
    let found = 0;

    for (let candidate = 2; candidate <= limit; candidate++) {
      if (isPrime(candidate)) found += 1;

      if (candidate % CHUNK !== 0) continue;

      // a worker that never pauses cannot receive messages, including a cancellation
      await new Promise((resolve) => setTimeout(resolve));

      if (!message.isPending()) break;

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

    return found;
  }),
}));

export type PrimesWorker = typeof worker;
Ready. Run the example to begin.Reset returns this example to its initial state.
Take it with you

Progress and cancellation belong to the call. There is no second channel to build.

01

Press Start. The bar and the Primes found value are driven by `message.reportProgress` inside the worker.

02

Press Cancel while it runs. The status turns to canceled, and the loop in the worker stops instead of finishing for nobody.

03

Start it again. The same worker serves the new call; a canceled call leaves nothing behind.

Read the full guide

The code

Wrapping the method with onMessage gives it the message of the call as second parameter. That is where progress is reported and where the method learns it was canceled.

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

/** How many numbers are checked before the worker pauses to listen for a cancellation. */
const CHUNK = 20_000;

const worker = defineWorker(({ onMessage }) => ({
  countPrimes: onMessage(async (limit: number, message) => {
    let found = 0;

    for (let candidate = 2; candidate <= limit; candidate++) {
      if (isPrime(candidate)) found += 1;

      if (candidate % CHUNK !== 0) continue;

      // a worker that never pauses cannot receive messages, including a cancellation
      await new Promise((resolve) => setTimeout(resolve));

      if (!message.isPending()) break;

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

    return found;
  }),
}));

export type PrimesWorker = typeof worker;

On the main thread the call returns a CancelablePromise. onProgress and cancel are methods of that promise.

ProgressDemo.tsx
import { useEffect, useRef, useState } from 'react';
import { unwrap } from 'easy-web-worker/createWorker';
import '../shared/demo.css';
import { createLog } from '../shared/log';
import { count } from '../shared/format';
import { createPrimesWorker } from './client';

type Status = 'idle' | 'running' | 'done' | 'canceled';

const log = createLog();
const limits = [2_000_000, 8_000_000, 20_000_000] as const;

export function ProgressDemo() {
  const [limit, setLimit] = useState<(typeof limits)[number]>(8_000_000);
  const [status, setStatus] = useState<Status>('idle');
  const [progress, setProgress] = useState(0);
  const [found, setFound] = useState(0);
  const worker = useRef<ReturnType<typeof createPrimesWorker> | null>(null);
  const task = useRef<ReturnType<ReturnType<typeof createPrimesWorker>['countPrimes']> | null>(null);

  useEffect(
    () => () => {
      if (worker.current) void unwrap(worker.current).dispose();
    },
    [],
  );

  const start = () => {
    worker.current ??= createPrimesWorker();

    setStatus('running');
    setProgress(0);
    setFound(0);
    log.write(`countPrimes(${count(limit)}) started`);

    task.current = worker.current.countPrimes(limit).onProgress((percentage, details) => {
      setProgress(percentage);
      setFound((details as { found: number }).found);
    });

    task.current.then(
      (total) => {
        setProgress(100);
        setFound(total);
        setStatus('done');
        log.write(`resolved · ${count(total)} primes below ${count(limit)}`);
      },
      (reason) => {
        setStatus('canceled');
        log.write(`canceled · reason: "${String(reason)}"`);
      },
    );
  };

  const cancel = () => {
    task.current?.cancel('Canceled by user');
  };

  const running = status === 'running';

  return (
    <div className="demo">
      <section className="demo-card" aria-label="Prime search">
        <fieldset className="segmented">
          <legend>Count the primes below</legend>
          {limits.map((value) => (
            <label key={value}>
              <input type="radio" name="primes-limit" checked={limit === value} disabled={running} onChange={() => setLimit(value)} />
              <span>{count(value)}</span>
            </label>
          ))}
        </fieldset>
        <div className="demo-actions">
          <button type="button" className="demo-button demo-button--primary" onClick={start} disabled={running}>
            {running ? 'Counting…' : 'Start'}
          </button>
          <button type="button" className="demo-button" onClick={cancel} disabled={!running}>
            Cancel
          </button>
        </div>
      </section>

      <section className="demo-card" aria-label="Progress">
        <span>Progress reported by the worker</span>
        <div className="demo-progress" role="progressbar" aria-valuemin={0} aria-valuemax={100} aria-valuenow={Math.round(progress)}>
          <span style={{ width: `${progress}%` }} />
        </div>
        <dl className="demo-stats">
          <div className={`demo-stat ${status === 'done' ? 'demo-stat--good' : status === 'canceled' ? 'demo-stat--bad' : ''}`}>
            <dt>Status</dt>
            <dd data-testid="primes-status">{status}</dd>
          </div>
          <div className="demo-stat">
            <dt>Primes found</dt>
            <dd data-testid="primes-found">{count(found)}</dd>
          </div>
          <div className="demo-stat">
            <dt>Progress</dt>
            <dd data-testid="primes-progress">{Math.round(progress)}%</dd>
          </div>
        </dl>
        <p className="demo-caption">
          Cancel stops the loop inside the worker. The promise on the main thread rejects with the reason you passed.
        </p>
      </section>
    </div>
  );
}

export const resetProgressDemo = () => {};

export const watchProgressDemo = log.watch;

Related documentation

Keep exploring

Another piece of the model.

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

Real Web Workers. Typed methods.
Cancelable promises.