UNPKG

featurehub-javascript-client-sdk

Version:
282 lines (225 loc) 7.46 kB
import { InternalFeatureRepository } from './internal_feature_repository'; import { EdgeServiceSupplier, FeatureHubConfig, fhLog } from './feature_hub_config'; import { StrategyAttributeCountryName, StrategyAttributeDeviceName, StrategyAttributePlatformName, } from './models'; import { FeatureStateHolder } from './feature_state'; import { EdgeService } from './edge_service'; import { FeatureHubRepository } from './featurehub_repository'; import { ClientContext } from './client_context'; export abstract class BaseClientContext implements ClientContext { protected readonly _repository: InternalFeatureRepository; protected _attributes = new Map<string, Array<string>>(); protected constructor(repository: InternalFeatureRepository) { this._repository = repository; } userKey(value: string): ClientContext { this._attributes.set('userkey', [value]); return this; } sessionKey(value: string): ClientContext { this._attributes.set('session', [value]); return this; } country(value: StrategyAttributeCountryName): ClientContext { this._attributes.set('country', [value]); return this; } device(value: StrategyAttributeDeviceName): ClientContext { this._attributes.set('device', [value]); return this; } platform(value: StrategyAttributePlatformName): ClientContext { this._attributes.set('platform', [value]); return this; } version(version: string): ClientContext { this._attributes.set('version', [version]); return this; } /** * @deprecated - use attributeValue * @param key * @param value */ // eslint-disable-next-line camelcase attribute_value(key: string, value: string): ClientContext { return this.attributeValue(key, value); } attributeValue(key: string, value: string): ClientContext { this._attributes.set(key, [value]); return this; } /** * @deprecated - use attributeValues * @param key * @param values */ // eslint-disable-next-line camelcase attribute_values(key: string, values: Array<string>): ClientContext { return this.attributeValues(key, values); } attributeValues(key: string, values: Array<string>): ClientContext { this._attributes.set(key, values); return this; } clear(): ClientContext { this._attributes.clear(); return this; } getAttr(key: string, defaultValue?: string): string | undefined { if (this._attributes.has(key)) { return this._attributes.get(key)![0]; } return defaultValue; } getAttrs(key: string): Array<string> { if (this._attributes.has(key)) { return this._attributes.get(key)!; } return []; } defaultPercentageKey(): string | undefined { return this._attributes.has('session') ? this.getAttr('session') : this.getAttr('userkey'); } isEnabled(name: string): boolean { return this.feature(name).isEnabled(); } isSet(name: string): boolean { return this.feature(name).isSet(); } getNumber(name: string, def?: number): number | undefined { const fsh = this.feature(name); return fsh.isSet() ? fsh.getNumber() : def; } getString(name: string, def?: string): string | undefined { const fsh = this.feature(name); return fsh.isSet() ? fsh.getString() : def; } getJson(name: string, def?: any): any | undefined { const fsh = this.feature(name); if (fsh.isSet()) { const val = fsh.getRawJson(); return JSON.parse(val!); } else { return def; } } getRawJson(name: string, def?: string): string | undefined { const fsh = this.feature(name); return fsh.isSet() ? fsh.getRawJson() : def; } getFlag(name: string, def?: boolean): boolean | undefined { const fsh = this.feature(name); return fsh.isSet() ? fsh.getBoolean() : def; } getBoolean(name: string, def?: boolean): boolean | undefined { const fsh = this.feature(name); return fsh.isSet() ? fsh.getBoolean() : def; } abstract build(): Promise<ClientContext>; abstract feature(name: string): FeatureStateHolder; // feature(name: string): FeatureStateHolder { // return this._repository.feature(name); // } abstract close(): void; repository(): FeatureHubRepository { return this._repository; } logAnalyticsEvent(action: string, other?: Map<string, string>, user?: string): void { if (user == null) { user = this.getAttr('userkey'); } if (user != null) { if (other == null) { other = new Map<string, string>(); } other.set('cid', user); } this._repository.logAnalyticsEvent(action, other); } } export class ServerEvalFeatureContext extends BaseClientContext { private readonly _edgeServiceSupplier: EdgeServiceSupplier; private _currentEdge: EdgeService | undefined; private _config?: FeatureHubConfig; private _xHeader: string | undefined; private _clientCount = 0; constructor(repository: InternalFeatureRepository, edgeServiceSupplier: EdgeServiceSupplier, config?: FeatureHubConfig) { super(repository); this._edgeServiceSupplier = edgeServiceSupplier; this._config = config; } addClient(): void { this._clientCount += 1; } removeClient(): boolean { this._clientCount -= 1; return this._clientCount <= 0; } async build(): Promise<ClientContext> { try { const newHeader = Array.from(this._attributes.entries()).map((key, ) => key[0] + '=' + encodeURIComponent(key[1].join(','))).sort().join(','); if (newHeader !== this._xHeader) { this._xHeader = newHeader; this._repository.notReady(); if (this._currentEdge != null && this._currentEdge.requiresReplacementOnHeaderChange()) { this._currentEdge.close(); this._currentEdge = undefined; } } if (this._currentEdge === undefined) { this._currentEdge = this._edgeServiceSupplier(); } if (this._currentEdge !== undefined) { await this._currentEdge.contextChange(this._xHeader); } } catch (e) { if (e) { fhLog.error('Failed to connect to FeatureHHub Edge to refresh context', e); } } return this; } close(): void { if (this._clientCount <= 1 && this._config !== undefined) { fhLog.trace('closing because client count is ', this._clientCount); this._config.close(); // tell the config to close us down } else if (this._currentEdge) { fhLog.trace('closing because directly requested close.'); this._currentEdge.close(); } } edgeService(): EdgeService | undefined { return this._currentEdge; } feature(name: string): FeatureStateHolder { return this._repository.feature(name); } } export class ClientEvalFeatureContext extends BaseClientContext { private readonly _edgeService: EdgeService; constructor(repository: InternalFeatureRepository, edgeService: EdgeService) { super(repository); this._edgeService = edgeService; } // eslint-disable-next-line require-await async build(): Promise<ClientContext> { this._edgeService.poll()?.then(() => {}).catch(() => {}); // in case it hasn't already been initialized return this; } close(): void { this._edgeService.close(); } edgeService(): EdgeService { return this._edgeService; } feature(name: string): FeatureStateHolder { return this._repository.feature(name).withContext(this); } }