@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.
- package/.babelrc +16 -0
- package/.circleci/config.yml +89 -0
- package/.eslintignore +5 -0
- package/.eslintrc.yaml +114 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +37 -0
- package/.github/ISSUE_TEMPLATE/config.yml +5 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
- package/.github/pull_request_template.md +21 -0
- package/.github/workflows/stale.yml +8 -0
- package/.hound.yml +33 -0
- package/.ldrelease/config.yml +28 -0
- package/.prettierrc +6 -0
- package/CHANGELOG.md +603 -0
- package/CODEOWNERS +2 -0
- package/CONTRIBUTING.md +55 -0
- package/LICENSE.txt +13 -0
- package/README.md +36 -0
- package/SECURITY.md +5 -0
- package/attribute_reference.js +217 -0
- package/big_segments.js +117 -0
- package/caching_store_wrapper.js +240 -0
- package/changes.json +30 -0
- package/configuration.js +235 -0
- package/context.js +98 -0
- package/context_filter.js +137 -0
- package/contract-tests/README.md +7 -0
- package/contract-tests/index.js +109 -0
- package/contract-tests/log.js +23 -0
- package/contract-tests/package.json +15 -0
- package/contract-tests/sdkClientEntity.js +110 -0
- package/contract-tests/testharness-suppressions.txt +2 -0
- package/diagnostic_events.js +151 -0
- package/docs/typedoc.js +10 -0
- package/errors.js +26 -0
- package/evaluator.js +822 -0
- package/event_factory.js +121 -0
- package/event_processor.js +320 -0
- package/event_summarizer.js +101 -0
- package/feature_store.js +120 -0
- package/feature_store_event_wrapper.js +258 -0
- package/file_data_source.js +192 -0
- package/flags_state.js +46 -0
- package/index.d.ts +2426 -0
- package/index.js +452 -0
- package/integrations.js +7 -0
- package/interfaces.js +2 -0
- package/loggers.js +125 -0
- package/messages.js +31 -0
- package/operators.js +106 -0
- package/package.json +105 -0
- package/polling.js +70 -0
- package/requestor.js +62 -0
- package/scripts/better-audit.sh +76 -0
- package/sharedtest/big_segment_store_tests.js +86 -0
- package/sharedtest/feature_store_tests.js +177 -0
- package/sharedtest/persistent_feature_store_tests.js +183 -0
- package/sharedtest/store_tests.js +7 -0
- package/streaming.js +179 -0
- package/test/LDClient-big-segments-test.js +92 -0
- package/test/LDClient-end-to-end-test.js +218 -0
- package/test/LDClient-evaluation-all-flags-test.js +226 -0
- package/test/LDClient-evaluation-test.js +204 -0
- package/test/LDClient-events-test.js +502 -0
- package/test/LDClient-listeners-test.js +180 -0
- package/test/LDClient-test.js +96 -0
- package/test/LDClient-tls-test.js +110 -0
- package/test/attribute_reference-test.js +494 -0
- package/test/big_segments-test.js +182 -0
- package/test/caching_store_wrapper-test.js +434 -0
- package/test/configuration-test.js +249 -0
- package/test/context-test.js +93 -0
- package/test/context_filter-test.js +424 -0
- package/test/diagnostic_events-test.js +152 -0
- package/test/evaluator-big-segments-test.js +301 -0
- package/test/evaluator-bucketing-test.js +333 -0
- package/test/evaluator-clause-test.js +277 -0
- package/test/evaluator-flag-test.js +452 -0
- package/test/evaluator-pre-conditions-test.js +105 -0
- package/test/evaluator-rule-test.js +131 -0
- package/test/evaluator-segment-match-test.js +310 -0
- package/test/evaluator_helpers.js +106 -0
- package/test/event_processor-test.js +680 -0
- package/test/event_summarizer-test.js +146 -0
- package/test/feature_store-test.js +42 -0
- package/test/feature_store_event_wrapper-test.js +182 -0
- package/test/feature_store_test_base.js +60 -0
- package/test/file_data_source-test.js +255 -0
- package/test/loggers-test.js +126 -0
- package/test/operators-test.js +102 -0
- package/test/polling-test.js +158 -0
- package/test/requestor-test.js +60 -0
- package/test/store_tests_big_segments-test.js +61 -0
- package/test/streaming-test.js +323 -0
- package/test/stubs.js +107 -0
- package/test/test_data-test.js +341 -0
- package/test/update_queue-test.js +61 -0
- package/test-types.ts +210 -0
- package/test_data.js +323 -0
- package/tsconfig.json +14 -0
- package/update_queue.js +28 -0
- package/utils/__tests__/httpUtils-test.js +39 -0
- package/utils/__tests__/wrapPromiseCallback-test.js +33 -0
- package/utils/asyncUtils.js +32 -0
- package/utils/httpUtils.js +105 -0
- package/utils/stringifyAttrs.js +14 -0
- package/utils/wrapPromiseCallback.js +36 -0
- package/versioned_data_kind.js +34 -0
package/CONTRIBUTING.md
ADDED
|
@@ -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
|
+
};
|
package/big_segments.js
ADDED
|
@@ -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;
|