Repository navigation
Sustained event burst invalidates the same key twice #1462
Description
- Repository:
deeplethe/utopia - Target commit:
fa77a956797e86fc0465971d890c59b85ee971ec - File:
web/src/useKbEvents.ts:29-54
Summary
sustained event burst invalidates the same key twice.
Root cause
push arms a timer only when it is null; flush clears the timer and pending keys, so a key arriving around the flush can start a second window and duplicate invalidation.
Affected code
web/src/useKbEvents.ts:
19 export const SETTLE_MS = 300;
20
21 /** 全图 overview 要数遍整个库:推送期间几秒重数一次就够了(#1296) */
22 export const OVERVIEW_SETTLE_MS = 3_000;
23
24 /**
25 * 把一阵失效合并成一次:`push` 只记键,静默 `settleMs` 后一起交给 `invalidate`,
26 * 同一个键及其前缀只失效一次;`flush` 给卸载与重连用,攒着的刷掉而不是丢掉。
27 * 抽成纯函数是为了能测;订阅那层只是把事件名映射到键。
28 */
29 export function createInvalidationCoalescer(
30 invalidate: (key: QueryKey) => void,
31 settleMs: number,
32 ) {
33 const pending = new Map<string, QueryKey>();
34 let timer: ReturnType<typeof setTimeout> | null = null;
35 const flush = () => {
36 if (timer !== null) {
37 clearTimeout(timer);
38 timer = null;
39 }
40 const keys = [...pending.values()];
41 pending.clear();
42 for (const key of keys) {
43 // 正常 graph 事件可能已排了 ["graph", kbId, "neighborhood"],重连又补上 ["graph", kbId]:
44 // 前缀已经覆盖后者,不能取消刚开始的请求再重取一遍。
45 if (keys.some((prefix) => prefix.length < key.length && partialMatchKey(key, prefix))) continue;
46 invalidate(key);
47 }
48 };
49 const push = (...keys: QueryKey[]) => {
50 for (const key of keys) pending.set(JSON.stringify(key), key);
51 if (timer === null) timer = setTimeout(flush, settleMs);
52 };
53 return { push, flush };
54 }
55
56 export function useKbEvents(kbId: string | undefined) {
57 const queryClient = useQueryClient();
58
59 useEffect(() => {
Expected behavior
one event burst should produce at most one invalidation/refetch per key or key prefix after the burst settles
Actual behavior
During a sustained extraction burst, createInvalidationCoalescer's push only arms a timer when timer is null; flush clears the timer and pending keys, so the next event starts a new 300ms window. The same burst therefore invalidates/refetches neighborhood and other fast keys every 300ms, and overview every 3s, instead of once after the burst.
Steps to reproduce
- Drive the real
useKbEventswith a 750 ms document burst at 15 ms intervals. - Count invalidations for the same query key.
- Compare with a single event or a burst shorter than the settle window.
Suggested fix
Re-check pending keys after flushing, or use a generation counter so a flush only retires the keys it observed.
Impact
Duplicate refetches during bursts, adding load and rendering churn.
Activity
WaylandYang commentedon Oct 30, 2026
This is by design: #1311 starts the window at the first event and does not reset it, so a steady push refreshes periodically instead of waiting for it to stop. The comments did read like a debounce, and #1465 corrects them. I'll close this when that merges.
WaylandYang commentedon Oct 30, 2026
Closing as by design. The comments are corrected in #1465.