TypeDrop

2026-05-27 Challenge

2026-05-27 Hard

Typed Retry-with-Backoff Fetch Orchestrator

You're building the resilient data-fetching layer for a financial trading dashboard. External market-data endpoints are flaky; your orchestrator must validate raw responses, retry failed requests with typed exponential back-off policies, fan out concurrent calls within a concurrency cap, and surface a strongly-typed per-endpoint result report — with zero `any`.

Goals

  • Implement branded-type construction (`makeEndpointUrl`) and runtime validation (`parseMarketQuote`) that narrows `unknown` to `MarketQuote` without type assertions.
  • Build a typed retry loop (`fetchWithRetry`) that applies exponential back-off, respects an `AbortSignal`, and correctly classifies every failure variant of `FetchError`.
  • Orchestrate concurrent fetches in fixed-size batches using `Promise.allSettled`, assembling a fully-typed `OrchestrationReport` keyed by URL string.
  • Aggregate the report into per-`AssetClass` statistics in a single pass and expose fulfilled URLs via a type-predicate guard — all under `strict: true` with zero `any`.
challenge.ts
// Key types and main orchestration signature

type EndpointUrl = string & { readonly __brand: "EndpointUrl" };
type AssetClass  = "equity" | "bond" | "crypto" | "commodity";

interface MarketQuote {
  readonly symbol: string;
  readonly price: number;
  readonly assetClass: AssetClass;
  readonly timestampMs: number;
}

type Ok<T>          = { readonly ok: true;  readonly value: T };
type Err<E extends string> = { readonly ok: false; readonly error: E; readonly detail: string };
type Result<T, E extends string> = Ok<T> | Err<E>;
type FetchError = "NETWORK_ERROR" | "TIMEOUT" | "INVALID_RESPONSE" | "RETRIES_EXHAUSTED";

type Fetcher = (url: EndpointUrl, signal: AbortSignal) => Promise<unknown>;

interface EndpointSpec { readonly url: EndpointUrl; readonly policy: BackoffPolicy; }

type EndpointOutcome =
  | { readonly status: "fulfilled"; readonly quote: MarketQuote }
  | { readonly status: "rejected";  readonly error: FetchError; readonly detail: string };

type OrchestrationReport = Readonly<Record<string, EndpointOutcome>>;

// Main entry point you must implement:
async function orchestrate(
  specs: ReadonlyArray<EndpointSpec>,
  fetcher: Fetcher,
  concurrencyLimit: number,
  outerSignal: AbortSignal
): Promise<OrchestrationReport> { /* TODO */ throw new Error(); }
Hints (click to reveal)

Hints

  • For `makeEndpointUrl`, the branded type can only be produced by returning a value that TypeScript already knows satisfies the brand — think about what the condition guarantees before you return.
  • In `fetchWithRetry`, a `Promise`-based sleep that also listens to an `AbortSignal` needs two racing paths inside `Promise.race`; one resolves after `setTimeout`, the other resolves/rejects when the signal fires.
  • For single-pass aggregation in `aggregateReport`, keep a running `{ sum, count, min, max }` accumulator per asset class inside a `Map` and compute `avgPrice` only at the end when writing the output record.

Or clone locally

git clone -b challenge/2026-05-27 https://github.com/niltonheck/typedrop.git