- Replace manual timeout-based domain event batching with `Throttler` - Keep provider query invalidation batched with trailing 100ms flushes - Cancel throttler and reset invalidation flag during EventRouter cleanup
Unified · Split
| @@ -8,6 +8,7 @@ import { · 7 unmodified lines | ||
| 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() { · 136 unmodified lines | ||
| 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() { · 29 unmodified lines | ||
| 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() { · 3 unmodified lines | ||
| 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() { · 78 unmodified lines | ||
| 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(); |