# Keyset Pagination _(no submodules)_ ## Purpose The keyset pagination module implements seek-based (keyset) pagination, which avoids the performance degradation of deep offset pagination by using the sort-key values of the last seen item as the cursor position. Unlike numeric offsets — which require the database to scan and discard all preceding rows — a keyset cursor lets the storage engine jump directly to the next page using an indexed seek. This module provides both adapter-backed (`fetchKeysetPage`) and in-memory (`keysetPaginate`) variants, plus encoding/decoding utilities for opaque keyset cursor strings. ## Features - `encodeKeysetCursor` — serialize `KeysetSortKey[]` to a URL-safe base64url string. - `decodeKeysetCursor` — deserialize and validate a keyset cursor; returns `null` on malformed input. - `extractSortKeys` — pull sort-key values from a typed item based on sort config. - `fetchKeysetPage` — fetch a page from an adapter starting after a keyset position. - `keysetPaginate` — paginate an in-memory array using keyset logic. ## Usage: Basic Example ```typescript import { keysetPaginate, encodeKeysetCursor, decodeKeysetCursor } from "@suaveplan/pagination/keyset"; const items = [ { id: 1, name: "Alice" }, { id: 2, name: "Bob" }, { id: 3, name: "Carol" }, { id: 4, name: "Dave" }, ]; const options = { limit: 2, sortKeys: [{ field: "id", direction: "asc" as const }] }; // First page const page1 = keysetPaginate(items, null, options); // page1.items → [{ id: 1 }, { id: 2 }] // page1.hasMore → true // Encode position for the next request const cursor = encodeKeysetCursor(page1.lastKeys!); // Second page — pass decoded keys const page2 = keysetPaginate(items, decodeKeysetCursor(cursor), options); // page2.items → [{ id: 3 }, { id: 4 }] // page2.hasMore → false ``` ## Usage: Advanced Example ```typescript import { fetchKeysetPage } from "@suaveplan/pagination/keyset"; import { createMemoryAdapter } from "@suaveplan/pagination/adapters"; const adapter = createMemoryAdapter( Array.from({ length: 100 }, (_, i) => ({ id: i + 1, score: Math.floor(i / 10) })) ); // Composite sort: score desc, then id asc const options = { limit: 5, sortKeys: [ { field: "score", direction: "desc" as const }, { field: "id", direction: "asc" as const }, ], }; let lastKeys = null; do { const page = await fetchKeysetPage(adapter, lastKeys, options); console.log(page.items); lastKeys = page.lastKeys; } while (lastKeys !== null); ``` ## API Reference ### `encodeKeysetCursor(keys: KeysetSortKey[]): string` Serialises the keys array to JSON, encodes as base64url. Throws `PaginationError(PAGINATION.INVALID_KEYSET_CURSOR)` only if `JSON.stringify` fails (e.g., circular references). ### `decodeKeysetCursor(cursor: string): KeysetSortKey[] | null` Decodes a base64url keyset cursor. Validates each element has `field: string`, `direction: "asc"|"desc"`, and `value: string|number`. Returns `null` on any invalid input. ### `extractSortKeys(item, sortKeys): KeysetSortKey[]` | Parameter | Type | |-----------|---------------------------------------------------| | `item` | `T` | | `sortKeys` | `Array<{ field: string; direction: SortDirection }>` | Returns `KeysetSortKey[]` with runtime values extracted from the item. ### `fetchKeysetPage(adapter, lastKeys, options): Promise>` Loads all items from the adapter, filters those after `lastKeys`, returns at most `options.limit`. Returns `{ items: [], hasMore: false, lastKeys: null }` when `sortKeys` is empty. ### `keysetPaginate(items, lastKeys, options): KeysetPageResult` Sorts `items` by the primary sort key, filters items after `lastKeys`, returns at most `options.limit`. Equivalent to `fetchKeysetPage` but operates on a plain array without async IO. ## Implementation Notes The comparison logic for `isAfterKeyset` short-circuits on the first discriminating key in a composite sort — exactly matching how a B-tree index seek works. String comparison uses `localeCompare` for locale-aware ordering; numeric comparison uses `<` to avoid IEEE-754 edge cases with `===`. The in-memory `keysetPaginate` sorts a shallow copy of the input array to avoid mutating the caller's data.