Skip to content

Commit

Permalink
First pass on the new RC API and the start of the AngularFireLazy effort
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
jamesdaniels committed Nov 13, 2019
1 parent d7d52c8 commit eb4bc00
Show file tree
Hide file tree
Showing 3 changed files with 128 additions and 38 deletions.
34 changes: 26 additions & 8 deletions src/analytics/analytics.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { Injectable, Inject, Optional, NgZone, InjectionToken } from '@angular/core';
import { Observable, from } from 'rxjs';
import { map, tap, filter, withLatestFrom } from 'rxjs/operators';
import { FirebaseAppConfig, FirebaseOptions, runOutsideAngular } from '@angular/fire';
import { map, tap, filter, withLatestFrom, shareReplay } from 'rxjs/operators';
import { Router, NavigationEnd, ActivationEnd } from '@angular/router';
import { FirebaseAnalytics, FIREBASE_OPTIONS, FIREBASE_APP_NAME, _firebaseAppFactory } from '@angular/fire';
import { FirebaseAppConfig, FirebaseOptions, runOutsideAngular, _lazySDKProxy, FirebaseAnalytics, FIREBASE_OPTIONS, FIREBASE_APP_NAME, _firebaseAppFactory } from '@angular/fire';
import { analytics, app } from 'firebase';

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

// SEMVER: once we move to Typescript 3.6 use `PromiseProxy<analytics.Analytics>`
type AnalyticsProxy = {
// TODO can we pull the richer types from the Firebase SDK .d.ts? ReturnType<T[K]> is infering
// I could even do this in a manual build-step
logEvent(eventName: string, eventParams?: {[key: string]: any}, options?: analytics.AnalyticsCallOptions): Promise<void>,
setCurrentScreen(screenName: string, options?: analytics.AnalyticsCallOptions): Promise<void>,
setUserId(id: string, options?: analytics.AnalyticsCallOptions): Promise<void>,
setUserProperties(properties: analytics.CustomParams, options?: analytics.AnalyticsCallOptions): Promise<void>,
setAnalyticsCollectionEnabled(enabled: boolean): Promise<void>,
app: Promise<app.App>
};

export interface AngularFireAnalytics extends AnalyticsProxy {};

@Injectable()
export class AngularFireAnalytics {

/**
* Firebase Analytics instance
*/
public readonly analytics: Observable<FirebaseAnalytics>;
private readonly analytics$: Observable<FirebaseAnalytics>;
private get analytics() { return this.analytics$.toPromise(); }

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

this.analytics = requireAnalytics.pipe(
this.analytics$ = requireAnalytics.pipe(
map(() => app.analytics()),
tap(analytics => {
if (analyticsCollectionEnabled === false) { analytics.setAnalyticsCollectionEnabled(false) }
}),
runOutsideAngular(zone)
runOutsideAngular(zone),
shareReplay(1)
);

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

return _lazySDKProxy(this, this.analytics, zone);

}

}
33 changes: 33 additions & 0 deletions src/core/angularfire2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,36 @@ export const runInZone = (zone: NgZone) => <T>(obs$: Observable<T>): Observable<
);
});
}

//SEMVER: once we move to TypeScript 3.6, we can use these to build lazy interfaces
/*
type FunctionPropertyNames<T> = { [K in keyof T]: T[K] extends Function ? K : never }[keyof T];
type PromiseReturningFunctionPropertyNames<T> = { [K in FunctionPropertyNames<T>]: ReturnType<T[K]> extends Promise<any> ? K : never }[FunctionPropertyNames<T>];
type NonPromiseReturningFunctionPropertyNames<T> = { [K in FunctionPropertyNames<T>]: ReturnType<T[K]> extends Promise<any> ? never : K }[FunctionPropertyNames<T>];
type NonFunctionPropertyNames<T> = { [K in keyof T]: T[K] extends Function ? never : K }[keyof T];
export type PromiseProxy<T> = { [K in NonFunctionPropertyNames<T>]: Promise<T[K]> } &
{ [K in NonPromiseReturningFunctionPropertyNames<T>]: (...args: Parameters<T[K]>) => Promise<ReturnType<T[K]>> } &
{ [K in PromiseReturningFunctionPropertyNames<T> ]: (...args: Parameters<T[K]>) => ReturnType<T[K]> };
*/

