featurehub-javascript-client-sdk
Version:
FeatureHub client/browser SDK
199 lines (156 loc) • 6.25 kB
text/typescript
import { AnalyticsCollector } from './analytics';
import { ClientContext } from './client_context';
import { BaseClientContext } from './context_impl';
import { PostLoadNewFeatureStateAvailableListener, Readyness, ReadynessListener } from './featurehub_repository';
import { FeatureStateHolder } from './feature_state';
import { FeatureStateBaseHolder } from './feature_state_holders';
import { FeatureStateValueInterceptor, InterceptorValueMatch } from './interceptors';
import { InternalFeatureRepository } from './internal_feature_repository';
import { FeatureEnvironmentCollection, FeatureValueType, FeatureRolloutStrategy, SSEResultState } from './models';
import { Applied, ApplyFeature } from './strategy_matcher';
import { CatchReleaseListenerHandler } from './feature_hub_config';
class LocalFeatureRepository implements InternalFeatureRepository {
// indexed by key as that what the user cares about
private features = new Map<string, FeatureStateBaseHolder>();
private analyticsCollectors = new Array<AnalyticsCollector>();
private _matchers: Array<FeatureStateValueInterceptor> = [];
private readonly _applyFeature: ApplyFeature;
constructor(environment: FeatureEnvironmentCollection, applyFeature?: ApplyFeature) {
this._applyFeature = applyFeature || new ApplyFeature();
environment.features?.forEach((fs) => {
const holder = new FeatureStateBaseHolder(this, fs.key);
holder.setFeatureState(fs);
this.features.set(fs.key, holder);
});
}
public apply(strategies: Array<FeatureRolloutStrategy>, key: string, featureValueId: string,
context: ClientContext): Applied {
return this._applyFeature.apply(strategies, key, featureValueId, context);
}
public get readyness(): Readyness {
return Readyness.Ready;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
public notify(state: SSEResultState, data: any): void {}
public addValueInterceptor(matcher: FeatureStateValueInterceptor) {
this._matchers.push(matcher);
matcher.repository(this);
}
public valueInterceptorMatched(key: string): InterceptorValueMatch | undefined {
for (const matcher of this._matchers) {
const m = matcher.matched(key);
if (m?.value) {
return m;
}
}
return undefined;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
public addPostLoadNewFeatureStateAvailableListener(_: PostLoadNewFeatureStateAvailableListener): CatchReleaseListenerHandler {
return 0;
}
public removePostLoadNewFeatureStateAvailableListener(_: PostLoadNewFeatureStateAvailableListener | CatchReleaseListenerHandler) {
}
public addReadynessListener(listener: ReadynessListener): number {
return this.addReadinessListener(listener);
}
public addReadinessListener(listener: ReadynessListener, _?: boolean): number {
listener(Readyness.Ready, true);
return 0;
}
public removeReadinessListener(_: ReadynessListener | number) {
}
notReady(): void {
}
public async broadcastReadynessState() {
}
public addAnalyticCollector(collector: AnalyticsCollector): void {
this.analyticsCollectors.push(collector);
}
public simpleFeatures(): Map<string, string | undefined> {
const vals = new Map<string, string | undefined>();
this.features.forEach((value, key) => {
if (value.getKey()) { // only include value features
let val: any;
switch (value.getType()) {// we need to pick up any overrides
case FeatureValueType.Boolean:
val = value.getBoolean() ? 'true' : 'false';
break;
case FeatureValueType.String:
val = value.getString();
break;
case FeatureValueType.Number:
val = value.getNumber();
break;
case FeatureValueType.Json:
val = value.getRawJson();
break;
default:
val = undefined;
}
vals.set(key, val === undefined ? val : val.toString());
}
});
return vals;
}
public logAnalyticsEvent(action: string, other?: Map<string, string>, ctx?: ClientContext) {
const featureStateAtCurrentTime: Array<FeatureStateBaseHolder> = [];
for (const fs of this.features.values()) {
if (fs.isSet()) {
const fsVal: FeatureStateBaseHolder = ctx == null ? fs : fs.withContext(ctx) as FeatureStateBaseHolder;
featureStateAtCurrentTime.push(fsVal.analyticsCopy());
}
}
this.analyticsCollectors.forEach((ac) => ac.logEvent(action, other || new Map<string,string>(),
featureStateAtCurrentTime));
}
public hasFeature(key: string): undefined | FeatureStateHolder {
return this.features.get(key);
}
public feature<T = any>(key: string): FeatureStateHolder<T> {
let holder = this.features.get(key);
if (holder === undefined) {
holder = new FeatureStateBaseHolder<T>(this, key);
this.features.set(key, holder);
}
return holder;
}
// deprecated
public getFeatureState(key: string): FeatureStateHolder {
return this.feature(key);
}
get catchAndReleaseMode(): boolean {
return false;
}
set catchAndReleaseMode(value: boolean) {}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
public async release(disableCatchAndRelease?: boolean): Promise<void> {}
public getFlag(key: string): boolean | undefined {
return this.feature(key).getFlag();
}
public getString(key: string): string | undefined {
return this.feature(key).getString();
}
public getJson(key: string): string | undefined {
return this.feature(key).getRawJson();
}
public getNumber(key: string): number | undefined {
return this.feature(key).getNumber();
}
public isSet(key: string): boolean {
return this.feature(key).isSet();
}
}
export class LocalClientContext extends BaseClientContext {
constructor(environment: FeatureEnvironmentCollection) {
super(new LocalFeatureRepository(environment));
}
// eslint-disable-next-line require-await
async build(): Promise<ClientContext> {
return this;
}
feature(name: string): FeatureStateHolder {
return this._repository.feature(name).withContext(this);
}
close() {}
}