Welcome back! In the previous chapter, Latest Event Anchoring, we learned how to load the "Live" view of a chatβthe most recent messages.
But a chat interface isn't just about what is happening now. It's a history book. Users expect to scroll up and see what was discussed yesterday or last week.
In this chapter, we will solve the "Scroll Up" problem using the Reverse Pagination Strategy.
Imagine you are reading a very long ancient scroll. You start reading at the very bottom (the most recent text).
When you reach the top of the section you are reading, you realize there is more text missing above it. To get the rest, you need to tell the librarian exactly where you stopped so they can unroll the previous section.
The Problem: You cannot simply say "Give me Page 2." In a live chat, new messages are constantly arriving, which shifts "Page 2" around. Relying on page numbers would result in skipping messages or seeing duplicates.
The Solution: We use a Cursor System. We take the ID of the oldest message currently on your screen (the top one) and use it as a bookmark. We ask the database: "Give me the 100 messages that came immediately before this ID."
before_id
This strategy relies on a specific piece of data called a "cursor." In our system, this cursor is the before_id.
firstId (the oldest ID in our current list) becomes the before_id for our next request.
We have simplified this logic into a function called fetchOlderEvents.
Here is the flow of how you would use it in your application code.
1. Identify the Bookmark
When you fetched the latest events (in Chapter 1), the system gave you a "bookmark" called firstId.
// From Chapter 1
const latestPage = await fetchLatestEvents(ctx, 100);
// This ID represents the top-most message on the user's screen
const bookmark = latestPage.firstId;
// This boolean tells us if there is even more history to fetch
const canScrollUp = latestPage.hasMore;
2. Fetch the Previous Batch When the user scrolls to the top, use that bookmark to get older data.
if (canScrollUp && bookmark) {
// Ask for history specifically BEFORE our bookmark
const olderPage = await fetchOlderEvents(ctx, bookmark, 100);
console.log(olderPage.events); // Messages from the past!
// Update the bookmark for the next time the user scrolls
const newBookmark = olderPage.firstId;
}
What happens here?
hasMore is true (the librarian says there is more text).bookmark) to the function.Let's visualize the timeline. We are traversing time backwards.
The code for this is located in sessionHistory.ts. It follows the same pattern as the function in the previous chapter but uses a different parameter.
1. The Specialist Function
This function's job is to ensure we are asking for before_id specifically.
// File: sessionHistory.ts
/** Older page: events immediately before `beforeId` cursor. */
export async function fetchOlderEvents(
ctx: HistoryAuthCtx,
beforeId: string,
limit = HISTORY_PAGE_SIZE,
): Promise<HistoryPage | null> {
// We pass 'before_id' to the generic fetcher
return fetchPage(ctx, { limit, before_id: beforeId }, 'fetchOlderEvents')
}
beforeId as a required argument.fetchPage (our Defensive API Wrapper), passing { before_id: beforeId } in the parameters object.2. The Shared Logic
Just like in Chapter 1, this function relies on fetchPage to handle the network call. It uses the ctx we will define in Session Authorization Context to handle security.
// File: sessionHistory.ts
async function fetchPage(
ctx: HistoryAuthCtx,
params: Record<string, string | number | boolean>,
label: string,
): Promise<HistoryPage | null> {
// The 'params' object now contains { before_id: "..." }
const resp = await axios
.get<SessionEventsResponse>(ctx.baseUrl, {
headers: ctx.headers,
params, // Axios attaches this to the URL: ?before_id=...
})
.catch(() => null)
// ...
fetchPage is versatile. In Chapter 1, it received anchor_to_latest. In Chapter 2, it receives before_id. It treats them both simply as params to send to the server.3. Updating the Pointers
When the data returns, we need to extract the new pointers so the user can scroll again later. This is part of the Data Normalization Layer.
// ... inside fetchPage ...
return {
events: Array.isArray(resp.data.data) ? resp.data.data : [],
// This is crucial: The API gives us the ID of the OLDEST item
// in this new batch. This becomes our NEXT bookmark.
firstId: resp.data.first_id,
hasMore: resp.data.has_more,
}
}
You now understand the Reverse Pagination Strategy.
By using the before_id cursor, we can stitch together a conversation history perfectly, regardless of how many new messages are coming in. We treat the top message as a "bookmark" to fetch the previous chapter of the story.
However, you might have noticed we are making network requests (axios.get) inside these functions. What if the network fails? What if the API sends back garbage data?
To make our assistant robust, we need to wrap these calls in a safety layer.
Next Chapter: Defensive API Wrapper
Generated by Code IQ