4.3 KiB
Cursor Pagination
(no submodules)
Purpose
The cursor module provides the in-memory building blocks for Relay-style cursor-based pagination. It offers utilities for constructing edges, connections, and metadata from any sorted data source without requiring a database adapter. All functions are pure — they accept plain arrays and return plain objects — making them trivially testable and completely portable across runtimes.
Cursor-based pagination is preferred over offset pagination for large datasets or real-time feeds because it avoids the "jumping rows" problem: items inserted or deleted between page fetches cannot corrupt a cursor-based window.
Features
calculateCursorMeta— computeCursorPaginationMeta(start/end cursor, hasNextPage, hasPreviousPage, optional totalCount) from an edge array.createCursorResponse— build aCursorPaginatedResponse<T>in one call.createConnection— build a Relay-styleConnection<T>withedgesandpageInfo.createEdge/createEdges— wrap nodes with their cursor strings.normalizeCursorRequest— clamp limit, default direction, from raw input.cursorPaginateArray— paginate an in-memory array with forward/backward cursor support.
Usage: Basic Example
import { cursorPaginateArray } from "@suaveplan/pagination/cursor";
const items = [{ id: "a" }, { id: "b" }, { id: "c" }, { id: "d" }];
// First page — no cursor, limit 2
const page1 = cursorPaginateArray(items, { limit: 2 }, (item) => item.id);
// page1.data → [{ id: "a" }, { id: "b" }]
// page1.meta.endCursor → "b"
// page1.meta.hasNextPage → true
// Next page — use endCursor from previous response
const page2 = cursorPaginateArray(
items,
{ cursor: page1.meta.endCursor ?? undefined, limit: 2 },
(item) => item.id
);
// page2.data → [{ id: "c" }, { id: "d" }]
// page2.meta.hasNextPage → false
Usage: Advanced Example
import {
createEdges,
createConnection,
normalizeCursorRequest,
} from "@suaveplan/pagination/cursor";
// Relay-style connection with totalCount
const users = [
{ id: "u1", name: "Alice" },
{ id: "u2", name: "Bob" },
];
const req = normalizeCursorRequest({ limit: 10 }, { maxLimit: 50 });
const edges = createEdges(users, (u) => u.id);
const connection = createConnection(edges, false, "forward", 2);
// connection.edges[0].cursor → "u1"
// connection.pageInfo.hasNextPage → false
// connection.totalCount → 2
API Reference
calculateCursorMeta<T>(edges, hasMore, direction?, totalCount?)
| Parameter | Type | Default |
|---|---|---|
edges |
Edge<T>[] |
— |
hasMore |
boolean |
— |
direction |
"forward" | "backward" |
"forward" |
totalCount |
number | undefined |
— |
Returns CursorPaginationMeta.
createCursorResponse<T>(edges, hasMore, direction?, totalCount?)
Returns CursorPaginatedResponse<T> — the data field contains the unwrapped nodes.
createConnection<T>(edges, hasMore, direction?, totalCount?)
Returns Connection<T> — the Relay-style form with edges, pageInfo, and optional totalCount.
createEdge<T>(node, cursor)
Returns Edge<T> — { node, cursor }.
createEdges<T>(nodes, getCursor)
Maps nodes to Edge<T>[] using the provided cursor extractor. The extractor receives (node, index).
normalizeCursorRequest(input, defaults?)
Clamps limit to [1, maxLimit] (default maxLimit = 100). Defaults direction to "forward". Returns a full CursorPaginationRequest.
cursorPaginateArray<T>(items, request, getCursor?)
Slices items according to cursor, limit, and direction. Default cursor extractor is (_, i) => String(i). Returns CursorPaginatedResponse<T>.
Implementation Notes
All cursor positions are string values matched by strict equality. The default extractor uses array indices as cursors; callers are encouraged to pass a stable domain-key extractor (e.g., item.id) in production. For backward pagination, startIndex is clamped to 0 to avoid negative slices. The hasNextPage/hasPreviousPage logic follows the GraphQL Relay Cursor Connections Specification.