GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — fix: stop twelve wrapped calls from holding the app unstable by armando-navarro · Pull Request #3827 · angular/angularfire

Conversation

@armando-navarro

@armando-navarro armando-navarro commented Oct 29, 2026 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Collaborator

Fixes #3826

  • Adds blockUntilFirst: false entries to tools/build.ts for ten callback-taking calls whose first callback a normal load may never run.
  • Replaces the generated fromRef with a hand-written one in src/database/overrides.ts that holds the app only for the value event.
  • Moves the isSupported entry zone log verbosity #3613 meant for firebase/remote-config into the block that exports it.
  • Regenerates the five affected files.

Checklist

  • Issue number for this PR: Eleven wrapped callbacks and remote-config isSupported hold the app unstable until something that may never happen #3826
  • Docs included?: in docs: document the 22.0.0-rc.1 changes #3835, the 22.0.0-rc.1 docs PR, which merges just before the release. Its first commit lists in docs/zones.md the calls that no longer hold the app unstable, and adds a sentence each under stateChanges, auditTrail and fromRef in docs/database.md.
  • Test units included?: yes, see Verification.
  • Breaking change?: no
  • In a clean directory, yarn install, yarn test run successfully?: yes, with npm: npm ci, npm run build, npm run build:jasmine, npm run test:node, npm run test:chrome-headless, npm run lint.

Changes

The mechanism:

  • ɵzoneWrap registers a pending task when a wrapped call is passed a callback.
  • It releases the task when that callback first runs or the returned unsubscribe is called.
  • Until then ApplicationRef.isStable is false: an SSR build's route extraction times out after 30 seconds (the beforeAuthStateChanged imported from @angular/fire/auth makes ng build fail during route extraction #3748 failure), and anything waiting on stability in the browser waits with it.

The ten calls whose first callback is not guaranteed, now { blockUntilFirst: false, logLevel: LogLevel.WARN }:

  • onLog: never stable until some Firebase SDK logs. It returns void, so no unsubscribe can release the task either.
  • onTokenChanged: never stable until a token is obtained, which a first visit with no auto refresh and no protected request never asks for.
  • onChildAdded: never stable on an empty node. A new listener's initial events are only child_added, one per existing child, and value.
  • onChildChanged, onChildMoved, onChildRemoved: never stable until someone changes, moves or removes a child. None fires on attach.
  • off(query, eventType, callback): never stable. It never calls the callback it is given and returns void.
  • enableLogging(fn): never stable until the database SDK logs something.
  • stateChanges and auditTrail (rxfire, default events): never stable on an empty node, since they emit only on child events.
  • Keeping logLevel: LogLevel.WARN means only the pending task changes: callbacks are still re-entered into the zone and the injection context, and a call outside an injection context still prints its per-function line at the warning level.

fromRef(ref, event) in @angular/fire/database, now hand-written:

  • One blockUntilFirst value cannot fit both of its uses: the value event always fires on attach, and a child event on an empty node never fires.
  • The new src/database/overrides.ts wraps rxfire's fromRef twice at module load, once holding the app until the first emission and once not, and picks by event. tools/build.ts marks it { override: true }, so the generated file re-exports it.
  • The exported type is rxfire's own, typeof fromRef.

isSupported in @angular/fire/remote-config, now { blockUntilFirst: false }:

  • With true, every call in a browser with IndexedDB held a pending task until the indexedDB.open it awaits answered, and for good in a browser that never answers.
  • zone log verbosity #3613 added isSupported: { blockUntilFirst: false } for it, matching the analytics and messaging entries, but in the rxfire/remote-config block, which exports no isSupported. Overrides match by name, so it did nothing.
  • build: classify the firebase symbols a version raise would newly wrap #3759 added the check that fails the build on such an entry, deleted this one and deferred the move.

What callers can notice

  • A server render no longer waits for the first batch of onChildAdded children, so the server HTML of a list filled that way can ship before they arrive (reasoned from the wrapper, not run).
  • onValue, object, list and fromRef(ref, ListenEvent.value) deliver a value on attach, empty node or not, and still hold the render.
  • fromRef with a child event no longer holds the render, matching onChildAdded and stateChanges. To make a server render wait for a list's initial contents, use list(ref) or listVal(ref), which start with a one-time read and still hold it.
  • off(ref, eventType, callback) no longer holds the app, but it still detaches nothing, because the wrapper replaces the callback with its own closure before the SDK compares callback identities.
  • The unsubscribe returned by onValue and onChild* is the supported way to detach.
  • Code that awaits ApplicationRef.whenStable() before chained Remote Config work no longer waits for isSupported.
  • Inside isSupported's .then, inject() throws and the zone is the one that attached the .then, at both flag values, so the callback sees no difference.
  • isSupported outside an injection context now logs its per-call line only at the verbose level, as the analytics and messaging copies do.

