4.7 KiB
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<T> or CursorPaginatedResponse<T> — they are not tied to a specific adapter or data source.
Features
createOffsetIterator—AsyncIterable<PaginatedIteratorResult<T>>driven by an offset fetcher function.createCursorIterator—AsyncIterable<PaginatedIteratorResult<T>>driven by a cursor fetcher function.collectAll— drain an iterable into a flatT[]; abortable.collectUpTo— drain up to N items; stops early; abortable.mapPaginated— map items across all pages, returning a flatU[].isPaginatedResponseEmpty— check whether a response has zero items.mergeOffsetResponses— combine sequential pages into a single{ data, total }.
Usage: Basic Example
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
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<T>(fetcher, opts?): AsyncIterable<PaginatedIteratorResult<T>>
| Parameter | Type | Default |
|---|---|---|
fetcher |
(page, limit) => Promise<OffsetPaginatedResponse<T>> |
— |
opts.startPage |
number |
1 |
opts.limit |
number |
20 |
opts.signal |
AbortSignal | undefined |
— |
Throws PaginationError(PAGINATION.ABORTED) between pages if the signal fires.
createCursorIterator<T>(fetcher, opts?): AsyncIterable<PaginatedIteratorResult<T>>
| Parameter | Type | Default |
|---|---|---|
fetcher |
(cursor, limit) => Promise<CursorPaginatedResponse<T>> |
— |
opts.limit |
number |
20 |
opts.signal |
AbortSignal | undefined |
— |
collectAll<T>(iterable, signal?): Promise<T[]>
Drains the entire iterable. Checks signal?.aborted after each page.
collectUpTo<T>(iterable, maxItems, signal?): Promise<T[]>
Returns at most maxItems items, stopping early without consuming subsequent pages.
mapPaginated<T, U>(iterable, mapper): Promise<U[]>
Maps each item with mapper and returns a flat U[].
isPaginatedResponseEmpty<T>(response): boolean
Returns response.data.length === 0. Accepts both OffsetPaginatedResponse<T> and CursorPaginatedResponse<T>.
mergeOffsetResponses<T>(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.