Katkat

Run Promise Tasks with Bounded Concurrency

WaymoFrontend Engineer · Onsite

Last updated

Implement an asynchronous task runner in TypeScript that limits concurrent execution, handles synchronous throws and rejections gracefully, and returns settled results in the original task order.

Problem Statement

In simulation dashboards and autonomous vehicle teleoperation tools, frontend clients often need to fetch dozens of assets or telemetry chunks without overwhelming the browser's network pool or backend endpoints.

Implement a function mapConcurrentSettled that executes an array of asynchronous task factories with a strict upper bound on concurrency.

Requirements

  1. Validation: If concurrency is not an integer or is less than 1, reject immediately with a RangeError.
  2. Execution Cap: At any point in time, no more than concurrency task factories may be actively executing.
  3. Single Execution: Each task factory must be invoked exactly once.
  4. No Fail-Fast: Execution must continue until all tasks have settled. A failure in one task must not abort remaining tasks.
  5. Sync & Async Errors: A task factory that throws synchronously must be captured as a rejection rather than halting the runner.
  6. Order Preservation: The returned promise resolves to an array of PromiseSettledResult objects, ordered to match the original tasks input order.

Interface

type PromiseSettledResult =
  | { status: 'fulfilled'; value: T }
  | { status: 'rejected'; reason: unknown };

type TaskFactory = () => Promise | T;

function mapConcurrentSettled(
  concurrency: number,
  tasks: TaskFactory[]
): Promise[]>;

Constraints

  • tasks.length is between 0 and 10,000.
  • concurrency is an integer `1 new Promise((resolve) => setTimeout(() => resolve('A'), 50)), () => Promise.reject(new Error('Network error')), () => { throw new Error('Sync fail'); }, () => 'B' ];

const results = await mapConcurrentSettled(2, tasks); // Output: // [ // { status: 'fulfilled', value: 'A' }, // { status: 'rejected', reason: Error('Network error') }, // { status: 'rejected', reason: Error('Sync fail') }, // { status: 'fulfilled', value: 'B' } // ]


### Example 2

```typescript
await mapConcurrentSettled(0, [() => Promise.resolve(1)]);
// Rejection: RangeError("concurrency must be an integer >= 1")

Edge Cases

  • Empty tasks array: Resolve immediately to [].
  • concurrency >= tasks.length: All tasks start immediately.
  • Task returns a non-promise value (e.g. primitive T): Must be handled as a resolved promise.

Hints

Hint 1

Pre-allocate an output array of size tasks.length and track a shared nextIndex pointer that worker loops consume.

Hint 2

Wrap task execution inside Promise.resolve().then(task) to catch both synchronous throws and asynchronous promise rejections seamlessly.

Hint 3

Spawn exactly Math.min(concurrency, tasks.length) worker promises that repeatedly pull tasks until the task queue is exhausted.

Follow-up questions

  • How would you extend this scheduler to accept an AbortSignal to cancel pending tasks and stop launching new ones?
  • How would you write a deterministic unit test to prove concurrency never exceeded k without relying on fragile setTimeout delays?
  • How would you modify the API to support prioritized tasks where critical simulation events jump ahead in the queue?