@openfeature/ofrep-web-provider 0.3.1 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.cjs.js +110 -64
- package/index.esm.d.ts +1 -0
- package/index.esm.js +112 -64
- package/package.json +11 -8
- package/src/lib/model/in-memory-cache.d.ts +6 -2
- package/src/lib/model/resolution-error.d.ts +2 -3
- package/src/lib/ofrep-web-provider.d.ts +6 -4
package/index.cjs.js
CHANGED
|
@@ -1,12 +1,40 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
|
-
|
|
5
|
-
var tslib = require('tslib');
|
|
6
|
-
var core = require('@openfeature/core');
|
|
7
3
|
var ofrepCore = require('@openfeature/ofrep-core');
|
|
8
4
|
var webSdk = require('@openfeature/web-sdk');
|
|
9
5
|
|
|
6
|
+
/******************************************************************************
|
|
7
|
+
Copyright (c) Microsoft Corporation.
|
|
8
|
+
|
|
9
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
10
|
+
purpose with or without fee is hereby granted.
|
|
11
|
+
|
|
12
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
13
|
+
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
14
|
+
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
15
|
+
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
16
|
+
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
17
|
+
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
18
|
+
PERFORMANCE OF THIS SOFTWARE.
|
|
19
|
+
***************************************************************************** */
|
|
20
|
+
/* global Reflect, Promise, SuppressedError, Symbol, Iterator */
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
function __awaiter(thisArg, _arguments, P, generator) {
|
|
24
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
25
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
26
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
27
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
28
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
29
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
|
|
34
|
+
var e = new Error(message);
|
|
35
|
+
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
36
|
+
};
|
|
37
|
+
|
|
10
38
|
var BulkEvaluationStatus;
|
|
11
39
|
(function (BulkEvaluationStatus) {
|
|
12
40
|
BulkEvaluationStatus["SUCCESS_NO_CHANGES"] = "SUCCESS_NO_CHANGES";
|
|
@@ -20,6 +48,16 @@ function isResolutionError(response) {
|
|
|
20
48
|
return 'reason' in response && 'errorCode' in response && !('value' in response);
|
|
21
49
|
}
|
|
22
50
|
|
|
51
|
+
const ErrorMessageMap = {
|
|
52
|
+
[webSdk.ErrorCode.FLAG_NOT_FOUND]: 'Flag was not found',
|
|
53
|
+
[webSdk.ErrorCode.GENERAL]: 'General error',
|
|
54
|
+
[webSdk.ErrorCode.INVALID_CONTEXT]: 'Context is invalid or could be parsed',
|
|
55
|
+
[webSdk.ErrorCode.PARSE_ERROR]: 'Flag or flag configuration could not be parsed',
|
|
56
|
+
[webSdk.ErrorCode.PROVIDER_FATAL]: 'Provider is in a fatal error state',
|
|
57
|
+
[webSdk.ErrorCode.PROVIDER_NOT_READY]: 'Provider is not yet ready',
|
|
58
|
+
[webSdk.ErrorCode.TARGETING_KEY_MISSING]: 'Targeting key is missing',
|
|
59
|
+
[webSdk.ErrorCode.TYPE_MISMATCH]: 'Flag is not of expected type',
|
|
60
|
+
};
|
|
23
61
|
class OFREPWebProvider {
|
|
24
62
|
constructor(options, logger) {
|
|
25
63
|
var _a;
|
|
@@ -30,11 +68,12 @@ class OFREPWebProvider {
|
|
|
30
68
|
this.runsOn = 'client';
|
|
31
69
|
this.events = new webSdk.OpenFeatureEventEmitter();
|
|
32
70
|
this._flagCache = {};
|
|
71
|
+
this._flagSetMetadataCache = {};
|
|
33
72
|
this._options = options;
|
|
34
73
|
this._logger = logger;
|
|
35
74
|
this._etag = null;
|
|
36
75
|
this._ofrepAPI = new ofrepCore.OFREPApi(this._options, this._options.fetchImplementation);
|
|
37
|
-
this._pollingInterval = (_a = this._options.pollInterval) !== null && _a !==
|
|
76
|
+
this._pollingInterval = (_a = this._options.pollInterval) !== null && _a !== undefined ? _a : this.DEFAULT_POLL_INTERVAL;
|
|
38
77
|
}
|
|
39
78
|
/**
|
|
40
79
|
* Returns a shallow copy of the flag cache, which is updated at initialization/context-change/configuration-change once the flags are re-evaluated.
|
|
@@ -47,11 +86,11 @@ class OFREPWebProvider {
|
|
|
47
86
|
* @param context - the context to use for the evaluation
|
|
48
87
|
*/
|
|
49
88
|
initialize(context) {
|
|
50
|
-
return
|
|
89
|
+
return __awaiter(this, undefined, undefined, function* () {
|
|
51
90
|
var _a;
|
|
52
91
|
try {
|
|
53
92
|
this._context = context;
|
|
54
|
-
yield this.
|
|
93
|
+
yield this._fetchFlags(context);
|
|
55
94
|
if (this._pollingInterval > 0) {
|
|
56
95
|
this.startPolling();
|
|
57
96
|
}
|
|
@@ -59,7 +98,7 @@ class OFREPWebProvider {
|
|
|
59
98
|
}
|
|
60
99
|
catch (error) {
|
|
61
100
|
if (error instanceof ofrepCore.OFREPApiUnauthorizedError || error instanceof ofrepCore.OFREPForbiddenError) {
|
|
62
|
-
throw new webSdk.ProviderFatalError('Initialization failed', error);
|
|
101
|
+
throw new webSdk.ProviderFatalError('Initialization failed', { cause: error });
|
|
63
102
|
}
|
|
64
103
|
throw error;
|
|
65
104
|
}
|
|
@@ -68,18 +107,17 @@ class OFREPWebProvider {
|
|
|
68
107
|
/* eslint-disable @typescript-eslint/no-unused-vars*/
|
|
69
108
|
/* to make overrides easier we keep these unused vars */
|
|
70
109
|
resolveBooleanEvaluation(flagKey, defaultValue, context) {
|
|
71
|
-
return this.
|
|
110
|
+
return this._resolve(flagKey, 'boolean', defaultValue);
|
|
72
111
|
}
|
|
73
112
|
resolveStringEvaluation(flagKey, defaultValue, context) {
|
|
74
|
-
return this.
|
|
113
|
+
return this._resolve(flagKey, 'string', defaultValue);
|
|
75
114
|
}
|
|
76
115
|
resolveNumberEvaluation(flagKey, defaultValue, context) {
|
|
77
|
-
return this.
|
|
116
|
+
return this._resolve(flagKey, 'number', defaultValue);
|
|
78
117
|
}
|
|
79
118
|
resolveObjectEvaluation(flagKey, defaultValue, context) {
|
|
80
|
-
return this.
|
|
119
|
+
return this._resolve(flagKey, 'object', defaultValue);
|
|
81
120
|
}
|
|
82
|
-
/* eslint-enable @typescript-eslint/no-unused-vars */
|
|
83
121
|
/**
|
|
84
122
|
* onContextChange is called when the context changes, it will re-evaluate the flags with the new context
|
|
85
123
|
* and update the cache.
|
|
@@ -87,7 +125,7 @@ class OFREPWebProvider {
|
|
|
87
125
|
* @param newContext - the new evaluation context
|
|
88
126
|
*/
|
|
89
127
|
onContextChange(oldContext, newContext) {
|
|
90
|
-
return
|
|
128
|
+
return __awaiter(this, undefined, undefined, function* () {
|
|
91
129
|
var _a, _b, _c;
|
|
92
130
|
try {
|
|
93
131
|
this._context = newContext;
|
|
@@ -96,21 +134,21 @@ class OFREPWebProvider {
|
|
|
96
134
|
// we do nothing because we should not call the endpoint
|
|
97
135
|
return;
|
|
98
136
|
}
|
|
99
|
-
yield this.
|
|
137
|
+
yield this._fetchFlags(newContext);
|
|
100
138
|
}
|
|
101
139
|
catch (error) {
|
|
102
140
|
if (error instanceof ofrepCore.OFREPApiTooManyRequestsError) {
|
|
103
|
-
(_a = this.events) === null || _a ===
|
|
141
|
+
(_a = this.events) === null || _a === undefined ? undefined : _a.emit(webSdk.ClientProviderEvents.Stale, { message: `${error.name}: ${error.message}` });
|
|
104
142
|
return;
|
|
105
143
|
}
|
|
106
144
|
if (error instanceof webSdk.OpenFeatureError ||
|
|
107
145
|
error instanceof ofrepCore.OFREPApiFetchError ||
|
|
108
146
|
error instanceof ofrepCore.OFREPApiUnauthorizedError ||
|
|
109
147
|
error instanceof ofrepCore.OFREPForbiddenError) {
|
|
110
|
-
(_b = this.events) === null || _b ===
|
|
148
|
+
(_b = this.events) === null || _b === undefined ? undefined : _b.emit(webSdk.ClientProviderEvents.Error, { message: `${error.name}: ${error.message}` });
|
|
111
149
|
return;
|
|
112
150
|
}
|
|
113
|
-
(_c = this.events) === null || _c ===
|
|
151
|
+
(_c = this.events) === null || _c === undefined ? undefined : _c.emit(webSdk.ClientProviderEvents.Error, { message: `Unknown error: ${error}` });
|
|
114
152
|
}
|
|
115
153
|
});
|
|
116
154
|
}
|
|
@@ -122,7 +160,7 @@ class OFREPWebProvider {
|
|
|
122
160
|
return Promise.resolve();
|
|
123
161
|
}
|
|
124
162
|
/**
|
|
125
|
-
*
|
|
163
|
+
* _fetchFlags is a function that will call the bulk evaluate flags endpoint to get the flags values.
|
|
126
164
|
* @param context - the context to use for the evaluation
|
|
127
165
|
* @private
|
|
128
166
|
* @returns EvaluationStatus if the evaluation the API returned a 304, 200.
|
|
@@ -131,9 +169,9 @@ class OFREPWebProvider {
|
|
|
131
169
|
* @throws ParseError if the API returned a 400 with the error code ParseError
|
|
132
170
|
* @throws GeneralError if the API returned a 400 with an unknown error code
|
|
133
171
|
*/
|
|
134
|
-
|
|
135
|
-
return
|
|
136
|
-
var _a
|
|
172
|
+
_fetchFlags(context) {
|
|
173
|
+
return __awaiter(this, undefined, undefined, function* () {
|
|
174
|
+
var _a;
|
|
137
175
|
try {
|
|
138
176
|
const evalReq = {
|
|
139
177
|
context,
|
|
@@ -144,31 +182,38 @@ class OFREPWebProvider {
|
|
|
144
182
|
return { status: BulkEvaluationStatus.SUCCESS_NO_CHANGES, flags: [] };
|
|
145
183
|
}
|
|
146
184
|
if (response.httpStatus !== 200) {
|
|
147
|
-
|
|
185
|
+
throw new webSdk.GeneralError(`Failed OFREP bulk evaluation request, status: ${response.httpStatus}`);
|
|
148
186
|
}
|
|
149
187
|
const bulkSuccessResp = response.value;
|
|
150
188
|
const newCache = {};
|
|
151
|
-
(
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
189
|
+
if ('flags' in bulkSuccessResp && Array.isArray(bulkSuccessResp.flags)) {
|
|
190
|
+
bulkSuccessResp.flags.forEach((evalResp) => {
|
|
191
|
+
if (ofrepCore.isEvaluationFailureResponse(evalResp)) {
|
|
192
|
+
newCache[evalResp.key] = {
|
|
193
|
+
reason: webSdk.StandardResolutionReasons.ERROR,
|
|
194
|
+
flagMetadata: evalResp.metadata,
|
|
195
|
+
errorCode: evalResp.errorCode,
|
|
196
|
+
errorDetails: evalResp.errorDetails,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
if (ofrepCore.isEvaluationSuccessResponse(evalResp) && evalResp.key) {
|
|
200
|
+
newCache[evalResp.key] = {
|
|
201
|
+
value: evalResp.value,
|
|
202
|
+
variant: evalResp.variant,
|
|
203
|
+
reason: evalResp.reason,
|
|
204
|
+
flagMetadata: evalResp.metadata,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
const listUpdatedFlags = this._getListUpdatedFlags(this._flagCache, newCache);
|
|
209
|
+
this._flagCache = newCache;
|
|
210
|
+
this._etag = (_a = response.httpResponse) === null || _a === void 0 ? void 0 : _a.headers.get('etag');
|
|
211
|
+
this._flagSetMetadataCache = typeof bulkSuccessResp.metadata === 'object' ? bulkSuccessResp.metadata : {};
|
|
212
|
+
return { status: BulkEvaluationStatus.SUCCESS_WITH_CHANGES, flags: listUpdatedFlags };
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
throw new Error('No flags in OFREP bulk evaluation response');
|
|
216
|
+
}
|
|
172
217
|
}
|
|
173
218
|
catch (error) {
|
|
174
219
|
if (error instanceof ofrepCore.OFREPApiTooManyRequestsError && error.retryAfterDate !== null) {
|
|
@@ -204,33 +249,34 @@ class OFREPWebProvider {
|
|
|
204
249
|
return changedKeys;
|
|
205
250
|
}
|
|
206
251
|
/**
|
|
207
|
-
*
|
|
252
|
+
* _resolve is a function retrieving the value from a flag in the cache.
|
|
208
253
|
* @param flagKey - name of the flag to retrieve
|
|
209
254
|
* @param type - type of the flag
|
|
255
|
+
* @param defaultValue - default value
|
|
210
256
|
* @private
|
|
211
257
|
*/
|
|
212
|
-
|
|
258
|
+
_resolve(flagKey, type, defaultValue) {
|
|
213
259
|
const resolved = this._flagCache[flagKey];
|
|
214
260
|
if (!resolved) {
|
|
215
|
-
|
|
261
|
+
return {
|
|
262
|
+
value: defaultValue,
|
|
263
|
+
flagMetadata: this._flagSetMetadataCache,
|
|
264
|
+
reason: webSdk.StandardResolutionReasons.ERROR,
|
|
265
|
+
errorCode: webSdk.ErrorCode.FLAG_NOT_FOUND,
|
|
266
|
+
errorMessage: ErrorMessageMap[webSdk.ErrorCode.FLAG_NOT_FOUND],
|
|
267
|
+
};
|
|
216
268
|
}
|
|
217
269
|
if (isResolutionError(resolved)) {
|
|
218
|
-
|
|
219
|
-
case ofrepCore.EvaluationFailureErrorCode.FlagNotFound:
|
|
220
|
-
throw new webSdk.FlagNotFoundError(`flag key ${flagKey} not found: ${resolved.errorDetails}`);
|
|
221
|
-
case ofrepCore.EvaluationFailureErrorCode.TargetingKeyMissing:
|
|
222
|
-
throw new core.TargetingKeyMissingError(`targeting key missing for flag key ${flagKey}: ${resolved.errorDetails}`);
|
|
223
|
-
case ofrepCore.EvaluationFailureErrorCode.InvalidContext:
|
|
224
|
-
throw new core.InvalidContextError(`invalid context for flag key ${flagKey}: ${resolved.errorDetails}`);
|
|
225
|
-
case ofrepCore.EvaluationFailureErrorCode.ParseError:
|
|
226
|
-
throw new core.ParseError(`parse error for flag key ${flagKey}: ${resolved.errorDetails}`);
|
|
227
|
-
case ofrepCore.EvaluationFailureErrorCode.General:
|
|
228
|
-
default:
|
|
229
|
-
throw new webSdk.GeneralError(`general error during flag evaluation for flag key ${flagKey}: ${resolved.errorDetails}`);
|
|
230
|
-
}
|
|
270
|
+
return Object.assign(Object.assign({}, resolved), { value: defaultValue, errorMessage: ErrorMessageMap[resolved.errorCode] });
|
|
231
271
|
}
|
|
232
272
|
if (typeof resolved.value !== type) {
|
|
233
|
-
|
|
273
|
+
return {
|
|
274
|
+
value: defaultValue,
|
|
275
|
+
flagMetadata: resolved.flagMetadata,
|
|
276
|
+
reason: webSdk.StandardResolutionReasons.ERROR,
|
|
277
|
+
errorCode: webSdk.ErrorCode.TYPE_MISMATCH,
|
|
278
|
+
errorMessage: ErrorMessageMap[webSdk.ErrorCode.TYPE_MISMATCH],
|
|
279
|
+
};
|
|
234
280
|
}
|
|
235
281
|
return {
|
|
236
282
|
variant: resolved.variant,
|
|
@@ -246,14 +292,14 @@ class OFREPWebProvider {
|
|
|
246
292
|
* @private
|
|
247
293
|
*/
|
|
248
294
|
startPolling() {
|
|
249
|
-
this._pollingIntervalId = setInterval(() =>
|
|
295
|
+
this._pollingIntervalId = setInterval(() => __awaiter(this, undefined, undefined, function* () {
|
|
250
296
|
var _a, _b;
|
|
251
297
|
try {
|
|
252
298
|
const now = new Date();
|
|
253
299
|
if (this._retryPollingAfter !== undefined && this._retryPollingAfter > now) {
|
|
254
300
|
return;
|
|
255
301
|
}
|
|
256
|
-
const res = yield this.
|
|
302
|
+
const res = yield this._fetchFlags(this._context);
|
|
257
303
|
if (res.status === BulkEvaluationStatus.SUCCESS_WITH_CHANGES) {
|
|
258
304
|
(_a = this.events) === null || _a === void 0 ? void 0 : _a.emit(webSdk.ClientProviderEvents.ConfigurationChanged, {
|
|
259
305
|
message: 'Flags updated',
|
|
@@ -262,7 +308,7 @@ class OFREPWebProvider {
|
|
|
262
308
|
}
|
|
263
309
|
}
|
|
264
310
|
catch (error) {
|
|
265
|
-
(_b = this.events) === null || _b ===
|
|
311
|
+
(_b = this.events) === null || _b === undefined ? undefined : _b.emit(webSdk.ClientProviderEvents.Stale, { message: `Error while polling: ${error}` });
|
|
266
312
|
}
|
|
267
313
|
}), this._pollingInterval);
|
|
268
314
|
}
|
package/index.esm.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./src/index";
|
package/index.esm.js
CHANGED
|
@@ -1,7 +1,37 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { StandardResolutionReasons,
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import { OFREPApi, OFREPApiUnauthorizedError, OFREPForbiddenError, OFREPApiTooManyRequestsError, OFREPApiFetchError, isEvaluationFailureResponse, isEvaluationSuccessResponse } from '@openfeature/ofrep-core';
|
|
2
|
+
import { ErrorCode, OpenFeatureEventEmitter, StandardResolutionReasons, ProviderFatalError, ClientProviderEvents, OpenFeatureError, GeneralError } from '@openfeature/web-sdk';
|
|
3
|
+
|
|
4
|
+
/******************************************************************************
|
|
5
|
+
Copyright (c) Microsoft Corporation.
|
|
6
|
+
|
|
7
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
8
|
+
purpose with or without fee is hereby granted.
|
|
9
|
+
|
|
10
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
11
|
+
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
12
|
+
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
13
|
+
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
14
|
+
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
15
|
+
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
16
|
+
PERFORMANCE OF THIS SOFTWARE.
|
|
17
|
+
***************************************************************************** */
|
|
18
|
+
/* global Reflect, Promise, SuppressedError, Symbol, Iterator */
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
function __awaiter(thisArg, _arguments, P, generator) {
|
|
22
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
23
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
24
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
25
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
26
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
27
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
|
|
32
|
+
var e = new Error(message);
|
|
33
|
+
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
34
|
+
};
|
|
5
35
|
|
|
6
36
|
var BulkEvaluationStatus;
|
|
7
37
|
(function (BulkEvaluationStatus) {
|
|
@@ -16,6 +46,16 @@ function isResolutionError(response) {
|
|
|
16
46
|
return 'reason' in response && 'errorCode' in response && !('value' in response);
|
|
17
47
|
}
|
|
18
48
|
|
|
49
|
+
const ErrorMessageMap = {
|
|
50
|
+
[ErrorCode.FLAG_NOT_FOUND]: 'Flag was not found',
|
|
51
|
+
[ErrorCode.GENERAL]: 'General error',
|
|
52
|
+
[ErrorCode.INVALID_CONTEXT]: 'Context is invalid or could be parsed',
|
|
53
|
+
[ErrorCode.PARSE_ERROR]: 'Flag or flag configuration could not be parsed',
|
|
54
|
+
[ErrorCode.PROVIDER_FATAL]: 'Provider is in a fatal error state',
|
|
55
|
+
[ErrorCode.PROVIDER_NOT_READY]: 'Provider is not yet ready',
|
|
56
|
+
[ErrorCode.TARGETING_KEY_MISSING]: 'Targeting key is missing',
|
|
57
|
+
[ErrorCode.TYPE_MISMATCH]: 'Flag is not of expected type',
|
|
58
|
+
};
|
|
19
59
|
class OFREPWebProvider {
|
|
20
60
|
constructor(options, logger) {
|
|
21
61
|
var _a;
|
|
@@ -26,11 +66,12 @@ class OFREPWebProvider {
|
|
|
26
66
|
this.runsOn = 'client';
|
|
27
67
|
this.events = new OpenFeatureEventEmitter();
|
|
28
68
|
this._flagCache = {};
|
|
69
|
+
this._flagSetMetadataCache = {};
|
|
29
70
|
this._options = options;
|
|
30
71
|
this._logger = logger;
|
|
31
72
|
this._etag = null;
|
|
32
73
|
this._ofrepAPI = new OFREPApi(this._options, this._options.fetchImplementation);
|
|
33
|
-
this._pollingInterval = (_a = this._options.pollInterval) !== null && _a !==
|
|
74
|
+
this._pollingInterval = (_a = this._options.pollInterval) !== null && _a !== undefined ? _a : this.DEFAULT_POLL_INTERVAL;
|
|
34
75
|
}
|
|
35
76
|
/**
|
|
36
77
|
* Returns a shallow copy of the flag cache, which is updated at initialization/context-change/configuration-change once the flags are re-evaluated.
|
|
@@ -43,11 +84,11 @@ class OFREPWebProvider {
|
|
|
43
84
|
* @param context - the context to use for the evaluation
|
|
44
85
|
*/
|
|
45
86
|
initialize(context) {
|
|
46
|
-
return __awaiter(this,
|
|
87
|
+
return __awaiter(this, undefined, undefined, function* () {
|
|
47
88
|
var _a;
|
|
48
89
|
try {
|
|
49
90
|
this._context = context;
|
|
50
|
-
yield this.
|
|
91
|
+
yield this._fetchFlags(context);
|
|
51
92
|
if (this._pollingInterval > 0) {
|
|
52
93
|
this.startPolling();
|
|
53
94
|
}
|
|
@@ -55,7 +96,7 @@ class OFREPWebProvider {
|
|
|
55
96
|
}
|
|
56
97
|
catch (error) {
|
|
57
98
|
if (error instanceof OFREPApiUnauthorizedError || error instanceof OFREPForbiddenError) {
|
|
58
|
-
throw new ProviderFatalError('Initialization failed', error);
|
|
99
|
+
throw new ProviderFatalError('Initialization failed', { cause: error });
|
|
59
100
|
}
|
|
60
101
|
throw error;
|
|
61
102
|
}
|
|
@@ -64,18 +105,17 @@ class OFREPWebProvider {
|
|
|
64
105
|
/* eslint-disable @typescript-eslint/no-unused-vars*/
|
|
65
106
|
/* to make overrides easier we keep these unused vars */
|
|
66
107
|
resolveBooleanEvaluation(flagKey, defaultValue, context) {
|
|
67
|
-
return this.
|
|
108
|
+
return this._resolve(flagKey, 'boolean', defaultValue);
|
|
68
109
|
}
|
|
69
110
|
resolveStringEvaluation(flagKey, defaultValue, context) {
|
|
70
|
-
return this.
|
|
111
|
+
return this._resolve(flagKey, 'string', defaultValue);
|
|
71
112
|
}
|
|
72
113
|
resolveNumberEvaluation(flagKey, defaultValue, context) {
|
|
73
|
-
return this.
|
|
114
|
+
return this._resolve(flagKey, 'number', defaultValue);
|
|
74
115
|
}
|
|
75
116
|
resolveObjectEvaluation(flagKey, defaultValue, context) {
|
|
76
|
-
return this.
|
|
117
|
+
return this._resolve(flagKey, 'object', defaultValue);
|
|
77
118
|
}
|
|
78
|
-
/* eslint-enable @typescript-eslint/no-unused-vars */
|
|
79
119
|
/**
|
|
80
120
|
* onContextChange is called when the context changes, it will re-evaluate the flags with the new context
|
|
81
121
|
* and update the cache.
|
|
@@ -83,7 +123,7 @@ class OFREPWebProvider {
|
|
|
83
123
|
* @param newContext - the new evaluation context
|
|
84
124
|
*/
|
|
85
125
|
onContextChange(oldContext, newContext) {
|
|
86
|
-
return __awaiter(this,
|
|
126
|
+
return __awaiter(this, undefined, undefined, function* () {
|
|
87
127
|
var _a, _b, _c;
|
|
88
128
|
try {
|
|
89
129
|
this._context = newContext;
|
|
@@ -92,21 +132,21 @@ class OFREPWebProvider {
|
|
|
92
132
|
// we do nothing because we should not call the endpoint
|
|
93
133
|
return;
|
|
94
134
|
}
|
|
95
|
-
yield this.
|
|
135
|
+
yield this._fetchFlags(newContext);
|
|
96
136
|
}
|
|
97
137
|
catch (error) {
|
|
98
138
|
if (error instanceof OFREPApiTooManyRequestsError) {
|
|
99
|
-
(_a = this.events) === null || _a ===
|
|
139
|
+
(_a = this.events) === null || _a === undefined ? undefined : _a.emit(ClientProviderEvents.Stale, { message: `${error.name}: ${error.message}` });
|
|
100
140
|
return;
|
|
101
141
|
}
|
|
102
142
|
if (error instanceof OpenFeatureError ||
|
|
103
143
|
error instanceof OFREPApiFetchError ||
|
|
104
144
|
error instanceof OFREPApiUnauthorizedError ||
|
|
105
145
|
error instanceof OFREPForbiddenError) {
|
|
106
|
-
(_b = this.events) === null || _b ===
|
|
146
|
+
(_b = this.events) === null || _b === undefined ? undefined : _b.emit(ClientProviderEvents.Error, { message: `${error.name}: ${error.message}` });
|
|
107
147
|
return;
|
|
108
148
|
}
|
|
109
|
-
(_c = this.events) === null || _c ===
|
|
149
|
+
(_c = this.events) === null || _c === undefined ? undefined : _c.emit(ClientProviderEvents.Error, { message: `Unknown error: ${error}` });
|
|
110
150
|
}
|
|
111
151
|
});
|
|
112
152
|
}
|
|
@@ -118,7 +158,7 @@ class OFREPWebProvider {
|
|
|
118
158
|
return Promise.resolve();
|
|
119
159
|
}
|
|
120
160
|
/**
|
|
121
|
-
*
|
|
161
|
+
* _fetchFlags is a function that will call the bulk evaluate flags endpoint to get the flags values.
|
|
122
162
|
* @param context - the context to use for the evaluation
|
|
123
163
|
* @private
|
|
124
164
|
* @returns EvaluationStatus if the evaluation the API returned a 304, 200.
|
|
@@ -127,9 +167,9 @@ class OFREPWebProvider {
|
|
|
127
167
|
* @throws ParseError if the API returned a 400 with the error code ParseError
|
|
128
168
|
* @throws GeneralError if the API returned a 400 with an unknown error code
|
|
129
169
|
*/
|
|
130
|
-
|
|
131
|
-
return __awaiter(this,
|
|
132
|
-
var _a
|
|
170
|
+
_fetchFlags(context) {
|
|
171
|
+
return __awaiter(this, undefined, undefined, function* () {
|
|
172
|
+
var _a;
|
|
133
173
|
try {
|
|
134
174
|
const evalReq = {
|
|
135
175
|
context,
|
|
@@ -140,31 +180,38 @@ class OFREPWebProvider {
|
|
|
140
180
|
return { status: BulkEvaluationStatus.SUCCESS_NO_CHANGES, flags: [] };
|
|
141
181
|
}
|
|
142
182
|
if (response.httpStatus !== 200) {
|
|
143
|
-
|
|
183
|
+
throw new GeneralError(`Failed OFREP bulk evaluation request, status: ${response.httpStatus}`);
|
|
144
184
|
}
|
|
145
185
|
const bulkSuccessResp = response.value;
|
|
146
186
|
const newCache = {};
|
|
147
|
-
(
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
187
|
+
if ('flags' in bulkSuccessResp && Array.isArray(bulkSuccessResp.flags)) {
|
|
188
|
+
bulkSuccessResp.flags.forEach((evalResp) => {
|
|
189
|
+
if (isEvaluationFailureResponse(evalResp)) {
|
|
190
|
+
newCache[evalResp.key] = {
|
|
191
|
+
reason: StandardResolutionReasons.ERROR,
|
|
192
|
+
flagMetadata: evalResp.metadata,
|
|
193
|
+
errorCode: evalResp.errorCode,
|
|
194
|
+
errorDetails: evalResp.errorDetails,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
if (isEvaluationSuccessResponse(evalResp) && evalResp.key) {
|
|
198
|
+
newCache[evalResp.key] = {
|
|
199
|
+
value: evalResp.value,
|
|
200
|
+
variant: evalResp.variant,
|
|
201
|
+
reason: evalResp.reason,
|
|
202
|
+
flagMetadata: evalResp.metadata,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
const listUpdatedFlags = this._getListUpdatedFlags(this._flagCache, newCache);
|
|
207
|
+
this._flagCache = newCache;
|
|
208
|
+
this._etag = (_a = response.httpResponse) === null || _a === void 0 ? void 0 : _a.headers.get('etag');
|
|
209
|
+
this._flagSetMetadataCache = typeof bulkSuccessResp.metadata === 'object' ? bulkSuccessResp.metadata : {};
|
|
210
|
+
return { status: BulkEvaluationStatus.SUCCESS_WITH_CHANGES, flags: listUpdatedFlags };
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
throw new Error('No flags in OFREP bulk evaluation response');
|
|
214
|
+
}
|
|
168
215
|
}
|
|
169
216
|
catch (error) {
|
|
170
217
|
if (error instanceof OFREPApiTooManyRequestsError && error.retryAfterDate !== null) {
|
|
@@ -200,33 +247,34 @@ class OFREPWebProvider {
|
|
|
200
247
|
return changedKeys;
|
|
201
248
|
}
|
|
202
249
|
/**
|
|
203
|
-
*
|
|
250
|
+
* _resolve is a function retrieving the value from a flag in the cache.
|
|
204
251
|
* @param flagKey - name of the flag to retrieve
|
|
205
252
|
* @param type - type of the flag
|
|
253
|
+
* @param defaultValue - default value
|
|
206
254
|
* @private
|
|
207
255
|
*/
|
|
208
|
-
|
|
256
|
+
_resolve(flagKey, type, defaultValue) {
|
|
209
257
|
const resolved = this._flagCache[flagKey];
|
|
210
258
|
if (!resolved) {
|
|
211
|
-
|
|
259
|
+
return {
|
|
260
|
+
value: defaultValue,
|
|
261
|
+
flagMetadata: this._flagSetMetadataCache,
|
|
262
|
+
reason: StandardResolutionReasons.ERROR,
|
|
263
|
+
errorCode: ErrorCode.FLAG_NOT_FOUND,
|
|
264
|
+
errorMessage: ErrorMessageMap[ErrorCode.FLAG_NOT_FOUND],
|
|
265
|
+
};
|
|
212
266
|
}
|
|
213
267
|
if (isResolutionError(resolved)) {
|
|
214
|
-
|
|
215
|
-
case EvaluationFailureErrorCode.FlagNotFound:
|
|
216
|
-
throw new FlagNotFoundError(`flag key ${flagKey} not found: ${resolved.errorDetails}`);
|
|
217
|
-
case EvaluationFailureErrorCode.TargetingKeyMissing:
|
|
218
|
-
throw new TargetingKeyMissingError(`targeting key missing for flag key ${flagKey}: ${resolved.errorDetails}`);
|
|
219
|
-
case EvaluationFailureErrorCode.InvalidContext:
|
|
220
|
-
throw new InvalidContextError(`invalid context for flag key ${flagKey}: ${resolved.errorDetails}`);
|
|
221
|
-
case EvaluationFailureErrorCode.ParseError:
|
|
222
|
-
throw new ParseError(`parse error for flag key ${flagKey}: ${resolved.errorDetails}`);
|
|
223
|
-
case EvaluationFailureErrorCode.General:
|
|
224
|
-
default:
|
|
225
|
-
throw new GeneralError(`general error during flag evaluation for flag key ${flagKey}: ${resolved.errorDetails}`);
|
|
226
|
-
}
|
|
268
|
+
return Object.assign(Object.assign({}, resolved), { value: defaultValue, errorMessage: ErrorMessageMap[resolved.errorCode] });
|
|
227
269
|
}
|
|
228
270
|
if (typeof resolved.value !== type) {
|
|
229
|
-
|
|
271
|
+
return {
|
|
272
|
+
value: defaultValue,
|
|
273
|
+
flagMetadata: resolved.flagMetadata,
|
|
274
|
+
reason: StandardResolutionReasons.ERROR,
|
|
275
|
+
errorCode: ErrorCode.TYPE_MISMATCH,
|
|
276
|
+
errorMessage: ErrorMessageMap[ErrorCode.TYPE_MISMATCH],
|
|
277
|
+
};
|
|
230
278
|
}
|
|
231
279
|
return {
|
|
232
280
|
variant: resolved.variant,
|
|
@@ -242,14 +290,14 @@ class OFREPWebProvider {
|
|
|
242
290
|
* @private
|
|
243
291
|
*/
|
|
244
292
|
startPolling() {
|
|
245
|
-
this._pollingIntervalId = setInterval(() => __awaiter(this,
|
|
293
|
+
this._pollingIntervalId = setInterval(() => __awaiter(this, undefined, undefined, function* () {
|
|
246
294
|
var _a, _b;
|
|
247
295
|
try {
|
|
248
296
|
const now = new Date();
|
|
249
297
|
if (this._retryPollingAfter !== undefined && this._retryPollingAfter > now) {
|
|
250
298
|
return;
|
|
251
299
|
}
|
|
252
|
-
const res = yield this.
|
|
300
|
+
const res = yield this._fetchFlags(this._context);
|
|
253
301
|
if (res.status === BulkEvaluationStatus.SUCCESS_WITH_CHANGES) {
|
|
254
302
|
(_a = this.events) === null || _a === void 0 ? void 0 : _a.emit(ClientProviderEvents.ConfigurationChanged, {
|
|
255
303
|
message: 'Flags updated',
|
|
@@ -258,7 +306,7 @@ class OFREPWebProvider {
|
|
|
258
306
|
}
|
|
259
307
|
}
|
|
260
308
|
catch (error) {
|
|
261
|
-
(_b = this.events) === null || _b ===
|
|
309
|
+
(_b = this.events) === null || _b === undefined ? undefined : _b.emit(ClientProviderEvents.Stale, { message: `Error while polling: ${error}` });
|
|
262
310
|
}
|
|
263
311
|
}), this._pollingInterval);
|
|
264
312
|
}
|
package/package.json
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openfeature/ofrep-web-provider",
|
|
3
|
-
"version": "0.3.
|
|
4
|
-
"
|
|
5
|
-
"tslib": "^2.3.0",
|
|
6
|
-
"@openfeature/ofrep-core": "0.2.0"
|
|
7
|
-
},
|
|
3
|
+
"version": "0.3.2",
|
|
4
|
+
"license": "Apache-2.0",
|
|
8
5
|
"main": "./index.cjs.js",
|
|
9
6
|
"typings": "./src/index.d.ts",
|
|
10
7
|
"scripts": {
|
|
@@ -12,15 +9,21 @@
|
|
|
12
9
|
"current-version": "echo $npm_package_version"
|
|
13
10
|
},
|
|
14
11
|
"peerDependencies": {
|
|
15
|
-
"@openfeature/web-sdk": "
|
|
12
|
+
"@openfeature/web-sdk": "^1.4.0"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"undici": "^5.0.0",
|
|
16
|
+
"@openfeature/ofrep-core": "^1.0.0"
|
|
16
17
|
},
|
|
17
18
|
"exports": {
|
|
18
19
|
"./package.json": "./package.json",
|
|
19
20
|
".": {
|
|
20
21
|
"module": "./index.esm.js",
|
|
22
|
+
"types": "./index.esm.d.ts",
|
|
21
23
|
"import": "./index.cjs.mjs",
|
|
22
24
|
"default": "./index.cjs.js"
|
|
23
25
|
}
|
|
24
26
|
},
|
|
25
|
-
"module": "./index.esm.js"
|
|
26
|
-
|
|
27
|
+
"module": "./index.esm.js",
|
|
28
|
+
"types": "./index.esm.d.ts"
|
|
29
|
+
}
|
|
@@ -1,8 +1,12 @@
|
|
|
1
|
-
import { FlagValue, ResolutionDetails } from '@openfeature/web-sdk';
|
|
1
|
+
import type { FlagMetadata, FlagValue, ResolutionDetails } from '@openfeature/web-sdk';
|
|
2
2
|
import { ResolutionError } from './resolution-error';
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
4
|
+
* Cache of flag values from bulk evaluation.
|
|
5
5
|
*/
|
|
6
6
|
export type FlagCache = {
|
|
7
7
|
[key: string]: ResolutionDetails<FlagValue> | ResolutionError;
|
|
8
8
|
};
|
|
9
|
+
/**
|
|
10
|
+
* Cache of metadata from bulk evaluation.
|
|
11
|
+
*/
|
|
12
|
+
export type MetadataCache = FlagMetadata;
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { ResolutionReason } from '@openfeature/web-sdk';
|
|
2
|
-
import { EvaluationFailureErrorCode } from '@openfeature/ofrep-core';
|
|
1
|
+
import { ErrorCode, ResolutionReason } from '@openfeature/web-sdk';
|
|
3
2
|
export type ResolutionError = {
|
|
4
3
|
reason: ResolutionReason;
|
|
5
|
-
errorCode:
|
|
4
|
+
errorCode: ErrorCode;
|
|
6
5
|
errorDetails?: string;
|
|
7
6
|
};
|
|
8
7
|
export declare function isResolutionError(response: unknown): response is ResolutionError;
|
|
@@ -16,6 +16,7 @@ export declare class OFREPWebProvider implements Provider {
|
|
|
16
16
|
private _pollingInterval;
|
|
17
17
|
private _retryPollingAfter;
|
|
18
18
|
private _flagCache;
|
|
19
|
+
private _flagSetMetadataCache;
|
|
19
20
|
private _context;
|
|
20
21
|
private _pollingIntervalId?;
|
|
21
22
|
constructor(options: OFREPWebProviderOptions, logger?: Logger);
|
|
@@ -44,7 +45,7 @@ export declare class OFREPWebProvider implements Provider {
|
|
|
44
45
|
*/
|
|
45
46
|
onClose?(): Promise<void>;
|
|
46
47
|
/**
|
|
47
|
-
*
|
|
48
|
+
* _fetchFlags is a function that will call the bulk evaluate flags endpoint to get the flags values.
|
|
48
49
|
* @param context - the context to use for the evaluation
|
|
49
50
|
* @private
|
|
50
51
|
* @returns EvaluationStatus if the evaluation the API returned a 304, 200.
|
|
@@ -53,7 +54,7 @@ export declare class OFREPWebProvider implements Provider {
|
|
|
53
54
|
* @throws ParseError if the API returned a 400 with the error code ParseError
|
|
54
55
|
* @throws GeneralError if the API returned a 400 with an unknown error code
|
|
55
56
|
*/
|
|
56
|
-
private
|
|
57
|
+
private _fetchFlags;
|
|
57
58
|
/**
|
|
58
59
|
* _getListUpdatedFlags is a function that will compare the old cache with the new cache and
|
|
59
60
|
* return the list of flags that have been updated / deleted / created.
|
|
@@ -63,12 +64,13 @@ export declare class OFREPWebProvider implements Provider {
|
|
|
63
64
|
*/
|
|
64
65
|
private _getListUpdatedFlags;
|
|
65
66
|
/**
|
|
66
|
-
*
|
|
67
|
+
* _resolve is a function retrieving the value from a flag in the cache.
|
|
67
68
|
* @param flagKey - name of the flag to retrieve
|
|
68
69
|
* @param type - type of the flag
|
|
70
|
+
* @param defaultValue - default value
|
|
69
71
|
* @private
|
|
70
72
|
*/
|
|
71
|
-
private
|
|
73
|
+
private _resolve;
|
|
72
74
|
/**
|
|
73
75
|
* Start polling for flag updates, it will call the bulk update function every pollInterval
|
|
74
76
|
* @private
|