⚡ Sangsakala Proxy 🏠 Home
Skip to content

fix(forms): isolate debounce abort listeners from reactive tracking - #71772

Open
splincode wants to merge 1 commit into
angular:mainfrom
splincode:fix/debounce-abort-tracking
Open

splincode wants to merge 1 commit into
angular:mainfrom
splincode:fix/debounce-abort-tracking

Conversation

@splincode

Copy link
Copy Markdown
Contributor

What changes

Abort listeners currently run inside the pendingSync linked-signal computation. Signal writes throw even in production, and reads become accidental dependencies that can cause a later typed value to be discarded. A completed debounce also retains its controller and gets aborted on a later touch.

Run listeners untracked and clear the controller when its debounce completes. Check orphaned/aborted/superseded state before reading pending state after the await.

Related to #71631. This deliberately keeps the linked signal and the existing lazy cancellation timing after direct model writes; it does not introduce an effect per field or promise eager cancellation. Public API signatures are unchanged.

Reproduction and production results

The three regression specs in this PR fail on main at 2d12a5e8f457f183545fa14364b6f8982ec21452 and pass with the fix.

The same cases were independently run in an AOT, ES2022, minified browser fixture built from that base and the patched source, with ngDevMode=false, ngJitMode=false, and runtime isDevMode() === false. It exercises public controlValue.set() and markAsTouched() field APIs.

Production scenario Before After
Complete debounce, then touch 1 unnecessary abort 0 aborts
Abort listener writes a signal after a direct model update 1 error; indicator stays true no error; indicator false
Earlier abort listener reads an unrelated signal, then it changes model keeps direct, losing second model contains second

Cost and memory comparison

Seven alternating baseline/patched pairs, fresh browser contexts, Chromium 158.0.8117.0 on Linux x64. One live form, 100 warmup edits, followed by two batches of 1000 completed edit/touch cycles. Heap readings follow three forced GCs.

Median, per 1000-cycle batch Before After
Unnecessary abort callbacks 1000 0
Time, second batch 14.5 ms 5.9 ms
Heap increase, first batch 132,672 bytes 134,728 bytes
Further heap increase, second batch 5,736 bytes 1,016 bytes

Timing ranges for the second batch: 11.0–17.6 ms before, 3.8–10.1 ms after. These are microbenchmark totals, not page-latency guarantees.

An unbounded memory leak was not established by this experiment. Both heaps largely plateau after warmup; no substantial memory-saving claim is made. The demonstrated production impact is lost input, failed signal writes, and redundant cancellation work.

Validation

  • Node: 556 specs, 0 failures.
  • Chromium: 362 specs, 0 failures, including existing removed-field/orphan coverage.
  • ng-dev format and git diff --check pass.
  • Local browser-test infrastructure needed a loopback fallback only when this container rejects os.networkInterfaces(); no Angular/test assertions were changed. Bazel telemetry was disabled.

angular-71631-production-repro.zip

@pullapprove
pullapprove Bot requested a review from JeanMeche November 2, 2026 16:57
@ngbot ngbot Bot added this to the Backlog milestone Nov 2, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please ask your agent to not create new test files every time.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair point, I’ll move these cases into the existing debounce specs.

Run custom abort listeners outside the pending-sync computation and clear
completed controllers before syncing their values. Guard orphaned and
superseded fields before reading pending state after an asynchronous debounce.

Preserve the existing lazy cancellation timing for direct model writes.

Related to angular#71631
@splincode
splincode force-pushed the fix/debounce-abort-tracking branch from cc86cbe to 79aa94c Compare November 2, 2026 17:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants