|
| 1 | +import { |
| 2 | + EvaluationContext, |
| 3 | + FlagNotFoundError, |
| 4 | + FlagValueType, |
| 5 | + GeneralError, |
| 6 | + JsonValue, |
| 7 | + Logger, |
| 8 | + OpenFeatureError, |
| 9 | + ProviderEvents, |
| 10 | + ResolutionDetails, |
| 11 | + StandardResolutionReasons, |
| 12 | + TypeMismatchError, |
| 13 | + ProviderStatus, |
| 14 | +} from '@openfeature/core'; |
| 15 | +import { Provider } from '../provider'; |
| 16 | +import { OpenFeatureEventEmitter } from '../../events'; |
| 17 | +import { FlagConfiguration, Flag } from './flag-configuration'; |
| 18 | +import { VariantNotFoundError } from './variant-not-found-error'; |
| 19 | + |
| 20 | +/** |
| 21 | + * A simple OpenFeature provider intended for demos and as a test stub. |
| 22 | + */ |
| 23 | +export class InMemoryProvider implements Provider { |
| 24 | + public readonly events = new OpenFeatureEventEmitter(); |
| 25 | + public readonly runsOn = 'client'; |
| 26 | + status: ProviderStatus = ProviderStatus.NOT_READY; |
| 27 | + readonly metadata = { |
| 28 | + name: 'in-memory', |
| 29 | + } as const; |
| 30 | + private _flagConfiguration: FlagConfiguration; |
| 31 | + private _context: EvaluationContext | undefined; |
| 32 | + |
| 33 | + constructor(flagConfiguration: FlagConfiguration = {}) { |
| 34 | + this._flagConfiguration = { ...flagConfiguration }; |
| 35 | + } |
| 36 | + |
| 37 | + async initialize(context?: EvaluationContext | undefined): Promise<void> { |
| 38 | + try { |
| 39 | + |
| 40 | + for (const key in this._flagConfiguration) { |
| 41 | + this.resolveFlagWithReason(key, context); |
| 42 | + } |
| 43 | + |
| 44 | + this._context = context; |
| 45 | + // set the provider's state, but don't emit events manually; |
| 46 | + // the SDK does this based on the resolution/rejection of the init promise |
| 47 | + this.status = ProviderStatus.READY; |
| 48 | + } catch (error) { |
| 49 | + this.status = ProviderStatus.ERROR; |
| 50 | + throw error; |
| 51 | + } |
| 52 | + } |
| 53 | + |
| 54 | + /** |
| 55 | + * Overwrites the configured flags. |
| 56 | + * @param { FlagConfiguration } flagConfiguration new flag configuration |
| 57 | + */ |
| 58 | + async putConfiguration(flagConfiguration: FlagConfiguration) { |
| 59 | + const flagsChanged = Object.entries(flagConfiguration) |
| 60 | + .filter(([key, value]) => this._flagConfiguration[key] !== value) |
| 61 | + .map(([key]) => key); |
| 62 | + |
| 63 | + this.status = ProviderStatus.STALE; |
| 64 | + this.events.emit(ProviderEvents.Stale); |
| 65 | + |
| 66 | + this._flagConfiguration = { ...flagConfiguration }; |
| 67 | + this.events.emit(ProviderEvents.ConfigurationChanged, { flagsChanged }); |
| 68 | + |
| 69 | + try { |
| 70 | + await this.initialize(this._context); |
| 71 | + // we need to emit our own events in this case, since it's not part of the init flow. |
| 72 | + this.events.emit(ProviderEvents.Ready); |
| 73 | + } catch (err) { |
| 74 | + this.events.emit(ProviderEvents.Error); |
| 75 | + throw err; |
| 76 | + } |
| 77 | + } |
| 78 | + |
| 79 | + resolveBooleanEvaluation( |
| 80 | + flagKey: string, |
| 81 | + defaultValue: boolean, |
| 82 | + context?: EvaluationContext, |
| 83 | + logger?: Logger, |
| 84 | + ): ResolutionDetails<boolean> { |
| 85 | + return this.resolveAndCheckFlag<boolean>(flagKey, defaultValue, context || this._context, logger); |
| 86 | + } |
| 87 | + |
| 88 | + resolveNumberEvaluation( |
| 89 | + flagKey: string, |
| 90 | + defaultValue: number, |
| 91 | + context?: EvaluationContext, |
| 92 | + logger?: Logger, |
| 93 | + ): ResolutionDetails<number> { |
| 94 | + return this.resolveAndCheckFlag<number>(flagKey, defaultValue, context || this._context, logger); |
| 95 | + } |
| 96 | + |
| 97 | + resolveStringEvaluation( |
| 98 | + flagKey: string, |
| 99 | + defaultValue: string, |
| 100 | + context?: EvaluationContext, |
| 101 | + logger?: Logger, |
| 102 | + ): ResolutionDetails<string> { |
| 103 | + return this.resolveAndCheckFlag<string>(flagKey, defaultValue, context || this._context, logger); |
| 104 | + } |
| 105 | + |
| 106 | + resolveObjectEvaluation<T extends JsonValue>( |
| 107 | + flagKey: string, |
| 108 | + defaultValue: T, |
| 109 | + context?: EvaluationContext, |
| 110 | + logger?: Logger, |
| 111 | + ): ResolutionDetails<T> { |
| 112 | + return this.resolveAndCheckFlag<T>(flagKey, defaultValue, context || this._context, logger); |
| 113 | + } |
| 114 | + |
| 115 | + private resolveAndCheckFlag<T extends JsonValue | FlagValueType>(flagKey: string, |
| 116 | + defaultValue: T, context?: EvaluationContext, logger?: Logger): ResolutionDetails<T> { |
| 117 | + if (!(flagKey in this._flagConfiguration)) { |
| 118 | + const message = `no flag found with key ${flagKey}`; |
| 119 | + logger?.debug(message); |
| 120 | + throw new FlagNotFoundError(message); |
| 121 | + } |
| 122 | + |
| 123 | + if (this._flagConfiguration[flagKey].disabled) { |
| 124 | + return { value: defaultValue, reason: StandardResolutionReasons.DISABLED }; |
| 125 | + } |
| 126 | + |
| 127 | + const resolvedFlag = this.resolveFlagWithReason(flagKey, context) as ResolutionDetails<T>; |
| 128 | + |
| 129 | + if (resolvedFlag.value === undefined) { |
| 130 | + const message = `no value associated with variant provided for ${flagKey} found`; |
| 131 | + logger?.error(message); |
| 132 | + throw new VariantNotFoundError(message); |
| 133 | + } |
| 134 | + |
| 135 | + if (typeof resolvedFlag.value != typeof defaultValue) { |
| 136 | + throw new TypeMismatchError(); |
| 137 | + } |
| 138 | + |
| 139 | + return resolvedFlag; |
| 140 | + } |
| 141 | + |
| 142 | + private resolveFlagWithReason<T extends JsonValue | FlagValueType>( |
| 143 | + flagKey: string, |
| 144 | + ctx?: EvaluationContext, |
| 145 | + ): ResolutionDetails<T> { |
| 146 | + try { |
| 147 | + const resolutionResult = this.lookupFlagValue<T>(flagKey, ctx); |
| 148 | + |
| 149 | + return resolutionResult; |
| 150 | + } catch (error: unknown) { |
| 151 | + if (!(error instanceof OpenFeatureError)) { |
| 152 | + throw new GeneralError((error as Error)?.message || 'unknown error'); |
| 153 | + } |
| 154 | + throw error; |
| 155 | + } |
| 156 | + } |
| 157 | + |
| 158 | + private lookupFlagValue<T extends JsonValue | FlagValueType>( |
| 159 | + flagKey: string, |
| 160 | + ctx?: EvaluationContext, |
| 161 | + ): ResolutionDetails<T> { |
| 162 | + const flagSpec: Flag = this._flagConfiguration[flagKey]; |
| 163 | + |
| 164 | + const isContextEval = ctx && flagSpec?.contextEvaluator; |
| 165 | + const variant = isContextEval ? flagSpec.contextEvaluator?.(ctx) : flagSpec.defaultVariant; |
| 166 | + |
| 167 | + const value = variant && flagSpec?.variants[variant]; |
| 168 | + |
| 169 | + const evalReason = isContextEval ? StandardResolutionReasons.TARGETING_MATCH : StandardResolutionReasons.STATIC; |
| 170 | + |
| 171 | + const reason = this.status === ProviderStatus.STALE ? StandardResolutionReasons.CACHED : evalReason; |
| 172 | + |
| 173 | + return { |
| 174 | + value: value as T, |
| 175 | + ...(variant && { variant }), |
| 176 | + reason, |
| 177 | + }; |
| 178 | + } |
| 179 | +} |
0 commit comments