Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
e5be927
Firebase v7, analytics and remote-config
jamesdaniels Oct 2, 2019
1cf961b
Cleaning up the DI tokens
jamesdaniels Oct 2, 2019
f5f67ad
Cleaning things up
jamesdaniels Oct 4, 2019
73413e8
DI and jazz
jamesdaniels Oct 4, 2019
dd0efb1
Bumping the tests
jamesdaniels Nov 8, 2019
dffca53
Adding to the root-spec
jamesdaniels Nov 9, 2019
9731e3c
Spelling is good.
jamesdaniels Nov 9, 2019
7308f9c
Merge branch 'master' into firebase-v7
jamesdaniels Nov 9, 2019
dcdf8bc
Have to include UMDs in the karma.conf
jamesdaniels Nov 9, 2019
6f71d46
Just importing performance is destablizing the app
jamesdaniels Nov 11, 2019
6638b9d
Merge branch 'master' into firebase-v7
jamesdaniels Nov 12, 2019
d7d52c8
Adding the zone arg to the app factory
jamesdaniels Nov 12, 2019
eb4bc00
First pass on the new RC API and the start of the AngularFireLazy effort
jamesdaniels Nov 13, 2019
89344cc
Update src/remote-config/tsconfig-test.json
jamesdaniels Nov 14, 2019
075afe6
Reworking things a bit
jamesdaniels Nov 20, 2019
b8b351a
Router as optional, drop this/private from screen tracking
jamesdaniels Nov 20, 2019
1e39052
Minor changes to RC
jamesdaniels Nov 20, 2019
768c21b
It's firebase_screen_class
jamesdaniels Nov 21, 2019
60c0cad
Reworking analytics
jamesdaniels Nov 22, 2019
9b2e920
current!
jamesdaniels Nov 22, 2019
916e069
Use _loadedConfig if available and scope screen_id on outlet
jamesdaniels Nov 22, 2019
5955925
Fixing the types to handle older Firebase SDKs
jamesdaniels Nov 22, 2019
659165e
SEMVER notes on the DI tokens
jamesdaniels Nov 22, 2019
62d90b9
Starting on the docs
jamesdaniels Nov 22, 2019
1a43ad1
Merge branch 'firebase-v7' of github.com:angular/angularfire2 into fi…
jamesdaniels Nov 22, 2019
67e1b55
Monitoring...
jamesdaniels Dec 9, 2019
91778ff
More work on analytics
jamesdaniels Dec 11, 2019
460170c
more expirimentation
jamesdaniels Dec 12, 2019
3c1ad1f
Flushing out RC more and fixing SSR issues
jamesdaniels Dec 14, 2019
e2d83c8
New RC API
jamesdaniels Dec 16, 2019
4601932
Mapping to objects and templates, budget pipe
jamesdaniels Dec 17, 2019
7fe92ed
More strict types
jamesdaniels Dec 17, 2019
d47dc3f
Fix proxy in Node, get component name for analytics in both JIT and AOT
jamesdaniels Dec 21, 2019
05c840b
Cleaning things up, beyond docs ready for release
jamesdaniels Jan 7, 2020
b46b382
Docs and cleanup
jamesdaniels Jan 7, 2020
ff65db8
Fixing analytics spec
jamesdaniels Jan 7, 2020
baaeccf
Adding more API to the docs
jamesdaniels Jan 7, 2020
04a3bb1
Further simplifications to the DI tokens
jamesdaniels Jan 7, 2020
c37fcb7
Add Analytics and RemoteConfig to install-and-setup
jamesdaniels Jan 7, 2020
6753a67
Merge branch 'master' into firebase-v7
jamesdaniels Jan 7, 2020
6f114c6
Our RC Value implements a Partial, so minors dont break
jamesdaniels Jan 7, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
New RC API
  • Loading branch information
