TypeDrop
2026-05-30 Challenge
2026-05-30
Hard
Typed Paginated API Cursor Engine
You're building the data-fetching layer for a large-scale analytics dashboard that must stream millions of records from a paginated REST API. Pages arrive as `unknown` JSON; your engine must validate them, thread opaque cursors through sequential fetches, enforce concurrency limits across multiple resource streams, and surface a strongly-typed per-resource aggregation report — with zero `any`.
Goals
- Implement `validateRawPage` to safely narrow `unknown` API responses into a typed `RawPage` using structural guards — no `as` or `any`.
- Implement `parsePage` to iterate raw record arrays, delegate payload validation, thread branded `Cursor` values, and collect partial errors without aborting.
- Implement `paginateStream` to fetch pages sequentially, advance the opaque cursor, honour `maxPages` and `delayMs`, and return a fully classified `ResourceReport`.
- Implement `runAggregation` to fan out multiple streams under a `concurrencyLimit`, collect all reports without letting one failure abort others, and compute the final `AggregationReport`.
challenge.ts
// Key types and main function signatures
export type Cursor = string & { readonly __brand: "Cursor" };
export type ResourceId = string & { readonly __brand: "ResourceId" };
export type FetchError =
| { kind: "network"; message: string }
| { kind: "validation"; field: string; message: string }
| { kind: "exhausted"; attempts: number };
export type Result<T> =
| { ok: true; value: T }
| { ok: false; error: FetchError };
export interface ResourceStream<TPayload> {
resourceId: ResourceId;
fetchPage: (cursor: Cursor | undefined) => Promise<unknown>;
validatePayload: (raw: unknown) => Result<TPayload>;
}
export interface PaginationPolicy {
maxPages: number;
delayMs: number;
concurrencyLimit: number;
}
// Validate raw unknown API response → typed RawPage
export function validateRawPage(raw: unknown): Result<RawPage> { /* TODO */ }
// Parse a validated page, delegating payload validation
export function parsePage<TPayload>(
rawPage: RawPage,
validatePayload: (raw: unknown) => Result<TPayload>
): ParsedPage<TPayload> { /* TODO */ }
// Paginate one stream sequentially, respecting policy
export async function paginateStream<TPayload>(
stream: ResourceStream<TPayload>,
policy: PaginationPolicy
): Promise<ResourceReport<TPayload>> { /* TODO */ }
// Fan out all streams with a concurrency cap, return full report
export async function runAggregation<TPayload>(
streams: ReadonlyArray<ResourceStream<TPayload>>,
policy: PaginationPolicy
): Promise<AggregationReport<TPayload>> { /* TODO */ }
Hints (click to reveal)
Hints
- For branded types (`Cursor`, `ResourceId`), the provided `makeCursor` / `makeResourceId` helpers are the only legal promotion points — use type predicates elsewhere to avoid `as`.
- A concurrency-pool without a library: maintain a `Set` of in-flight Promises, `await Promise.race(inFlight)` whenever the set reaches `concurrencyLimit`, then remove the resolved promise and add the next one.
- Thread the cursor through `paginateStream` with a `let cursor: Cursor | undefined = undefined` and update it to `page.nextCursor ?? undefined` after each page — the `null` sentinel is your loop exit condition.
Useful resources
Or clone locally
git clone -b challenge/2026-05-30 https://github.com/niltonheck/typedrop.git