TypeDrop

2026-08-03 Challenge

2026-08-03 Hard

Typed Concurrent Task Scheduler with Priority & Retry

You're building the background job engine for a data-pipeline platform. Tasks arrive with priorities, concurrency slots are limited, and flaky tasks must be retried with exponential back-off — all while the scheduler exposes a strongly-typed, generic result stream so callers never lose track of which task produced which outcome.

Goals

  • Implement `createScheduler` so it runs tasks with correct concurrency limits, priority ordering (desc), and all three retry policies (none / fixed / exponential back-off).
  • Make `globalTimeoutMs` race against the work loop and settle every pending or in-flight task as rejected with a `TimeoutError`.
  • Implement `extractFulfilled<T>` so that a user-supplied type guard narrows `unknown` results to `T[]` with no unsafe casts.
  • Implement the phantom-type `taskBuilder<T>()` fluent API so that calling `.build()` is a compile-time error unless both `.withPriority()` and `.withWork()` have been called in the chain.
challenge.ts
// Key types & main factory signature

export type TaskId = string & { readonly __brand: "TaskId" };
export type Priority = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10;

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

export interface Task<T> {
  readonly id: TaskId;
  readonly priority: Priority;
  readonly retry: RetryPolicy;
  readonly work: (attempt: number) => Promise<T>;
}

export type TaskResult<T> =
  | { status: "fulfilled"; id: TaskId; value: T;      attempts: number }
  | { status: "rejected";  id: TaskId; reason: unknown; attempts: number };

// Scheduler factory — implement this
export function createScheduler(config: SchedulerConfig): Scheduler { ... }

// Phantom-typed fluent builder — build() only appears after
// withPriority() AND withWork() have both been called
export function taskBuilder<T>(): TaskBuilder<T, { hasPriority: false; hasWork: false }> { ... }
Hints (click to reveal)

Hints

  • For the concurrency loop, maintain a `running` counter and a `next()` function that dequeues the highest-priority task and starts it only when `running < config.concurrency`; call `next()` again each time a task slot frees up.
  • For `globalTimeoutMs`, create a single `Promise<never>` that rejects with `TimeoutError` after the timeout, then `Promise.race` it against each individual task's work promise inside your retry loop — abort early and mark remaining queued tasks rejected if the race fires.
  • For the phantom-type builder, store state in a plain object literal and cast the return type of each `with*` method to the narrower `TaskBuilder<T, S & { hasPriority: true }>` intersection — the trick is that `build` lives on the *type* but the runtime object always has it; TypeScript simply hides it until the state constraint is met.

Or clone locally

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