launchdarkly-js-sdk-common 4.3.1 → 4.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -0
- package/package.json +1 -1
- package/src/Stream.js +45 -3
- package/src/__tests__/Stream-test.js +40 -2
- package/src/headers.js +2 -1
- package/src/messages.js +4 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to the `launchdarkly-js-sdk-common` package will be documented in this file. Changes that affect the dependent SDKs such as `launchdarkly-js-client-sdk` should also be logged in those projects, in the next release that uses the updated version of this package. This project adheres to [Semantic Versioning](http://semver.org).
|
|
4
4
|
|
|
5
|
+
## [4.3.1] - 2022-10-17
|
|
6
|
+
### Fixed:
|
|
7
|
+
- Fixed an issue that prevented the `flag-used` inspector from being called.
|
|
8
|
+
|
|
5
9
|
## [4.3.0] - 2022-10-17
|
|
6
10
|
### Added:
|
|
7
11
|
- Added support for `Inspectors` that can be used for collecting information for monitoring, analytics, and debugging.
|
package/package.json
CHANGED
package/src/Stream.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const messages = require('./messages');
|
|
2
2
|
const { appendUrlPath, base64URLEncode, objectHasOwnProperty } = require('./utils');
|
|
3
3
|
const { getLDHeaders, transformHeaders } = require('./headers');
|
|
4
|
+
const { isHttpErrorRecoverable } = require('./errors');
|
|
4
5
|
|
|
5
6
|
// The underlying event source implementation is abstracted via the platform object, which should
|
|
6
7
|
// have these three properties:
|
|
@@ -16,6 +17,8 @@ const { getLDHeaders, transformHeaders } = require('./headers');
|
|
|
16
17
|
// interval between heartbeats from the LaunchDarkly streaming server. If this amount of time elapses
|
|
17
18
|
// with no new data, the connection will be cycled.
|
|
18
19
|
const streamReadTimeoutMillis = 5 * 60 * 1000; // 5 minutes
|
|
20
|
+
const maxRetryDelay = 30 * 1000; // Maximum retry delay 30 seconds.
|
|
21
|
+
const jitterRatio = 0.5; // Delay should be 50%-100% of calculated time.
|
|
19
22
|
|
|
20
23
|
function Stream(platform, config, environment, diagnosticsAccumulator) {
|
|
21
24
|
const baseUrl = config.streamUrl;
|
|
@@ -24,7 +27,7 @@ function Stream(platform, config, environment, diagnosticsAccumulator) {
|
|
|
24
27
|
const evalUrlPrefix = appendUrlPath(baseUrl, '/eval/' + environment);
|
|
25
28
|
const useReport = config.useReport;
|
|
26
29
|
const withReasons = config.evaluationReasons;
|
|
27
|
-
const
|
|
30
|
+
const baseReconnectDelay = config.streamReconnectDelay;
|
|
28
31
|
const headers = getLDHeaders(platform, config);
|
|
29
32
|
let firstConnectionErrorLogged = false;
|
|
30
33
|
let es = null;
|
|
@@ -33,6 +36,22 @@ function Stream(platform, config, environment, diagnosticsAccumulator) {
|
|
|
33
36
|
let user = null;
|
|
34
37
|
let hash = null;
|
|
35
38
|
let handlers = null;
|
|
39
|
+
let retryCount = 0;
|
|
40
|
+
|
|
41
|
+
function backoff() {
|
|
42
|
+
const delay = baseReconnectDelay * Math.pow(2, retryCount);
|
|
43
|
+
return delay > maxRetryDelay ? maxRetryDelay : delay;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function jitter(computedDelayMillis) {
|
|
47
|
+
return computedDelayMillis - Math.trunc(Math.random() * jitterRatio * computedDelayMillis);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function getNextRetryDelay() {
|
|
51
|
+
const delay = jitter(backoff());
|
|
52
|
+
retryCount += 1;
|
|
53
|
+
return delay;
|
|
54
|
+
}
|
|
36
55
|
|
|
37
56
|
stream.connect = function(newUser, newHash, newHandlers) {
|
|
38
57
|
user = newUser;
|
|
@@ -63,13 +82,31 @@ function Stream(platform, config, environment, diagnosticsAccumulator) {
|
|
|
63
82
|
};
|
|
64
83
|
|
|
65
84
|
function handleError(err) {
|
|
85
|
+
// The event source may not produce a status. But the LaunchDarkly
|
|
86
|
+
// polyfill can. If we can get the status, then we should stop retrying
|
|
87
|
+
// on certain error codes.
|
|
88
|
+
if (err.status && typeof err.status === 'number' && !isHttpErrorRecoverable(err.status)) {
|
|
89
|
+
// If we encounter an unrecoverable condition, then we do not want to
|
|
90
|
+
// retry anymore.
|
|
91
|
+
closeConnection();
|
|
92
|
+
logger.error(messages.unrecoverableStreamError(err));
|
|
93
|
+
// Ensure any pending retry attempts are not done.
|
|
94
|
+
if (reconnectTimeoutReference) {
|
|
95
|
+
clearTimeout(reconnectTimeoutReference);
|
|
96
|
+
reconnectTimeoutReference = null;
|
|
97
|
+
}
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const delay = getNextRetryDelay();
|
|
102
|
+
|
|
66
103
|
if (!firstConnectionErrorLogged) {
|
|
67
|
-
logger.warn(messages.streamError(err,
|
|
104
|
+
logger.warn(messages.streamError(err, delay));
|
|
68
105
|
firstConnectionErrorLogged = true;
|
|
69
106
|
}
|
|
70
107
|
logConnectionResult(false);
|
|
71
108
|
closeConnection();
|
|
72
|
-
tryConnect(
|
|
109
|
+
tryConnect(delay);
|
|
73
110
|
}
|
|
74
111
|
|
|
75
112
|
function tryConnect(delay) {
|
|
@@ -123,6 +160,11 @@ function Stream(platform, config, environment, diagnosticsAccumulator) {
|
|
|
123
160
|
}
|
|
124
161
|
|
|
125
162
|
es.onerror = handleError;
|
|
163
|
+
|
|
164
|
+
es.onopen = () => {
|
|
165
|
+
// If the connection is a success, then reset the retryCount.
|
|
166
|
+
retryCount = 0;
|
|
167
|
+
};
|
|
126
168
|
}
|
|
127
169
|
}
|
|
128
170
|
|
|
@@ -170,6 +170,7 @@ describe('Stream', () => {
|
|
|
170
170
|
const nAttempts = 5;
|
|
171
171
|
for (let i = 0; i < nAttempts; i++) {
|
|
172
172
|
es.mockError('test error');
|
|
173
|
+
await sleepAsync(10);
|
|
173
174
|
const created1 = await platform.testing.expectStream();
|
|
174
175
|
const es1 = created1.eventSource;
|
|
175
176
|
|
|
@@ -185,6 +186,40 @@ describe('Stream', () => {
|
|
|
185
186
|
}
|
|
186
187
|
});
|
|
187
188
|
|
|
189
|
+
it.each([401, 403])('does not reconnect after an unrecoverable error', async status => {
|
|
190
|
+
const config = { ...defaultConfig, streamReconnectDelay: 1, useReport: false };
|
|
191
|
+
const stream = new Stream(platform, config, envName);
|
|
192
|
+
stream.connect(user);
|
|
193
|
+
|
|
194
|
+
const created = await platform.testing.expectStream();
|
|
195
|
+
const es = created.eventSource;
|
|
196
|
+
|
|
197
|
+
expect(es.readyState).toBe(EventSource.CONNECTING);
|
|
198
|
+
es.mockOpen();
|
|
199
|
+
expect(es.readyState).toBe(EventSource.OPEN);
|
|
200
|
+
|
|
201
|
+
es.mockError({ status });
|
|
202
|
+
await sleepAsync(10);
|
|
203
|
+
expect(platform.testing.eventSourcesCreated.length()).toEqual(0);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it.each([400, 408, 429])('does reconnect after a recoverable error', async status => {
|
|
207
|
+
const config = { ...defaultConfig, streamReconnectDelay: 1, useReport: false };
|
|
208
|
+
const stream = new Stream(platform, config, envName);
|
|
209
|
+
stream.connect(user);
|
|
210
|
+
|
|
211
|
+
const created = await platform.testing.expectStream();
|
|
212
|
+
const es = created.eventSource;
|
|
213
|
+
|
|
214
|
+
expect(es.readyState).toBe(EventSource.CONNECTING);
|
|
215
|
+
es.mockOpen();
|
|
216
|
+
expect(es.readyState).toBe(EventSource.OPEN);
|
|
217
|
+
|
|
218
|
+
es.mockError({ status });
|
|
219
|
+
await sleepAsync(10);
|
|
220
|
+
expect(platform.testing.eventSourcesCreated.length()).toEqual(1);
|
|
221
|
+
});
|
|
222
|
+
|
|
188
223
|
it('logs a warning for only the first failed connection attempt', async () => {
|
|
189
224
|
const config = { ...defaultConfig, streamReconnectDelay: 1 };
|
|
190
225
|
const stream = new Stream(platform, config, envName);
|
|
@@ -197,6 +232,7 @@ describe('Stream', () => {
|
|
|
197
232
|
const nAttempts = 5;
|
|
198
233
|
for (let i = 0; i < nAttempts; i++) {
|
|
199
234
|
es.mockError('test error');
|
|
235
|
+
await sleepAsync(10);
|
|
200
236
|
const created1 = await platform.testing.expectStream();
|
|
201
237
|
es = created1.eventSource;
|
|
202
238
|
es.mockOpen();
|
|
@@ -221,6 +257,7 @@ describe('Stream', () => {
|
|
|
221
257
|
const nAttempts = 5;
|
|
222
258
|
for (let i = 0; i < nAttempts; i++) {
|
|
223
259
|
es.mockError('test error #1');
|
|
260
|
+
await sleepAsync(10);
|
|
224
261
|
const created1 = await platform.testing.expectStream();
|
|
225
262
|
es = created1.eventSource;
|
|
226
263
|
es.mockOpen();
|
|
@@ -232,6 +269,7 @@ describe('Stream', () => {
|
|
|
232
269
|
|
|
233
270
|
for (let i = 0; i < nAttempts; i++) {
|
|
234
271
|
es.mockError('test error #2');
|
|
272
|
+
await sleepAsync(10);
|
|
235
273
|
const created1 = await platform.testing.expectStream();
|
|
236
274
|
es = created1.eventSource;
|
|
237
275
|
es.mockOpen();
|
|
@@ -239,8 +277,8 @@ describe('Stream', () => {
|
|
|
239
277
|
|
|
240
278
|
// make sure there is just a single logged message rather than five (one per attempt)
|
|
241
279
|
expect(logger.output.warn).toEqual([
|
|
242
|
-
|
|
243
|
-
|
|
280
|
+
expect.stringContaining('test error #1'),
|
|
281
|
+
expect.stringContaining('test error #2'),
|
|
244
282
|
]);
|
|
245
283
|
});
|
|
246
284
|
|
package/src/headers.js
CHANGED
|
@@ -17,9 +17,10 @@ function getLDHeaders(platform, options) {
|
|
|
17
17
|
if (tagKeys.length) {
|
|
18
18
|
h['x-launchdarkly-tags'] = tagKeys
|
|
19
19
|
.sort()
|
|
20
|
-
.
|
|
20
|
+
.map(
|
|
21
21
|
key => (Array.isArray(tags[key]) ? tags[key].sort().map(value => `${key}/${value}`) : [`${key}/${tags[key]}`])
|
|
22
22
|
)
|
|
23
|
+
.reduce((flattened, item) => flattened.concat(item), [])
|
|
23
24
|
.join(' ');
|
|
24
25
|
}
|
|
25
26
|
return h;
|
package/src/messages.js
CHANGED
|
@@ -123,7 +123,7 @@ const streamError = function(err, streamReconnectDelay) {
|
|
|
123
123
|
return (
|
|
124
124
|
'Error on stream connection: ' +
|
|
125
125
|
errorString(err) +
|
|
126
|
-
', will continue retrying
|
|
126
|
+
', will continue retrying after ' +
|
|
127
127
|
streamReconnectDelay +
|
|
128
128
|
' milliseconds.'
|
|
129
129
|
);
|
|
@@ -131,6 +131,8 @@ const streamError = function(err, streamReconnectDelay) {
|
|
|
131
131
|
|
|
132
132
|
const unknownOption = name => 'Ignoring unknown config option "' + name + '"';
|
|
133
133
|
|
|
134
|
+
const unrecoverableStreamError = err => `Error on stream connection ${errorString(err)}, giving up permanently`;
|
|
135
|
+
|
|
134
136
|
const wrongOptionType = (name, expectedType, actualType) =>
|
|
135
137
|
'Config option "' + name + '" should be of type ' + expectedType + ', got ' + actualType + ', using default value';
|
|
136
138
|
|
|
@@ -228,6 +230,7 @@ module.exports = {
|
|
|
228
230
|
tagValueTooLong,
|
|
229
231
|
unknownCustomEventKey,
|
|
230
232
|
unknownOption,
|
|
233
|
+
unrecoverableStreamError,
|
|
231
234
|
userNotSpecified,
|
|
232
235
|
wrongOptionType,
|
|
233
236
|
wrongOptionTypeBoolean,
|