jamesdaniels committed Dec 16, 2019
commit e2d83c87669152b49abd8e063fce6b707d1b3dc8
2 changes: 1 addition & 1 deletion src/remote-config/public_api.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export * from './remote-config';
export * from './remote-config.module';
export * from './remote-config.module';
180 changes: 84 additions & 96 deletions src/remote-config/remote-config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Injectable, Inject, Optional, NgZone, InjectionToken, PLATFORM_ID } from '@angular/core';
import { Observable, concat, of, empty, pipe, OperatorFunction } from 'rxjs';
import { map, switchMap, tap, shareReplay, distinctUntilChanged, filter, groupBy, mergeMap } from 'rxjs/operators';
import { Observable, concat, of, pipe, OperatorFunction, UnaryFunction } from 'rxjs';
import { map, switchMap, tap, shareReplay, distinctUntilChanged, filter, groupBy, mergeMap, scan, withLatestFrom, startWith } from 'rxjs/operators';
import { FirebaseAppConfig, FirebaseOptions, ɵlazySDKProxy, FIREBASE_OPTIONS, FIREBASE_APP_NAME } from '@angular/fire';
import { remoteConfig } from 'firebase/app';

Expand Down Expand Up @@ -50,48 +50,24 @@ export class Parameter extends Value {
}
}

type Filter<T, K={}, M=any> = T extends {[key:string]: M} ?
OperatorFunction<T, {[key:string]: M & K}> :
OperatorFunction<T, T & K>;

const filterKey = (attribute: any, test: (param:any) => boolean) => pipe(
map((value:Parameter | Record<string, Parameter>) => {
const param = value[attribute];
if (param) {
if (test(param)) {
return value;
} else {
return undefined;
}
} else {
const filtered = Object.keys(value).reduce((c, k) => {
if (test(value[k][attribute])) {
return {...c, [k]: value[k]};
} else {
return c;
}
}, {});
return Object.keys(filtered).length > 0 ? filtered : undefined
}
}),
filter(a => !!a)
) as any; // TODO figure out the typing here

export const filterStatic = <T>(): Filter<T, {_source: 'static', getSource: () => 'static'}> => filterKey('_source', s => s === 'static');
export const filterRemote = <T>(): Filter<T, {_source: 'remote', getSource: () => 'remote'}> => filterKey('_source', s => s === 'remote');
export const filterDefault = <T>(): Filter<T, {_source: 'default', getSource: () => 'default'}> => filterKey('_source', s => s === 'default');

const DEFAULT_INTERVAL = 60 * 60 * 1000; // 1 hour
export const filterFresh = <T>(howRecentInMillis: number = DEFAULT_INTERVAL): OperatorFunction<T, T> => filterKey('fetchTimeMillis', f => f + howRecentInMillis >= new Date().getTime());
// If it's a Parameter array, test any, else test the individual Parameter
const filterTest = (fn: (param:Parameter) => boolean) => filter<Parameter|Parameter[]>(it => Array.isArray(it) ? it.some(fn) : fn(it))

// Allow the user to bypass the default values and wait till they get something from the server, even if it's a cached copy;
// if used in conjuntion with first() it will only fetch RC values from the server if they aren't cached locally
export const filterRemote = () => filterTest(p => p.getSource() === 'remote');

// filterFresh allows the developer to effectively set up a maximum cache time
export const filterFresh = (howRecentInMillis: number) => filterTest(p => p.fetchTimeMillis + howRecentInMillis >= new Date().getTime());

