11 KiB
11 KiB
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/withpackage.json,tsconfig.json,tsconfig.lib.json,vite.config.ts,vitest.config.ts,README.mdskeleton - 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, andSTRUCTURE.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—PaginationErrorbase extendsSuaveplanError;defineCodesforPAGINATION.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.tsand auto-register via side-effect import insrc/index.ts - 2.4 Tests: error construction, code narrowing, error message round-trip,
defineCodesregistration - 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
.mdfor each schemas file
4. Cursor pagination
- 4.1 Implement
src/cursor/cursor.ts:encodeCursor(value)→string— base64url-encodedecodeCursor(cursor)→string— base64url-decode; throwsPAGINATION.INVALID_CURSORon malformed inputvalidateCursor(cursor)→booleannormalizeCursorRequest(request)→CursorPaginationOptions— apply defaults, clamp limit, validate directioncalculateCursorMeta(opts)→CursorPaginationMetacreateEdge(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)) === sfor all non-empty strings; monotone cursor position property on sorted arrays - 4.5 Co-located
src/cursor/cursor.mdandsrc/cursor/cursor-pagination.md
5. Offset pagination
- 5.1 Implement
src/offset/offset.ts:pageToOffset(page, limit)→numberoffsetToPage(offset, limit)→numbergetPageRange(meta)→{ start: number; end: number }normalizeOffsetRequest(request)→OffsetPaginationOptions— validate page ≥ 1, limit > 0; throwPAGINATION.INVALID_PAGE/PAGINATION.INVALID_LIMITcalculateOffsetMeta(opts)→OffsetPaginationMetacreateOffsetResponse(data, meta)→OffsetPaginatedResponse<T>paginateArray(array, opts)→OffsetPaginatedResponse<T>estimateTotalCount(adapter, opts)→Promise<number>mergeOffsetResponses(responses)→OffsetPaginatedResponse<T>
- 5.2 Unit tests:
pageToOffset/offsetToPageinverses, normalization error paths,paginateArrayboundary conditions (first page, last page, empty array, page beyond end), meta correctness, merge responses - 5.3 Property-based tests:
paginateArrayalways returns≤ limititems;pageToOffset(offsetToPage(o, l), l) === ofor 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; throwsPAGINATION.INVALID_KEYSET_CURSORon non-serialisable valuesdecodeKeysetCursor(cursor)→Record<string, unknown>— base64url + JSON.parse; throwsPAGINATION.INVALID_KEYSET_CURSORon malformed inputextractSortKeys(item, config)→KeysetSortKey[]keysetPaginate(array, opts)→KeysetPageResult<T>— pure in-memory keyset pagination over a sorted arrayfetchKeysetPage(adapter, opts)→Promise<KeysetPageResult<T>>
- 6.2 Unit tests: encode/decode round-trips, invalid cursor error path, sort key extraction,
keysetPaginateforward and backward traversal,fetchKeysetPagewithMemoryPaginationAdapter - 6.3 Property-based tests:
decodeKeysetCursor(encodeKeysetCursor(keys))deep-equalskeysfor 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 + base64urldecodePageToken(token)→PageTokenPayload— base64url + JSON.parse + Zod validation; throwsPAGINATION.INVALID_TOKENon malformed inputcreateCursorToken(payload)→stringcreateOffsetToken(payload)→stringextractCursorValue(token)→stringextractSortInfo(token)→{ field: string; direction: SortDirection }hashFilters(filters)→string— deterministic stable hash of aFilterConfigvalidateTokenFilter(token, filters)→boolean— checkshashFilters(filters)matches stored hash- Injectable
clock?: Clockfor token timestamp (defaults toDate.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-equalspayloadfor arbitraryPageTokenPayloadshapes - 7.4 Co-located
src/page-token/page-token.md
8. Helpers
- 8.1 Implement
src/helpers/helpers.ts:isPaginatedResponseEmpty(response)→booleanmapPaginated(response, fn)→PaginatedResponse<U>createCursorIterator(fetchFn, opts)→AsyncIterableIterator<T>—opts.signal?: AbortSignal; throwsPAGINATION.ABORTEDon abort between pagescreateOffsetIterator(fetchFn, opts)→AsyncIterableIterator<T>— same abort behaviourcollectAll(iterator, signal?)→Promise<T[]>— forwards signal; respects abort mid-collectcollectUpTo(iterator, limit, signal?)→Promise<T[]>— stops atlimititems; respects signalmergeOffsetResponsesre-exported from offset module for convenience
- 8.2 Unit tests:
isPaginatedResponseEmptyedge cases,mapPaginatedtype transformation, cursor iterator forward traversal, offset iterator forward traversal,collectAll/collectUpToon 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 andcreateSortConfig<T>(fields)factoryMemoryPaginationAdapter<T>class — full in-memory implementation: sorts bySortConfig, applies cursor/keyset/offset slicing, respects allPaginationAdapteroptscreateMemoryAdapter<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)
- Counter
- 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+decodeCursorround-trip × 10 000 iterations (mitata) - 11.2
src/__benchmarks__/offset-paginate.bench.ts—paginateArrayon a 10 000-item array × 10 000 iterations - 11.3
src/__benchmarks__/keyset-encode-decode.bench.ts—encodeKeysetCursor+decodeKeysetCursorround-trip × 10 000 iterations - 11.4 Publish baseline numbers in
README.md
12. Docs
- 12.1 Co-located
.mdfor every implementation file (≥200 words, both simple and advanced examples, full API reference) - 12.2 Rewrite
README.mdwith 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/paginationgreen - 13.2
bunx turbo test:coverage --filter=@suaveplan/paginationat 100/100/100/100 - 13.3
bunx turbo build --filter=@suaveplan/paginationgreen - 13.4
bun scripts/publish.ts --package @suaveplan/paginationto Verdaccio at 0.0.0 - 13.5 Archive OpenSpec change to
openspec/archive/2026-04-19-enterprise-pagination/