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"]);