From 551a7a050e8cd25e795026a1060c9d5b5ea028bf Mon Sep 17 00:00:00 2001 From: Adam Malczewski Date: Sat, 10 Oct 2026 01:36:39 +0900 Subject: perf(chat): fast open of long in-flight turns - Batch live chat events: fold a burst in one pass per animation frame (rAF + timeout fallback for hidden tabs) instead of one reactive cycle per WS frame. Flushed before send/resync to keep ordering. - De-dup the late-join replay against committed history: provisional chunks of the in-flight turn already persisted (CR-6 per-step persistence) are hidden by aligned-prefix matching (fail-safe: mismatch shows, never hides). - Memoize chunks/turnMetrics getters by state identity. - LazyCollapse: tool/thinking card bodies mount only on first open. - content-visibility:auto on transcript rows. - scripts/probe-replay.ts + bench-pure.ts for measuring the replay path. --- src/app/App.test.ts | 1 + src/app/store.svelte.ts | 29 +++++++++++++++++++++++++++++ src/app/store.test.ts | 2 ++ 3 files changed, 32 insertions(+) (limited to 'src/app') diff --git a/src/app/App.test.ts b/src/app/App.test.ts index 026c5c7..26f5cdb 100644 --- a/src/app/App.test.ts +++ b/src/app/App.test.ts @@ -438,6 +438,7 @@ describe("App component interaction tests", () => { socketFactory: () => ws, fetchImpl: fakeFetchImpl(), localStorage: createFakeStorage(), + chatFlushScheduler: null, }); ws.resolveOpen(); diff --git a/src/app/store.svelte.ts b/src/app/store.svelte.ts index 73b19bc..afccced 100644 --- a/src/app/store.svelte.ts +++ b/src/app/store.svelte.ts @@ -648,6 +648,13 @@ export interface CreateAppStoreOptions { localStorage?: Storage; /** The workspace to scope to at boot (its URL slug); "default" if absent. */ workspaceId?: string; + /** + * How live chat events are batched before folding into a chat store. + * Default: once per animation frame (with a timeout fallback for hidden + * tabs). `null` = fold every event synchronously (tests that assert on the + * transcript immediately after feeding a frame). + */ + chatFlushScheduler?: ((flush: () => void) => void) | null; } function createHistorySync(httpBase: string, fetchImpl: typeof fetch): HistorySync { @@ -871,6 +878,27 @@ export function createAppStore(opts?: CreateAppStoreOptions): AppStore { // Deltas are routed here in addition to `chatStores`. const watchStores = new Map(); + /** + * Coalesce live chat events to at most one fold + render per animation + * frame (a late-join replay of a long turn arrives as a burst of WS frames; + * folding each separately re-derives the whole transcript per frame). A + * timeout backs up rAF, which browsers pause in hidden tabs — a backgrounded + * tab still applies its events (just less often). + */ + function scheduleChatFlush(flush: () => void): void { + let ran = false; + const run = (): void => { + if (ran) return; + ran = true; + flush(); + }; + if (typeof requestAnimationFrame === "function") requestAnimationFrame(run); + setTimeout(run, 250); + } + + const chatFlushScheduler = + opts?.chatFlushScheduler === undefined ? scheduleChatFlush : opts.chatFlushScheduler; + function createChatFor(conversationId: string, model: string, workspaceId: string): ChatStore { return createChatStore({ conversationId, @@ -890,6 +918,7 @@ export function createAppStore(opts?: CreateAppStoreOptions): AppStore { // `setChatLimit`. chatLimit: normalizeChatLimit(chatLimitStore.load()), canUnload: () => (unloadGate === null ? true : unloadGate()), + ...(chatFlushScheduler !== null ? { scheduleFlush: chatFlushScheduler } : {}), onError: (context, err) => { reportError(`${context} (conversation: ${conversationId})`, err); }, diff --git a/src/app/store.test.ts b/src/app/store.test.ts index 206eaa4..89e418e 100644 --- a/src/app/store.test.ts +++ b/src/app/store.test.ts @@ -509,6 +509,7 @@ describe("createAppStore", () => { socketFactory: () => ws, fetchImpl: fakeFetchImpl(), localStorage: createFakeStorage(), + chatFlushScheduler: null, }); ws.resolveOpen(); @@ -887,6 +888,7 @@ describe("createAppStore", () => { socketFactory: () => ws, fetchImpl: fakeFetchImpl(), localStorage: createFakeStorage(), + chatFlushScheduler: null, }); ws.resolveOpen(); -- cgit v1.2.3