@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
@@ -0,0 +1,258 @@
1
+ const dataKind = require('./versioned_data_kind');
2
+
3
+ function NamespacedDataSet() {
4
+ let itemsByNamespace = {};
5
+
6
+ function get(namespace, key) {
7
+ const items = itemsByNamespace[namespace];
8
+ return items && items[key];
9
+ }
10
+
11
+ function set(namespace, key, value) {
12
+ let items = itemsByNamespace[namespace];
13
+ if (!items) {
14
+ items = {};
15
+ itemsByNamespace[namespace] = items;
16
+ }
17
+ items[key] = value;
18
+ }
19
+
20
+ function remove(namespace, key) {
21
+ const items = itemsByNamespace[namespace];
22
+ if (items) {
23
+ delete items[key];
24
+ }
25
+ }
26
+
27
+ function removeAll() {
28
+ itemsByNamespace = {};
29
+ }
30
+
31
+ function enumerate(callback) {
32
+ for (const [ns, items] of Object.entries(itemsByNamespace)) {
33
+ const keys = Object.keys(items).sort(); // sort to make tests determinate
34
+ for (const key of keys) {
35
+ callback(ns, key, items[key]);
36
+ }
37
+ }
38
+ }
39
+
40
+ function mergeFrom(otherSet) {
41
+ otherSet.enumerate(set);
42
+ }
43
+
44
+ return {
45
+ get: get,
46
+ set: set,
47
+ remove: remove,
48
+ removeAll: removeAll,
49
+ enumerate: enumerate,
50
+ mergeFrom: mergeFrom,
51
+ toString: () => JSON.stringify(itemsByNamespace),
52
+ };
53
+ }
54
+
55
+ function DependencyTracker() {
56
+ const dependenciesFrom = NamespacedDataSet();
57
+ const dependenciesTo = NamespacedDataSet();
58
+ // dependenciesFrom: for a given flag/segment key, what are the flags/segments it relies on
59
+ // dependenciesTo: for a given flag/segment key, what are the flags/segments that rely on it
60
+
61
+ function updateDependenciesFrom(namespace, key, newDependencySet) {
62
+ const oldDependencySet = dependenciesFrom.get(namespace, key);
63
+ oldDependencySet &&
64
+ oldDependencySet.enumerate((depNs, depKey) => {
65
+ const depsToThisDep = dependenciesTo.get(depNs, depKey);
66
+ depsToThisDep && depsToThisDep.remove(namespace, key);
67
+ });
68
+
69
+ dependenciesFrom.set(namespace, key, newDependencySet);
70
+ newDependencySet &&
71
+ newDependencySet.enumerate((depNs, depKey) => {
72
+ let depsToThisDep = dependenciesTo.get(depNs, depKey);
73
+ if (!depsToThisDep) {
74
+ depsToThisDep = NamespacedDataSet();
75
+ dependenciesTo.set(depNs, depKey, depsToThisDep);
76
+ }
77
+ depsToThisDep.set(namespace, key, true);
78
+ });
79
+ }
80
+
81
+ function updateModifiedItems(inDependencySet, modifiedNamespace, modifiedKey) {
82
+ if (!inDependencySet.get(modifiedNamespace, modifiedKey)) {
83
+ inDependencySet.set(modifiedNamespace, modifiedKey, true);
84
+ const affectedItems = dependenciesTo.get(modifiedNamespace, modifiedKey);
85
+ affectedItems &&
86
+ affectedItems.enumerate((ns, key) => {
87
+ updateModifiedItems(inDependencySet, ns, key);
88
+ });
89
+ }
90
+ }
91
+
92
+ function reset() {
93
+ dependenciesFrom.removeAll();
94
+ dependenciesTo.removeAll();
95
+ }
96
+
97
+ return {
98
+ updateDependenciesFrom: updateDependenciesFrom,
99
+ updateModifiedItems: updateModifiedItems,
100
+ reset: reset,
101
+ };
102
+ }
103
+
104
+ function FeatureStoreEventWrapper(featureStore, emitter) {
105
+ const dependencyTracker = DependencyTracker();
106
+
107
+ function hasEventListeners() {
108
+ // Before we do something that could generate a change event, we'll check whether anyone is
109
+ // currently listening for such events. If they're not, then we can skip the whole "query the
110
+ // old data so we can compare it to the new data and see if there was a change" step, which
111
+ // could be expensive with a persistent feature store.
112
+ return emitter.eventNames().some(name => name === 'update' || name.substring(0, 7) === 'update:'); // Node 6 may not have startsWith()
113
+ }
114
+
115
+ function addIfModified(namespace, key, oldValue, newValue, toDataSet) {
116
+ if (newValue && oldValue && newValue.version <= oldValue.version) {
117
+ return;
118
+ }
119
+ dependencyTracker.updateModifiedItems(toDataSet, namespace, key);
120
+ }
121
+
122
+ function sendChangeEvents(dataSet) {
123
+ dataSet.enumerate((namespace, key) => {
124
+ if (namespace === dataKind.features.namespace) {
125
+ const arg = { key: key };
126
+ setImmediate(() => {
127
+ emitter.emit('update', arg);
128
+ });
129
+ setImmediate(() => {
130
+ emitter.emit(`update:${key}`, arg);
131
+ });
132
+ }
133
+ });
134
+ }
135
+
136
+ function computeDependencies(kind, item) {
137
+ const ret = NamespacedDataSet();
138
+ if (kind === dataKind.features) {
139
+ for (const prereq of item.prerequisites || []) {
140
+ ret.set(dataKind.features.namespace, prereq.key, true);
141
+ }
142
+ }
143
+
144
+ if (kind === dataKind.features || kind === dataKind.segments) {
145
+ for (const rule of item.rules || []) {
146
+ for (const clause of rule.clauses || []) {
147
+ if (clause.op === 'segmentMatch') {
148
+ for (const value of clause.values) {
149
+ ret.set(dataKind.segments.namespace, value, true);
150
+ }
151
+ }
152
+ }
153
+ }
154
+ }
155
+ return ret;
156
+ }
157
+
158
+ return {
159
+ get: featureStore.get.bind(featureStore),
160
+ all: featureStore.all.bind(featureStore),
161
+ initialized: featureStore.initialized.bind(featureStore),
162
+ close: featureStore.close.bind(featureStore),
163
+
164
+ init: (newData, callback) => {
165
+ const checkForChanges = hasEventListeners();
166
+ const doInit = oldData => {
167
+ featureStore.init(newData, () => {
168
+ dependencyTracker.reset();
169
+
170
+ for (const [namespace, items] of Object.entries(newData)) {
171
+ const kind = dataKind[namespace];
172
+ for (const key of Object.keys(items || {})) {
173
+ const item = items[key];
174
+ dependencyTracker.updateDependenciesFrom(namespace, key, computeDependencies(kind, item));
175
+ }
176
+ }
177
+
178
+ if (checkForChanges) {
179
+ const updatedItems = NamespacedDataSet();
180
+ for (const namespace of Object.keys(newData)) {
181
+ const oldDataForKind = oldData[namespace];
182
+ const newDataForKind = newData[namespace];
183
+ const mergedData = Object.assign({}, oldDataForKind, newDataForKind);
184
+ for (const key of Object.keys(mergedData)) {
185
+ addIfModified(
186
+ namespace,
187
+ key,
188
+ oldDataForKind && oldDataForKind[key],
189
+ newDataForKind && newDataForKind[key],
190
+ updatedItems
191
+ );
192
+ }
193
+ }
194
+ sendChangeEvents(updatedItems);
195
+ }
196
+
197
+ callback && callback();
198
+ });
199
+ };
200
+
201
+ if (checkForChanges) {
202
+ featureStore.all(dataKind.features, oldFlags => {
203
+ featureStore.all(dataKind.segments, oldSegments => {
204
+ const oldData = {};
205
+ oldData[dataKind.features.namespace] = oldFlags;
206
+ oldData[dataKind.segments.namespace] = oldSegments;
207
+ doInit(oldData);
208
+ });
209
+ });
210
+ } else {
211
+ doInit();
212
+ }
213
+ },
214
+
215
+ delete: (kind, key, version, callback) => {
216
+ const checkForChanges = hasEventListeners();
217
+ const doDelete = oldItem => {
218
+ featureStore.delete(kind, key, version, () => {
219
+ dependencyTracker.updateDependenciesFrom(kind.namespace, key, null);
220
+ if (checkForChanges) {
221
+ const updatedItems = NamespacedDataSet();
222
+ addIfModified(kind.namespace, key, oldItem, { version: version, deleted: true }, updatedItems);
223
+ sendChangeEvents(updatedItems);
224
+ }
225
+ callback && callback();
226
+ });
227
+ };
228
+ if (checkForChanges) {
229
+ featureStore.get(kind, key, doDelete);
230
+ } else {
231
+ doDelete();
232
+ }
233
+ },
234
+
235
+ upsert: (kind, newItem, callback) => {
236
+ const key = newItem.key;
237
+ const checkForChanges = hasEventListeners();
238
+ const doUpsert = oldItem => {
239
+ featureStore.upsert(kind, newItem, () => {
240
+ dependencyTracker.updateDependenciesFrom(kind.namespace, key, computeDependencies(kind, newItem));
241
+ if (checkForChanges) {
242
+ const updatedItems = NamespacedDataSet();
243
+ addIfModified(kind.namespace, key, oldItem, newItem, updatedItems);
244
+ sendChangeEvents(updatedItems);
245
+ }
246
+ callback && callback();
247
+ });
248
+ };
249
+ if (checkForChanges) {
250
+ featureStore.get(kind, key, doUpsert);
251
+ } else {
252
+ doUpsert();
253
+ }
254
+ },
255
+ };
256
+ }
257
+
258
+ module.exports = FeatureStoreEventWrapper;
@@ -0,0 +1,192 @@
1
+ const fs = require('fs'),
2
+ dataKind = require('./versioned_data_kind'),
3
+ loggers = require('./loggers');
4
+
5
+ let yamlAvailable;
6
+ let yamlParser;
7
+
8
+ /*
9
+ FileDataSource provides a way to use local files as a source of feature flag state, instead of
10
+ connecting to LaunchDarkly. This would typically be used in a test environment.
11
+
12
+ See documentation in index.d.ts.
13
+ */
14
+ function FileDataSource(options) {
15
+ if (yamlAvailable === undefined) {
16
+ try {
17
+ const yaml = require('yaml');
18
+ yamlAvailable = true;
19
+ yamlParser = yaml.parse;
20
+ } catch (err) {
21
+ yamlAvailable = false;
22
+ }
23
+ }
24
+ // If the yaml package is available, we can use its parser for all files because
25
+ // every valid JSON document is also a valid YAML document.
26
+ const parseData = yamlAvailable ? yamlParser : JSON.parse;
27
+
28
+ const paths = (options && options.paths) || [];
29
+ const autoUpdate = !!options.autoUpdate;
30
+
31
+ return config => {
32
+ const logger = options.logger || config.logger || loggers.nullLogger();
33
+ const featureStore = config.featureStore;
34
+ const timestamps = {};
35
+ let watchers = [];
36
+ let pendingUpdate = false;
37
+ let inited = false;
38
+
39
+ function getFileTimestampPromise(path) {
40
+ return new Promise((resolve, reject) => {
41
+ fs.stat(path, (err, stat) => {
42
+ if (err) {
43
+ reject(err);
44
+ } else {
45
+ resolve(stat.mtimeMs || stat.mtime); // mtimeMs isn't always available; either of these values will work for us
46
+ }
47
+ });
48
+ });
49
+ }
50
+
51
+ function loadFilePromise(path, allDataIn) {
52
+ const allData = allDataIn;
53
+ return new Promise((resolve, reject) =>
54
+ fs.readFile(path, 'utf8', (err, data) => (err ? reject(err) : resolve(data)))
55
+ )
56
+ .then(data => {
57
+ const parsed = parseData(data) || {};
58
+ const addItem = (kind, item) => {
59
+ if (!allData[kind.namespace]) {
60
+ allData[kind.namespace] = {};
61
+ }
62
+ if (allData[kind.namespace][item.key]) {
63
+ throw new Error('found duplicate key: "' + item.key + '"');
64
+ } else {
65
+ allData[kind.namespace][item.key] = item;
66
+ }
67
+ };
68
+ Object.keys(parsed.flags || {}).forEach(key => {
69
+ addItem(dataKind.features, parsed.flags[key]);
70
+ });
71
+ Object.keys(parsed.flagValues || {}).forEach(key => {
72
+ addItem(dataKind.features, makeFlagWithValue(key, parsed.flagValues[key]));
73
+ });
74
+ Object.keys(parsed.segments || {}).forEach(key => {
75
+ addItem(dataKind.segments, parsed.segments[key]);
76
+ });
77
+ logger.info('Loaded flags from ' + path);
78
+ })
79
+ .then(() => getFileTimestampPromise(path))
80
+ .then(timestamp => {
81
+ timestamps[path] = timestamp;
82
+ });
83
+ }
84
+
85
+ function loadAllPromise() {
86
+ pendingUpdate = false;
87
+ const allData = {};
88
+ let p = Promise.resolve();
89
+ for (let i = 0; i < paths.length; i++) {
90
+ (path => {
91
+ p = p
92
+ .then(() => loadFilePromise(path, allData))
93
+ .catch(e => {
94
+ throw new Error('Unable to load flags: ' + e + ' [' + path + ']');
95
+ });
96
+ })(paths[i]);
97
+ }
98
+ return p.then(() => initStorePromise(allData));
99
+ }
100
+
101
+ function initStorePromise(data) {
102
+ return new Promise(resolve =>
103
+ featureStore.init(data, () => {
104
+ inited = true;
105
+ resolve();
106
+ })
107
+ );
108
+ }
109
+
110
+ function makeFlagWithValue(key, value) {
111
+ return {
112
+ key: key,
113
+ on: true,
114
+ fallthrough: { variation: 0 },
115
+ variations: [value],
116
+ };
117
+ }
118
+
119
+ function maybeReloadForPath(path) {
120
+ if (pendingUpdate) {
121
+ return; // coalesce updates so we don't do multiple reloads if a whole set of files was just updated
122
+ }
123
+ const reload = () => {
124
+ loadAllPromise()
125
+ .then(() => {
126
+ logger.warn('Reloaded flags from file data');
127
+ })
128
+ .catch(() => {});
129
+ };
130
+ getFileTimestampPromise(path)
131
+ .then(timestamp => {
132
+ // We do this check of the modified time because there's a known issue with fs.watch()
133
+ // reporting multiple changes when really the file has only changed once.
134
+ if (timestamp !== timestamps[path]) {
135
+ pendingUpdate = true;
136
+ setTimeout(reload, 10);
137
+ // The 10ms delay above is arbitrary - we just don't want to have the number be zero,
138
+ // because in a case where multiple fs.watch events are fired off one after another,
139
+ // we want the reload to happen only after all of the event handlers have executed.
140
+ }
141
+ })
142
+ .catch(() => {
143
+ logger.warn('Unexpected error trying to get timestamp of file: ' + path);
144
+ });
145
+ }
146
+
147
+ function startWatching() {
148
+ paths.forEach(path => {
149
+ const watcher = fs.watch(path, { persistent: false }, () => {
150
+ maybeReloadForPath(path);
151
+ });
152
+ watchers.push(watcher);
153
+ });
154
+ }
155
+
156
+ function stopWatching() {
157
+ watchers.forEach(w => w.close());
158
+ watchers = [];
159
+ }
160
+
161
+ const fds = {};
162
+
163
+ fds.start = fn => {
164
+ const cb = fn || (() => {});
165
+
166
+ if (autoUpdate) {
167
+ startWatching();
168
+ }
169
+
170
+ loadAllPromise().then(
171
+ () => cb(),
172
+ err => cb(err)
173
+ );
174
+ };
175
+
176
+ fds.stop = () => {
177
+ if (autoUpdate) {
178
+ stopWatching();
179
+ }
180
+ };
181
+
182
+ fds.initialized = () => inited;
183
+
184
+ fds.close = () => {
185
+ fds.stop();
186
+ };
187
+
188
+ return fds;
189
+ };
190
+ }
191
+
192
+ module.exports = FileDataSource;
package/flags_state.js ADDED
@@ -0,0 +1,46 @@
1
+ function FlagsStateBuilder(valid, withReasons) {
2
+ const builder = {};
3
+ const flagValues = {};
4
+ const flagMetadata = {};
5
+
6
+ builder.addFlag = (flag, value, variation, reason, trackEvents, trackReason, detailsOnlyIfTracked) => {
7
+ flagValues[flag.key] = value;
8
+ const meta = {};
9
+ if (variation !== undefined && variation !== null) {
10
+ meta.variation = variation;
11
+ }
12
+ const omitDetails =
13
+ detailsOnlyIfTracked &&
14
+ !trackEvents &&
15
+ !trackReason &&
16
+ (flag.debugEventsUntilDate === undefined || flag.debugEventsUntilDate === null);
17
+ if (!omitDetails) {
18
+ meta.version = flag.version;
19
+ }
20
+ if (reason && (trackReason || (withReasons && !omitDetails))) {
21
+ meta.reason = reason;
22
+ }
23
+ if (trackEvents) {
24
+ meta.trackEvents = true;
25
+ }
26
+ if (trackReason) {
27
+ meta.trackReason = true;
28
+ }
29
+ if (flag.debugEventsUntilDate !== undefined && flag.debugEventsUntilDate !== null) {
30
+ meta.debugEventsUntilDate = flag.debugEventsUntilDate;
31
+ }
32
+ flagMetadata[flag.key] = meta;
33
+ };
34
+
35
+ builder.build = () => ({
36
+ valid: valid,
37
+ allValues: () => flagValues,
38
+ getFlagValue: key => flagValues[key],
39
+ getFlagReason: key => (flagMetadata[key] ? flagMetadata[key].reason : null),
40
+ toJSON: () => Object.assign({}, flagValues, { $flagsState: flagMetadata, $valid: valid }),
41
+ });
42
+
43
+ return builder;
44
+ }
45
+
46
+ module.exports = FlagsStateBuilder;