Problem
Currently, useLiveInfiniteQuery uses a "peek ahead" pattern to determine if there's a next page:
// Request one extra item
const expectedLimit = loadedPageCount * pageSize + 1 // +1 for peek ahead
// Check if we got the extra item
const hasMore = dataArray.length > totalItemsRequested
This works well for:
- Electric collections: Push-based sync, no control over what's available (though we could also modify this to avoid the extra fetch)
- LiveQueryCollection: Reactive local computation, no external API
But it's wasteful for QueryCollection where users control the queryFn and their API already returns pagination metadata:
queryFn: async (ctx) => {
const response = await fetch(url)
const json = await response.json()
// API already tells us hasNextPage!
return json.items // { items: [...], hasNextPage: true, totalCount: 100 }
}
The extra item fetch wastes:
- Network bandwidth
- Database query costs
- API rate limits
Proposed Solution
Add collection-level pagination metadata storage:
// Collections expose pagination metadata API
interface Collection {
setPaginationMeta(sessionId: string, meta: PaginationMeta): void
getPaginationMeta(sessionId: string): PaginationMeta | undefined
}
type PaginationMeta = {
hasNextPage?: boolean
hasPrevPage?: boolean
nextCursor?: string
prevCursor?: string
totalCount?: number
}
Session ID from Query Predicates
Session ID should be a deterministic hash of:
- Collection ID
- WHERE clauses
- ORDER BY clauses
- Excluding: limit, offset
This enables:
- Same query = same session = reuse pagination state
- Different query = different session = isolated pagination
- Multiple concurrent infinite queries on same collection don't interfere
Implementation using existing serializePredicates pattern:
const sessionId = hashKey([
collectionId,
JSON.stringify({ where: opts.where, orderBy: opts.orderBy })
])
Usage in QueryCollection
queryFn: async (ctx) => {
const response = await fetch(url)
const json = await response.json()
// Store pagination metadata
ctx.collection.setPaginationMeta(ctx.sessionId, {
hasNextPage: json.hasNextPage,
totalCount: json.totalCount,
nextCursor: json.nextCursor // For future cursor support
})
return json.items
}
Updated useLiveInfiniteQuery
// Check for pagination metadata first
const meta = queryResult.collection.getPaginationMeta(sessionId)
const hasNextPage = meta?.hasNextPage ?? fallbackToPeekAhead()
// Adjust window: no +1 if metadata exists
const limit = meta ? loadedPageCount * pageSize : loadedPageCount * pageSize + 1
Broader Context: Cursor-Based Pagination
This also lays groundwork for cursor-based pagination (from related discussion):
Cursor Storage Concept
- Store cursor per infinite query session
- QueryFn retrieves stored cursor and passes to API
- If page under-fills, keep fetching until limit met
- Requires consistent ORDER BY matching cursor's order
Metadata Columns Approach
To support multiple concurrent queries with different ORDER BY on same collection:
- Session ID isolates pagination sets
- Each session tracks its own cursor/position
- Prevents "page mixing" when different orderings are used
Primary Key Tie-breaker
- Using PK as final ORDER BY element provides deterministic ordering
- Enables DB-native cursor positioning
- Could be enforced when cursor mode is enabled
Cursor + Offset Hybrid
Potentially support starting with offset then switching to cursor:
// Jump to page 5 via offset
// Then infinite scroll from there via cursor
useLiveInfiniteQuery(query, {
pageSize: 20,
initialOffset: 100 // Start at item 100
// Then use cursor from API
})
Benefits
- ✅ QueryCollection: No wasteful +1 fetch when API provides hasNextPage
- ✅ Backward compatible: Collections without metadata fall back to peek ahead
- ✅ Foundation for cursors: Enables future cursor-based pagination
- ✅ Session isolation: Multiple concurrent infinite queries work correctly
- ✅ Extensible: Can store other metadata (totalCount, sync progress, etc.)
Open Questions
- Storage location: Collection property vs separate registry?
- API surface: Methods on collection vs separate pagination manager?
- Cursor implementation: Should we tackle cursors now or separate issue?
- Order validation: Should we detect/prevent ORDER BY changes within session?
- Cursor + offset: Should both be supported simultaneously or exclusive modes?
Related Use Cases
Beyond hasNextPage, this could also support:
- Total count for "X of Y" UI
- Sync progress tracking
- Stream positions/snapshots
- Transaction IDs for Electric collections
- Custom pagination metadata from APIs
Problem
Currently,
useLiveInfiniteQueryuses a "peek ahead" pattern to determine if there's a next page:This works well for:
But it's wasteful for QueryCollection where users control the
queryFnand their API already returns pagination metadata:The extra item fetch wastes:
Proposed Solution
Add collection-level pagination metadata storage:
Session ID from Query Predicates
Session ID should be a deterministic hash of:
This enables:
Implementation using existing
serializePredicatespattern:Usage in QueryCollection
Updated useLiveInfiniteQuery
Broader Context: Cursor-Based Pagination
This also lays groundwork for cursor-based pagination (from related discussion):
Cursor Storage Concept
Metadata Columns Approach
To support multiple concurrent queries with different ORDER BY on same collection:
Primary Key Tie-breaker
Cursor + Offset Hybrid
Potentially support starting with offset then switching to cursor:
Benefits
Open Questions
Related Use Cases
Beyond hasNextPage, this could also support: