first commit
This commit is contained in:
103
docs/foundation/data/pagination/src/keyset/keyset-pagination.md
Normal file
103
docs/foundation/data/pagination/src/keyset/keyset-pagination.md
Normal file
@@ -0,0 +1,103 @@
|
||||
# Keyset Pagination
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## 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<T>(item, sortKeys): KeysetSortKey[]`
|
||||
|
||||
| Parameter | Type |
|
||||
|-----------|---------------------------------------------------|
|
||||
| `item` | `T` |
|
||||
| `sortKeys` | `Array<{ field: string; direction: SortDirection }>` |
|
||||
|
||||
Returns `KeysetSortKey[]` with runtime values extracted from the item.
|
||||
|
||||
### `fetchKeysetPage<T>(adapter, lastKeys, options): Promise<KeysetPageResult<T>>`
|
||||
|
||||
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<T>(items, lastKeys, options): KeysetPageResult<T>`
|
||||
|
||||
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.
|
||||
Reference in New Issue
Block a user