import { Inject , Injectable , InjectionToken , NgZone , Optional , PLATFORM_ID } from '@angular/core' ; import { concat , EMPTY , MonoTypeOperatorFunction , Observable , of , OperatorFunction , pipe } from 'rxjs' ; import { debounceTime , distinctUntilChanged , filter , groupBy , map , mergeMap , observeOn , scan , shareReplay , startWith , switchMap , withLatestFrom } from 'rxjs/operators' ; import { ɵAngularFireSchedulers , keepUnstableUntilFirst } from '@angular/fire' ; import { ɵlazySDKProxy , ɵPromiseProxy , ɵapplyMixins } from '@angular/fire/compat' ; import { FirebaseOptions } from 'firebase/app' ; import { ɵfirebaseAppFactory , FIREBASE_APP_NAME , FIREBASE_OPTIONS , ɵcacheInstance } from '@angular/fire/compat' ; import firebase from 'firebase/compat/app' ; import { Settings } from './interfaces' ; import { proxyPolyfillCompat } from './base' ; import { isSupported } from 'firebase/remote-config' ; export interface ConfigTemplate { [ key ] : string | number | boolean ; } export const SETTINGS = new InjectionToken < Settings > ( 'angularfire2.remoteConfig.settings' ) ; export const DEFAULTS = new InjectionToken < ConfigTemplate > ( 'angularfire2.remoteConfig.defaultConfig' ) ; export interface AngularFireRemoteConfig extends ɵPromiseProxy < firebase . remoteConfig . RemoteConfig > { } const AS_TO_FN = { strings : 'asString' , numbers : 'asNumber' , booleans : 'asBoolean' } ; const STATIC_VALUES = { numbers : 0 , booleans : false , strings : undefined } ; const proxyAll = ( observable , as ) => new Proxy ( observable . pipe ( mapToObject ( as as any ) ) , { get : ( self , name ) => self [ name ] || observable . pipe ( map ( all => all . find ( p => p . key === name ) ) , map ( param => param ? param [ AS_TO_FN [ as ] ] ( ) : STATIC_VALUES [ as ] ) , distinctUntilChanged ( ) ) } ) as any ; export class Value implements firebase . remoteConfig . Value { asBoolean ( ) { return [ '1' , 'true' , 't' , 'y' , 'yes' , 'on' ] . indexOf ( this . _value . toLowerCase ( ) ) > - 1 ; } asString ( ) { return this . _value ; } asNumber ( ) { return Number ( this . _value ) || 0 ; } getSource ( ) { return this . _source ; } constructor ( public _source , public _value ) { } } export class Parameter extends Value { constructor ( public key , public fetchTimeMillis , source , value ) { super ( source , value ) ; } } const filterTest = ( fn ) => filter < Parameter | Parameter [ ] > ( it => Array . isArray ( it ) ? it . some ( fn ) : fn ( it ) ) ; export const filterRemote = ( ) => filterTest ( p => p . getSource ( ) === 'remote' ) ; export const filterFresh = ( howRecentInMillis ) => filterTest ( p => p . fetchTimeMillis + howRecentInMillis >= new Date ( ) . getTime ( ) ) ; const scanToParametersArray = ( remoteConfig ) : OperatorFunction < { [ key ] : firebase . remoteConfig . Value ; } , Parameter [ ] > => pipe ( withLatestFrom ( remoteConfig ) , scan ( ( existing , [ all , rc ] ) => { 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 > ) ) ; @ Injectable ( { providedIn : 'any' } ) export class AngularFireRemoteConfig { readonly changes : Observable < Parameter > ; readonly parameters : Observable < Parameter [ ] > ; readonly numbers : Observable < { [ key ] : number | undefined ; } > & { [ key ] : Observable < number > ; } ; readonly booleans : Observable < { [ key ] : boolean | undefined ; } > & { [ key ] : Observable < boolean > ; } ; readonly strings : Observable < { [ key ] : string | undefined ; } > & { [ key ] : Observable < string | undefined > ; } ; constructor ( @ Inject ( FIREBASE_OPTIONS ) options , @ Optional ( ) @ Inject ( FIREBASE_APP_NAME ) name , @ Optional ( ) @ Inject ( SETTINGS ) settings , @ Optional ( ) @ Inject ( DEFAULTS ) defaultConfig , private zone , schedulers , @ Inject ( PLATFORM_ID ) platformId ) { const remoteConfig$ = of ( undefined ) . pipe ( observeOn ( schedulers . outsideAngular ) , switchMap ( ( ) => isSupported ( ) ) , switchMap ( isSupported => isSupported ? import ( 'firebase/compat/remote-config' ) : EMPTY ) , map ( ( ) => ɵfirebaseAppFactory ( options , zone , name ) ) , map ( app => ɵcacheInstance ( `${ app . name }.remote-config` , 'AngularFireRemoteConfig' , app . name , ( ) => { const rc = app . remoteConfig ( ) ; if ( settings ) { rc . settings = settings ; } if ( defaultConfig ) { rc . defaultConfig = defaultConfig ; } return rc ; } , [ settings , defaultConfig ] ) ) , startWith < firebase . remoteConfig . RemoteConfig , undefined > ( undefined ) , shareReplay ( { bufferSize : 1 , refCount : false } ) ) ; const loadedRemoteConfig$ = remoteConfig$ . pipe ( filter < firebase . remoteConfig . RemoteConfig > ( rc => ! ! rc ) ) ; const default$ = of ( Object . keys ( defaultConfig || { } ) . reduce ( ( c , k ) => ( { ... c , [ k ] : new Value ( 'default' , defaultConfig [ k ] . toString ( ) ) } ) , { } ) ) ; const filterOutDefaults = map < { [ key ] : firebase . remoteConfig . Value ; } , { [ key ] : firebase . remoteConfig . Value ; } > ( all => Object . keys ( all ) . filter ( key => all [ key ] . getSource ( ) !== 'default' ) . reduce ( ( acc , key ) => ( { ... acc , [ key ] : all [ key ] } ) , { } ) ) ; const existing$ = loadedRemoteConfig$ . pipe ( switchMap ( rc => rc . activate ( ) . then ( ( ) => rc . ensureInitialized ( ) ) . then ( ( ) => rc . getAll ( ) ) ) , filterOutDefaults ) ; const fresh$ = loadedRemoteConfig$ . pipe ( switchMap ( rc => zone . runOutsideAngular ( ( ) => rc . fetchAndActivate ( ) . then ( ( ) => rc . ensureInitialized ( ) ) . then ( ( ) => rc . getAll ( ) ) ) ) , filterOutDefaults ) ; this . parameters = concat ( default$ , existing$ , fresh$ ) . pipe ( scanToParametersArray ( remoteConfig$ ) , keepUnstableUntilFirst , shareReplay ( { bufferSize : 1 , refCount : true } ) ) ; this . changes = this . parameters . pipe ( switchMap ( params => of ( ... params ) ) , groupBy ( param => param . key ) , mergeMap ( group => group . pipe ( distinctUntilChanged ( ) ) ) ) ; this . strings = proxyAll ( this . parameters , 'strings' ) ; this . booleans = proxyAll ( this . parameters , 'booleans' ) ; this . numbers = proxyAll ( this . parameters , 'numbers' ) ; return ɵlazySDKProxy ( this , loadedRemoteConfig$ , zone ) ; } } export const budget = < T > ( interval ) : MonoTypeOperatorFunction < T > => ( source ) => new Observable < T > ( observer => { let timedOut = false ; const timeout = setTimeout ( ( ) => { observer . complete ( ) ; timedOut = true ; } , interval ) ; return source . subscribe ( { next ( val ) { if ( ! timedOut ) { observer . next ( val ) ; } } , error ( err ) { if ( ! timedOut ) { clearTimeout ( timeout ) ; observer . error ( err ) ; } } , complete ( ) { if ( ! timedOut ) { clearTimeout ( timeout ) ; observer . complete ( ) ; } } } ) ; } ) ; const typedMethod = ( it ) => { switch ( typeof it ) { case 'string' : return 'asString' ; case 'boolean' : return 'asBoolean' ; case 'number' : return 'asNumber' ; default : return 'asString' ; } } ; export function scanToObject ( ) ; export function scanToObject ( to ) ; export function scanToObject ( to ) ; export function scanToObject ( to ) ; export function scanToObject < T extends ConfigTemplate > ( template ) ; export function scanToObject < T extends ConfigTemplate > ( to = 'strings' ) { return pipe ( scan ( ( c , p ) => ( { ... c , [ p . key ] : typeof to === 'object' ? p [ typedMethod ( to [ p . key ] ) ] ( ) : p [ AS_TO_FN [ to ] ] ( ) } ) , typeof to === 'object' ? to as T & { [ key ] : string | undefined ; } : { } as { [ key ] : number | boolean | string ; } ) , debounceTime ( 1 ) , budget ( 10 ) , distinctUntilChanged ( ( a , b ) => JSON . stringify ( a ) === JSON . stringify ( b ) ) ) ; } export function mapToObject ( ) ; export function mapToObject ( to ) ; export function mapToObject ( to ) ; export function mapToObject ( to ) ; export function mapToObject < T extends ConfigTemplate > ( template ) ; export function mapToObject < T extends ConfigTemplate > ( to = 'strings' ) { return pipe ( map ( ( params ) => params . reduce ( ( c , p ) => ( { ... c , [ p . key ] : typeof to === 'object' ? p [ typedMethod ( to [ p . key ] ) ] ( ) : p [ AS_TO_FN [ to ] ] ( ) } ) , typeof to === 'object' ? to as T & { [ key ] : string | undefined ; } : { } as { [ key ] : number | boolean | string ; } ) ) , distinctUntilChanged ( ( a , b ) => JSON . stringify ( a ) === JSON . stringify ( b ) ) ) ; } ɵapplyMixins ( AngularFireRemoteConfig , [ proxyPolyfillCompat ] ) ;