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.

Or clone locally

git clone -b challenge/2026-05-30 https://github.com/niltonheck/typedrop.git