Verification

  • Measured in Chrome on a zone.js Angular 22 app and a zoneless one, against the Database and Firestore emulators.
  • Each run recorded every ApplicationRef.isStable emission and counted PendingTasks.add calls.
  • At true, the never-firing cases above registered a task that was never released.
  • At true, onChildAdded on a populated node, onValue, onSnapshotsInSync and onTokenChanged with auto refresh on released theirs within 2 to 23 ms, and onChildChanged released as soon as a child was written.
  • At false, no task was registered in any run, and inside every callback that ran inject() worked and, under zone.js, NgZone.isInAngularZone() was true.
  • A fresh Angular 22 SSR app with onLog in provideAppInitializer: ng build fails at the route extractor's 30-second timeout with Routes extraction was aborted when onLog comes from @angular/fire/app, and builds in about 2 s from firebase/app.
  • isSupported at true holds the app unstable until the IndexedDB check answers, tens of milliseconds in Chrome, and for the whole window when indexedDB.open is stubbed to never fire. At false it registers no task.
  • Specs: 19 new in src/app/app.spec.ts, src/app-check/app-check.spec.ts and src/database/database.spec.ts, and 1 in src/remote-config/remote-config.spec.ts. Each asserts whether the call registers a pending task.
  • The app, app-check and database spec files each have a positive control proving the spy sees the wrapper's registrations, and fromRef has one spec per event kind.
  • Dropping onLog, onChildChanged or isSupported from tools/build.ts and rebuilding fails the matching spec, and so does making the hand-written fromRef always wait or never wait.
  • npm run build regenerates with the unrecognized-override check passing, and npm run test:node, npm run test:chrome-headless and npm run lint pass.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
@armando-navarro
fix: stop twelve wrapped calls from holding the app unstable
e669e1d
ɵzoneWrap holds a pending task from the moment a wrapped call gets a
callback until that callback first runs. For onLog, onTokenChanged,
the Database child listeners (onChildAdded on an empty node, the other
three until a later change), off, enableLogging, and the rxfire
streams fromRef, stateChanges and auditTrail, nothing guarantees that
first run, so ApplicationRef.isStable can stay false for good and an
SSR build's route extraction times out, as angular#3748 reported for
beforeAuthStateChanged. The eleven keep logLevel WARN, so only the
pending task changes.

isSupported in remote-config is the twelfth: angular#3613 put its
blockUntilFirst: false entry in the rxfire block, which exports no
isSupported, so it never applied, and angular#3759 removed the dead entry.

Server renders no longer wait for onChildAdded's first batch (onValue,
object and list still wait), and off(ref, type, cb) still detaches
nothing, because the wrapper replaces the callback it is given.

Fixes angular#3826
@armando-navarro
armando-navarro requested a review from tyler-reitz October 29, 2026 23:05
@armando-navarro armando-navarro added backport: 21.1.x Cherry-pick onto the 21.1.x branch for a 21.1 minor release bump: patch Size of the change: a fix or docs change that reaches users comp: app-check App Check (src/app-check). comp: build/pipeline Build, bundling, packaging, release pipeline. comp: core FirebaseApp DI core (src/app). comp: database Realtime Database (src/database). comp: remote-config Remote Config (src/remote-config). comp: ssr Server-side rendering, hydration, @angular/ssr interop. comp: zones Change detection / zone.js / zoneless. type: bug Defect: expected behavior doesn't happen. labels Oct 29, 2026
@armando-navarro armando-navarro mentioned this pull request Oct 30, 2026
fix(core): wrap observer-object handlers like functions, and let a wrapped promise settle after teardown #3829
Merged
tyler-reitz
tyler-reitz requested changes Oct 30, 2026
View reviewed changes

@tyler-reitz tyler-reitz left a comment

Copy link
Copy Markdown
Collaborator

There was a problem hiding this comment.

Choose a reason for hiding this comment

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

One blocking point: the fromRef override applies to every event, not only the child events the body describes.

