summaryrefslogtreecommitdiffhomepage
path: root/src/app
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-10-10 01:36:39 +0900
committerAdam Malczewski <[email protected]>2026-10-10 01:36:39 +0900
commit551a7a050e8cd25e795026a1060c9d5b5ea028bf (patch)
treecba29eb5ada26778c09aea7cc767e2ddf665bce8 /src/app
parentd8381e201cb1407a939b496642a0803aec58a47d (diff)
downloaddispatch-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.ts1
-rw-r--r--src/app/store.svelte.ts29
-rw-r--r--src/app/store.test.ts2
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();