first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,90 @@
# 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<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.