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