Files
skills/docs/foundation/data/pagination/src/cursor/cursor.md
2026-05-29 08:20:56 +01:00

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 — compute CursorPaginationMeta (start/end cursor, hasNextPage, hasPreviousPage, optional totalCount) from an edge array.
  • createCursorResponse — build a CursorPaginatedResponse<T> in one call.
  • createConnection — build a Relay-style Connection<T> with edges and pageInfo.
  • 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.