UNPKG

featurehub-javascript-client-sdk

Version:
301 lines (241 loc) 8.03 kB
import { FeatureListener, FeatureListenerHandle, FeatureStateHolder } from './feature_state'; import { FeatureState, FeatureValueType } from './models'; import { ClientContext } from './client_context'; import { InternalFeatureRepository } from './internal_feature_repository'; import { ListenerUtils } from './listener_utils'; import { fhLog } from './feature_hub_config'; interface ListenerTracker { listener: FeatureListener; holder: FeatureStateHolder; } interface ListenerOriginal { value: any; } export class FeatureStateBaseHolder<T = any> implements FeatureStateHolder<T> { protected internalFeatureState: FeatureState | undefined; protected _key: string; protected listeners: Map<number, ListenerTracker> = new Map<number, ListenerTracker>(); protected _repo: InternalFeatureRepository; protected _ctx: ClientContext | undefined; // eslint-disable-next-line no-use-before-define protected parentHolder: FeatureStateBaseHolder | undefined; constructor(repository: InternalFeatureRepository, key: string, existingHolder?: FeatureStateBaseHolder) { if (existingHolder !== null && existingHolder !== undefined) { this.listeners = existingHolder.listeners; } this._repo = repository; this._key = key; } get key(): string { return this.getKey(); } get str(): string | undefined { return this.getString(); } get flag(): boolean | undefined { return this.getFlag(); } get num(): number | undefined { return this.getNumber(); } get rawJson(): string | undefined { return this.getRawJson(); } get exists(): boolean { return this.internalFeatureState !== undefined; } get locked(): boolean { return this.isLocked(); } get enabled(): boolean { return this.isEnabled(); } get version(): number { return this.getVersion(); } get type(): FeatureValueType | undefined { return this.getType(); } public withContext(param: ClientContext): FeatureStateHolder { const fsh = this._copy(); fsh._ctx = param; return fsh; } public isEnabled(): boolean { return this.getBoolean() === true; } public addListener(listener: FeatureListener<T>): FeatureListenerHandle { const pos = ListenerUtils.newListenerKey(this.listeners); if (this._ctx !== undefined) { this.listeners.set(pos, { listener: () => listener(this), holder: this }); } else { this.listeners.set(pos, { listener: listener, holder: this }); } return pos; } public removeListener(handle: FeatureListener<T> | FeatureListenerHandle) { ListenerUtils.removeListener(this.listeners, handle); } public getBoolean(): boolean | undefined { return this._getValue(FeatureValueType.Boolean) as boolean | undefined; } public getFlag(): boolean | undefined { return this.getBoolean(); } public getKey(): string { return this._key; } getNumber(): number | undefined { return this._getValue(FeatureValueType.Number) as number | undefined; } getRawJson(): string | undefined { return this._getValue(FeatureValueType.Json) as string | undefined; } getString(): string | undefined { return this._getValue(FeatureValueType.String) as string | undefined; } isSet(): boolean { const val = this._getValue(); return val !== undefined && val != null; } getFeatureState(): FeatureState | undefined { return this.featureState(); } /// returns true if the value changed, _only_ the repository should call this /// as it is dereferenced via the parentHolder setFeatureState(fs: FeatureState | undefined): boolean { const existingValue = this._getValue(); const existingLocked = this.locked; // capture all the original values of the listeners const listenerValues: Map<number, ListenerOriginal> = new Map<number, ListenerOriginal>(); this.listeners.forEach((value, key) => { listenerValues.set(key, { value: value.holder.value }); }); this.internalFeatureState = fs; // the lock changing is not part of the contextual evaluation of values changing, and is constant across all listeners. const changedLocked = existingLocked !== this.featureState()?.l; // did at least the default value change, even if there are no listeners for the state? let changed = changedLocked || existingValue !== this._getValue(fs?.type); this.listeners.forEach((value, key) => { const original = listenerValues.get(key); if (changedLocked || original?.value !== value.holder.value) { changed = true; try { value.listener(value.holder); } catch (e) { fhLog.error('Failed to trigger listener', e); } } }); return changed; } copy(): FeatureStateHolder { return this._copy(); } // we need the internal feature state set to be consistent analyticsCopy(): FeatureStateBaseHolder { const c = this._copy(); c.internalFeatureState = this.internalFeatureState; return c; } getType(): FeatureValueType | undefined { return this.featureState()?.type; } getVersion(): number { const version1 = this.featureState()?.version; return version1 !== undefined ? version1 : -1; } isLocked(): boolean { return this.featureState() === undefined ? false : this.featureState()!.l!; } triggerListeners(feature: FeatureStateHolder): void { this.listeners.forEach((l) => { try { l.listener(feature || this); } catch (_) { // } // don't care }); } private _copy(): FeatureStateBaseHolder { const bh = new FeatureStateBaseHolder(this._repo, this._key, this); bh.parentHolder = this; return bh; } private featureState(): FeatureState | undefined { if (this.internalFeatureState !== undefined) { return this.internalFeatureState; } if (this.parentHolder !== undefined) { return this.parentHolder.featureState(); } return this.internalFeatureState; } private _getValue(type?: FeatureValueType, parseJson = false): any | undefined { if (!type) { type = this.getType(); } if (!type) { return undefined; } if (!this.isLocked()) { const intercept = this._repo.valueInterceptorMatched(this._key); if (intercept?.value) { return this._castType(type, intercept.value, parseJson); } } const featureState = this.featureState(); if (!featureState || (featureState.type !== type)) { return undefined; } if (this._ctx != null && featureState.strategies?.length) { const matched = this._repo.apply(featureState!.strategies || [], this._key, featureState.id, this._ctx); if (matched.matched) { return this._castType(type, matched.value, parseJson); } } return featureState?.value; } private _castType(type: FeatureValueType, value?: any, parseJson = false): any | undefined { if (value == null) { return undefined; } if (type === FeatureValueType.Boolean) { return typeof value === 'boolean' ? value : ('true' === value.toString()); } else if (type === FeatureValueType.String) { return value.toString(); } else if (type === FeatureValueType.Number) { if (typeof value === 'number') { return value; } if (value.includes('.')) { return parseFloat(value); } // tslint:disable-next-line:radix return parseInt(value); } else if (type === FeatureValueType.Json) { if (parseJson) { try { return JSON.parse(value.toString()); } catch { return {}; // default return empty obj } } return value.toString(); } else { return value.toString(); } } get value(): T { return this._getValue(this.getType(), true); } get featureProperties(): Record<string, string> | undefined { return this.featureState()?.fp ?? undefined; } }