From e669e1d1207dcfb2db1dacfe3fc2ba16bd9f3803 Mon Sep 17 00:00:00 2001 From: Armando Navarro Date: Thu, 29 Oct 2026 16:04:24 -0700 Subject: [PATCH 1/2] fix: stop twelve wrapped calls from holding the app unstable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ɵ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 #3748 reported for beforeAuthStateChanged. The eleven keep logLevel WARN, so only the pending task changes. isSupported in remote-config is the twelfth: #3613 put its blockUntilFirst: false entry in the rxfire block, which exports no isSupported, so it never applied, and #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 #3826 --- src/app-check/app-check.spec.ts | 41 +++++++++- src/app-check/firebase.ts | 2 +- src/app/app.spec.ts | 27 ++++++- src/app/firebase.ts | 2 +- src/database/database.spec.ts | 100 +++++++++++++++++++++++- src/database/firebase.ts | 12 +-- src/database/rxfire.ts | 6 +- src/remote-config/firebase.ts | 2 +- src/remote-config/remote-config.spec.ts | 10 ++- tools/build.ts | 28 ++++++- 10 files changed, 207 insertions(+), 23 deletions(-) diff --git a/src/app-check/app-check.spec.ts b/src/app-check/app-check.spec.ts index dd88f1e13..861b18dbc 100644 --- a/src/app-check/app-check.spec.ts +++ b/src/app-check/app-check.spec.ts @@ -1,7 +1,7 @@ -import { PLATFORM_ID } from '@angular/core'; +import { PLATFORM_ID, PendingTasks } from '@angular/core'; import { TestBed } from '@angular/core/testing'; -import { FirebaseApp, getApp, initializeApp, provideFirebaseApp } from '@angular/fire/app'; -import { APP_CHECK_ON_SERVER, AppCheck, CustomProvider, initializeAppCheck, provideAppCheck } from '@angular/fire/app-check'; +import { FirebaseApp, deleteApp, getApp, initializeApp, provideFirebaseApp } from '@angular/fire/app'; +import { APP_CHECK_ON_SERVER, AppCheck, CustomProvider, initializeAppCheck, onTokenChanged, provideAppCheck } from '@angular/fire/app-check'; import { Auth, connectAuthEmulator, getAuth, provideAuth } from '@angular/fire/auth'; import { COMMON_CONFIG } from '../test-config'; import { rando } from '../utils'; @@ -109,4 +109,39 @@ describe('AppCheck', () => { expect(appCheck.app).toEqual(TestBed.inject(FirebaseApp)); }); + describe('zone wrapping', () => { + let appCheck: AppCheck; + let addTask: jasmine.Spy; + let cleanups: (() => void)[]; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + provideFirebaseApp(() => initializeApp(COMMON_CONFIG, appName)), + provideTestAppCheck(), + ], + }); + appCheck = TestBed.inject(AppCheck); + addTask = spyOn(TestBed.inject(PendingTasks), 'add').and.callThrough(); + cleanups = []; + }); + + afterEach(() => { + // Listeners re-enter this test's injector, so none may outlive the TestBed. + cleanups.forEach(cleanup => cleanup()); + }); + + it('onTokenChanged does not hold the application unstable', () => { + const unsubscribe = TestBed.runInInjectionContext(() => onTokenChanged(appCheck, () => undefined)); + cleanups.push(unsubscribe); + expect(addTask).not.toHaveBeenCalled(); + }); + + it('a blocking call in the same context does register a task', async () => { + await TestBed.runInInjectionContext(() => deleteApp(initializeApp(COMMON_CONFIG, rando()))); + expect(addTask).toHaveBeenCalled(); + }); + + }); + }); diff --git a/src/app-check/firebase.ts b/src/app-check/firebase.ts index bfaefcc29..e7928dafb 100644 --- a/src/app-check/firebase.ts +++ b/src/app-check/firebase.ts @@ -12,5 +12,5 @@ import { export const getLimitedUseToken = ɵzoneWrap(_getLimitedUseToken, true, 2); export const getToken = ɵzoneWrap(_getToken, true); export const initializeAppCheck = ɵzoneWrap(_initializeAppCheck, true); -export const onTokenChanged = ɵzoneWrap(_onTokenChanged, true); +export const onTokenChanged = ɵzoneWrap(_onTokenChanged, false, 1); export const setTokenAutoRefreshEnabled = ɵzoneWrap(_setTokenAutoRefreshEnabled, true); diff --git a/src/app/app.spec.ts b/src/app/app.spec.ts index 62af613f8..d5262aa9c 100644 --- a/src/app/app.spec.ts +++ b/src/app/app.spec.ts @@ -1,5 +1,6 @@ +import { PendingTasks } from '@angular/core'; import { TestBed } from '@angular/core/testing'; -import { FirebaseApp, initializeApp, provideFirebaseApp } from '@angular/fire/app'; +import { FirebaseApp, deleteApp, initializeApp, onLog, provideFirebaseApp } from '@angular/fire/app'; import { COMMON_CONFIG } from '../test-config'; import { rando } from '../utils'; @@ -30,4 +31,28 @@ describe('FirebaseApp', () => { }); + describe('zone wrapping', () => { + let addTask: jasmine.Spy; + + beforeEach(() => { + addTask = spyOn(TestBed.inject(PendingTasks), 'add').and.callThrough(); + }); + + afterEach(() => { + // The handler onLog installs re-enters this test's injector, so it must not outlive the TestBed. + TestBed.runInInjectionContext(() => onLog(null)); + }); + + it('onLog does not hold the application unstable', () => { + TestBed.runInInjectionContext(() => onLog(() => undefined)); + expect(addTask).not.toHaveBeenCalled(); + }); + + it('a blocking call in the same context does register a task', async () => { + await TestBed.runInInjectionContext(() => deleteApp(initializeApp(COMMON_CONFIG, rando()))); + expect(addTask).toHaveBeenCalled(); + }); + + }); + }); diff --git a/src/app/firebase.ts b/src/app/firebase.ts index 7d9487193..a28f8f453 100644 --- a/src/app/firebase.ts +++ b/src/app/firebase.ts @@ -17,6 +17,6 @@ export const getApp = ɵzoneWrap(_getApp, true); export const getApps = ɵzoneWrap(_getApps, true); export const initializeApp = ɵzoneWrap(_initializeApp, true); export const initializeServerApp = ɵzoneWrap(_initializeServerApp, true); -export const onLog = ɵzoneWrap(_onLog, true); +export const onLog = ɵzoneWrap(_onLog, false, 1); export const registerVersion = ɵzoneWrap(_registerVersion, true); export const setLogLevel = ɵzoneWrap(_setLogLevel, true); diff --git a/src/database/database.spec.ts b/src/database/database.spec.ts index 003464a4f..ae420b59a 100644 --- a/src/database/database.spec.ts +++ b/src/database/database.spec.ts @@ -1,7 +1,29 @@ +import { PendingTasks } from '@angular/core'; import { TestBed } from '@angular/core/testing'; import { FirebaseApp, getApp, initializeApp, provideFirebaseApp } from '@angular/fire/app'; -import { Database, connectDatabaseEmulator, getDatabase, provideDatabase } from '@angular/fire/database'; -import { COMMON_CONFIG } from '../test-config'; +import { + DataSnapshot, + Database, + ListenEvent, + Query, + Unsubscribe, + auditTrail, + connectDatabaseEmulator, + enableLogging, + fromRef, + getDatabase, + list, + off, + onChildAdded, + onChildChanged, + onChildMoved, + onChildRemoved, + onValue, + provideDatabase, + ref, + stateChanges, +} from '@angular/fire/database'; +import { COMMON_CONFIG, databaseEmulatorPort } from '../test-config'; import { rando } from '../utils'; describe('Database', () => { @@ -19,7 +41,7 @@ describe('Database', () => { provideFirebaseApp(() => initializeApp(COMMON_CONFIG, appName)), provideDatabase(() => { providedDatabase = getDatabase(getApp(appName)); - connectDatabaseEmulator(providedDatabase, 'localhost', 9002); + connectDatabaseEmulator(providedDatabase, 'localhost', databaseEmulatorPort); return providedDatabase; }), ], @@ -34,6 +56,78 @@ describe('Database', () => { expect(database.app).toEqual(app); }); + describe('zone wrapping', () => { + let addTask: jasmine.Spy; + let cleanups: (() => void)[]; + + beforeEach(() => { + addTask = spyOn(TestBed.inject(PendingTasks), 'add').and.callThrough(); + cleanups = []; + }); + + afterEach(() => { + // Listeners re-enter this test's injector, so none may outlive the TestBed. + cleanups.forEach(cleanup => cleanup()); + }); + + type ChildListener = (query: Query, callback: (snapshot: DataSnapshot) => void) => Unsubscribe; + const childListeners: [string, ChildListener][] = [ + ['onChildAdded', onChildAdded], + ['onChildChanged', onChildChanged], + ['onChildMoved', onChildMoved], + ['onChildRemoved', onChildRemoved], + ]; + childListeners.forEach(([name, listen]) => { + it(`${name} does not hold the application unstable`, () => { + const unsubscribe = TestBed.runInInjectionContext(() => listen(ref(database, rando()), () => undefined)); + cleanups.push(unsubscribe); + expect(addTask).not.toHaveBeenCalled(); + }); + }); + + it('off does not hold the application unstable', () => { + TestBed.runInInjectionContext(() => off(ref(database, rando()), 'value', () => undefined)); + expect(addTask).not.toHaveBeenCalled(); + }); + + it('enableLogging does not hold the application unstable', () => { + TestBed.runInInjectionContext(() => enableLogging(() => undefined)); + cleanups.push(() => TestBed.runInInjectionContext(() => enableLogging(false))); + expect(addTask).not.toHaveBeenCalled(); + }); + + it('fromRef does not hold the application unstable', () => { + const subscription = TestBed.runInInjectionContext(() => fromRef(ref(database, rando()), ListenEvent.added)).subscribe(); + cleanups.push(() => subscription.unsubscribe()); + expect(addTask).not.toHaveBeenCalled(); + }); + + it('stateChanges does not hold the application unstable', () => { + const subscription = TestBed.runInInjectionContext(() => stateChanges(ref(database, rando()))).subscribe(); + cleanups.push(() => subscription.unsubscribe()); + expect(addTask).not.toHaveBeenCalled(); + }); + + it('auditTrail does not hold the application unstable', () => { + const subscription = TestBed.runInInjectionContext(() => auditTrail(ref(database, rando()))).subscribe(); + cleanups.push(() => subscription.unsubscribe()); + expect(addTask).not.toHaveBeenCalled(); + }); + + it('onValue in the same context does register a task', () => { + const unsubscribe = TestBed.runInInjectionContext(() => onValue(ref(database, rando()), () => undefined)); + cleanups.push(unsubscribe); + expect(addTask).toHaveBeenCalledTimes(1); + }); + + it('list in the same context does register a task', () => { + const subscription = TestBed.runInInjectionContext(() => list(ref(database, rando()))).subscribe(); + cleanups.push(() => subscription.unsubscribe()); + expect(addTask).toHaveBeenCalledTimes(1); + }); + + }); + }); }); diff --git a/src/database/firebase.ts b/src/database/firebase.ts index 738794e8e..756f41588 100644 --- a/src/database/firebase.ts +++ b/src/database/firebase.ts @@ -44,7 +44,7 @@ import { export const child = ɵzoneWrap(_child, true, 2); export const connectDatabaseEmulator = ɵzoneWrap(_connectDatabaseEmulator, true); -export const enableLogging = ɵzoneWrap(_enableLogging, true); +export const enableLogging = ɵzoneWrap(_enableLogging, false, 1); export const endAt = ɵzoneWrap(_endAt, true, 2); export const endBefore = ɵzoneWrap(_endBefore, true, 2); export const equalTo = ɵzoneWrap(_equalTo, true, 2); @@ -57,11 +57,11 @@ export const goOnline = ɵzoneWrap(_goOnline, true); export const increment = ɵzoneWrap(_increment, true, 2); export const limitToFirst = ɵzoneWrap(_limitToFirst, true, 2); export const limitToLast = ɵzoneWrap(_limitToLast, true, 2); -export const off = ɵzoneWrap(_off, true); -export const onChildAdded = ɵzoneWrap(_onChildAdded, true); -export const onChildChanged = ɵzoneWrap(_onChildChanged, true); -export const onChildMoved = ɵzoneWrap(_onChildMoved, true); -export const onChildRemoved = ɵzoneWrap(_onChildRemoved, true); +export const off = ɵzoneWrap(_off, false, 1); +export const onChildAdded = ɵzoneWrap(_onChildAdded, false, 1); +export const onChildChanged = ɵzoneWrap(_onChildChanged, false, 1); +export const onChildMoved = ɵzoneWrap(_onChildMoved, false, 1); +export const onChildRemoved = ɵzoneWrap(_onChildRemoved, false, 1); export const onDisconnect = ɵzoneWrap(_onDisconnect, true); export const onValue = ɵzoneWrap(_onValue, true); export const orderByChild = ɵzoneWrap(_orderByChild, true, 2); diff --git a/src/database/rxfire.ts b/src/database/rxfire.ts index b34588af7..43a2b7de0 100644 --- a/src/database/rxfire.ts +++ b/src/database/rxfire.ts @@ -16,11 +16,11 @@ export { ListenerMethods } from 'rxfire/database'; -export const auditTrail = ɵzoneWrap(_auditTrail, true); +export const auditTrail = ɵzoneWrap(_auditTrail, false, 1); export const changeToData = ɵzoneWrap(_changeToData, true); -export const fromRef = ɵzoneWrap(_fromRef, true); +export const fromRef = ɵzoneWrap(_fromRef, false, 1); export const list = ɵzoneWrap(_list, true); export const listVal = ɵzoneWrap(_listVal, true); export const object = ɵzoneWrap(_object, true); export const objectVal = ɵzoneWrap(_objectVal, true); -export const stateChanges = ɵzoneWrap(_stateChanges, true); +export const stateChanges = ɵzoneWrap(_stateChanges, false, 1); diff --git a/src/remote-config/firebase.ts b/src/remote-config/firebase.ts index 4b5540b2f..bc343a991 100644 --- a/src/remote-config/firebase.ts +++ b/src/remote-config/firebase.ts @@ -28,7 +28,7 @@ export const getNumber = ɵzoneWrap(_getNumber, true); export const getRemoteConfig = ɵzoneWrap(_getRemoteConfig, true); export const getString = ɵzoneWrap(_getString, true); export const getValue = ɵzoneWrap(_getValue, true); -export const isSupported = ɵzoneWrap(_isSupported, true); +export const isSupported = ɵzoneWrap(_isSupported, false); export const onConfigUpdate = ɵzoneWrap(_onConfigUpdate, true); export const setCustomSignals = ɵzoneWrap(_setCustomSignals, true); export const setLogLevel = ɵzoneWrap(_setLogLevel, true); diff --git a/src/remote-config/remote-config.spec.ts b/src/remote-config/remote-config.spec.ts index fc2842062..ec3dcd775 100644 --- a/src/remote-config/remote-config.spec.ts +++ b/src/remote-config/remote-config.spec.ts @@ -1,6 +1,7 @@ +import { PendingTasks } from '@angular/core'; import { TestBed } from '@angular/core/testing'; import { FirebaseApp, getApp, initializeApp, provideFirebaseApp } from '@angular/fire/app'; -import { RemoteConfig, getRemoteConfig, provideRemoteConfig } from '@angular/fire/remote-config'; +import { RemoteConfig, getRemoteConfig, isSupported, provideRemoteConfig } from '@angular/fire/remote-config'; import { COMMON_CONFIG } from '../test-config'; import { rando } from '../utils'; @@ -33,6 +34,13 @@ describe('RemoteConfig', () => { expect(remoteConfig.app).toEqual(app); }); + it('isSupported does not hold the application unstable', async () => { + const addTask = spyOn(TestBed.inject(PendingTasks), 'add').and.callThrough(); + const supported = TestBed.runInInjectionContext(isSupported); + expect(addTask).not.toHaveBeenCalled(); + expect(typeof await supported).toBe('boolean'); + }); + }); }); diff --git a/tools/build.ts b/tools/build.ts index 36f11be9e..6df089cf8 100644 --- a/tools/build.ts +++ b/tools/build.ts @@ -161,9 +161,14 @@ ${exportedZoneWrappedFns} setUserId: { logLevel: LogLevel.VERBOSE }, setUserProperties: { logLevel: LogLevel.VERBOSE }, }), - reexport('app', 'firebase', 'firebase/app', tsKeys()), + reexport('app', 'firebase', 'firebase/app', tsKeys(), { + // The callback runs only when an SDK logs: `blockUntilFirst: false` otherwise `ApplicationRef.isStable` may never become `true`. + onLog: { blockUntilFirst: false, logLevel: LogLevel.WARN }, + }), reexport('app-check', 'firebase', 'firebase/app-check', tsKeys(), { getLimitedUseToken: { logLevel: LogLevel.VERBOSE }, + // The callback runs only once a token is obtained, which a first visit may never request: `blockUntilFirst: false` otherwise `ApplicationRef.isStable` may never become `true`. + onTokenChanged: { blockUntilFirst: false, logLevel: LogLevel.WARN }, }), reexport('auth', 'rxfire', 'rxfire/auth', tsKeys()), reexport('auth', 'firebase', 'firebase/auth', tsKeys(), { @@ -218,15 +223,30 @@ ${exportedZoneWrappedFns} verifyBeforeUpdateEmail: { logLevel: LogLevel.VERBOSE }, verifyPasswordResetCode: { logLevel: LogLevel.VERBOSE }, }), - reexport('database', 'rxfire', 'rxfire/database', tsKeys()), + reexport('database', 'rxfire', 'rxfire/database', tsKeys(), { + // These emit only on child events, which an empty node never produces: `blockUntilFirst: false` otherwise `ApplicationRef.isStable` may never become `true`. + auditTrail: { blockUntilFirst: false, logLevel: LogLevel.WARN }, + fromRef: { blockUntilFirst: false, logLevel: LogLevel.WARN }, + stateChanges: { blockUntilFirst: false, logLevel: LogLevel.WARN }, + }), reexport('database', 'firebase', 'firebase/database', tsKeys(), { child: { logLevel: LogLevel.VERBOSE }, + // The logger callback runs only when a live connection logs: `blockUntilFirst: false` otherwise `ApplicationRef.isStable` may never become `true`. + enableLogging: { blockUntilFirst: false, logLevel: LogLevel.WARN }, + endAt: { logLevel: LogLevel.VERBOSE }, endBefore: { logLevel: LogLevel.VERBOSE }, equalTo: { logLevel: LogLevel.VERBOSE }, increment: { logLevel: LogLevel.VERBOSE }, limitToFirst: { logLevel: LogLevel.VERBOSE }, limitToLast: { logLevel: LogLevel.VERBOSE }, + // The detach call never runs the callback it is given, and a child listener's callback first runs on a later change, or never on an empty node: `blockUntilFirst: false` otherwise `ApplicationRef.isStable` may never become `true`. + off: { blockUntilFirst: false, logLevel: LogLevel.WARN }, + onChildAdded: { blockUntilFirst: false, logLevel: LogLevel.WARN }, + onChildChanged: { blockUntilFirst: false, logLevel: LogLevel.WARN }, + onChildMoved: { blockUntilFirst: false, logLevel: LogLevel.WARN }, + onChildRemoved: { blockUntilFirst: false, logLevel: LogLevel.WARN }, + orderByChild: { logLevel: LogLevel.VERBOSE }, orderByKey: { logLevel: LogLevel.VERBOSE }, orderByPriority: { logLevel: LogLevel.VERBOSE }, @@ -278,7 +298,9 @@ ${exportedZoneWrappedFns} getBoolean: { exportName: 'getBooleanChanges' }, getAll: { exportName: 'getAllChanges' }, }), - reexport('remote-config', 'firebase', 'firebase/remote-config', tsKeys()), + reexport('remote-config', 'firebase', 'firebase/remote-config', tsKeys(), { + isSupported: { blockUntilFirst: false }, + }), reexport('storage', 'rxfire', 'rxfire/storage', tsKeys(), { getDownloadURL: null, getMetadata: null, From 23b4c3d85dedf81f2dbdf27deb3c24b109f8376e Mon Sep 17 00:00:00 2001 From: Armando Navarro Date: Sat, 31 Oct 2026 02:32:25 -0700 Subject: [PATCH 2/2] fix(database): hold the app for fromRef's value event only 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. --- src/database/database.spec.ts | 14 +++++++++++--- src/database/overrides.ts | 9 +++++++++ src/database/rxfire.ts | 6 ++++-- tools/build.ts | 4 +++- 4 files changed, 27 insertions(+), 6 deletions(-) create mode 100644 src/database/overrides.ts diff --git a/src/database/database.spec.ts b/src/database/database.spec.ts index ae420b59a..608461b82 100644 --- a/src/database/database.spec.ts +++ b/src/database/database.spec.ts @@ -96,10 +96,18 @@ describe('Database', () => { expect(addTask).not.toHaveBeenCalled(); }); - it('fromRef does not hold the application unstable', () => { - const subscription = TestBed.runInInjectionContext(() => fromRef(ref(database, rando()), ListenEvent.added)).subscribe(); + [ListenEvent.added, ListenEvent.changed, ListenEvent.moved, ListenEvent.removed].forEach(event => { + it(`fromRef on ${event} does not hold the application unstable`, () => { + const subscription = TestBed.runInInjectionContext(() => fromRef(ref(database, rando()), event)).subscribe(); + cleanups.push(() => subscription.unsubscribe()); + expect(addTask).not.toHaveBeenCalled(); + }); + }); + + it('fromRef on the value event registers a pending task', () => { + const subscription = TestBed.runInInjectionContext(() => fromRef(ref(database, rando()), ListenEvent.value)).subscribe(); cleanups.push(() => subscription.unsubscribe()); - expect(addTask).not.toHaveBeenCalled(); + expect(addTask).toHaveBeenCalledTimes(1); }); it('stateChanges does not hold the application unstable', () => { diff --git a/src/database/overrides.ts b/src/database/overrides.ts new file mode 100644 index 000000000..18904433c --- /dev/null +++ b/src/database/overrides.ts @@ -0,0 +1,9 @@ +import { LogLevel, ɵzoneWrap } from '@angular/fire'; +import { ListenEvent, fromRef as _fromRef } from 'rxfire/database'; + +const fromRefUntilFirst = ɵzoneWrap(_fromRef, true, LogLevel.WARN); +const fromRefWithoutWaiting = ɵzoneWrap(_fromRef, false, LogLevel.WARN); + +// Only the value event fires on attach whatever the node holds. A child event may never fire. +export const fromRef: typeof _fromRef = (ref, event) => + event === ListenEvent.value ? fromRefUntilFirst(ref, event) : fromRefWithoutWaiting(ref, event); diff --git a/src/database/rxfire.ts b/src/database/rxfire.ts index 43a2b7de0..13951e90e 100644 --- a/src/database/rxfire.ts +++ b/src/database/rxfire.ts @@ -3,7 +3,6 @@ import { ɵzoneWrap } from '@angular/fire'; import { auditTrail as _auditTrail, changeToData as _changeToData, - fromRef as _fromRef, list as _list, listVal as _listVal, object as _object, @@ -16,9 +15,12 @@ export { ListenerMethods } from 'rxfire/database'; +export { + fromRef +} from './overrides'; + export const auditTrail = ɵzoneWrap(_auditTrail, false, 1); export const changeToData = ɵzoneWrap(_changeToData, true); -export const fromRef = ɵzoneWrap(_fromRef, false, 1); export const list = ɵzoneWrap(_list, true); export const listVal = ɵzoneWrap(_listVal, true); export const object = ɵzoneWrap(_object, true); diff --git a/tools/build.ts b/tools/build.ts index 6df089cf8..e3c611a3f 100644 --- a/tools/build.ts +++ b/tools/build.ts @@ -226,8 +226,10 @@ ${exportedZoneWrappedFns} reexport('database', 'rxfire', 'rxfire/database', tsKeys(), { // These emit only on child events, which an empty node never produces: `blockUntilFirst: false` otherwise `ApplicationRef.isStable` may never become `true`. auditTrail: { blockUntilFirst: false, logLevel: LogLevel.WARN }, - fromRef: { blockUntilFirst: false, logLevel: LogLevel.WARN }, stateChanges: { blockUntilFirst: false, logLevel: LogLevel.WARN }, + + // Hand-written in `src/database/overrides.ts`, which determines `blockUntilFirst` per event. + fromRef: { override: true }, }), reexport('database', 'firebase', 'firebase/database', tsKeys(), { child: { logLevel: LogLevel.VERBOSE },