Skip to content

Commit 5e044ef

Browse files
luizgribeirotoddbaertbeeme1mr
authored
feat(server): add in memory provider (#585)
## This PR Implements the following features for `inMemoryProvider`: - default value for flags - reason for flag evaluation - Context based evaluation ### Related Issues It's part of #565 --------- Signed-off-by: Luiz Ribeiro <[email protected]> Co-authored-by: Todd Baert <[email protected]> Co-authored-by: Michael Beemer <[email protected]>
1 parent 2b4dbec commit 5e044ef

File tree

7 files changed

+788
-4
lines changed

7 files changed

+788
-4
lines changed

packages/server/README.md

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -186,17 +186,28 @@ A name is a logical identifier which can be used to associate clients with a par
186186
If a name has no associated provider, the global provider is used.
187187

188188
```ts
189-
import { OpenFeature } from "@openfeature/js-sdk";
189+
import { OpenFeature, InMemoryProvider } from "@openfeature/js-sdk";
190+
191+
const myFlags = {
192+
'v2_enabled': {
193+
variants: {
194+
on: true,
195+
off: false
196+
},
197+
disabled: false,
198+
defaultVariant: "on"
199+
}
200+
};
190201

191202
// Registering the default provider
192-
OpenFeature.setProvider(NewLocalProvider());
203+
OpenFeature.setProvider(InMemoryProvider(myFlags));
193204
// Registering a named provider
194-
OpenFeature.setProvider("clientForCache", new NewCachedProvider());
205+
OpenFeature.setProvider("otherClient", new InMemoryProvider(someOtherFlags));
195206

196207
// A Client backed by default provider
197208
const clientWithDefault = OpenFeature.getClient();
198209
// A Client backed by NewCachedProvider
199-
const clientForCache = OpenFeature.getClient("clientForCache");
210+
const clientForCache = OpenFeature.getClient("otherClient");
200211
```
201212

202213
### Eventing
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/**
2+
* Don't export types from this file publicly.
3+
* It might cause confusion since these types are not a part of the general API,
4+
* but just for the in-memory provider.
5+
*/
6+
import { EvaluationContext, JsonValue } from '@openfeature/shared';
7+
8+
type Variants<T> = Record<string, T>;
9+
10+
/**
11+
* A Feature Flag definition, containing it's specification
12+
*/
13+
export type Flag = {
14+
/**
15+
* An object containing all possible flags mappings (variant -> flag value)
16+
*/
17+
variants: Variants<boolean> | Variants<string> | Variants<number> | Variants<JsonValue>;
18+
/**
19+
* The variant it will resolve to in STATIC evaluation
20+
*/
21+
defaultVariant: string;
22+
/**
23+
* Determines if flag evaluation is enabled or not for this flag.
24+
* If false, falls back to the default value provided to the client
25+
*/
26+
disabled: boolean;
27+
/**
28+
* Function used in order to evaluate a flag to a specific value given the provided context.
29+
* It should return a variant key.
30+
* If it does not return a valid variant it falls back to the default value provided to the client
31+
* @param EvaluationContext
32+
*/
33+
contextEvaluator?: (ctx: EvaluationContext) => string;
34+
};
35+
36+
export type FlagConfiguration = Record<string, Flag>;
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import {
2+
EvaluationContext,
3+
FlagNotFoundError,
4+
FlagValueType,
5+
GeneralError,
6+
JsonValue,
7+
Logger,
8+
OpenFeatureError,
9+
OpenFeatureEventEmitter,
10+
ProviderEvents,
11+
ResolutionDetails,
12+
StandardResolutionReasons,
13+
TypeMismatchError
14+
} from '@openfeature/shared';
15+
import { Provider } from '../provider';
16+
import { Flag, FlagConfiguration } from './flag-configuration';
17+
import { VariantFoundError } from './variant-not-found-error';
18+
19+
/**
20+
* A simple OpenFeature provider intended for demos and as a test stub.
21+
*/
22+
export class InMemoryProvider implements Provider {
23+
public readonly events = new OpenFeatureEventEmitter();
24+
public readonly runsOn = 'server';
25+
readonly metadata = {
26+
name: 'in-memory',
27+
} as const;
28+
private _flagConfiguration: FlagConfiguration;
29+
30+
constructor(flagConfiguration: FlagConfiguration = {}) {
31+
this._flagConfiguration = { ...flagConfiguration };
32+
}
33+
34+
/**
35+
* Overwrites the configured flags.
36+
* @param { FlagConfiguration } flagConfiguration new flag configuration
37+
*/
38+
putConfiguration(flagConfiguration: FlagConfiguration) {
39+
const flagsChanged = Object.entries(flagConfiguration)
40+
.filter(([key, value]) => this._flagConfiguration[key] !== value)
41+
.map(([key]) => key);
42+
43+
this._flagConfiguration = { ...flagConfiguration };
44+
this.events.emit(ProviderEvents.ConfigurationChanged, { flagsChanged });
45+
}
46+
47+
resolveBooleanEvaluation(
48+
flagKey: string,
49+
defaultValue: boolean,
50+
context?: EvaluationContext,
51+
logger?: Logger,
52+
): Promise<ResolutionDetails<boolean>> {
53+
return this.resolveFlagWithReason<boolean>(flagKey, defaultValue, context, logger);
54+
}
55+
56+
resolveNumberEvaluation(
57+
flagKey: string,
58+
defaultValue: number,
59+
context?: EvaluationContext,
60+
logger?: Logger,
61+
): Promise<ResolutionDetails<number>> {
62+
return this.resolveFlagWithReason<number>(flagKey, defaultValue, context, logger);
63+
}
64+
65+
async resolveStringEvaluation(
66+
flagKey: string,
67+
defaultValue: string,
68+
context?: EvaluationContext,
69+
logger?: Logger,
70+
): Promise<ResolutionDetails<string>> {
71+
return this.resolveFlagWithReason<string>(flagKey, defaultValue, context, logger);
72+
}
73+
74+
async resolveObjectEvaluation<T extends JsonValue>(
75+
flagKey: string,
76+
defaultValue: T,
77+
context?: EvaluationContext,
78+
logger?: Logger,
79+
): Promise<ResolutionDetails<T>> {
80+
return this.resolveFlagWithReason<T>(flagKey, defaultValue, context, logger);
81+
}
82+
83+
private async resolveFlagWithReason<T extends JsonValue | FlagValueType>(
84+
flagKey: string,
85+
defaultValue: T,
86+
ctx?: EvaluationContext,
87+
logger?: Logger,
88+
): Promise<ResolutionDetails<T>> {
89+
try {
90+
const resolutionResult = this.lookupFlagValue(flagKey, defaultValue, ctx, logger);
91+
92+
if (typeof resolutionResult?.value != typeof defaultValue) {
93+
throw new TypeMismatchError();
94+
}
95+
96+
return resolutionResult;
97+
} catch (error: unknown) {
98+
if (!(error instanceof OpenFeatureError)) {
99+
throw new GeneralError((error as Error)?.message || 'unknown error');
100+
}
101+
throw error;
102+
}
103+
}
104+
105+
private lookupFlagValue<T extends JsonValue | FlagValueType>(
106+
flagKey: string,
107+
defaultValue: T,
108+
ctx?: EvaluationContext,
109+
logger?: Logger,
110+
): ResolutionDetails<T> {
111+
if (!(flagKey in this._flagConfiguration)) {
112+
const message = `no flag found with key ${flagKey}`;
113+
logger?.debug(message);
114+
throw new FlagNotFoundError(message);
115+
}
116+
const flagSpec: Flag = this._flagConfiguration[flagKey];
117+
118+
if (flagSpec.disabled) {
119+
return { value: defaultValue, reason: StandardResolutionReasons.DISABLED };
120+
}
121+
122+
const isContextEval = ctx && flagSpec?.contextEvaluator;
123+
const variant = isContextEval ? flagSpec.contextEvaluator?.(ctx) : flagSpec.defaultVariant;
124+
125+
const value = variant && flagSpec?.variants[variant];
126+
127+
if (value === undefined) {
128+
const message = `no value associated with variant ${variant}`;
129+
logger?.error(message);
130+
throw new VariantFoundError(message);
131+
}
132+
133+
return {
134+
value: value as T,
135+
...(variant && { variant }),
136+
reason: isContextEval ? StandardResolutionReasons.TARGETING_MATCH : StandardResolutionReasons.STATIC,
137+
};
138+
}
139+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './in-memory-provider';
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { ErrorCode, OpenFeatureError } from '@openfeature/shared';
2+
3+
/**
4+
* A custom error for the in-memory provider.
5+
* Indicates the resolved or default variant doesn't exist.
6+
*/
7+
export class VariantFoundError extends OpenFeatureError {
8+
code: ErrorCode;
9+
constructor(message?: string) {
10+
super(message);
11+
Object.setPrototypeOf(this, VariantFoundError.prototype);
12+
this.name = 'VariantFoundError';
13+
this.code = ErrorCode.GENERAL;
14+
}
15+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export * from './provider';
22
export * from './no-op-provider';
3+
export * from './in-memory-provider';

0 commit comments

Comments
 (0)