TypeDrop

2026-02-27 Challenge

2026-02-27 Hard

Typed Paginated API Client with Retry & Concurrency

You're building a typed data-ingestion pipeline for an analytics platform. Remote REST endpoints return paginated results, requests can fail transiently, and multiple endpoints must be fetched in parallel — but with a concurrency cap to avoid hammering the servers.

Goals

  • Define a fully-typed discriminated-union Result<T,E> and a two-variant ApiError type, then implement ok() and err() constructor helpers.
  • Implement withRetry to transparently retry transient ApiErrors up to maxAttempts times while short-circuiting on permanent errors.
  • Implement fetchAllPages to sequentially follow the nextPage cursor and fetchWithConcurrencyLimit to run tasks in parallel with a sliding-window concurrency cap.
  • Wire all helpers together in ingestEndpoints, bridging the Result world into the FetchPage contract and returning partitioned successes and failures.
challenge.ts

// Core result type — discriminated union
export type Ok<T>  = { ok: true;  value: T };
export type Err<E> = { ok: false; error: E };
export type Result<T, E> = Ok<T> | Err<E>;

// Paginated API shape
export interface PagedResponse<T> {
  items:    T[];
  nextPage: number | null;
  total:    number;
}
export type FetchPage<T> = (page: number) => Promise<PagedResponse<T>>;

// Typed error hierarchy
export type ApiError =
  | { kind: "transient"; message: string; retryAfterMs: number }
  | { kind: "permanent"; message: string; statusCode: number };

// Main orchestrator signature
export async function ingestEndpoints<T>(
  endpoints: ReadonlyArray<EndpointConfig<T>>,
  concurrencyLimit: number,
): Promise<{
  successes: Array<{ id: string; items: T[] }>;
  failures:  Array<{ id: string; error: ApiError }>;
}>;
Hints (click to reveal)

Hints

  • For withRetry, narrow on both result.ok and error.kind — TypeScript will ensure the transient branch has retryAfterMs and the permanent branch has statusCode without extra casting.
  • fetchWithConcurrencyLimit is easiest with a running pool: kick off `limit` tasks immediately, then as each one settles, start the next unstarted task — track indices carefully to preserve output order.
  • In ingestEndpoints, you need to adapt FetchPage<T> (which returns a plain Promise) to work with withRetry (which expects () => Promise<Result<T,ApiError>>). Create a small local wrapper per page call that maps fetch errors to ApiError results.

Or clone locally

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