@Injectable()
export class AngularFireRemoteConfig {

readonly changes: Observable<Parameter>;
readonly values: Observable<Record<string, Parameter>> & Record<string, Observable<Parameter>>;
readonly numbers: Observable<Record<string, number>> & Record<string, Observable<number>>;
readonly booleans: Observable<Record<string, boolean>> & Record<string, Observable<boolean>>;
readonly strings: Observable<Record<string, string>> & Record<string, Observable<string>>;
readonly changes: Observable<Parameter>;
readonly parameters: Observable<Parameter[]>;
readonly numbers: Observable<Record<string, number>> & Record<string, Observable<number>>;
readonly booleans: Observable<Record<string, boolean>> & Record<string, Observable<boolean>>;
readonly strings: Observable<Record<string, string>> & Record<string, Observable<string|undefined>>;

constructor(
@Inject(FIREBASE_OPTIONS) options:FirebaseOptions,
Expand All @@ -101,91 +77,103 @@ export class AngularFireRemoteConfig {
@Inject(PLATFORM_ID) platformId:Object,
private zone: NgZone
) {

let default$: Observable<{[key:string]: remoteConfig.Value}> = of(Object.keys(defaultConfig || {}).reduce(
(c, k) => ({...c, [k]: new Value("default", defaultConfig![k].toString()) }), {}
));

let _remoteConfig: remoteConfig.RemoteConfig|undefined = undefined;
const fetchTimeMillis = () => _remoteConfig && _remoteConfig.fetchTimeMillis || -1;

const remoteConfig = of(undefined).pipe(
const remoteConfig$ = of(undefined).pipe(
// @ts-ignore zapping in the UMD in the build script
switchMap(() => zone.runOutsideAngular(() => import('firebase/remote-config'))),
map(() => _firebaseAppFactory(options, zone, nameOrConfig)),
// SEMVER no need to cast once we drop older Firebase
map(app => <remoteConfig.RemoteConfig>app.remoteConfig()),
tap(rc => {
if (settings) { rc.settings = settings }
if (defaultConfig) { rc.defaultConfig = defaultConfig }
default$ = empty(); // once the SDK is loaded, we don't need our defaults anylonger
_remoteConfig = rc; // hack, keep the state around for easy injection of fetchTimeMillis
// FYI we don't load the defaults into remote config, since we have our own implementation
// see the comment on scanToParametersArray
}),
startWith(undefined),
runOutsideAngular(zone),
shareReplay(1)
shareReplay({ bufferSize: 1, refCount: false })
);

const existing = of(undefined).pipe(
switchMap(() => remoteConfig),
const loadedRemoteConfig$ = remoteConfig$.pipe(
filter<remoteConfig.RemoteConfig>(rc => !!rc)
);

let default$: Observable<{[key:string]: remoteConfig.Value}> = of(Object.keys(defaultConfig || {}).reduce(
(c, k) => ({...c, [k]: new Value("default", defaultConfig![k].toString()) }), {}
));

const existing$ = loadedRemoteConfig$.pipe(
switchMap(rc => rc.activate().then(() => rc.getAll()))
);

let fresh = of(undefined).pipe(
switchMap(() => remoteConfig),
const fresh$ = loadedRemoteConfig$.pipe(
switchMap(rc => zone.runOutsideAngular(() => rc.fetchAndActivate().then(() => rc.getAll())))
);

const all = concat(default$, existing, fresh).pipe(
distinctUntilChanged((a, b) => JSON.stringify(a) === JSON.stringify(b)),
map(all => Object.keys(all).reduce((c, k) => ({...c, [k]: new Parameter(k, fetchTimeMillis(), all[k].getSource(), all[k].asString())}), {} as Record<string, Parameter>)),
this.parameters = concat(default$, existing$, fresh$).pipe(
scanToParametersArray(remoteConfig$),
shareReplay({ bufferSize: 1, refCount: true })
);

this.changes = all.pipe(
map(all => Object.values(all)),
this.changes = this.parameters.pipe(
switchMap(params => of(...params)),
groupBy(param => param.key),
mergeMap(group => group.pipe(
distinctUntilChanged((a,b) => JSON.stringify(a) === JSON.stringify(b))
distinctUntilChanged()
))
);

this.values = new Proxy(all, {
get: (self, name:string) => self[name] || all.pipe(
map(rc => rc[name] ? rc[name] : undefined),
distinctUntilChanged((a,b) => JSON.stringify(a) === JSON.stringify(b))
)
}) as any; // TODO types

// TODO change the any, once i figure out how to type the proxies better
const allAs = (type: 'Number'|'Boolean'|'String') => all.pipe(
map(all => Object.values(all).reduce((c, p) => ({...c, [p.key]: p[`as${type}`]()}), {})),
distinctUntilChanged((a,b) => JSON.stringify(a) === JSON.stringify(b))
) as any;

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

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

this.numbers = new Proxy(allAs('Number'), {
get: (self, name:string) => self[name] || all.pipe(
map(rc => rc[name] ? rc[name].asNumber() : 0),
distinctUntilChanged()
)
});
this.strings = proxyAll(this.parameters, 'asString');
this.booleans = proxyAll(this.parameters, 'asBoolean');
this.numbers = proxyAll(this.parameters, 'asNumber');

// TODO fix the proxy for server
return isPlatformServer(platformId) ? this : ɵlazySDKProxy(this, remoteConfig, zone);
return isPlatformServer(platformId) ? this : ɵlazySDKProxy(this, remoteConfig$, zone);
}

}

// I ditched loading the defaults into RC and a simple map for scan since we already have our own defaults implementation.
// The idea here being that if they have a default that never loads from the server, they will be able to tell via fetchTimeMillis on the Parameter.
// Also if it doesn't come from the server it won't emit again in .changes, due to the distinctUntilChanged, which we can simplify to === rather than deep comparison
const scanToParametersArray = (remoteConfig: Observable<remoteConfig.RemoteConfig|undefined>): OperatorFunction<Record<string, remoteConfig.Value>, Parameter[]> => pipe(
withLatestFrom(remoteConfig),
scan((existing, [all, rc]) => {
// SEMVER use "new Set" to unique once we're only targeting es6
// at the scale we expect remote config to be at, we probably won't see a performance hit from this unoptimized uniqueness implementation
// const allKeys = [...new Set([...existing.map(p => p.key), ...Object.keys(all)])];
const allKeys = [...existing.map(p => p.key), ...Object.keys(all)].filter((v, i, a) => a.indexOf(v) === i);
return allKeys.map(key => {
const updatedValue = all[key];
return updatedValue ? new Parameter(key, rc ? rc.fetchTimeMillis : -1, updatedValue.getSource(), updatedValue.asString())
: existing.find(p => p.key === key)!
});
}, [] as Array<Parameter>)
);

const PROXY_DEFAULTS = {'asNumber': 0, 'asBoolean': false, 'asString': undefined};


function mapToObject(fn: 'asNumber'): UnaryFunction<Observable<Parameter[]>, Observable<Record<string, number>>>;
function mapToObject(fn: 'asBoolean'): UnaryFunction<Observable<Parameter[]>, Observable<Record<string, boolean>>>;
function mapToObject(fn: 'asString'): UnaryFunction<Observable<Parameter[]>, Observable<Record<string, string|undefined>>>;
function mapToObject(fn: 'asNumber'|'asBoolean'|'asString') {
return pipe(
map((params: Parameter[]) => params.reduce((c, p) => ({...c, [p.key]: p[fn]()}), {} as Record<string, number|boolean|string|undefined>)),
distinctUntilChanged((a,b) => JSON.stringify(a) === JSON.stringify(b))
);
};

export const mapAsStrings = () => mapToObject('asString');
export const mapAsBooleans = () => mapToObject('asBoolean');
export const mapAsNumbers = () => mapToObject('asNumber');

// TODO look into the types here, I don't like the anys
const proxyAll = (observable: Observable<Parameter[]>, fn: 'asNumber'|'asBoolean'|'asString') => new Proxy(
observable.pipe(mapToObject(fn as any)), {
get: (self, name:string) => self[name] || self.pipe(
map(all => all[name] || PROXY_DEFAULTS[fn]),
distinctUntilChanged()
)
}
) as any;