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