Files
suaveplan/openspec/archive/2026-04-19-enterprise-pagination/tasks.md
2026-05-27 23:29:53 +01:00

11 KiB
Raw Blame History

Archived 2026-04-19: all tasks retroactively ticked; package shipped under universal/data/pagination.

Tasks: enterprise-pagination

1. Scaffold

  • 1.1 Create packages/universal/pagination/ with package.json, tsconfig.json, tsconfig.lib.json, vite.config.ts, vitest.config.ts, README.md skeleton
  • 1.2 Register coverage thresholds at 100/100/100/100 in vitest.config.ts; provider = istanbul
  • 1.3 Add cross-cutting guard tests in tests/: dep-graph, no-disallowed, tree-shake
  • 1.4 Add package to root CLAUDE.md, README.md, and STRUCTURE.yml

2. Types + errors

  • 2.1 Implement src/types/pagination.types.ts — Connection<T>, CursorDirection, CursorPageResult<T>, CursorPaginatedResponse<T>, CursorPaginationMeta, CursorPaginationOptions, CursorPaginationRequest, Edge<T>, FilterConfig, KeysetPageResult<T>, KeysetPaginationOptions, KeysetSortKey, OffsetPaginatedResponse<T>, OffsetPaginationMeta, OffsetPaginationRequest, PageResult<T>, PageTokenPayload, PaginatedIteratorResult<T>, PaginationDefaults, SortConfig<T>, SortDirection
  • 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
  • 2.3 Implement src/error-codes.ts and auto-register via side-effect import in src/index.ts
  • 2.4 Tests: error construction, code narrowing, error message round-trip, defineCodes registration
  • 2.5 Co-located src/errors/pagination-error.md

3. Zod schemas

  • 3.1 src/cursor/cursor.schemas.ts — CursorPaginationRequestSchema, CursorPaginatedResponseSchema, CursorPaginationMetaSchema, EdgeSchema, ConnectionSchema
  • 3.2 src/offset/offset.schemas.ts — OffsetPaginationRequestSchema, OffsetPaginatedResponseSchema, OffsetPaginationMetaSchema
  • 3.3 src/keyset/keyset.schemas.ts — KeysetPaginationOptionsSchema, KeysetPageResultSchema, KeysetSortKeySchema
  • 3.4 src/page-token/page-token.schemas.ts — PageTokenPayloadSchema
  • 3.5 src/adapters/adapters.schemas.ts — SortConfigSchema, PaginationDefaultsSchema
  • 3.6 Tests: valid inputs parse, invalid inputs throw with PAGINATION.* code, schema .parse() round-trips match type inferences
  • 3.7 Co-located .md for each schemas file

4. Cursor pagination

  • 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<T>
    • createEdges(nodes, cursorFn) → Edge<T>[]
    • createConnection(edges, meta) → Connection<T>
    • createCursorResponse(data, opts) → CursorPaginatedResponse<T>
    • cursorPaginateArray(array, opts) → CursorPaginatedResponse<T>
  • 4.2 Implement src/cursor/cursor-pagination.ts:
    • fetchAfter(adapter, cursor, opts) → Promise<CursorPageResult<T>>
    • fetchBefore(adapter, cursor, opts) → Promise<CursorPageResult<T>>
  • 4.3 Unit tests: encode/decode round-trips, invalid cursor error, normalization defaults, meta calculation, edge/connection construction, array pagination with and without cursors
  • 4.4 Property-based tests: decodeCursor(encodeCursor(s)) === s for all non-empty strings; monotone cursor position property on sorted arrays
  • 4.5 Co-located src/cursor/cursor.md and src/cursor/cursor-pagination.md

5. Offset pagination

  • 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<T>
    • paginateArray(array, opts) → OffsetPaginatedResponse<T>
    • estimateTotalCount(adapter, opts) → Promise<number>
    • mergeOffsetResponses(responses) → OffsetPaginatedResponse<T>
  • 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
  • 5.3 Property-based tests: paginateArray always returns ≤ limit items; pageToOffset(offsetToPage(o, l), l) === o for valid inputs
  • 5.4 Co-located src/offset/offset.md

