Pagination docs were scattered across api-types.md (types + cursor
semantics), data-api-in-renderer.md (hooks), data-api-in-main.md (offset
example + keyset note), api-design-guidelines.md (query params), and
data-ordering-guide.md (cache shapes + determinism), with no single
discoverable home for the offset-vs-cursor model.
Add docs/references/data/data-pagination-guide.md as the canonical hub
(mirrors data-ordering-guide.md): two modes, four-layer quickstart, wire
contract, server impl (offset + keyset cursor + multi-band caveat),
renderer consumption, FTS pagination, gotchas, and a see-also map. Other
docs keep their authoritative slice and link to the guide; the migrated
conceptual prose is removed from api-types.md to avoid duplication.
Also fix two pre-existing broken anchors found while verifying links
(database-patterns withWriteTx; ordering guide section number).
DataApi handlers and their services may perform only SQLite reads/writes via Drizzle; fs/network/process/external-service side effects are prohibited regardless of nesting depth or an accompanying DB write. Add a Hard Rule section to api-design-guidelines, scope-limit service domain workflows to DB I/O, and echo the boundary in the overview and README.
libsql client-ts upstream issue #288 makes PRAGMA busy_timeout ineffective
for async transactions, so concurrent db.transaction() calls reliably surface
SQLITE_BUSY. Introduce DbService.withWriteTx as a serialized write helper:
- Process-wide FIFO mutex (async-mutex) serializes write transactions.
- libsql client's default BEGIN IMMEDIATE protects against read-then-write
tx upgrade failures (no override needed at the drizzle layer).
- Single 50ms BUSY retry guards against transient external locks.
Reads do NOT need this — WAL gives readers snapshot isolation that is never
blocked by writers.
Includes unit tests (FIFO ordering, finally release on throw, single BUSY
retry, persistent BUSY rethrow, non-BUSY passthrough) plus a real-libsql
integration test. Updates the DbService test mock with a passthrough
withWriteTx so dependent services do not throw "is not a function" in
tests. Documents the API in database-patterns.md and points
CLAUDE.md / data-api-overview.md at the new pattern.
Fixes#14593 (mutate type collapses to CursorPaginationResponse, erasing
subtype fields like BranchMessagesResponse.activeNodeId) and #14594
(default flatMap bakes in a "page-load order == display order" assumption
that breaks on branch-walk + column-reverse layouts). Also closes a dual
silent-failure where useInfiniteQuery silently accepted offset paths
(stuck at page 1) and usePaginatedQuery silently accepted cursor paths
(total stays 0).
useInfiniteQuery now exposes raw pages: TResponse[] (no items field),
preserving subtype fields and typing mutate as SWRInfiniteKeyedMutator
correctly. The new useInfiniteFlatItems hook derives the flat list with
explicit reversePages / reverseItems switches, so flattening is no longer
hidden behind an implicit page-load ordering. Path generics on both hooks
are gated via CursorPaginatedPath / OffsetPaginatedPath, built on
InferPaginationMode so the optional nextCursor field cannot collapse
offset paths into the cursor guard structurally.
DEFAULT_SWR_OPTIONS realigned for IPC (not HTTP) semantics: DataApiService
is the single retry decision point (shouldRetryOnError: false), reconnect
revalidation disabled, keepPreviousData enabled to suppress search/
pagination flicker. loadNext drops its isValidating guard (SWR's
dedupingInterval is the right dedup site); usePaginatedQuery
reset-on-query-change uses unstable_serialize to be key-order independent.
useInfiniteQuery had zero consumers when rewritten — the breaking removal
of the items field carries no migration cost. Comprehensive test coverage
for type contracts, flat-items behavior, infinite integration, and
paginated reset; renderer data docs synced.
Extend the renderer data hooks to cover three mutation shapes that
previously had to drop to imperative dataApiService calls:
- Template paths (e.g. `/providers/:providerId`) with a runtime `params`
option on useQuery / useMutation / useInfiniteQuery / usePaginatedQuery,
so a single hook instance can operate on ids chosen at call time.
ParamsForPath derives types directly from the schema's existing
`params: {...}` declarations (no template-string parsing).
- Function-form `refresh: ({ args, result }) => ConcreteApiPaths[]` for
invalidation keys that depend on trigger input or server response. Args
are closure-captured at trigger entry to avoid races between concurrent
calls.
- Explicit `/*` suffix for path-segment prefix matching on refresh and
invalidate patterns, preserving the trailing slash so `/providers/*`
doesn't match siblings like `/providers-archived`.
A single `resolveTemplate` function is the canonical path-replacement
point, so `useQuery('/providers/:id', { params: { id: 'abc' } })` and
`useQuery('/providers/abc')` produce byte-for-byte identical cache keys.
Dev-mode assertions flag invalid `/*` patterns (bare wildcard or missing
trailing slash) and warn on concurrent triggers against the same template
hook instance, which would share SWR mutation state.
Fully backward compatible: existing `refresh: ['/topics']` and
concrete-path hook calls compile and behave identically.
Services now handle both business logic and data access directly via Drizzle ORM.
Repository pattern is strongly discouraged unless absolutely necessary.
Signed-off-by: fullex <0xfullex@gmail.com>