Skip to content

Commit eb4bc00

Browse files
committed
First pass on the new RC API and the start of the AngularFireLazy effort
* Add all, numbers, strings, and booleans Observables to AngularFireRemoteConfig * Proxy all of firebase.remoteConfig() in AngularFireRemoteConfig dealing with lazy loading of the SDK * Same effort with AngularFireAnalytics
1 parent d7d52c8 commit eb4bc00

File tree

3 files changed

+128
-38
lines changed

3 files changed

+128
-38
lines changed

src/analytics/analytics.ts

+26-8
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { Injectable, Inject, Optional, NgZone, InjectionToken } from '@angular/core';
22
import { Observable, from } from 'rxjs';
3-
import { map, tap, filter, withLatestFrom } from 'rxjs/operators';
4-
import { FirebaseAppConfig, FirebaseOptions, runOutsideAngular } from '@angular/fire';
3+
import { map, tap, filter, withLatestFrom, shareReplay } from 'rxjs/operators';
54
import { Router, NavigationEnd, ActivationEnd } from '@angular/router';
6-
import { FirebaseAnalytics, FIREBASE_OPTIONS, FIREBASE_APP_NAME, _firebaseAppFactory } from '@angular/fire';
5+
import { FirebaseAppConfig, FirebaseOptions, runOutsideAngular, _lazySDKProxy, FirebaseAnalytics, FIREBASE_OPTIONS, FIREBASE_APP_NAME, _firebaseAppFactory } from '@angular/fire';
6+
import { analytics, app } from 'firebase';
77

