flagsmith-nodejs 5.1.1 → 6.0.1
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/build/cjs/sdk/analytics.d.ts +1 -1
- package/build/cjs/sdk/analytics.js +1 -1
- package/build/cjs/sdk/index.d.ts +44 -42
- package/build/cjs/sdk/index.js +150 -152
- package/build/cjs/sdk/models.d.ts +7 -5
- package/build/cjs/sdk/polling_manager.d.ts +3 -1
- package/build/cjs/sdk/polling_manager.js +9 -2
- package/build/cjs/sdk/types.d.ts +73 -5
- package/build/cjs/sdk/utils.d.ts +24 -1
- package/build/cjs/sdk/utils.js +50 -18
- package/build/esm/sdk/analytics.d.ts +1 -1
- package/build/esm/sdk/analytics.js +1 -1
- package/build/esm/sdk/index.d.ts +44 -42
- package/build/esm/sdk/index.js +151 -153
- package/build/esm/sdk/models.d.ts +7 -5
- package/build/esm/sdk/polling_manager.d.ts +3 -1
- package/build/esm/sdk/polling_manager.js +9 -2
- package/build/esm/sdk/types.d.ts +73 -5
- package/build/esm/sdk/utils.d.ts +24 -1
- package/build/esm/sdk/utils.js +48 -17
- package/package.json +2 -2
- package/sdk/analytics.ts +5 -5
- package/sdk/index.ts +173 -176
- package/sdk/models.ts +9 -12
- package/sdk/polling_manager.ts +9 -2
- package/sdk/types.ts +74 -3
- package/sdk/utils.ts +50 -16
- package/tests/sdk/analytics.test.ts +0 -4
- package/tests/sdk/flagsmith-cache.test.ts +5 -17
- package/tests/sdk/flagsmith-environment-flags.test.ts +1 -29
- package/tests/sdk/flagsmith-identity-flags.test.ts +20 -15
- package/tests/sdk/flagsmith.test.ts +88 -83
- package/tests/sdk/polling.test.ts +0 -4
- package/tests/sdk/utils.ts +28 -6
- package/vitest.config.ts +1 -0
- /package/{.prettierrc.js → .prettierrc.cjs} +0 -0
package/build/esm/sdk/utils.js
CHANGED
|
@@ -33,24 +33,55 @@ export const delay = (ms) => new Promise(resolve => setTimeout(() => resolve(und
|
|
|
33
33
|
export const retryFetch = (url,
|
|
34
34
|
// built-in RequestInit type doesn't have dispatcher/agent
|
|
35
35
|
fetchOptions, retries = 3, timeoutMs = 10, // set an overall timeout for this function
|
|
36
|
-
customFetch) => {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
customFetch(url, {
|
|
36
|
+
retryDelayMs = 1000, customFetch) => {
|
|
37
|
+
const retryWrapper = async (n) => {
|
|
38
|
+
try {
|
|
39
|
+
return await customFetch(url, {
|
|
40
40
|
...fetchOptions,
|
|
41
41
|
signal: AbortSignal.timeout(timeoutMs)
|
|
42
|
-
})
|
|
43
|
-
.then(res => resolve(res))
|
|
44
|
-
.catch(async (err) => {
|
|
45
|
-
if (n > 0) {
|
|
46
|
-
await delay(1000);
|
|
47
|
-
retryWrapper(--n);
|
|
48
|
-
}
|
|
49
|
-
else {
|
|
50
|
-
reject(err);
|
|
51
|
-
}
|
|
52
42
|
});
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
|
|
43
|
+
}
|
|
44
|
+
catch (e) {
|
|
45
|
+
if (n > 0) {
|
|
46
|
+
await delay(retryDelayMs);
|
|
47
|
+
return await retryWrapper(n - 1);
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
throw e;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
return retryWrapper(retries);
|
|
56
55
|
};
|
|
56
|
+
/**
|
|
57
|
+
* A deferred promise can be resolved or rejected outside its creation scope.
|
|
58
|
+
*
|
|
59
|
+
* @template T The type of the value that the deferred promise will resolve to.
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* const deferred = new Deferred<string>()
|
|
63
|
+
*
|
|
64
|
+
* // Pass the promise somewhere
|
|
65
|
+
* performAsyncOperation(deferred.promise)
|
|
66
|
+
*
|
|
67
|
+
* // Resolve it when ready from anywhere
|
|
68
|
+
* deferred.resolve("Operation completed")
|
|
69
|
+
* deferred.failed("Error")
|
|
70
|
+
*/
|
|
71
|
+
export class Deferred {
|
|
72
|
+
promise;
|
|
73
|
+
resolvePromise;
|
|
74
|
+
rejectPromise;
|
|
75
|
+
constructor(initial) {
|
|
76
|
+
this.promise = new Promise((resolve, reject) => {
|
|
77
|
+
this.resolvePromise = resolve;
|
|
78
|
+
this.rejectPromise = reject;
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
resolve(value) {
|
|
82
|
+
this.resolvePromise(value);
|
|
83
|
+
}
|
|
84
|
+
reject(reason) {
|
|
85
|
+
this.rejectPromise(reason);
|
|
86
|
+
}
|
|
87
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flagsmith-nodejs",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "6.0.1",
|
|
4
4
|
"description": "Flagsmith lets you manage features flags and remote config across web, mobile and server side applications. Deliver true Continuous Integration. Get builds out faster. Control who has access to new features.",
|
|
5
5
|
"main": "./build/cjs/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
"@types/semver": "^7.3.9",
|
|
70
70
|
"@types/uuid": "^8.3.4",
|
|
71
71
|
"@vitest/coverage-v8": "^2.1.2",
|
|
72
|
-
"esbuild": "^0.
|
|
72
|
+
"esbuild": "^0.25.0",
|
|
73
73
|
"husky": "^7.0.4",
|
|
74
74
|
"prettier": "^2.2.1",
|
|
75
75
|
"typescript": "^4.9.5",
|
package/sdk/analytics.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { pino, Logger } from 'pino';
|
|
2
2
|
import { Fetch } from "./types.js";
|
|
3
|
-
import {
|
|
3
|
+
import { FlagsmithConfig } from "./types.js";
|
|
4
4
|
|
|
5
5
|
export const ANALYTICS_ENDPOINT = './analytics/flags/';
|
|
6
6
|
|
|
@@ -21,17 +21,17 @@ export interface AnalyticsProcessorOptions {
|
|
|
21
21
|
logger?: Logger;
|
|
22
22
|
/** Custom {@link fetch} implementation to use for API requests. **/
|
|
23
23
|
fetch?: Fetch
|
|
24
|
-
|
|
24
|
+
|
|
25
25
|
/** @deprecated Use {@link analyticsUrl} instead. **/
|
|
26
26
|
baseApiUrl?: string;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
30
|
* Tracks how often individual features are evaluated whenever {@link trackFeature} is called.
|
|
31
|
-
*
|
|
31
|
+
*
|
|
32
32
|
* Analytics data is posted after {@link trackFeature} is called and at least {@link ANALYTICS_TIMER} seconds have
|
|
33
33
|
* passed since the previous analytics API request was made (if any), or by calling {@link flush}.
|
|
34
|
-
*
|
|
34
|
+
*
|
|
35
35
|
* Data will stay in memory indefinitely until it can be successfully posted to the API.
|
|
36
36
|
* @see https://docs.flagsmith.com/advanced-use/flag-analytics.
|
|
37
37
|
*/
|
|
@@ -89,7 +89,7 @@ export class AnalyticsProcessor {
|
|
|
89
89
|
/**
|
|
90
90
|
* Track a single evaluation event for a feature.
|
|
91
91
|
*
|
|
92
|
-
*
|
|
92
|
+
* @see FlagsmithConfig.enableAnalytics
|
|
93
93
|
*/
|
|
94
94
|
trackFeature(featureName: string) {
|
|
95
95
|
this.analyticsData[featureName] = (this.analyticsData[featureName] || 0) + 1;
|
package/sdk/index.ts
CHANGED
|
@@ -7,11 +7,11 @@ import { TraitModel } from '../flagsmith-engine/index.js';
|
|
|
7
7
|
|
|
8
8
|
import {ANALYTICS_ENDPOINT, AnalyticsProcessor} from './analytics.js';
|
|
9
9
|
import { BaseOfflineHandler } from './offline_handlers.js';
|
|
10
|
-
import { FlagsmithAPIError
|
|
10
|
+
import { FlagsmithAPIError } from './errors.js';
|
|
11
11
|
|
|
12
12
|
import { DefaultFlag, Flags } from './models.js';
|
|
13
13
|
import { EnvironmentDataPollingManager } from './polling_manager.js';
|
|
14
|
-
import { generateIdentitiesData, retryFetch } from './utils.js';
|
|
14
|
+
import { Deferred, generateIdentitiesData, retryFetch } from './utils.js';
|
|
15
15
|
import { SegmentModel } from '../flagsmith-engine/index.js';
|
|
16
16
|
import { getIdentitySegments } from '../flagsmith-engine/segments/evaluators.js';
|
|
17
17
|
import { Fetch, FlagsmithCache, FlagsmithConfig, FlagsmithTraitValue, ITraitConfig } from './types.js';
|
|
@@ -27,6 +27,30 @@ export { FlagsmithCache, FlagsmithConfig } from './types.js';
|
|
|
27
27
|
const DEFAULT_API_URL = 'https://edge.api.flagsmith.com/api/v1/';
|
|
28
28
|
const DEFAULT_REQUEST_TIMEOUT_SECONDS = 10;
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* A client for evaluating Flagsmith feature flags.
|
|
32
|
+
*
|
|
33
|
+
* Flags are evaluated remotely by the Flagsmith API over HTTP by default.
|
|
34
|
+
* To evaluate flags locally, create the client using {@link FlagsmithConfig.enableLocalEvaluation} and a server-side SDK key.
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* import { Flagsmith, Flags, DefaultFlag } from 'flagsmith-nodejs'
|
|
38
|
+
*
|
|
39
|
+
* const flagsmith = new Flagsmith({
|
|
40
|
+
* environmentKey: 'your_sdk_key',
|
|
41
|
+
* defaultFlagHandler: (flagKey: string) => { new DefaultFlag(...) },
|
|
42
|
+
* });
|
|
43
|
+
*
|
|
44
|
+
* // Fetch the current environment flags
|
|
45
|
+
* const environmentFlags: Flags = flagsmith.getEnvironmentFlags()
|
|
46
|
+
* const isFooEnabled: boolean = environmentFlags.isFeatureEnabled('foo')
|
|
47
|
+
*
|
|
48
|
+
* // Evaluate flags for any identity
|
|
49
|
+
* const identityFlags: Flags = flagsmith.getIdentityFlags('my_user_123', {'vip': true})
|
|
50
|
+
* const bannerVariation: string = identityFlags.getFeatureValue('banner_flag')
|
|
51
|
+
*
|
|
52
|
+
* @see FlagsmithConfig
|
|
53
|
+
*/
|
|
30
54
|
export class Flagsmith {
|
|
31
55
|
environmentKey?: string = undefined;
|
|
32
56
|
apiUrl?: string = undefined;
|
|
@@ -45,66 +69,35 @@ export class Flagsmith {
|
|
|
45
69
|
environmentUrl?: string;
|
|
46
70
|
|
|
47
71
|
environmentDataPollingManager?: EnvironmentDataPollingManager;
|
|
48
|
-
environment
|
|
72
|
+
private environment?: EnvironmentModel;
|
|
49
73
|
offlineMode: boolean = false;
|
|
50
74
|
offlineHandler?: BaseOfflineHandler = undefined;
|
|
51
75
|
|
|
52
76
|
identitiesWithOverridesByIdentifier?: Map<string, IdentityModel>;
|
|
53
77
|
|
|
54
78
|
private cache?: FlagsmithCache;
|
|
55
|
-
private onEnvironmentChange
|
|
79
|
+
private onEnvironmentChange: (error: Error | null, result?: EnvironmentModel) => void;
|
|
56
80
|
private analyticsProcessor?: AnalyticsProcessor;
|
|
57
81
|
private logger: Logger;
|
|
58
82
|
private customFetch: Fetch;
|
|
83
|
+
private readonly requestRetryDelayMilliseconds: number;
|
|
84
|
+
|
|
59
85
|
/**
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
* Provides an interface for interacting with the Flagsmith http API.
|
|
63
|
-
* Basic Usage::
|
|
64
|
-
*
|
|
65
|
-
* import flagsmith from Flagsmith
|
|
66
|
-
* const flagsmith = new Flagsmith({environmentKey: '<your API key>'});
|
|
67
|
-
* const environmentFlags = flagsmith.getEnvironmentFlags();
|
|
68
|
-
* const featureEnabled = environmentFlags.isFeatureEnabled('foo');
|
|
69
|
-
* const identityFlags = flagsmith.getIdentityFlags('identifier', {'foo': 'bar'});
|
|
70
|
-
* const featureEnabledForIdentity = identityFlags.isFeatureEnabled("foo")
|
|
86
|
+
* Creates a new {@link Flagsmith} client.
|
|
71
87
|
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
@param {number} data.requestTimeoutSeconds: Number of seconds to wait for a request to
|
|
78
|
-
complete before terminating the request
|
|
79
|
-
@param {boolean} data.enableLocalEvaluation: Enables local evaluation of flags
|
|
80
|
-
@param {number} data.environmentRefreshIntervalSeconds: If using local evaluation,
|
|
81
|
-
specify the interval period between refreshes of local environment data
|
|
82
|
-
@param {number} data.retries: a urllib3.Retry object to use on all http requests to the
|
|
83
|
-
Flagsmith API
|
|
84
|
-
@param {boolean} data.enableAnalytics: if enabled, sends additional requests to the Flagsmith
|
|
85
|
-
API to power flag analytics charts
|
|
86
|
-
@param data.defaultFlagHandler: callable which will be used in the case where
|
|
87
|
-
flags cannot be retrieved from the API or a non-existent feature is
|
|
88
|
-
requested
|
|
89
|
-
@param data.logger: an instance of the pino Logger class to use for logging
|
|
90
|
-
@param {boolean} data.offlineMode: sets the client into offline mode. Relies on offlineHandler for
|
|
91
|
-
evaluating flags.
|
|
92
|
-
@param {BaseOfflineHandler} data.offlineHandler: provide a handler for offline logic. Used to get environment
|
|
93
|
-
document from another source when in offlineMode. Works in place of
|
|
94
|
-
defaultFlagHandler if offlineMode is not set and using remote evaluation.
|
|
95
|
-
*/
|
|
96
|
-
constructor(data: FlagsmithConfig = {}) {
|
|
97
|
-
// if (!data.offlineMode && !data.environmentKey) {
|
|
98
|
-
// throw new Error('ValueError: environmentKey is required.');
|
|
99
|
-
// }
|
|
100
|
-
|
|
88
|
+
* If using local evaluation, the environment will be fetched lazily when needed by any method. Polling the
|
|
89
|
+
* environment for updates will start after {@link environmentRefreshIntervalSeconds} once the client is created.
|
|
90
|
+
* @param data The {@link FlagsmithConfig} options for this client.
|
|
91
|
+
*/
|
|
92
|
+
constructor(data: FlagsmithConfig) {
|
|
101
93
|
this.agent = data.agent;
|
|
102
94
|
this.customFetch = data.fetch ?? fetch;
|
|
103
95
|
this.environmentKey = data.environmentKey;
|
|
104
|
-
this.apiUrl = data.apiUrl ||
|
|
96
|
+
this.apiUrl = data.apiUrl || DEFAULT_API_URL;
|
|
105
97
|
this.customHeaders = data.customHeaders;
|
|
106
98
|
this.requestTimeoutMs =
|
|
107
99
|
1000 * (data.requestTimeoutSeconds ?? DEFAULT_REQUEST_TIMEOUT_SECONDS);
|
|
100
|
+
this.requestRetryDelayMilliseconds = data.requestRetryDelayMilliseconds ?? 1000;
|
|
108
101
|
this.enableLocalEvaluation = data.enableLocalEvaluation;
|
|
109
102
|
this.environmentRefreshIntervalSeconds =
|
|
110
103
|
data.environmentRefreshIntervalSeconds || this.environmentRefreshIntervalSeconds;
|
|
@@ -112,7 +105,7 @@ export class Flagsmith {
|
|
|
112
105
|
this.enableAnalytics = data.enableAnalytics || false;
|
|
113
106
|
this.defaultFlagHandler = data.defaultFlagHandler;
|
|
114
107
|
|
|
115
|
-
this.onEnvironmentChange = data.onEnvironmentChange;
|
|
108
|
+
this.onEnvironmentChange = (error, result) => data.onEnvironmentChange?.(error, result);
|
|
116
109
|
this.logger = data.logger || pino();
|
|
117
110
|
this.offlineMode = data.offlineMode || false;
|
|
118
111
|
this.offlineHandler = data.offlineHandler;
|
|
@@ -124,10 +117,6 @@ export class Flagsmith {
|
|
|
124
117
|
throw new Error('ValueError: Cannot use both defaultFlagHandler and offlineHandler.');
|
|
125
118
|
}
|
|
126
119
|
|
|
127
|
-
if (this.offlineHandler) {
|
|
128
|
-
this.environment = this.offlineHandler.getEnvironment();
|
|
129
|
-
}
|
|
130
|
-
|
|
131
120
|
if (!!data.cache) {
|
|
132
121
|
this.cache = data.cache;
|
|
133
122
|
}
|
|
@@ -146,16 +135,16 @@ export class Flagsmith {
|
|
|
146
135
|
|
|
147
136
|
if (this.enableLocalEvaluation) {
|
|
148
137
|
if (!this.environmentKey.startsWith('ser.')) {
|
|
149
|
-
|
|
150
|
-
|
|
138
|
+
throw new Error('Using local evaluation requires a server-side environment key');
|
|
139
|
+
}
|
|
140
|
+
if (this.environmentRefreshIntervalSeconds > 0){
|
|
141
|
+
this.environmentDataPollingManager = new EnvironmentDataPollingManager(
|
|
142
|
+
this,
|
|
143
|
+
this.environmentRefreshIntervalSeconds,
|
|
144
|
+
this.logger,
|
|
151
145
|
);
|
|
146
|
+
this.environmentDataPollingManager.start();
|
|
152
147
|
}
|
|
153
|
-
this.environmentDataPollingManager = new EnvironmentDataPollingManager(
|
|
154
|
-
this,
|
|
155
|
-
this.environmentRefreshIntervalSeconds
|
|
156
|
-
);
|
|
157
|
-
this.environmentDataPollingManager.start();
|
|
158
|
-
this.updateEnvironment();
|
|
159
148
|
}
|
|
160
149
|
|
|
161
150
|
if (data.enableAnalytics) {
|
|
@@ -178,18 +167,21 @@ export class Flagsmith {
|
|
|
178
167
|
if (!!cachedItem) {
|
|
179
168
|
return cachedItem;
|
|
180
169
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
this.
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
170
|
+
try {
|
|
171
|
+
if (this.enableLocalEvaluation || this.offlineMode) {
|
|
172
|
+
return await this.getEnvironmentFlagsFromDocument();
|
|
173
|
+
}
|
|
174
|
+
return await this.getEnvironmentFlagsFromApi();
|
|
175
|
+
} catch (error) {
|
|
176
|
+
if (!this.defaultFlagHandler) {
|
|
177
|
+
throw new Error('getEnvironmentFlags failed and no default flag handler was provided', { cause: error });
|
|
178
|
+
}
|
|
179
|
+
this.logger.error(error, 'getEnvironmentFlags failed');
|
|
180
|
+
return new Flags({
|
|
181
|
+
flags: {},
|
|
182
|
+
defaultFlagHandler: this.defaultFlagHandler
|
|
183
|
+
});
|
|
190
184
|
}
|
|
191
|
-
|
|
192
|
-
return this.getEnvironmentFlagsFromApi();
|
|
193
185
|
}
|
|
194
186
|
|
|
195
187
|
/**
|
|
@@ -217,18 +209,21 @@ export class Flagsmith {
|
|
|
217
209
|
return cachedItem;
|
|
218
210
|
}
|
|
219
211
|
traits = traits || {};
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
this.
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
212
|
+
try {
|
|
213
|
+
if (this.enableLocalEvaluation || this.offlineMode) {
|
|
214
|
+
return await this.getIdentityFlagsFromDocument(identifier, traits || {});
|
|
215
|
+
}
|
|
216
|
+
return await this.getIdentityFlagsFromApi(identifier, traits, transient);
|
|
217
|
+
} catch (error) {
|
|
218
|
+
if (!this.defaultFlagHandler) {
|
|
219
|
+
throw new Error('getIdentityFlags failed and no default flag handler was provided', { cause: error })
|
|
220
|
+
}
|
|
221
|
+
this.logger.error(error, 'getIdentityFlags failed');
|
|
222
|
+
return new Flags({
|
|
223
|
+
flags: {},
|
|
224
|
+
defaultFlagHandler: this.defaultFlagHandler
|
|
225
|
+
});
|
|
229
226
|
}
|
|
230
|
-
|
|
231
|
-
return this.getIdentityFlagsFromApi(identifier, traits, transient);
|
|
232
227
|
}
|
|
233
228
|
|
|
234
229
|
/**
|
|
@@ -242,66 +237,69 @@ export class Flagsmith {
|
|
|
242
237
|
Flagsmith, e.g. {"num_orders": 10}
|
|
243
238
|
* @returns Segments that the given identity belongs to.
|
|
244
239
|
*/
|
|
245
|
-
getIdentitySegments(
|
|
240
|
+
async getIdentitySegments(
|
|
246
241
|
identifier: string,
|
|
247
242
|
traits?: { [key: string]: any }
|
|
248
243
|
): Promise<SegmentModel[]> {
|
|
249
244
|
if (!identifier) {
|
|
250
245
|
throw new Error('`identifier` argument is missing or invalid.');
|
|
251
246
|
}
|
|
247
|
+
if (!this.enableLocalEvaluation) {
|
|
248
|
+
this.logger.error('This function is only permitted with local evaluation.');
|
|
249
|
+
return Promise.resolve([]);
|
|
250
|
+
}
|
|
252
251
|
|
|
253
252
|
traits = traits || {};
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
);
|
|
253
|
+
const environment = await this.getEnvironment();
|
|
254
|
+
const identityModel = this.getIdentityModel(
|
|
255
|
+
environment,
|
|
256
|
+
identifier,
|
|
257
|
+
Object.keys(traits || {}).map(key => ({
|
|
258
|
+
key,
|
|
259
|
+
value: traits?.[key]
|
|
260
|
+
}))
|
|
261
|
+
);
|
|
264
262
|
|
|
265
|
-
|
|
266
|
-
return resolve(segments);
|
|
267
|
-
}).catch(e => reject(e));
|
|
268
|
-
});
|
|
269
|
-
}
|
|
270
|
-
console.error('This function is only permitted with local evaluation.');
|
|
271
|
-
return Promise.resolve([]);
|
|
263
|
+
return getIdentitySegments(environment, identityModel);
|
|
272
264
|
}
|
|
273
265
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
* You only need to call this if you wish to bypass environmentRefreshIntervalSeconds.
|
|
278
|
-
*/
|
|
279
|
-
async updateEnvironment() {
|
|
266
|
+
private async fetchEnvironment(): Promise<EnvironmentModel> {
|
|
267
|
+
const deferred = new Deferred<EnvironmentModel>();
|
|
268
|
+
this.environmentPromise = deferred.promise;
|
|
280
269
|
try {
|
|
281
|
-
const
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
this.environment = res;
|
|
285
|
-
});
|
|
286
|
-
await this.environmentPromise;
|
|
287
|
-
} else {
|
|
288
|
-
this.environment = await request;
|
|
289
|
-
}
|
|
290
|
-
if (this.environment.identityOverrides?.length) {
|
|
270
|
+
const environment = await this.getEnvironmentFromApi();
|
|
271
|
+
this.environment = environment;
|
|
272
|
+
if (environment.identityOverrides?.length) {
|
|
291
273
|
this.identitiesWithOverridesByIdentifier = new Map<string, IdentityModel>(
|
|
292
|
-
|
|
293
|
-
identity.identifier,
|
|
294
|
-
identity
|
|
295
|
-
])
|
|
274
|
+
environment.identityOverrides.map(identity => [identity.identifier, identity])
|
|
296
275
|
);
|
|
297
276
|
}
|
|
298
|
-
|
|
299
|
-
|
|
277
|
+
deferred.resolve(environment);
|
|
278
|
+
return deferred.promise;
|
|
279
|
+
} catch (error) {
|
|
280
|
+
deferred.reject(error);
|
|
281
|
+
return deferred.promise;
|
|
282
|
+
} finally {
|
|
283
|
+
this.environmentPromise = undefined;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Fetch the latest environment state from the Flagsmith API to use for local flag evaluation.
|
|
289
|
+
*
|
|
290
|
+
* If the environment is currently being fetched, calling this method will not cause additional fetches.
|
|
291
|
+
*/
|
|
292
|
+
async updateEnvironment(): Promise<void> {
|
|
293
|
+
try {
|
|
294
|
+
if (this.environmentPromise) {
|
|
295
|
+
await this.environmentPromise
|
|
296
|
+
return
|
|
300
297
|
}
|
|
298
|
+
const environment = await this.fetchEnvironment();
|
|
299
|
+
this.onEnvironmentChange(null, environment);
|
|
301
300
|
} catch (e) {
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
}
|
|
301
|
+
this.logger.error(e, 'updateEnvironment failed');
|
|
302
|
+
this.onEnvironmentChange(e as Error);
|
|
305
303
|
}
|
|
306
304
|
}
|
|
307
305
|
|
|
@@ -335,6 +333,7 @@ export class Flagsmith {
|
|
|
335
333
|
},
|
|
336
334
|
this.retries,
|
|
337
335
|
this.requestTimeoutMs,
|
|
336
|
+
this.requestRetryDelayMilliseconds,
|
|
338
337
|
this.customFetch,
|
|
339
338
|
);
|
|
340
339
|
|
|
@@ -350,7 +349,25 @@ export class Flagsmith {
|
|
|
350
349
|
/**
|
|
351
350
|
* This promise ensures that the environment is retrieved before attempting to locally evaluate.
|
|
352
351
|
*/
|
|
353
|
-
private environmentPromise
|
|
352
|
+
private environmentPromise?: Promise<EnvironmentModel>;
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Returns the current environment, fetching it from the API if needed.
|
|
356
|
+
*
|
|
357
|
+
* Calling this method concurrently while the environment is being fetched will not cause additional requests.
|
|
358
|
+
*/
|
|
359
|
+
async getEnvironment(): Promise<EnvironmentModel> {
|
|
360
|
+
if (this.offlineHandler) {
|
|
361
|
+
return this.offlineHandler.getEnvironment();
|
|
362
|
+
}
|
|
363
|
+
if (this.environment) {
|
|
364
|
+
return this.environment;
|
|
365
|
+
}
|
|
366
|
+
if (!this.environmentPromise) {
|
|
367
|
+
this.environmentPromise = this.fetchEnvironment();
|
|
368
|
+
}
|
|
369
|
+
return this.environmentPromise;
|
|
370
|
+
}
|
|
354
371
|
|
|
355
372
|
private async getEnvironmentFromApi() {
|
|
356
373
|
if (!this.environmentUrl) {
|
|
@@ -361,8 +378,9 @@ export class Flagsmith {
|
|
|
361
378
|
}
|
|
362
379
|
|
|
363
380
|
private async getEnvironmentFlagsFromDocument(): Promise<Flags> {
|
|
381
|
+
const environment = await this.getEnvironment();
|
|
364
382
|
const flags = Flags.fromFeatureStateModels({
|
|
365
|
-
featureStates: getEnvironmentFeatureStates(
|
|
383
|
+
featureStates: getEnvironmentFeatureStates(environment),
|
|
366
384
|
analyticsProcessor: this.analyticsProcessor,
|
|
367
385
|
defaultFlagHandler: this.defaultFlagHandler
|
|
368
386
|
});
|
|
@@ -376,7 +394,9 @@ export class Flagsmith {
|
|
|
376
394
|
identifier: string,
|
|
377
395
|
traits: { [key: string]: any }
|
|
378
396
|
): Promise<Flags> {
|
|
397
|
+
const environment = await this.getEnvironment();
|
|
379
398
|
const identityModel = this.getIdentityModel(
|
|
399
|
+
environment,
|
|
380
400
|
identifier,
|
|
381
401
|
Object.keys(traits).map(key => ({
|
|
382
402
|
key,
|
|
@@ -384,7 +404,7 @@ export class Flagsmith {
|
|
|
384
404
|
}))
|
|
385
405
|
);
|
|
386
406
|
|
|
387
|
-
const featureStates = getIdentityFeatureStates(
|
|
407
|
+
const featureStates = getIdentityFeatureStates(environment, identityModel);
|
|
388
408
|
|
|
389
409
|
const flags = Flags.fromFeatureStateModels({
|
|
390
410
|
featureStates: featureStates,
|
|
@@ -404,30 +424,16 @@ export class Flagsmith {
|
|
|
404
424
|
if (!this.environmentFlagsUrl) {
|
|
405
425
|
throw new Error('`apiUrl` argument is missing or invalid.');
|
|
406
426
|
}
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
await this.cache.set('flags', flags);
|
|
416
|
-
}
|
|
417
|
-
return flags;
|
|
418
|
-
} catch (e) {
|
|
419
|
-
if (this.offlineHandler) {
|
|
420
|
-
return this.getEnvironmentFlagsFromDocument();
|
|
421
|
-
}
|
|
422
|
-
if (this.defaultFlagHandler) {
|
|
423
|
-
return new Flags({
|
|
424
|
-
flags: {},
|
|
425
|
-
defaultFlagHandler: this.defaultFlagHandler
|
|
426
|
-
});
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
throw e;
|
|
427
|
+
const apiFlags = await this.getJSONResponse(this.environmentFlagsUrl, 'GET');
|
|
428
|
+
const flags = Flags.fromAPIFlags({
|
|
429
|
+
apiFlags: apiFlags,
|
|
430
|
+
analyticsProcessor: this.analyticsProcessor,
|
|
431
|
+
defaultFlagHandler: this.defaultFlagHandler
|
|
432
|
+
});
|
|
433
|
+
if (!!this.cache) {
|
|
434
|
+
await this.cache.set('flags', flags);
|
|
430
435
|
}
|
|
436
|
+
return flags;
|
|
431
437
|
}
|
|
432
438
|
|
|
433
439
|
private async getIdentityFlagsFromApi(
|
|
@@ -438,41 +444,32 @@ export class Flagsmith {
|
|
|
438
444
|
if (!this.identitiesUrl) {
|
|
439
445
|
throw new Error('`apiUrl` argument is missing or invalid.');
|
|
440
446
|
}
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
await this.cache.set(`flags-${identifier}`, flags);
|
|
451
|
-
}
|
|
452
|
-
return flags;
|
|
453
|
-
} catch (e) {
|
|
454
|
-
if (this.offlineHandler) {
|
|
455
|
-
return this.getIdentityFlagsFromDocument(identifier, traits);
|
|
456
|
-
}
|
|
457
|
-
if (this.defaultFlagHandler) {
|
|
458
|
-
return new Flags({
|
|
459
|
-
flags: {},
|
|
460
|
-
defaultFlagHandler: this.defaultFlagHandler
|
|
461
|
-
});
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
throw e;
|
|
447
|
+
const data = generateIdentitiesData(identifier, traits, transient);
|
|
448
|
+
const jsonResponse = await this.getJSONResponse(this.identitiesUrl, 'POST', data);
|
|
449
|
+
const flags = Flags.fromAPIFlags({
|
|
450
|
+
apiFlags: jsonResponse['flags'],
|
|
451
|
+
analyticsProcessor: this.analyticsProcessor,
|
|
452
|
+
defaultFlagHandler: this.defaultFlagHandler
|
|
453
|
+
});
|
|
454
|
+
if (!!this.cache) {
|
|
455
|
+
await this.cache.set(`flags-${identifier}`, flags);
|
|
465
456
|
}
|
|
457
|
+
return flags;
|
|
466
458
|
}
|
|
467
459
|
|
|
468
|
-
private getIdentityModel(
|
|
460
|
+
private getIdentityModel(
|
|
461
|
+
environment: EnvironmentModel,
|
|
462
|
+
identifier: string,
|
|
463
|
+
traits: { key: string; value: any }[]
|
|
464
|
+
) {
|
|
469
465
|
const traitModels = traits.map(trait => new TraitModel(trait.key, trait.value));
|
|
470
|
-
let identityWithOverrides =
|
|
466
|
+
let identityWithOverrides =
|
|
467
|
+
this.identitiesWithOverridesByIdentifier?.get(identifier);
|
|
471
468
|
if (identityWithOverrides) {
|
|
472
469
|
identityWithOverrides.updateTraits(traitModels);
|
|
473
470
|
return identityWithOverrides;
|
|
474
471
|
}
|
|
475
|
-
return new IdentityModel('0', traitModels, [],
|
|
472
|
+
return new IdentityModel('0', traitModels, [], environment.apiKey, identifier);
|
|
476
473
|
}
|
|
477
474
|
}
|
|
478
475
|
|