Skip to content

fix(frontend): resolve stale closure in chatSearch.ts debounce (#1225) - #1292

Merged
github-actions[bot] merged 1 commit into
leojay-net:mainfrom
Mercy017:fix/issue-1225-debounce-stale-closure
Jul 27, 2026
Merged

fix(frontend): resolve stale closure in chatSearch.ts debounce (#1225)#1292
github-actions[bot] merged 1 commit into
leojay-net:mainfrom
Mercy017:fix/issue-1225-debounce-stale-closure

Conversation

@Mercy017

Copy link
Copy Markdown
Contributor

Root cause

debounce() in chatSearch.ts had three related bugs:

  1. Stale fn closurefn was captured at debounce() call time and never updated. When callers recreated their callback (e.g. an inline arrow function on each render), the debounced wrapper kept calling the original, stale function.
  2. Stale timer referencetimer was never reset to null after the timeout fired. Future clearTimeout(timer) calls operated on an already-expired ID, giving a false sense of cancellation.
  3. No cancel() method — there was no way for a caller to abort a pending invocation on component unmount, causing callbacks to fire against unmounted state.

Fix (src/lib/chatSearch.ts)

  • Introduced latestFn variable inside debounce; added updateFn(newFn) on the returned function so callers can swap the underlying callback without rebuilding the debounced wrapper.
  • timer = null is now set inside the setTimeout callback so the reference is always accurate.
  • Added cancel() method that clears and nulls the pending timer.
  • Exported the DebouncedFn<T> interface for typed usage.

Regression tests (src/lib/chatSearch.test.ts)

Six new tests under describe('debounce') covering:

  • Basic fire-after-delay
  • Debounce (rapid successive calls)
  • updateFn calls the new fn, not the original (stale-closure regression)
  • cancel() prevents firing
  • Timer reference is null after fire (no stale ID)
  • Call after cancel works correctly

Closes #1225
Closes #1223

…y-net#1225)

Root cause: debounce() captured fn at creation time via closure. If the
caller's callback was recreated on each render the debounced wrapper
kept calling the original (stale) fn. Timer ref was also never reset to
null after firing, leaving a dangling ID.

Fix:
- Store latestFn internally; expose updateFn() so callers can swap the
  callback without recreating the debounced wrapper.
- Reset timer = null inside the timeout callback.
- Add cancel() so callers can abort pending timers on unmount.

Regression tests cover all three failure modes.
@drips-wave

drips-wave Bot commented Jul 27, 2026

Copy link
Copy Markdown

@Mercy017 Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@github-actions
github-actions Bot merged commit d74ca75 into leojay-net:main Jul 27, 2026
3 checks passed
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.

fix(frontend): resolve stale closure in chatSearch.ts fix(frontend): resolve stale closure in chatHistory.ts

2 participants