-
Notifications
You must be signed in to change notification settings - Fork 2.2k
/
Copy pathzones.ts
191 lines (175 loc) · 6.23 KB
/
zones.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
/* eslint-disable @typescript-eslint/ban-ts-comment */
import {
EnvironmentInjector,
Injectable,
NgZone,
PendingTasks,
inject,
isDevMode,
runInInjectionContext
} from '@angular/core';
import { pendingUntilEvent } from '@angular/core/rxjs-interop';
import {
Observable,
SchedulerAction,
SchedulerLike,
Subscription,
asyncScheduler,
queueScheduler
} from 'rxjs';
import { observeOn, subscribeOn } from 'rxjs/operators';
declare const Zone: {current: unknown} | undefined;
export enum LogLevel {
"SILENT" = 0,
"WARN" = 1,
"VERBOSE" = 2,
}
var currentLogLevel = (isDevMode() && typeof Zone !== "undefined") ? LogLevel.WARN : LogLevel.SILENT;
export const setLogLevel = (logLevel: LogLevel) => currentLogLevel = logLevel;
/**
* Schedules tasks so that they are invoked inside the Zone that is passed in the constructor.
*/
export class ɵZoneScheduler implements SchedulerLike {
constructor(private zone: any, private delegate: any = queueScheduler) {
}
now() {
return this.delegate.now();
}
schedule(work: (this: SchedulerAction<any>, state?: any) => void, delay?: number, state?: any): Subscription {
const targetZone = this.zone;
// Wrap the specified work function to make sure that if nested scheduling takes place the
// work is executed in the correct zone
const workInZone = function(this: SchedulerAction<any>, state: any) {
if (targetZone) {
targetZone.runGuarded(() => {
work.apply(this, [state]);
});
} else {
work.apply(this, [state]);
}
};
// Scheduling itself needs to be run in zone to ensure setInterval calls for async scheduling are done
// inside the correct zone. This scheduler needs to schedule asynchronously always to ensure that
// firebase emissions are never synchronous. Specifying a delay causes issues with the queueScheduler delegate.
return this.delegate.schedule(workInZone, delay, state);
}
}
@Injectable({
providedIn: 'root',
})
export class ɵAngularFireSchedulers {
public readonly outsideAngular: ɵZoneScheduler;
public readonly insideAngular: ɵZoneScheduler;
constructor() {
const ngZone = inject(NgZone);
this.outsideAngular = ngZone.runOutsideAngular(
() => new ɵZoneScheduler(typeof Zone === 'undefined' ? undefined : Zone.current)
);
this.insideAngular = ngZone.run(
() => new ɵZoneScheduler(
typeof Zone === 'undefined' ? undefined : Zone.current,
asyncScheduler
)
);
}
}
var alreadyWarned = false;
function warnOutsideInjectionContext(original: any, logLevel: LogLevel) {
if (!alreadyWarned && (currentLogLevel > LogLevel.SILENT || isDevMode())) {
alreadyWarned = true;
console.warn("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");
}
if (currentLogLevel >= logLevel) {
console.warn(`Firebase API called outside injection context: ${original.name}`);
}
}
function runOutsideAngular<T>(fn: (...args: any[]) => T): T {
const ngZone = inject(NgZone, { optional: true });
if (!ngZone) {return fn();}
return ngZone.runOutsideAngular(() => fn());
}
function run<T>(fn: (...args: any[]) => T): T {
const ngZone = inject(NgZone, { optional: true });
if (!ngZone) {return fn();}
return ngZone.run(() => fn());
}
const zoneWrapFn = (
it: (...args: any[]) => any,
taskDone: VoidFunction | undefined,
injector: EnvironmentInjector,
) => {
return (...args: any[]) => {
if (taskDone) {
setTimeout(taskDone, 0);
}
return runInInjectionContext(injector, () => run(() => it.apply(this, args)));
};
};
export const ɵzoneWrap = <T= unknown>(it: T, blockUntilFirst: boolean, logLevel?: LogLevel): T => {
logLevel ||= blockUntilFirst ? LogLevel.WARN : LogLevel.VERBOSE;
// function() is needed for the arguments object
return function () {
let taskDone: VoidFunction | undefined;
const _arguments = arguments;
let schedulers: ɵAngularFireSchedulers;
let pendingTasks: PendingTasks;
let injector: EnvironmentInjector;
try {
schedulers = inject(ɵAngularFireSchedulers);
pendingTasks = inject(PendingTasks);
injector = inject(EnvironmentInjector);
} catch(e) {
warnOutsideInjectionContext(it, logLevel);
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
// only once one of the callback functions is tripped.
for (let i = 0; i < arguments.length; i++) {
if (typeof _arguments[i] === 'function') {
if (blockUntilFirst) {
taskDone ||= run(() => pendingTasks.add());
}
// TODO create a microtask to track callback functions
_arguments[i] = zoneWrapFn(_arguments[i], taskDone, injector);
}
}
const ret = runOutsideAngular(() => (it as any).apply(this, _arguments));
if (!blockUntilFirst) {
if (ret instanceof Observable) {
return ret.pipe(
subscribeOn(schedulers.outsideAngular),
observeOn(schedulers.insideAngular),
);
} else {
return run(() => ret);
}
}
if (ret instanceof Observable) {
return ret.pipe(
subscribeOn(schedulers.outsideAngular),
observeOn(schedulers.insideAngular),
pendingUntilEvent(injector),
);
} else if (ret instanceof Promise) {
// eslint-disable-next-line @typescript-eslint/no-misused-promises
return run(
() =>
new Promise((resolve, reject) => {
pendingTasks.run(() => ret).then(
(it) => runInInjectionContext(injector, () => run(() => resolve(it))),
(reason) => runInInjectionContext(injector, () => run(() => reject(reason)))
);
}));
} else if (typeof ret === 'function' && taskDone) {
// Handle unsubscribe
// function() is needed for the arguments object
return function () {
setTimeout(taskDone, 0);
return ret.apply(this, arguments);
};
} else {
// TODO how do we handle storage uploads in Zone? and other stuff with cancel() etc?
return run(() => ret);
}
} as any;
};