@depup/launchdarkly-node-server-sdk 7.0.4-depup.0

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.
Files changed (107) hide show
  1. package/.babelrc +16 -0
  2. package/.circleci/config.yml +89 -0
  3. package/.eslintignore +5 -0
  4. package/.eslintrc.yaml +114 -0
  5. package/.github/ISSUE_TEMPLATE/bug_report.md +37 -0
  6. package/.github/ISSUE_TEMPLATE/config.yml +5 -0
  7. package/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
  8. package/.github/pull_request_template.md +21 -0
  9. package/.github/workflows/stale.yml +8 -0
  10. package/.hound.yml +33 -0
  11. package/.ldrelease/config.yml +28 -0
  12. package/.prettierrc +6 -0
  13. package/CHANGELOG.md +603 -0
  14. package/CODEOWNERS +2 -0
  15. package/CONTRIBUTING.md +55 -0
  16. package/LICENSE.txt +13 -0
  17. package/README.md +36 -0
  18. package/SECURITY.md +5 -0
  19. package/attribute_reference.js +217 -0
  20. package/big_segments.js +117 -0
  21. package/caching_store_wrapper.js +240 -0
  22. package/changes.json +30 -0
  23. package/configuration.js +235 -0
  24. package/context.js +98 -0
  25. package/context_filter.js +137 -0
  26. package/contract-tests/README.md +7 -0
  27. package/contract-tests/index.js +109 -0
  28. package/contract-tests/log.js +23 -0
  29. package/contract-tests/package.json +15 -0
  30. package/contract-tests/sdkClientEntity.js +110 -0
  31. package/contract-tests/testharness-suppressions.txt +2 -0
  32. package/diagnostic_events.js +151 -0
  33. package/docs/typedoc.js +10 -0
  34. package/errors.js +26 -0
  35. package/evaluator.js +822 -0
  36. package/event_factory.js +121 -0
  37. package/event_processor.js +320 -0
  38. package/event_summarizer.js +101 -0
  39. package/feature_store.js +120 -0
  40. package/feature_store_event_wrapper.js +258 -0
  41. package/file_data_source.js +192 -0
  42. package/flags_state.js +46 -0
  43. package/index.d.ts +2426 -0
  44. package/index.js +452 -0
  45. package/integrations.js +7 -0
  46. package/interfaces.js +2 -0
  47. package/loggers.js +125 -0
  48. package/messages.js +31 -0
  49. package/operators.js +106 -0
  50. package/package.json +105 -0
  51. package/polling.js +70 -0
  52. package/requestor.js +62 -0
  53. package/scripts/better-audit.sh +76 -0
  54. package/sharedtest/big_segment_store_tests.js +86 -0
  55. package/sharedtest/feature_store_tests.js +177 -0
  56. package/sharedtest/persistent_feature_store_tests.js +183 -0
  57. package/sharedtest/store_tests.js +7 -0
  58. package/streaming.js +179 -0
  59. package/test/LDClient-big-segments-test.js +92 -0
  60. package/test/LDClient-end-to-end-test.js +218 -0
  61. package/test/LDClient-evaluation-all-flags-test.js +226 -0
  62. package/test/LDClient-evaluation-test.js +204 -0
  63. package/test/LDClient-events-test.js +502 -0
  64. package/test/LDClient-listeners-test.js +180 -0
  65. package/test/LDClient-test.js +96 -0
  66. package/test/LDClient-tls-test.js +110 -0
  67. package/test/attribute_reference-test.js +494 -0
  68. package/test/big_segments-test.js +182 -0
  69. package/test/caching_store_wrapper-test.js +434 -0
  70. package/test/configuration-test.js +249 -0
  71. package/test/context-test.js +93 -0
  72. package/test/context_filter-test.js +424 -0
  73. package/test/diagnostic_events-test.js +152 -0
  74. package/test/evaluator-big-segments-test.js +301 -0
  75. package/test/evaluator-bucketing-test.js +333 -0
  76. package/test/evaluator-clause-test.js +277 -0
  77. package/test/evaluator-flag-test.js +452 -0
  78. package/test/evaluator-pre-conditions-test.js +105 -0
  79. package/test/evaluator-rule-test.js +131 -0
  80. package/test/evaluator-segment-match-test.js +310 -0
  81. package/test/evaluator_helpers.js +106 -0
  82. package/test/event_processor-test.js +680 -0
  83. package/test/event_summarizer-test.js +146 -0
  84. package/test/feature_store-test.js +42 -0
  85. package/test/feature_store_event_wrapper-test.js +182 -0
  86. package/test/feature_store_test_base.js +60 -0
  87. package/test/file_data_source-test.js +255 -0
  88. package/test/loggers-test.js +126 -0
  89. package/test/operators-test.js +102 -0
  90. package/test/polling-test.js +158 -0
  91. package/test/requestor-test.js +60 -0
  92. package/test/store_tests_big_segments-test.js +61 -0
  93. package/test/streaming-test.js +323 -0
  94. package/test/stubs.js +107 -0
  95. package/test/test_data-test.js +341 -0
  96. package/test/update_queue-test.js +61 -0
  97. package/test-types.ts +210 -0
  98. package/test_data.js +323 -0
  99. package/tsconfig.json +14 -0
  100. package/update_queue.js +28 -0
  101. package/utils/__tests__/httpUtils-test.js +39 -0
  102. package/utils/__tests__/wrapPromiseCallback-test.js +33 -0
  103. package/utils/asyncUtils.js +32 -0
  104. package/utils/httpUtils.js +105 -0
  105. package/utils/stringifyAttrs.js +14 -0
  106. package/utils/wrapPromiseCallback.js +36 -0
  107. package/versioned_data_kind.js +34 -0
