# Pagination Helpers _(no submodules)_ ## Purpose The helpers module provides higher-order utilities for consuming paginated APIs: async iterators for offset and cursor strategies, collection helpers that drain iterators into arrays, a mapping helper, an emptiness check, and a merge utility for combining pre-fetched pages. All iterators support `AbortSignal` so long-running collection passes can be cancelled between page fetches without leaving hanging promises. These helpers work with any function that returns an `OffsetPaginatedResponse` or `CursorPaginatedResponse` — they are not tied to a specific adapter or data source. ## Features - `createOffsetIterator` — `AsyncIterable>` driven by an offset fetcher function. - `createCursorIterator` — `AsyncIterable>` driven by a cursor fetcher function. - `collectAll` — drain an iterable into a flat `T[]`; abortable. - `collectUpTo` — drain up to N items; stops early; abortable. - `mapPaginated` — map items across all pages, returning a flat `U[]`. - `isPaginatedResponseEmpty` — check whether a response has zero items. - `mergeOffsetResponses` — combine sequential pages into a single `{ data, total }`. ## Usage: Basic Example ```typescript import { createOffsetIterator, collectAll } from "@suaveplan/pagination/helpers"; import { paginateArray } from "@suaveplan/pagination/offset"; const items = Array.from({ length: 25 }, (_, i) => i + 1); const iter = createOffsetIterator( (page, limit) => Promise.resolve(paginateArray(items, page, limit)), { limit: 10 } ); const all = await collectAll(iter); // → [1, 2, ..., 25] ``` ## Usage: Advanced Example ```typescript import { createCursorIterator, collectUpTo, mapPaginated, } from "@suaveplan/pagination/helpers"; // Abort after 2 seconds const controller = new AbortController(); setTimeout(() => controller.abort(), 2000); const iter = createCursorIterator( async (cursor, limit) => fetchUsersPage(cursor, limit), { limit: 50, signal: controller.signal } ); // Collect at most 200 users const users = await collectUpTo(iter, 200, controller.signal); // Map without collecting all pages first const names = await mapPaginated( createCursorIterator(async (c, l) => fetchUsersPage(c, l), { limit: 100 }), (u) => u.name ); ``` ## API Reference ### `createOffsetIterator(fetcher, opts?): AsyncIterable>` | Parameter | Type | Default | |------------------|------------------------------------------------------|---------| | `fetcher` | `(page, limit) => Promise>` | — | | `opts.startPage` | `number` | `1` | | `opts.limit` | `number` | `20` | | `opts.signal` | `AbortSignal \| undefined` | — | Throws `PaginationError(PAGINATION.ABORTED)` between pages if the signal fires. ### `createCursorIterator(fetcher, opts?): AsyncIterable>` | Parameter | Type | Default | |--------------|-------------------------------------------------------------|---------| | `fetcher` | `(cursor, limit) => Promise>` | — | | `opts.limit` | `number` | `20` | | `opts.signal` | `AbortSignal \| undefined` | — | ### `collectAll(iterable, signal?): Promise` Drains the entire iterable. Checks `signal?.aborted` after each page. ### `collectUpTo(iterable, maxItems, signal?): Promise` Returns at most `maxItems` items, stopping early without consuming subsequent pages. ### `mapPaginated(iterable, mapper): Promise` Maps each item with `mapper` and returns a flat `U[]`. ### `isPaginatedResponseEmpty(response): boolean` Returns `response.data.length === 0`. Accepts both `OffsetPaginatedResponse` and `CursorPaginatedResponse`. ### `mergeOffsetResponses(responses): { data: T[]; total: number }` Concatenates `data` arrays; `total` is taken from the first response (assumed to be the authoritative count for all pages). ## Implementation Notes The `createOffsetIterator` and `createCursorIterator` factories close over their state (current page / cursor, done flag) in the `[Symbol.asyncIterator]()` factory, so each `for await` loop gets a fresh independent cursor. The abort check happens both before and after the async fetch call to handle signals that fire during IO.