TypeDrop

2026-09-17 Challenge

2026-09-17 Hard

Typed Paginated API Client with Retry, Cancellation & Result Aggregation

You're building the data-fetching core for an analytics dashboard that streams paginated reports from a REST API. Each page must be fetched with exponential-backoff retry, the entire operation must be cancellable via an AbortSignal, and every page's records must be aggregated into a single typed summary — with the compiler enforcing every fetch shape, error variant, and aggregation contract.

Goals

  • Implement `fetchPageWithRetry` with exponential-backoff retry, non-retryable error short-circuiting, and AbortSignal awareness — all fully typed.
  • Implement the pure `aggregateRecords` function that folds a flat record list into a per-region `ReportSummary` with correct averages.
  • Implement `fetchAllPages` to orchestrate sequential pagination, enforce the `maxPages` cap, thread the AbortSignal through every fetch, and short-circuit on any fetch failure.
  • Model all error variants as a discriminated union and ensure every branch is handled exhaustively — the compiler must reject any unhandled case.
challenge.ts
// Key types & main function signatures at a glance:

type PageCursor = Brand<string, "PageCursor">;

type FetchError =
  | { readonly kind: "NetworkError"; readonly message: string }
  | { readonly kind: "TimeoutError"; readonly afterMs: number }
  | { readonly kind: "ParseError"; readonly raw: string }
  | { readonly kind: "AbortedError" }
  | { readonly kind: "MaxRetriesExceeded"; readonly attempts: number; readonly lastError: FetchError };

type Result<T, E> =
  | { readonly ok: true;  readonly value: T }
  | { readonly ok: false; readonly error: E };

type PageFetcher = (cursor: PageCursor | null, signal: AbortSignal) => Promise<ReportPage>;

// TODO A — single-page fetch with exponential-backoff retry
async function fetchPageWithRetry(
  cursor: PageCursor | null,
  fetcher: PageFetcher,
  config: RetryConfig,
  signal: AbortSignal
): Promise<Result<ReportPage, FetchError>>;

// TODO B — pure aggregation of all collected records
function aggregateRecords(
  records: readonly ReportRecord[],
  pagesConsumed: number
): ReportSummary;

// TODO C — orchestrate full pagination and return aggregated summary
async function fetchAllPages(
  fetcher: PageFetcher,
  config: PaginatedClientConfig,
  signal: AbortSignal
): Promise<Result<ReportSummary, FetchError>>;
Hints (click to reveal)

Hints

  • For retry logic, catch the thrown FetchError from the PageFetcher and check its `kind` against `nonRetryableKinds` — a `ReadonlyArray.includes` call narrows cleanly here.
  • Use a single mutable `records` accumulator array and push each page's records in sequence inside `fetchAllPages`; pass the final array and page count to `aggregateRecords`.
  • To build `byRegion` in `aggregateRecords`, consider a `Map<ReportRecord['region'], ...>` for accumulation, then convert to a plain `Record` at the end — `Object.fromEntries` pairs well with `satisfies`.

Or clone locally

git clone -b challenge/2026-09-17 https://github.com/niltonheck/typedrop.git