package/test_data.js ADDED
@@ -0,0 +1,323 @@
1
+ const dataKind = require('./versioned_data_kind');
2
+ const { promisify } = require('util');
3
+
4
+ function TestData() {
5
+ const existingFlagBuilders = {};
6
+ const currentFlags = {};
7
+ const currentSegments = {};
8
+ const dataSourceImpls = [];
9
+
10
+ function makeInitData() {
11
+ return {
12
+ [dataKind.features.namespace]: { ...currentFlags },
13
+ [dataKind.segments.namespace]: { ...currentSegments },
14
+ };
15
+ }
16
+
17
+ const td = config => {
18
+ const featureStore = config.featureStore;
19
+
20
+ const tds = {
21
+ start: start,
22
+ stop: stop,
23
+ initialized: () => true,
24
+ close: stop,
25
+ upsert: upsert,
26
+ };
27
+
28
+ function start(cb = () => {}) {
29
+ featureStore.init(makeInitData(), cb);
30
+ }
31
+
32
+ function stop() {
33
+ dataSourceImpls.splice(dataSourceImpls.indexOf(this));
34
+ }
35
+
36
+ function upsert(kind, value, cb = () => {}) {
37
+ featureStore.upsert(kind, value, cb);
38
+ }
39
+
40
+ dataSourceImpls.push(tds);
41
+
42
+ return tds;
43
+ };
44
+
45
+ td.flag = flagName => {
46
+ if (existingFlagBuilders[flagName]) {
47
+ return existingFlagBuilders[flagName].copy();
48
+ } else {
49
+ return new TestDataFlagBuilder(flagName).booleanFlag();
50
+ }
51
+ };
52
+
53
+ td.update = flagBuilder => {
54
+ const oldItem = currentFlags[flagBuilder._key];
55
+ const oldVersion = oldItem ? oldItem.version : 0;
56
+ const newFlag = flagBuilder.build(oldVersion + 1);
57
+ currentFlags[flagBuilder._key] = newFlag;
58
+ existingFlagBuilders[flagBuilder._key] = flagBuilder.copy();
59
+
60
+ return Promise.all(dataSourceImpls.map(impl => promisify(impl.upsert)(dataKind.features, newFlag)));
61
+ };
62
+
63
+ function usePreconfiguredItem(kind, item, current) {
64
+ const oldItem = current[item.key];
65
+ const newItem = { ...item, version: oldItem ? oldItem.version + 1 : item.version };
66
+ /* eslint-disable no-param-reassign */
67
+ current[item.key] = newItem;
68
+ /* eslint-enable no-param-reassign */
69
+
70
+ return Promise.all(dataSourceImpls.map(impl => promisify(impl.upsert)(kind, newItem)));
71
+ }
72
+
73
+ td.usePreconfiguredFlag = flag => usePreconfiguredItem(dataKind.features, flag, currentFlags);
74
+
75
+ td.usePreconfiguredSegment = segment => usePreconfiguredItem(dataKind.segments, segment, currentSegments);
76
+
77
+ return td;
78
+ }
79
+
80
+ function TestDataFlagBuilder(flagName) {
81
+ this._key = flagName;
82
+ this._on = true;
83
+ this._variations = [];
84
+ }
85
+
86
+ const TRUE_VARIATION_INDEX = 0;
87
+ const FALSE_VARIATION_INDEX = 1;
88
+
89
+ function variationForBoolean(aBool) {
90
+ return aBool ? TRUE_VARIATION_INDEX : FALSE_VARIATION_INDEX;
91
+ }
92
+
93
+ TestDataFlagBuilder.prototype.copy = function () {
94
+ const to = new TestDataFlagBuilder(this._key);
95
+ to._variations = [...this._variations];
96
+ to._offVariation = this._offVariation;
97
+ to._on = this._on;
98
+ to._fallthroughVariation = this._fallthroughVariation;
99
+ to._targetsByVariation = !this._targetsByVariation ? null : new Map(this._targetsByVariation);
100
+ to._rules = !this._rules ? null : this._rules.map(r => r.copy(this));
101
+ return to;
102
+ };
103
+
104
+ TestDataFlagBuilder.prototype.isBooleanFlag = function () {
105
+ return (
106
+ this._variations.length === 2 &&
107
+ this._variations[TRUE_VARIATION_INDEX] === true &&
108
+ this._variations[FALSE_VARIATION_INDEX] === false
109
+ );
110
+ };
111
+
112
+ TestDataFlagBuilder.prototype.booleanFlag = function () {
113
+ if (this.isBooleanFlag()) {
114
+ return this;
115
+ } else {
116
+ return this.variations(true, false).fallthroughVariation(TRUE_VARIATION_INDEX).offVariation(FALSE_VARIATION_INDEX);
117
+ }
118
+ };
119
+
120
+ TestDataFlagBuilder.prototype.on = function (aBool) {
121
+ this._on = aBool;
122
+ return this;
123
+ };
124
+
125
+ TestDataFlagBuilder.prototype.fallthroughVariation = function (variation) {
126
+ if (typeof variation === 'boolean') {
127
+ this.booleanFlag().fallthroughVariation(variationForBoolean(variation));
128
+ } else {
129
+ this._fallthroughVariation = variation;
130
+ }
131
+ return this;
132
+ };
133
+
134
+ TestDataFlagBuilder.prototype.offVariation = function (variation) {
135
+ if (typeof variation === 'boolean') {
136
+ this.booleanFlag().offVariation(variationForBoolean(variation));
137
+ } else {
138
+ this._offVariation = variation;
139
+ }
140
+ return this;
141
+ };
142
+
143
+ TestDataFlagBuilder.prototype.variations = function (...values) {
144
+ this._variations = [...values];
145
+ return this;
146
+ };
147
+
148
+ TestDataFlagBuilder.prototype.clearAllTargets = function () {
149
+ this._targetsByVariation = null;
150
+ return this;
151
+ };
152
+
153
+ TestDataFlagBuilder.prototype.clearRules = function () {
154
+ this._rules = null;
155
+ return this;
156
+ };
157
+
158
+ TestDataFlagBuilder.prototype.variationForAll = function (variation) {
159
+ return this.on(true).clearRules().clearAllTargets().fallthroughVariation(variation);
160
+ };
161
+
162
+ TestDataFlagBuilder.prototype.valueForAll = function (value) {
163
+ return this.variations(value).variationForAll(0);
164
+ };
165
+
166
+ TestDataFlagBuilder.prototype.variationForContext = function (contextKind, contextKey, variation) {
167
+ if (typeof variation === 'boolean') {
168
+ return this.booleanFlag().variationForContext(contextKind, contextKey, variationForBoolean(variation));
169
+ }
170
+
171
+ if (!this._targetsByVariation) {
172
+ this._targetsByVariation = new Map();
173
+ }
174
+
175
+ this._variations.forEach((_, i) => {
176
+ if (i === variation) {
177
+ //If there is nothing set at the current variation then set it to the empty array
178
+ const targetsForVariation = this._targetsByVariation.get(i) || new Map();
179
+
180
+ if (!targetsForVariation.has(contextKind)) {
181
+ targetsForVariation.set(contextKind, []);
182
+ }
183
+ const exists = targetsForVariation.get(contextKind).indexOf(contextKey) !== -1;
184
+ // Add context to current variation set if they arent already there
185
+ if (!exists) {
186
+ targetsForVariation.get(contextKind).push(contextKey);
187
+ }
188
+
189
+ this._targetsByVariation.set(i, targetsForVariation);
190
+ } else {
191
+ // remove user from other variation set if necessary
192
+ const targetsForVariation = this._targetsByVariation.get(i);
193
+ if (targetsForVariation) {
194
+ const targetsForContextKind = targetsForVariation.get(contextKind);
195
+ if (targetsForContextKind) {
196
+ const targetIndex = targetsForContextKind.indexOf(contextKey);
197
+ if (targetIndex !== -1) {
198
+ targetsForContextKind.splice(targetIndex, 1);
199
+ if (!targetsForContextKind.length) {
200
+ targetsForVariation.delete(contextKind);
201
+ }
202
+ }
203
+ }
204
+ if (!targetsForVariation.size) {
205
+ this._targetsByVariation.delete(i);
206
+ }
207
+ }
208
+ }
209
+ });
210
+
211
+ return this;
212
+ };
213
+
214
+ TestDataFlagBuilder.prototype.variationForUser = function (key, variation) {
215
+ return this.variationForContext('user', key, variation);
216
+ };
217
+
218
+ TestDataFlagBuilder.prototype.addRule = function (flagRuleBuilder) {
219
+ if (!this._rules) {
220
+ this._rules = [];
221
+ }
222
+ this._rules.push(flagRuleBuilder);
223
+ };
224
+
225
+ TestDataFlagBuilder.prototype.ifMatch = function (contextKind, attribute, ...values) {
226
+ const flagRuleBuilder = new TestDataRuleBuilder(this);
227
+ return flagRuleBuilder.andMatch(contextKind, attribute, ...values);
228
+ };
229
+
230
+ TestDataFlagBuilder.prototype.ifNotMatch = function (contextKind, attribute, ...values) {
231
+ const flagRuleBuilder = new TestDataRuleBuilder(this);
232
+ return flagRuleBuilder.andNotMatch(contextKind, attribute, ...values);
233
+ };
234
+
235
+ TestDataFlagBuilder.prototype.build = function (version) {
236
+ const baseFlagObject = {
237
+ key: this._key,
238
+ version: version,
239
+ on: this._on,
240
+ offVariation: this._offVariation,
241
+ fallthrough: {
242
+ variation: this._fallthroughVariation,
243
+ },
244
+ variations: [...this._variations],
245
+ };
246
+
247
+ if (this._targetsByVariation) {
248
+ const contextTargets = [];
249
+ for (const [variation, contextTargetsForVariation] of this._targetsByVariation) {
250
+ for (const [contextKind, values] of contextTargetsForVariation) {
251
+ contextTargets.push({
252
+ contextKind,
253
+ values,
254
+ variation,
255
+ });
256
+ }
257
+ }
258
+ baseFlagObject.contextTargets = contextTargets;
259
+ }
260
+
261
+ if (this._rules) {
262
+ baseFlagObject.rules = this._rules.map((rule, i) => rule.build(i));
263
+ }
264
+
265
+ return baseFlagObject;
266
+ };
267
+
268
+ /* TestDataRuleBuilder */
269
+ function TestDataRuleBuilder(flagBuilder) {
270
+ this._flagBuilder = flagBuilder;
271
+ this._clauses = [];
272
+ this._variation = null;
273
+ }
274
+
275
+ TestDataRuleBuilder.prototype.andMatch = function (contextKind, attribute, ...values) {
276
+ this._clauses.push({
277
+ contextKind,
278
+ attribute: attribute,
279
+ op: 'in',
280
+ values: values,
281
+ negate: false,
282
+ });
283
+ return this;
284
+ };
285
+
286
+ TestDataRuleBuilder.prototype.andNotMatch = function (contextKind, attribute, ...values) {
287
+ this._clauses.push({
288
+ contextKind,
289
+ attribute: attribute,
290
+ op: 'in',
291
+ values: values,
292
+ negate: true,
293
+ });
294
+ return this;
295
+ };
296
+
297
+ TestDataRuleBuilder.prototype.thenReturn = function (variation) {
298
+ if (typeof variation === 'boolean') {
299
+ this._flagBuilder.booleanFlag();
300
+ return this.thenReturn(variationForBoolean(variation));
301
+ }
302
+
303
+ this._variation = variation;
304
+ this._flagBuilder.addRule(this);
305
+ return this._flagBuilder;
306
+ };
307
+
308
+ TestDataRuleBuilder.prototype.build = function (id) {
309
+ return {
310
+ id: 'rule' + id,
311
+ variation: this._variation,
312
+ clauses: this._clauses,
313
+ };
314
+ };
315
+
316
+ TestDataRuleBuilder.prototype.copy = function (flagBuilder) {
317
+ const flagRuleBuilder = new TestDataRuleBuilder(flagBuilder);
318
+ flagRuleBuilder._clauses = JSON.parse(JSON.stringify(this._clauses));
319
+ flagRuleBuilder._variation = JSON.parse(JSON.stringify(this._variation));
320
+ return flagRuleBuilder;
321
+ };
322
+
323
+ module.exports = TestData;
package/tsconfig.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "module": "commonjs",
4
+ "strict": true,
5
+ "lib": [
6
+ "dom",
7
+ "es6"
8
+ ]
9
+ },
10
+ "files": [
11
+ "index.d.ts",
12
+ "test-types.ts"
13
+ ]
14
+ }
@@ -0,0 +1,28 @@
1
+ function UpdateQueue() {
2
+ const updateQueue = [];
3
+ this.enqueue = (updateFn, fnArgs, cb) => {
4
+ updateQueue.push([updateFn, fnArgs, cb]);
5
+ if (updateQueue.length === 1) {
6
+ // if nothing else is in progress, we can start this one right away
7
+ executePendingUpdates();
8
+ }
9
+ };
10
+ function executePendingUpdates() {
11
+ if (updateQueue.length > 0) {
12
+ const entry = updateQueue[0];
13
+ const fn = entry[0];
14
+ const args = entry[1];
15
+ const cb = entry[2];
16
+ const newCb = () => {
17
+ updateQueue.shift();
18
+ if (updateQueue.length > 0) {
19
+ setImmediate(executePendingUpdates);
20
+ }
21
+ cb && cb();
22
+ };
23
+ fn.apply(null, args.concat([newCb]));
24
+ }
25
+ }
26
+ }
27
+
28
+ module.exports = UpdateQueue;
@@ -0,0 +1,39 @@
1
+ const httpUtils = require('../httpUtils');
2
+ const packageJson = require('../../package.json');
3
+
4
+ it('sets SDK key', () => {
5
+ const h = httpUtils.getDefaultHeaders('my-sdk-key', {});
6
+ expect(h).toMatchObject({ authorization: 'my-sdk-key' });
7
+ });
8
+
9
+ it('sets user agent', () => {
10
+ const h = httpUtils.getDefaultHeaders('my-sdk-key', {});
11
+ expect(h).toMatchObject({ 'user-agent': 'NodeJSClient/' + packageJson.version });
12
+ });
13
+
14
+ it('does not include wrapper header by default', () => {
15
+ const h = httpUtils.getDefaultHeaders('my-sdk-key', {});
16
+ expect(h['x-launchdarkly-wrapper']).toBeUndefined();
17
+ });
18
+
19
+ it('sets wrapper header with name only', () => {
20
+ const h = httpUtils.getDefaultHeaders('my-sdk-key', { wrapperName: 'my-wrapper' });
21
+ expect(h).toMatchObject({ 'x-launchdarkly-wrapper': 'my-wrapper' });
22
+ });
23
+
24
+ it('sets wrapper header with name and version', () => {
25
+ const h = httpUtils.getDefaultHeaders('my-sdk-key', { wrapperName: 'my-wrapper', wrapperVersion: '2.0' });
26
+ expect(h).toMatchObject({ 'x-launchdarkly-wrapper': 'my-wrapper/2.0' });
27
+ });
28
+
29
+ it('sets the X-LaunchDarkly-Tags header with valid tags.', () => {
30
+ const h = httpUtils.getDefaultHeaders('my-sdk-key', {
31
+ application: {
32
+ id: 'test-application',
33
+ version: 'test-version',
34
+ },
35
+ });
36
+ expect(h).toMatchObject({
37
+ 'x-launchdarkly-tags': 'application-id/test-application application-version/test-version',
38
+ });
39
+ });
@@ -0,0 +1,33 @@
1
+ const wrapPromiseCallback = require('../wrapPromiseCallback');
2
+
3
+ describe('wrapPromiseCallback', () => {
4
+ it('should resolve to the value', () => {
5
+ const promise = wrapPromiseCallback(Promise.resolve('woohoo'));
6
+ return expect(promise).resolves.toBe('woohoo');
7
+ });
8
+
9
+ it('should reject with the error', () => {
10
+ const error = new Error('something went wrong');
11
+ const promise = wrapPromiseCallback(Promise.reject(error));
12
+ return expect(promise).rejects.toBe(error);
13
+ });
14
+
15
+ it('should call the callback with a value if the promise resolves', done => {
16
+ const promise = wrapPromiseCallback(Promise.resolve('woohoo'), (error, value) => {
17
+ expect(promise).toBeUndefined();
18
+ expect(error).toBeNull();
19
+ expect(value).toBe('woohoo');
20
+ done();
21
+ });
22
+ });
23
+
24
+ it('should call the callback with an error if the promise rejects', done => {
25
+ const actualError = new Error('something went wrong');
26
+ const promise = wrapPromiseCallback(Promise.reject(actualError), (error, value) => {
27
+ expect(promise).toBeUndefined();
28
+ expect(error).toBe(actualError);
29
+ expect(value).toBeNull();
30
+ done();
31
+ });
32
+ });
33
+ });
@@ -0,0 +1,32 @@
1
+ const async = require('async');
2
+
3
+ // The safeAsync functions allow us to use async collection functions as efficiently as possible
4
+ // while avoiding stack overflows. When the async utilities call our iteratee, they provide a
5
+ // callback for delivering a result. Calling that callback directly is efficient (we are not
6
+ // really worried about blocking a thread by doing too many computations without yielding; our
7
+ // flag evaluations are pretty fast, and if we end up having to do any I/O, that will cause us
8
+ // to yield anyway)... but, if there are many items in the collection, it will result in too
9
+ // many nested calls. So, we'll pick an arbitrary threshold of how many items can be in the
10
+ // collection before we switch over to deferring the callbacks with setImmediate().
11
+
12
+ const maxNestedCalls = 50;
13
+
14
+ function safeIteratee(collection, iteratee) {
15
+ if (!collection || collection.length <= maxNestedCalls) {
16
+ return iteratee;
17
+ }
18
+ return (value, callback) => iteratee(value, (...args) => setImmediate(callback, ...args));
19
+ }
20
+
21
+ function safeAsyncEach(collection, iteratee, resultCallback) {
22
+ return async.each(collection, safeIteratee(collection, iteratee), resultCallback);
23
+ }
24
+
25
+ function safeAsyncEachSeries(collection, iteratee, resultCallback) {
26
+ return async.eachSeries(collection, safeIteratee(collection, iteratee), resultCallback);
27
+ }
28
+
29
+ module.exports = {
30
+ safeAsyncEach: safeAsyncEach,
31
+ safeAsyncEachSeries: safeAsyncEachSeries,
32
+ };
@@ -0,0 +1,105 @@
1
+ const http = require('http');
2
+ const https = require('https');
3
+ const url = require('url');
4
+ const configuration = require('../configuration');
5
+
6
+ const packageJson = require('../package.json');
7
+
8
+ const userAgent = 'NodeJSClient/' + packageJson.version;
9
+
10
+ function getDefaultHeaders(sdkKey, config) {
11
+ // Use lowercase header names for convenience in our test code, where we may be checking for headers in a
12
+ // real HTTP request that will be lowercased by the request API
13
+ const ret = {
14
+ authorization: sdkKey,
15
+ 'user-agent': userAgent,
16
+ };
17
+ if (config.wrapperName) {
18
+ ret['x-launchdarkly-wrapper'] = config.wrapperVersion
19
+ ? config.wrapperName + '/' + config.wrapperVersion
20
+ : config.wrapperName;
21
+ }
22
+ const tags = configuration.getTags(config);
23
+ const tagKeys = Object.keys(tags);
24
+ if (tagKeys.length) {
25
+ ret['x-launchdarkly-tags'] = tagKeys
26
+ .sort()
27
+ .flatMap(key =>
28
+ Array.isArray(tags[key]) ? tags[key].sort().map(value => `${key}/${value}`) : [`${key}/${tags[key]}`]
29
+ )
30
+ .join(' ');
31
+ }
32
+ return ret;
33
+ }
34
+
35
+ // Convenience wrapper for making an HTTP/HTTPS request via Node's standard modules. Unlike http.request,
36
+ // the callback takes (error, response, body) parameters instead of just (response).
37
+ function httpRequest(requestUrl, options, body, config, callback) {
38
+ // Note: https.request allows a url parameter to be passed separately from options, but only in v10.9.0+, so
39
+ // we still have to parse the URL until our minimum Node version is increased.
40
+ const urlOpts = url.parse(requestUrl);
41
+ const isSecure = urlOpts.protocol === 'https:';
42
+ const allOptions = Object.assign(
43
+ {},
44
+ config && config.tlsParams,
45
+ urlOpts,
46
+ {
47
+ timeout: config && config.timeout ? config.timeout * 1000 : undefined,
48
+ agent: config && config.proxyAgent,
49
+ },
50
+ options
51
+ );
52
+ const req = (isSecure ? https : http).request(allOptions, resp => {
53
+ let body = '';
54
+ resp.on('data', chunk => {
55
+ body += chunk;
56
+ });
57
+ resp.on('end', () => {
58
+ callback(null, resp, body);
59
+ });
60
+ });
61
+ req.on('error', err => {
62
+ callback(err);
63
+ });
64
+ if (body !== null && body !== undefined) {
65
+ req.write(body);
66
+ }
67
+ req.end();
68
+ }
69
+
70
+ // Creates an in-memory etag cache and returns a wrapper for httpRequest that uses the cache. This is a
71
+ // naive implementation that does not place a bound on the cache; the SDK will normally always be hitting
72
+ // the same URL (the only time we don't is if we get an "indirect/put" stream event, but in that case we
73
+ // deliberately do not use the cache).
74
+ function httpWithETagCache() {
75
+ const cache = {};
76
+ return (requestUrl, options, body, config, callback) => {
77
+ const cacheEntry = cache[requestUrl];
78
+ const cachedEtag = cacheEntry && cacheEntry.etag;
79
+ let newOptions = options;
80
+ if (cachedEtag) {
81
+ const newHeaders = Object.assign({}, options && options.headers, { 'if-none-match': cachedEtag });
82
+ newOptions = Object.assign({}, options, { headers: newHeaders });
83
+ }
84
+ return httpRequest(requestUrl, newOptions, body, config, (err, resp, body) => {
85
+ if (err) {
86
+ callback(err);
87
+ } else {
88
+ if (resp.statusCode === 304 && cacheEntry) {
89
+ callback(null, resp, cacheEntry.body);
90
+ } else {
91
+ if (resp.headers['etag']) {
92
+ cache[requestUrl] = { etag: resp.headers['etag'], body };
93
+ }
94
+ callback(null, resp, body);
95
+ }
96
+ }
97
+ });
98
+ };
99
+ }
100
+
101
+ module.exports = {
102
+ getDefaultHeaders,
103
+ httpRequest,
104
+ httpWithETagCache,
105
+ };
@@ -0,0 +1,14 @@
1
+ module.exports = function stringifyAttrs(object, attrs) {
2
+ if (!object) {
3
+ return object;
4
+ }
5
+ let newObject;
6
+ for (const attr of attrs) {
7
+ const value = object[attr];
8
+ if (value !== undefined && typeof value !== 'string') {
9
+ newObject = newObject || Object.assign({}, object);
10
+ newObject[attr] = String(value);
11
+ }
12
+ }
13
+ return newObject || object;
14
+ };
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Wrap a promise to invoke an optional callback upon resolution or rejection.
3
+ *
4
+ * This function assumes the callback follows the Node.js callback type: (err, value) => void
5
+ *
6
+ * If a callback is provided:
7
+ * - if the promise is resolved, invoke the callback with (null, value)
8
+ * - if the promise is rejected, invoke the callback with (error, null)
9
+ *
10
+ * @param {Promise<any>} promise
11
+ * @param {Function} callback
12
+ * @returns Promise<any> | undefined
13
+ */
14
+ module.exports = function wrapPromiseCallback(promise, callback) {
15
+ const ret = promise.then(
16
+ value => {
17
+ if (callback) {
18
+ setImmediate(() => {
19
+ callback(null, value);
20
+ });
21
+ }
22
+ return value;
23
+ },
24
+ error => {
25
+ if (callback) {
26
+ setImmediate(() => {
27
+ callback(error, null);
28
+ });
29
+ } else {
30
+ return Promise.reject(error);
31
+ }
32
+ }
33
+ );
34
+
35
+ return !callback ? ret : undefined;
36
+ };
@@ -0,0 +1,34 @@
1
+ /*
2
+ These objects denote the types of data that can be stored in the feature store and
3
+ referenced in the API. If we add another storable data type in the future, as long as it
4
+ follows the same pattern (having "key", "version", and "deleted" properties), we only need
5
+ to add a corresponding constant here and the existing store should be able to handle it.
6
+
7
+ Note, for things to work correctly, the "namespace" property must match the key used in
8
+ module.exports.
9
+ */
10
+
11
+ const features = {
12
+ namespace: 'features',
13
+ streamApiPath: '/flags/',
14
+ requestPath: '/sdk/latest-flags/',
15
+ priority: 1,
16
+ getDependencyKeys: flag => {
17
+ if (!flag.prerequisites || !flag.prerequisites.length) {
18
+ return [];
19
+ }
20
+ return flag.prerequisites.map(p => p.key);
21
+ },
22
+ };
23
+
24
+ const segments = {
25
+ namespace: 'segments',
26
+ streamApiPath: '/segments/',
27
+ requestPath: '/sdk/latest-segments/',
28
+ priority: 0,
29
+ };
30
+
31
+ module.exports = {
32
+ features: features,
33
+ segments: segments,
34
+ };