# 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 ```typescript 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 ```typescript 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(adapter, cursor, options, getSortValue): Promise>` | Parameter | Type | Description | |---------------|-----------------------------------|------------------------------------------| | `adapter` | `CursorAdapter` | 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(adapter, cursor, options, getSortValue): Promise>` 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.