@launchdarkly/js-client-sdk-common 1.8.0 → 1.9.1-beta.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/CHANGELOG.md +27 -0
- package/dist/DataManager.d.ts +18 -2
- package/dist/DataManager.d.ts.map +1 -1
- package/dist/HookRunner.d.ts +12 -0
- package/dist/HookRunner.d.ts.map +1 -0
- package/dist/LDClientImpl.d.ts +24 -21
- package/dist/LDClientImpl.d.ts.map +1 -1
- package/dist/LDEmitter.d.ts +17 -5
- package/dist/LDEmitter.d.ts.map +1 -1
- package/dist/api/LDClient.d.ts +13 -0
- package/dist/api/LDClient.d.ts.map +1 -1
- package/dist/api/LDInspection.d.ts +105 -0
- package/dist/api/LDInspection.d.ts.map +1 -0
- package/dist/api/LDOptions.d.ts +31 -2
- package/dist/api/LDOptions.d.ts.map +1 -1
- package/dist/api/index.d.ts +1 -0
- package/dist/api/index.d.ts.map +1 -1
- package/dist/api/integrations/Hooks.d.ts +133 -0
- package/dist/api/integrations/Hooks.d.ts.map +1 -0
- package/dist/api/integrations/index.d.ts +2 -0
- package/dist/api/integrations/index.d.ts.map +1 -0
- package/dist/configuration/Configuration.d.ts +10 -8
- package/dist/configuration/Configuration.d.ts.map +1 -1
- package/dist/configuration/validators.d.ts.map +1 -1
- package/dist/context/addAutoEnv.d.ts +2 -2
- package/dist/context/addAutoEnv.d.ts.map +1 -1
- package/dist/context/ensureKey.d.ts +1 -1
- package/dist/context/ensureKey.d.ts.map +1 -1
- package/dist/{streaming → datasource}/DataSourceConfig.d.ts +5 -0
- package/dist/datasource/DataSourceConfig.d.ts.map +1 -0
- package/dist/datasource/DataSourceEventHandler.d.ts +16 -0
- package/dist/datasource/DataSourceEventHandler.d.ts.map +1 -0
- package/dist/datasource/DataSourceStatus.d.ts +39 -0
- package/dist/datasource/DataSourceStatus.d.ts.map +1 -0
- package/dist/datasource/DataSourceStatusErrorInfo.d.ts +8 -0
- package/dist/datasource/DataSourceStatusErrorInfo.d.ts.map +1 -0
- package/dist/datasource/DataSourceStatusManager.d.ts +40 -0
- package/dist/datasource/DataSourceStatusManager.d.ts.map +1 -0
- package/dist/datasource/Requestor.d.ts +26 -0
- package/dist/datasource/Requestor.d.ts.map +1 -0
- package/dist/evaluation/evaluationDetail.d.ts.map +1 -1
- package/dist/flag-manager/FlagManager.d.ts +14 -5
- package/dist/flag-manager/FlagManager.d.ts.map +1 -1
- package/dist/flag-manager/FlagPersistence.d.ts +13 -13
- package/dist/flag-manager/FlagPersistence.d.ts.map +1 -1
- package/dist/flag-manager/FlagStore.d.ts +1 -1
- package/dist/flag-manager/FlagStore.d.ts.map +1 -1
- package/dist/flag-manager/FlagUpdater.d.ts +6 -5
- package/dist/flag-manager/FlagUpdater.d.ts.map +1 -1
- package/dist/index.cjs +1893 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +10 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +1865 -0
- package/dist/index.mjs.map +1 -0
- package/dist/inspection/InspectorManager.d.ts +41 -0
- package/dist/inspection/InspectorManager.d.ts.map +1 -0
- package/dist/inspection/createSafeInspector.d.ts +8 -0
- package/dist/inspection/createSafeInspector.d.ts.map +1 -0
- package/dist/inspection/getInspectorHook.d.ts +4 -0
- package/dist/inspection/getInspectorHook.d.ts.map +1 -0
- package/dist/inspection/messages.d.ts +3 -0
- package/dist/inspection/messages.d.ts.map +1 -0
- package/dist/polling/PollingProcessor.d.ts.map +1 -1
- package/dist/streaming/StreamingProcessor.d.ts +18 -16
- package/dist/streaming/StreamingProcessor.d.ts.map +1 -1
- package/dist/streaming/index.d.ts +1 -1
- package/dist/streaming/index.d.ts.map +1 -1
- package/dist/types/index.d.ts +4 -3
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +18 -7
- package/dist/DataManager.js +0 -98
- package/dist/DataManager.js.map +0 -1
- package/dist/LDClientImpl.js +0 -334
- package/dist/LDClientImpl.js.map +0 -1
- package/dist/LDEmitter.js +0 -64
- package/dist/LDEmitter.js.map +0 -1
- package/dist/api/ConnectionMode.js +0 -3
- package/dist/api/ConnectionMode.js.map +0 -1
- package/dist/api/LDClient.js +0 -3
- package/dist/api/LDClient.js.map +0 -1
- package/dist/api/LDEvaluationDetail.js +0 -3
- package/dist/api/LDEvaluationDetail.js.map +0 -1
- package/dist/api/LDIdentifyOptions.js +0 -3
- package/dist/api/LDIdentifyOptions.js.map +0 -1
- package/dist/api/LDOptions.js +0 -3
- package/dist/api/LDOptions.js.map +0 -1
- package/dist/api/index.js +0 -21
- package/dist/api/index.js.map +0 -1
- package/dist/configuration/Configuration.js +0 -79
- package/dist/configuration/Configuration.js.map +0 -1
- package/dist/configuration/index.js +0 -8
- package/dist/configuration/index.js.map +0 -1
- package/dist/configuration/validators.js +0 -29
- package/dist/configuration/validators.js.map +0 -1
- package/dist/context/addAutoEnv.js +0 -113
- package/dist/context/addAutoEnv.js.map +0 -1
- package/dist/context/ensureKey.js +0 -72
- package/dist/context/ensureKey.js.map +0 -1
- package/dist/crypto/digest.js +0 -14
- package/dist/crypto/digest.js.map +0 -1
- package/dist/diagnostics/createDiagnosticsInitConfig.js +0 -19
- package/dist/diagnostics/createDiagnosticsInitConfig.js.map +0 -1
- package/dist/diagnostics/createDiagnosticsManager.js +0 -12
- package/dist/diagnostics/createDiagnosticsManager.js.map +0 -1
- package/dist/evaluation/evaluationDetail.js +0 -20
- package/dist/evaluation/evaluationDetail.js.map +0 -1
- package/dist/events/EventFactory.js +0 -25
- package/dist/events/EventFactory.js.map +0 -1
- package/dist/events/createEventProcessor.js +0 -11
- package/dist/events/createEventProcessor.js.map +0 -1
- package/dist/flag-manager/ContextIndex.js +0 -64
- package/dist/flag-manager/ContextIndex.js.map +0 -1
- package/dist/flag-manager/FlagManager.js +0 -48
- package/dist/flag-manager/FlagManager.js.map +0 -1
- package/dist/flag-manager/FlagPersistence.js +0 -120
- package/dist/flag-manager/FlagPersistence.js.map +0 -1
- package/dist/flag-manager/FlagStore.js +0 -31
- package/dist/flag-manager/FlagStore.js.map +0 -1
- package/dist/flag-manager/FlagUpdater.js +0 -69
- package/dist/flag-manager/FlagUpdater.js.map +0 -1
- package/dist/flag-manager/ItemDescriptor.js +0 -3
- package/dist/flag-manager/ItemDescriptor.js.map +0 -1
- package/dist/flag-manager/calculateChangedKeys.js +0 -22
- package/dist/flag-manager/calculateChangedKeys.js.map +0 -1
- package/dist/index.js +0 -26
- package/dist/index.js.map +0 -1
- package/dist/polling/PollingProcessor.js +0 -96
- package/dist/polling/PollingProcessor.js.map +0 -1
- package/dist/polling/Requestor.d.ts +0 -21
- package/dist/polling/Requestor.d.ts.map +0 -1
- package/dist/polling/Requestor.js +0 -48
- package/dist/polling/Requestor.js.map +0 -1
- package/dist/storage/getOrGenerateKey.js +0 -21
- package/dist/storage/getOrGenerateKey.js.map +0 -1
- package/dist/storage/namespaceUtils.js +0 -62
- package/dist/storage/namespaceUtils.js.map +0 -1
- package/dist/streaming/DataSourceConfig.d.ts.map +0 -1
- package/dist/streaming/DataSourceConfig.js +0 -3
- package/dist/streaming/DataSourceConfig.js.map +0 -1
- package/dist/streaming/StreamingProcessor.js +0 -126
- package/dist/streaming/StreamingProcessor.js.map +0 -1
- package/dist/streaming/index.js +0 -6
- package/dist/streaming/index.js.map +0 -1
- package/dist/types/index.js +0 -3
- package/dist/types/index.js.map +0 -1
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,1865 @@
|
|
|
1
|
+
import { getPollingUri, TypeValidators, createSafeLogger, ServiceEndpoints, ApplicationTags, OptionMessages, NumberWithMinimum, SafeLogger, internal, deepCompact, clone, secondsToMillis, ClientContext, fastDeepEqual, defaultHeaders, Context, timedPromise, AutoEnvAttributes, LDClientError, isHttpRecoverable, httpErrorMessage, LDPollingError, DataSourceErrorKind, getStreamingUri, shouldRetry, LDStreamingError } from '@launchdarkly/js-sdk-common';
|
|
2
|
+
export * from '@launchdarkly/js-sdk-common';
|
|
3
|
+
import * as jsSdkCommon from '@launchdarkly/js-sdk-common';
|
|
4
|
+
export { jsSdkCommon as platform };
|
|
5
|
+
|
|
6
|
+
var DataSourceState;
|
|
7
|
+
(function (DataSourceState) {
|
|
8
|
+
DataSourceState["Initializing"] = "INITIALIZING";
|
|
9
|
+
DataSourceState["Valid"] = "VALID";
|
|
10
|
+
DataSourceState["Interrupted"] = "INTERRUPTED";
|
|
11
|
+
DataSourceState["SetOffline"] = "SET_OFFLINE";
|
|
12
|
+
DataSourceState["Closed"] = "CLOSED";
|
|
13
|
+
// TODO: SDK-702 - Implement network availability behaviors
|
|
14
|
+
// NetworkUnavailable,
|
|
15
|
+
})(DataSourceState || (DataSourceState = {}));
|
|
16
|
+
|
|
17
|
+
// eslint-disable-next-line max-classes-per-file
|
|
18
|
+
function isOk(status) {
|
|
19
|
+
return status >= 200 && status <= 299;
|
|
20
|
+
}
|
|
21
|
+
class LDRequestError extends Error {
|
|
22
|
+
constructor(message, status) {
|
|
23
|
+
super(message);
|
|
24
|
+
this.status = status;
|
|
25
|
+
this.name = 'LaunchDarklyRequestError';
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Note: The requestor is implemented independently from polling such that it can be used to
|
|
30
|
+
* make a one-off request.
|
|
31
|
+
*/
|
|
32
|
+
class Requestor {
|
|
33
|
+
constructor(_requests, _uri, _headers, _method, _body) {
|
|
34
|
+
this._requests = _requests;
|
|
35
|
+
this._uri = _uri;
|
|
36
|
+
this._headers = _headers;
|
|
37
|
+
this._method = _method;
|
|
38
|
+
this._body = _body;
|
|
39
|
+
}
|
|
40
|
+
async requestPayload() {
|
|
41
|
+
let status;
|
|
42
|
+
try {
|
|
43
|
+
const res = await this._requests.fetch(this._uri, {
|
|
44
|
+
method: this._method,
|
|
45
|
+
headers: this._headers,
|
|
46
|
+
body: this._body,
|
|
47
|
+
});
|
|
48
|
+
if (isOk(res.status)) {
|
|
49
|
+
return await res.text();
|
|
50
|
+
}
|
|
51
|
+
// Assigning so it can be thrown after the try/catch.
|
|
52
|
+
status = res.status;
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
throw new LDRequestError(err?.message);
|
|
56
|
+
}
|
|
57
|
+
throw new LDRequestError(`Unexpected status code: ${status}`, status);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function makeRequestor(plainContextString, serviceEndpoints, paths, requests, encoding, baseHeaders, baseQueryParams, withReasons, useReport, secureModeHash) {
|
|
61
|
+
let body;
|
|
62
|
+
let method = 'GET';
|
|
63
|
+
const headers = { ...baseHeaders };
|
|
64
|
+
if (useReport) {
|
|
65
|
+
method = 'REPORT';
|
|
66
|
+
headers['content-type'] = 'application/json';
|
|
67
|
+
body = plainContextString; // context is in body for REPORT
|
|
68
|
+
}
|
|
69
|
+
const path = useReport
|
|
70
|
+
? paths.pathReport(encoding, plainContextString)
|
|
71
|
+
: paths.pathGet(encoding, plainContextString);
|
|
72
|
+
const parameters = [...(baseQueryParams ?? [])];
|
|
73
|
+
if (withReasons) {
|
|
74
|
+
parameters.push({ key: 'withReasons', value: 'true' });
|
|
75
|
+
}
|
|
76
|
+
if (secureModeHash) {
|
|
77
|
+
parameters.push({ key: 'h', value: secureModeHash });
|
|
78
|
+
}
|
|
79
|
+
const uri = getPollingUri(serviceEndpoints, path, parameters);
|
|
80
|
+
return new Requestor(requests, uri, headers, method, body);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// eslint-disable-next-line max-classes-per-file
|
|
84
|
+
const validators = {
|
|
85
|
+
logger: TypeValidators.Object,
|
|
86
|
+
maxCachedContexts: TypeValidators.numberWithMin(0),
|
|
87
|
+
baseUri: TypeValidators.String,
|
|
88
|
+
streamUri: TypeValidators.String,
|
|
89
|
+
eventsUri: TypeValidators.String,
|
|
90
|
+
capacity: TypeValidators.numberWithMin(1),
|
|
91
|
+
diagnosticRecordingInterval: TypeValidators.numberWithMin(2),
|
|
92
|
+
flushInterval: TypeValidators.numberWithMin(2),
|
|
93
|
+
streamInitialReconnectDelay: TypeValidators.numberWithMin(0),
|
|
94
|
+
allAttributesPrivate: TypeValidators.Boolean,
|
|
95
|
+
debug: TypeValidators.Boolean,
|
|
96
|
+
diagnosticOptOut: TypeValidators.Boolean,
|
|
97
|
+
withReasons: TypeValidators.Boolean,
|
|
98
|
+
sendEvents: TypeValidators.Boolean,
|
|
99
|
+
pollInterval: TypeValidators.numberWithMin(30),
|
|
100
|
+
useReport: TypeValidators.Boolean,
|
|
101
|
+
privateAttributes: TypeValidators.StringArray,
|
|
102
|
+
applicationInfo: TypeValidators.Object,
|
|
103
|
+
wrapperName: TypeValidators.String,
|
|
104
|
+
wrapperVersion: TypeValidators.String,
|
|
105
|
+
payloadFilterKey: TypeValidators.stringMatchingRegex(/^[a-zA-Z0-9](\w|\.|-)*$/),
|
|
106
|
+
hooks: TypeValidators.createTypeArray('Hook[]', {}),
|
|
107
|
+
inspectors: TypeValidators.createTypeArray('LDInspection', {}),
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const DEFAULT_POLLING_INTERVAL = 60 * 5;
|
|
111
|
+
const DEFAULT_POLLING = 'https://clientsdk.launchdarkly.com';
|
|
112
|
+
const DEFAULT_STREAM = 'https://clientstream.launchdarkly.com';
|
|
113
|
+
function ensureSafeLogger(logger) {
|
|
114
|
+
if (logger instanceof SafeLogger) {
|
|
115
|
+
return logger;
|
|
116
|
+
}
|
|
117
|
+
// Even if logger is not defined this will produce a valid logger.
|
|
118
|
+
return createSafeLogger(logger);
|
|
119
|
+
}
|
|
120
|
+
class ConfigurationImpl {
|
|
121
|
+
constructor(pristineOptions = {}, internalOptions = {}) {
|
|
122
|
+
this.logger = createSafeLogger();
|
|
123
|
+
// Naming conventions is not followed for these lines because the config validation
|
|
124
|
+
// accesses members based on the keys of the options. (sdk-763)
|
|
125
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
126
|
+
this.baseUri = DEFAULT_POLLING;
|
|
127
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
128
|
+
this.eventsUri = ServiceEndpoints.DEFAULT_EVENTS;
|
|
129
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
130
|
+
this.streamUri = DEFAULT_STREAM;
|
|
131
|
+
this.maxCachedContexts = 5;
|
|
132
|
+
this.capacity = 100;
|
|
133
|
+
this.diagnosticRecordingInterval = 900;
|
|
134
|
+
this.flushInterval = 30;
|
|
135
|
+
this.streamInitialReconnectDelay = 1;
|
|
136
|
+
this.allAttributesPrivate = false;
|
|
137
|
+
this.debug = false;
|
|
138
|
+
this.diagnosticOptOut = false;
|
|
139
|
+
this.sendEvents = true;
|
|
140
|
+
this.sendLDHeaders = true;
|
|
141
|
+
this.useReport = false;
|
|
142
|
+
this.withReasons = false;
|
|
143
|
+
this.privateAttributes = [];
|
|
144
|
+
this.pollInterval = DEFAULT_POLLING_INTERVAL;
|
|
145
|
+
this.hooks = [];
|
|
146
|
+
this.inspectors = [];
|
|
147
|
+
this.logger = ensureSafeLogger(pristineOptions.logger);
|
|
148
|
+
const errors = this._validateTypesAndNames(pristineOptions);
|
|
149
|
+
errors.forEach((e) => this.logger.warn(e));
|
|
150
|
+
this.serviceEndpoints = new ServiceEndpoints(this.streamUri, this.baseUri, this.eventsUri, internalOptions.analyticsEventPath, internalOptions.diagnosticEventPath, internalOptions.includeAuthorizationHeader, pristineOptions.payloadFilterKey);
|
|
151
|
+
this.useReport = pristineOptions.useReport ?? false;
|
|
152
|
+
this.tags = new ApplicationTags({ application: this.applicationInfo, logger: this.logger });
|
|
153
|
+
this.userAgentHeaderName = internalOptions.userAgentHeaderName ?? 'user-agent';
|
|
154
|
+
this.trackEventModifier = internalOptions.trackEventModifier ?? ((event) => event);
|
|
155
|
+
}
|
|
156
|
+
_validateTypesAndNames(pristineOptions) {
|
|
157
|
+
const errors = [];
|
|
158
|
+
Object.entries(pristineOptions).forEach(([k, v]) => {
|
|
159
|
+
const validator = validators[k];
|
|
160
|
+
if (validator) {
|
|
161
|
+
if (!validator.is(v)) {
|
|
162
|
+
const validatorType = validator.getType();
|
|
163
|
+
if (validatorType === 'boolean') {
|
|
164
|
+
errors.push(OptionMessages.wrongOptionTypeBoolean(k, typeof v));
|
|
165
|
+
this[k] = !!v;
|
|
166
|
+
}
|
|
167
|
+
else if (validatorType === 'boolean | undefined | null') {
|
|
168
|
+
errors.push(OptionMessages.wrongOptionTypeBoolean(k, typeof v));
|
|
169
|
+
if (typeof v !== 'boolean' && typeof v !== 'undefined' && v !== null) {
|
|
170
|
+
this[k] = !!v;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
else if (validator instanceof NumberWithMinimum && TypeValidators.Number.is(v)) {
|
|
174
|
+
const { min } = validator;
|
|
175
|
+
errors.push(OptionMessages.optionBelowMinimum(k, v, min));
|
|
176
|
+
this[k] = min;
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
errors.push(OptionMessages.wrongOptionType(k, validator.getType(), typeof v));
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
else if (k === 'logger') ;
|
|
183
|
+
else {
|
|
184
|
+
// if an option is explicitly null, coerce to undefined
|
|
185
|
+
this[k] = v ?? undefined;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
errors.push(OptionMessages.unknownOption(k));
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
return errors;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async function digest(hasher, encoding) {
|
|
197
|
+
if (hasher.digest) {
|
|
198
|
+
return hasher.digest(encoding);
|
|
199
|
+
}
|
|
200
|
+
if (hasher.asyncDigest) {
|
|
201
|
+
return hasher.asyncDigest(encoding);
|
|
202
|
+
}
|
|
203
|
+
// This represents an error in platform implementation.
|
|
204
|
+
throw new Error('Platform must implement digest or asyncDigest');
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* This function will retrieve a previously generated key for the given {@link storageKey} if it
|
|
209
|
+
* exists or generate and store one on the fly if it does not already exist.
|
|
210
|
+
* @param storageKey keyed storage location where the generated key should live. See {@link namespaceForGeneratedContextKey}
|
|
211
|
+
* for related exmaples of generating a storage key and usage.
|
|
212
|
+
* @param platform crypto and storage implementations for necessary operations
|
|
213
|
+
* @returns the generated key
|
|
214
|
+
*/
|
|
215
|
+
const getOrGenerateKey = async (storageKey, { crypto, storage }) => {
|
|
216
|
+
let generatedKey = await storage?.get(storageKey);
|
|
217
|
+
if (!generatedKey) {
|
|
218
|
+
generatedKey = crypto.randomUUID();
|
|
219
|
+
await storage?.set(storageKey, generatedKey);
|
|
220
|
+
}
|
|
221
|
+
return generatedKey;
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Hashes the input and encodes it as base64
|
|
226
|
+
*/
|
|
227
|
+
function hashAndBase64Encode(crypto) {
|
|
228
|
+
return async (input) => digest(crypto.createHash('sha256').update(input), 'base64');
|
|
229
|
+
}
|
|
230
|
+
const noop = async (input) => input; // no-op transform
|
|
231
|
+
async function concatNamespacesAndValues(parts) {
|
|
232
|
+
const processedParts = await Promise.all(parts.map((part) => part.transform(part.value))); // use the transform from each part to transform the value
|
|
233
|
+
return processedParts.join('_');
|
|
234
|
+
}
|
|
235
|
+
async function namespaceForEnvironment(crypto, sdkKey) {
|
|
236
|
+
return concatNamespacesAndValues([
|
|
237
|
+
{ value: 'LaunchDarkly', transform: noop },
|
|
238
|
+
{ value: sdkKey, transform: hashAndBase64Encode(crypto) }, // hash sdk key and encode it
|
|
239
|
+
]);
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* @deprecated prefer {@link namespaceForGeneratedContextKey}. At one time we only generated keys for
|
|
243
|
+
* anonymous contexts and they were namespaced in LaunchDarkly_AnonymousKeys. Eventually we started
|
|
244
|
+
* generating context keys for non-anonymous contexts such as for the Auto Environment Attributes
|
|
245
|
+
* feature and those were namespaced in LaunchDarkly_ContextKeys. This function can be removed
|
|
246
|
+
* when the data under the LaunchDarkly_AnonymousKeys namespace is merged with data under the
|
|
247
|
+
* LaunchDarkly_ContextKeys namespace.
|
|
248
|
+
*/
|
|
249
|
+
async function namespaceForAnonymousGeneratedContextKey(kind) {
|
|
250
|
+
return concatNamespacesAndValues([
|
|
251
|
+
{ value: 'LaunchDarkly', transform: noop },
|
|
252
|
+
{ value: 'AnonymousKeys', transform: noop },
|
|
253
|
+
{ value: kind, transform: noop }, // existing SDKs are not hashing or encoding this kind, though they should have
|
|
254
|
+
]);
|
|
255
|
+
}
|
|
256
|
+
async function namespaceForGeneratedContextKey(kind) {
|
|
257
|
+
return concatNamespacesAndValues([
|
|
258
|
+
{ value: 'LaunchDarkly', transform: noop },
|
|
259
|
+
{ value: 'ContextKeys', transform: noop },
|
|
260
|
+
{ value: kind, transform: noop }, // existing SDKs are not hashing or encoding this kind, though they should have
|
|
261
|
+
]);
|
|
262
|
+
}
|
|
263
|
+
async function namespaceForContextIndex(environmentNamespace) {
|
|
264
|
+
return concatNamespacesAndValues([
|
|
265
|
+
{ value: environmentNamespace, transform: noop },
|
|
266
|
+
{ value: 'ContextIndex', transform: noop },
|
|
267
|
+
]);
|
|
268
|
+
}
|
|
269
|
+
async function namespaceForContextData(crypto, environmentNamespace, context) {
|
|
270
|
+
return concatNamespacesAndValues([
|
|
271
|
+
{ value: environmentNamespace, transform: noop },
|
|
272
|
+
{ value: context.canonicalKey, transform: hashAndBase64Encode(crypto) }, // hash and encode canonical key
|
|
273
|
+
]);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/* eslint-disable @typescript-eslint/naming-convention */
|
|
277
|
+
const { isLegacyUser: isLegacyUser$1, isSingleKind: isSingleKind$1, isMultiKind: isMultiKind$1 } = internal;
|
|
278
|
+
const defaultAutoEnvSchemaVersion = '1.0';
|
|
279
|
+
const toMulti = (c) => {
|
|
280
|
+
const { kind, ...contextCommon } = c;
|
|
281
|
+
return {
|
|
282
|
+
kind: 'multi',
|
|
283
|
+
[kind]: contextCommon,
|
|
284
|
+
};
|
|
285
|
+
};
|
|
286
|
+
/**
|
|
287
|
+
* Clones the LDApplication object and populates the key, envAttributesVersion, id and version fields.
|
|
288
|
+
*
|
|
289
|
+
* @param crypto
|
|
290
|
+
* @param info
|
|
291
|
+
* @param applicationInfo
|
|
292
|
+
* @param config
|
|
293
|
+
* @return An LDApplication object with populated key, envAttributesVersion, id and version.
|
|
294
|
+
*/
|
|
295
|
+
const addApplicationInfo = async ({ crypto, info }, { applicationInfo }) => {
|
|
296
|
+
const { ld_application } = info.platformData();
|
|
297
|
+
let app = deepCompact(ld_application) ?? {};
|
|
298
|
+
const id = applicationInfo?.id || app?.id;
|
|
299
|
+
if (id) {
|
|
300
|
+
const version = applicationInfo?.version || app?.version;
|
|
301
|
+
const name = applicationInfo?.name || app?.name;
|
|
302
|
+
const versionName = applicationInfo?.versionName || app?.versionName;
|
|
303
|
+
app = {
|
|
304
|
+
...app,
|
|
305
|
+
id,
|
|
306
|
+
// only add props if they are defined
|
|
307
|
+
...(version ? { version } : {}),
|
|
308
|
+
...(name ? { name } : {}),
|
|
309
|
+
...(versionName ? { versionName } : {}),
|
|
310
|
+
};
|
|
311
|
+
app.key = await digest(crypto.createHash('sha256').update(id), 'base64');
|
|
312
|
+
app.envAttributesVersion = app.envAttributesVersion || defaultAutoEnvSchemaVersion;
|
|
313
|
+
return app;
|
|
314
|
+
}
|
|
315
|
+
return undefined;
|
|
316
|
+
};
|
|
317
|
+
/**
|
|
318
|
+
* Clones the LDDevice object and populates the key and envAttributesVersion field.
|
|
319
|
+
*
|
|
320
|
+
* @param platform
|
|
321
|
+
* @return An LDDevice object with populated key and envAttributesVersion.
|
|
322
|
+
*/
|
|
323
|
+
const addDeviceInfo = async (platform) => {
|
|
324
|
+
const { ld_device, os } = platform.info.platformData();
|
|
325
|
+
const device = deepCompact(ld_device) ?? {};
|
|
326
|
+
const name = os?.name || device.os?.name;
|
|
327
|
+
const version = os?.version || device.os?.version;
|
|
328
|
+
const family = device.os?.family;
|
|
329
|
+
// only add device.os if there's data
|
|
330
|
+
if (name || version || family) {
|
|
331
|
+
device.os = {
|
|
332
|
+
// only add props if they are defined
|
|
333
|
+
...(name ? { name } : {}),
|
|
334
|
+
...(version ? { version } : {}),
|
|
335
|
+
...(family ? { family } : {}),
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
// Check if device has any meaningful data before we return it.
|
|
339
|
+
if (Object.keys(device).filter((k) => k !== 'key' && k !== 'envAttributesVersion').length) {
|
|
340
|
+
const ldDeviceNamespace = await namespaceForGeneratedContextKey('ld_device');
|
|
341
|
+
device.key = await getOrGenerateKey(ldDeviceNamespace, platform);
|
|
342
|
+
device.envAttributesVersion = device.envAttributesVersion || defaultAutoEnvSchemaVersion;
|
|
343
|
+
return device;
|
|
344
|
+
}
|
|
345
|
+
return undefined;
|
|
346
|
+
};
|
|
347
|
+
const addAutoEnv = async (context, platform, config) => {
|
|
348
|
+
// LDUser is not supported for auto env reporting
|
|
349
|
+
if (isLegacyUser$1(context)) {
|
|
350
|
+
return context;
|
|
351
|
+
}
|
|
352
|
+
let ld_application;
|
|
353
|
+
let ld_device;
|
|
354
|
+
// Check if customer contexts exist. Only override if they are not provided.
|
|
355
|
+
if ((isSingleKind$1(context) && context.kind !== 'ld_application') ||
|
|
356
|
+
(isMultiKind$1(context) && !context.ld_application)) {
|
|
357
|
+
ld_application = await addApplicationInfo(platform, config);
|
|
358
|
+
}
|
|
359
|
+
else {
|
|
360
|
+
config.logger.warn('Not adding ld_application environment attributes because it already exists.');
|
|
361
|
+
}
|
|
362
|
+
if ((isSingleKind$1(context) && context.kind !== 'ld_device') ||
|
|
363
|
+
(isMultiKind$1(context) && !context.ld_device)) {
|
|
364
|
+
ld_device = await addDeviceInfo(platform);
|
|
365
|
+
}
|
|
366
|
+
else {
|
|
367
|
+
config.logger.warn('Not adding ld_device environment attributes because it already exists.');
|
|
368
|
+
}
|
|
369
|
+
// Unable to automatically add environment attributes for kind: {}. {} already exists.
|
|
370
|
+
if (ld_application || ld_device) {
|
|
371
|
+
const multi = isSingleKind$1(context) ? toMulti(context) : context;
|
|
372
|
+
return {
|
|
373
|
+
...multi,
|
|
374
|
+
...(ld_application ? { ld_application } : {}),
|
|
375
|
+
...(ld_device ? { ld_device } : {}),
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
return context;
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
const { isLegacyUser, isMultiKind, isSingleKind } = internal;
|
|
382
|
+
/**
|
|
383
|
+
* This is the root ensureKey function. All other ensureKey functions reduce to this.
|
|
384
|
+
*
|
|
385
|
+
* - ensureKeyCommon // private root function
|
|
386
|
+
* - ensureKeySingle
|
|
387
|
+
* - ensureKeyMulti
|
|
388
|
+
* - ensureKeyLegacy
|
|
389
|
+
* - ensureKey // exported for external use
|
|
390
|
+
*
|
|
391
|
+
* @param kind The LDContext kind
|
|
392
|
+
* @param c The LDContext object
|
|
393
|
+
* @param platform Platform containing crypto and storage needed for storing and querying keys.
|
|
394
|
+
*/
|
|
395
|
+
const ensureKeyCommon = async (kind, c, platform) => {
|
|
396
|
+
const { anonymous, key } = c;
|
|
397
|
+
if (anonymous && !key) {
|
|
398
|
+
const storageKey = await namespaceForAnonymousGeneratedContextKey(kind);
|
|
399
|
+
// This mutates a cloned copy of the original context from ensureyKey so this is safe.
|
|
400
|
+
// eslint-disable-next-line no-param-reassign
|
|
401
|
+
c.key = await getOrGenerateKey(storageKey, platform);
|
|
402
|
+
}
|
|
403
|
+
};
|
|
404
|
+
const ensureKeySingle = async (c, platform) => {
|
|
405
|
+
await ensureKeyCommon(c.kind, c, platform);
|
|
406
|
+
};
|
|
407
|
+
const ensureKeyMulti = async (multiContext, platform) => {
|
|
408
|
+
const { kind, ...singleContexts } = multiContext;
|
|
409
|
+
return Promise.all(Object.entries(singleContexts).map(([k, c]) => ensureKeyCommon(k, c, platform)));
|
|
410
|
+
};
|
|
411
|
+
const ensureKeyLegacy = async (c, platform) => {
|
|
412
|
+
await ensureKeyCommon('user', c, platform);
|
|
413
|
+
};
|
|
414
|
+
/**
|
|
415
|
+
* Ensure a key is always present in anonymous contexts. Non-anonymous contexts
|
|
416
|
+
* are not processed and will just be returned as is.
|
|
417
|
+
*
|
|
418
|
+
* @param context
|
|
419
|
+
* @param platform
|
|
420
|
+
*/
|
|
421
|
+
const ensureKey = async (context, platform) => {
|
|
422
|
+
const cloned = clone(context);
|
|
423
|
+
if (isSingleKind(cloned)) {
|
|
424
|
+
await ensureKeySingle(cloned, platform);
|
|
425
|
+
}
|
|
426
|
+
if (isMultiKind(cloned)) {
|
|
427
|
+
await ensureKeyMulti(cloned, platform);
|
|
428
|
+
}
|
|
429
|
+
if (isLegacyUser(cloned)) {
|
|
430
|
+
await ensureKeyLegacy(cloned, platform);
|
|
431
|
+
}
|
|
432
|
+
return cloned;
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
const createDiagnosticsInitConfig = (config) => ({
|
|
436
|
+
customBaseURI: config.serviceEndpoints.polling !== DEFAULT_POLLING,
|
|
437
|
+
customStreamURI: config.serviceEndpoints.streaming !== DEFAULT_STREAM,
|
|
438
|
+
customEventsURI: config.serviceEndpoints.events !== ServiceEndpoints.DEFAULT_EVENTS,
|
|
439
|
+
eventsCapacity: config.capacity,
|
|
440
|
+
eventsFlushIntervalMillis: secondsToMillis(config.flushInterval),
|
|
441
|
+
reconnectTimeMillis: secondsToMillis(config.streamInitialReconnectDelay),
|
|
442
|
+
diagnosticRecordingIntervalMillis: secondsToMillis(config.diagnosticRecordingInterval),
|
|
443
|
+
allAttributesPrivate: config.allAttributesPrivate,
|
|
444
|
+
// TODO: Implement when corresponding features are implemented.
|
|
445
|
+
usingSecureMode: false,
|
|
446
|
+
bootstrapMode: false,
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
const createDiagnosticsManager = (clientSideID, config, platform) => {
|
|
450
|
+
if (config.sendEvents && !config.diagnosticOptOut) {
|
|
451
|
+
return new internal.DiagnosticsManager(clientSideID, platform, createDiagnosticsInitConfig(config));
|
|
452
|
+
}
|
|
453
|
+
return undefined;
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
function createErrorEvaluationDetail(errorKind, def) {
|
|
457
|
+
return {
|
|
458
|
+
value: def ?? null,
|
|
459
|
+
variationIndex: null,
|
|
460
|
+
reason: { kind: 'ERROR', errorKind },
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
function createSuccessEvaluationDetail(value, variationIndex, reason) {
|
|
464
|
+
const res = {
|
|
465
|
+
value,
|
|
466
|
+
variationIndex: variationIndex ?? null,
|
|
467
|
+
reason: reason ?? null,
|
|
468
|
+
};
|
|
469
|
+
return res;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const createEventProcessor = (clientSideID, config, platform, baseHeaders, diagnosticsManager) => {
|
|
473
|
+
if (config.sendEvents) {
|
|
474
|
+
return new internal.EventProcessor({ ...config, eventsCapacity: config.capacity }, new ClientContext(clientSideID, config, platform), baseHeaders, undefined, diagnosticsManager, false);
|
|
475
|
+
}
|
|
476
|
+
return undefined;
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* @internal
|
|
481
|
+
*/
|
|
482
|
+
class EventFactory extends internal.EventFactoryBase {
|
|
483
|
+
evalEventClient(flagKey, value, defaultVal, flag, context, reason) {
|
|
484
|
+
const { trackEvents, debugEventsUntilDate, trackReason, flagVersion, version, variation } = flag;
|
|
485
|
+
return super.evalEvent({
|
|
486
|
+
addExperimentData: trackReason,
|
|
487
|
+
context,
|
|
488
|
+
debugEventsUntilDate,
|
|
489
|
+
defaultVal,
|
|
490
|
+
flagKey,
|
|
491
|
+
reason,
|
|
492
|
+
trackEvents: !!trackEvents,
|
|
493
|
+
value,
|
|
494
|
+
variation,
|
|
495
|
+
version: flagVersion ?? version,
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* An index for tracking the most recently used contexts by timestamp with the ability to
|
|
502
|
+
* update entry timestamps and prune out least used contexts above a max capacity provided.
|
|
503
|
+
*/
|
|
504
|
+
class ContextIndex {
|
|
505
|
+
constructor() {
|
|
506
|
+
this.container = { index: new Array() };
|
|
507
|
+
}
|
|
508
|
+
/**
|
|
509
|
+
* Creates a {@link ContextIndex} from its JSON representation (likely retrieved from persistence).
|
|
510
|
+
* @param json representation of the {@link ContextIndex}
|
|
511
|
+
* @returns the {@link ContextIndex}
|
|
512
|
+
*/
|
|
513
|
+
static fromJson(json) {
|
|
514
|
+
const contextIndex = new ContextIndex();
|
|
515
|
+
try {
|
|
516
|
+
contextIndex.container = JSON.parse(json);
|
|
517
|
+
}
|
|
518
|
+
catch (e) {
|
|
519
|
+
/* ignoring error and returning empty index */
|
|
520
|
+
}
|
|
521
|
+
return contextIndex;
|
|
522
|
+
}
|
|
523
|
+
/**
|
|
524
|
+
* @returns the JSON representation of the {@link ContextIndex} (like for saving to persistence)
|
|
525
|
+
*/
|
|
526
|
+
toJson() {
|
|
527
|
+
return JSON.stringify(this.container);
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* Notice that a context has been used and when it was used. This will update an existing record
|
|
531
|
+
* with the given timestamp, or create a new record if one doesn't exist.
|
|
532
|
+
* @param id of the corresponding context
|
|
533
|
+
* @param timestamp in millis since epoch
|
|
534
|
+
*/
|
|
535
|
+
notice(id, timestamp) {
|
|
536
|
+
const entry = this.container.index.find((it) => it.id === id);
|
|
537
|
+
if (entry === undefined) {
|
|
538
|
+
this.container.index.push({ id, timestamp });
|
|
539
|
+
}
|
|
540
|
+
else {
|
|
541
|
+
entry.timestamp = timestamp;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* Prune the index to the specified max size and then return the IDs
|
|
546
|
+
* @param maxContexts the maximum number of contexts to retain after this prune
|
|
547
|
+
* @returns an array of removed entries
|
|
548
|
+
*/
|
|
549
|
+
prune(maxContexts) {
|
|
550
|
+
const clampedMax = Math.max(maxContexts, 0); // clamp to [0, infinity)
|
|
551
|
+
if (this.container.index.length > clampedMax) {
|
|
552
|
+
// sort by timestamp so that older timestamps appear first in the array
|
|
553
|
+
this.container.index.sort((a, b) => a.timestamp - b.timestamp);
|
|
554
|
+
// delete the first N many elements above capacity. splice returns removed elements
|
|
555
|
+
return this.container.index.splice(0, this.container.index.length - clampedMax);
|
|
556
|
+
}
|
|
557
|
+
return [];
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* This class handles persisting and loading flag values from a persistent
|
|
563
|
+
* store. It intercepts updates and forwards them to the flag updater and
|
|
564
|
+
* then persists changes after the updater has completed.
|
|
565
|
+
*/
|
|
566
|
+
class FlagPersistence {
|
|
567
|
+
constructor(_platform, _environmentNamespace, _maxCachedContexts, _flagStore, _flagUpdater, _logger, _timeStamper = () => Date.now()) {
|
|
568
|
+
this._platform = _platform;
|
|
569
|
+
this._environmentNamespace = _environmentNamespace;
|
|
570
|
+
this._maxCachedContexts = _maxCachedContexts;
|
|
571
|
+
this._flagStore = _flagStore;
|
|
572
|
+
this._flagUpdater = _flagUpdater;
|
|
573
|
+
this._logger = _logger;
|
|
574
|
+
this._timeStamper = _timeStamper;
|
|
575
|
+
this._indexKeyPromise = namespaceForContextIndex(this._environmentNamespace);
|
|
576
|
+
}
|
|
577
|
+
/**
|
|
578
|
+
* Inits flag persistence for the provided context with the provided flags. This will result
|
|
579
|
+
* in the underlying {@link FlagUpdater} switching its active context.
|
|
580
|
+
*/
|
|
581
|
+
async init(context, newFlags) {
|
|
582
|
+
this._flagUpdater.init(context, newFlags);
|
|
583
|
+
await this._storeCache(context);
|
|
584
|
+
}
|
|
585
|
+
/**
|
|
586
|
+
* Upserts a flag into the {@link FlagUpdater} and stores that to persistence if the upsert
|
|
587
|
+
* was successful / accepted. An upsert may be rejected if the provided context is not
|
|
588
|
+
* the active context.
|
|
589
|
+
*/
|
|
590
|
+
async upsert(context, key, item) {
|
|
591
|
+
if (this._flagUpdater.upsert(context, key, item)) {
|
|
592
|
+
await this._storeCache(context);
|
|
593
|
+
return true;
|
|
594
|
+
}
|
|
595
|
+
return false;
|
|
596
|
+
}
|
|
597
|
+
/**
|
|
598
|
+
* Loads the flags from persistence for the provided context and gives those to the
|
|
599
|
+
* {@link FlagUpdater} this {@link FlagPersistence} was constructed with.
|
|
600
|
+
*/
|
|
601
|
+
async loadCached(context) {
|
|
602
|
+
const storageKey = await namespaceForContextData(this._platform.crypto, this._environmentNamespace, context);
|
|
603
|
+
let flagsJson = await this._platform.storage?.get(storageKey);
|
|
604
|
+
if (flagsJson === null || flagsJson === undefined) {
|
|
605
|
+
// Fallback: in version <10.3.1 flag data was stored under the canonical key, check
|
|
606
|
+
// to see if data is present and migrate the data if present.
|
|
607
|
+
flagsJson = await this._platform.storage?.get(context.canonicalKey);
|
|
608
|
+
if (flagsJson === null || flagsJson === undefined) {
|
|
609
|
+
// return false indicating cache did not load if flag json is still absent
|
|
610
|
+
return false;
|
|
611
|
+
}
|
|
612
|
+
// migrate data from version <10.3.1 and cleanup data that was under canonical key
|
|
613
|
+
await this._platform.storage?.set(storageKey, flagsJson);
|
|
614
|
+
await this._platform.storage?.clear(context.canonicalKey);
|
|
615
|
+
}
|
|
616
|
+
try {
|
|
617
|
+
const flags = JSON.parse(flagsJson);
|
|
618
|
+
// mapping flags to item descriptors
|
|
619
|
+
const descriptors = Object.entries(flags).reduce((acc, [key, flag]) => {
|
|
620
|
+
acc[key] = { version: flag.version, flag };
|
|
621
|
+
return acc;
|
|
622
|
+
}, {});
|
|
623
|
+
this._flagUpdater.initCached(context, descriptors);
|
|
624
|
+
this._logger.debug('Loaded cached flag evaluations from persistent storage');
|
|
625
|
+
return true;
|
|
626
|
+
}
|
|
627
|
+
catch (e) {
|
|
628
|
+
this._logger.warn(`Could not load cached flag evaluations from persistent storage: ${e.message}`);
|
|
629
|
+
return false;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
async _loadIndex() {
|
|
633
|
+
if (this._contextIndex !== undefined) {
|
|
634
|
+
return this._contextIndex;
|
|
635
|
+
}
|
|
636
|
+
const json = await this._platform.storage?.get(await this._indexKeyPromise);
|
|
637
|
+
if (!json) {
|
|
638
|
+
this._contextIndex = new ContextIndex();
|
|
639
|
+
return this._contextIndex;
|
|
640
|
+
}
|
|
641
|
+
try {
|
|
642
|
+
this._contextIndex = ContextIndex.fromJson(json);
|
|
643
|
+
this._logger.debug('Loaded context index from persistent storage');
|
|
644
|
+
}
|
|
645
|
+
catch (e) {
|
|
646
|
+
this._logger.warn(`Could not load index from persistent storage: ${e.message}`);
|
|
647
|
+
this._contextIndex = new ContextIndex();
|
|
648
|
+
}
|
|
649
|
+
return this._contextIndex;
|
|
650
|
+
}
|
|
651
|
+
async _storeCache(context) {
|
|
652
|
+
const index = await this._loadIndex();
|
|
653
|
+
const storageKey = await namespaceForContextData(this._platform.crypto, this._environmentNamespace, context);
|
|
654
|
+
index.notice(storageKey, this._timeStamper());
|
|
655
|
+
const pruned = index.prune(this._maxCachedContexts);
|
|
656
|
+
await Promise.all(pruned.map(async (it) => this._platform.storage?.clear(it.id)));
|
|
657
|
+
// store index
|
|
658
|
+
await this._platform.storage?.set(await this._indexKeyPromise, index.toJson());
|
|
659
|
+
const allFlags = this._flagStore.getAll();
|
|
660
|
+
// mapping item descriptors to flags
|
|
661
|
+
const flags = Object.entries(allFlags).reduce((acc, [key, descriptor]) => {
|
|
662
|
+
if (descriptor.flag !== null && descriptor.flag !== undefined) {
|
|
663
|
+
acc[key] = descriptor.flag;
|
|
664
|
+
}
|
|
665
|
+
return acc;
|
|
666
|
+
}, {});
|
|
667
|
+
const jsonAll = JSON.stringify(flags);
|
|
668
|
+
// store flag data
|
|
669
|
+
await this._platform.storage?.set(storageKey, jsonAll);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/**
|
|
674
|
+
* In memory flag store.
|
|
675
|
+
*/
|
|
676
|
+
class DefaultFlagStore {
|
|
677
|
+
constructor() {
|
|
678
|
+
this._flags = {};
|
|
679
|
+
}
|
|
680
|
+
init(newFlags) {
|
|
681
|
+
this._flags = Object.entries(newFlags).reduce((acc, [key, flag]) => {
|
|
682
|
+
acc[key] = flag;
|
|
683
|
+
return acc;
|
|
684
|
+
}, {});
|
|
685
|
+
}
|
|
686
|
+
insertOrUpdate(key, update) {
|
|
687
|
+
this._flags[key] = update;
|
|
688
|
+
}
|
|
689
|
+
get(key) {
|
|
690
|
+
if (Object.prototype.hasOwnProperty.call(this._flags, key)) {
|
|
691
|
+
return this._flags[key];
|
|
692
|
+
}
|
|
693
|
+
return undefined;
|
|
694
|
+
}
|
|
695
|
+
getAll() {
|
|
696
|
+
return this._flags;
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
function calculateChangedKeys(existingObject, newObject) {
|
|
701
|
+
const changedKeys = [];
|
|
702
|
+
// property deleted or updated
|
|
703
|
+
Object.entries(existingObject).forEach(([k, f]) => {
|
|
704
|
+
const subObject = newObject[k];
|
|
705
|
+
if (!subObject || !fastDeepEqual(f, subObject)) {
|
|
706
|
+
changedKeys.push(k);
|
|
707
|
+
}
|
|
708
|
+
});
|
|
709
|
+
// property added
|
|
710
|
+
Object.keys(newObject).forEach((k) => {
|
|
711
|
+
if (!existingObject[k]) {
|
|
712
|
+
changedKeys.push(k);
|
|
713
|
+
}
|
|
714
|
+
});
|
|
715
|
+
return changedKeys;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
/**
|
|
719
|
+
* The flag updater handles logic required during the flag update process.
|
|
720
|
+
* It handles versions checking to handle out of order flag updates and
|
|
721
|
+
* also handles flag comparisons for change notification.
|
|
722
|
+
*/
|
|
723
|
+
class FlagUpdater {
|
|
724
|
+
constructor(flagStore, logger) {
|
|
725
|
+
this._changeCallbacks = new Array();
|
|
726
|
+
this._flagStore = flagStore;
|
|
727
|
+
this._logger = logger;
|
|
728
|
+
}
|
|
729
|
+
init(context, newFlags) {
|
|
730
|
+
this._activeContextKey = context.canonicalKey;
|
|
731
|
+
const oldFlags = this._flagStore.getAll();
|
|
732
|
+
this._flagStore.init(newFlags);
|
|
733
|
+
const changed = calculateChangedKeys(oldFlags, newFlags);
|
|
734
|
+
if (changed.length > 0) {
|
|
735
|
+
this._changeCallbacks.forEach((callback) => {
|
|
736
|
+
try {
|
|
737
|
+
callback(context, changed, 'init');
|
|
738
|
+
}
|
|
739
|
+
catch (err) {
|
|
740
|
+
/* intentionally empty */
|
|
741
|
+
}
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
initCached(context, newFlags) {
|
|
746
|
+
if (this._activeContextKey === context.canonicalKey) {
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
this.init(context, newFlags);
|
|
750
|
+
}
|
|
751
|
+
upsert(context, key, item) {
|
|
752
|
+
if (this._activeContextKey !== context.canonicalKey) {
|
|
753
|
+
this._logger.warn('Received an update for an inactive context.');
|
|
754
|
+
return false;
|
|
755
|
+
}
|
|
756
|
+
const currentValue = this._flagStore.get(key);
|
|
757
|
+
if (currentValue !== undefined && currentValue.version >= item.version) {
|
|
758
|
+
// this is an out of order update that can be ignored
|
|
759
|
+
return false;
|
|
760
|
+
}
|
|
761
|
+
this._flagStore.insertOrUpdate(key, item);
|
|
762
|
+
this._changeCallbacks.forEach((callback) => {
|
|
763
|
+
try {
|
|
764
|
+
callback(context, [key], 'patch');
|
|
765
|
+
}
|
|
766
|
+
catch (err) {
|
|
767
|
+
/* intentionally empty */
|
|
768
|
+
}
|
|
769
|
+
});
|
|
770
|
+
return true;
|
|
771
|
+
}
|
|
772
|
+
on(callback) {
|
|
773
|
+
this._changeCallbacks.push(callback);
|
|
774
|
+
}
|
|
775
|
+
off(callback) {
|
|
776
|
+
const index = this._changeCallbacks.indexOf(callback);
|
|
777
|
+
if (index > -1) {
|
|
778
|
+
this._changeCallbacks.splice(index, 1);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
class DefaultFlagManager {
|
|
784
|
+
/**
|
|
785
|
+
* @param platform implementation of various platform provided functionality
|
|
786
|
+
* @param sdkKey that will be used to distinguish different environments
|
|
787
|
+
* @param maxCachedContexts that specifies the max number of contexts that will be cached in persistence
|
|
788
|
+
* @param logger used for logging various messages
|
|
789
|
+
* @param timeStamper exists for testing purposes
|
|
790
|
+
*/
|
|
791
|
+
constructor(platform, sdkKey, maxCachedContexts, logger, timeStamper = () => Date.now()) {
|
|
792
|
+
this._flagStore = new DefaultFlagStore();
|
|
793
|
+
this._flagUpdater = new FlagUpdater(this._flagStore, logger);
|
|
794
|
+
this._flagPersistencePromise = this._initPersistence(platform, sdkKey, maxCachedContexts, logger, timeStamper);
|
|
795
|
+
}
|
|
796
|
+
async _initPersistence(platform, sdkKey, maxCachedContexts, logger, timeStamper = () => Date.now()) {
|
|
797
|
+
const environmentNamespace = await namespaceForEnvironment(platform.crypto, sdkKey);
|
|
798
|
+
return new FlagPersistence(platform, environmentNamespace, maxCachedContexts, this._flagStore, this._flagUpdater, logger, timeStamper);
|
|
799
|
+
}
|
|
800
|
+
get(key) {
|
|
801
|
+
return this._flagStore.get(key);
|
|
802
|
+
}
|
|
803
|
+
getAll() {
|
|
804
|
+
return this._flagStore.getAll();
|
|
805
|
+
}
|
|
806
|
+
setBootstrap(context, newFlags) {
|
|
807
|
+
// Bypasses the persistence as we do not want to put these flags into any cache.
|
|
808
|
+
// Generally speaking persistence likely *SHOULD* be disabled when using bootstrap.
|
|
809
|
+
this._flagUpdater.init(context, newFlags);
|
|
810
|
+
}
|
|
811
|
+
async init(context, newFlags) {
|
|
812
|
+
return (await this._flagPersistencePromise).init(context, newFlags);
|
|
813
|
+
}
|
|
814
|
+
async upsert(context, key, item) {
|
|
815
|
+
return (await this._flagPersistencePromise).upsert(context, key, item);
|
|
816
|
+
}
|
|
817
|
+
async loadCached(context) {
|
|
818
|
+
return (await this._flagPersistencePromise).loadCached(context);
|
|
819
|
+
}
|
|
820
|
+
on(callback) {
|
|
821
|
+
this._flagUpdater.on(callback);
|
|
822
|
+
}
|
|
823
|
+
off(callback) {
|
|
824
|
+
this._flagUpdater.off(callback);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
const UNKNOWN_HOOK_NAME = 'unknown hook';
|
|
829
|
+
const BEFORE_EVALUATION_STAGE_NAME = 'beforeEvaluation';
|
|
830
|
+
const AFTER_EVALUATION_STAGE_NAME = 'afterEvaluation';
|
|
831
|
+
function tryExecuteStage(logger, method, hookName, stage, def) {
|
|
832
|
+
try {
|
|
833
|
+
return stage();
|
|
834
|
+
}
|
|
835
|
+
catch (err) {
|
|
836
|
+
logger?.error(`An error was encountered in "${method}" of the "${hookName}" hook: ${err}`);
|
|
837
|
+
return def;
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
function getHookName(logger, hook) {
|
|
841
|
+
try {
|
|
842
|
+
return hook.getMetadata().name || UNKNOWN_HOOK_NAME;
|
|
843
|
+
}
|
|
844
|
+
catch {
|
|
845
|
+
logger.error(`Exception thrown getting metadata for hook. Unable to get hook name.`);
|
|
846
|
+
return UNKNOWN_HOOK_NAME;
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
function executeBeforeEvaluation(logger, hooks, hookContext) {
|
|
850
|
+
return hooks.map((hook) => tryExecuteStage(logger, BEFORE_EVALUATION_STAGE_NAME, getHookName(logger, hook), () => hook?.beforeEvaluation?.(hookContext, {}) ?? {}, {}));
|
|
851
|
+
}
|
|
852
|
+
function executeAfterEvaluation(logger, hooks, hookContext, updatedData, result) {
|
|
853
|
+
// This iterates in reverse, versus reversing a shallow copy of the hooks,
|
|
854
|
+
// for efficiency.
|
|
855
|
+
for (let hookIndex = hooks.length - 1; hookIndex >= 0; hookIndex -= 1) {
|
|
856
|
+
const hook = hooks[hookIndex];
|
|
857
|
+
const data = updatedData[hookIndex];
|
|
858
|
+
tryExecuteStage(logger, AFTER_EVALUATION_STAGE_NAME, getHookName(logger, hook), () => hook?.afterEvaluation?.(hookContext, data, result) ?? {}, {});
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
function executeBeforeIdentify(logger, hooks, hookContext) {
|
|
862
|
+
return hooks.map((hook) => tryExecuteStage(logger, BEFORE_EVALUATION_STAGE_NAME, getHookName(logger, hook), () => hook?.beforeIdentify?.(hookContext, {}) ?? {}, {}));
|
|
863
|
+
}
|
|
864
|
+
function executeAfterIdentify(logger, hooks, hookContext, updatedData, result) {
|
|
865
|
+
// This iterates in reverse, versus reversing a shallow copy of the hooks,
|
|
866
|
+
// for efficiency.
|
|
867
|
+
for (let hookIndex = hooks.length - 1; hookIndex >= 0; hookIndex -= 1) {
|
|
868
|
+
const hook = hooks[hookIndex];
|
|
869
|
+
const data = updatedData[hookIndex];
|
|
870
|
+
tryExecuteStage(logger, AFTER_EVALUATION_STAGE_NAME, getHookName(logger, hook), () => hook?.afterIdentify?.(hookContext, data, result) ?? {}, {});
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
class HookRunner {
|
|
874
|
+
constructor(_logger, initialHooks) {
|
|
875
|
+
this._logger = _logger;
|
|
876
|
+
this._hooks = [];
|
|
877
|
+
this._hooks.push(...initialHooks);
|
|
878
|
+
}
|
|
879
|
+
withEvaluation(key, context, defaultValue, method) {
|
|
880
|
+
if (this._hooks.length === 0) {
|
|
881
|
+
return method();
|
|
882
|
+
}
|
|
883
|
+
const hooks = [...this._hooks];
|
|
884
|
+
const hookContext = {
|
|
885
|
+
flagKey: key,
|
|
886
|
+
context,
|
|
887
|
+
defaultValue,
|
|
888
|
+
};
|
|
889
|
+
const hookData = executeBeforeEvaluation(this._logger, hooks, hookContext);
|
|
890
|
+
const result = method();
|
|
891
|
+
executeAfterEvaluation(this._logger, hooks, hookContext, hookData, result);
|
|
892
|
+
return result;
|
|
893
|
+
}
|
|
894
|
+
identify(context, timeout) {
|
|
895
|
+
const hooks = [...this._hooks];
|
|
896
|
+
const hookContext = {
|
|
897
|
+
context,
|
|
898
|
+
timeout,
|
|
899
|
+
};
|
|
900
|
+
const hookData = executeBeforeIdentify(this._logger, hooks, hookContext);
|
|
901
|
+
return (result) => {
|
|
902
|
+
executeAfterIdentify(this._logger, hooks, hookContext, hookData, result);
|
|
903
|
+
};
|
|
904
|
+
}
|
|
905
|
+
addHook(hook) {
|
|
906
|
+
this._hooks.push(hook);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
function getInspectorHook(inspectorManager) {
|
|
911
|
+
return {
|
|
912
|
+
getMetadata() {
|
|
913
|
+
return {
|
|
914
|
+
name: 'LaunchDarkly-Inspector-Adapter',
|
|
915
|
+
};
|
|
916
|
+
},
|
|
917
|
+
afterEvaluation: (hookContext, data, detail) => {
|
|
918
|
+
inspectorManager.onFlagUsed(hookContext.flagKey, detail, hookContext.context);
|
|
919
|
+
return data;
|
|
920
|
+
},
|
|
921
|
+
afterIdentify(hookContext, data, _result) {
|
|
922
|
+
inspectorManager.onIdentityChanged(hookContext.context);
|
|
923
|
+
return data;
|
|
924
|
+
},
|
|
925
|
+
};
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
function invalidInspector(type, name) {
|
|
929
|
+
return `an inspector: "${name}" of an invalid type (${type}) was configured`;
|
|
930
|
+
}
|
|
931
|
+
function inspectorMethodError(type, name) {
|
|
932
|
+
return `an inspector: "${name}" of type: "${type}" generated an exception`;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
/**
|
|
936
|
+
* Wrap an inspector ensuring that calling its methods are safe.
|
|
937
|
+
* @param inspector Inspector to wrap.
|
|
938
|
+
*/
|
|
939
|
+
function createSafeInspector(inspector, logger) {
|
|
940
|
+
let errorLogged = false;
|
|
941
|
+
const wrapper = {
|
|
942
|
+
method: (...args) => {
|
|
943
|
+
try {
|
|
944
|
+
// We are proxying arguments here to the underlying method. Typescript doesn't care
|
|
945
|
+
// for this as it cannot validate the parameters are correct, but we are also the caller
|
|
946
|
+
// in this case and will dispatch things with the correct arguments. The dispatch to this
|
|
947
|
+
// will itself happen with a type guard.
|
|
948
|
+
// @ts-ignore
|
|
949
|
+
inspector.method(...args);
|
|
950
|
+
}
|
|
951
|
+
catch {
|
|
952
|
+
// If something goes wrong in an inspector we want to log that something
|
|
953
|
+
// went wrong. We don't want to flood the logs, so we only log something
|
|
954
|
+
// the first time that something goes wrong.
|
|
955
|
+
// We do not include the exception in the log, because we do not know what
|
|
956
|
+
// kind of data it may contain.
|
|
957
|
+
if (!errorLogged) {
|
|
958
|
+
errorLogged = true;
|
|
959
|
+
logger.warn(inspectorMethodError(wrapper.type, wrapper.name));
|
|
960
|
+
}
|
|
961
|
+
// Prevent errors.
|
|
962
|
+
}
|
|
963
|
+
},
|
|
964
|
+
type: inspector.type,
|
|
965
|
+
name: inspector.name,
|
|
966
|
+
synchronous: inspector.synchronous,
|
|
967
|
+
};
|
|
968
|
+
return wrapper;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
const FLAG_USED_TYPE = 'flag-used';
|
|
972
|
+
const FLAG_DETAILS_CHANGED_TYPE = 'flag-details-changed';
|
|
973
|
+
const FLAG_DETAIL_CHANGED_TYPE = 'flag-detail-changed';
|
|
974
|
+
const IDENTITY_CHANGED_TYPE = 'client-identity-changed';
|
|
975
|
+
const VALID__TYPES = [
|
|
976
|
+
FLAG_USED_TYPE,
|
|
977
|
+
FLAG_DETAILS_CHANGED_TYPE,
|
|
978
|
+
FLAG_DETAIL_CHANGED_TYPE,
|
|
979
|
+
IDENTITY_CHANGED_TYPE,
|
|
980
|
+
];
|
|
981
|
+
function validateInspector(inspector, logger) {
|
|
982
|
+
const valid = VALID__TYPES.includes(inspector.type) &&
|
|
983
|
+
inspector.method &&
|
|
984
|
+
typeof inspector.method === 'function';
|
|
985
|
+
if (!valid) {
|
|
986
|
+
logger.warn(invalidInspector(inspector.type, inspector.name));
|
|
987
|
+
}
|
|
988
|
+
return valid;
|
|
989
|
+
}
|
|
990
|
+
/**
|
|
991
|
+
* Manages dispatching of inspection data to registered inspectors.
|
|
992
|
+
*/
|
|
993
|
+
class InspectorManager {
|
|
994
|
+
constructor(inspectors, logger) {
|
|
995
|
+
this._safeInspectors = [];
|
|
996
|
+
const validInspectors = inspectors.filter((inspector) => validateInspector(inspector, logger));
|
|
997
|
+
this._safeInspectors = validInspectors.map((inspector) => createSafeInspector(inspector, logger));
|
|
998
|
+
}
|
|
999
|
+
hasInspectors() {
|
|
1000
|
+
return this._safeInspectors.length !== 0;
|
|
1001
|
+
}
|
|
1002
|
+
/**
|
|
1003
|
+
* Notify registered inspectors of a flag being used.
|
|
1004
|
+
*
|
|
1005
|
+
* @param flagKey The key for the flag.
|
|
1006
|
+
* @param detail The LDEvaluationDetail for the flag.
|
|
1007
|
+
* @param context The LDContext for the flag.
|
|
1008
|
+
*/
|
|
1009
|
+
onFlagUsed(flagKey, detail, context) {
|
|
1010
|
+
this._safeInspectors.forEach((inspector) => {
|
|
1011
|
+
if (inspector.type === FLAG_USED_TYPE) {
|
|
1012
|
+
inspector.method(flagKey, detail, context);
|
|
1013
|
+
}
|
|
1014
|
+
});
|
|
1015
|
+
}
|
|
1016
|
+
/**
|
|
1017
|
+
* Notify registered inspectors that the flags have been replaced.
|
|
1018
|
+
*
|
|
1019
|
+
* @param flags The current flags as a Record<string, LDEvaluationDetail>.
|
|
1020
|
+
*/
|
|
1021
|
+
onFlagsChanged(flags) {
|
|
1022
|
+
this._safeInspectors.forEach((inspector) => {
|
|
1023
|
+
if (inspector.type === FLAG_DETAILS_CHANGED_TYPE) {
|
|
1024
|
+
inspector.method(flags);
|
|
1025
|
+
}
|
|
1026
|
+
});
|
|
1027
|
+
}
|
|
1028
|
+
/**
|
|
1029
|
+
* Notify registered inspectors that a flag value has changed.
|
|
1030
|
+
*
|
|
1031
|
+
* @param flagKey The key for the flag that changed.
|
|
1032
|
+
* @param flag An `LDEvaluationDetail` for the flag.
|
|
1033
|
+
*/
|
|
1034
|
+
onFlagChanged(flagKey, flag) {
|
|
1035
|
+
this._safeInspectors.forEach((inspector) => {
|
|
1036
|
+
if (inspector.type === FLAG_DETAIL_CHANGED_TYPE) {
|
|
1037
|
+
inspector.method(flagKey, flag);
|
|
1038
|
+
}
|
|
1039
|
+
});
|
|
1040
|
+
}
|
|
1041
|
+
/**
|
|
1042
|
+
* Notify the registered inspectors that the context identity has changed.
|
|
1043
|
+
*
|
|
1044
|
+
* The notification itself will be dispatched asynchronously.
|
|
1045
|
+
*
|
|
1046
|
+
* @param context The `LDContext` which is now identified.
|
|
1047
|
+
*/
|
|
1048
|
+
onIdentityChanged(context) {
|
|
1049
|
+
this._safeInspectors.forEach((inspector) => {
|
|
1050
|
+
if (inspector.type === IDENTITY_CHANGED_TYPE) {
|
|
1051
|
+
inspector.method(context);
|
|
1052
|
+
}
|
|
1053
|
+
});
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
/**
|
|
1058
|
+
* Implementation Note: There should not be any default listeners for change events in a client
|
|
1059
|
+
* implementation. Default listeners mean a client cannot determine when there are actual
|
|
1060
|
+
* application developer provided listeners. If we require default listeners, then we should add
|
|
1061
|
+
* a system to allow listeners which have counts independent of the primary listener counts.
|
|
1062
|
+
*/
|
|
1063
|
+
class LDEmitter {
|
|
1064
|
+
constructor(_logger) {
|
|
1065
|
+
this._logger = _logger;
|
|
1066
|
+
this._listeners = new Map();
|
|
1067
|
+
}
|
|
1068
|
+
on(name, listener) {
|
|
1069
|
+
if (!this._listeners.has(name)) {
|
|
1070
|
+
this._listeners.set(name, [listener]);
|
|
1071
|
+
}
|
|
1072
|
+
else {
|
|
1073
|
+
this._listeners.get(name)?.push(listener);
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
/**
|
|
1077
|
+
* Unsubscribe one or all events.
|
|
1078
|
+
*
|
|
1079
|
+
* @param name
|
|
1080
|
+
* @param listener Optional. If unspecified, all listeners for the event will be removed.
|
|
1081
|
+
*/
|
|
1082
|
+
off(name, listener) {
|
|
1083
|
+
const existingListeners = this._listeners.get(name);
|
|
1084
|
+
if (!existingListeners) {
|
|
1085
|
+
return;
|
|
1086
|
+
}
|
|
1087
|
+
if (listener) {
|
|
1088
|
+
// remove from internal cache
|
|
1089
|
+
const updated = existingListeners.filter((fn) => fn !== listener);
|
|
1090
|
+
if (updated.length === 0) {
|
|
1091
|
+
this._listeners.delete(name);
|
|
1092
|
+
}
|
|
1093
|
+
else {
|
|
1094
|
+
this._listeners.set(name, updated);
|
|
1095
|
+
}
|
|
1096
|
+
return;
|
|
1097
|
+
}
|
|
1098
|
+
// listener was not specified, so remove them all for that event
|
|
1099
|
+
this._listeners.delete(name);
|
|
1100
|
+
}
|
|
1101
|
+
_invokeListener(listener, name, ...detail) {
|
|
1102
|
+
try {
|
|
1103
|
+
listener(...detail);
|
|
1104
|
+
}
|
|
1105
|
+
catch (err) {
|
|
1106
|
+
this._logger?.error(`Encountered error invoking handler for "${name}", detail: "${err}"`);
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
emit(name, ...detail) {
|
|
1110
|
+
const listeners = this._listeners.get(name);
|
|
1111
|
+
listeners?.forEach((listener) => this._invokeListener(listener, name, ...detail));
|
|
1112
|
+
}
|
|
1113
|
+
eventNames() {
|
|
1114
|
+
return [...this._listeners.keys()];
|
|
1115
|
+
}
|
|
1116
|
+
listenerCount(name) {
|
|
1117
|
+
return this._listeners.get(name)?.length ?? 0;
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
const { ClientMessages, ErrorKinds } = internal;
|
|
1122
|
+
class LDClientImpl {
|
|
1123
|
+
/**
|
|
1124
|
+
* Creates the client object synchronously. No async, no network calls.
|
|
1125
|
+
*/
|
|
1126
|
+
constructor(sdkKey, autoEnvAttributes, platform, options, dataManagerFactory, internalOptions) {
|
|
1127
|
+
this.sdkKey = sdkKey;
|
|
1128
|
+
this.autoEnvAttributes = autoEnvAttributes;
|
|
1129
|
+
this.platform = platform;
|
|
1130
|
+
this._identifyTimeout = 5;
|
|
1131
|
+
this._highTimeoutThreshold = 15;
|
|
1132
|
+
this._eventFactoryDefault = new EventFactory(false);
|
|
1133
|
+
this._eventFactoryWithReasons = new EventFactory(true);
|
|
1134
|
+
this._eventSendingEnabled = false;
|
|
1135
|
+
if (!sdkKey) {
|
|
1136
|
+
throw new Error('You must configure the client with a client-side SDK key');
|
|
1137
|
+
}
|
|
1138
|
+
if (!platform.encoding) {
|
|
1139
|
+
throw new Error('Platform must implement Encoding because btoa is required.');
|
|
1140
|
+
}
|
|
1141
|
+
this._config = new ConfigurationImpl(options, internalOptions);
|
|
1142
|
+
this.logger = this._config.logger;
|
|
1143
|
+
this._baseHeaders = defaultHeaders(this.sdkKey, this.platform.info, this._config.tags, this._config.serviceEndpoints.includeAuthorizationHeader, this._config.userAgentHeaderName);
|
|
1144
|
+
this._flagManager = new DefaultFlagManager(this.platform, sdkKey, this._config.maxCachedContexts, this._config.logger);
|
|
1145
|
+
this._diagnosticsManager = createDiagnosticsManager(sdkKey, this._config, platform);
|
|
1146
|
+
this._eventProcessor = createEventProcessor(sdkKey, this._config, platform, this._baseHeaders, this._diagnosticsManager);
|
|
1147
|
+
this.emitter = new LDEmitter();
|
|
1148
|
+
this.emitter.on('error', (c, err) => {
|
|
1149
|
+
this.logger.error(`error: ${err}, context: ${JSON.stringify(c)}`);
|
|
1150
|
+
});
|
|
1151
|
+
this._flagManager.on((context, flagKeys, type) => {
|
|
1152
|
+
this._handleInspectionChanged(flagKeys, type);
|
|
1153
|
+
const ldContext = Context.toLDContext(context);
|
|
1154
|
+
this.emitter.emit('change', ldContext, flagKeys);
|
|
1155
|
+
flagKeys.forEach((it) => {
|
|
1156
|
+
this.emitter.emit(`change:${it}`, ldContext);
|
|
1157
|
+
});
|
|
1158
|
+
});
|
|
1159
|
+
this.dataManager = dataManagerFactory(this._flagManager, this._config, this._baseHeaders, this.emitter, this._diagnosticsManager);
|
|
1160
|
+
this._hookRunner = new HookRunner(this.logger, this._config.hooks);
|
|
1161
|
+
this._inspectorManager = new InspectorManager(this._config.inspectors, this.logger);
|
|
1162
|
+
if (this._inspectorManager.hasInspectors()) {
|
|
1163
|
+
this._hookRunner.addHook(getInspectorHook(this._inspectorManager));
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
allFlags() {
|
|
1167
|
+
// extracting all flag values
|
|
1168
|
+
const result = Object.entries(this._flagManager.getAll()).reduce((acc, [key, descriptor]) => {
|
|
1169
|
+
if (descriptor.flag !== null && descriptor.flag !== undefined && !descriptor.flag.deleted) {
|
|
1170
|
+
acc[key] = descriptor.flag.value;
|
|
1171
|
+
}
|
|
1172
|
+
return acc;
|
|
1173
|
+
}, {});
|
|
1174
|
+
return result;
|
|
1175
|
+
}
|
|
1176
|
+
async close() {
|
|
1177
|
+
await this.flush();
|
|
1178
|
+
this._eventProcessor?.close();
|
|
1179
|
+
this._updateProcessor?.close();
|
|
1180
|
+
this.logger.debug('Closed event processor and data source.');
|
|
1181
|
+
}
|
|
1182
|
+
async flush() {
|
|
1183
|
+
try {
|
|
1184
|
+
await this._eventProcessor?.flush();
|
|
1185
|
+
this.logger.debug('Successfully flushed event processor.');
|
|
1186
|
+
}
|
|
1187
|
+
catch (e) {
|
|
1188
|
+
this.logger.error(`Error flushing event processor: ${e}.`);
|
|
1189
|
+
return { error: e, result: false };
|
|
1190
|
+
}
|
|
1191
|
+
return { result: true };
|
|
1192
|
+
}
|
|
1193
|
+
getContext() {
|
|
1194
|
+
// The LDContext returned here may have been modified by the SDK (for example: adding auto env attributes).
|
|
1195
|
+
// We are returning an LDContext here to maintain a consistent represetnation of context to the consuming
|
|
1196
|
+
// code. We are returned the unchecked context so that if a consumer identifies with an invalid context
|
|
1197
|
+
// and then calls getContext, they get back the same context they provided, without any assertion about
|
|
1198
|
+
// validity.
|
|
1199
|
+
return this._uncheckedContext ? clone(this._uncheckedContext) : undefined;
|
|
1200
|
+
}
|
|
1201
|
+
getInternalContext() {
|
|
1202
|
+
return this._checkedContext;
|
|
1203
|
+
}
|
|
1204
|
+
_createIdentifyPromise(timeout) {
|
|
1205
|
+
let res;
|
|
1206
|
+
let rej;
|
|
1207
|
+
const slow = new Promise((resolve, reject) => {
|
|
1208
|
+
res = resolve;
|
|
1209
|
+
rej = reject;
|
|
1210
|
+
});
|
|
1211
|
+
const timed = timedPromise(timeout, 'identify');
|
|
1212
|
+
const raced = Promise.race([timed, slow]).catch((e) => {
|
|
1213
|
+
if (e.message.includes('timed out')) {
|
|
1214
|
+
this.logger.error(`identify error: ${e}`);
|
|
1215
|
+
}
|
|
1216
|
+
throw e;
|
|
1217
|
+
});
|
|
1218
|
+
return { identifyPromise: raced, identifyResolve: res, identifyReject: rej };
|
|
1219
|
+
}
|
|
1220
|
+
/**
|
|
1221
|
+
* Identifies a context to LaunchDarkly. See {@link LDClient.identify}.
|
|
1222
|
+
*
|
|
1223
|
+
* @param pristineContext The LDContext object to be identified.
|
|
1224
|
+
* @param identifyOptions Optional configuration. See {@link LDIdentifyOptions}.
|
|
1225
|
+
* @returns A Promise which resolves when the flag values for the specified
|
|
1226
|
+
* context are available. It rejects when:
|
|
1227
|
+
*
|
|
1228
|
+
* 1. The context is unspecified or has no key.
|
|
1229
|
+
*
|
|
1230
|
+
* 2. The identify timeout is exceeded. In client SDKs this defaults to 5s.
|
|
1231
|
+
* You can customize this timeout with {@link LDIdentifyOptions | identifyOptions}.
|
|
1232
|
+
*
|
|
1233
|
+
* 3. A network error is encountered during initialization.
|
|
1234
|
+
*/
|
|
1235
|
+
async identify(pristineContext, identifyOptions) {
|
|
1236
|
+
if (identifyOptions?.timeout) {
|
|
1237
|
+
this._identifyTimeout = identifyOptions.timeout;
|
|
1238
|
+
}
|
|
1239
|
+
if (this._identifyTimeout > this._highTimeoutThreshold) {
|
|
1240
|
+
this.logger.warn('The identify function was called with a timeout greater than ' +
|
|
1241
|
+
`${this._highTimeoutThreshold} seconds. We recommend a timeout of less than ` +
|
|
1242
|
+
`${this._highTimeoutThreshold} seconds.`);
|
|
1243
|
+
}
|
|
1244
|
+
let context = await ensureKey(pristineContext, this.platform);
|
|
1245
|
+
if (this.autoEnvAttributes === AutoEnvAttributes.Enabled) {
|
|
1246
|
+
context = await addAutoEnv(context, this.platform, this._config);
|
|
1247
|
+
}
|
|
1248
|
+
const checkedContext = Context.fromLDContext(context);
|
|
1249
|
+
if (!checkedContext.valid) {
|
|
1250
|
+
const error = new Error('Context was unspecified or had no key');
|
|
1251
|
+
this.emitter.emit('error', context, error);
|
|
1252
|
+
return Promise.reject(error);
|
|
1253
|
+
}
|
|
1254
|
+
this._uncheckedContext = context;
|
|
1255
|
+
this._checkedContext = checkedContext;
|
|
1256
|
+
this._eventProcessor?.sendEvent(this._eventFactoryDefault.identifyEvent(this._checkedContext));
|
|
1257
|
+
const { identifyPromise, identifyResolve, identifyReject } = this._createIdentifyPromise(this._identifyTimeout);
|
|
1258
|
+
this.logger.debug(`Identifying ${JSON.stringify(this._checkedContext)}`);
|
|
1259
|
+
const afterIdentify = this._hookRunner.identify(context, identifyOptions?.timeout);
|
|
1260
|
+
await this.dataManager.identify(identifyResolve, identifyReject, checkedContext, identifyOptions);
|
|
1261
|
+
return identifyPromise.then((res) => {
|
|
1262
|
+
afterIdentify({ status: 'completed' });
|
|
1263
|
+
return res;
|
|
1264
|
+
}, (e) => {
|
|
1265
|
+
afterIdentify({ status: 'error' });
|
|
1266
|
+
throw e;
|
|
1267
|
+
});
|
|
1268
|
+
}
|
|
1269
|
+
on(eventName, listener) {
|
|
1270
|
+
this.emitter.on(eventName, listener);
|
|
1271
|
+
}
|
|
1272
|
+
off(eventName, listener) {
|
|
1273
|
+
this.emitter.off(eventName, listener);
|
|
1274
|
+
}
|
|
1275
|
+
track(key, data, metricValue) {
|
|
1276
|
+
if (!this._checkedContext || !this._checkedContext.valid) {
|
|
1277
|
+
this.logger.warn(ClientMessages.MissingContextKeyNoEvent);
|
|
1278
|
+
return;
|
|
1279
|
+
}
|
|
1280
|
+
// 0 is valid, so do not truthy check the metric value
|
|
1281
|
+
if (metricValue !== undefined && !TypeValidators.Number.is(metricValue)) {
|
|
1282
|
+
this.logger?.warn(ClientMessages.invalidMetricValue(typeof metricValue));
|
|
1283
|
+
}
|
|
1284
|
+
this._eventProcessor?.sendEvent(this._config.trackEventModifier(this._eventFactoryDefault.customEvent(key, this._checkedContext, data, metricValue)));
|
|
1285
|
+
}
|
|
1286
|
+
_variationInternal(flagKey, defaultValue, eventFactory, typeChecker) {
|
|
1287
|
+
if (!this._uncheckedContext) {
|
|
1288
|
+
this.logger.debug(ClientMessages.MissingContextKeyNoEvent);
|
|
1289
|
+
return createErrorEvaluationDetail(ErrorKinds.UserNotSpecified, defaultValue);
|
|
1290
|
+
}
|
|
1291
|
+
const evalContext = Context.fromLDContext(this._uncheckedContext);
|
|
1292
|
+
const foundItem = this._flagManager.get(flagKey);
|
|
1293
|
+
if (foundItem === undefined || foundItem.flag.deleted) {
|
|
1294
|
+
const defVal = defaultValue ?? null;
|
|
1295
|
+
const error = new LDClientError(`Unknown feature flag "${flagKey}"; returning default value ${defVal}.`);
|
|
1296
|
+
this.emitter.emit('error', this._uncheckedContext, error);
|
|
1297
|
+
this._eventProcessor?.sendEvent(this._eventFactoryDefault.unknownFlagEvent(flagKey, defVal, evalContext));
|
|
1298
|
+
return createErrorEvaluationDetail(ErrorKinds.FlagNotFound, defaultValue);
|
|
1299
|
+
}
|
|
1300
|
+
const { reason, value, variation, prerequisites } = foundItem.flag;
|
|
1301
|
+
if (typeChecker) {
|
|
1302
|
+
const [matched, type] = typeChecker(value);
|
|
1303
|
+
if (!matched) {
|
|
1304
|
+
this._eventProcessor?.sendEvent(eventFactory.evalEventClient(flagKey, defaultValue, // track default value on type errors
|
|
1305
|
+
defaultValue, foundItem.flag, evalContext, reason));
|
|
1306
|
+
const error = new LDClientError(`Wrong type "${type}" for feature flag "${flagKey}"; returning default value`);
|
|
1307
|
+
this.emitter.emit('error', this._uncheckedContext, error);
|
|
1308
|
+
return createErrorEvaluationDetail(ErrorKinds.WrongType, defaultValue);
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
const successDetail = createSuccessEvaluationDetail(value, variation, reason);
|
|
1312
|
+
if (value === undefined || value === null) {
|
|
1313
|
+
this.logger.debug('Result value is null. Providing default value.');
|
|
1314
|
+
successDetail.value = defaultValue;
|
|
1315
|
+
}
|
|
1316
|
+
prerequisites?.forEach((prereqKey) => {
|
|
1317
|
+
this.variation(prereqKey, undefined);
|
|
1318
|
+
});
|
|
1319
|
+
this._eventProcessor?.sendEvent(eventFactory.evalEventClient(flagKey, value, defaultValue, foundItem.flag, evalContext, reason));
|
|
1320
|
+
return successDetail;
|
|
1321
|
+
}
|
|
1322
|
+
variation(flagKey, defaultValue) {
|
|
1323
|
+
const { value } = this._hookRunner.withEvaluation(flagKey, this._uncheckedContext, defaultValue, () => this._variationInternal(flagKey, defaultValue, this._eventFactoryDefault));
|
|
1324
|
+
return value;
|
|
1325
|
+
}
|
|
1326
|
+
variationDetail(flagKey, defaultValue) {
|
|
1327
|
+
return this._hookRunner.withEvaluation(flagKey, this._uncheckedContext, defaultValue, () => this._variationInternal(flagKey, defaultValue, this._eventFactoryWithReasons));
|
|
1328
|
+
}
|
|
1329
|
+
_typedEval(key, defaultValue, eventFactory, typeChecker) {
|
|
1330
|
+
return this._hookRunner.withEvaluation(key, this._uncheckedContext, defaultValue, () => this._variationInternal(key, defaultValue, eventFactory, typeChecker));
|
|
1331
|
+
}
|
|
1332
|
+
boolVariation(key, defaultValue) {
|
|
1333
|
+
return this._typedEval(key, defaultValue, this._eventFactoryDefault, (value) => [
|
|
1334
|
+
TypeValidators.Boolean.is(value),
|
|
1335
|
+
TypeValidators.Boolean.getType(),
|
|
1336
|
+
]).value;
|
|
1337
|
+
}
|
|
1338
|
+
jsonVariation(key, defaultValue) {
|
|
1339
|
+
return this.variation(key, defaultValue);
|
|
1340
|
+
}
|
|
1341
|
+
numberVariation(key, defaultValue) {
|
|
1342
|
+
return this._typedEval(key, defaultValue, this._eventFactoryDefault, (value) => [
|
|
1343
|
+
TypeValidators.Number.is(value),
|
|
1344
|
+
TypeValidators.Number.getType(),
|
|
1345
|
+
]).value;
|
|
1346
|
+
}
|
|
1347
|
+
stringVariation(key, defaultValue) {
|
|
1348
|
+
return this._typedEval(key, defaultValue, this._eventFactoryDefault, (value) => [
|
|
1349
|
+
TypeValidators.String.is(value),
|
|
1350
|
+
TypeValidators.String.getType(),
|
|
1351
|
+
]).value;
|
|
1352
|
+
}
|
|
1353
|
+
boolVariationDetail(key, defaultValue) {
|
|
1354
|
+
return this._typedEval(key, defaultValue, this._eventFactoryWithReasons, (value) => [
|
|
1355
|
+
TypeValidators.Boolean.is(value),
|
|
1356
|
+
TypeValidators.Boolean.getType(),
|
|
1357
|
+
]);
|
|
1358
|
+
}
|
|
1359
|
+
numberVariationDetail(key, defaultValue) {
|
|
1360
|
+
return this._typedEval(key, defaultValue, this._eventFactoryWithReasons, (value) => [
|
|
1361
|
+
TypeValidators.Number.is(value),
|
|
1362
|
+
TypeValidators.Number.getType(),
|
|
1363
|
+
]);
|
|
1364
|
+
}
|
|
1365
|
+
stringVariationDetail(key, defaultValue) {
|
|
1366
|
+
return this._typedEval(key, defaultValue, this._eventFactoryWithReasons, (value) => [
|
|
1367
|
+
TypeValidators.String.is(value),
|
|
1368
|
+
TypeValidators.String.getType(),
|
|
1369
|
+
]);
|
|
1370
|
+
}
|
|
1371
|
+
jsonVariationDetail(key, defaultValue) {
|
|
1372
|
+
return this.variationDetail(key, defaultValue);
|
|
1373
|
+
}
|
|
1374
|
+
addHook(hook) {
|
|
1375
|
+
this._hookRunner.addHook(hook);
|
|
1376
|
+
}
|
|
1377
|
+
/**
|
|
1378
|
+
* Enable/Disable event sending.
|
|
1379
|
+
* @param enabled True to enable event processing, false to disable.
|
|
1380
|
+
* @param flush True to flush while disabling. Useful to flush on certain state transitions.
|
|
1381
|
+
*/
|
|
1382
|
+
setEventSendingEnabled(enabled, flush) {
|
|
1383
|
+
if (this._eventSendingEnabled === enabled) {
|
|
1384
|
+
return;
|
|
1385
|
+
}
|
|
1386
|
+
this._eventSendingEnabled = enabled;
|
|
1387
|
+
if (enabled) {
|
|
1388
|
+
this.logger.debug('Starting event processor');
|
|
1389
|
+
this._eventProcessor?.start();
|
|
1390
|
+
}
|
|
1391
|
+
else if (flush) {
|
|
1392
|
+
this.logger?.debug('Flushing event processor before disabling.');
|
|
1393
|
+
// Disable and flush.
|
|
1394
|
+
this.flush().then(() => {
|
|
1395
|
+
// While waiting for the flush event sending could be re-enabled, in which case
|
|
1396
|
+
// we do not want to close the event processor.
|
|
1397
|
+
if (!this._eventSendingEnabled) {
|
|
1398
|
+
this.logger?.debug('Stopping event processor.');
|
|
1399
|
+
this._eventProcessor?.close();
|
|
1400
|
+
}
|
|
1401
|
+
});
|
|
1402
|
+
}
|
|
1403
|
+
else {
|
|
1404
|
+
// Just disabled.
|
|
1405
|
+
this.logger?.debug('Stopping event processor.');
|
|
1406
|
+
this._eventProcessor?.close();
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
sendEvent(event) {
|
|
1410
|
+
this._eventProcessor?.sendEvent(event);
|
|
1411
|
+
}
|
|
1412
|
+
_handleInspectionChanged(flagKeys, type) {
|
|
1413
|
+
if (!this._inspectorManager.hasInspectors()) {
|
|
1414
|
+
return;
|
|
1415
|
+
}
|
|
1416
|
+
const details = {};
|
|
1417
|
+
flagKeys.forEach((flagKey) => {
|
|
1418
|
+
const item = this._flagManager.get(flagKey);
|
|
1419
|
+
if (item?.flag && !item.flag.deleted) {
|
|
1420
|
+
const { reason, value, variation } = item.flag;
|
|
1421
|
+
details[flagKey] = createSuccessEvaluationDetail(value, variation, reason);
|
|
1422
|
+
}
|
|
1423
|
+
});
|
|
1424
|
+
if (type === 'init') {
|
|
1425
|
+
this._inspectorManager.onFlagsChanged(details);
|
|
1426
|
+
}
|
|
1427
|
+
else if (type === 'patch') {
|
|
1428
|
+
Object.entries(details).forEach(([flagKey, detail]) => {
|
|
1429
|
+
this._inspectorManager.onFlagChanged(flagKey, detail);
|
|
1430
|
+
});
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
class DataSourceEventHandler {
|
|
1436
|
+
constructor(_flagManager, _statusManager, _logger) {
|
|
1437
|
+
this._flagManager = _flagManager;
|
|
1438
|
+
this._statusManager = _statusManager;
|
|
1439
|
+
this._logger = _logger;
|
|
1440
|
+
}
|
|
1441
|
+
async handlePut(context, flags) {
|
|
1442
|
+
this._logger.debug(`Got PUT: ${Object.keys(flags)}`);
|
|
1443
|
+
// mapping flags to item descriptors
|
|
1444
|
+
const descriptors = Object.entries(flags).reduce((acc, [key, flag]) => {
|
|
1445
|
+
acc[key] = { version: flag.version, flag };
|
|
1446
|
+
return acc;
|
|
1447
|
+
}, {});
|
|
1448
|
+
await this._flagManager.init(context, descriptors);
|
|
1449
|
+
this._statusManager.requestStateUpdate(DataSourceState.Valid);
|
|
1450
|
+
}
|
|
1451
|
+
async handlePatch(context, patchFlag) {
|
|
1452
|
+
this._logger.debug(`Got PATCH ${JSON.stringify(patchFlag, null, 2)}`);
|
|
1453
|
+
this._flagManager.upsert(context, patchFlag.key, {
|
|
1454
|
+
version: patchFlag.version,
|
|
1455
|
+
flag: patchFlag,
|
|
1456
|
+
});
|
|
1457
|
+
}
|
|
1458
|
+
async handleDelete(context, deleteFlag) {
|
|
1459
|
+
this._logger.debug(`Got DELETE ${JSON.stringify(deleteFlag, null, 2)}`);
|
|
1460
|
+
this._flagManager.upsert(context, deleteFlag.key, {
|
|
1461
|
+
version: deleteFlag.version,
|
|
1462
|
+
flag: {
|
|
1463
|
+
...deleteFlag,
|
|
1464
|
+
deleted: true,
|
|
1465
|
+
// props below are set to sensible defaults. they are irrelevant
|
|
1466
|
+
// because this flag has been deleted.
|
|
1467
|
+
flagVersion: 0,
|
|
1468
|
+
value: undefined,
|
|
1469
|
+
variation: 0,
|
|
1470
|
+
trackEvents: false,
|
|
1471
|
+
},
|
|
1472
|
+
});
|
|
1473
|
+
}
|
|
1474
|
+
handleStreamingError(error) {
|
|
1475
|
+
this._statusManager.reportError(error.kind, error.message, error.code, error.recoverable);
|
|
1476
|
+
}
|
|
1477
|
+
handlePollingError(error) {
|
|
1478
|
+
this._statusManager.reportError(error.kind, error.message, error.status, error.recoverable);
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
/**
|
|
1483
|
+
* Tracks the current data source status and emits updates when the status changes.
|
|
1484
|
+
*/
|
|
1485
|
+
class DataSourceStatusManager {
|
|
1486
|
+
constructor(_emitter, timeStamper = () => Date.now()) {
|
|
1487
|
+
this._emitter = _emitter;
|
|
1488
|
+
this._state = DataSourceState.Closed;
|
|
1489
|
+
this._stateSinceMillis = timeStamper();
|
|
1490
|
+
this._timeStamper = timeStamper;
|
|
1491
|
+
}
|
|
1492
|
+
get status() {
|
|
1493
|
+
return {
|
|
1494
|
+
state: this._state,
|
|
1495
|
+
stateSince: this._stateSinceMillis,
|
|
1496
|
+
lastError: this._errorInfo,
|
|
1497
|
+
};
|
|
1498
|
+
}
|
|
1499
|
+
/**
|
|
1500
|
+
* Updates the state of the manager.
|
|
1501
|
+
*
|
|
1502
|
+
* @param requestedState to track
|
|
1503
|
+
* @param isError to indicate that the state update is a result of an error occurring.
|
|
1504
|
+
*/
|
|
1505
|
+
_updateState(requestedState, isError = false) {
|
|
1506
|
+
const newState = requestedState === DataSourceState.Interrupted && this._state === DataSourceState.Initializing // don't go to interrupted from initializing (recoverable errors when initializing are not noteworthy)
|
|
1507
|
+
? DataSourceState.Initializing
|
|
1508
|
+
: requestedState;
|
|
1509
|
+
const changedState = this._state !== newState;
|
|
1510
|
+
if (changedState) {
|
|
1511
|
+
this._state = newState;
|
|
1512
|
+
this._stateSinceMillis = this._timeStamper();
|
|
1513
|
+
}
|
|
1514
|
+
if (changedState || isError) {
|
|
1515
|
+
this._emitter.emit('dataSourceStatus', this.status);
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
/**
|
|
1519
|
+
* Requests the manager move to the provided state. This request may be ignored
|
|
1520
|
+
* if the current state cannot transition to the requested state.
|
|
1521
|
+
* @param state that is requested
|
|
1522
|
+
*/
|
|
1523
|
+
requestStateUpdate(state) {
|
|
1524
|
+
this._updateState(state);
|
|
1525
|
+
}
|
|
1526
|
+
/**
|
|
1527
|
+
* Reports a datasource error to this manager. Since the {@link DataSourceStatus} includes error
|
|
1528
|
+
* information, it is possible that that a {@link DataSourceStatus} update is emitted with
|
|
1529
|
+
* the same {@link DataSourceState}.
|
|
1530
|
+
*
|
|
1531
|
+
* @param kind of the error
|
|
1532
|
+
* @param message for the error
|
|
1533
|
+
* @param statusCode of the error if there was one
|
|
1534
|
+
* @param recoverable to indicate that the error is anticipated to be recoverable
|
|
1535
|
+
*/
|
|
1536
|
+
reportError(kind, message, statusCode, recoverable = false) {
|
|
1537
|
+
const errorInfo = {
|
|
1538
|
+
kind,
|
|
1539
|
+
message,
|
|
1540
|
+
statusCode,
|
|
1541
|
+
time: this._timeStamper(),
|
|
1542
|
+
};
|
|
1543
|
+
this._errorInfo = errorInfo;
|
|
1544
|
+
this._updateState(recoverable ? DataSourceState.Interrupted : DataSourceState.Closed, true);
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
/**
|
|
1549
|
+
* @internal
|
|
1550
|
+
*/
|
|
1551
|
+
class PollingProcessor {
|
|
1552
|
+
constructor(_requestor, _pollIntervalSeconds, _dataHandler, _errorHandler, _logger) {
|
|
1553
|
+
this._requestor = _requestor;
|
|
1554
|
+
this._pollIntervalSeconds = _pollIntervalSeconds;
|
|
1555
|
+
this._dataHandler = _dataHandler;
|
|
1556
|
+
this._errorHandler = _errorHandler;
|
|
1557
|
+
this._logger = _logger;
|
|
1558
|
+
this._stopped = false;
|
|
1559
|
+
}
|
|
1560
|
+
async _poll() {
|
|
1561
|
+
if (this._stopped) {
|
|
1562
|
+
return;
|
|
1563
|
+
}
|
|
1564
|
+
const reportJsonError = (data) => {
|
|
1565
|
+
this._logger?.error('Polling received invalid data');
|
|
1566
|
+
this._logger?.debug(`Invalid JSON follows: ${data}`);
|
|
1567
|
+
this._errorHandler?.(new LDPollingError(DataSourceErrorKind.InvalidData, 'Malformed JSON data in polling response'));
|
|
1568
|
+
};
|
|
1569
|
+
this._logger?.debug('Polling LaunchDarkly for feature flag updates');
|
|
1570
|
+
const startTime = Date.now();
|
|
1571
|
+
try {
|
|
1572
|
+
const res = await this._requestor.requestPayload();
|
|
1573
|
+
try {
|
|
1574
|
+
const flags = JSON.parse(res);
|
|
1575
|
+
try {
|
|
1576
|
+
this._dataHandler?.(flags);
|
|
1577
|
+
}
|
|
1578
|
+
catch (err) {
|
|
1579
|
+
this._logger?.error(`Exception from data handler: ${err}`);
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
catch {
|
|
1583
|
+
reportJsonError(res);
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
catch (err) {
|
|
1587
|
+
const requestError = err;
|
|
1588
|
+
if (requestError.status !== undefined) {
|
|
1589
|
+
if (!isHttpRecoverable(requestError.status)) {
|
|
1590
|
+
this._logger?.error(httpErrorMessage(err, 'polling request'));
|
|
1591
|
+
this._errorHandler?.(new LDPollingError(DataSourceErrorKind.ErrorResponse, requestError.message, requestError.status));
|
|
1592
|
+
return;
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
this._logger?.error(httpErrorMessage(err, 'polling request', 'will retry'));
|
|
1596
|
+
}
|
|
1597
|
+
const elapsed = Date.now() - startTime;
|
|
1598
|
+
const sleepFor = Math.max(this._pollIntervalSeconds * 1000 - elapsed, 0);
|
|
1599
|
+
this._logger?.debug('Elapsed: %d ms, sleeping for %d ms', elapsed, sleepFor);
|
|
1600
|
+
this._timeoutHandle = setTimeout(() => {
|
|
1601
|
+
this._poll();
|
|
1602
|
+
}, sleepFor);
|
|
1603
|
+
}
|
|
1604
|
+
start() {
|
|
1605
|
+
this._poll();
|
|
1606
|
+
}
|
|
1607
|
+
stop() {
|
|
1608
|
+
if (this._timeoutHandle) {
|
|
1609
|
+
clearTimeout(this._timeoutHandle);
|
|
1610
|
+
this._timeoutHandle = undefined;
|
|
1611
|
+
}
|
|
1612
|
+
this._stopped = true;
|
|
1613
|
+
}
|
|
1614
|
+
close() {
|
|
1615
|
+
this.stop();
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
const reportJsonError = (type, data, logger, errorHandler) => {
|
|
1620
|
+
logger?.error(`Stream received invalid data in "${type}" message`);
|
|
1621
|
+
logger?.debug(`Invalid JSON follows: ${data}`);
|
|
1622
|
+
errorHandler?.(new LDStreamingError(DataSourceErrorKind.InvalidData, 'Malformed JSON data in event stream'));
|
|
1623
|
+
};
|
|
1624
|
+
class StreamingProcessor {
|
|
1625
|
+
constructor(_plainContextString, _dataSourceConfig, _listeners, _requests, encoding, _pollingRequestor, _diagnosticsManager, _errorHandler, _logger) {
|
|
1626
|
+
this._plainContextString = _plainContextString;
|
|
1627
|
+
this._dataSourceConfig = _dataSourceConfig;
|
|
1628
|
+
this._listeners = _listeners;
|
|
1629
|
+
this._requests = _requests;
|
|
1630
|
+
this._pollingRequestor = _pollingRequestor;
|
|
1631
|
+
this._diagnosticsManager = _diagnosticsManager;
|
|
1632
|
+
this._errorHandler = _errorHandler;
|
|
1633
|
+
this._logger = _logger;
|
|
1634
|
+
let path;
|
|
1635
|
+
if (_dataSourceConfig.useReport && !_requests.getEventSourceCapabilities().customMethod) {
|
|
1636
|
+
path = _dataSourceConfig.paths.pathPing(encoding, _plainContextString);
|
|
1637
|
+
}
|
|
1638
|
+
else {
|
|
1639
|
+
path = _dataSourceConfig.useReport
|
|
1640
|
+
? _dataSourceConfig.paths.pathReport(encoding, _plainContextString)
|
|
1641
|
+
: _dataSourceConfig.paths.pathGet(encoding, _plainContextString);
|
|
1642
|
+
}
|
|
1643
|
+
const parameters = [
|
|
1644
|
+
...(_dataSourceConfig.queryParameters ?? []),
|
|
1645
|
+
];
|
|
1646
|
+
if (this._dataSourceConfig.withReasons) {
|
|
1647
|
+
parameters.push({ key: 'withReasons', value: 'true' });
|
|
1648
|
+
}
|
|
1649
|
+
this._requests = _requests;
|
|
1650
|
+
this._headers = { ..._dataSourceConfig.baseHeaders };
|
|
1651
|
+
this._logger = _logger;
|
|
1652
|
+
this._streamUri = getStreamingUri(_dataSourceConfig.serviceEndpoints, path, parameters);
|
|
1653
|
+
}
|
|
1654
|
+
_logConnectionStarted() {
|
|
1655
|
+
this._connectionAttemptStartTime = Date.now();
|
|
1656
|
+
}
|
|
1657
|
+
_logConnectionResult(success) {
|
|
1658
|
+
if (this._connectionAttemptStartTime && this._diagnosticsManager) {
|
|
1659
|
+
this._diagnosticsManager.recordStreamInit(this._connectionAttemptStartTime, !success, Date.now() - this._connectionAttemptStartTime);
|
|
1660
|
+
}
|
|
1661
|
+
this._connectionAttemptStartTime = undefined;
|
|
1662
|
+
}
|
|
1663
|
+
/**
|
|
1664
|
+
* This is a wrapper around the passed errorHandler which adds additional
|
|
1665
|
+
* diagnostics and logging logic.
|
|
1666
|
+
*
|
|
1667
|
+
* @param err The error to be logged and handled.
|
|
1668
|
+
* @return boolean whether to retry the connection.
|
|
1669
|
+
*
|
|
1670
|
+
* @private
|
|
1671
|
+
*/
|
|
1672
|
+
_retryAndHandleError(err) {
|
|
1673
|
+
if (!shouldRetry(err)) {
|
|
1674
|
+
this._logConnectionResult(false);
|
|
1675
|
+
this._errorHandler?.(new LDStreamingError(DataSourceErrorKind.ErrorResponse, err.message, err.status, false));
|
|
1676
|
+
this._logger?.error(httpErrorMessage(err, 'streaming request'));
|
|
1677
|
+
return false;
|
|
1678
|
+
}
|
|
1679
|
+
this._logger?.warn(httpErrorMessage(err, 'streaming request', 'will retry'));
|
|
1680
|
+
this._logConnectionResult(false);
|
|
1681
|
+
this._logConnectionStarted();
|
|
1682
|
+
return true;
|
|
1683
|
+
}
|
|
1684
|
+
start() {
|
|
1685
|
+
this._logConnectionStarted();
|
|
1686
|
+
let methodAndBodyOverrides;
|
|
1687
|
+
if (this._dataSourceConfig.useReport) {
|
|
1688
|
+
// REPORT will include a body, so content type is required.
|
|
1689
|
+
this._headers['content-type'] = 'application/json';
|
|
1690
|
+
// orverrides default method with REPORT and adds body.
|
|
1691
|
+
methodAndBodyOverrides = { method: 'REPORT', body: this._plainContextString };
|
|
1692
|
+
}
|
|
1693
|
+
else {
|
|
1694
|
+
// no method or body override
|
|
1695
|
+
methodAndBodyOverrides = {};
|
|
1696
|
+
}
|
|
1697
|
+
// TLS is handled by the platform implementation.
|
|
1698
|
+
const eventSource = this._requests.createEventSource(this._streamUri, {
|
|
1699
|
+
headers: this._headers,
|
|
1700
|
+
...methodAndBodyOverrides,
|
|
1701
|
+
errorFilter: (error) => this._retryAndHandleError(error),
|
|
1702
|
+
initialRetryDelayMillis: this._dataSourceConfig.initialRetryDelayMillis,
|
|
1703
|
+
readTimeoutMillis: 5 * 60 * 1000,
|
|
1704
|
+
retryResetIntervalMillis: 60 * 1000,
|
|
1705
|
+
});
|
|
1706
|
+
this._eventSource = eventSource;
|
|
1707
|
+
eventSource.onclose = () => {
|
|
1708
|
+
this._logger?.info('Closed LaunchDarkly stream connection');
|
|
1709
|
+
};
|
|
1710
|
+
eventSource.onerror = () => {
|
|
1711
|
+
// The work is done by `errorFilter`.
|
|
1712
|
+
};
|
|
1713
|
+
eventSource.onopen = () => {
|
|
1714
|
+
this._logger?.info('Opened LaunchDarkly stream connection');
|
|
1715
|
+
};
|
|
1716
|
+
eventSource.onretrying = (e) => {
|
|
1717
|
+
this._logger?.info(`Will retry stream connection in ${e.delayMillis} milliseconds`);
|
|
1718
|
+
};
|
|
1719
|
+
this._listeners.forEach(({ deserializeData, processJson }, eventName) => {
|
|
1720
|
+
eventSource.addEventListener(eventName, (event) => {
|
|
1721
|
+
this._logger?.debug(`Received ${eventName} event`);
|
|
1722
|
+
if (event?.data) {
|
|
1723
|
+
this._logConnectionResult(true);
|
|
1724
|
+
const { data } = event;
|
|
1725
|
+
const dataJson = deserializeData(data);
|
|
1726
|
+
if (!dataJson) {
|
|
1727
|
+
reportJsonError(eventName, data, this._logger, this._errorHandler);
|
|
1728
|
+
return;
|
|
1729
|
+
}
|
|
1730
|
+
processJson(dataJson);
|
|
1731
|
+
}
|
|
1732
|
+
else {
|
|
1733
|
+
this._errorHandler?.(new LDStreamingError(DataSourceErrorKind.InvalidData, 'Unexpected payload from event stream'));
|
|
1734
|
+
}
|
|
1735
|
+
});
|
|
1736
|
+
});
|
|
1737
|
+
// here we set up a listener that will poll when ping is received
|
|
1738
|
+
eventSource.addEventListener('ping', async () => {
|
|
1739
|
+
this._logger?.debug('Got PING, going to poll LaunchDarkly for feature flag updates');
|
|
1740
|
+
try {
|
|
1741
|
+
const res = await this._pollingRequestor.requestPayload();
|
|
1742
|
+
try {
|
|
1743
|
+
const payload = JSON.parse(res);
|
|
1744
|
+
try {
|
|
1745
|
+
// forward the payload on to the PUT listener
|
|
1746
|
+
this._listeners.get('put')?.processJson(payload);
|
|
1747
|
+
}
|
|
1748
|
+
catch (err) {
|
|
1749
|
+
this._logger?.error(`Exception from data handler: ${err}`);
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
catch {
|
|
1753
|
+
this._logger?.error('Polling after ping received invalid data');
|
|
1754
|
+
this._logger?.debug(`Invalid JSON follows: ${res}`);
|
|
1755
|
+
this._errorHandler?.(new LDPollingError(DataSourceErrorKind.InvalidData, 'Malformed JSON data in ping polling response'));
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
catch (err) {
|
|
1759
|
+
const requestError = err;
|
|
1760
|
+
this._errorHandler?.(new LDPollingError(DataSourceErrorKind.ErrorResponse, requestError.message, requestError.status));
|
|
1761
|
+
}
|
|
1762
|
+
});
|
|
1763
|
+
}
|
|
1764
|
+
stop() {
|
|
1765
|
+
this._eventSource?.close();
|
|
1766
|
+
this._eventSource = undefined;
|
|
1767
|
+
}
|
|
1768
|
+
close() {
|
|
1769
|
+
this.stop();
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
class BaseDataManager {
|
|
1774
|
+
constructor(platform, flagManager, credential, config, getPollingPaths, getStreamingPaths, baseHeaders, emitter, diagnosticsManager) {
|
|
1775
|
+
this.platform = platform;
|
|
1776
|
+
this.flagManager = flagManager;
|
|
1777
|
+
this.credential = credential;
|
|
1778
|
+
this.config = config;
|
|
1779
|
+
this.getPollingPaths = getPollingPaths;
|
|
1780
|
+
this.getStreamingPaths = getStreamingPaths;
|
|
1781
|
+
this.baseHeaders = baseHeaders;
|
|
1782
|
+
this.emitter = emitter;
|
|
1783
|
+
this.diagnosticsManager = diagnosticsManager;
|
|
1784
|
+
this.logger = config.logger;
|
|
1785
|
+
this.dataSourceStatusManager = new DataSourceStatusManager(emitter);
|
|
1786
|
+
this._dataSourceEventHandler = new DataSourceEventHandler(flagManager, this.dataSourceStatusManager, this.config.logger);
|
|
1787
|
+
}
|
|
1788
|
+
/**
|
|
1789
|
+
* Set additional connection parameters for requests polling/streaming.
|
|
1790
|
+
*/
|
|
1791
|
+
setConnectionParams(connectionParams) {
|
|
1792
|
+
this._connectionParams = connectionParams;
|
|
1793
|
+
}
|
|
1794
|
+
createPollingProcessor(context, checkedContext, requestor, identifyResolve, identifyReject) {
|
|
1795
|
+
const processor = new PollingProcessor(requestor, this.config.pollInterval, async (flags) => {
|
|
1796
|
+
await this._dataSourceEventHandler.handlePut(checkedContext, flags);
|
|
1797
|
+
identifyResolve?.();
|
|
1798
|
+
}, (err) => {
|
|
1799
|
+
this.emitter.emit('error', context, err);
|
|
1800
|
+
this._dataSourceEventHandler.handlePollingError(err);
|
|
1801
|
+
identifyReject?.(err);
|
|
1802
|
+
}, this.logger);
|
|
1803
|
+
this.updateProcessor = this._decorateProcessorWithStatusReporting(processor, this.dataSourceStatusManager);
|
|
1804
|
+
}
|
|
1805
|
+
createStreamingProcessor(context, checkedContext, pollingRequestor, identifyResolve, identifyReject) {
|
|
1806
|
+
const processor = new StreamingProcessor(JSON.stringify(context), {
|
|
1807
|
+
credential: this.credential,
|
|
1808
|
+
serviceEndpoints: this.config.serviceEndpoints,
|
|
1809
|
+
paths: this.getStreamingPaths(),
|
|
1810
|
+
baseHeaders: this.baseHeaders,
|
|
1811
|
+
initialRetryDelayMillis: this.config.streamInitialReconnectDelay * 1000,
|
|
1812
|
+
withReasons: this.config.withReasons,
|
|
1813
|
+
useReport: this.config.useReport,
|
|
1814
|
+
queryParameters: this._connectionParams?.queryParameters,
|
|
1815
|
+
}, this.createStreamListeners(checkedContext, identifyResolve), this.platform.requests, this.platform.encoding, pollingRequestor, this.diagnosticsManager, (e) => {
|
|
1816
|
+
this.emitter.emit('error', context, e);
|
|
1817
|
+
this._dataSourceEventHandler.handleStreamingError(e);
|
|
1818
|
+
identifyReject?.(e);
|
|
1819
|
+
}, this.logger);
|
|
1820
|
+
this.updateProcessor = this._decorateProcessorWithStatusReporting(processor, this.dataSourceStatusManager);
|
|
1821
|
+
}
|
|
1822
|
+
createStreamListeners(context, identifyResolve) {
|
|
1823
|
+
const listeners = new Map();
|
|
1824
|
+
listeners.set('put', {
|
|
1825
|
+
deserializeData: JSON.parse,
|
|
1826
|
+
processJson: async (flags) => {
|
|
1827
|
+
await this._dataSourceEventHandler.handlePut(context, flags);
|
|
1828
|
+
identifyResolve?.();
|
|
1829
|
+
},
|
|
1830
|
+
});
|
|
1831
|
+
listeners.set('patch', {
|
|
1832
|
+
deserializeData: JSON.parse,
|
|
1833
|
+
processJson: async (patchFlag) => {
|
|
1834
|
+
this._dataSourceEventHandler.handlePatch(context, patchFlag);
|
|
1835
|
+
},
|
|
1836
|
+
});
|
|
1837
|
+
listeners.set('delete', {
|
|
1838
|
+
deserializeData: JSON.parse,
|
|
1839
|
+
processJson: async (deleteFlag) => {
|
|
1840
|
+
this._dataSourceEventHandler.handleDelete(context, deleteFlag);
|
|
1841
|
+
},
|
|
1842
|
+
});
|
|
1843
|
+
return listeners;
|
|
1844
|
+
}
|
|
1845
|
+
_decorateProcessorWithStatusReporting(processor, statusManager) {
|
|
1846
|
+
return {
|
|
1847
|
+
start: () => {
|
|
1848
|
+
// update status before starting processor to ensure potential errors are reported after initializing
|
|
1849
|
+
statusManager.requestStateUpdate(DataSourceState.Initializing);
|
|
1850
|
+
processor.start();
|
|
1851
|
+
},
|
|
1852
|
+
stop: () => {
|
|
1853
|
+
processor.stop();
|
|
1854
|
+
statusManager.requestStateUpdate(DataSourceState.Closed);
|
|
1855
|
+
},
|
|
1856
|
+
close: () => {
|
|
1857
|
+
processor.close();
|
|
1858
|
+
statusManager.requestStateUpdate(DataSourceState.Closed);
|
|
1859
|
+
},
|
|
1860
|
+
};
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
export { BaseDataManager, DataSourceState, LDClientImpl, Requestor, makeRequestor };
|
|
1865
|
+
//# sourceMappingURL=index.mjs.map
|