From df75467361b109d3b0774258bc1dcb1d9698175e Mon Sep 17 00:00:00 2001 From: Wayland Yang Date: Fri, 30 Oct 2026 17:52:19 +0800 Subject: [PATCH] The overview deadline rides the open fast window, and the coalescer's comments say window, not silence When the overview timer fired it flushed the fast layer, so keys still inside their 300 ms window went out early and a steady push paid one extra fast refetch per overview window. It now only pushes into the fast layer. With no fast window open the overview lands up to SETTLE_MS later. Reconnect and unmount still flush through flushAll, so prefix folding is unchanged. The window starts at the first event and does not reset (#1311), which the comments described as silence. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Wayland Yang --- web/src/useEventReconnect.test.ts | 19 ++++++++++++++++++- web/src/useKbEvents.ts | 17 ++++++++--------- 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/web/src/useEventReconnect.test.ts b/web/src/useEventReconnect.test.ts index c54d22f15..2f0cc17ac 100644 --- a/web/src/useEventReconnect.test.ts +++ b/web/src/useEventReconnect.test.ts @@ -236,9 +236,26 @@ describe("KB stream reconnect", () => { source.emit("graph"); await vi.advanceTimersByTimeAsync(100); } - await vi.advanceTimersByTimeAsync(OVERVIEW_SETTLE_MS - 1_000 - 1); + await vi.advanceTimersByTimeAsync(OVERVIEW_SETTLE_MS - 1_000); + expect(overview.fetch).toHaveBeenCalledTimes(1); + await vi.advanceTimersByTimeAsync(SETTLE_MS); + expect(overview.fetch).toHaveBeenCalledTimes(2); + }); + + it("the overview deadline rides the open fast window instead of flushing it early", async () => { + const client = createClient(); + const overview = await watch(client, ["graph", "kb-a", "overview", 200]); + const neighborhood = await watch(client, ["graph", "kb-a", "neighborhood", "e1"]); + const { source } = mount(() => useKbEvents("kb-a")); + source.emit("open"); + source.emit("graph"); + await vi.advanceTimersByTimeAsync(OVERVIEW_SETTLE_MS - 100); + source.emit("graph"); + await vi.advanceTimersByTimeAsync(SETTLE_MS - 1); + expect(neighborhood.fetch).toHaveBeenCalledTimes(2); expect(overview.fetch).toHaveBeenCalledTimes(1); await vi.advanceTimersByTimeAsync(1); + expect(neighborhood.fetch).toHaveBeenCalledTimes(3); expect(overview.fetch).toHaveBeenCalledTimes(2); }); diff --git a/web/src/useKbEvents.ts b/web/src/useKbEvents.ts index ea7c39fe9..24d5727f2 100644 --- a/web/src/useKbEvents.ts +++ b/web/src/useKbEvents.ts @@ -4,8 +4,9 @@ // // **失效是合并着做的。** 一篇文档抽取时每落一条事实就发一个 graph 事件, // 从前每个事件各失效一次,图谱页在那几秒里把 overview 重取了十几遍—— -// 每次都是同一张图,最后一次才算数。所以事件只把 key 记下来,停顿一小会 -// 再一次性失效:一阵事件只换来一次重取,最后那次一定包含之前所有的变化。 +// 每次都是同一张图,最后一次才算数。所以事件只把 key 记下来,等一个窗口 +// 再一次性失效:窗口里的事件只换来一次重取,最后那次一定包含之前所有的变化。 +// 窗口从第一个事件算起、中途不重置:持续推送时定期刷,不必等推送停下。 // 幂等性没变,只是把「每条都刷」变成「刷最后一条」。 // // 失效不看 staleTime:这里失效的键在 queryDefaults.ts 里放到了 30 秒,靠的就是 @@ -15,14 +16,14 @@ import { partialMatchKey, useQueryClient, type QueryKey } from "@tanstack/react- import { subscribeEvents } from "./eventStream"; import { STREAM_KEYS } from "./queryDefaults"; -/** 一阵事件之间的静默期。抽取落事实的间隔远小于它,人眼看不出这点延迟 */ +/** 合并窗口。抽取落事实的间隔远小于它,人眼看不出这点延迟 */ export const SETTLE_MS = 300; /** 全图 overview 要数遍整个库:推送期间几秒重数一次就够了(#1296) */ export const OVERVIEW_SETTLE_MS = 3_000; /** - * 把一阵失效合并成一次:`push` 只记键,静默 `settleMs` 后一起交给 `invalidate`, + * 把一个窗口里的失效合并成一次:`push` 只记键,第一次 `push` 的 `settleMs` 后一起交给 `invalidate`, * 同一个键及其前缀只失效一次;`flush` 给卸载与重连用,攒着的刷掉而不是丢掉。 * 抽成纯函数是为了能测;订阅那层只是把事件名映射到键。 */ @@ -62,11 +63,9 @@ export function useKbEvents(kbId: string | undefined) { (key) => queryClient.invalidateQueries({ queryKey: key }), SETTLE_MS, ); - // overview 到点也经快的那层刷:重连补上的 ["graph", kbId] 已覆盖它,不再多取一遍 - const overview = createInvalidationCoalescer((key) => { - push(key); - flush(); - }, OVERVIEW_SETTLE_MS); + // overview 到点也交给快的那层:搭上正在走的窗口,不把别的键提前刷掉; + // 重连补上的 ["graph", kbId] 已覆盖它,不再多取一遍 + const overview = createInvalidationCoalescer((key) => push(key), OVERVIEW_SETTLE_MS); const pushGraph = () => { push(["graph", kbId, "neighborhood"]); overview.push(["graph", kbId, "overview"]);