Files
2026-05-27 23:47:53 +01:00

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 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

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.