Skip to content
easy-cancelable-promise

Groups and concurrency

Run a list of tasks with groupAsCancelablePromise, limit how many run at once, follow their progress and cancel all of them with one call.

GuideTypeScriptTry an example

One promise for many tasks

groupAsCancelablePromise takes a list of tasks and returns one CancelablePromise that resolves with all the results. A task is a function that returns a promise, or a promise that is already running.

basic.ts
import { CancelablePromise, groupAsCancelablePromise } from 'easy-cancelable-promise';

const fetchUser = (id: number) =>
  new CancelablePromise<string>((resolve, _reject, { onCancel }) => {
    const timer = setTimeout(() => resolve(`user ${id}`), 10);

    onCancel(() => clearTimeout(timer));
  });

export async function basic() {
  // functions are called when their turn arrives; promises are already running
  const group = groupAsCancelablePromise<string[]>(
    [() => fetchUser(1), () => fetchUser(2), () => fetchUser(3), () => fetchUser(4), () => fetchUser(5)],
    { maxConcurrent: 2 }, // only 2 at a time
  );

  // an empty list returns null
  if (!group) return [];

  group.onProgress((percentage) => console.log(`${percentage}% complete`)); // 20, 40, 60, 80, 100

  return group; // ['user 1', 'user 2', 'user 3', 'user 4', 'user 5'], in the order of the tasks
}
  • A function is called when its turn arrives. That is what makes the concurrency limit work.
  • The results keep the position of their tasks, no matter which one finishes first.
  • The group reports its progress: the percentage of tasks completed.

Cancel the group

One call cancels the tasks that are running and leaves the tasks in the queue unstarted.

cancel.ts
import { CancelablePromise, groupAsCancelablePromise } from 'easy-cancelable-promise';

export async function cancelGroup() {
  const started: number[] = [];
  const stopped: number[] = [];

  const task = (id: number) => () => {
    started.push(id);

    return new CancelablePromise<number>((resolve, _reject, { onCancel }) => {
      const timer = setTimeout(() => resolve(id), 5000);

      onCancel(() => {
        clearTimeout(timer);
        stopped.push(id);
      });
    });
  };

  const group = groupAsCancelablePromise<number[]>([task(1), task(2), task(3), task(4)], { maxConcurrent: 2 })!;

  // one call: the running tasks are canceled, the tasks in the queue never start
  group.cancel('User navigated away');

  const reason = await group.catch((error) => error);

  return { started, stopped, reason, status: group.status };
  // { started: [1, 2], stopped: [1, 2], reason: 'User navigated away', status: 'canceled' }
}

It also works the other way: if one task is canceled, the group is canceled, and that cancels the rest.

When a task fails

If a task is rejected, the group is rejected with the same reason and no more tasks are started. Tasks that are already running are not canceled.

errors.ts
import { groupAsCancelablePromise } from 'easy-cancelable-promise';

export async function errors() {
  const started: string[] = [];

  const task = (name: string, fails = false) => () => {
    started.push(name);

    return fails ? Promise.reject(new Error(`${name} failed`)) : Promise.resolve(name);
  };

  const group = groupAsCancelablePromise<string[]>([task('a'), task('b', true), task('c'), task('d')], {
    maxConcurrent: 2,
  })!;

  // the group rejects with the reason of the first task that failed
  const reason = await group.catch((error) => (error as Error).message);

  return { reason, started, status: group.status };
  // { reason: 'b failed', started: ['a', 'b'], status: 'rejected' }: the tasks in the queue never started
}

Configuration

config.ts
import { groupAsCancelablePromise } from 'easy-cancelable-promise';

export async function config() {
  const events: string[] = [];
  const step = (name: string) => () => Promise.resolve(name);

  const results = await groupAsCancelablePromise<string[]>([step('a'), step('b'), step('c')], {
    // how many tasks run at the same time (default: 8)
    maxConcurrent: 2,

    // start each task only after the previous one finished (default: false)
    executeInOrder: false,

    // called before each task starts, with the position of the task
    beforeEachCallback: (index) => events.push(`starting ${index}`),

    // called after each task that succeeded, with its result and its position
    afterEachCallback: (result, index) => events.push(`finished ${index}: ${String(result)}`),

    // called once, when every task succeeded, with all the results
    onQueueEmptyCallback: (all) => events.push(`done: ${String(all)}`),
  });

  return { results, events };
  // results: ['a', 'b', 'c']
  // events: ['starting 0', 'starting 1', 'finished 0: a', 'finished 1: b', 'starting 2', 'finished 2: c', 'done: a,b,c']
}
OptionDefaultPurpose
maxConcurrent8How many tasks run at the same time. Values below 1 behave as 1
executeInOrderfalseStarts each task only after the previous one finished
beforeEachCallbacknoneCalled before each task starts, with the position of the task
afterEachCallbacknoneCalled after each task that succeeded, with its result and position
onQueueEmptyCallbacknoneCalled once, when every task succeeded, with all the results

Tasks run in batches of maxConcurrent: the next batch starts when every task of the current one finished.

One at a time, in order

sequential.ts
import { groupAsCancelablePromise } from 'easy-cancelable-promise';

export async function sequential() {
  const order: string[] = [];

  const step = (name: string, milliseconds: number) => () =>
    new Promise<string>((resolve) => {
      order.push(`start ${name}`);

      setTimeout(() => {
        order.push(`end ${name}`);
        resolve(name);
      }, milliseconds);
    });

  // each task starts only after the previous one finished
  const results = await groupAsCancelablePromise<string[]>([step('one', 20), step('two', 5), step('three', 1)], {
    executeInOrder: true,
  });

  return { results, order };
  // results: ['one', 'two', 'three']
  // order: ['start one', 'end one', 'start two', 'end two', 'start three', 'end three']
}

Next

Documentation
↑↓ Navigate ↵ Open Esc Closeeasy-cancelable-promise docs
Get started

Promises you can actually control.