TypeDrop

2026-08-11 Challenge

2026-08-11 Hard

Typed Async Task Scheduler with Concurrency Limits & Retry

You're building the job-execution engine for a data-pipeline platform. Hundreds of heterogeneous tasks arrive at once, each with its own input/output type, priority, and retry policy — the scheduler must cap concurrent execution, retry failed tasks with typed error classification, and return a discriminated settlement record for every task so callers can exhaustively handle successes and failures without casting.

Goals

  • Implement `TaskSettlements<T>` as a mapped tuple type that uses `infer` to extract the output type `O` from each `Task<I, O>` element, producing `TaskSettlement<O>` per slot.
  • Implement `runScheduler` with a slot-based concurrency semaphore, priority sorting (high → medium → low), per-task AbortController timeout racing, and all three retry policy variants (none, fixed, exponential) while never retrying permanent errors.
  • Implement `handleSettlement` using `Extract<>` to narrow the discriminated union and dispatch to the correct typed handler without any casting.
  • Ensure the returned settlements are in the original task input order (not priority order), and that `attempts` accurately counts how many executions were made per task.
challenge.ts

// Key types at a glance
type TaskId = string & { readonly __brand: "TaskId" };

export type RetryPolicy =
  | { kind: "none" }
  | { kind: "fixed";       attempts: number; delayMs: number }
  | { kind: "exponential"; attempts: number; baseDelayMs: number; maxDelayMs: number };

export interface Task<I, O> {
  readonly id: TaskId;
  readonly input: I;
  readonly priority: "high" | "medium" | "low";
  readonly timeoutMs: number;
  readonly retryPolicy: RetryPolicy;
  readonly execute: (input: I, signal: AbortSignal) => Promise<O>;
  readonly classify: (thrown: unknown) => TaskError;
}

export type TaskSettlement<O> =
  | { status: "fulfilled"; taskId: TaskId; output: O;      attempts: number }
  | { status: "rejected";  taskId: TaskId; error: TaskError; attempts: number };

// The mapped tuple type YOU must implement:
export type TaskSettlements<T extends readonly Task<unknown, unknown>[]> = {
  [K in keyof T]: never; // ← replace never
};

// Main entry point:
export async function runScheduler<const T extends readonly Task<unknown, unknown>[]>(
  tasks: T,
  options: SchedulerOptions
): Promise<TaskSettlements<T>>
Hints (click to reveal)

Hints

  • For `TaskSettlements`, try `T[K] extends Task<infer _I, infer O> ? TaskSettlement<O> : never` inside the mapped type — `const T` preserves tuple indices so `keyof T` iterates positional slots.
  • For the concurrency semaphore, maintain a counter of active slots; when a slot frees up, pull the next task off the priority-sorted queue and start it — `Promise` chaining or a recursive drain loop both work.
  • For the timeout race, create an `AbortController`, call `controller.abort()` in a `setTimeout`, and `Promise.race([task.execute(input, controller.signal), timeoutPromise])` — make sure to clear the timer on success to avoid leaks.

Or clone locally

git clone -b challenge/2026-08-11 https://github.com/niltonheck/typedrop.git