11import { Injectable , Inject , Optional , NgZone , InjectionToken , PLATFORM_ID } from '@angular/core' ;
2- import { Observable , concat , of , empty , pipe , OperatorFunction } from 'rxjs' ;
3- import { map , switchMap , tap , shareReplay , distinctUntilChanged , filter , groupBy , mergeMap } from 'rxjs/operators' ;
2+ import { Observable , concat , of , pipe , OperatorFunction , UnaryFunction } from 'rxjs' ;
3+ import { map , switchMap , tap , shareReplay , distinctUntilChanged , filter , groupBy , mergeMap , scan , withLatestFrom , startWith } from 'rxjs/operators' ;
44import { FirebaseAppConfig , FirebaseOptions , ɵlazySDKProxy , FIREBASE_OPTIONS , FIREBASE_APP_NAME } from '@angular/fire' ;
55import { remoteConfig } from 'firebase/app' ;
66
@@ -50,48 +50,24 @@ export class Parameter extends Value {
5050 }
5151}
5252
53- type Filter < T , K = { } , M = any > = T extends { [ key :string ] : M } ?
54- OperatorFunction < T , { [ key :string ] : M & K } > :
55- OperatorFunction < T , T & K > ;
56-
57- const filterKey = ( attribute : any , test : ( param :any ) => boolean ) => pipe (
58- map ( ( value :Parameter | Record < string , Parameter > ) => {
59- const param = value [ attribute ] ;
60- if ( param ) {
61- if ( test ( param ) ) {
62- return value ;
63- } else {
64- return undefined ;
65- }
66- } else {
67- const filtered = Object . keys ( value ) . reduce ( ( c , k ) => {
68- if ( test ( value [ k ] [ attribute ] ) ) {
69- return { ...c , [ k ] : value [ k ] } ;
70- } else {
71- return c ;
72- }
73- } , { } ) ;
74- return Object . keys ( filtered ) . length > 0 ? filtered : undefined
75- }
76- } ) ,
77- filter ( a => ! ! a )
78- ) as any ; // TODO figure out the typing here
79-
80- export const filterStatic = < T > ( ) : Filter < T , { _source : 'static' , getSource : ( ) => 'static' } > => filterKey ( '_source' , s => s === 'static' ) ;
81- export const filterRemote = < T > ( ) : Filter < T , { _source : 'remote' , getSource : ( ) => 'remote' } > => filterKey ( '_source' , s => s === 'remote' ) ;
82- export const filterDefault = < T > ( ) : Filter < T , { _source : 'default' , getSource : ( ) => 'default' } > => filterKey ( '_source' , s => s === 'default' ) ;
83-
84- const DEFAULT_INTERVAL = 60 * 60 * 1000 ; // 1 hour
85- export const filterFresh = < T > ( howRecentInMillis : number = DEFAULT_INTERVAL ) : OperatorFunction < T , T > => filterKey ( 'fetchTimeMillis' , f => f + howRecentInMillis >= new Date ( ) . getTime ( ) ) ;
53+ // If it's a Parameter array, test any, else test the individual Parameter
54+ const filterTest = ( fn : ( param :Parameter ) => boolean ) => filter < Parameter | Parameter [ ] > ( it => Array . isArray ( it ) ? it . some ( fn ) : fn ( it ) )
55+
56+ // Allow the user to bypass the default values and wait till they get something from the server, even if it's a cached copy;
57+ // if used in conjuntion with first() it will only fetch RC values from the server if they aren't cached locally
58+ export const filterRemote = ( ) => filterTest ( p => p . getSource ( ) === 'remote' ) ;
59+
60+ // filterFresh allows the developer to effectively set up a maximum cache time
61+ export const filterFresh = ( howRecentInMillis : number ) => filterTest ( p => p . fetchTimeMillis + howRecentInMillis >= new Date ( ) . getTime ( ) ) ;
8662
8763@Injectable ( )
8864export class AngularFireRemoteConfig {
8965
90- readonly changes : Observable < Parameter > ;
91- readonly values : Observable < Record < string , Parameter > > & Record < string , Observable < Parameter > > ;
92- readonly numbers : Observable < Record < string , number > > & Record < string , Observable < number > > ;
93- readonly booleans : Observable < Record < string , boolean > > & Record < string , Observable < boolean > > ;
94- readonly strings : Observable < Record < string , string > > & Record < string , Observable < string > > ;
66+ readonly changes : Observable < Parameter > ;
67+ readonly parameters : Observable < Parameter [ ] > ;
68+ readonly numbers : Observable < Record < string , number > > & Record < string , Observable < number > > ;
69+ readonly booleans : Observable < Record < string , boolean > > & Record < string , Observable < boolean > > ;
70+ readonly strings : Observable < Record < string , string > > & Record < string , Observable < string | undefined > > ;
9571
9672 constructor (
9773 @Inject ( FIREBASE_OPTIONS ) options :FirebaseOptions ,
@@ -101,91 +77,103 @@ export class AngularFireRemoteConfig {
10177 @Inject ( PLATFORM_ID ) platformId :Object ,
10278 private zone : NgZone
10379 ) {
104-
105- let default$ : Observable < { [ key :string ] : remoteConfig . Value } > = of ( Object . keys ( defaultConfig || { } ) . reduce (
106- ( c , k ) => ( { ...c , [ k ] : new Value ( "default" , defaultConfig ! [ k ] . toString ( ) ) } ) , { }
107- ) ) ;
108-
109- let _remoteConfig : remoteConfig . RemoteConfig | undefined = undefined ;
110- const fetchTimeMillis = ( ) => _remoteConfig && _remoteConfig . fetchTimeMillis || - 1 ;
11180
112- const remoteConfig = of ( undefined ) . pipe (
81+ const remoteConfig$ = of ( undefined ) . pipe (
11382 // @ts -ignore zapping in the UMD in the build script
11483 switchMap ( ( ) => zone . runOutsideAngular ( ( ) => import ( 'firebase/remote-config' ) ) ) ,
11584 map ( ( ) => _firebaseAppFactory ( options , zone , nameOrConfig ) ) ,
11685 // SEMVER no need to cast once we drop older Firebase
11786 map ( app => < remoteConfig . RemoteConfig > app . remoteConfig ( ) ) ,
11887 tap ( rc => {
11988 if ( settings ) { rc . settings = settings }
120- if ( defaultConfig ) { rc . defaultConfig = defaultConfig }
121- default$ = empty ( ) ; // once the SDK is loaded, we don't need our defaults anylonger
122- _remoteConfig = rc ; // hack, keep the state around for easy injection of fetchTimeMillis
89+ // FYI we don't load the defaults into remote config, since we have our own implementation
90+ // see the comment on scanToParametersArray
12391 } ) ,
92+ startWith ( undefined ) ,
12493 runOutsideAngular ( zone ) ,
125- shareReplay ( 1 )
94+ shareReplay ( { bufferSize : 1 , refCount : false } )
12695 ) ;
12796
128- const existing = of ( undefined ) . pipe (
129- switchMap ( ( ) => remoteConfig ) ,
97+ const loadedRemoteConfig$ = remoteConfig$ . pipe (
98+ filter < remoteConfig . RemoteConfig > ( rc => ! ! rc )
99+ ) ;
100+
101+ let default$ : Observable < { [ key :string ] : remoteConfig . Value } > = of ( Object . keys ( defaultConfig || { } ) . reduce (
102+ ( c , k ) => ( { ...c , [ k ] : new Value ( "default" , defaultConfig ! [ k ] . toString ( ) ) } ) , { }
103+ ) ) ;
104+
105+ const existing$ = loadedRemoteConfig$ . pipe (
130106 switchMap ( rc => rc . activate ( ) . then ( ( ) => rc . getAll ( ) ) )
131107 ) ;
132108
133- let fresh = of ( undefined ) . pipe (
134- switchMap ( ( ) => remoteConfig ) ,
109+ const fresh$ = loadedRemoteConfig$ . pipe (
135110 switchMap ( rc => zone . runOutsideAngular ( ( ) => rc . fetchAndActivate ( ) . then ( ( ) => rc . getAll ( ) ) ) )
136111 ) ;
137112
138- const all = concat ( default$ , existing , fresh ) . pipe (
139- distinctUntilChanged ( ( a , b ) => JSON . stringify ( a ) === JSON . stringify ( b ) ) ,
140- map ( all => Object . keys ( all ) . reduce ( ( c , k ) => ( { ...c , [ k ] : new Parameter ( k , fetchTimeMillis ( ) , all [ k ] . getSource ( ) , all [ k ] . asString ( ) ) } ) , { } as Record < string , Parameter > ) ) ,
113+ this . parameters = concat ( default$ , existing$ , fresh$ ) . pipe (
114+ scanToParametersArray ( remoteConfig$ ) ,
141115 shareReplay ( { bufferSize : 1 , refCount : true } )
142116 ) ;
143117
144- this . changes = all . pipe (
145- map ( all => Object . values ( all ) ) ,
118+ this . changes = this . parameters . pipe (
146119 switchMap ( params => of ( ...params ) ) ,
147120 groupBy ( param => param . key ) ,
148121 mergeMap ( group => group . pipe (
149- distinctUntilChanged ( ( a , b ) => JSON . stringify ( a ) === JSON . stringify ( b ) )
122+ distinctUntilChanged ( )
150123 ) )
151124 ) ;
152125
153- this . values = new Proxy ( all , {
154- get : ( self , name :string ) => self [ name ] || all . pipe (
155- map ( rc => rc [ name ] ? rc [ name ] : undefined ) ,
156- distinctUntilChanged ( ( a , b ) => JSON . stringify ( a ) === JSON . stringify ( b ) )
157- )
158- } ) as any ; // TODO types
159-
160- // TODO change the any, once i figure out how to type the proxies better
161- const allAs = ( type : 'Number' | 'Boolean' | 'String' ) => all . pipe (
162- map ( all => Object . values ( all ) . reduce ( ( c , p ) => ( { ...c , [ p . key ] : p [ `as${ type } ` ] ( ) } ) , { } ) ) ,
163- distinctUntilChanged ( ( a , b ) => JSON . stringify ( a ) === JSON . stringify ( b ) )
164- ) as any ;
165-
166- this . strings = new Proxy ( allAs ( 'String' ) , {
167- get : ( self , name :string ) => self [ name ] || all . pipe (
168- map ( rc => rc [ name ] ? rc [ name ] . asString ( ) : undefined ) ,
169- distinctUntilChanged ( )
170- )
171- } ) ;
172-
173- this . booleans = new Proxy ( allAs ( 'Boolean' ) , {
174- get : ( self , name :string ) => self [ name ] || all . pipe (
175- map ( rc => rc [ name ] ? rc [ name ] . asBoolean ( ) : false ) ,
176- distinctUntilChanged ( )
177- )
178- } ) ;
179-
180- this . numbers = new Proxy ( allAs ( 'Number' ) , {
181- get : ( self , name :string ) => self [ name ] || all . pipe (
182- map ( rc => rc [ name ] ? rc [ name ] . asNumber ( ) : 0 ) ,
183- distinctUntilChanged ( )
184- )
185- } ) ;
126+ this . strings = proxyAll ( this . parameters , 'asString' ) ;
127+ this . booleans = proxyAll ( this . parameters , 'asBoolean' ) ;
128+ this . numbers = proxyAll ( this . parameters , 'asNumber' ) ;
186129
187130 // TODO fix the proxy for server
188- return isPlatformServer ( platformId ) ? this : ɵlazySDKProxy ( this , remoteConfig , zone ) ;
131+ return isPlatformServer ( platformId ) ? this : ɵlazySDKProxy ( this , remoteConfig$ , zone ) ;
189132 }
190133
191134}
135+
136+ // I ditched loading the defaults into RC and a simple map for scan since we already have our own defaults implementation.
137+ // 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.
138+ // 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
139+ const scanToParametersArray = ( remoteConfig : Observable < remoteConfig . RemoteConfig | undefined > ) : OperatorFunction < Record < string , remoteConfig . Value > , Parameter [ ] > => pipe (
140+ withLatestFrom ( remoteConfig ) ,
141+ scan ( ( existing , [ all , rc ] ) => {
142+ // SEMVER use "new Set" to unique once we're only targeting es6
143+ // at the scale we expect remote config to be at, we probably won't see a performance hit from this unoptimized uniqueness implementation
144+ // const allKeys = [...new Set([...existing.map(p => p.key), ...Object.keys(all)])];
145+ const allKeys = [ ...existing . map ( p => p . key ) , ...Object . keys ( all ) ] . filter ( ( v , i , a ) => a . indexOf ( v ) === i ) ;
146+ return allKeys . map ( key => {
147+ const updatedValue = all [ key ] ;
148+ return updatedValue ? new Parameter ( key , rc ? rc . fetchTimeMillis : - 1 , updatedValue . getSource ( ) , updatedValue . asString ( ) )
149+ : existing . find ( p => p . key === key ) !
150+ } ) ;
151+ } , [ ] as Array < Parameter > )
152+ ) ;
153+
154+ const PROXY_DEFAULTS = { 'asNumber' : 0 , 'asBoolean' : false , 'asString' : undefined } ;
155+
156+
157+ function mapToObject ( fn : 'asNumber' ) : UnaryFunction < Observable < Parameter [ ] > , Observable < Record < string , number > > > ;
158+ function mapToObject ( fn : 'asBoolean' ) : UnaryFunction < Observable < Parameter [ ] > , Observable < Record < string , boolean > > > ;
159+ function mapToObject ( fn : 'asString' ) : UnaryFunction < Observable < Parameter [ ] > , Observable < Record < string , string | undefined > > > ;
160+ function mapToObject ( fn : 'asNumber' | 'asBoolean' | 'asString' ) {
161+ return pipe (
162+ map ( ( params : Parameter [ ] ) => params . reduce ( ( c , p ) => ( { ...c , [ p . key ] : p [ fn ] ( ) } ) , { } as Record < string , number | boolean | string | undefined > ) ) ,
163+ distinctUntilChanged ( ( a , b ) => JSON . stringify ( a ) === JSON . stringify ( b ) )
164+ ) ;
165+ } ;
166+
167+ export const mapAsStrings = ( ) => mapToObject ( 'asString' ) ;
168+ export const mapAsBooleans = ( ) => mapToObject ( 'asBoolean' ) ;
169+ export const mapAsNumbers = ( ) => mapToObject ( 'asNumber' ) ;
170+
171+ // TODO look into the types here, I don't like the anys
172+ const proxyAll = ( observable : Observable < Parameter [ ] > , fn : 'asNumber' | 'asBoolean' | 'asString' ) => new Proxy (
173+ observable . pipe ( mapToObject ( fn as any ) ) , {
174+ get : ( self , name :string ) => self [ name ] || self . pipe (
175+ map ( all => all [ name ] || PROXY_DEFAULTS [ fn ] ) ,
176+ distinctUntilChanged ( )
177+ )
178+ }
179+ ) as any ;
0 commit comments