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