Repository navigation
feat(zones)!: deliver Observable read values as microtasks - #3833
Merged
armando-navarro merged 2 commits intoNov 4, 2026
Merged
Conversation
The insideAngular scheduler was built over asyncScheduler, so every value from an Observable read reached its subscriber one setInterval turn after Firebase handed it over. A private AsapScheduler instance delivers at the next microtask checkpoint instead, still never synchronously. It is private because rxjs's shared asapScheduler cancels the rest of a batch when one action throws. Measured on Angular 22.2 apps, zone.js and zoneless: delivery moves ahead of a zero-delay timer in every run, change detection and ordering are unchanged, and a server render reports stable about a millisecond sooner. The compat AngularFireAuth streams move too. BREAKING CHANGE: Observable reads (collectionData, docData, authState, user, objectVal, listVal and the rest) now deliver at the next microtask checkpoint instead of after a zero-delay timer. A test that waits only for microtasks now sees a value whose source emits in a microtask. Code that orders its own setTimeout against a read's value sees the timer run after the value, not before. In a zoneless server render, a value arriving between stability and serialization reaches a hand-written TransferState entry but not the HTML. Fixes angular#3832
tyler-reitz
approved these changes
Nov 2, 2026
tyler-reitz
left a comment
Collaborator
There was a problem hiding this comment.
Approving. I confirmed the new spec fails on both counts when asyncScheduler is swapped back in, and passes on the branch. One ask before rc.1: the body says the upgrade-guide entry lands in a separate docs PR, but there isn't one yet and #3835 doesn't mention it. Could you add the entry to #3835 so the breaking change ships with its migration note?
armando-navarro
added a commit
to armando-navarro/angularfire
that referenced
this pull request
Nov 4, 2026
angular#3833 moves every Observable read from a setInterval timer to the next microtask checkpoint. As a breaking change it needs a section in the upgrade guide: which tests now see a value they used to wait for, the setTimeout ordering flip, and the zoneless server render case for hand-written TransferState entries. Refs angular#3832
Collaborator
Author
|
Thanks, Tyler. I've added the docs update in a commit on #3835, a new section "Observable reads deliver as microtasks" in |
armando-navarro
enabled auto-merge (squash)
November 4, 2026 17:40
armando-navarro
added a commit
that referenced
this pull request
Nov 13, 2026
* docs: say which wrapped calls do not hold the app unstable docs/zones.md says callback APIs hold the app unstable until an initial value, so a server render waits for data. #3827 makes a set of calls exceptions because their first callback may never come. Waiting on a callback that never comes would keep the app from ever becoming stable. List those calls in docs/zones.md, and point at onValue, object, list, listVal and fromRef with the value event as the calls that still make a server render wait. In docs/database.md, say the same under stateChanges, auditTrail and fromRef. Refs #3826 * docs: describe firebase 13 support in the v22 upgrade guide #3831 lets AngularFire 22 accept firebase 12 or 13, and the ng update migration now writes the range of the firebase major already installed. The upgrade guide still said the migration writes ^12.18.0 and quoted peer warnings that name only that range. State the supported range and what the migration writes, update the Yarn 1 and pnpm 9/10 warnings to their current wording, and add a section on moving to firebase 13: VertexAIBackend is removed and its packages declare Node 24.12 or later. Refs #3830 * docs: describe microtask delivery in the v22 upgrade guide #3833 moves every Observable read from a setInterval timer to the next microtask checkpoint. As a breaking change it needs a section in the upgrade guide: which tests now see a value they used to wait for, the setTimeout ordering flip, and the zoneless server render case for hand-written TransferState entries. Refs #3832 * docs: describe the callback pending-task change in the v22 upgrade guide #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 #3836 * docs: say a handler passed as an observer object is wrapped too #3829 wraps a handler passed as an observer object, such as { next, error }, the way it wraps a function, so the injection context reaches both forms. Refs #3828 * docs(compat): add a setup section in NgModule and standalone form The compat API's setup steps left docs/install-and-setup.md when #3236 rewrote it for the modular API, and nothing replaced them. docs/compat.md had no setup at all, the Realtime Database pages pointed at an "Installation guide" that no longer covered compat, and no page showed the standalone form. A standalone app with only the modular providers then fails its first compat injection with NG0201 for angularfire2.app.options (#3683). The new section on docs/compat.md gives both forms, says when the modular providers may stay, states the error the missing module produces, and lists the compat module of each product. The twelve compat pages that had an NgModule-only snippet or no setup at all now point at it. Fixes #3683 * docs: say which package managers refuse an incompatible firebase The v22 upgrade guide said an incompatible firebase is refused at install time. Only npm refuses it, and only when package.json requests the incompatible version. Yarn and pnpm install it with a warning. * docs: say which Firestore functions are not zone wrapped #3842 re-exports Firestore's query and reference builders, field values, comparators and setLogLevel without the zone wrapper. In docs/zones.md, add a paragraph listing them, change the switchMap example to a read, since building the query no longer warns, and say the Verbose log level covers only wrapped calls. Refs #3842 * docs(auth): build the server app with initializeBrowserOrServerApp The server-rendering guide's step 4 becomes the one call, the five details become three, the App Check token gets a relay section, the response gets a Cache-Control header, and the manual callback stays in a "Building the app yourself" section that step 4 points at, for apps that prefer it or whose AngularFire version has no such function. The release modes say what a throwing ngOnDestroy does to a timed release. * docs(auth): describe the sample as an example of the one-call server app The sample app now has one provideFirebaseApp with initializeBrowserOrServerApp in its shared app.config.ts (#3845), so the sentence about its split client and server files no longer holds. Refs #3843 * docs: say how to move from 22.0.0-rc.0 in the v22 upgrade guide The guide covered upgrades from 21, a 21 release candidate and a 22 canary build, but not from the first 22 release candidate. ng add saves a prerelease as an exact version, so an app on 22.0.0-rc.0 stays there until ng update @angular/fire@next moves it. That command also runs migration-v22 again, which reports "No changes made" for a firebase entry already inside the supported range. Refs #3834 * docs(auth): say which hooks stop a releaseAfterRender release The caveat on releaseAfterRender said a throwing ngOnDestroy "stops that teardown". It now says the release is a DestroyRef.onDestroy callback, that Angular runs it after every service's and component's ngOnDestroy, and that an ngOnDestroy method or an ApplicationRef.onDestroy listener that throws keeps it from running. The default release, which does not run during the injector's destroy, is unaffected either way. A new paragraph says to call initializeBrowserOrServerApp in app.config.ts rather than a route's providers: a route's child injector is not destroyed with the request, so a release registered there never runs. Refs #3844 * docs(auth): say the default release still happens from a route's providers Tyler asked on #3844 whether the default release, now tied to a root service, releases a server app built in a lazy route's providers. It does: the request's root injector is destroyed, the route's child injector is not, so only a releaseAfterRender release registered there is lost. Checked with a child environment injector that is never destroyed, in both modes. Refs #3844 * docs: add onConfigUpdate to the calls that do not hold the app unstable #3829 set onConfigUpdate to blockUntilFirst: false in tools/build.ts, because its observer runs only when a fetched template differs from the active one. The list in docs/zones.md left it out. Found in the #3835 review. Refs #3829 * docs(auth): say a second provideFirebaseApp can render signed out The bullet said a second registration makes a signed-in render fail on a freshly started server. With more than one registration, FirebaseApp resolves to the default app. Run on fresh servers, a second plain initializeApp(config) registration creates that app on the first request, so signed-in pages render signed out with no error. Only when no registration creates the default app, as with two initializeBrowserOrServerApp registrations, does the render fail, and only until a signed-out request creates it. Found in the #3835 review. Refs #3844
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.
Checklist
yarn install,yarn testrun successfully? yes, asnpm ciandnpm testDescription
Builds the
insideAngularscheduler insrc/zones.tsover a privateAsapSchedulerinstance instead of rxjs'sasyncScheduler, so every value from an Observable-returning read reaches the subscriber at the next microtask checkpoint instead of after asetIntervalturn.asapSchedulerbecause the shared one runs every action scheduled in a turn as one batch and cancels the rest of the batch when one action throws, which would let another library's action drop a delivery.AsapSchedulerandAsapActioncome fromrxjs/internal/scheduler/, a path rxjs 7.8 lists in itsexportsmap but keeps underinternal.What this grants, measured on Angular 22.2 zone.js and zoneless apps, before and after:
collectionDataagainst the Firestore emulator and the compatauthStateagainst the Auth emulator in Chrome.What does not change:
What upgrading apps may notice:
fakeAsyncwithflushMicrotasks()alone) now sees a value whose source emits in a microtask, as Auth and a promise-based mock do. Firestore hands its snapshots over in asetTimeout, so Firestore reads still need a timer to advance. Tests that advance timers are unchanged.setTimeoutwhen a read emits sees that timer run after the value arrives, not before.TransferStateentry but not the HTML, because the zoneless change-detection pass is itself a later timer. Before this change such a value reached neither.AngularFireAuthstreams move with the modular reads. CompatAngularFireFunctionsandAngularFireMessagingonly hand over their SDK instance through this scheduler, and compat Database and Firestore do not use it.Verification:
src/zones.spec.ts: a wrapped Observable's value arrives asynchronously, inside the Angular zone, and before a zero-delay timer registered when the source emitted, and the delivery scheduler is anAsapSchedulerthat is not rxjs's shared instance. Fails onmain, passes here.npm run lint,npm run test:node,npm run test:chrome-headlessandnpm run test:firefox-headlesspass on the branch.