@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,121 @@
1
+ function isExperiment(flag, reason) {
2
+ if (reason) {
3
+ // If the reason says we're in an experiment, we are. Otherwise, apply
4
+ // the legacy rule exclusion logic.
5
+ if (reason.inExperiment) {
6
+ return true;
7
+ }
8
+ switch (reason.kind) {
9
+ case 'RULE_MATCH': {
10
+ const index = reason.ruleIndex;
11
+ if (index !== undefined) {
12
+ const rules = flag.rules || [];
13
+ return index >= 0 && index < rules.length && !!rules[index].trackEvents;
14
+ }
15
+ break;
16
+ }
17
+ case 'FALLTHROUGH':
18
+ return !!flag.trackEventsFallthrough;
19
+ }
20
+ }
21
+ return false;
22
+ }
23
+
24
+ function EventFactory(withReasons) {
25
+ const ef = {};
26
+
27
+ ef.newEvalEvent = (flag, context, detail, defaultVal, prereqOfFlag) => {
28
+ const addExperimentData = isExperiment(flag, detail.reason);
29
+ const e = {
30
+ kind: 'feature',
31
+ creationDate: new Date().getTime(),
32
+ key: flag.key,
33
+ context,
34
+ value: detail.value,
35
+ variation: detail.variationIndex,
36
+ default: defaultVal,
37
+ version: flag.version,
38
+ };
39
+ // the following properties are handled separately so we don't waste bandwidth on unused keys
40
+ if (addExperimentData || flag.trackEvents) {
41
+ e.trackEvents = true;
42
+ }
43
+ if (flag.debugEventsUntilDate) {
44
+ e.debugEventsUntilDate = flag.debugEventsUntilDate;
45
+ }
46
+ if (prereqOfFlag) {
47
+ e.prereqOf = prereqOfFlag.key;
48
+ }
49
+ if (addExperimentData || withReasons) {
50
+ e.reason = detail.reason;
51
+ }
52
+ return e;
53
+ };
54
+
55
+ ef.newDefaultEvent = (flag, context, detail) => {
56
+ const e = {
57
+ kind: 'feature',
58
+ creationDate: new Date().getTime(),
59
+ key: flag.key,
60
+ context,
61
+ value: detail.value,
62
+ default: detail.value,
63
+ version: flag.version,
64
+ };
65
+ // the following properties are handled separately so we don't waste bandwidth on unused keys
66
+ if (flag.trackEvents) {
67
+ e.trackEvents = true;
68
+ }
69
+ if (flag.debugEventsUntilDate) {
70
+ e.debugEventsUntilDate = flag.debugEventsUntilDate;
71
+ }
72
+ if (withReasons) {
73
+ e.reason = detail.reason;
74
+ }
75
+ return e;
76
+ };
77
+
78
+ ef.newUnknownFlagEvent = (key, context, detail) => {
79
+ const e = {
80
+ kind: 'feature',
81
+ creationDate: new Date().getTime(),
82
+ key: key,
83
+ context,
84
+ value: detail.value,
85
+ default: detail.value,
86
+ };
87
+ if (withReasons) {
88
+ e.reason = detail.reason;
89
+ }
90
+ return e;
91
+ };
92
+
93
+ ef.newIdentifyEvent = context => ({
94
+ kind: 'identify',
95
+ creationDate: new Date().getTime(),
96
+ context,
97
+ });
98
+
99
+ ef.newCustomEvent = (eventName, context, data, metricValue) => {
100
+ const e = {
101
+ kind: 'custom',
102
+ creationDate: new Date().getTime(),
103
+ key: eventName,
104
+ context,
105
+ };
106
+ if (data !== null && data !== undefined) {
107
+ e.data = data;
108
+ }
109
+ if (metricValue !== null && metricValue !== undefined) {
110
+ e.metricValue = metricValue;
111
+ }
112
+ return e;
113
+ };
114
+
115
+ return ef;
116
+ }
117
+
118
+ module.exports = {
119
+ EventFactory,
120
+ isExperiment,
121
+ };
@@ -0,0 +1,320 @@
1
+ const LRUCache = require('lru-cache');
2
+ const { v4: uuidv4 } = require('uuid');
3
+
4
+ const EventSummarizer = require('./event_summarizer');
5
+ const ContextFilter = require('./context_filter');
6
+ const errors = require('./errors');
7
+ const httpUtils = require('./utils/httpUtils');
8
+ const messages = require('./messages');
9
+ const wrapPromiseCallback = require('./utils/wrapPromiseCallback');
10
+ const { getCanonicalKey } = require('./context');
11
+
12
+ function EventProcessor(sdkKey, config, errorReporter, diagnosticsManager) {
13
+ const ep = {};
14
+
15
+ const contextFilter = ContextFilter(config),
16
+ summarizer = EventSummarizer(),
17
+ contextKeysCache = new LRUCache({ max: config.contextKeysCapacity }),
18
+ mainEventsUri = config.eventsUri + '/bulk',
19
+ diagnosticEventsUri = config.eventsUri + '/diagnostic';
20
+
21
+ let queue = [],
22
+ lastKnownPastTime = 0,
23
+ droppedEvents = 0,
24
+ deduplicatedUsers = 0,
25
+ exceededCapacity = false,
26
+ eventsInLastBatch = 0,
27
+ shutdown = false,
28
+ diagnosticsTimer;
29
+
30
+ function enqueue(event) {
31
+ if (queue.length < config.capacity) {
32
+ queue.push(event);
33
+ exceededCapacity = false;
34
+ } else {
35
+ if (!exceededCapacity) {
36
+ exceededCapacity = true;
37
+ config.logger.warn('Exceeded event queue capacity. Increase capacity to avoid dropping events.');
38
+ }
39
+ droppedEvents++;
40
+ }
41
+ }
42
+
43
+ function shouldDebugEvent(event) {
44
+ if (event.debugEventsUntilDate) {
45
+ if (event.debugEventsUntilDate > lastKnownPastTime && event.debugEventsUntilDate > new Date().getTime()) {
46
+ return true;
47
+ }
48
+ }
49
+ return false;
50
+ }
51
+
52
+ function makeOutputEvent(event) {
53
+ switch (event.kind) {
54
+ case 'feature': {
55
+ const debug = !!event.debug;
56
+ const out = {
57
+ kind: debug ? 'debug' : 'feature',
58
+ creationDate: event.creationDate,
59
+ key: event.key,
60
+ value: event.value,
61
+ default: event.default,
62
+ prereqOf: event.prereqOf,
63
+ };
64
+ if (event.variation !== undefined && event.variation !== null) {
65
+ out.variation = event.variation;
66
+ }
67
+ if (event.version !== undefined && event.version !== null) {
68
+ out.version = event.version;
69
+ }
70
+ if (event.reason) {
71
+ out.reason = event.reason;
72
+ }
73
+ if (debug) {
74
+ out.context = processContext(event);
75
+ } else {
76
+ out.contextKeys = getContextKeys(event);
77
+ }
78
+ return out;
79
+ }
80
+ case 'identify':
81
+ return {
82
+ kind: 'identify',
83
+ creationDate: event.creationDate,
84
+ context: processContext(event),
85
+ };
86
+ case 'custom': {
87
+ const out = {
88
+ kind: 'custom',
89
+ creationDate: event.creationDate,
90
+ key: event.key,
91
+ };
92
+
93
+ out.contextKeys = getContextKeys(event);
94
+
95
+ if (event.data !== null && event.data !== undefined) {
96
+ out.data = event.data;
97
+ }
98
+ if (event.metricValue !== null && event.metricValue !== undefined) {
99
+ out.metricValue = event.metricValue;
100
+ }
101
+
102
+ return out;
103
+ }
104
+ default:
105
+ return event;
106
+ }
107
+ }
108
+
109
+ function processContext(event) {
110
+ return contextFilter.filter(event.context);
111
+ }
112
+
113
+ function getCacheKey(event) {
114
+ const context = event.context;
115
+ return getCanonicalKey(context);
116
+ }
117
+
118
+ function getContextKeys(event) {
119
+ const keys = {};
120
+ const context = event.context;
121
+ if (context !== undefined) {
122
+ if (context.kind === undefined && context.key) {
123
+ keys.user = String(context.key);
124
+ } else if (context.kind !== 'multi') {
125
+ keys[context.kind] = String(context.key);
126
+ } else if (context.kind === 'multi') {
127
+ Object.keys(context)
128
+ .filter(key => key !== 'kind')
129
+ .forEach(key => {
130
+ if (context[key] !== undefined && context[key].key !== undefined) {
131
+ keys[key] = context[key].key;
132
+ }
133
+ });
134
+ }
135
+ return keys;
136
+ }
137
+ return undefined;
138
+ }
139
+
140
+ ep.sendEvent = event => {
141
+ let addIndexEvent = false,
142
+ addFullEvent = false,
143
+ addDebugEvent = false;
144
+
145
+ if (shutdown) {
146
+ return;
147
+ }
148
+
149
+ // Always record the event in the summarizer.
150
+ summarizer.summarizeEvent(event);
151
+
152
+ // Decide whether to add the event to the payload. Feature events may be added twice, once for
153
+ // the event (if tracked) and once for debugging.
154
+ if (event.kind === 'feature') {
155
+ addFullEvent = event.trackEvents;
156
+ addDebugEvent = shouldDebugEvent(event);
157
+ } else {
158
+ addFullEvent = true;
159
+ }
160
+
161
+ // For each user we haven't seen before, we add an index event - unless this is already
162
+ // an identify event for that user.
163
+ if (event.context) {
164
+ const isIdentify = event.kind === 'identify';
165
+ if (contextKeysCache.get(getCacheKey(event))) {
166
+ if (!isIdentify) {
167
+ deduplicatedUsers++;
168
+ }
169
+ } else {
170
+ contextKeysCache.set(getCacheKey(event), true);
171
+ if (!isIdentify) {
172
+ addIndexEvent = true;
173
+ }
174
+ }
175
+ }
176
+
177
+ if (addIndexEvent) {
178
+ enqueue({
179
+ kind: 'index',
180
+ creationDate: event.creationDate,
181
+ context: processContext(event),
182
+ });
183
+ }
184
+ if (addFullEvent) {
185
+ enqueue(makeOutputEvent(event));
186
+ }
187
+ if (addDebugEvent) {
188
+ const debugEvent = Object.assign({}, event, { debug: true });
189
+ enqueue(makeOutputEvent(debugEvent));
190
+ }
191
+ };
192
+
193
+ ep.flush = function (callback) {
194
+ return wrapPromiseCallback(
195
+ new Promise((resolve, reject) => {
196
+ if (shutdown) {
197
+ const err = new errors.LDInvalidSDKKeyError('Events cannot be posted because SDK key is invalid');
198
+ reject(err);
199
+ return;
200
+ }
201
+
202
+ const worklist = queue;
203
+ queue = [];
204
+ const summary = summarizer.getSummary();
205
+ summarizer.clearSummary();
206
+ if (Object.keys(summary.features).length) {
207
+ summary.kind = 'summary';
208
+ worklist.push(summary);
209
+ }
210
+
211
+ if (!worklist.length) {
212
+ resolve();
213
+ return;
214
+ }
215
+
216
+ eventsInLastBatch = worklist.length;
217
+ config.logger.debug('Flushing %d events', worklist.length);
218
+
219
+ tryPostingEvents(worklist, mainEventsUri, uuidv4(), resolve, reject, true);
220
+ }),
221
+ callback
222
+ );
223
+ };
224
+
225
+ function tryPostingEvents(events, uri, payloadId, resolve, reject, canRetry) {
226
+ const retryOrReject = err => {
227
+ if (canRetry) {
228
+ config.logger && config.logger.warn('Will retry posting events after 1 second');
229
+ setTimeout(() => {
230
+ tryPostingEvents(events, uri, payloadId, resolve, reject, false);
231
+ }, 1000);
232
+ } else {
233
+ reject(err);
234
+ }
235
+ };
236
+
237
+ const headers = Object.assign({ 'Content-Type': 'application/json' }, httpUtils.getDefaultHeaders(sdkKey, config));
238
+ if (payloadId) {
239
+ headers['X-LaunchDarkly-Payload-ID'] = payloadId;
240
+ headers['X-LaunchDarkly-Event-Schema'] = '4';
241
+ }
242
+
243
+ const options = { method: 'POST', headers };
244
+ const body = JSON.stringify(events);
245
+ httpUtils.httpRequest(uri, options, body, config, (err, resp) => {
246
+ if (err) {
247
+ retryOrReject(err);
248
+ return;
249
+ }
250
+ if (resp.headers['date']) {
251
+ const date = Date.parse(resp.headers['date']);
252
+ if (date) {
253
+ lastKnownPastTime = date;
254
+ }
255
+ }
256
+ if (resp.statusCode > 204) {
257
+ const err = new errors.LDUnexpectedResponseError(
258
+ messages.httpErrorMessage({ status: resp.statusCode }, 'event posting', 'some events were dropped')
259
+ );
260
+ errorReporter && errorReporter(err);
261
+ if (!errors.isHttpErrorRecoverable(resp.statusCode)) {
262
+ reject(err);
263
+ shutdown = true;
264
+ } else {
265
+ retryOrReject(err);
266
+ }
267
+ } else {
268
+ resolve();
269
+ }
270
+ });
271
+ }
272
+
273
+ function postDiagnosticEvent(event) {
274
+ tryPostingEvents(
275
+ event,
276
+ diagnosticEventsUri,
277
+ null,
278
+ () => {},
279
+ () => {},
280
+ true
281
+ );
282
+ }
283
+
284
+ const flushTimer = setInterval(() => {
285
+ ep.flush().then(
286
+ () => {},
287
+ () => {}
288
+ );
289
+ }, config.flushInterval * 1000);
290
+
291
+ const flushUsersTimer = setInterval(() => {
292
+ contextKeysCache.reset();
293
+ }, config.contextKeysFlushInterval * 1000);
294
+
295
+ ep.close = () => {
296
+ clearInterval(flushTimer);
297
+ clearInterval(flushUsersTimer);
298
+ diagnosticsTimer && clearInterval(diagnosticsTimer);
299
+ };
300
+
301
+ if (!config.diagnosticOptOut && diagnosticsManager) {
302
+ const initEvent = diagnosticsManager.createInitEvent();
303
+ postDiagnosticEvent(initEvent);
304
+
305
+ diagnosticsTimer = setInterval(() => {
306
+ const statsEvent = diagnosticsManager.createStatsEventAndReset(
307
+ droppedEvents,
308
+ deduplicatedUsers,
309
+ eventsInLastBatch
310
+ );
311
+ droppedEvents = 0;
312
+ deduplicatedUsers = 0;
313
+ postDiagnosticEvent(statsEvent);
314
+ }, config.diagnosticRecordingInterval * 1000);
315
+ }
316
+
317
+ return ep;
318
+ }
319
+
320
+ module.exports = EventProcessor;
@@ -0,0 +1,101 @@
1
+ const { getContextKinds } = require('./context');
2
+
3
+ function getKinds(event) {
4
+ if (event.context) {
5
+ return getContextKinds(event.context);
6
+ }
7
+ if (event.contextKeys) {
8
+ return Object.keys(event.contextKeys);
9
+ }
10
+ return [];
11
+ }
12
+
13
+ function EventSummarizer() {
14
+ const es = {};
15
+
16
+ let startDate = 0,
17
+ endDate = 0,
18
+ counters = {},
19
+ contextKinds = {};
20
+
21
+ es.summarizeEvent = event => {
22
+ if (event.kind === 'feature') {
23
+ const counterKey =
24
+ event.key +
25
+ ':' +
26
+ (event.variation !== null && event.variation !== undefined ? event.variation : '') +
27
+ ':' +
28
+ (event.version !== null && event.version !== undefined ? event.version : '');
29
+ const counterVal = counters[counterKey];
30
+ let kinds = contextKinds[event.key];
31
+ if (!kinds) {
32
+ kinds = new Set();
33
+ contextKinds[event.key] = kinds;
34
+ }
35
+ getKinds(event).forEach(kind => kinds.add(kind));
36
+
37
+ if (counterVal) {
38
+ counterVal.count = counterVal.count + 1;
39
+ } else {
40
+ counters[counterKey] = {
41
+ count: 1,
42
+ key: event.key,
43
+ version: event.version,
44
+ variation: event.variation,
45
+ value: event.value,
46
+ default: event.default,
47
+ };
48
+ }
49
+ if (startDate === 0 || event.creationDate < startDate) {
50
+ startDate = event.creationDate;
51
+ }
52
+ if (event.creationDate > endDate) {
53
+ endDate = event.creationDate;
54
+ }
55
+ }
56
+ };
57
+
58
+ es.getSummary = () => {
59
+ const flagsOut = {};
60
+ for (const c of Object.values(counters)) {
61
+ let flag = flagsOut[c.key];
62
+ if (!flag) {
63
+ flag = {
64
+ default: c.default,
65
+ counters: [],
66
+ contextKinds: [...contextKinds[c.key]],
67
+ };
68
+ flagsOut[c.key] = flag;
69
+ }
70
+ const counterOut = {
71
+ value: c.value,
72
+ count: c.count,
73
+ };
74
+ if (c.variation !== undefined && c.variation !== null) {
75
+ counterOut.variation = c.variation;
76
+ }
77
+ if (c.version !== undefined && c.version !== null) {
78
+ counterOut.version = c.version;
79
+ } else {
80
+ counterOut.unknown = true;
81
+ }
82
+ flag.counters.push(counterOut);
83
+ }
84
+ return {
85
+ startDate: startDate,
86
+ endDate: endDate,
87
+ features: flagsOut,
88
+ };
89
+ };
90
+
91
+ es.clearSummary = () => {
92
+ startDate = 0;
93
+ endDate = 0;
94
+ counters = {};
95
+ contextKinds = {};
96
+ };
97
+
98
+ return es;
99
+ }
100
+
101
+ module.exports = EventSummarizer;
@@ -0,0 +1,120 @@
1
+ // The default in-memory implementation of a feature store, which holds feature flags and
2
+ // other related data received from LaunchDarkly.
3
+ //
4
+ // Other implementations of the same interface can be used by passing them in the featureStore
5
+ // property of the client configuration (that's why the interface here is async, even though
6
+ // the in-memory store doesn't do anything asynchronous - because other implementations may
7
+ // need to be async). The interface is defined by LDFeatureStore in index.d.ts.
8
+ //
9
+ // Additional implementations should use CachingStoreWrapper if possible.
10
+
11
+ // Note that the contract for feature store methods does *not* require callbacks to be deferred
12
+ // with setImmediate, process.nextTick, etc. It is both allowed and desirable to call them
13
+ // directly whenever possible (i.e. if we don't actually have to do any I/O), since otherwise
14
+ // feature flag retrieval is a major performance bottleneck. These methods are for internal use
15
+ // by the SDK, and the SDK does not make any assumptions about whether a callback executes
16
+ // before or after the next statement.
17
+
18
+ function InMemoryFeatureStore() {
19
+ let allData = {};
20
+ let initCalled = false;
21
+
22
+ const store = {};
23
+
24
+ function callbackResult(cb, result) {
25
+ cb && cb(result);
26
+ }
27
+
28
+ store.get = (kind, key, cb) => {
29
+ const items = allData[kind.namespace] || {};
30
+ if (Object.hasOwnProperty.call(items, key)) {
31
+ const item = items[key];
32
+
33
+ if (!item || item.deleted) {
34
+ callbackResult(cb, null);
35
+ } else {
36
+ callbackResult(cb, item);
37
+ }
38
+ } else {
39
+ callbackResult(cb, null);
40
+ }
41
+ };
42
+
43
+ store.all = (kind, cb) => {
44
+ const results = {};
45
+ const items = allData[kind.namespace] || {};
46
+
47
+ for (const [key, item] of Object.entries(items)) {
48
+ if (item && !item.deleted) {
49
+ results[key] = item;
50
+ }
51
+ }
52
+
53
+ callbackResult(cb, results);
54
+ };
55
+
56
+ store.init = (newData, cb) => {
57
+ allData = newData;
58
+ initCalled = true;
59
+ callbackResult(cb);
60
+ };
61
+
62
+ store.delete = (kind, key, version, cb) => {
63
+ let items = allData[kind.namespace];
64
+ if (!items) {
65
+ items = {};
66
+ allData[kind] = items;
67
+ }
68
+ const deletedItem = { version: version, deleted: true };
69
+ if (Object.hasOwnProperty.call(items, key)) {
70
+ const old = items[key];
71
+ if (!old || old.version < version) {
72
+ items[key] = deletedItem;
73
+ }
74
+ } else {
75
+ items[key] = deletedItem;
76
+ }
77
+
78
+ callbackResult(cb);
79
+ };
80
+
81
+ store.upsert = (kind, item, cb) => {
82
+ const key = item.key;
83
+ let items = allData[kind.namespace];
84
+ if (!items) {
85
+ items = {};
86
+ allData[kind.namespace] = items;
87
+ }
88
+
89
+ if (Object.hasOwnProperty.call(items, key)) {
90
+ const old = items[key];
91
+ if (old && old.version < item.version) {
92
+ items[key] = clone(item);
93
+ }
94
+ } else {
95
+ items[key] = clone(item);
96
+ }
97
+
98
+ callbackResult(cb);
99
+ };
100
+
101
+ store.initialized = cb => {
102
+ callbackResult(cb, initCalled === true);
103
+ };
104
+
105
+ store.close = () => {
106
+ // Close on the in-memory store is a no-op
107
+ };
108
+
109
+ store.description = 'memory';
110
+
111
+ return store;
112
+ }
113
+
114
+ // Deep clone an object. Does not preserve any
115
+ // functions on the object
116
+ function clone(obj) {
117
+ return JSON.parse(JSON.stringify(obj));
118
+ }
119
+
120
+ module.exports = InMemoryFeatureStore;