TypeDrop

2026-07-20 Challenge

2026-07-20 Medium

Typed Retry-with-Backoff Task Queue

You're building the background job runner for a data-pipeline service. Tasks arrive with typed inputs and outputs, and each task must be retried up to a configurable limit on failure — using exponential backoff — before being marked as permanently failed. The queue must surface per-task results without losing successful jobs, and the whole run must respect a shared AbortSignal for graceful shutdown.

Goals

  • Define `TaskResult<T>` as a discriminated union and `Task<I, O>` / `RetryPolicy` / `QueueResult<I, O>` as fully-typed structures with no `any`.
  • Implement `runWithRetry` with exponential backoff, stopping early when the `AbortSignal` fires during a delay.
  • Implement `runQueue` to run all tasks concurrently via `Promise.all`, ensuring per-task failures never prevent other tasks from completing.
  • Return results in the same order as the input array, each paired with its task `id`, `input`, and `TaskResult`.
challenge.ts
// Key types and main function signatures

type TaskResult<T> =
  | { status: "fulfilled"; value: T }
  | { status: "failed"; attempts: number; lastError: Error };

type Task<I, O> = {
  id: string;
  input: I;
  execute: (input: I, signal: AbortSignal) => Promise<O>;
};

type RetryPolicy = {
  maxAttempts: number;
  baseDelayMs: number;
  backoffFactor: number;
};

type QueueResult<I, O> = { id: string; input: I; result: TaskResult<O> };

declare function runWithRetry<I, O>(
  task: Task<I, O>,
  policy: RetryPolicy,
  signal: AbortSignal,
): Promise<TaskResult<O>>;

declare function runQueue<I, O>(
  tasks: ReadonlyArray<Task<I, O>>,
  policy: RetryPolicy,
  signal: AbortSignal,
): Promise<ReadonlyArray<QueueResult<I, O>>>;
Hints (click to reveal)

Hints

  • The discriminant in `TaskResult<T>` lets TypeScript narrow the union — use `result.status === 'fulfilled'` to safely access `value`.
  • Wrap `abortableSleep` in a `try/catch` inside the retry loop: if it rejects with 'Aborted', return a failed result immediately rather than continuing.
  • In `runQueue`, map tasks to `runWithRetry` calls and wrap each in `Promise.all` — `runWithRetry` already resolves (never rejects), so `Promise.all` will always settle.

Or clone locally

git clone -b challenge/2026-07-20 https://github.com/niltonheck/typedrop.git