In rxfire, object(query) is fromRef(query, ListenEvent.value), so after this change object(ref) still holds the render but the documented fromRef(ref, 'value') doesn't. A probe spec on this branch shows it: fromRef(ref(database, rando()), ListenEvent.value) registers 0 pending tasks, while object(ref(database, rando())) registers 1. On main, fromRef is generated with true, so I'd expect it to register one there (read from the generated file, not run).

Overrides match by name, so I don't think tools/build.ts can narrow this, and the tradeoff may well be right, since a child-event fromRef can hang forever. Either way, please add it to "What callers can notice" and to the docs follow-up, plus a spec pinning the 'value' case so the behavior is deliberate rather than incidental.

Non-blocking: two stray blank lines in the database override block in tools/build.ts, after enableLogging and after onChildRemoved.

Everything else checked out: a fresh build regenerates the five files byte-identical to the PR, and Chrome headless passes 132/132 locally.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
@armando-navarro
fix(database): hold the app for fromRef's value event only
23b4c3d
fromRef takes the event to listen for, and one blockUntilFirst value
cannot fit both uses: value always fires on attach, while a child event
may never fire. A hand-written fromRef in overrides.ts
wraps rxfire's fromRef both ways and picks by event, so
fromRef(ref, ListenEvent.value) holds a server render like object(ref)
and child events do not.
@armando-navarro

armando-navarro commented Oct 31, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks, Tyler. Rather than document the value case losing its wait, I made fromRef determine blockUntilFirst per event. It uses { override: true } to wire up a hand-written fromRef in src/database/overrides.ts, which:

  • wraps fromRef with blockUntilFirst: true for ListenEvent.value.
  • wraps fromRef with blockUntilFirst: false for everything else.

So fromRef(ref, ListenEvent.value) holds the render again, like object(ref), and child events still don't.

Specs cover every event: value registers exactly one task, and each of the four child events registers none. Making the selector always wait, never wait, or wait for any child event fails the matching spec.

The PR description now says the above.

The two blank lines are intentional: each marks where the comment above it stops applying, so orderByChild and endAt don't read as covered by it.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@armando-navarro
armando-navarro requested a review from tyler-reitz October 31, 2026 09:50
@armando-navarro armando-navarro mentioned this pull request Oct 31, 2026
docs: document the 22.0.0-rc.1 changes #3835
Open
tyler-reitz
tyler-reitz approved these changes Nov 2, 2026
View reviewed changes

@tyler-reitz tyler-reitz left a comment

Copy link
Copy Markdown
Collaborator

There was a problem hiding this comment.

Choose a reason for hiding this comment

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

Thanks, the per-event fromRef override resolves my blocking point. I confirmed the new specs go red when the override is forced to never wait (value spec) and always wait (all four child specs), and a fresh build regenerates the five files byte-identical. One small thing for the squash: the first commit's body still lists fromRef among the calls that no longer hold the app, so please drop it from that list in the final message.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
@armando-navarro
Merge branch 'main' into a60-blocking-callbacks
06c6d99
@armando-navarro
armando-navarro merged commit c593ecd into angular:main Nov 4, 2026
42 of 43 checks passed

Uh oh!

There was an error while loading. Please reload this page.

@armando-navarro armando-navarro added this to the 22.0.0-rc.1 milestone Nov 4, 2026
@armando-navarro armando-navarro mentioned this pull request Nov 5, 2026
feat(zones)!: remove callback pending tasks right after the callback #3837
Merged
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Reviewers

@tyler-reitz tyler-reitz tyler-reitz approved these changes

Uh oh!

There was an error while loading. Please reload this page.

Assignees

No one assigned

Labels

backport: 21.1.x Cherry-pick onto the 21.1.x branch for a 21.1 minor release bump: patch Size of the change: a fix or docs change that reaches users comp: app-check App Check (src/app-check). comp: build/pipeline Build, bundling, packaging, release pipeline. comp: core FirebaseApp DI core (src/app). comp: database Realtime Database (src/database). comp: remote-config Remote Config (src/remote-config). comp: ssr Server-side rendering, hydration, @angular/ssr interop. comp: zones Change detection / zone.js / zoneless. type: bug Defect: expected behavior doesn't happen.

Projects

None yet

Milestone

22.0.0-rc.1

Development

Successfully merging this pull request may close these issues.

Eleven wrapped callbacks and remote-config isSupported hold the app unstable until something that may never happen

Uh oh!

There was an error while loading. Please reload this page.

2 participants

@armando-navarro @tyler-reitz
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.
You are visitor 01402503 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.