Initial Commit
This commit is contained in:
110
packages/foundation/data/pagination/src/helpers/helpers.md
Normal file
110
packages/foundation/data/pagination/src/helpers/helpers.md
Normal 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.
|
||||
Reference in New Issue
Block a user