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