Repository navigation
feat(zones)!: remove callback pending tasks right after the callback - #3837
Conversation
A wrapped callback read (onSnapshot, onAuthStateChanged, onValue and the other callback-taking functions wrapped with a pending task) removed Angular's pending task through setTimeout(taskDone, 0), so the task outlived the first callback by one setTimeout(fn, 0) delay, and the same on unsubscribe, next to a 2021 TODO asking for a microtask instead. The task is now removed right after the callback body and right after the unsubscribe call, the same way Angular's own pendingUntilEvent removes its task, inside a finally so a callback that throws does not leave its task behind. The task now goes away before any setTimeout(fn, 0) callback registered when the callback was delivered, and a zone.js server render reports stable about a millisecond sooner after a single callback read's first value. BREAKING CHANGE: a callback read's pending task is removed right after its first callback runs, and right after unsubscribe, instead of one setTimeout(fn, 0) delay later. whenStable() and server rendering can resolve up to that much sooner after the first value. Angular's own change-detection tick still runs on a timer, so a test that advances fake timers before awaiting whenStable() still needs the advance.
angular#3837 removes a callback read's pending task right after the callback instead of one setTimeout(fn, 0) delay later. As a breaking change it needs a section in the upgrade guide: what moves sooner, and which tests notice. Refs angular#3836
tyler-reitz
left a comment
There was a problem hiding this comment.
Requesting changes for one spec.
The unsubscribe spec passes even when the task is never released. Deleting taskDone() from the unsubscribe finally and rebuilding leaves karma at 142/142, because the 500ms fallback in observeRelease() resolves to the same all-false value that spec expects. The other three assert afterCallbackBody: true, which the fallback can't produce. Having the remover resolve with released: true and the fallback with released: false would separate the two.
Non-blocking: this conflicts with #3829 in src/zones.ts and src/zones.spec.ts. When resolving, I'd put #3829's injector.destroyed early return inside the new try, so a handler that runs after teardown still releases its task.
Otherwise checked: all four new specs fail against main's zones.ts, and the branch builds with no generated-file drift and passes 142/142 locally.
…rotask angular#3829's destroyed-injector early return now sits inside the try, so the finally removes the pending task on that path too. The argument loop is main's, without the TODO this branch deleted. angular#3829's spec comment about a 0 ms timer now gives the reason the per-spec counter still holds. # Conflicts: # src/zones.spec.ts # src/zones.ts
The unsubscribe spec expected all four recorded fields false, which is what the 500 ms fallback records when the remover is never called, so it passed with the unsubscribe removal deleted. Each result now carries released, true from the remover and false from the fallback. A new spec removes the task for a handler that fires after its child environment injector is destroyed. It fails if the destroyed-injector early return is moved outside the try.
|
Thanks for catching that, @tyler-reitz. Both points are in. Unsubscribe spec
Merge with #3829
|
Checklist
setTimeout(fn, 0)later #3836yarn install,yarn testrun successfully? yes, asnpm ciandnpm testDescription
Removes the pending task a callback-taking wrapper registers in a
finallyright after the callback body, and right after the unsubscribe call, instead of throughsetTimeout(taskDone, 0)insrc/zones.ts. Deletes the 2021 TODO next to the wrapping call that asked for this.finallyruns whether the callback returns or throws, so a throwing callback does not leave its task behind, which the timer also guaranteed.pendingUntilEventworks: it forwards the value and then removes its task synchronously.What this grants, measured on Angular 22.2 zone.js and zoneless apps, before and after:
setTimeout(fn, 0)callback registered at delivery, in every run: a synthetic callback in Chrome and Firefox, the unsubscribe path, and the realonSnapshotagainst the Firestore emulator. With Realtime Database on cached data, where the SDK invokes the callback inside the registering call, the task is removed inside that call.What does not change:
whenStable()resolves under fake timers. Angular's change-detection tick is itself a timer, so a test that advances fake timers before awaitingwhenStable()still needs the advance, before and after.What upgrading apps may notice:
whenStable()and server rendering follow a callback read's first value onesetTimeout(fn, 0)delay sooner.Verification:
src/zones.spec.ts, each observing the remover functionPendingTasks.add()returns. All four fail onmainand pass here:setTimeout(fn, 0)callback registered just before deliverynpm run lint,npm run test:node,npm run test:chrome-headlessandnpm run test:firefox-headlesspass on the branch.