launchdarkly-js-sdk-common 4.0.0 → 4.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.circleci/config.yml +22 -0
- package/.eslintignore +4 -0
- package/.eslintrc.yaml +104 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +37 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
- package/.github/pull_request_template.md +21 -0
- package/.ldrelease/config.yml +24 -0
- package/.prettierignore +1 -0
- package/.prettierrc +5 -0
- package/CHANGELOG.md +4 -0
- package/CONTRIBUTING.md +45 -0
- package/babel.config.js +18 -0
- package/docs/typedoc.js +11 -0
- package/jest.config.js +15 -0
- package/package.json +3 -29
- package/scripts/better-audit.sh +76 -0
- package/src/EventEmitter.js +60 -0
- package/src/EventProcessor.js +175 -0
- package/src/EventSender.js +87 -0
- package/src/EventSummarizer.js +84 -0
- package/src/Identity.js +26 -0
- package/src/InitializationState.js +83 -0
- package/src/PersistentFlagStore.js +50 -0
- package/src/PersistentStorage.js +81 -0
- package/src/Requestor.js +111 -0
- package/src/Stream.js +154 -0
- package/src/UserFilter.js +75 -0
- package/src/UserValidator.js +56 -0
- package/src/__tests__/.eslintrc.yaml +7 -0
- package/src/__tests__/EventProcessor-test.js +559 -0
- package/src/__tests__/EventSender-test.js +252 -0
- package/src/__tests__/EventSource-mock.js +61 -0
- package/src/__tests__/EventSummarizer-test.js +103 -0
- package/src/__tests__/LDClient-events-test.js +757 -0
- package/src/__tests__/LDClient-localstorage-test.js +179 -0
- package/src/__tests__/LDClient-streaming-test.js +683 -0
- package/src/__tests__/LDClient-test.js +761 -0
- package/src/__tests__/PersistentFlagStore-test.js +111 -0
- package/src/__tests__/Requestor-test.js +362 -0
- package/src/__tests__/Stream-test.js +299 -0
- package/src/__tests__/UserFilter-test.js +93 -0
- package/src/__tests__/UserValidator-test.js +57 -0
- package/src/__tests__/configuration-test.js +217 -0
- package/src/__tests__/diagnosticEvents-test.js +449 -0
- package/src/__tests__/loggers-test.js +149 -0
- package/src/__tests__/mockHttp.js +122 -0
- package/src/__tests__/promiseCoalescer-test.js +128 -0
- package/src/__tests__/stubPlatform.js +148 -0
- package/src/__tests__/testUtils.js +77 -0
- package/src/__tests__/utils-test.js +148 -0
- package/src/configuration.js +151 -0
- package/src/diagnosticEvents.js +269 -0
- package/src/errors.js +37 -0
- package/src/index.js +772 -0
- package/src/jest.setup.js +1 -0
- package/src/loggers.js +93 -0
- package/src/messages.js +217 -0
- package/src/promiseCoalescer.js +52 -0
- package/src/utils.js +214 -0
- package/test-types.ts +96 -0
- package/tsconfig.json +13 -0
- package/dist/ldclient-common.cjs.js +0 -2
- package/dist/ldclient-common.cjs.js.map +0 -1
- package/dist/ldclient-common.es.js +0 -2
- package/dist/ldclient-common.es.js.map +0 -1
- package/dist/ldclient-common.min.js +0 -2
- package/dist/ldclient-common.min.js.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
// Test environment setup
|
package/src/loggers.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
const logLevels = ['debug', 'info', 'warn', 'error', 'none'];
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A simple logger that writes to stderr.
|
|
5
|
+
*/
|
|
6
|
+
function commonBasicLogger(options, formatFn) {
|
|
7
|
+
if (options && options.destination && typeof options.destination !== 'function') {
|
|
8
|
+
throw new Error('destination for basicLogger was set to a non-function');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function toConsole(methodName) {
|
|
12
|
+
// The global console variable is not guaranteed to be defined at all times in all browsers:
|
|
13
|
+
// https://www.beyondjava.net/console-log-surprises-with-internet-explorer-11-and-edge
|
|
14
|
+
return function(line) {
|
|
15
|
+
if (console && console[methodName]) {
|
|
16
|
+
console[methodName].call(console, line);
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
const destinations =
|
|
21
|
+
options && options.destination
|
|
22
|
+
? [options.destination, options.destination, options.destination, options.destination]
|
|
23
|
+
: [toConsole('log'), toConsole('info'), toConsole('warn'), toConsole('error')];
|
|
24
|
+
const prependLevelToMessage = !!(options && options.destination); // if we're writing to console.warn, etc. we don't need the prefix
|
|
25
|
+
const prefix =
|
|
26
|
+
!options || options.prefix === undefined || options.prefix === null ? '[LaunchDarkly] ' : options.prefix;
|
|
27
|
+
|
|
28
|
+
let minLevel = 1; // default is 'info'
|
|
29
|
+
if (options && options.level) {
|
|
30
|
+
for (let i = 0; i < logLevels.length; i++) {
|
|
31
|
+
if (logLevels[i] === options.level) {
|
|
32
|
+
minLevel = i;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function write(levelIndex, levelName, args) {
|
|
38
|
+
if (args.length < 1) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
let line;
|
|
42
|
+
const fullPrefix = prependLevelToMessage ? levelName + ': ' + prefix : prefix;
|
|
43
|
+
if (args.length === 1 || !formatFn) {
|
|
44
|
+
line = fullPrefix + args[0];
|
|
45
|
+
} else {
|
|
46
|
+
const tempArgs = [...args];
|
|
47
|
+
tempArgs[0] = fullPrefix + tempArgs[0];
|
|
48
|
+
line = formatFn(...tempArgs);
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
destinations[levelIndex](line);
|
|
52
|
+
} catch (err) {
|
|
53
|
+
console &&
|
|
54
|
+
console.log &&
|
|
55
|
+
console.log("[LaunchDarkly] Configured logger's " + levelName + ' method threw an exception: ' + err);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const logger = {};
|
|
60
|
+
for (let i = 0; i < logLevels.length; i++) {
|
|
61
|
+
const levelName = logLevels[i];
|
|
62
|
+
if (levelName !== 'none') {
|
|
63
|
+
if (i < minLevel) {
|
|
64
|
+
logger[levelName] = () => {};
|
|
65
|
+
} else {
|
|
66
|
+
const levelIndex = i;
|
|
67
|
+
logger[levelName] = function() {
|
|
68
|
+
// can't use arrow function with "arguments"
|
|
69
|
+
write(levelIndex, levelName, arguments);
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return logger;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function validateLogger(logger) {
|
|
79
|
+
logLevels.forEach(level => {
|
|
80
|
+
if (level !== 'none' && (!logger[level] || typeof logger[level] !== 'function')) {
|
|
81
|
+
throw new Error('Provided logger instance must support logger.' + level + '(...) method');
|
|
82
|
+
// Note that the SDK normally does not throw exceptions to the application, but that rule
|
|
83
|
+
// does not apply to LDClient.init() which will throw an exception if the parameters are so
|
|
84
|
+
// invalid that we cannot proceed with creating the client. An invalid logger meets those
|
|
85
|
+
// criteria since the SDK calls the logger during nearly all of its operations.
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
module.exports = {
|
|
91
|
+
commonBasicLogger,
|
|
92
|
+
validateLogger,
|
|
93
|
+
};
|
package/src/messages.js
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
const errors = require('./errors');
|
|
2
|
+
|
|
3
|
+
function errorString(err) {
|
|
4
|
+
if (err && err.message) {
|
|
5
|
+
return err.message;
|
|
6
|
+
}
|
|
7
|
+
if (typeof err === 'string' || err instanceof String) {
|
|
8
|
+
return err;
|
|
9
|
+
}
|
|
10
|
+
return JSON.stringify(err);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const clientInitialized = function() {
|
|
14
|
+
return 'LaunchDarkly client initialized';
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const docLink =
|
|
18
|
+
' Please see https://docs.launchdarkly.com/sdk/client-side/javascript#initializing-the-client for instructions on SDK initialization.';
|
|
19
|
+
|
|
20
|
+
const clientNotReady = function() {
|
|
21
|
+
return 'LaunchDarkly client is not ready';
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const eventCapacityExceeded = function() {
|
|
25
|
+
return 'Exceeded event queue capacity. Increase capacity to avoid dropping events.';
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const eventWithoutUser = function() {
|
|
29
|
+
return 'Be sure to call `identify` in the LaunchDarkly client: https://docs.launchdarkly.com/sdk/features/identify#javascript';
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const invalidContentType = function(contentType) {
|
|
33
|
+
return 'Expected application/json content type but got "' + contentType + '"';
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const invalidKey = function() {
|
|
37
|
+
return 'Event key must be a string';
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const localStorageUnavailable = function(err) {
|
|
41
|
+
return 'local storage is unavailable: ' + errorString(err);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const networkError = e => 'network error' + (e ? ' (' + e + ')' : '');
|
|
45
|
+
|
|
46
|
+
// We should remove unknownCustomEventKey in the future - see comments in track() in index.js
|
|
47
|
+
const unknownCustomEventKey = function(key) {
|
|
48
|
+
return 'Custom event "' + key + '" does not exist';
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const environmentNotFound = function() {
|
|
52
|
+
return 'Environment not found. Double check that you specified a valid environment/client-side ID.' + docLink;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const environmentNotSpecified = function() {
|
|
56
|
+
return 'No environment/client-side ID was specified.' + docLink;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const errorFetchingFlags = function(err) {
|
|
60
|
+
return 'Error fetching flag settings: ' + errorString(err);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const userNotSpecified = function() {
|
|
64
|
+
return 'No user specified.' + docLink;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const invalidUser = function() {
|
|
68
|
+
return 'Invalid user specified.' + docLink;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const bootstrapOldFormat = function() {
|
|
72
|
+
return (
|
|
73
|
+
'LaunchDarkly client was initialized with bootstrap data that did not include flag metadata. ' +
|
|
74
|
+
'Events may not be sent correctly.' +
|
|
75
|
+
docLink
|
|
76
|
+
);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const bootstrapInvalid = function() {
|
|
80
|
+
return 'LaunchDarkly bootstrap data is not available because the back end could not read the flags.';
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const deprecated = function(oldName, newName) {
|
|
84
|
+
if (newName) {
|
|
85
|
+
return '"' + oldName + '" is deprecated, please use "' + newName + '"';
|
|
86
|
+
}
|
|
87
|
+
return '"' + oldName + '" is deprecated';
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const httpErrorMessage = function(status, context, retryMessage) {
|
|
91
|
+
return (
|
|
92
|
+
'Received error ' +
|
|
93
|
+
status +
|
|
94
|
+
(status === 401 ? ' (invalid SDK key)' : '') +
|
|
95
|
+
' for ' +
|
|
96
|
+
context +
|
|
97
|
+
' - ' +
|
|
98
|
+
(errors.isHttpErrorRecoverable(status) ? retryMessage : 'giving up permanently')
|
|
99
|
+
);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const httpUnavailable = function() {
|
|
103
|
+
return 'Cannot make HTTP requests in this environment.' + docLink;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const identifyDisabled = function() {
|
|
107
|
+
return 'identify() has no effect here; it must be called on the main client instance';
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const streamClosing = function() {
|
|
111
|
+
return 'Closing stream connection';
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const streamConnecting = function(url) {
|
|
115
|
+
return 'Opening stream connection to ' + url;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const streamError = function(err, streamReconnectDelay) {
|
|
119
|
+
return (
|
|
120
|
+
'Error on stream connection: ' +
|
|
121
|
+
errorString(err) +
|
|
122
|
+
', will continue retrying every ' +
|
|
123
|
+
streamReconnectDelay +
|
|
124
|
+
' milliseconds.'
|
|
125
|
+
);
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const unknownOption = name => 'Ignoring unknown config option "' + name + '"';
|
|
129
|
+
|
|
130
|
+
const wrongOptionType = (name, expectedType, actualType) =>
|
|
131
|
+
'Config option "' + name + '" should be of type ' + expectedType + ', got ' + actualType + ', using default value';
|
|
132
|
+
|
|
133
|
+
const wrongOptionTypeBoolean = (name, actualType) =>
|
|
134
|
+
'Config option "' + name + '" should be a boolean, got ' + actualType + ', converting to boolean';
|
|
135
|
+
|
|
136
|
+
const optionBelowMinimum = (name, value, minimum) =>
|
|
137
|
+
'Config option "' + name + '" was set to ' + value + ', changing to minimum value of ' + minimum;
|
|
138
|
+
|
|
139
|
+
const debugPolling = function(url) {
|
|
140
|
+
return 'polling for feature flags at ' + url;
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const debugStreamPing = function() {
|
|
144
|
+
return 'received ping message from stream';
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const debugStreamPut = function() {
|
|
148
|
+
return 'received streaming update for all flags';
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const debugStreamPatch = function(key) {
|
|
152
|
+
return 'received streaming update for flag "' + key + '"';
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const debugStreamPatchIgnored = function(key) {
|
|
156
|
+
return 'received streaming update for flag "' + key + '" but ignored due to version check';
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const debugStreamDelete = function(key) {
|
|
160
|
+
return 'received streaming deletion for flag "' + key + '"';
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const debugStreamDeleteIgnored = function(key) {
|
|
164
|
+
return 'received streaming deletion for flag "' + key + '" but ignored due to version check';
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const debugEnqueueingEvent = function(kind) {
|
|
168
|
+
return 'enqueueing "' + kind + '" event';
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const debugPostingEvents = function(count) {
|
|
172
|
+
return 'sending ' + count + ' events';
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const debugPostingDiagnosticEvent = function(event) {
|
|
176
|
+
return 'sending diagnostic event (' + event.kind + ')';
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
module.exports = {
|
|
180
|
+
bootstrapInvalid,
|
|
181
|
+
bootstrapOldFormat,
|
|
182
|
+
clientInitialized,
|
|
183
|
+
clientNotReady,
|
|
184
|
+
debugEnqueueingEvent,
|
|
185
|
+
debugPostingDiagnosticEvent,
|
|
186
|
+
debugPostingEvents,
|
|
187
|
+
debugStreamDelete,
|
|
188
|
+
debugStreamDeleteIgnored,
|
|
189
|
+
debugStreamPatch,
|
|
190
|
+
debugStreamPatchIgnored,
|
|
191
|
+
debugStreamPing,
|
|
192
|
+
debugPolling,
|
|
193
|
+
debugStreamPut,
|
|
194
|
+
deprecated,
|
|
195
|
+
environmentNotFound,
|
|
196
|
+
environmentNotSpecified,
|
|
197
|
+
errorFetchingFlags,
|
|
198
|
+
eventCapacityExceeded,
|
|
199
|
+
eventWithoutUser,
|
|
200
|
+
httpErrorMessage,
|
|
201
|
+
httpUnavailable,
|
|
202
|
+
identifyDisabled,
|
|
203
|
+
invalidContentType,
|
|
204
|
+
invalidKey,
|
|
205
|
+
invalidUser,
|
|
206
|
+
localStorageUnavailable,
|
|
207
|
+
networkError,
|
|
208
|
+
optionBelowMinimum,
|
|
209
|
+
streamClosing,
|
|
210
|
+
streamConnecting,
|
|
211
|
+
streamError,
|
|
212
|
+
unknownCustomEventKey,
|
|
213
|
+
unknownOption,
|
|
214
|
+
userNotSpecified,
|
|
215
|
+
wrongOptionType,
|
|
216
|
+
wrongOptionTypeBoolean,
|
|
217
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// This function allows a series of Promises to be coalesced such that only the most recently
|
|
2
|
+
// added one actually matters. For instance, if several HTTP requests are made to the same
|
|
3
|
+
// endpoint and we want to ensure that whoever made each one always gets the latest data, each
|
|
4
|
+
// can be passed to addPromise (on the same coalescer) and each caller can wait on the
|
|
5
|
+
// coalescer.resultPromise; all three will then receive the result (or error) from the *last*
|
|
6
|
+
// request, and the results of the first two will be discarded.
|
|
7
|
+
//
|
|
8
|
+
// The cancelFn callback, if present, will be called whenever an existing promise is being
|
|
9
|
+
// discarded. This can be used for instance to abort an HTTP request that's now obsolete.
|
|
10
|
+
//
|
|
11
|
+
// The finallyFn callback, if present, is called on completion of the whole thing. This is
|
|
12
|
+
// different from calling coalescer.resultPromise.finally() because it is executed before any
|
|
13
|
+
// other handlers. Its purpose is to tell the caller that this coalescer should no longer be used.
|
|
14
|
+
|
|
15
|
+
function promiseCoalescer(finallyFn) {
|
|
16
|
+
let currentPromise;
|
|
17
|
+
let currentCancelFn;
|
|
18
|
+
let finalResolve;
|
|
19
|
+
let finalReject;
|
|
20
|
+
|
|
21
|
+
const coalescer = {};
|
|
22
|
+
|
|
23
|
+
coalescer.addPromise = (p, cancelFn) => {
|
|
24
|
+
currentPromise = p;
|
|
25
|
+
currentCancelFn && currentCancelFn();
|
|
26
|
+
currentCancelFn = cancelFn;
|
|
27
|
+
|
|
28
|
+
p.then(
|
|
29
|
+
result => {
|
|
30
|
+
if (currentPromise === p) {
|
|
31
|
+
finalResolve(result);
|
|
32
|
+
finallyFn && finallyFn();
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
error => {
|
|
36
|
+
if (currentPromise === p) {
|
|
37
|
+
finalReject(error);
|
|
38
|
+
finallyFn && finallyFn();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
coalescer.resultPromise = new Promise((resolve, reject) => {
|
|
45
|
+
finalResolve = resolve;
|
|
46
|
+
finalReject = reject;
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
return coalescer;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
module.exports = promiseCoalescer;
|
package/src/utils.js
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
const base64 = require('base64-js');
|
|
2
|
+
const fastDeepEqual = require('fast-deep-equal');
|
|
3
|
+
|
|
4
|
+
const userAttrsToStringify = ['key', 'secondary', 'ip', 'country', 'email', 'firstName', 'lastName', 'avatar', 'name'];
|
|
5
|
+
|
|
6
|
+
// See http://ecmanaut.blogspot.com/2006/07/encoding-decoding-utf8-in-javascript.html
|
|
7
|
+
function btoa(s) {
|
|
8
|
+
const escaped = unescape(encodeURIComponent(s));
|
|
9
|
+
return base64.fromByteArray(stringToBytes(escaped));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function stringToBytes(s) {
|
|
13
|
+
const b = [];
|
|
14
|
+
for (let i = 0; i < s.length; i++) {
|
|
15
|
+
b.push(s.charCodeAt(i));
|
|
16
|
+
}
|
|
17
|
+
return b;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function base64URLEncode(s) {
|
|
21
|
+
return (
|
|
22
|
+
btoa(s)
|
|
23
|
+
// eslint-disable-next-line
|
|
24
|
+
.replace(/=/g, '')
|
|
25
|
+
.replace(/\+/g, '-')
|
|
26
|
+
.replace(/\//g, '_')
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function clone(obj) {
|
|
31
|
+
return JSON.parse(JSON.stringify(obj));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function deepEquals(a, b) {
|
|
35
|
+
return fastDeepEqual(a, b);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Events emitted in LDClient's initialize method will happen before the consumer
|
|
39
|
+
// can register a listener, so defer them to next tick.
|
|
40
|
+
function onNextTick(cb) {
|
|
41
|
+
setTimeout(cb, 0);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Wrap a promise to invoke an optional callback upon resolution or rejection.
|
|
46
|
+
*
|
|
47
|
+
* This function assumes the callback follows the Node.js callback type: (err, value) => void
|
|
48
|
+
*
|
|
49
|
+
* If a callback is provided:
|
|
50
|
+
* - if the promise is resolved, invoke the callback with (null, value)
|
|
51
|
+
* - if the promise is rejected, invoke the callback with (error, null)
|
|
52
|
+
*
|
|
53
|
+
* @param {Promise<any>} promise
|
|
54
|
+
* @param {Function} callback
|
|
55
|
+
* @returns Promise<any> | undefined
|
|
56
|
+
*/
|
|
57
|
+
function wrapPromiseCallback(promise, callback) {
|
|
58
|
+
const ret = promise.then(
|
|
59
|
+
value => {
|
|
60
|
+
if (callback) {
|
|
61
|
+
setTimeout(() => {
|
|
62
|
+
callback(null, value);
|
|
63
|
+
}, 0);
|
|
64
|
+
}
|
|
65
|
+
return value;
|
|
66
|
+
},
|
|
67
|
+
error => {
|
|
68
|
+
if (callback) {
|
|
69
|
+
setTimeout(() => {
|
|
70
|
+
callback(error, null);
|
|
71
|
+
}, 0);
|
|
72
|
+
} else {
|
|
73
|
+
return Promise.reject(error);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
return !callback ? ret : undefined;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Takes a map of flag keys to values, and returns the more verbose structure used by the
|
|
83
|
+
* client stream.
|
|
84
|
+
*/
|
|
85
|
+
function transformValuesToVersionedValues(flags) {
|
|
86
|
+
const ret = {};
|
|
87
|
+
for (const key in flags) {
|
|
88
|
+
if (objectHasOwnProperty(flags, key)) {
|
|
89
|
+
ret[key] = { value: flags[key], version: 0 };
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return ret;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Converts the internal flag state map to a simple map of flag keys to values.
|
|
97
|
+
*/
|
|
98
|
+
function transformVersionedValuesToValues(flagsState) {
|
|
99
|
+
const ret = {};
|
|
100
|
+
for (const key in flagsState) {
|
|
101
|
+
if (objectHasOwnProperty(flagsState, key)) {
|
|
102
|
+
ret[key] = flagsState[key].value;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return ret;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Returns an array of event groups each of which can be safely URL-encoded
|
|
110
|
+
* without hitting the safe maximum URL length of certain browsers.
|
|
111
|
+
*
|
|
112
|
+
* @param {number} maxLength maximum URL length targeted
|
|
113
|
+
* @param {Array[Object}]} events queue of events to divide
|
|
114
|
+
* @returns Array[Array[Object]]
|
|
115
|
+
*/
|
|
116
|
+
function chunkUserEventsForUrl(maxLength, events) {
|
|
117
|
+
const allEvents = events.slice(0);
|
|
118
|
+
const allChunks = [];
|
|
119
|
+
let remainingSpace = maxLength;
|
|
120
|
+
let chunk;
|
|
121
|
+
|
|
122
|
+
while (allEvents.length > 0) {
|
|
123
|
+
chunk = [];
|
|
124
|
+
|
|
125
|
+
while (remainingSpace > 0) {
|
|
126
|
+
const event = allEvents.shift();
|
|
127
|
+
if (!event) {
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
remainingSpace = remainingSpace - base64URLEncode(JSON.stringify(event)).length;
|
|
131
|
+
// If we are over the max size, put this one back on the queue
|
|
132
|
+
// to try in the next round, unless this event alone is larger
|
|
133
|
+
// than the limit, in which case, screw it, and try it anyway.
|
|
134
|
+
if (remainingSpace < 0 && chunk.length > 0) {
|
|
135
|
+
allEvents.unshift(event);
|
|
136
|
+
} else {
|
|
137
|
+
chunk.push(event);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
remainingSpace = maxLength;
|
|
142
|
+
allChunks.push(chunk);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return allChunks;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function getLDUserAgentString(platform) {
|
|
149
|
+
const version = platform.version || VERSION;
|
|
150
|
+
return platform.userAgent + '/' + version;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function getLDHeaders(platform, options) {
|
|
154
|
+
if (options && !options.sendLDHeaders) {
|
|
155
|
+
return {};
|
|
156
|
+
}
|
|
157
|
+
const h = {};
|
|
158
|
+
h[platform.userAgentHeaderName || 'User-Agent'] = getLDUserAgentString(platform);
|
|
159
|
+
if (options && options.wrapperName) {
|
|
160
|
+
h['X-LaunchDarkly-Wrapper'] = options.wrapperVersion
|
|
161
|
+
? options.wrapperName + '/' + options.wrapperVersion
|
|
162
|
+
: options.wrapperName;
|
|
163
|
+
}
|
|
164
|
+
return h;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function transformHeaders(headers, options) {
|
|
168
|
+
if (!options || !options.requestHeaderTransform) {
|
|
169
|
+
return headers;
|
|
170
|
+
}
|
|
171
|
+
return options.requestHeaderTransform({ ...headers });
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function extend(...objects) {
|
|
175
|
+
return objects.reduce((acc, obj) => ({ ...acc, ...obj }), {});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function objectHasOwnProperty(object, name) {
|
|
179
|
+
return Object.prototype.hasOwnProperty.call(object, name);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function sanitizeUser(user) {
|
|
183
|
+
if (!user) {
|
|
184
|
+
return user;
|
|
185
|
+
}
|
|
186
|
+
let newUser;
|
|
187
|
+
for (const i in userAttrsToStringify) {
|
|
188
|
+
const attr = userAttrsToStringify[i];
|
|
189
|
+
const value = user[attr];
|
|
190
|
+
if (value !== undefined && typeof value !== 'string') {
|
|
191
|
+
newUser = newUser || { ...user };
|
|
192
|
+
newUser[attr] = String(value);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return newUser || user;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
module.exports = {
|
|
199
|
+
base64URLEncode,
|
|
200
|
+
btoa,
|
|
201
|
+
chunkUserEventsForUrl,
|
|
202
|
+
clone,
|
|
203
|
+
deepEquals,
|
|
204
|
+
extend,
|
|
205
|
+
getLDHeaders,
|
|
206
|
+
getLDUserAgentString,
|
|
207
|
+
objectHasOwnProperty,
|
|
208
|
+
onNextTick,
|
|
209
|
+
sanitizeUser,
|
|
210
|
+
transformHeaders,
|
|
211
|
+
transformValuesToVersionedValues,
|
|
212
|
+
transformVersionedValuesToValues,
|
|
213
|
+
wrapPromiseCallback,
|
|
214
|
+
};
|
package/test-types.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
|
|
2
|
+
// This file exists only so that we can run the TypeScript compiler in the CI build
|
|
3
|
+
// to validate our typings.d.ts file.
|
|
4
|
+
|
|
5
|
+
import * as ld from 'launchdarkly-js-sdk-common';
|
|
6
|
+
|
|
7
|
+
var ver: string = ld.version;
|
|
8
|
+
|
|
9
|
+
var userWithKeyOnly: ld.LDUser = { key: 'user' };
|
|
10
|
+
var anonUserWithNoKey: ld.LDUser = { anonymous: true };
|
|
11
|
+
var anonUserWithKey: ld.LDUser = { key: 'anon-user', anonymous: true };
|
|
12
|
+
var user: ld.LDUser = {
|
|
13
|
+
key: 'user',
|
|
14
|
+
secondary: 'otherkey',
|
|
15
|
+
name: 'name',
|
|
16
|
+
firstName: 'first',
|
|
17
|
+
lastName: 'last',
|
|
18
|
+
email: 'test@example.com',
|
|
19
|
+
avatar: 'http://avatar.url',
|
|
20
|
+
ip: '1.1.1.1',
|
|
21
|
+
country: 'us',
|
|
22
|
+
anonymous: true,
|
|
23
|
+
custom: {
|
|
24
|
+
'a': 's',
|
|
25
|
+
'b': true,
|
|
26
|
+
'c': 3,
|
|
27
|
+
'd': [ 'x', 'y' ],
|
|
28
|
+
'e': [ true, false ],
|
|
29
|
+
'f': [ 1, 2 ]
|
|
30
|
+
},
|
|
31
|
+
privateAttributeNames: [ 'name', 'email' ]
|
|
32
|
+
};
|
|
33
|
+
var logger: ld.LDLogger = ld.commonBasicLogger({ level: 'info' });
|
|
34
|
+
var allBaseOptions: ld.LDOptionsBase = {
|
|
35
|
+
bootstrap: { },
|
|
36
|
+
baseUrl: '',
|
|
37
|
+
eventsUrl: '',
|
|
38
|
+
streamUrl: '',
|
|
39
|
+
streaming: true,
|
|
40
|
+
useReport: true,
|
|
41
|
+
sendLDHeaders: true,
|
|
42
|
+
requestHeaderTransform: (x) => x,
|
|
43
|
+
evaluationReasons: true,
|
|
44
|
+
sendEvents: true,
|
|
45
|
+
allAttributesPrivate: true,
|
|
46
|
+
privateAttributeNames: [ 'x' ],
|
|
47
|
+
inlineUsersInEvents: true,
|
|
48
|
+
allowFrequentDuplicateEvents: true,
|
|
49
|
+
sendEventsOnlyForVariation: true,
|
|
50
|
+
flushInterval: 1,
|
|
51
|
+
streamReconnectDelay: 1,
|
|
52
|
+
logger: logger
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
var client: ld.LDClientBase = {} as ld.LDClientBase; // wouldn't do this in real life, it's just so the following statements will compile
|
|
56
|
+
|
|
57
|
+
client.waitUntilReady().then(() => {});
|
|
58
|
+
client.waitForInitialization().then(() => {});
|
|
59
|
+
|
|
60
|
+
client.identify(user).then(() => {});
|
|
61
|
+
client.identify(user, undefined, () => {});
|
|
62
|
+
client.identify(user, 'hash').then(() => {});
|
|
63
|
+
|
|
64
|
+
client.alias(user, anonUserWithKey);
|
|
65
|
+
|
|
66
|
+
var user: ld.LDUser = client.getUser();
|
|
67
|
+
|
|
68
|
+
client.flush(() => {});
|
|
69
|
+
client.flush().then(() => {});
|
|
70
|
+
|
|
71
|
+
var boolFlagValue: ld.LDFlagValue = client.variation('key', false);
|
|
72
|
+
var numberFlagValue: ld.LDFlagValue = client.variation('key', 2);
|
|
73
|
+
var stringFlagValue: ld.LDFlagValue = client.variation('key', 'default');
|
|
74
|
+
var jsonFlagValue: ld.LDFlagValue = client.variation('key', [ 'a', 'b' ]);
|
|
75
|
+
|
|
76
|
+
var detail: ld.LDEvaluationDetail = client.variationDetail('key', 'default');
|
|
77
|
+
var detailValue: ld.LDFlagValue = detail.value;
|
|
78
|
+
var detailIndex: number | undefined = detail.variationIndex;
|
|
79
|
+
var detailReason: ld.LDEvaluationReason | undefined = detail.reason;
|
|
80
|
+
|
|
81
|
+
client.setStreaming(true);
|
|
82
|
+
client.setStreaming();
|
|
83
|
+
|
|
84
|
+
function handleEvent() {}
|
|
85
|
+
client.on('event', handleEvent);
|
|
86
|
+
client.off('event', handleEvent);
|
|
87
|
+
|
|
88
|
+
client.track('event');
|
|
89
|
+
client.track('event', { someData: 'x' });
|
|
90
|
+
client.track('event', null, 3.5);
|
|
91
|
+
|
|
92
|
+
var flagSet: ld.LDFlagSet = client.allFlags();
|
|
93
|
+
var flagSetValue: ld.LDFlagValue = flagSet['key'];
|
|
94
|
+
|
|
95
|
+
client.close(() => {});
|
|
96
|
+
client.close().then(() => {});
|