Skip to content

Add collection-level pagination metadata support to avoid wasteful peek-ahead pattern #863

Description

@KyleAMathews

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

  1. Storage location: Collection property vs separate registry?
  2. API surface: Methods on collection vs separate pagination manager?
  3. Cursor implementation: Should we tackle cursors now or separate issue?
  4. Order validation: Should we detect/prevent ORDER BY changes within session?
  5. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions