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

4.0 KiB

Cursor Pagination (Adapter-based)

Purpose

The cursor-pagination module provides base64url cursor encoding/decoding and adapter-backed forward/backward fetch operations for cursor-based pagination. Unlike cursor.ts — which operates on in-memory arrays — this module integrates with the PaginationAdapter interface so it can drive any data source (database, HTTP API, or in-memory store). Cursors are opaque URL-safe strings; they encode a typed value (string | number) so the type is preserved through the round-trip.

Features

  • encodeCursor — wraps a value in { v } JSON and encodes to base64url (URL-safe, no padding).
  • decodeCursor — decodes a base64url cursor; returns null on any malformed input.
  • validateCursor — predicate: returns true for well-formed cursors.
  • fetchAfter — loads items after a cursor using a PaginationAdapter; supports AbortSignal.
  • fetchBefore — loads items before a cursor; supports AbortSignal.

Usage: Basic Example

import { encodeCursor, decodeCursor, validateCursor } from "@suaveplan/pagination/cursor";

const token = encodeCursor("user_42");
// → "eyJ2IjoidXNlcl80MiJ9" (URL-safe base64url)

const value = decodeCursor(token);
// → "user_42"

validateCursor(token);  // → true
validateCursor("!!!"); // → false

Usage: Advanced Example

import { fetchAfter } from "@suaveplan/pagination/cursor";
import { createMemoryAdapter } from "@suaveplan/pagination/adapters";

const adapter = createMemoryAdapter([
  { id: 1, name: "Alice" },
  { id: 2, name: "Bob" },
  { id: 3, name: "Carol" },
]);

const page = await fetchAfter(
  adapter,
  undefined,           // no cursor → start from beginning
  { limit: 2, sortBy: "id", sortDirection: "asc" },
  (item) => item.id    // sort-key extractor
);
// page.items → [{ id: 1 }, { id: 2 }]
// page.hasNextPage → true
// page.endCursor → encodeCursor(2)

// Abort support
const controller = new AbortController();
controller.abort();
await fetchAfter(adapter, undefined, { limit: 10, signal: controller.signal }, (i) => i.id);
// throws PaginationError(PAGINATION.ABORTED)

API Reference

encodeCursor(value: string | number): string

Encodes a value to base64url. The JSON wrapper { v: value } preserves the type across decode.

decodeCursor(cursor: string): string | number | null

Decodes a base64url cursor. Returns null if the input is not valid base64url or the JSON payload is malformed.

validateCursor(cursor: string): boolean

Returns true iff decodeCursor returns a non-null value.

fetchAfter<T>(adapter, cursor, options, getSortValue): Promise<CursorPageResult<T>>

Parameter Type Description
adapter CursorAdapter<T> Data source adapter
cursor string | undefined Position to start after (undefined = start)
options CursorPaginationOptions limit, sortBy, sortDirection, filter, signal
getSortValue (item: T) => string | number Extracts the sortable key from an item

Throws PaginationError(PAGINATION.ABORTED) if options.signal is already aborted or fires during the fetch.

fetchBefore<T>(adapter, cursor, options, getSortValue): Promise<CursorPageResult<T>>

Same signature as fetchAfter, but slices the window ending before the cursor position.

Implementation Notes

The base64url alphabet uses - and _ instead of + and /, and strips trailing = padding — producing tokens safe for URL path segments and query parameters with no encoding required. Both fetchAfter and fetchBefore load all matching items in a single adapter call and then slice in memory; this is intentional for the in-memory reference adapter. Production adapters will implement efficient seeks directly in the storage layer.