Initial Commit

This commit is contained in:
2026-05-27 23:47:53 +01:00
parent fbc70f96e6
commit 9b28db724f
18103 changed files with 1600554 additions and 0 deletions

View File

@@ -0,0 +1,110 @@
# Pagination Helpers
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## 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
```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<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.