TypeDrop
2026-07-27 Challenge
2026-07-27
Medium
Typed Retry & Concurrency-Limited Task Runner
You're building the job-execution engine for a data-pipeline orchestrator. Individual tasks are async functions that can fail transiently, so the runner must retry them with exponential back-off, enforce a global concurrency cap so the host isn't overwhelmed, and return a fully-typed settled report for every task — success value or final error — without losing the original task's return type.
Goals
- Implement `withRetry<T>` so it retries a failing task up to `maxAttempts` times with exponential back-off and always resolves with a `TaskReport<T>` (never rejects).
- Implement `runWithConcurrencyLimit<T>` so that at most `config.concurrency` tasks run simultaneously and results are returned in the original submission order.
- Define the `ReportFor<Tasks>` mapped type that converts a tuple of `Task<T>` types into a positionally-matched tuple of `TaskReport<T>` types.
- Use discriminated unions and type narrowing (`report.status === 'fulfilled'`) throughout — no `any`, `as`, or non-null assertions.
challenge.ts
/** A named async task that returns a value of type T. */
interface Task<T> {
readonly name: string;
readonly run: () => Promise<T>;
}
/** Discriminated union: fulfilled arm */
type TaskSuccess<T> = {
readonly status: "fulfilled";
readonly name: string;
readonly value: T;
};
/** Discriminated union: rejected arm */
type TaskFailure = {
readonly status: "rejected";
readonly name: string;
readonly attempts: number;
readonly error: unknown;
};
type TaskReport<T> = TaskSuccess<T> | TaskFailure;
interface RunnerConfig {
readonly maxAttempts: number;
readonly baseDelayMs: number;
readonly concurrency: number;
}
// Wraps one task with exponential-backoff retry — always resolves.
declare function withRetry<T>(task: Task<T>, config: RetryConfig): Promise<TaskReport<T>>;
// Runs all tasks with a concurrency cap — results in submission order.
declare function runWithConcurrencyLimit<T>(tasks: Task<T>[], config: RunnerConfig): Promise<TaskReport<T>[]>;
Hints (click to reveal)
Hints
- For the concurrency cap, spawn exactly `concurrency` 'worker' async functions that each loop over a shared index pointer — no external library needed.
- Exponential back-off: delay before retry N (1-indexed) is `baseDelayMs * 2^(N-1)`. Use the provided `sleep` helper.
- For `ReportFor`, map over the tuple with `[K in keyof Tasks]: Tasks[K] extends Task<infer T> ? TaskReport<T> : never` — TypeScript preserves tuple positions when you map over `keyof` of a tuple type.
Useful resources
Or clone locally
git clone -b challenge/2026-07-27 https://github.com/niltonheck/typedrop.git