Skip to content

Commit

Permalink
Adding more injector safety (#3590)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
jamesdaniels authored Dec 13, 2024
1 parent 01597da commit 45ccd39
Show file tree
Hide file tree
Showing 3 changed files with 55 additions and 11 deletions.
2 changes: 1 addition & 1 deletion src/auth/firebase.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

63 changes: 53 additions & 10 deletions src/zones.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import {
Injector,
NgZone,
PendingTasks,
inject
inject,
isDevMode,
runInInjectionContext
} from '@angular/core';
import { pendingUntilEvent } from '@angular/core/rxjs-interop';
import {
Expand Down Expand Up @@ -76,31 +78,71 @@ function getSchedulers() {
return inject(ɵAngularFireSchedulers);
}

var alreadyWarned = false;
function warnOutsideInjectionContext(original: any, operation: string) {
if (isDevMode()) {
console.warn(`Firebase API called outside injection context: ${operation}(${original.name})`);
if (!alreadyWarned) {
alreadyWarned = true;
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");
}
}
}

function runOutsideAngular<T>(fn: (...args: any[]) => T): T {
return inject(NgZone).runOutsideAngular(() => fn());
let ngZone: NgZone|undefined;
try {
ngZone = inject(NgZone);
} catch(e) {
warnOutsideInjectionContext(fn, "runOutsideAngular");
}
if (!ngZone) {return fn();}
return ngZone.runOutsideAngular(() => fn());
}

function run<T>(fn: (...args: any[]) => T): T {
return inject(NgZone).run(() => fn());
let ngZone: NgZone|undefined;
try {
ngZone = inject(NgZone);
} catch(e) {
warnOutsideInjectionContext(fn, "run");
}
if (!ngZone) {return fn();}
return ngZone.run(() => fn());
}

export function observeOutsideAngular<T>(obs$: Observable<T>): Observable<T> {
return obs$.pipe(observeOn(getSchedulers().outsideAngular));
let schedulers: ɵAngularFireSchedulers|undefined;
try {
schedulers = getSchedulers();
} catch(e) {
warnOutsideInjectionContext(obs$, "observeOutsideAngular");
}
if (!schedulers) {return obs$;}
return obs$.pipe(observeOn(schedulers.outsideAngular));
}

export function observeInsideAngular<T>(obs$: Observable<T>): Observable<T> {
return obs$.pipe(observeOn(getSchedulers().insideAngular));
let schedulers: ɵAngularFireSchedulers|undefined;
try {
schedulers = getSchedulers();
} catch(e) {
warnOutsideInjectionContext(obs$, "observeInsideAngular");
}
if (!schedulers) {return obs$;}
return obs$.pipe(observeOn(schedulers.insideAngular));
}

const zoneWrapFn = (
it: (...args: any[]) => any,
taskDone: VoidFunction | undefined
taskDone: VoidFunction | undefined,
injector: Injector,
) => {
return (...args: any[]) => {
if (taskDone) {
setTimeout(taskDone, 0);
}
return run(() => it.apply(this, args));
return runInInjectionContext(injector, () => run(() => it.apply(this, args)));
};
};

Expand All @@ -117,6 +159,7 @@ export const ɵzoneWrap = <T= unknown>(it: T, blockUntilFirst: boolean): T => {
pendingTasks = inject(PendingTasks);
injector = inject(Injector);
} catch(e) {
warnOutsideInjectionContext(it, "ɵzoneWrap");
return (it as any).apply(this, _arguments);
}
// if this is a callback function, e.g, onSnapshot, we should create a pending task and complete it
Expand All @@ -127,7 +170,7 @@ export const ɵzoneWrap = <T= unknown>(it: T, blockUntilFirst: boolean): T => {
taskDone ||= run(() => pendingTasks.add());
}
// TODO create a microtask to track callback functions
_arguments[i] = zoneWrapFn(_arguments[i], taskDone);
_arguments[i] = zoneWrapFn(_arguments[i], taskDone, injector);
}
}
const ret = runOutsideAngular(() => (it as any).apply(this, _arguments));
Expand All @@ -153,8 +196,8 @@ export const ɵzoneWrap = <T= unknown>(it: T, blockUntilFirst: boolean): T => {
() =>
new Promise((resolve, reject) => {
pendingTasks.run(() => ret).then(
(it) => run(() => resolve(it)),
(reason) => run(() => reject(reason))
(it) => runInInjectionContext(injector, () => run(() => resolve(it))),
(reason) => runInInjectionContext(injector, () => run(() => reject(reason)))
);
})
);
Expand Down
1 change: 1 addition & 0 deletions tools/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ ${exportedZoneWrappedFns}
browserSessionPersistence: null,
indexedDBLocalPersistence: null,
prodErrorMap: null,
beforeAuthStateChanged: { blockUntilFirst: false },
}),
reexport('database', 'rxfire', 'rxfire/database', tsKeys<typeof import('rxfire/database')>()),
reexport('database', 'firebase', 'firebase/database', tsKeys<typeof import('firebase/database')>()),
Expand Down

0 comments on commit 45ccd39

Please sign in to comment.