fix(core): cancel stale debounce timers - #70430
Open
rmnsnpk wants to merge 1 commit into
Open
Conversation
JeanMeche
approved these changes
Aug 27, 2026
JeanMeche
left a comment
Member
There was a problem hiding this comment.
Great catch and set of tests !
Member
|
Can you make sure the squash all the commit together in the |
Author
Sure |
The debounce() utility scheduled a setTimeout when using a numeric wait value, but the scheduled timer was never cleared when a new value arrived, the observable threw, or the injector was destroyed. This caused pending timers to fire after invalidation and leak beyond the owner's lifecycle. Refactor timer cancellation into a dedicated helper and track the pending timer id so any stale timer is cleared on new values or teardown. Adds tests covering cancellation on new values, errors, and injector destruction.
rmnsnpk
force-pushed
the
fix-debounced-timer-leak
branch
from
August 27, 2026 12:07
d0f948a to
9adcde8
Compare
JeanMeche
approved these changes
Aug 28, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
PR Checklist
Please check if your PR fulfills the following requirements:
PR Type
What kind of change does this PR introduce?
What is the current behavior?
When
debounced()is given a numericwait, thesetTimeoutscheduled on each sourcechange is never cancelled. Superseded timers stay queued until they fire, at which point
the
active === resultguard discards their result. The resource reports the correctvalue, so there is no incorrect state — but the timers outlive their usefulness:
its delay, retaining its callback closure and the captured value. In a
fakeAsynctestthis fails the spec with
1 timer(s) still in the queue., which is what a consumer hitswhen a component holding a
debounced()is torn down mid-wait.surplus timer wakes up only to be discarded by the guard.
Issue Number: N/A
What is the new behavior?
The pending timer is tracked and cleared once it becomes obsolete, in three places:
The timer id is also released when the timer fires, so a completed timer is never cleared
afterwards — in a browser its id may already have been reused by an unrelated timer.
Cancellation deliberately happens after the equality check that returns early, so a
value that is
equalto the one already pending leaves the running timer counting down.This is why the cleanup is explicit rather than done through the effect's
onCleanup:the effect re-runs whenever the source notifies, including for equal values, and
cancelling on that path would kill a live debounce with nothing rescheduled, leaving the
resource stuck in
loading.No public API or observable state transitions change.
Does this PR introduce a breaking change?
Other information
Four specs were added under a
timer cleanupblock indebounce_spec.ts. Three of themfail without the fix:
timer for "a" not cleared,Expected 0 to be 2timer outlived the injectortimer survived the errorThey assert on
clearTimeoutvia a spy, comparing timer ids by identity — in Node a timerid is a
Timeoutobject, which no deep-equality matcher should try to serialize.Verified with:
70 specs, 0 failures with the fix; 3 failures on the parent commit.