1
1
import {
2
- EvaluationFailureErrorCode ,
3
2
EvaluationRequest ,
4
3
EvaluationResponse ,
5
4
OFREPApi ,
6
5
OFREPApiFetchError ,
7
6
OFREPApiTooManyRequestsError ,
8
7
OFREPApiUnauthorizedError ,
9
8
OFREPForbiddenError ,
10
- handleEvaluationError ,
11
9
isEvaluationFailureResponse ,
12
10
isEvaluationSuccessResponse ,
13
11
} from '@openfeature/ofrep-core' ;
14
12
import {
15
13
ClientProviderEvents ,
14
+ ErrorCode ,
16
15
EvaluationContext ,
17
- FlagMetadata ,
18
- FlagNotFoundError ,
19
16
FlagValue ,
20
- GeneralError ,
21
17
Hook ,
22
- InvalidContextError ,
23
18
JsonValue ,
24
19
Logger ,
25
20
OpenFeatureError ,
26
21
OpenFeatureEventEmitter ,
27
- ParseError ,
28
22
Provider ,
29
23
ProviderFatalError ,
30
24
ResolutionDetails ,
31
25
StandardResolutionReasons ,
32
- TargetingKeyMissingError ,
33
- TypeMismatchError ,
34
26
} from '@openfeature/web-sdk' ;
35
27
import { BulkEvaluationStatus , EvaluateFlagsResponse } from './model/evaluate-flags-response' ;
36
- import { FlagCache } from './model/in-memory-cache' ;
28
+ import { FlagCache , MetadataCache } from './model/in-memory-cache' ;
37
29
import { OFREPWebProviderOptions } from './model/ofrep-web-provider-options' ;
38
30
import { isResolutionError } from './model/resolution-error' ;
39
31
32
+ const ERROR_TO_MESSAGE : { [ key in ErrorCode ] : string } = {
33
+ [ ErrorCode . FLAG_NOT_FOUND ] : 'Flag was not found' ,
34
+ [ ErrorCode . GENERAL ] : 'General error' ,
35
+ [ ErrorCode . INVALID_CONTEXT ] : 'Context is invalid or could be parsed' ,
36
+ [ ErrorCode . PARSE_ERROR ] : 'Flag or flag configuration could not be parsed' ,
37
+ [ ErrorCode . PROVIDER_FATAL ] : 'Provider is in a fatal error state' ,
38
+ [ ErrorCode . PROVIDER_NOT_READY ] : 'Provider is not yet ready' ,
39
+ [ ErrorCode . TARGETING_KEY_MISSING ] : 'Targeting key is missing' ,
40
+ [ ErrorCode . TYPE_MISMATCH ] : 'Flag is not of expect type' ,
41
+ } ;
42
+
40
43
export class OFREPWebProvider implements Provider {
41
44
DEFAULT_POLL_INTERVAL = 30000 ;
42
45
@@ -52,10 +55,11 @@ export class OFREPWebProvider implements Provider {
52
55
// _options is the options used to configure the provider.
53
56
private _options : OFREPWebProviderOptions ;
54
57
private _ofrepAPI : OFREPApi ;
55
- private _etag : string | null ;
58
+ private _etag : string | null | undefined ;
56
59
private _pollingInterval : number ;
57
60
private _retryPollingAfter : Date | undefined ;
58
61
private _flagCache : FlagCache = { } ;
62
+ private _flagSetMetadataCache : MetadataCache = { } ;
59
63
private _context : EvaluationContext | undefined ;
60
64
private _pollingIntervalId ?: number ;
61
65
@@ -81,7 +85,7 @@ export class OFREPWebProvider implements Provider {
81
85
async initialize ( context ?: EvaluationContext | undefined ) : Promise < void > {
82
86
try {
83
87
this . _context = context ;
84
- await this . _evaluateFlags ( context ) ;
88
+ await this . _fetchFlags ( context ) ;
85
89
86
90
if ( this . _pollingInterval > 0 ) {
87
91
this . startPolling ( ) ;
@@ -102,28 +106,28 @@ export class OFREPWebProvider implements Provider {
102
106
defaultValue : boolean ,
103
107
context : EvaluationContext ,
104
108
) : ResolutionDetails < boolean > {
105
- return this . evaluate ( flagKey , 'boolean' ) ;
109
+ return this . evaluate ( flagKey , 'boolean' , defaultValue ) ;
106
110
}
107
111
resolveStringEvaluation (
108
112
flagKey : string ,
109
113
defaultValue : string ,
110
114
context : EvaluationContext ,
111
115
) : ResolutionDetails < string > {
112
- return this . evaluate ( flagKey , 'string' ) ;
116
+ return this . evaluate ( flagKey , 'string' , defaultValue ) ;
113
117
}
114
118
resolveNumberEvaluation (
115
119
flagKey : string ,
116
120
defaultValue : number ,
117
121
context : EvaluationContext ,
118
122
) : ResolutionDetails < number > {
119
- return this . evaluate ( flagKey , 'number' ) ;
123
+ return this . evaluate ( flagKey , 'number' , defaultValue ) ;
120
124
}
121
125
resolveObjectEvaluation < T extends JsonValue > (
122
126
flagKey : string ,
123
127
defaultValue : T ,
124
128
context : EvaluationContext ,
125
129
) : ResolutionDetails < T > {
126
- return this . evaluate ( flagKey , 'object' ) ;
130
+ return this . evaluate ( flagKey , 'object' , defaultValue ) ;
127
131
}
128
132
/* eslint-enable @typescript-eslint/no-unused-vars */
129
133
@@ -143,7 +147,7 @@ export class OFREPWebProvider implements Provider {
143
147
return ;
144
148
}
145
149
146
- await this . _evaluateFlags ( newContext ) ;
150
+ await this . _fetchFlags ( newContext ) ;
147
151
} catch ( error ) {
148
152
if ( error instanceof OFREPApiTooManyRequestsError ) {
149
153
this . events ?. emit ( ClientProviderEvents . Stale , { message : `${ error . name } : ${ error . message } ` } ) ;
@@ -172,7 +176,7 @@ export class OFREPWebProvider implements Provider {
172
176
}
173
177
174
178
/**
175
- * _evaluateFlags is a function that will call the bulk evaluate flags endpoint to get the flags values.
179
+ * _fetchFlags is a function that will call the bulk evaluate flags endpoint to get the flags values.
176
180
* @param context - the context to use for the evaluation
177
181
* @private
178
182
* @returns EvaluationStatus if the evaluation the API returned a 304, 200.
@@ -181,7 +185,7 @@ export class OFREPWebProvider implements Provider {
181
185
* @throws ParseError if the API returned a 400 with the error code ParseError
182
186
* @throws GeneralError if the API returned a 400 with an unknown error code
183
187
*/
184
- private async _evaluateFlags ( context ?: EvaluationContext | undefined ) : Promise < EvaluateFlagsResponse > {
188
+ private async _fetchFlags ( context ?: EvaluationContext | undefined ) : Promise < EvaluateFlagsResponse > {
185
189
try {
186
190
const evalReq : EvaluationRequest = {
187
191
context,
@@ -194,34 +198,42 @@ export class OFREPWebProvider implements Provider {
194
198
}
195
199
196
200
if ( response . httpStatus !== 200 ) {
197
- handleEvaluationError ( response ) ;
201
+ throw new Error ( `Failed OFREP bulk evaluation request, status: ${ response . httpStatus } ` ) ;
198
202
}
199
203
200
204
const bulkSuccessResp = response . value ;
201
205
const newCache : FlagCache = { } ;
202
206
203
- bulkSuccessResp . flags ?. forEach ( ( evalResp : EvaluationResponse ) => {
204
- if ( isEvaluationFailureResponse ( evalResp ) ) {
205
- newCache [ evalResp . key ] = {
206
- errorCode : evalResp . errorCode ,
207
- errorDetails : evalResp . errorDetails ,
208
- reason : StandardResolutionReasons . ERROR ,
209
- } ;
210
- }
207
+ if ( 'metadata' in bulkSuccessResp && typeof bulkSuccessResp . flags === 'object' ) {
208
+ this . _flagSetMetadataCache = bulkSuccessResp . metadata || { } ;
209
+ }
211
210
212
- if ( isEvaluationSuccessResponse ( evalResp ) && evalResp . key ) {
213
- newCache [ evalResp . key ] = {
214
- value : evalResp . value ,
215
- flagMetadata : evalResp . metadata as FlagMetadata ,
216
- reason : evalResp . reason ,
217
- variant : evalResp . variant ,
218
- } ;
219
- }
220
- } ) ;
221
- const listUpdatedFlags = this . _getListUpdatedFlags ( this . _flagCache , newCache ) ;
222
- this . _flagCache = newCache ;
223
- this . _etag = response . httpResponse ?. headers . get ( 'etag' ) ;
224
- return { status : BulkEvaluationStatus . SUCCESS_WITH_CHANGES , flags : listUpdatedFlags } ;
211
+ if ( 'flags' in bulkSuccessResp && typeof bulkSuccessResp . flags === 'object' ) {
212
+ bulkSuccessResp . flags ?. forEach ( ( evalResp : EvaluationResponse ) => {
213
+ if ( isEvaluationFailureResponse ( evalResp ) ) {
214
+ newCache [ evalResp . key ] = {
215
+ errorCode : evalResp . errorCode ,
216
+ errorDetails : evalResp . errorDetails ,
217
+ reason : StandardResolutionReasons . ERROR ,
218
+ } ;
219
+ }
220
+
221
+ if ( isEvaluationSuccessResponse ( evalResp ) && evalResp . key ) {
222
+ newCache [ evalResp . key ] = {
223
+ value : evalResp . value ,
224
+ flagMetadata : evalResp . metadata ,
225
+ reason : evalResp . reason ,
226
+ variant : evalResp . variant ,
227
+ } ;
228
+ }
229
+ } ) ;
230
+ const listUpdatedFlags = this . _getListUpdatedFlags ( this . _flagCache , newCache ) ;
231
+ this . _flagCache = newCache ;
232
+ this . _etag = response . httpResponse ?. headers . get ( 'etag' ) ;
233
+ return { status : BulkEvaluationStatus . SUCCESS_WITH_CHANGES , flags : listUpdatedFlags } ;
234
+ } else {
235
+ throw new Error ( 'No flags in OFREP bulk evaluation response' ) ;
236
+ }
225
237
} catch ( error ) {
226
238
if ( error instanceof OFREPApiTooManyRequestsError && error . retryAfterDate !== null ) {
227
239
this . _retryPollingAfter = error . retryAfterDate ;
@@ -263,34 +275,38 @@ export class OFREPWebProvider implements Provider {
263
275
* Evaluate is a function retrieving the value from a flag in the cache.
264
276
* @param flagKey - name of the flag to retrieve
265
277
* @param type - type of the flag
278
+ * @param defaultValue - default value
266
279
* @private
267
280
*/
268
- private evaluate < T extends FlagValue > ( flagKey : string , type : string ) : ResolutionDetails < T > {
281
+ private evaluate < T extends FlagValue > ( flagKey : string , type : string , defaultValue : T ) : ResolutionDetails < T > {
269
282
const resolved = this . _flagCache [ flagKey ] ;
283
+
270
284
if ( ! resolved ) {
271
- throw new FlagNotFoundError ( `flag key ${ flagKey } not found in cache` ) ;
285
+ return {
286
+ value : defaultValue ,
287
+ flagMetadata : this . _flagSetMetadataCache ,
288
+ reason : 'ERROR' ,
289
+ errorCode : ErrorCode . FLAG_NOT_FOUND ,
290
+ errorMessage : ERROR_TO_MESSAGE [ ErrorCode . FLAG_NOT_FOUND ] ,
291
+ } ;
272
292
}
273
293
274
294
if ( isResolutionError ( resolved ) ) {
275
- switch ( resolved . errorCode ) {
276
- case EvaluationFailureErrorCode . FlagNotFound :
277
- throw new FlagNotFoundError ( `flag key ${ flagKey } not found: ${ resolved . errorDetails } ` ) ;
278
- case EvaluationFailureErrorCode . TargetingKeyMissing :
279
- throw new TargetingKeyMissingError ( `targeting key missing for flag key ${ flagKey } : ${ resolved . errorDetails } ` ) ;
280
- case EvaluationFailureErrorCode . InvalidContext :
281
- throw new InvalidContextError ( `invalid context for flag key ${ flagKey } : ${ resolved . errorDetails } ` ) ;
282
- case EvaluationFailureErrorCode . ParseError :
283
- throw new ParseError ( `parse error for flag key ${ flagKey } : ${ resolved . errorDetails } ` ) ;
284
- case EvaluationFailureErrorCode . General :
285
- default :
286
- throw new GeneralError (
287
- `general error during flag evaluation for flag key ${ flagKey } : ${ resolved . errorDetails } ` ,
288
- ) ;
289
- }
295
+ return {
296
+ ...resolved ,
297
+ value : defaultValue ,
298
+ errorMessage : ERROR_TO_MESSAGE [ resolved . errorCode ] ,
299
+ } ;
290
300
}
291
301
292
302
if ( typeof resolved . value !== type ) {
293
- throw new TypeMismatchError ( `flag key ${ flagKey } is not of type ${ type } ` ) ;
303
+ return {
304
+ value : defaultValue ,
305
+ flagMetadata : this . _flagSetMetadataCache ,
306
+ reason : 'ERROR' ,
307
+ errorCode : ErrorCode . TYPE_MISMATCH ,
308
+ errorMessage : ERROR_TO_MESSAGE [ ErrorCode . TYPE_MISMATCH ] ,
309
+ } ;
294
310
}
295
311
296
312
return {
@@ -314,7 +330,7 @@ export class OFREPWebProvider implements Provider {
314
330
if ( this . _retryPollingAfter !== undefined && this . _retryPollingAfter > now ) {
315
331
return ;
316
332
}
317
- const res = await this . _evaluateFlags ( this . _context ) ;
333
+ const res = await this . _fetchFlags ( this . _context ) ;
318
334
if ( res . status === BulkEvaluationStatus . SUCCESS_WITH_CHANGES ) {
319
335
this . events ?. emit ( ClientProviderEvents . ConfigurationChanged , {
320
336
message : 'Flags updated' ,
0 commit comments