Skip to content

Commit 45ccd39

Browse files
authored
Adding more injector safety (#3590)
* Zone wrapper noops for our other helpers * Add a warning / error on potential Zone / hydration issues * Pass injection context to `zoneWrapFn` * Pass injection context into the Promise wrapper * `beforeAuthStateChanged` should not block
1 parent 01597da commit 45ccd39

File tree

3 files changed

+55
-11
lines changed

3 files changed

+55
-11
lines changed

src/auth/firebase.ts

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/zones.ts

+53-10
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ import {
44
Injector,
55
NgZone,
66
PendingTasks,
7-
inject
7+
inject,
8+
isDevMode,
9+
runInInjectionContext
810
} from '@angular/core';
911
import { pendingUntilEvent } from '@angular/core/rxjs-interop';
1012
import {
@@ -76,31 +78,71 @@ function getSchedulers() {
7678
return inject(ɵAngularFireSchedulers);
7779
}
7880

81+
var alreadyWarned = false;
82+
function warnOutsideInjectionContext(original: any, operation: string) {
83+
if (isDevMode()) {
84+
console.warn(`Firebase API called outside injection context: ${operation}(${original.name})`);
85+
if (!alreadyWarned) {
86+
alreadyWarned = true;
87+
console.error("Calling Firebase APIs outside of an Injection context may destabilize your application leading to subtle change-detection and hydration bugs. Find more at https://github.com/angular/angularfire/blob/main/docs/zones.md");
88+
}
89+
}
90+
}
91+
7992
function runOutsideAngular<T>(fn: (...args: any[]) => T): T {
80-
return inject(NgZone).runOutsideAngular(() => fn());
93+
let ngZone: NgZone|undefined;
94+
try {
95+
ngZone = inject(NgZone);
96+
} catch(e) {
97+
warnOutsideInjectionContext(fn, "runOutsideAngular");
98+
}
99+
if (!ngZone) {return fn();}
100+
return ngZone.runOutsideAngular(() => fn());
81101
}
82102

83103
function run<T>(fn: (...args: any[]) => T): T {
84-
return inject(NgZone).run(() => fn());
104+
let ngZone: NgZone|undefined;
105+
try {
106+
ngZone = inject(NgZone);
107+
} catch(e) {
108+
warnOutsideInjectionContext(fn, "run");
109+
}
110+
if (!ngZone) {return fn();}
111+
return ngZone.run(() => fn());
85112
}
86113

87114
export function observeOutsideAngular<T>(obs$: Observable<T>): Observable<T> {
88-
return obs$.pipe(observeOn(getSchedulers().outsideAngular));
115+
let schedulers: ɵAngularFireSchedulers|undefined;
116+
try {
117+
schedulers = getSchedulers();
118+
} catch(e) {
119+
warnOutsideInjectionContext(obs$, "observeOutsideAngular");
120+
}
121+
if (!schedulers) {return obs$;}
122+
return obs$.pipe(observeOn(schedulers.outsideAngular));
89123
}
90124

91125
export function observeInsideAngular<T>(obs$: Observable<T>): Observable<T> {
92-
return obs$.pipe(observeOn(getSchedulers().insideAngular));
126+
let schedulers: ɵAngularFireSchedulers|undefined;
127+
try {
128+
schedulers = getSchedulers();
129+
} catch(e) {
130+
warnOutsideInjectionContext(obs$, "observeInsideAngular");
131+
}
132+
if (!schedulers) {return obs$;}
133+
return obs$.pipe(observeOn(schedulers.insideAngular));
93134
}
94135

95136
const zoneWrapFn = (
96137
it: (...args: any[]) => any,
97-
taskDone: VoidFunction | undefined
138+
taskDone: VoidFunction | undefined,
139+
injector: Injector,
98140
) => {
99141
return (...args: any[]) => {
100142
if (taskDone) {
101143
setTimeout(taskDone, 0);
102144
}
103-
return run(() => it.apply(this, args));
145+
return runInInjectionContext(injector, () => run(() => it.apply(this, args)));
104146
};
105147
};
106148

@@ -117,6 +159,7 @@ export const ɵzoneWrap = <T= unknown>(it: T, blockUntilFirst: boolean): T => {
117159
pendingTasks = inject(PendingTasks);
118160
injector = inject(Injector);
119161
} catch(e) {
162+
warnOutsideInjectionContext(it, "ɵzoneWrap");
120163
return (it as any).apply(this, _arguments);
121164
}
122165
// if this is a callback function, e.g, onSnapshot, we should create a pending task and complete it
@@ -127,7 +170,7 @@ export const ɵzoneWrap = <T= unknown>(it: T, blockUntilFirst: boolean): T => {
127170
taskDone ||= run(() => pendingTasks.add());
128171
}
129172
// TODO create a microtask to track callback functions
130-
_arguments[i] = zoneWrapFn(_arguments[i], taskDone);
173+
_arguments[i] = zoneWrapFn(_arguments[i], taskDone, injector);
131174
}
132175
}
133176
const ret = runOutsideAngular(() => (it as any).apply(this, _arguments));
@@ -153,8 +196,8 @@ export const ɵzoneWrap = <T= unknown>(it: T, blockUntilFirst: boolean): T => {
153196
() =>
154197
new Promise((resolve, reject) => {
155198
pendingTasks.run(() => ret).then(
156-
(it) => run(() => resolve(it)),
157-
(reason) => run(() => reject(reason))
199+
(it) => runInInjectionContext(injector, () => run(() => resolve(it))),
200+
(reason) => runInInjectionContext(injector, () => run(() => reject(reason)))
158201
);
159202
})
160203
);

tools/build.ts

+1
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ ${exportedZoneWrappedFns}
7575
browserSessionPersistence: null,
7676
indexedDBLocalPersistence: null,
7777
prodErrorMap: null,
78+
beforeAuthStateChanged: { blockUntilFirst: false },
7879
}),
7980
reexport('database', 'rxfire', 'rxfire/database', tsKeys<typeof import('rxfire/database')>()),
8081
reexport('database', 'firebase', 'firebase/database', tsKeys<typeof import('firebase/database')>()),

0 commit comments

Comments
 (0)