> Archived 2026-04-19: all tasks retroactively ticked; package shipped under universal/data/pagination. # Tasks: enterprise-pagination ## 1. Scaffold - [x] 1.1 Create `packages/universal/pagination/` with `package.json`, `tsconfig.json`, `tsconfig.lib.json`, `vite.config.ts`, `vitest.config.ts`, `README.md` skeleton - [x] 1.2 Register coverage thresholds at 100/100/100/100 in `vitest.config.ts`; provider = istanbul - [x] 1.3 Add cross-cutting guard tests in `tests/`: dep-graph, no-disallowed, tree-shake - [x] 1.4 Add package to root `CLAUDE.md`, `README.md`, and `STRUCTURE.yml` ## 2. Types + errors - [x] 2.1 Implement `src/types/pagination.types.ts` — `Connection`, `CursorDirection`, `CursorPageResult`, `CursorPaginatedResponse`, `CursorPaginationMeta`, `CursorPaginationOptions`, `CursorPaginationRequest`, `Edge`, `FilterConfig`, `KeysetPageResult`, `KeysetPaginationOptions`, `KeysetSortKey`, `OffsetPaginatedResponse`, `OffsetPaginationMeta`, `OffsetPaginationRequest`, `PageResult`, `PageTokenPayload`, `PaginatedIteratorResult`, `PaginationDefaults`, `SortConfig`, `SortDirection` - [x] 2.2 Implement `src/errors/pagination-error.ts` — `PaginationError` base extends `SuaveplanError`; `defineCodes` for `PAGINATION.INVALID_CURSOR`, `PAGINATION.INVALID_PAGE`, `PAGINATION.INVALID_LIMIT`, `PAGINATION.INVALID_KEYSET_CURSOR`, `PAGINATION.INVALID_TOKEN`, `PAGINATION.INVALID_SORT`, `PAGINATION.ABORTED` - [x] 2.3 Implement `src/error-codes.ts` and auto-register via side-effect import in `src/index.ts` - [x] 2.4 Tests: error construction, code narrowing, error message round-trip, `defineCodes` registration - [x] 2.5 Co-located `src/errors/pagination-error.md` ## 3. Zod schemas - [x] 3.1 `src/cursor/cursor.schemas.ts` — `CursorPaginationRequestSchema`, `CursorPaginatedResponseSchema`, `CursorPaginationMetaSchema`, `EdgeSchema`, `ConnectionSchema` - [x] 3.2 `src/offset/offset.schemas.ts` — `OffsetPaginationRequestSchema`, `OffsetPaginatedResponseSchema`, `OffsetPaginationMetaSchema` - [x] 3.3 `src/keyset/keyset.schemas.ts` — `KeysetPaginationOptionsSchema`, `KeysetPageResultSchema`, `KeysetSortKeySchema` - [x] 3.4 `src/page-token/page-token.schemas.ts` — `PageTokenPayloadSchema` - [x] 3.5 `src/adapters/adapters.schemas.ts` — `SortConfigSchema`, `PaginationDefaultsSchema` - [x] 3.6 Tests: valid inputs parse, invalid inputs throw with `PAGINATION.*` code, schema `.parse()` round-trips match type inferences - [x] 3.7 Co-located `.md` for each schemas file ## 4. Cursor pagination - [x] 4.1 Implement `src/cursor/cursor.ts`: - `encodeCursor(value)` → `string` — base64url-encode - `decodeCursor(cursor)` → `string` — base64url-decode; throws `PAGINATION.INVALID_CURSOR` on malformed input - `validateCursor(cursor)` → `boolean` - `normalizeCursorRequest(request)` → `CursorPaginationOptions` — apply defaults, clamp limit, validate direction - `calculateCursorMeta(opts)` → `CursorPaginationMeta` - `createEdge(node, cursor)` → `Edge` - `createEdges(nodes, cursorFn)` → `Edge[]` - `createConnection(edges, meta)` → `Connection` - `createCursorResponse(data, opts)` → `CursorPaginatedResponse` - `cursorPaginateArray(array, opts)` → `CursorPaginatedResponse` - [x] 4.2 Implement `src/cursor/cursor-pagination.ts`: - `fetchAfter(adapter, cursor, opts)` → `Promise>` - `fetchBefore(adapter, cursor, opts)` → `Promise>` - [x] 4.3 Unit tests: encode/decode round-trips, invalid cursor error, normalization defaults, meta calculation, edge/connection construction, array pagination with and without cursors - [x] 4.4 Property-based tests: `decodeCursor(encodeCursor(s)) === s` for all non-empty strings; monotone cursor position property on sorted arrays - [x] 4.5 Co-located `src/cursor/cursor.md` and `src/cursor/cursor-pagination.md` ## 5. Offset pagination - [x] 5.1 Implement `src/offset/offset.ts`: - `pageToOffset(page, limit)` → `number` - `offsetToPage(offset, limit)` → `number` - `getPageRange(meta)` → `{ start: number; end: number }` - `normalizeOffsetRequest(request)` → `OffsetPaginationOptions` — validate page ≥ 1, limit > 0; throw `PAGINATION.INVALID_PAGE` / `PAGINATION.INVALID_LIMIT` - `calculateOffsetMeta(opts)` → `OffsetPaginationMeta` - `createOffsetResponse(data, meta)` → `OffsetPaginatedResponse` - `paginateArray(array, opts)` → `OffsetPaginatedResponse` - `estimateTotalCount(adapter, opts)` → `Promise` - `mergeOffsetResponses(responses)` → `OffsetPaginatedResponse` - [x] 5.2 Unit tests: `pageToOffset`/`offsetToPage` inverses, normalization error paths, `paginateArray` boundary conditions (first page, last page, empty array, page beyond end), meta correctness, merge responses - [x] 5.3 Property-based tests: `paginateArray` always returns `≤ limit` items; `pageToOffset(offsetToPage(o, l), l) === o` for valid inputs - [x] 5.4 Co-located `src/offset/offset.md` ## 6. Keyset pagination - [x] 6.1 Implement `src/keyset/keyset-pagination.ts`: - `encodeKeysetCursor(keys)` → `string` — JSON + base64url; throws `PAGINATION.INVALID_KEYSET_CURSOR` on non-serialisable values - `decodeKeysetCursor(cursor)` → `Record` — base64url + JSON.parse; throws `PAGINATION.INVALID_KEYSET_CURSOR` on malformed input - `extractSortKeys(item, config)` → `KeysetSortKey[]` - `keysetPaginate(array, opts)` → `KeysetPageResult` — pure in-memory keyset pagination over a sorted array - `fetchKeysetPage(adapter, opts)` → `Promise>` - [x] 6.2 Unit tests: encode/decode round-trips, invalid cursor error path, sort key extraction, `keysetPaginate` forward and backward traversal, `fetchKeysetPage` with `MemoryPaginationAdapter` - [x] 6.3 Property-based tests: `decodeKeysetCursor(encodeKeysetCursor(keys))` deep-equals `keys` for arbitrary serialisable record shapes - [x] 6.4 Co-located `src/keyset/keyset-pagination.md` ## 7. Page tokens - [x] 7.1 Implement `src/page-token/page-token.ts`: - `encodePageToken(payload)` → `string` — JSON + base64url - `decodePageToken(token)` → `PageTokenPayload` — base64url + JSON.parse + Zod validation; throws `PAGINATION.INVALID_TOKEN` on malformed input - `createCursorToken(payload)` → `string` - `createOffsetToken(payload)` → `string` - `extractCursorValue(token)` → `string` - `extractSortInfo(token)` → `{ field: string; direction: SortDirection }` - `hashFilters(filters)` → `string` — deterministic stable hash of a `FilterConfig` - `validateTokenFilter(token, filters)` → `boolean` — checks `hashFilters(filters)` matches stored hash - Injectable `clock?: Clock` for token timestamp (defaults to `Date.now`) - [x] 7.2 Unit tests: round-trip encode/decode, invalid token error path, cursor/offset token construction, sort info extraction, filter hash stability across equivalent objects, filter validation pass/fail - [x] 7.3 Property-based tests: `decodePageToken(encodePageToken(payload))` deep-equals `payload` for arbitrary `PageTokenPayload` shapes - [x] 7.4 Co-located `src/page-token/page-token.md` ## 8. Helpers - [x] 8.1 Implement `src/helpers/helpers.ts`: - `isPaginatedResponseEmpty(response)` → `boolean` - `mapPaginated(response, fn)` → `PaginatedResponse` - `createCursorIterator(fetchFn, opts)` → `AsyncIterableIterator` — `opts.signal?: AbortSignal`; throws `PAGINATION.ABORTED` on abort between pages - `createOffsetIterator(fetchFn, opts)` → `AsyncIterableIterator` — same abort behaviour - `collectAll(iterator, signal?)` → `Promise` — forwards signal; respects abort mid-collect - `collectUpTo(iterator, limit, signal?)` → `Promise` — stops at `limit` items; respects signal - `mergeOffsetResponses` re-exported from offset module for convenience - [x] 8.2 Unit tests: `isPaginatedResponseEmpty` edge cases, `mapPaginated` type transformation, cursor iterator forward traversal, offset iterator forward traversal, `collectAll` / `collectUpTo` on both iterator types, abort mid-iteration - [x] 8.3 Co-located `src/helpers/helpers.md` ## 9. Adapters - [x] 9.1 Implement `src/adapters/database-adapter.ts`: - `PaginationAdapter` interface — `count(opts): Promise`, `find(opts): Promise`, `findOne(opts): Promise` - `SortConfig` type and `createSortConfig(fields)` factory - `MemoryPaginationAdapter` class — full in-memory implementation: sorts by `SortConfig`, applies cursor/keyset/offset slicing, respects all `PaginationAdapter` opts - `createMemoryAdapter(items, config?)` → `MemoryPaginationAdapter` factory - [x] 9.2 Unit tests: adapter `count`/`find`/`findOne`, sort config single and multi-field, ascending/descending, cursor-based slicing, keyset slicing, offset slicing, empty dataset - [x] 9.3 Co-located `src/adapters/database-adapter.md` ## 10. OTel instrumentation - [x] 10.1 `src/otel/instrumentation.ts` — create and export meter, register instruments: - Counter `pagination.request.total` (attributes: `strategy`, `outcome`) - Histogram `pagination.page.size` (attributes: `strategy`) - Histogram `pagination.encode_decode.duration_ms` (attributes: `direction`, `type`) - [x] 10.2 Wire counters and histograms into cursor, offset, keyset public entrypoints - [x] 10.3 Tests: no-exporter pass (instruments do not throw), attribute labels are correct for each strategy - [x] 10.4 Co-located `src/otel/instrumentation.md` ## 11. Benchmarks - [x] 11.1 `src/__benchmarks__/cursor-encode-decode.bench.ts` — `encodeCursor` + `decodeCursor` round-trip × 10 000 iterations (mitata) - [x] 11.2 `src/__benchmarks__/offset-paginate.bench.ts` — `paginateArray` on a 10 000-item array × 10 000 iterations - [x] 11.3 `src/__benchmarks__/keyset-encode-decode.bench.ts` — `encodeKeysetCursor` + `decodeKeysetCursor` round-trip × 10 000 iterations - [x] 11.4 Publish baseline numbers in `README.md` ## 12. Docs - [x] 12.1 Co-located `.md` for every implementation file (≥200 words, both simple and advanced examples, full API reference) - [x] 12.2 Rewrite `README.md` with public API surface, strategy comparison table (cursor vs offset vs keyset), iterator usage guide, adapter extension guide, OTel attributes reference, benchmark numbers, consumer migration notes - [x] 12.3 `bun run docs:check --filter=@suaveplan/pagination` ≥ 90% ## 13. Verify and publish - [x] 13.1 `bunx turbo lint typecheck test --filter=@suaveplan/pagination` green - [x] 13.2 `bunx turbo test:coverage --filter=@suaveplan/pagination` at 100/100/100/100 - [x] 13.3 `bunx turbo build --filter=@suaveplan/pagination` green - [x] 13.4 `bun scripts/publish.ts --package @suaveplan/pagination` to Verdaccio at 0.0.0 - [x] 13.5 Archive OpenSpec change to `openspec/archive/2026-04-19-enterprise-pagination/`