Skip to content

Commit 4601932

Browse files
committed
Mapping to objects and templates, budget pipe
1 parent e2d83c8 commit 4601932

File tree

1 file changed

+64
-26
lines changed

1 file changed

+64
-26
lines changed

src/remote-config/remote-config.ts

+64-26
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import { Injectable, Inject, Optional, NgZone, InjectionToken, PLATFORM_ID } from '@angular/core';
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';
2+
import { Observable, concat, of, pipe, OperatorFunction } from 'rxjs';
3+
import { map, switchMap, tap, shareReplay, distinctUntilChanged, filter, groupBy, mergeMap, scan, withLatestFrom, startWith, debounceTime } from 'rxjs/operators';
44
import { FirebaseAppConfig, FirebaseOptions, ɵlazySDKProxy, FIREBASE_OPTIONS, FIREBASE_APP_NAME } from '@angular/fire';
55
import { remoteConfig } from 'firebase/app';
66

7-
export interface DefaultConfig {[key:string]: string|number|boolean};
7+
export interface ConfigTemplate {[key:string]: string|number|boolean};
88

99
export const REMOTE_CONFIG_SETTINGS = new InjectionToken<remoteConfig.Settings>('angularfire2.remoteConfig.settings');
10-
export const DEFAULT_CONFIG = new InjectionToken<DefaultConfig>('angularfire2.remoteConfig.defaultConfig');
10+
export const DEFAULT_CONFIG = new InjectionToken<ConfigTemplate>('angularfire2.remoteConfig.defaultConfig');
1111

1212
import { FirebaseRemoteConfig, _firebaseAppFactory, runOutsideAngular } from '@angular/fire';
1313
import { isPlatformServer } from '@angular/common';
@@ -65,15 +65,15 @@ export class AngularFireRemoteConfig {
6565

6666
readonly changes: Observable<Parameter>;
6767
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>>;
68+
readonly numbers: Observable<{[key:string]: number}> & {[key:string]: Observable<number>};
69+
readonly booleans: Observable<{[key:string]: boolean}> & {[key:string]: Observable<boolean>};
70+
readonly strings: Observable<{[key:string]: string}> & {[key:string]: Observable<string|undefined>};
7171

7272
constructor(
7373
@Inject(FIREBASE_OPTIONS) options:FirebaseOptions,
7474
@Optional() @Inject(FIREBASE_APP_NAME) nameOrConfig:string|FirebaseAppConfig|null|undefined,
7575
@Optional() @Inject(REMOTE_CONFIG_SETTINGS) settings:remoteConfig.Settings|null,
76-
@Optional() @Inject(DEFAULT_CONFIG) defaultConfig:DefaultConfig|null,
76+
@Optional() @Inject(DEFAULT_CONFIG) defaultConfig:ConfigTemplate|null,
7777
@Inject(PLATFORM_ID) platformId:Object,
7878
private zone: NgZone
7979
) {
@@ -123,9 +123,9 @@ export class AngularFireRemoteConfig {
123123
))
124124
);
125125

126-
this.strings = proxyAll(this.parameters, 'asString');
127-
this.booleans = proxyAll(this.parameters, 'asBoolean');
128-
this.numbers = proxyAll(this.parameters, 'asNumber');
126+
this.strings = proxyAll(this.parameters, 'strings');
127+
this.booleans = proxyAll(this.parameters, 'booleans');
128+
this.numbers = proxyAll(this.parameters, 'numbers');
129129

