@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,55 @@
1
+ # Contributing to the LaunchDarkly Server-Side SDK for Node.js
2
+
3
+ LaunchDarkly has published an [SDK contributor's guide](https://docs.launchdarkly.com/sdk/concepts/contributors-guide) that provides a detailed explanation of how our SDKs work. See below for additional information on how to contribute to this SDK.
4
+
5
+ ## Submitting bug reports and feature requests
6
+
7
+ The LaunchDarkly SDK team monitors the [issue tracker](https://github.com/launchdarkly/node-server-sdk/issues) in the SDK repository. Bug reports and feature requests specific to this SDK should be filed in this issue tracker. The SDK team will respond to all newly filed issues within two business days.
8
+
9
+ ## Submitting pull requests
10
+
11
+ We encourage pull requests and other contributions from the community. Before submitting pull requests, ensure that all temporary or unintended code is removed. Don't worry about adding reviewers to the pull request; the LaunchDarkly SDK team will add themselves. The SDK team will acknowledge all pull requests within two business days.
12
+
13
+ ## Build instructions
14
+
15
+ ### Prerequisites
16
+
17
+ The project should be built and tested against the lowest compatible version, Node 12. It uses `npm`, which is bundled in all supported versions of Node.
18
+
19
+ ### Setup
20
+
21
+ To install project dependencies, from the project root directory:
22
+
23
+ ```
24
+ npm install
25
+ ```
26
+
27
+ ### Testing
28
+
29
+ To run all unit tests:
30
+
31
+ ```
32
+ npm test
33
+ ```
34
+
35
+ To verify that the TypeScript declarations compile correctly (this involves compiling the file `test-types.ts`, so if you have changed any types or interfaces, you will want to update that code):
36
+
37
+ ```
38
+ npm run check-typescript
39
+ ```
40
+
41
+ To run the SDK contract test suite (see [`contract-tests/README.md`](./contract-tests/README.md)):
42
+
43
+ ```bash
44
+ npm run contract-tests
45
+ ```
46
+
47
+ ### Auditing package dependencies
48
+
49
+ The `npm audit` tool compares all dependencies and transitive dependencies to a database of package versions with known vulnerabilities. However, the output of this tool includes both runtime and development dependencies.
50
+
51
+ Runtime dependencies can affect applications using the SDK; they can only be fixed by updating one of the explicit dependencies in `package.json`. Development dependencies cannot affect applications, but will still cause `npm audit` to flag the project; they can be fixed by running `npm audit fix` to add overrides for transitive dependencies in `package-lock.json`.
52
+
53
+ It is important _not_ to run `npm audit fix` if there are any bad _runtime_ dependencies, because it will hide the problem in our own build, without actually fixing the vulnerability when an application uses the SDK.
54
+
55
+ The script `scripts/better-audit.sh`, which is run in the CI build and can also be run manually, processes the output of `npm audit` to eliminate all duplicate entries and then determines whether each entry is coming from a runtime dependency or a development dependency. If there are any runtime ones, it terminates with an error code so the build will fail.
package/LICENSE.txt ADDED
@@ -0,0 +1,13 @@
1
+ Copyright 2016 Catamorphic, Co.
2
+
3
+ Licensed under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License.
5
+ You may obtain a copy of the License at
6
+
7
+ http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ Unless required by applicable law or agreed to in writing, software
10
+ distributed under the License is distributed on an "AS IS" BASIS,
11
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ See the License for the specific language governing permissions and
13
+ limitations under the License.
package/README.md ADDED
@@ -0,0 +1,36 @@
1
+ # @depup/launchdarkly-node-server-sdk
2
+
3
+ > Dependency-bumped version of [launchdarkly-node-server-sdk](https://www.npmjs.com/package/launchdarkly-node-server-sdk)
4
+
5
+ Generated by [DepUp](https://github.com/depup/npm) -- all production
6
+ dependencies bumped to latest versions.
7
+
8
+ ## Installation
9
+
10
+ ```bash
11
+ npm install @depup/launchdarkly-node-server-sdk
12
+ ```
13
+
14
+ | Field | Value |
15
+ |-------|-------|
16
+ | Original | [launchdarkly-node-server-sdk](https://www.npmjs.com/package/launchdarkly-node-server-sdk) @ 7.0.4 |
17
+ | Processed | 2026-03-17 |
18
+ | Smoke test | passed |
19
+ | Deps updated | 6 |
20
+
21
+ ## Dependency Changes
22
+
23
+ | Dependency | From | To |
24
+ |------------|------|-----|
25
+ | async | ^3.2.4 | ^3.2.6 |
26
+ | launchdarkly-eventsource | 1.4.4 | ^2.2.0 |
27
+ | lru-cache | ^6.0.0 | ^11.2.7 |
28
+ | node-cache | ^5.1.0 | ^5.1.2 |
29
+ | semver | ^7.5.4 | ^7.7.4 |
30
+ | uuid | ^8.3.2 | ^13.0.0 |
31
+
32
+ ---
33
+
34
+ Source: https://github.com/depup/npm | Original: https://www.npmjs.com/package/launchdarkly-node-server-sdk
35
+
36
+ License inherited from the original package.
package/SECURITY.md ADDED
@@ -0,0 +1,5 @@
1
+ # Reporting and Fixing Security Issues
2
+
3
+ Please report all security issues to the LaunchDarkly security team by submitting a bug bounty report to our [HackerOne program](https://hackerone.com/launchdarkly?type=team). LaunchDarkly will triage and address all valid security issues following the response targets defined in our program policy. Valid security issues may be eligible for a bounty.
4
+
5
+ Please do not open issues or pull requests for security issues. This makes the problem immediately visible to everyone, including potentially malicious actors.
@@ -0,0 +1,217 @@
1
+ /**
2
+ * Take a key string and escape the characters to allow it to be used as a reference.
3
+ * @param {string} key
4
+ * @returns {string} The processed key.
5
+ */
6
+ function processEscapeCharacters(key) {
7
+ return key.replace(/~/g, '~0').replace(/\//g, '~1');
8
+ }
9
+
10
+ /**
11
+ * @param {string} reference The reference to get the components of.
12
+ * @returns {string[]} The components of the reference. Escape characters will be converted to their representative values.
13
+ */
14
+ function getComponents(reference) {
15
+ const referenceWithoutPrefix = reference.startsWith('/') ? reference.substring(1) : reference;
16
+ return referenceWithoutPrefix
17
+ .split('/')
18
+ .map(component => (component.indexOf('~') >= 0 ? component.replace(/~1/g, '/').replace(/~0/g, '~') : component));
19
+ }
20
+
21
+ /**
22
+ * @param {string} reference The reference to check if it is a literal.
23
+ * @returns true if the reference is a literal.
24
+ */
25
+ function isLiteral(reference) {
26
+ return !reference.startsWith('/');
27
+ }
28
+
29
+ /**
30
+ * Get an attribute value from a literal.
31
+ * @param {Object} target
32
+ * @param {string} literal
33
+ */
34
+ function getFromLiteral(target, literal) {
35
+ if (target !== null && target !== undefined && Object.prototype.hasOwnProperty.call(target, literal)) {
36
+ return target[literal];
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Gets the `target` object's value at the `reference`'s location.
42
+ *
43
+ * This method method follows the rules for accessing attributes for use
44
+ * in evaluating clauses.
45
+ *
46
+ * Accessing the root of the target will always result in undefined.
47
+ *
48
+ * @param {Object} target
49
+ * @param {string} reference
50
+ * @returns The `target` object's value at the `reference`'s location.
51
+ * Undefined if the field does not exist or if the reference is not valid.
52
+ */
53
+ function get(target, reference) {
54
+ if (reference === '' || reference === '/') {
55
+ return undefined;
56
+ }
57
+
58
+ if (isLiteral(reference)) {
59
+ return getFromLiteral(target, reference);
60
+ }
61
+
62
+ const components = getComponents(reference);
63
+ let current = target;
64
+ for (const component of components) {
65
+ if (
66
+ current !== null &&
67
+ current !== undefined &&
68
+ typeof current === 'object' &&
69
+ // We do not want to allow indexing into an array.
70
+ !Array.isArray(current) &&
71
+ // For arrays and strings, in addition to objects, a hasOwnProperty check
72
+ // will be true for indexes (as strings or numbers), which are present
73
+ // in the object/string/array.
74
+ Object.prototype.hasOwnProperty.call(current, component)
75
+ ) {
76
+ current = current[component];
77
+ } else {
78
+ return undefined;
79
+ }
80
+ }
81
+
82
+ return current;
83
+ }
84
+
85
+ /**
86
+ * Compare two references and determine if they are equivalent.
87
+ * @param {string} a
88
+ * @param {string} b
89
+ */
90
+ function compare(a, b) {
91
+ const aIsLiteral = isLiteral(a);
92
+ const bIsLiteral = isLiteral(b);
93
+ if (aIsLiteral && bIsLiteral) {
94
+ return a === b;
95
+ }
96
+ if (aIsLiteral) {
97
+ const bComponents = getComponents(b);
98
+ if (bComponents.length !== 1) {
99
+ return false;
100
+ }
101
+ return a === bComponents[0];
102
+ }
103
+ if (bIsLiteral) {
104
+ const aComponents = getComponents(a);
105
+ if (aComponents.length !== 1) {
106
+ return false;
107
+ }
108
+ return b === aComponents[0];
109
+ }
110
+ return a === b;
111
+ }
112
+
113
+ /**
114
+ * @param {string} a
115
+ * @param {string} b
116
+ * @returns The two strings joined by '/'.
117
+ */
118
+ function join(a, b) {
119
+ return `${a}/${b}`;
120
+ }
121
+
122
+ /**
123
+ * There are cases where a field could have been named with a preceeding '/'.
124
+ * If that attribute was private, then the literal would appear to be a reference.
125
+ * This method can be used to convert a literal to a reference in such situations.
126
+ * @param {string} literal The literal to convert to a reference.
127
+ * @returns A literal which has been converted to a reference.
128
+ */
129
+ function literalToReference(literal) {
130
+ return `/${processEscapeCharacters(literal)}`;
131
+ }
132
+
133
+ /**
134
+ * Clone an object excluding the values referenced by a list of references.
135
+ * @param {Object} target The object to clone.
136
+ * @param {string[]} references A list of references from the cloned object.
137
+ * @returns {{cloned: Object, excluded: string[]}} The cloned object and a list of excluded values.
138
+ */
139
+ function cloneExcluding(target, references) {
140
+ const stack = [];
141
+ const cloned = {};
142
+ const excluded = [];
143
+
144
+ stack.push(
145
+ ...Object.keys(target).map(key => ({
146
+ key,
147
+ ptr: literalToReference(key),
148
+ source: target,
149
+ parent: cloned,
150
+ visited: [target],
151
+ }))
152
+ );
153
+
154
+ while (stack.length) {
155
+ const item = stack.pop();
156
+ if (!references.some(ptr => compare(ptr, item.ptr))) {
157
+ const value = item.source[item.key];
158
+
159
+ // Handle null because it overlaps with object, which we will want to handle later.
160
+ if (value === null) {
161
+ item.parent[item.key] = value;
162
+ } else if (Array.isArray(value)) {
163
+ item.parent[item.key] = [...value];
164
+ } else if (typeof value === 'object') {
165
+ //Arrays and null must already be handled.
166
+
167
+ //Prevent cycles by not visiting the same object
168
+ //with in the same branch. Parallel branches
169
+ //may contain the same object.
170
+ if (item.visited.includes(value)) {
171
+ continue;
172
+ }
173
+
174
+ item.parent[item.key] = {};
175
+
176
+ stack.push(
177
+ ...Object.keys(value).map(key => ({
178
+ key,
179
+ ptr: join(item.ptr, processEscapeCharacters(key)),
180
+ source: value,
181
+ parent: item.parent[item.key],
182
+ visited: [...item.visited, value],
183
+ }))
184
+ );
185
+ } else {
186
+ item.parent[item.key] = value;
187
+ }
188
+ } else {
189
+ excluded.push(item.ptr);
190
+ }
191
+ }
192
+ return { cloned, excluded: excluded.sort() };
193
+ }
194
+
195
+ function isValidReference(reference) {
196
+ return !reference.match(/\/\/|(^\/.*~[^0|^1])|~$/);
197
+ }
198
+
199
+ /**
200
+ * Check if the given attribute reference is for the "kind" attribute.
201
+ * @param {string} reference String containing an attribute reference.
202
+ */
203
+ function isKind(reference) {
204
+ // There are only 2 valid ways to specify the kind attribute,
205
+ // so this just checks them. Given the current flow of evaluation
206
+ // this is much less intense a process than doing full validation and parsing.
207
+ return reference === 'kind' || reference === '/kind';
208
+ }
209
+
210
+ module.exports = {
211
+ cloneExcluding,
212
+ compare,
213
+ get,
214
+ isValidReference,
215
+ literalToReference,
216
+ isKind,
217
+ };
@@ -0,0 +1,117 @@
1
+ const { createHash } = require('crypto');
2
+ const { EventEmitter } = require('events');
3
+ const LRUCache = require('lru-cache');
4
+
5
+ const defaultStaleAfter = 120;
6
+ const defaultStatusPollInterval = 5;
7
+ const defaultUserCacheSize = 1000;
8
+ const defaultUserCacheTime = 5;
9
+ const emptyMembership = {};
10
+
11
+ function BigSegmentStoreManager(store, config, logger) {
12
+ const staleTimeMs = (config.staleAfter > 0 ? config.staleAfter : defaultStaleAfter) * 1000;
13
+ const pollIntervalMs = (config.statusPollInterval > 0 ? config.statusPollInterval : defaultStatusPollInterval) * 1000;
14
+ const pollTask = store ? setInterval(() => pollStoreAndUpdateStatus(), pollIntervalMs) : null;
15
+ const cache = store
16
+ ? new LRUCache({
17
+ max: config.userCacheSize || defaultUserCacheSize,
18
+ maxAge: (config.userCacheTime || defaultUserCacheTime) * 1000,
19
+ })
20
+ : null;
21
+ let lastStatus;
22
+
23
+ const ret = {};
24
+
25
+ ret.close = () => {
26
+ clearInterval(pollTask);
27
+ store && store.close && store.close();
28
+ };
29
+
30
+ const statusProvider = new EventEmitter();
31
+ ret.statusProvider = statusProvider;
32
+ statusProvider.getStatus = () => lastStatus;
33
+ statusProvider.requireStatus = async () => {
34
+ if (!lastStatus) {
35
+ await pollStoreAndUpdateStatus();
36
+ }
37
+ return lastStatus;
38
+ };
39
+
40
+ // Called by the evaluator when it needs to get the Big Segment membership state for a user.
41
+ //
42
+ // If there is a cached membership state for the user, it returns the cached state. Otherwise,
43
+ // it converts the user key into the hash string used by the BigSegmentStore, queries the store,
44
+ // and caches the result.
45
+ //
46
+ // The return value is a two-element array where the first element is the membership object,
47
+ // and the second element is a status value ("HEALTHY", "STALE", or "STORE_ERROR"). An undefined
48
+ // return value is equivalent to [ null, "NOT_CONFIGURED" ];
49
+ ret.getUserMembership = async userKey => {
50
+ if (!store) {
51
+ return undefined;
52
+ }
53
+ let membership = cache.get(userKey);
54
+ if (!membership) {
55
+ try {
56
+ membership = await store.getUserMembership(hashForUserKey(userKey));
57
+ if (membership === null || membership === undefined) {
58
+ membership = emptyMembership;
59
+ }
60
+ cache.set(userKey, membership);
61
+ } catch (e) {
62
+ logger.error('Big Segment store membership query returned error: ' + e);
63
+ return [null, 'STORE_ERROR'];
64
+ }
65
+ cache.set(userKey, membership);
66
+ }
67
+ if (!lastStatus) {
68
+ await pollStoreAndUpdateStatus();
69
+ }
70
+ if (!lastStatus.available) {
71
+ return [membership, 'STORE_ERROR'];
72
+ }
73
+ return [membership, lastStatus.stale ? 'STALE' : 'HEALTHY'];
74
+ };
75
+
76
+ async function pollStoreAndUpdateStatus() {
77
+ if (!store) {
78
+ lastStatus = { available: false, stale: false };
79
+ return;
80
+ }
81
+ logger.debug('Querying Big Segment store status');
82
+ let newStatus;
83
+ try {
84
+ const metadata = await store.getMetadata();
85
+ newStatus = { available: true, stale: !metadata || !metadata.lastUpToDate || isStale(metadata.lastUpToDate) };
86
+ } catch (e) {
87
+ logger.error('Big Segment store status query returned error: ' + e);
88
+ newStatus = { available: false, stale: false };
89
+ }
90
+ if (!lastStatus || lastStatus.available !== newStatus.available || lastStatus.stale !== newStatus.stale) {
91
+ logger.debug(
92
+ 'Big Segment store status changed from %s to %s',
93
+ JSON.stringify(lastStatus),
94
+ JSON.stringify(newStatus)
95
+ );
96
+ lastStatus = newStatus;
97
+ statusProvider.emit('change', newStatus);
98
+ }
99
+ }
100
+
101
+ function isStale(timestamp) {
102
+ return new Date().getTime() - timestamp >= staleTimeMs;
103
+ }
104
+
105
+ return ret;
106
+ }
107
+
108
+ function hashForUserKey(userKey) {
109
+ const hasher = createHash('sha256');
110
+ hasher.update(userKey);
111
+ return hasher.digest('base64');
112
+ }
113
+
114
+ module.exports = {
115
+ BigSegmentStoreManager,
116
+ hashForUserKey,
117
+ };
@@ -0,0 +1,240 @@
1
+ const NodeCache = require('node-cache'),
2
+ dataKind = require('./versioned_data_kind'),
3
+ UpdateQueue = require('./update_queue');
4
+
5
+ function cacheKey(kind, key) {
6
+ return kind.namespace + ':' + key;
7
+ }
8
+
9
+ function allCacheKey(kind) {
10
+ return '$all:' + kind.namespace;
11
+ }
12
+
13
+ const initializedKey = '$checkedInit';
14
+
15
+ /*
16
+ CachingStoreWrapper provides commonly needed functionality for implementations of an
17
+ SDK feature store. The underlyingStore must implement a simplified interface for
18
+ querying and updating the data store, while CachingStoreWrapper adds optional caching of
19
+ stored items and of the initialized state, and ensures that asynchronous operations are
20
+ serialized correctly.
21
+
22
+ The underlyingStore object must have the following methods:
23
+
24
+ - getInternal(kind, key, callback): Queries a single item from the data store. The kind
25
+ parameter is an object with a "namespace" property that uniquely identifies the
26
+ category of data (features, segments), and the key is the unique key within that
27
+ category. It calls the callback with the resulting item as a parameter, or, if no such
28
+ item exists, null/undefined. It should not attempt to filter out any items, nor to
29
+ cache any items.
30
+
31
+ - getAllInternal(kind, callback): Queries all items in a given category from the data
32
+ store, calling the callback with an object where each key is the item's key and each
33
+ value is the item. It should not attempt to filter out any items, nor to cache any items.
34
+
35
+ - upsertInternal(kind, newItem, callback): Adds or updates a single item. If an item with
36
+ the same key already exists (in the category specified by "kind"), it should update it
37
+ only if the new item's "version" property is greater than the old one. On completion, it
38
+ should call the callback with the final state of the item, i.e. if the update succeeded
39
+ then it passes the item that was passed in, and if the update failed due to the version
40
+ check then it passes the item that is currently in the data store (this ensures that
41
+ caching works correctly). Note that deletions are implemented by upserting a placeholder
42
+ item with the property "deleted: true".
43
+
44
+ - initializedInternal(callback): Tests whether the data store contains a complete data
45
+ set, meaning that initInternal() or initOrdereInternal() has been called at least once.
46
+ In a shared data store, it should be able to detect this even if the store was
47
+ initialized by a different process, i.e. the test should be based on looking at what is
48
+ in the data store. The method does not need to worry about caching this value;
49
+ CachingStoreWrapper will only call it when necessary. Call callback with true or false.
50
+
51
+ - initInternal(allData, callback): Replaces the entire contents of the data store. This
52
+ should be done atomically (i.e. within a transaction); if that isn't possible, use
53
+ initOrderedInternal() instead. The allData parameter is an object where each key is one
54
+ of the "kind" objects, and each value is an object with the keys and values of all
55
+ items of that kind. Call callback with no parameters when done.
56
+ OR:
57
+ - initOrderedInternal(collections, callback): Replaces the entire contents of the data
58
+ store. The collections parameter is an array of objects, each of which has "kind" and
59
+ "items" properties; "items" is an array of data items. Each array should be processed
60
+ in the specified order. The store should delete any obsolete items only after writing
61
+ all of the items provided.
62
+ */
63
+ function CachingStoreWrapper(underlyingStore, ttl, description) {
64
+ const cache = ttl ? new NodeCache({ stdTTL: ttl }) : null;
65
+ const queue = new UpdateQueue();
66
+ let initialized = false;
67
+
68
+ this.underlyingStore = underlyingStore;
69
+ this.description = description;
70
+
71
+ this.init = (allData, cb) => {
72
+ queue.enqueue(
73
+ cb => {
74
+ // The underlying store can either implement initInternal, which receives unordered data,
75
+ // or initOrderedInternal, which receives ordered data (for implementations that cannot do
76
+ // an atomic update and therefore need to be told what order to do the operations in).
77
+ const afterInit = () => {
78
+ initialized = true;
79
+
80
+ if (cache) {
81
+ cache.del(initializedKey);
82
+ cache.flushAll();
83
+
84
+ // populate cache with initial data
85
+ Object.keys(allData).forEach(kindNamespace => {
86
+ const kind = dataKind[kindNamespace];
87
+ const items = allData[kindNamespace];
88
+ cache.set(allCacheKey(kind), items);
89
+ Object.keys(items).forEach(key => {
90
+ cache.set(cacheKey(kind, key), items[key]);
91
+ });
92
+ });
93
+ }
94
+
95
+ cb();
96
+ };
97
+
98
+ if (underlyingStore.initOrderedInternal) {
99
+ const orderedData = sortAllCollections(allData);
100
+ underlyingStore.initOrderedInternal(orderedData, afterInit);
101
+ } else {
102
+ underlyingStore.initInternal(allData, afterInit);
103
+ }
104
+ },
105
+ [],
106
+ cb
107
+ );
108
+ };
109
+
110
+ this.initialized = cb => {
111
+ if (initialized) {
112
+ cb(true);
113
+ } else if (cache && cache.get(initializedKey)) {
114
+ cb(false);
115
+ } else {
116
+ underlyingStore.initializedInternal(inited => {
117
+ initialized = inited;
118
+ if (!initialized) {
119
+ cache && cache.set(initializedKey, true);
120
+ }
121
+ cb(initialized);
122
+ });
123
+ }
124
+ };
125
+
126
+ this.all = (kind, cb) => {
127
+ const items = cache && cache.get(allCacheKey(kind));
128
+ if (items) {
129
+ cb(items);
130
+ return;
131
+ }
132
+
133
+ underlyingStore.getAllInternal(kind, items => {
134
+ if (items === null || items === undefined) {
135
+ cb(items);
136
+ return;
137
+ }
138
+ const filteredItems = {};
139
+ Object.keys(items).forEach(key => {
140
+ const item = items[key];
141
+ if (item && !item.deleted) {
142
+ filteredItems[key] = item;
143
+ }
144
+ });
145
+ cache && cache.set(allCacheKey(kind), filteredItems);
146
+ cb(filteredItems);
147
+ });
148
+ };
149
+
150
+ this.get = (kind, key, cb) => {
151
+ if (cache) {
152
+ const item = cache.get(cacheKey(kind, key));
153
+ if (item !== undefined) {
154
+ cb(itemOnlyIfNotDeleted(item));
155
+ return;
156
+ }
157
+ }
158
+
159
+ underlyingStore.getInternal(kind, key, item => {
160
+ cache && cache.set(cacheKey(kind, key), item);
161
+ cb(itemOnlyIfNotDeleted(item));
162
+ });
163
+ };
164
+
165
+ function itemOnlyIfNotDeleted(item) {
166
+ return !item || item.deleted ? null : item;
167
+ }
168
+
169
+ this.upsert = (kind, newItem, cb) => {
170
+ queue.enqueue(
171
+ cb => {
172
+ flushAllCaches();
173
+ underlyingStore.upsertInternal(kind, newItem, (err, updatedItem) => {
174
+ if (!err) {
175
+ cache && cache.set(cacheKey(kind, newItem.key), updatedItem);
176
+ }
177
+ cb();
178
+ });
179
+ },
180
+ [],
181
+ cb
182
+ );
183
+ };
184
+
185
+ this.delete = (kind, key, version, cb) => {
186
+ this.upsert(kind, { key: key, version: version, deleted: true }, cb);
187
+ };
188
+
189
+ this.close = () => {
190
+ cache && cache.close();
191
+ underlyingStore.close();
192
+ };
193
+
194
+ function flushAllCaches() {
195
+ if (!cache) {
196
+ return;
197
+ }
198
+ for (const eachKind of Object.values(dataKind)) {
199
+ cache.del(allCacheKey(eachKind));
200
+ }
201
+ }
202
+
203
+ // This and the next function are used by init() to provide the best ordering of items
204
+ // to write the underlying store, if the store supports the initOrderedInternal method.
205
+ function sortAllCollections(dataMap) {
206
+ const result = [];
207
+ Object.keys(dataMap).forEach(kindNamespace => {
208
+ const kind = dataKind[kindNamespace];
209
+ result.push({ kind: kind, items: sortCollection(kind, dataMap[kindNamespace]) });
210
+ });
211
+ const kindPriority = kind => (kind.priority === undefined ? kind.namespace.length : kind.priority);
212
+ result.sort((i1, i2) => kindPriority(i1.kind) - kindPriority(i2.kind));
213
+ return result;
214
+ }
215
+
216
+ function sortCollection(kind, itemsMap) {
217
+ const itemsOut = [];
218
+ const remainingItems = new Set(Object.keys(itemsMap));
219
+ const addWithDependenciesFirst = key => {
220
+ if (remainingItems.has(key)) {
221
+ remainingItems.delete(key);
222
+ const item = itemsMap[key];
223
+ if (kind.getDependencyKeys) {
224
+ kind.getDependencyKeys(item).forEach(prereqKey => {
225
+ addWithDependenciesFirst(prereqKey);
226
+ });
227
+ }
228
+ itemsOut.push(item);
229
+ }
230
+ };
231
+ while (remainingItems.size > 0) {
232
+ // pick a random item that hasn't been updated yet
233
+ const key = remainingItems.values().next().value;
234
+ addWithDependenciesFirst(key);
235
+ }
236
+ return itemsOut;
237
+ }
238
+ }
239
+
240
+ module.exports = CachingStoreWrapper;