Repository navigation
fix(component): extend message pages to the order boundary #321
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -618,6 +618,7 @@ export const cloneMessageBatch = internalMutation({ | |
| paginationOpts: args.paginationOpts, | ||
| statuses: args.statuses, | ||
| upToAndIncludingMessageId: args.upToAndIncludingMessageId, | ||
| completeOrders: false, | ||
| }); | ||
|
|
||
| const existing = | ||
|
|
@@ -736,6 +737,12 @@ export const listMessagesByThreadIdArgs = { | |
| paginationOpts: v.optional(paginationOptsValidator), | ||
| statuses: v.optional(v.array(vMessageStatus)), | ||
| upToAndIncludingMessageId: v.optional(v.id("messages")), | ||
| /** | ||
| * Extend each descending page to the end of its oldest order so a UI | ||
| * message is never split across pages. Defaults to true. Pass false when | ||
| * only the newest rows matter and the page size must be exact. | ||
| */ | ||
| completeOrders: v.optional(v.boolean()), | ||
| }; | ||
| export const listMessagesByThreadId = query({ | ||
| args: listMessagesByThreadIdArgs, | ||
|
|
@@ -781,18 +788,67 @@ async function listMessagesByThreadIdHandler( | |
| ), | ||
| ), | ||
| ); | ||
| const paginationOpts = args.paginationOpts ?? { | ||
| numItems: DEFAULT_RECENT_MESSAGES, | ||
| cursor: null, | ||
| }; | ||
| const messages = await mergedStream(streams, ["order", "stepOrder"]).paginate( | ||
| args.paginationOpts ?? { | ||
| numItems: DEFAULT_RECENT_MESSAGES, | ||
| cursor: null, | ||
| }, | ||
| paginationOpts, | ||
| ); | ||
| if (messages.page.length === 0) { | ||
| messages.isDone = true; | ||
| return messages; | ||
| } | ||
| // A UI message is assembled from every row sharing one order, so a page | ||
| // that ends partway through an order leaves the client with a fragment it | ||
| // cannot render or reconcile with its streaming counterpart. Extend the | ||
| // page to the order boundary so the continue cursor never splits an order. | ||
| // Only "desc" is handled: the remaining rows of the oldest order are the | ||
| // ones with a lower stepOrder, a bounded set. With endCursor the range is | ||
| // fixed by the caller, and a caller-supplied row or byte limit, or a page | ||
| // the paginator already asked to split, means the caller is managing | ||
| // transaction budget and the page must not grow. An order with more than | ||
| // MAX_ORDER_COMPLETION_MESSAGES remaining rows is completed across | ||
| // subsequent pages instead. | ||
| const oldest = messages.page[messages.page.length - 1]; | ||
| if ( | ||
| order === "desc" && | ||
| args.completeOrders !== false && | ||
| !messages.isDone && | ||
| !messages.pageStatus && | ||
| !paginationOpts.endCursor && | ||
| paginationOpts.maximumRowsRead === undefined && | ||
| paginationOpts.maximumBytesRead === undefined && | ||
|
Comment on lines
+820
to
+821
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ideally they could pass these and they would only come into play if the limits were actually hit: usually these are really conservative values to avoid running out of memory.. if they are hit, we could still return a complete order by returning fewer items, but then we'd need to return a matching endCursor, which you're already handling a version of below |
||
| oldest.stepOrder > 0 | ||
| ) { | ||
| const paginateFrom = (numItems: number) => | ||
| mergedStream(streams, ["order", "stepOrder"]).paginate({ | ||
| numItems, | ||
| cursor: messages.continueCursor, | ||
| }); | ||
| let completion = await paginateFrom( | ||
| Math.min(oldest.stepOrder, MAX_ORDER_COMPLETION_MESSAGES), | ||
| ); | ||
| // stepOrder overestimates when a filter or deletion removed rows from the | ||
| // order. The stream is ordered, so the order's rows are a prefix of the | ||
| // page; re-read exactly that prefix so the cursor stops at the boundary | ||
| // rather than past rows the caller never received. | ||
| const kept = completion.page.findIndex((m) => m.order !== oldest.order); | ||
| if (kept === 0) { | ||
| return messages; | ||
| } | ||
| if (kept > 0) { | ||
| completion = await paginateFrom(kept); | ||
| } | ||
| messages.page.push(...completion.page); | ||
| messages.continueCursor = completion.continueCursor; | ||
| messages.isDone = completion.isDone; | ||
| } | ||
| return messages; | ||
| } | ||
|
|
||
| const MAX_ORDER_COMPLETION_MESSAGES = 1_000; | ||
|
|
||
| export const getMessagesByIds = query({ | ||
| args: { messageIds: v.array(v.id("messages")) }, | ||
| handler: async (ctx, args) => { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It also seems reasonable to request a complete order in ascending order, so let's at least fail a request that passes "asc" order and
completeOrders: true