130130
// TODO fix the proxy for server
131131
return isPlatformServer(platformId) ? this : ɵlazySDKProxy(this, remoteConfig$, zone);
@@ -136,7 +136,7 @@ export class AngularFireRemoteConfig {
136136
// I ditched loading the defaults into RC and a simple map for scan since we already have our own defaults implementation.
137137
// 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.
138138
// 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(
139+
const scanToParametersArray = (remoteConfig: Observable<remoteConfig.RemoteConfig|undefined>): OperatorFunction<{[key:string]: remoteConfig.Value}, Parameter[]> => pipe(
140140
withLatestFrom(remoteConfig),
141141
scan((existing, [all, rc]) => {
142142
// SEMVER use "new Set" to unique once we're only targeting es6
@@ -151,28 +151,66 @@ const scanToParametersArray = (remoteConfig: Observable<remoteConfig.RemoteConfi
151151
}, [] as Array<Parameter>)
152152
);
153153

154-
const PROXY_DEFAULTS = {'asNumber': 0, 'asBoolean': false, 'asString': undefined};
155-
154+
const AS_TO_FN = { 'strings': 'asString', 'numbers': 'asNumber', 'booleans': 'asBoolean' };
155+
const PROXY_DEFAULTS = { 'numbers': 0, 'booleans': false, 'strings': undefined };
156+
157+
export const budget = (interval: number) => <T>(source: Observable<T>) => new Observable<T>(observer => {
158+
let timedOut = false;
159+
// TODO use scheduler task rather than settimeout
160+
const timeout = setTimeout(() => {
161+
observer.complete();
162+
timedOut = true;
163+
}, interval);
164+
return source.subscribe({
165+
next(val) { if (!timedOut) { observer.next(val); } },
166+
error(err) { if (!timedOut) { clearTimeout(timeout); observer.error(err); } },
167+
complete() { if (!timedOut) { clearTimeout(timeout); observer.complete(); } }
168+
})
169+
});
170+
171+
const typedMethod = (it:any) => {
172+
switch(typeof it) {
173+
case 'string': return 'asString';
174+
case 'boolean': return 'asBoolean';
175+
case 'number': return 'asNumber';
176+
default: return 'asString';
177+
}
178+
};
156179

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') {
180+
export function scanToObject(): OperatorFunction<Parameter, {[key:string]: string}>;
181+
export function scanToObject(as: 'numbers'): OperatorFunction<Parameter, {[key:string]: number}>;
182+
export function scanToObject(as: 'booleans'): OperatorFunction<Parameter, {[key:string]: boolean}>;
183+
export function scanToObject(as: 'strings'): OperatorFunction<Parameter, {[key:string]: string}>;
184+
export function scanToObject<T extends ConfigTemplate>(template: T): OperatorFunction<Parameter, T & {[key:string]: string|undefined}>;
185+
export function scanToObject(as: 'numbers'|'booleans'|'strings'|ConfigTemplate = 'strings') {
161186
return pipe(
162-
map((params: Parameter[]) => params.reduce((c, p) => ({...c, [p.key]: p[fn]()}), {} as Record<string, number|boolean|string|undefined>)),
187+
// TODO cleanup
188+
scan((c, p: Parameter) => ({...c, [p.key]: typeof as === 'object' ? p[typedMethod(as[p.key])]() : p[AS_TO_FN[as]]()}), typeof as === 'object' ? as : {} as {[key:string]: number|boolean|string}),
189+
debounceTime(1),
190+
budget(10),
163191
distinctUntilChanged((a,b) => JSON.stringify(a) === JSON.stringify(b))
164192
);
165193
};
166194

167-
export const mapAsStrings = () => mapToObject('asString');
168-
export const mapAsBooleans = () => mapToObject('asBoolean');
169-
export const mapAsNumbers = () => mapToObject('asNumber');
195+
export function mapToObject(): OperatorFunction<Parameter[], {[key:string]: string}>;
196+
export function mapToObject(as: 'numbers'): OperatorFunction<Parameter[], {[key:string]: number}>;
197+
export function mapToObject(as: 'booleans'): OperatorFunction<Parameter[], {[key:string]: boolean}>;
198+
export function mapToObject(as: 'strings'): OperatorFunction<Parameter[], {[key:string]: string}>;
199+
export function mapToObject<T extends ConfigTemplate>(template: T): OperatorFunction<Parameter[], T & {[key:string]: string|undefined}>;
200+
export function mapToObject(as: 'numbers'|'booleans'|'strings'|ConfigTemplate = 'strings') {
201+
return pipe(
202+
// TODO this is getting a little long, cleanup
203+
map((params: Parameter[]) => params.reduce((c, p) => ({...c, [p.key]: typeof as === 'object' ? p[typedMethod(as[p.key])]() : p[AS_TO_FN[as]]()}), typeof as === 'object' ? as : {} as {[key:string]: number|boolean|string})),
204+
distinctUntilChanged((a,b) => JSON.stringify(a) === JSON.stringify(b))
205+
);
206+
};
170207

171208
// 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]),
209+
const proxyAll = (observable: Observable<Parameter[]>, as: 'numbers'|'booleans'|'strings') => new Proxy(
210+
observable.pipe(mapToObject(as as any)), {
211+
get: (self, name:string) => self[name] || observable.pipe(
212+
map(all => all.find(p => p.key === name)),
213+
map(param => param ? param[AS_TO_FN[as]]() : PROXY_DEFAULTS[as]),
176214
distinctUntilChanged()
177215
)
178216
}

0 commit comments

Comments
 (0)