Pagination Contract
This repo uses cursor-based pagination for any list that can grow without bound (messages, inbox conversations, etc.). The goal is: stable ordering, no duplicates between pages, and consistent semantics across all storage backends.
Definitions #
- Each endpoint defines a fixed sort key and direction (ASC/DESC), and a cursor type that matches that sort key.
PageDirdescribes the relation to the cursor in terms of the sort key, not UI scrolling.Before: items strictly before the cursor.After: items strictly after the cursor.
- Tie-breaker IDs are mandatory to make ordering deterministic.
Cursor Comparison Rules #
For a cursor (ts, id), comparisons are applied relative to the endpoint's display order.
- For
ASCendpoints:Before:(ts < cursor.ts) OR (ts == cursor.ts AND id < cursor.id)After:(ts > cursor.ts) OR (ts == cursor.ts AND id > cursor.id)
- For
DESCendpoints:Before:(ts > cursor.ts) OR (ts == cursor.ts AND id > cursor.id)After:(ts < cursor.ts) OR (ts == cursor.ts AND id < cursor.id)
The DB query must always include an ORDER BY that matches the endpoint order.
Page Fields #
Page<T, C> has:
items: returned in the endpoint's defined order.next: cursor to continue in the samePageDiras the request.prev: cursor to go in the oppositePageDir.
This allows a client to implement infinite scroll by repeatedly calling the same endpoint with:
cursor = page.nextdir = (unchanged)
How To Compute next and prev #
Given the returned items and a request dir:
- For
ASCendpoints:After:next = max,prev = minBefore:next = min,prev = max
- For
DESCendpoints:After:next = min,prev = maxBefore:next = max,prev = min
If items is empty, both cursors are None.
Endpoint Ordering (Current) #
- Messages in a thread:
ORDER BY sent_at DESC, id DESC(DB fetches newest-first; API returns in display order oldest-first)- cursor:
(sent_at, message_id)
- Inbox conversations (DM + Group):
ORDER BY last_activity_at DESC, id DESC- cursor:
(last_activity_at, conversation_id)
- Channels:
ORDER BY created_at ASC, id ASC(stable, oldest-first)- cursor:
(created_at, conversation_id)