export const _lazySDKProxy = (klass: any, promise: Promise<any>, zone: NgZone) => new Proxy(klass, {
get: (_, name) => zone.runOutsideAngular(() =>
klass[name] || new Proxy(() =>
promise.then(mod => {
const ret = mod[name];
// TODO move to proper type guards
if (typeof ret == 'function') {
return ret.bind(mod);
} else if (ret && ret.then) {
return ret.then((res:any) => zone.run(() => res));
} else {
return zone.run(() => ret);
}
}), {
get: (self, name) => self()[name],
// TODO handle callbacks
apply: (self, _, args) => self().then(it => it(...args))
})
)
});
99 changes: 69 additions & 30 deletions src/remote-config/remote-config.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Injectable, Inject, Optional, NgZone, InjectionToken } from '@angular/core';
import { Observable, from, concat } from 'rxjs';
import { map, switchMap, tap, take } from 'rxjs/operators';
import { FirebaseAppConfig, FirebaseOptions, FIREBASE_OPTIONS, FIREBASE_APP_NAME } from '@angular/fire';
import { map, switchMap, tap, shareReplay, distinctUntilChanged } from 'rxjs/operators';
import { FirebaseAppConfig, FirebaseOptions, _lazySDKProxy, FIREBASE_OPTIONS, FIREBASE_APP_NAME } from '@angular/fire';
import { remoteConfig } from 'firebase/app';

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

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

@Injectable()
export class AngularFireRemoteConfig {
// SEMVER: once we move to Typescript 3.6 use `PromiseProxy<remoteConfig.RemoteConfig>` rather than hardcoding
type RemoteConfigProxy = {
activate: () => Promise<void>;
ensureInitialized: () => Promise<void>;
fetch: () => Promise<void>;
fetchAndActivate: () => Promise<void>;
getAll: () => Promise<{[key:string]: remoteConfig.Value}>;
getBoolean: (key:string) => Promise<boolean>;
getNumber: (key:string) => Promise<number>;
getString: (key:string) => Promise<string>;
getValue: (key:string) => Promise<remoteConfig.Value>;
setLogLevel: (logLevel: remoteConfig.LogLevel) => Promise<void>;
settings: Promise<remoteConfig.Settings>;
defaultConfig: Promise<{
[key: string]: string | number | boolean;
}>;
fetchTimeMillis: Promise<number>;
lastFetchStatus: Promise<remoteConfig.FetchStatus>;
};

/**
* Firebase RemoteConfig instance
*/
public readonly remoteConfig: Observable<FirebaseRemoteConfig>;
export interface AngularFireRemoteConfig extends RemoteConfigProxy {};

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

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

public readonly activate: Observable<{[key:string]: remoteConfig.Value}>;
readonly all: Observable<{[key:string]: remoteConfig.Value}>;
readonly numbers: Observable<{[key:string]: number}> & {[key:string]: Observable<number>};
readonly booleans: Observable<{[key:string]: boolean}> & {[key:string]: Observable<boolean>};
readonly strings: Observable<{[key:string]: string}> & {[key:string]: Observable<string>};

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

this.remoteConfig = requireRemoteConfig.pipe(
this.remoteConfig$ = requireRemoteConfig.pipe(
map(rc => rc.registerRemoteConfig(firebase)),
map(() => _firebaseAppFactory(options, zone, nameOrConfig)),
map(app => app.remoteConfig()),
tap(rc => {
if (settings) { rc.settings = settings }
if (defaultConfig) { rc.defaultConfig = defaultConfig }
}),
runOutsideAngular(zone)
);

this.activate = this.remoteConfig.pipe(
switchMap(rc => rc.activate().then(() => rc)),
tap(rc => rc.fetch()),
map(rc => rc.getAll()),
runOutsideAngular(zone),
take(1)
)

this.freshConfiguration = this.remoteConfig.pipe(
switchMap(rc => rc.fetchAndActivate().then(() => rc.getAll())),
runOutsideAngular(zone),
take(1)
)
shareReplay(1)
);

this.configuration = this.remoteConfig.pipe(
this.all = this.remoteConfig$.pipe(
switchMap(rc => concat(
rc.activate().then(() => rc.getAll()),
rc.fetchAndActivate().then(() => rc.getAll())
)),
runOutsideAngular(zone)
)
runOutsideAngular(zone),
// TODO startWith(rehydrate(deafultConfig)),
shareReplay(1)
// TODO distinctUntilChanged(compareFn)
);

const allAs = (type: 'String'|'Boolean'|'Number') => this.all.pipe(
map(all => Object.keys(all).reduce((c, k) => {
c[k] = all[k][`as${type}`]();
return c;
}, {}))
) as any;

this.strings = new Proxy(allAs('String'), {
get: (self, name:string) => self[name] || this.all.pipe(
map(rc => rc[name].asString()),
distinctUntilChanged()
)
});

this.booleans = new Proxy(allAs('Boolean'), {
get: (self, name:string) => self[name] || this.all.pipe(
map(rc => rc[name].asBoolean()),
distinctUntilChanged()
)
});

this.numbers = new Proxy(allAs('Number'), {
get: (self, name:string) => self[name] || this.all.pipe(
map(rc => rc[name].asNumber()),
distinctUntilChanged()
)
});

return _lazySDKProxy(this, this.remoteConfig, zone);
}

}

0 comments on commit eb4bc00

Please sign in to comment.