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; returnsnullon any malformed input.validateCursor— predicate: returnstruefor well-formed cursors.fetchAfter— loads items after a cursor using aPaginationAdapter; supportsAbortSignal.fetchBefore— loads items before a cursor; supportsAbortSignal.
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.