Skip to content

Commit 2f7ff00

Browse files
authored
Batch streamed message.updates through the rAF flush (#2413)
Batch streamed message.updates through the same rAF flush as content
1 parent 3b4e090 commit 2f7ff00

1 file changed

Lines changed: 39 additions & 18 deletions

File tree

src/routes/conversation/[id]/+page.svelte

Lines changed: 39 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,11 @@
1111
import { ERROR_MESSAGES, error } from "$lib/stores/errors";
1212
import { findCurrentModel } from "$lib/utils/models";
1313
import type { Message } from "$lib/types/Message";
14-
import { MessageUpdateStatus, MessageUpdateType } from "$lib/types/MessageUpdate";
14+
import {
15+
MessageUpdateStatus,
16+
MessageUpdateType,
17+
type MessageUpdate,
18+
} from "$lib/types/MessageUpdate";
1519
import { useConversationsStore } from "$lib/stores/conversations.svelte";
1620
import file2base64 from "$lib/utils/file2base64";
1721
import { addChildren } from "$lib/utils/tree/addChildren";
@@ -292,11 +296,26 @@
292296
let lastUpdateTime = new Date();
293297
let frameFlushScheduled = false;
294298
299+
// Local authoritative copy of message.updates during streaming.
300+
// Assigning the reactive field on every network chunk (~100/s on fast
301+
// providers) re-triggers the full markdown block derivation per chunk;
302+
// buffering here keeps `.updates` on the same flush cadence as
303+
// `.content`. All readers inside this loop must use the buffer, not
304+
// the $state field, or they would see stale data between flushes.
305+
let updatesBuffer: MessageUpdate[] = messageToWriteTo.updates ?? [];
306+
let updatesDirty = false;
307+
295308
const flushBuffer = (currentTime: Date) => {
296-
if (buffer.length === 0) return;
297-
messageToWriteTo.content += buffer;
298-
buffer = "";
299-
lastUpdateTime = currentTime;
309+
if (buffer.length === 0 && !updatesDirty) return;
310+
if (buffer.length > 0) {
311+
messageToWriteTo.content += buffer;
312+
buffer = "";
313+
lastUpdateTime = currentTime;
314+
}
315+
if (updatesDirty) {
316+
messageToWriteTo.updates = updatesBuffer;
317+
updatesDirty = false;
318+
}
300319
};
301320
302321
const scheduleFrameFlush = () => {
@@ -315,6 +334,10 @@
315334
316335
for await (const update of messageUpdatesIterator) {
317336
if ($isAborted) {
337+
// Commit anything still sitting in the content/updates buffers:
338+
// the navigation-abort path skips the post-stream refresh, so a
339+
// dropped buffer here would be lost from the UI for good.
340+
flushBuffer(new Date());
318341
messageUpdatesAbortController.abort();
319342
return;
320343
}
@@ -331,28 +354,29 @@
331354
332355
if (!isKeepAlive) {
333356
if (update.type === MessageUpdateType.Stream) {
334-
const existingUpdates = messageToWriteTo.updates ?? [];
335-
const lastUpdate = existingUpdates.at(-1);
357+
const lastUpdate = updatesBuffer.at(-1);
336358
if (lastUpdate?.type === MessageUpdateType.Stream) {
337359
// Create fresh objects/arrays so the UI reacts to merged tokens
338360
const merged = {
339361
...lastUpdate,
340362
token: (lastUpdate.token ?? "") + (update.token ?? ""),
341363
};
342-
messageToWriteTo.updates = [...existingUpdates.slice(0, -1), merged];
364+
updatesBuffer = [...updatesBuffer.slice(0, -1), merged];
343365
} else {
344-
messageToWriteTo.updates = [...existingUpdates, update];
366+
updatesBuffer = [...updatesBuffer, update];
345367
}
346368
} else {
347-
messageToWriteTo.updates = [...(messageToWriteTo.updates ?? []), update];
369+
updatesBuffer = [...updatesBuffer, update];
348370
}
371+
updatesDirty = true;
349372
}
350373
const currentTime = new Date();
351374
352375
// If we receive a non-stream update (e.g. tool/status/final answer),
353-
// flush any buffered stream tokens so the UI doesn't appear to cut
354-
// mid-sentence while tools are running or the final answer arrives.
355-
if (update.type !== MessageUpdateType.Stream && buffer.length > 0) {
376+
// flush buffered stream tokens and pending updates so the UI doesn't
377+
// appear to cut mid-sentence while tools are running or the final
378+
// answer arrives.
379+
if (update.type !== MessageUpdateType.Stream) {
356380
flushBuffer(currentTime);
357381
}
358382
@@ -377,8 +401,7 @@
377401
// pre‑tool streamed content when appropriate.
378402
const finalText = update.text ?? "";
379403
const isInterrupted = update.interrupted === true;
380-
const hadTools =
381-
messageToWriteTo.updates?.some((u) => u.type === MessageUpdateType.Tool) ?? false;
404+
const hadTools = updatesBuffer.some((u) => u.type === MessageUpdateType.Tool);
382405
383406
if (isInterrupted) {
384407
if (!messageToWriteTo.content) {
@@ -463,9 +486,7 @@
463486
}
464487
}
465488
466-
if (buffer.length > 0) {
467-
flushBuffer(new Date());
468-
}
489+
flushBuffer(new Date());
469490
} catch (err) {
470491
if ($isAborted || (err instanceof DOMException && err.name === "AbortError")) {
471492
// User-initiated abort, not an error

0 commit comments

Comments
 (0)