Cancellation and progress
Report progress from a worker, cancel a call from either thread, and stop work that never pauses.
Progress on the same call
Every call returns a CancelablePromise. Progress and cancellation belong to that promise, not to a second
channel.
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:
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()becomesfalse. A loop can check it between chunks, as above.message.onCancel(callback)runs the callback. Use it to abort requests and release resources.
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
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:
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.