authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-03-09 12:45:34-07:00
committergravatar for jmarminge@gmail.comJulius Marminge <jmarminge@gmail.com> 2026-03-09 14:02:29-07:00
loge765051f148bf6006e30118eb47f5d33ea9e32be
treea78bc9ecd1a220d5bfe924b44338b205738e88ad
parentf5e4df5cab247021501876358fd72c7196cd96b9

Use React Pacer throttler for domain event snapshot sync

- 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

1 files changed, 19 insertions(+), 24 deletions(-)

apps/web/src/routes/__root.tsx+19-24
......@@ -8,6 +8,7 @@ import {
88} from "@tanstack/react-router";
99import { useEffect, useRef } from "react";
1010import { QueryClient, useQueryClient } from "@tanstack/react-query";
11import { Throttler } from "@tanstack/react-pacer";
1112
1213import { APP_DISPLAY_NAME } from "../branding";
1314import { Button } from "../components/ui/button";
......@@ -150,6 +151,7 @@ function EventRouter() {
150151 let latestSequence = 0;
151152 let syncing = false;
152153 let pending = false;
154 let needsProviderInvalidation = false;
153155
154156 const flushSnapshotSync = async (): Promise<void> => {
155157 const snapshot = await api.orchestration.getSnapshot();
......@@ -185,21 +187,20 @@ function EventRouter() {
185187 syncing = false;
186188 };
187189
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 );
203204
204205 const unsubDomainEvent = api.orchestration.onDomainEvent((event) => {
205206 if (event.sequence <= latestSequence) {
......@@ -209,12 +210,7 @@ function EventRouter() {
209210 if (event.type === "thread.turn-diff-completed" || event.type === "thread.reverted") {
210211 needsProviderInvalidation = true;
211212 }
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();
218214 });
219215 const unsubTerminalEvent = api.terminal.onEvent((event) => {
220216 const hasRunningSubprocess = terminalRunningSubprocessFromEvent(event);
......@@ -299,9 +295,8 @@ function EventRouter() {
299295 });
300296 return () => {
301297 disposed = true;
302 if (batchTimer !== null) {
303 clearTimeout(batchTimer);
304 }
298 needsProviderInvalidation = false;
299 domainEventFlushThrottler.cancel();
305300 unsubDomainEvent();
306301 unsubTerminalEvent();
307302 unsubWelcome();