Katkat

Name-Keyed Async Task Scheduler with Sequential Lanes, Cancel and Clear by Name

AirwallexFrontend Engineer · Technical Screen

Last updated

Implement a JavaScript task scheduler for async functions where tasks sharing a name run strictly in order while tasks with different names run concurrently. Follow-ups add cancelling the running task for a name and clearing every running and queued task by name, testing promise handling, race conditions and API design.

Create a task scheduler for asynchronous functions, built in the style of an event emitter. Each task is submitted under a name together with an async function. The scheduler must satisfy two rules:

  • Tasks with the same name execute one after another: a task starts only after the previous task with that name has finished, and tasks with the same name run in the order they were scheduled.
  • Tasks with different names execute concurrently and independently: a slow or failing task under one name never delays or affects tasks under another name.

Implement it in JavaScript or TypeScript. One possible starting interface, which you may extend if you explain why:

interface TaskScheduler {
 schedule(name: string, task: () => Promise<unknown>): void;
 on(event: string, listener: (payload: unknown) => void): void;
}

The interview continued with two follow-ups, given below as Parts 2 and 3.

Clarifying Questions

  • Should schedule also return a promise for the task's result, or are results reported only through events? Which lifecycle events should the emitter publish?
  • If a task rejects or throws, should the next task with the same name still run?
  • Is there any limit on how many names may run at the same time?
  • Can a task call back into the scheduler, for example to schedule another task under its own name?

Part 1 — Same-name order, cross-name concurrency

Implement schedule and the event interface so that both rules hold. Show exactly how the next task for a name gets started, including when the current task fails.

What This Part Should Cover

  • Strict per-name ordering with no overlap, and full independence across names
  • Starting the next task after both success and failure, including a synchronous throw
  • Cleaning up per-name state once a name has nothing left to run
  • How results and errors reach the caller: a returned promise, emitted events, or both

Part 2 — Cancel the running task by name

Add cancel(name): cancel the task currently running under name and move on to the next scheduled task with that name.

Clarifying Questions for this Part

  • Must cancelling stop the task's actual work, or only stop the scheduler from waiting for it?
  • What should the code that scheduled the cancelled task observe?

What This Part Should Cover

  • The precise meaning of cancellation, and how a cooperative task learns about it
  • Protection against the cancelled task's late settlement
  • What the cancelled task's caller and the event listeners observe
  • Behavior when nothing is running under that name

Part 3 — Clear all tasks by name

Add clear(name): clear all running and scheduled tasks with that name.

What This Part Should Cover

  • Discarding the waiting tasks and settling or reporting each of them
  • Cancelling the running task by reusing the Part 2 logic, in the right order
  • Tasks scheduled during or after the clear
  • Releasing the name's state afterwards

Hints

Hint 1

State per name: Think about what you need to remember for each name to know whether something is already running, and what event should trigger the next task for that name.

Hint 2

Failures must not stall the line: Decide what happens to the waiting tasks of a name when the running one rejects, and make sure that code path still starts the next task.

Hint 3

You cannot stop a promise: A running async function cannot be stopped from outside. Think about what the scheduler can do on its own side, and what it could offer the task so the task can stop itself.

Hint 4

The late finisher: The cancelled task's promise may still settle after the next task has started. Make sure that late settlement cannot advance the queue a second time or report a result.

Hint 5

Clear versus cancel: Compare which parts of the per-name state each operation touches, and decide whether a task scheduled right after a clear should run.

Hint 6

Nobody left waiting: Every discarded task had a caller waiting on it. Decide how each of them learns that its task will never run.

Follow-up questions

  • How would you add a global limit on the number of tasks running at once across all names, while keeping per-name order?
  • How would you add a timeout per task, and how does it interact with `cancel`?
  • How would you write deterministic unit tests for these rules without relying on real timers?
  • What changes if failed tasks must be retried with backoff before the next task with the same name starts?