6. Keyset pagination

  • 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<string, unknown> — base64url + JSON.parse; throws PAGINATION.INVALID_KEYSET_CURSOR on malformed input
    • extractSortKeys(item, config) → KeysetSortKey[]
    • keysetPaginate(array, opts) → KeysetPageResult<T> — pure in-memory keyset pagination over a sorted array
    • fetchKeysetPage(adapter, opts) → Promise<KeysetPageResult<T>>
  • 6.2 Unit tests: encode/decode round-trips, invalid cursor error path, sort key extraction, keysetPaginate forward and backward traversal, fetchKeysetPage with MemoryPaginationAdapter
  • 6.3 Property-based tests: decodeKeysetCursor(encodeKeysetCursor(keys)) deep-equals keys for arbitrary serialisable record shapes
  • 6.4 Co-located src/keyset/keyset-pagination.md

7. Page tokens

  • 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)
  • 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
  • 7.3 Property-based tests: decodePageToken(encodePageToken(payload)) deep-equals payload for arbitrary PageTokenPayload shapes
  • 7.4 Co-located src/page-token/page-token.md

8. Helpers

  • 8.1 Implement src/helpers/helpers.ts:
    • isPaginatedResponseEmpty(response) → boolean
    • mapPaginated(response, fn) → PaginatedResponse<U>
    • createCursorIterator(fetchFn, opts) → AsyncIterableIterator<T> — opts.signal?: AbortSignal; throws PAGINATION.ABORTED on abort between pages
    • createOffsetIterator(fetchFn, opts) → AsyncIterableIterator<T> — same abort behaviour
    • collectAll(iterator, signal?) → Promise<T[]> — forwards signal; respects abort mid-collect
    • collectUpTo(iterator, limit, signal?) → Promise<T[]> — stops at limit items; respects signal
    • mergeOffsetResponses re-exported from offset module for convenience
  • 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
  • 8.3 Co-located src/helpers/helpers.md

9. Adapters

  • 9.1 Implement src/adapters/database-adapter.ts:
    • PaginationAdapter<T> interface — count(opts): Promise<number>, find(opts): Promise<T[]>, findOne(opts): Promise<T | null>
    • SortConfig<T> type and createSortConfig<T>(fields) factory
    • MemoryPaginationAdapter<T> class — full in-memory implementation: sorts by SortConfig, applies cursor/keyset/offset slicing, respects all PaginationAdapter opts
    • createMemoryAdapter<T>(items, config?) → MemoryPaginationAdapter<T> factory
  • 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
  • 9.3 Co-located src/adapters/database-adapter.md

10. OTel instrumentation

  • 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)
  • 10.2 Wire counters and histograms into cursor, offset, keyset public entrypoints
  • 10.3 Tests: no-exporter pass (instruments do not throw), attribute labels are correct for each strategy
  • 10.4 Co-located src/otel/instrumentation.md

11. Benchmarks

  • 11.1 src/__benchmarks__/cursor-encode-decode.bench.ts — encodeCursor + decodeCursor round-trip × 10 000 iterations (mitata)
  • 11.2 src/__benchmarks__/offset-paginate.bench.ts — paginateArray on a 10 000-item array × 10 000 iterations
  • 11.3 src/__benchmarks__/keyset-encode-decode.bench.ts — encodeKeysetCursor + decodeKeysetCursor round-trip × 10 000 iterations
  • 11.4 Publish baseline numbers in README.md

12. Docs

  • 12.1 Co-located .md for every implementation file (≥200 words, both simple and advanced examples, full API reference)
  • 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
  • 12.3 bun run docs:check --filter=@suaveplan/pagination ≥ 90%

13. Verify and publish

  • 13.1 bunx turbo lint typecheck test --filter=@suaveplan/pagination green
  • 13.2 bunx turbo test:coverage --filter=@suaveplan/pagination at 100/100/100/100
  • 13.3 bunx turbo build --filter=@suaveplan/pagination green
  • 13.4 bun scripts/publish.ts --package @suaveplan/pagination to Verdaccio at 0.0.0
  • 13.5 Archive OpenSpec change to openspec/archive/2026-04-19-enterprise-pagination/