TypeDrop

2026-10-26 Challenge

2026-10-26 Hard

Typed Concurrent Task Scheduler with Retry & Cancellation

You're building the background job engine for a data-pipeline platform. Tasks arrive with priorities, dependencies, and retry policies — the scheduler must run them with a concurrency cap, propagate cancellation cleanly, and return a fully-typed result manifest that distinguishes successes, failures, and cancellations without a single `any` in sight.

Goals

  • Implement branded-type constructors (`toTaskId`, `toMs`) using a generic identity trick — no `as` allowed.
  • Implement `OutputTypeOf<T>` as a conditional type using `infer` to extract the output type from a `TaskDefinition<TOutput>`.
  • Implement `topologicalSort` using Kahn's BFS algorithm, detecting cycles and unknown dependency references.
  • Implement `schedule` to run tasks with a concurrency cap, per-task timeouts via `AbortController`, retry back-off, dependency-failure cancellation, `failFast` mode, and external signal propagation — returning a fully-typed `ScheduleManifest`.
challenge.ts

// Core domain types + main function signature

type TaskId = string & { readonly __brand: "TaskId" };
type Ms     = number & { readonly __brand: "Ms" };

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

interface TaskDefinition<TOutput> {
  readonly id:         TaskId;
  readonly priority:   "critical" | "high" | "normal" | "low";
  readonly dependsOn:  ReadonlyArray<TaskId>;
  readonly retry:      RetryPolicy;
  readonly timeoutMs:  Ms;
  readonly work:       (signal: AbortSignal) => Promise<TOutput>;
}

type TaskResult<TOutput> =
  | { status: "success";   id: TaskId; output: TOutput; attempts: number; durationMs: number }
  | { status: "failure";   id: TaskId; error: unknown;  attempts: number; durationMs: number }
  | { status: "cancelled"; id: TaskId; reason: "timeout" | "dependency-failed" | "aborted" };

type TaskRegistry = Record<string, TaskDefinition<unknown>>;

type ScheduleManifest<TRegistry extends TaskRegistry> = {
  readonly [K in keyof TRegistry]: TaskResult<OutputTypeOf<TRegistry[K]>>;
};

// Entry point — implement this!
async function schedule<TRegistry extends TaskRegistry>(
  registry: TRegistry,
  config: SchedulerConfig,
  signal?: AbortSignal
): Promise<ScheduleManifest<TRegistry>>
Hints (click to reveal)

Hints

  • For branded constructors without `as`: a function whose return type is declared as the branded type will let the compiler accept a plain `string`/`number` return — use a runtime validation guard and let the declared return type do the branding.
  • For `OutputTypeOf`, write: `type OutputTypeOf<T> = T extends TaskDefinition<infer O> ? O : never;`
  • Model the scheduler's inner loop as a recursive "drain" function: after each task settles, check which newly-unblocked tasks can enter the priority queue, then fill concurrency slots — `Promise.race` over in-flight tasks is your friend for the concurrency cap.

Or clone locally

git clone -b challenge/2026-10-26 https://github.com/niltonheck/typedrop.git