| ... | ... | @@ -8,6 +8,7 @@ import { |
| 8 | 8 | } from "@tanstack/react-router"; |
| 9 | 9 | import { useEffect, useRef } from "react"; |
| 10 | 10 | import { QueryClient, useQueryClient } from "@tanstack/react-query"; |
| 11 | import { Throttler } from "@tanstack/react-pacer"; |
| 11 | 12 | |
| 12 | 13 | import { APP_DISPLAY_NAME } from "../branding"; |
| 13 | 14 | import { Button } from "../components/ui/button"; |
| ... | ... | @@ -150,6 +151,7 @@ function EventRouter() { |
| 150 | 151 | let latestSequence = 0; |
| 151 | 152 | let syncing = false; |
| 152 | 153 | let pending = false; |
| 154 | let needsProviderInvalidation = false; |
| 153 | 155 | |
| 154 | 156 | const flushSnapshotSync = async (): Promise<void> => { |
| 155 | 157 | const snapshot = await api.orchestration.getSnapshot(); |
| ... | ... | @@ -185,21 +187,20 @@ function EventRouter() { |
| 185 | 187 | syncing = false; |
| 186 | 188 | }; |
| 187 | 189 | |
| 188 | | void syncSnapshot().catch(() => undefined); |
| 189 | | |
| 190 | | // Batch domain events: collect over a short window then sync once. |
| 191 | | const DOMAIN_EVENT_BATCH_MS = 100; |
| 192 | | let batchTimer: ReturnType<typeof setTimeout> | null = null; |
| 193 | | let needsProviderInvalidation = false; |
| 194 | | |
| 195 | | const flushBatch = () => { |
| 196 | | batchTimer = null; |
| 197 | | if (needsProviderInvalidation) { |
| 198 | | needsProviderInvalidation = false; |
| 199 | | void queryClient.invalidateQueries({ queryKey: providerQueryKeys.all }); |
| 200 | | } |
| 201 | | void syncSnapshot(); |
| 202 | | }; |
| 190 | const domainEventFlushThrottler = new Throttler( |
| 191 | () => { |
| 192 | if (needsProviderInvalidation) { |
| 193 | needsProviderInvalidation = false; |
| 194 | void queryClient.invalidateQueries({ queryKey: providerQueryKeys.all }); |
| 195 | } |
| 196 | void syncSnapshot(); |
| 197 | }, |
| 198 | { |
| 199 | wait: 100, |
| 200 | leading: false, |
| 201 | trailing: true, |
| 202 | }, |
| 203 | ); |
| 203 | 204 | |
| 204 | 205 | const unsubDomainEvent = api.orchestration.onDomainEvent((event) => { |
| 205 | 206 | if (event.sequence <= latestSequence) { |
| ... | ... | @@ -209,12 +210,7 @@ function EventRouter() { |
| 209 | 210 | if (event.type === "thread.turn-diff-completed" || event.type === "thread.reverted") { |
| 210 | 211 | needsProviderInvalidation = true; |
| 211 | 212 | } |
| 212 | | // Throttle (not debounce): schedule a flush if none is pending. |
| 213 | | // This guarantees at most one syncSnapshot per batch window |
| 214 | | // without starving the UI during sustained event bursts. |
| 215 | | if (batchTimer === null) { |
| 216 | | batchTimer = setTimeout(flushBatch, DOMAIN_EVENT_BATCH_MS); |
| 217 | | } |
| 213 | domainEventFlushThrottler.maybeExecute(); |
| 218 | 214 | }); |
| 219 | 215 | const unsubTerminalEvent = api.terminal.onEvent((event) => { |
| 220 | 216 | const hasRunningSubprocess = terminalRunningSubprocessFromEvent(event); |
| ... | ... | @@ -299,9 +295,8 @@ function EventRouter() { |
| 299 | 295 | }); |
| 300 | 296 | return () => { |
| 301 | 297 | disposed = true; |
| 302 | | if (batchTimer !== null) { |
| 303 | | clearTimeout(batchTimer); |
| 304 | | } |
| 298 | needsProviderInvalidation = false; |
| 299 | domainEventFlushThrottler.cancel(); |
| 305 | 300 | unsubDomainEvent(); |
| 306 | 301 | unsubTerminalEvent(); |
| 307 | 302 | unsubWelcome(); |