88
export const AUTOMATICALLY_SET_CURRENT_SCREEN = new InjectionToken<boolean>('angularfire2.analytics.setCurrentScreen');
99
export const AUTOMATICALLY_LOG_SCREEN_VIEWS = new InjectionToken<boolean>('angularfire2.analytics.logScreenViews');
@@ -15,13 +15,28 @@ export const APP_NAME = new InjectionToken<string>('angularfire2.analytics.appNa
1515
export const DEFAULT_APP_VERSION = '?';
1616
export const DEFAULT_APP_NAME = 'Angular App';
1717

18+
// SEMVER: once we move to Typescript 3.6 use `PromiseProxy<analytics.Analytics>`
19+
type AnalyticsProxy = {
20+
// TODO can we pull the richer types from the Firebase SDK .d.ts? ReturnType<T[K]> is infering
21+
// I could even do this in a manual build-step
22+
logEvent(eventName: string, eventParams?: {[key: string]: any}, options?: analytics.AnalyticsCallOptions): Promise<void>,
23+
setCurrentScreen(screenName: string, options?: analytics.AnalyticsCallOptions): Promise<void>,
24+
setUserId(id: string, options?: analytics.AnalyticsCallOptions): Promise<void>,
25+
setUserProperties(properties: analytics.CustomParams, options?: analytics.AnalyticsCallOptions): Promise<void>,
26+
setAnalyticsCollectionEnabled(enabled: boolean): Promise<void>,
27+
app: Promise<app.App>
28+
};
29+
30+
export interface AngularFireAnalytics extends AnalyticsProxy {};
31+
1832
@Injectable()
1933
export class AngularFireAnalytics {
2034

2135
/**
2236
* Firebase Analytics instance
2337
*/
24-
public readonly analytics: Observable<FirebaseAnalytics>;
38+
private readonly analytics$: Observable<FirebaseAnalytics>;
39+
private get analytics() { return this.analytics$.toPromise(); }
2540

2641
constructor(
2742
@Inject(FIREBASE_OPTIONS) options:FirebaseOptions,
@@ -39,12 +54,13 @@ export class AngularFireAnalytics {
3954
const requireAnalytics = from(import('firebase/analytics'));
4055
const app = _firebaseAppFactory(options, zone, nameOrConfig);
4156

42-
this.analytics = requireAnalytics.pipe(
57+
this.analytics$ = requireAnalytics.pipe(
4358
map(() => app.analytics()),
4459
tap(analytics => {
4560
if (analyticsCollectionEnabled === false) { analytics.setAnalyticsCollectionEnabled(false) }
4661
}),
47-
runOutsideAngular(zone)
62+
runOutsideAngular(zone),
63+
shareReplay(1)
4864
);
4965

5066
if (router && (automaticallySetCurrentScreen !== false || automaticallyLogScreenViews !== false)) {
@@ -53,7 +69,7 @@ export class AngularFireAnalytics {
5369
const activationEndEvents = router.events.pipe(filter<ActivationEnd>(e => e instanceof ActivationEnd));
5470
const navigationEndEvents = router.events.pipe(filter<NavigationEnd>(e => e instanceof NavigationEnd));
5571
navigationEndEvents.pipe(
56-
withLatestFrom(activationEndEvents, this.analytics),
72+
withLatestFrom(activationEndEvents, this.analytics$),
5773
tap(([navigationEnd, activationEnd, analytics]) => {
5874
const url = navigationEnd.url;
5975
const screen_name = activationEnd.snapshot.routeConfig && activationEnd.snapshot.routeConfig.path || undefined;
@@ -73,12 +89,14 @@ export class AngularFireAnalytics {
7389
// TODO do something other than just check auth presence, what if it's lazy loaded?
7490
if (app.auth && automaticallyTrackUserIdentifier !== false) {
7591
new Observable<firebase.User|null>(app.auth().onAuthStateChanged.bind(app.auth())).pipe(
76-
withLatestFrom(this.analytics),
92+
withLatestFrom(this.analytics$),
7793
tap(([user, analytics]) => analytics.setUserId(user ? user.uid : null!, { global: true })),
7894
runOutsideAngular(zone)
7995
).subscribe()
8096
}
8197

98+
return _lazySDKProxy(this, this.analytics, zone);
99+
82100
}
83101

84102
}

src/core/angularfire2.ts

+33
Original file line numberDiff line numberDiff line change
@@ -66,3 +66,36 @@ export const runInZone = (zone: NgZone) => <T>(obs$: Observable<T>): Observable<
6666
);
6767
});
6868
}
69+
70+
//SEMVER: once we move to TypeScript 3.6, we can use these to build lazy interfaces
71+
/*
72+
type FunctionPropertyNames<T> = { [K in keyof T]: T[K] extends Function ? K : never }[keyof T];
73+
type PromiseReturningFunctionPropertyNames<T> = { [K in FunctionPropertyNames<T>]: ReturnType<T[K]> extends Promise<any> ? K : never }[FunctionPropertyNames<T>];
74+
type NonPromiseReturningFunctionPropertyNames<T> = { [K in FunctionPropertyNames<T>]: ReturnType<T[K]> extends Promise<any> ? never : K }[FunctionPropertyNames<T>];
75+
type NonFunctionPropertyNames<T> = { [K in keyof T]: T[K] extends Function ? never : K }[keyof T];
76+
77+
export type PromiseProxy<T> = { [K in NonFunctionPropertyNames<T>]: Promise<T[K]> } &
78+
{ [K in NonPromiseReturningFunctionPropertyNames<T>]: (...args: Parameters<T[K]>) => Promise<ReturnType<T[K]>> } &
79+
{ [K in PromiseReturningFunctionPropertyNames<T> ]: (...args: Parameters<T[K]>) => ReturnType<T[K]> };
80+
*/
81+
82+
export const _lazySDKProxy = (klass: any, promise: Promise<any>, zone: NgZone) => new Proxy(klass, {
83+
get: (_, name) => zone.runOutsideAngular(() =>
84+
klass[name] || new Proxy(() =>
85+
promise.then(mod => {
86+
const ret = mod[name];
87+
// TODO move to proper type guards
88+
if (typeof ret == 'function') {
89+
return ret.bind(mod);
90+
} else if (ret && ret.then) {
91+
return ret.then((res:any) => zone.run(() => res));
92+
} else {
93+
return zone.run(() => ret);
94+
}
95+
}), {
96+
get: (self, name) => self()[name],
97+
// TODO handle callbacks
98+
apply: (self, _, args) => self().then(it => it(...args))
99+
})
100+
)
101+
});

src/remote-config/remote-config.ts

+69-30
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Injectable, Inject, Optional, NgZone, InjectionToken } from '@angular/core';
22
import { Observable, from, concat } from 'rxjs';
3-
import { map, switchMap, tap, take } from 'rxjs/operators';
4-
import { FirebaseAppConfig, FirebaseOptions, FIREBASE_OPTIONS, FIREBASE_APP_NAME } from '@angular/fire';
3+
import { map, switchMap, tap, shareReplay, distinctUntilChanged } from 'rxjs/operators';
4+
import { FirebaseAppConfig, FirebaseOptions, _lazySDKProxy, FIREBASE_OPTIONS, FIREBASE_APP_NAME } from '@angular/fire';
55
import { remoteConfig } from 'firebase/app';
66

77
// @ts-ignore
@@ -14,19 +14,38 @@ export const DEFAULT_CONFIG = new InjectionToken<DefaultConfig>('angularfire2.re
1414

1515
import { FirebaseRemoteConfig, _firebaseAppFactory, runOutsideAngular } from '@angular/fire';
1616

17-
@Injectable()
18-
export class AngularFireRemoteConfig {
17+
// SEMVER: once we move to Typescript 3.6 use `PromiseProxy<remoteConfig.RemoteConfig>` rather than hardcoding
18+
type RemoteConfigProxy = {
19+
activate: () => Promise<void>;
20+
ensureInitialized: () => Promise<void>;
21+
fetch: () => Promise<void>;
22+
fetchAndActivate: () => Promise<void>;
23+
getAll: () => Promise<{[key:string]: remoteConfig.Value}>;
24+
getBoolean: (key:string) => Promise<boolean>;
25+
getNumber: (key:string) => Promise<number>;
26+
getString: (key:string) => Promise<string>;
27+
getValue: (key:string) => Promise<remoteConfig.Value>;
28+
setLogLevel: (logLevel: remoteConfig.LogLevel) => Promise<void>;
29+
settings: Promise<remoteConfig.Settings>;
30+
defaultConfig: Promise<{
31+
[key: string]: string | number | boolean;
32+
}>;
33+
fetchTimeMillis: Promise<number>;
34+
lastFetchStatus: Promise<remoteConfig.FetchStatus>;
35+
};
1936

20-
/**
21-
* Firebase RemoteConfig instance
22-
*/
23-
public readonly remoteConfig: Observable<FirebaseRemoteConfig>;
37+
export interface AngularFireRemoteConfig extends RemoteConfigProxy {};
2438

25-
public readonly freshConfiguration: Observable<{[key:string]: remoteConfig.Value}>;
39+
@Injectable()
40+
export class AngularFireRemoteConfig {
2641

27-
public readonly configuration: Observable<{[key:string]: remoteConfig.Value}>;
42+
private readonly remoteConfig$: Observable<remoteConfig.RemoteConfig>;
43+
private get remoteConfig() { return this.remoteConfig$.toPromise(); }
2844

29-
public readonly activate: Observable<{[key:string]: remoteConfig.Value}>;
45+
readonly all: Observable<{[key:string]: remoteConfig.Value}>;
46+
readonly numbers: Observable<{[key:string]: number}> & {[key:string]: Observable<number>};
47+
readonly booleans: Observable<{[key:string]: boolean}> & {[key:string]: Observable<boolean>};
48+
readonly strings: Observable<{[key:string]: string}> & {[key:string]: Observable<string>};
3049

3150
constructor(
3251
@Inject(FIREBASE_OPTIONS) options:FirebaseOptions,
@@ -41,38 +60,58 @@ export class AngularFireRemoteConfig {
4160
// @ts-ignore zapping in the UMD in the build script
4261
const requireRemoteConfig = from(import('@firebase/remote-config'));
4362

44-
this.remoteConfig = requireRemoteConfig.pipe(
63+
this.remoteConfig$ = requireRemoteConfig.pipe(
4564
map(rc => rc.registerRemoteConfig(firebase)),
4665
map(() => _firebaseAppFactory(options, zone, nameOrConfig)),
4766
map(app => app.remoteConfig()),
4867
tap(rc => {
4968
if (settings) { rc.settings = settings }
5069
if (defaultConfig) { rc.defaultConfig = defaultConfig }
5170
}),
52-
runOutsideAngular(zone)
53-
);
54-
55-
this.activate = this.remoteConfig.pipe(
56-
switchMap(rc => rc.activate().then(() => rc)),
57-
tap(rc => rc.fetch()),
58-
map(rc => rc.getAll()),
59-
runOutsideAngular(zone),
60-
take(1)
61-
)
62-
63-
this.freshConfiguration = this.remoteConfig.pipe(
64-
switchMap(rc => rc.fetchAndActivate().then(() => rc.getAll())),
6571
runOutsideAngular(zone),
66-
take(1)
67-
)
72+
shareReplay(1)
73+
);
6874

69-
this.configuration = this.remoteConfig.pipe(
75+
this.all = this.remoteConfig$.pipe(
7076
switchMap(rc => concat(
7177
rc.activate().then(() => rc.getAll()),
7278
rc.fetchAndActivate().then(() => rc.getAll())
7379
)),
74-
runOutsideAngular(zone)
75-
)
80+
runOutsideAngular(zone),
81+
// TODO startWith(rehydrate(deafultConfig)),
82+
shareReplay(1)
83+
// TODO distinctUntilChanged(compareFn)
84+
);
85+
86+
const allAs = (type: 'String'|'Boolean'|'Number') => this.all.pipe(
87+
map(all => Object.keys(all).reduce((c, k) => {
88+
c[k] = all[k][`as${type}`]();
89+
return c;
90+
}, {}))
91+
) as any;
92+
93+
this.strings = new Proxy(allAs('String'), {
94+
get: (self, name:string) => self[name] || this.all.pipe(
95+
map(rc => rc[name].asString()),
96+
distinctUntilChanged()
97+
)
98+
});
99+
100+
this.booleans = new Proxy(allAs('Boolean'), {
101+
get: (self, name:string) => self[name] || this.all.pipe(
102+
map(rc => rc[name].asBoolean()),
103+
distinctUntilChanged()
104+
)
105+
});
106+
107+
this.numbers = new Proxy(allAs('Number'), {
108+
get: (self, name:string) => self[name] || this.all.pipe(
109+
map(rc => rc[name].asNumber()),
110+
distinctUntilChanged()
111+
)
112+
});
113+
114+
return _lazySDKProxy(this, this.remoteConfig, zone);
76115
}
77116

78117
}

0 commit comments

Comments
 (0)