diff options
Diffstat (limited to 'src/app')
| -rw-r--r-- | src/app/App.test.ts | 1 | ||||
| -rw-r--r-- | src/app/store.svelte.ts | 29 | ||||
| -rw-r--r-- | src/app/store.test.ts | 2 |
3 files changed, 32 insertions, 0 deletions
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<string, ChatStore>(); + /** + * 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(); |
