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.
Useful resources
Or clone locally
git clone -b challenge/2026-08-11 https://github.com/niltonheck/typedrop.git