diff options
| author | Adam Malczewski <[email protected]> | 2026-10-10 01:36:39 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-10-10 01:36:39 +0900 |
| commit | 551a7a050e8cd25e795026a1060c9d5b5ea028bf (patch) | |
| tree | cba29eb5ada26778c09aea7cc767e2ddf665bce8 /src/app | |
| parent | d8381e201cb1407a939b496642a0803aec58a47d (diff) | |
| download | dispatch-web-551a7a050e8cd25e795026a1060c9d5b5ea028bf.tar.gz dispatch-web-551a7a050e8cd25e795026a1060c9d5b5ea028bf.zip | |
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.
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(); |
