Skip to content

perf(flow-chat): guard stale subscription cleanup and add MRU fast path for reader state - #3831

Open
Raing5Days wants to merge 1 commit into
openBitFun:mainfrom
Raing5Days:perf/auto-opcase-auto-readerstate-01-1108-2035
Open

Raing5Days wants to merge 1 commit into
openBitFun:mainfrom
Raing5Days:perf/auto-opcase-auto-readerstate-01-1108-2035

Conversation

@Raing5Days

Copy link
Copy Markdown
Contributor

Problem

The reader store had three cleanup paths that only checked whether an owner set had become empty — never whether the set being deleted was still the one registered for that key. A stale lease release or unsubscribe closure that ran after the same turn/group/scope had been re-created could therefore evict the newer bucket, silently dropping live subscribers and leaving interaction:* / group:* keys stuck at their previous value. On top of that, every set and every notifyProjection allocated a fresh dispatch closure via forEach(listener => listener()), subscribe allocated a Set and an extra map.set even when a bucket already existed, and scope resolution always performed a delete + set LRU bump on the backing Map, turning the hottest lookup path into needless map churn.

Solution

  • Identity-guarded cleanup. Lease and hold releases now delete only when the set they own is still the registered one: !owners.size && this.turnInteractions.get(turn) === owners (same pattern for groupHolds). The subscribe disposer returns early when delete was a no-op and only removes the key when this.listeners.get(key) === listeners, so a stale closure can never evict a newer bucket that happens to share the key.
  • Allocation-free dispatch. notifyProjection and the listener fan-out in set iterate the live Set with for..of instead of forEach + arrow. No closure per notification, and Set iteration is mutation-tolerant, so a listener that unsubscribes mid-dispatch is handled correctly.
  • Single-lookup subscribe. One get, and the bucket is created as new Set([listener]) only on a miss.
  • Bounded MRU fast path. A module-level mostRecentScope skips the delete + set bump when the resolved scope is already the most recently used one — correct because Map#set on an existing key does not reorder. The registry stays capped at MAX_REMEMBERED_VIEWS = 24; nothing became unbounded and no counters or caches were added beyond that single string.

The public surface is unchanged: callers see the same synchronous read/write semantics, the same revision bumps, and the same cleanup contracts. Internally each mutation still replaces state rather than mutating shared structures, so immutability at the boundary is preserved.

Verification

  • Unit tests for the store covering subscribe/unsubscribe, lease release ordering (stale disposer after re-subscribe), and eviction at the 24-entry cap.
  • Explicitly exercised unsubscribing from inside a dispatch to confirm later listeners are still notified and order is stable.
  • Confirmed the MRU fast path and the slow path leave the backing Map in identical contents and ordering, and that the registry never exceeds its cap.
  • Typechecked; no changes to exported API; zero regressions in flow-chat timeline projection behavior.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant