Skip to content

Sustained event burst invalidates the same key twice #1462

Closed as not planned
Closed as not planned

Description

@soarkit
  • 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

  1. Drive the real useKbEvents with a 750 ms document burst at 15 ms intervals.
  2. Count invalidations for the same query key.
  3. 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

  1. WaylandYang commented on Oct 30, 2026

    @WaylandYang
    Contributor

    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.

  2. WaylandYang commented on Oct 30, 2026

    @WaylandYang
    Contributor

    Closing as by design. The comments are corrected in #1465.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions