@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
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
const { BigSegmentStoreManager, hashForUserKey } = require('../big_segments');
|
|
2
|
+
const { nullLogger } = require('../loggers');
|
|
3
|
+
const { AsyncQueue } = require('launchdarkly-js-test-helpers');
|
|
4
|
+
|
|
5
|
+
describe('BigSegmentStoreManager', () => {
|
|
6
|
+
const userKey = 'userkey', userHash = hashForUserKey(userKey);
|
|
7
|
+
const logger = nullLogger();
|
|
8
|
+
const alwaysUpToDate = async () => {
|
|
9
|
+
return { lastUpToDate: new Date().getTime() };
|
|
10
|
+
};
|
|
11
|
+
const alwaysStale = async () => {
|
|
12
|
+
return { lastUpToDate: new Date().getTime() - 1000000 };
|
|
13
|
+
};
|
|
14
|
+
function membershipForExpectedUser(expectedMembership) {
|
|
15
|
+
return async (hash) => {
|
|
16
|
+
expect(hash).toEqual(userHash);
|
|
17
|
+
return expectedMembership;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
async function withManager(store, config, action) {
|
|
21
|
+
const m = BigSegmentStoreManager(store, config, logger);
|
|
22
|
+
try {
|
|
23
|
+
await action(m);
|
|
24
|
+
} finally {
|
|
25
|
+
m.close();
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe('membership query', () => {
|
|
30
|
+
it('with uncached result and healthy status', async () => {
|
|
31
|
+
const expectedMembership = { key1: true, key2: true };
|
|
32
|
+
const store = {
|
|
33
|
+
getMetadata: alwaysUpToDate,
|
|
34
|
+
getUserMembership: membershipForExpectedUser(expectedMembership),
|
|
35
|
+
};
|
|
36
|
+
await withManager(store, {}, async m => {
|
|
37
|
+
const result = await m.getUserMembership(userKey);
|
|
38
|
+
expect(result).toEqual([ expectedMembership, 'HEALTHY' ]);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('with cached result and healthy status', async () => {
|
|
43
|
+
const expectedMembership = { key1: true, key2: true };
|
|
44
|
+
let queryCount = 0;
|
|
45
|
+
const store = {
|
|
46
|
+
getMetadata: alwaysUpToDate,
|
|
47
|
+
getUserMembership: async hash => {
|
|
48
|
+
queryCount++;
|
|
49
|
+
return await membershipForExpectedUser(expectedMembership)(hash);
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
await withManager(store, {}, async m => {
|
|
53
|
+
const result1 = await m.getUserMembership(userKey);
|
|
54
|
+
expect(result1).toEqual([ expectedMembership, 'HEALTHY' ]);
|
|
55
|
+
const result2 = await m.getUserMembership(userKey);
|
|
56
|
+
expect(result2).toEqual(result1);
|
|
57
|
+
|
|
58
|
+
expect(queryCount).toEqual(1);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('with stale status', async () => {
|
|
63
|
+
const expectedMembership = { key1: true, key2: true };
|
|
64
|
+
const store = {
|
|
65
|
+
getMetadata: alwaysStale,
|
|
66
|
+
getUserMembership: membershipForExpectedUser(expectedMembership),
|
|
67
|
+
};
|
|
68
|
+
await withManager(store, {}, async m => {
|
|
69
|
+
const result = await m.getUserMembership(userKey);
|
|
70
|
+
expect(result).toEqual([ expectedMembership, 'STALE' ]);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('with stale status due to no store metadata', async () => {
|
|
75
|
+
const expectedMembership = { key1: true, key2: true };
|
|
76
|
+
const store = {
|
|
77
|
+
getMetadata: async () => undefined,
|
|
78
|
+
getUserMembership: membershipForExpectedUser(expectedMembership),
|
|
79
|
+
};
|
|
80
|
+
await withManager(store, {}, async m => {
|
|
81
|
+
const result = await m.getUserMembership(userKey);
|
|
82
|
+
expect(result).toEqual([ expectedMembership, 'STALE' ]);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('least recent user is evicted from cache', async () => {
|
|
87
|
+
const userKey1 = 'userkey1', userKey2 = 'userkey2', userKey3 = 'userkey3';
|
|
88
|
+
const userHash1 = hashForUserKey(userKey1), userHash2 = hashForUserKey(userKey2), userHash3 = hashForUserKey(userKey3);
|
|
89
|
+
const memberships = {};
|
|
90
|
+
memberships[userHash1] = { seg1: true };
|
|
91
|
+
memberships[userHash2] = { seg2: true };
|
|
92
|
+
memberships[userHash3] = { seg3: true };
|
|
93
|
+
let queriedUsers = [];
|
|
94
|
+
const store = {
|
|
95
|
+
getMetadata: alwaysUpToDate,
|
|
96
|
+
getUserMembership: async hash => {
|
|
97
|
+
queriedUsers.push(hash);
|
|
98
|
+
return memberships[hash];
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
const config = { userCacheSize: 2 };
|
|
102
|
+
await withManager(store, config, async m => {
|
|
103
|
+
const result1 = await m.getUserMembership(userKey1);
|
|
104
|
+
const result2 = await m.getUserMembership(userKey2);
|
|
105
|
+
const result3 = await m.getUserMembership(userKey3);
|
|
106
|
+
expect(result1).toEqual([ memberships[userHash1], 'HEALTHY' ]);
|
|
107
|
+
expect(result2).toEqual([ memberships[userHash2], 'HEALTHY' ]);
|
|
108
|
+
expect(result3).toEqual([ memberships[userHash3], 'HEALTHY' ]);
|
|
109
|
+
|
|
110
|
+
expect(queriedUsers).toEqual([ userHash1, userHash2, userHash3 ]);
|
|
111
|
+
|
|
112
|
+
// Since the capacity is only 2 and userKey1 was the least recently used, that key should be
|
|
113
|
+
// evicted by the userKey3 query. Now only userKey2 and userKey3 are in the cache, and
|
|
114
|
+
// querying them again should not cause a new query to the store.
|
|
115
|
+
|
|
116
|
+
const result2a = await m.getUserMembership(userKey2);
|
|
117
|
+
const result3a = await m.getUserMembership(userKey3);
|
|
118
|
+
expect(result2a).toEqual(result2);
|
|
119
|
+
expect(result3a).toEqual(result3);
|
|
120
|
+
|
|
121
|
+
expect(queriedUsers).toEqual([ userHash1, userHash2, userHash3 ]);
|
|
122
|
+
|
|
123
|
+
const result1a = await m.getUserMembership(userKey1);
|
|
124
|
+
expect(result1a).toEqual(result1);
|
|
125
|
+
|
|
126
|
+
expect(queriedUsers).toEqual([ userHash1, userHash2, userHash3, userHash1 ]);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
describe('status polling', () => {
|
|
132
|
+
it('detects store unavailability', async () => {
|
|
133
|
+
const store = {
|
|
134
|
+
getMetadata: alwaysUpToDate,
|
|
135
|
+
};
|
|
136
|
+
await withManager(store, { statusPollInterval: 0.01 }, async m => {
|
|
137
|
+
const status1 = await m.statusProvider.requireStatus();
|
|
138
|
+
expect(status1.available).toBe(true);
|
|
139
|
+
|
|
140
|
+
const statuses = new AsyncQueue();
|
|
141
|
+
m.statusProvider.on('change', s => statuses.add(s));
|
|
142
|
+
|
|
143
|
+
store.getMetadata = async () => { throw new Error('sorry'); };
|
|
144
|
+
|
|
145
|
+
const status2 = await statuses.take();
|
|
146
|
+
expect(status2.available).toBe(false);
|
|
147
|
+
expect(m.statusProvider.getStatus()).toEqual(status2);
|
|
148
|
+
|
|
149
|
+
store.getMetadata = alwaysUpToDate;
|
|
150
|
+
|
|
151
|
+
const status3 = await statuses.take();
|
|
152
|
+
expect(status3.available).toBe(true);
|
|
153
|
+
expect(m.statusProvider.getStatus()).toEqual(status3);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('detects stale status', async () => {
|
|
158
|
+
const store = {
|
|
159
|
+
getMetadata: alwaysUpToDate,
|
|
160
|
+
};
|
|
161
|
+
await withManager(store, { statusPollInterval: 0.01, staleAfter: 0.2 }, async m => {
|
|
162
|
+
const status1 = await m.statusProvider.requireStatus();
|
|
163
|
+
expect(status1.stale).toBe(false);
|
|
164
|
+
|
|
165
|
+
const statuses = new AsyncQueue();
|
|
166
|
+
m.statusProvider.on('change', s => statuses.add(s));
|
|
167
|
+
|
|
168
|
+
store.getMetadata = alwaysStale;
|
|
169
|
+
|
|
170
|
+
const status2 = await statuses.take();
|
|
171
|
+
expect(status2.stale).toBe(true);
|
|
172
|
+
expect(m.statusProvider.getStatus()).toEqual(status2);
|
|
173
|
+
|
|
174
|
+
store.getMetadata = alwaysUpToDate;
|
|
175
|
+
|
|
176
|
+
const status3 = await statuses.take();
|
|
177
|
+
expect(status3.stale).toBe(false);
|
|
178
|
+
expect(m.statusProvider.getStatus()).toEqual(status3);
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
});
|
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
var CachingStoreWrapper = require('../caching_store_wrapper');
|
|
2
|
+
var features = require('../versioned_data_kind').features;
|
|
3
|
+
var segments = require('../versioned_data_kind').segments;
|
|
4
|
+
const { promisifySingle, sleepAsync } = require('launchdarkly-js-test-helpers');
|
|
5
|
+
|
|
6
|
+
function MockCore() {
|
|
7
|
+
const c = {
|
|
8
|
+
data: { features: {} },
|
|
9
|
+
inited: false,
|
|
10
|
+
initQueriedCount: 0,
|
|
11
|
+
getAllError: false,
|
|
12
|
+
upsertError: null,
|
|
13
|
+
closed: false,
|
|
14
|
+
|
|
15
|
+
initInternal: function(newData, cb) {
|
|
16
|
+
c.data = newData;
|
|
17
|
+
cb();
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
getInternal: function(kind, key, cb) {
|
|
21
|
+
cb(c.data[kind.namespace][key]);
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
getAllInternal: function(kind, cb) {
|
|
25
|
+
cb(c.getAllError ? null : c.data[kind.namespace]);
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
upsertInternal: function(kind, item, cb) {
|
|
29
|
+
if (c.upsertError) {
|
|
30
|
+
cb(c.upsertError, null);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const oldItem = c.data[kind.namespace][item.key];
|
|
34
|
+
if (oldItem && oldItem.version >= item.version) {
|
|
35
|
+
cb(null, oldItem);
|
|
36
|
+
} else {
|
|
37
|
+
c.data[kind.namespace][item.key] = item;
|
|
38
|
+
cb(null, item);
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
initializedInternal: function(cb) {
|
|
43
|
+
c.initQueriedCount++;
|
|
44
|
+
cb(c.inited);
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
close: function() {
|
|
48
|
+
c.closed = true;
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
forceSet: function(kind, item) {
|
|
52
|
+
c.data[kind.namespace][item.key] = item;
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
forceRemove: function(kind, key) {
|
|
56
|
+
delete c.data[kind.namespace][key];
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
return c;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function MockOrderedCore() {
|
|
63
|
+
const c = {
|
|
64
|
+
data: { features: {} },
|
|
65
|
+
|
|
66
|
+
initOrderedInternal: function(newData, cb) {
|
|
67
|
+
c.data = newData;
|
|
68
|
+
cb();
|
|
69
|
+
},
|
|
70
|
+
// don't bother mocking the rest of the stuff since the wrapper behaves identically except for init
|
|
71
|
+
};
|
|
72
|
+
return c;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const cacheSeconds = 15;
|
|
76
|
+
|
|
77
|
+
function runCachedAndUncachedTests(name, testFn, coreFn) {
|
|
78
|
+
var makeCore = coreFn ? coreFn : MockCore;
|
|
79
|
+
describe(name, function() {
|
|
80
|
+
const core1 = makeCore();
|
|
81
|
+
const wrapper1 = new CachingStoreWrapper(core1, cacheSeconds);
|
|
82
|
+
it('cached', async () => await testFn(wrapper1, core1, true), 1000);
|
|
83
|
+
|
|
84
|
+
const core2 = makeCore();
|
|
85
|
+
const wrapper2 = new CachingStoreWrapper(core2, 0);
|
|
86
|
+
it('uncached', async () => await testFn(wrapper2, core2, false), 1000);
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function runCachedTestOnly(name, testFn, coreFn) {
|
|
91
|
+
var makeCore = coreFn ? coreFn : MockCore;
|
|
92
|
+
it(name, async () => {
|
|
93
|
+
const core = makeCore();
|
|
94
|
+
const wrapper = new CachingStoreWrapper(core, cacheSeconds);
|
|
95
|
+
await testFn(wrapper, core);
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
describe('CachingStoreWrapper', function() {
|
|
100
|
+
|
|
101
|
+
runCachedAndUncachedTests('get()', async (wrapper, core, isCached) => {
|
|
102
|
+
const flagv1 = { key: 'flag', version: 1 };
|
|
103
|
+
const flagv2 = { key: 'flag', version: 2 };
|
|
104
|
+
|
|
105
|
+
core.forceSet(features, flagv1);
|
|
106
|
+
|
|
107
|
+
var item = await promisifySingle(wrapper.get)(features, flagv1.key);
|
|
108
|
+
expect(item).toEqual(flagv1);
|
|
109
|
+
|
|
110
|
+
core.forceSet(features, flagv2); // Make a change that bypasses the cache
|
|
111
|
+
|
|
112
|
+
item = await promisifySingle(wrapper.get)(features, flagv1.key);
|
|
113
|
+
// If cached, it should return the cached value rather than calling the underlying getter
|
|
114
|
+
expect(item).toEqual(isCached ? flagv1 : flagv2);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
runCachedAndUncachedTests('get() with deleted item', async (wrapper, core, isCached) => {
|
|
118
|
+
const flagv1 = { key: 'flag', version: 1, deleted: true };
|
|
119
|
+
const flagv2 = { key: 'flag', version: 2, deleted: false };
|
|
120
|
+
|
|
121
|
+
core.forceSet(features, flagv1);
|
|
122
|
+
|
|
123
|
+
var item = await promisifySingle(wrapper.get)(features, flagv1.key);
|
|
124
|
+
expect(item).toBe(null);
|
|
125
|
+
|
|
126
|
+
core.forceSet(features, flagv2); // Make a change that bypasses the cache
|
|
127
|
+
|
|
128
|
+
item = await promisifySingle(wrapper.get)(features, flagv2.key);
|
|
129
|
+
// If cached, the deleted state should persist in the cache
|
|
130
|
+
expect(item).toEqual(isCached ? null : flagv2);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
runCachedAndUncachedTests('get() with missing item', async (wrapper, core, isCached) => {
|
|
134
|
+
const flag = { key: 'flag', version: 1 };
|
|
135
|
+
|
|
136
|
+
var item = await promisifySingle(wrapper.get)(features, flag.key);
|
|
137
|
+
expect(item).toBe(null);
|
|
138
|
+
|
|
139
|
+
core.forceSet(features, flag);
|
|
140
|
+
|
|
141
|
+
item = await promisifySingle(wrapper.get)(features, flag.key);
|
|
142
|
+
// If cached, the previous null result should persist in the cache
|
|
143
|
+
expect(item).toEqual(isCached ? null : flag);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
runCachedTestOnly('cached get() uses values from init()', async (wrapper, core) => {
|
|
147
|
+
const flagv1 = { key: 'flag', version: 1 };
|
|
148
|
+
const flagv2 = { key: 'flag', version: 2 };
|
|
149
|
+
|
|
150
|
+
const allData = { features: { 'flag': flagv1 } };
|
|
151
|
+
|
|
152
|
+
await promisifySingle(wrapper.init)(allData);
|
|
153
|
+
expect(core.data).toEqual(allData);
|
|
154
|
+
|
|
155
|
+
core.forceSet(features, flagv2);
|
|
156
|
+
|
|
157
|
+
var item = await promisifySingle(wrapper.get)(features, flagv1.key);
|
|
158
|
+
expect(item).toEqual(flagv1);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
runCachedAndUncachedTests('all()', async (wrapper, core, isCached) => {
|
|
162
|
+
const flag1 = { key: 'flag1', version: 1 };
|
|
163
|
+
const flag2 = { key: 'flag2', version: 1 };
|
|
164
|
+
|
|
165
|
+
core.forceSet(features, flag1);
|
|
166
|
+
core.forceSet(features, flag2);
|
|
167
|
+
|
|
168
|
+
var items = await promisifySingle(wrapper.all)(features);
|
|
169
|
+
expect(items).toEqual({ 'flag1': flag1, 'flag2': flag2 });
|
|
170
|
+
|
|
171
|
+
core.forceRemove(features, flag2.key);
|
|
172
|
+
|
|
173
|
+
items = await promisifySingle(wrapper.all)(features);
|
|
174
|
+
if (isCached) {
|
|
175
|
+
expect(items).toEqual({ 'flag1': flag1, 'flag2': flag2 });
|
|
176
|
+
} else {
|
|
177
|
+
expect(items).toEqual({ 'flag1': flag1 });
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
runCachedAndUncachedTests('all() with deleted item', async (wrapper, core, isCached) => {
|
|
182
|
+
const flag1 = { key: 'flag1', version: 1 };
|
|
183
|
+
const flag2 = { key: 'flag2', version: 1, deleted: true };
|
|
184
|
+
|
|
185
|
+
core.forceSet(features, flag1);
|
|
186
|
+
core.forceSet(features, flag2);
|
|
187
|
+
|
|
188
|
+
var items = await promisifySingle(wrapper.all)(features);
|
|
189
|
+
expect(items).toEqual({ 'flag1': flag1 });
|
|
190
|
+
|
|
191
|
+
core.forceRemove(features, flag1.key);
|
|
192
|
+
|
|
193
|
+
items = await promisifySingle(wrapper.all)(features);
|
|
194
|
+
if (isCached) {
|
|
195
|
+
expect(items).toEqual({ 'flag1': flag1 });
|
|
196
|
+
} else {
|
|
197
|
+
expect(items).toEqual({ });
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
runCachedAndUncachedTests('all() error condition', async (wrapper, core, isCached) => {
|
|
202
|
+
core.getAllError = true;
|
|
203
|
+
|
|
204
|
+
var items = await promisifySingle(wrapper.all)(features);
|
|
205
|
+
expect(items).toBe(null);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
runCachedTestOnly('cached all() uses values from init()', async (wrapper, core) => {
|
|
209
|
+
const flag1 = { key: 'flag1', version: 1 };
|
|
210
|
+
const flag2 = { key: 'flag2', version: 1 };
|
|
211
|
+
|
|
212
|
+
const allData = { features: { flag1: flag1, flag2: flag2 } };
|
|
213
|
+
|
|
214
|
+
await promisifySingle(wrapper.init)(allData);
|
|
215
|
+
core.forceRemove(features, flag2.key);
|
|
216
|
+
|
|
217
|
+
var items = await promisifySingle(wrapper.all)(features);
|
|
218
|
+
expect(items).toEqual({ flag1: flag1, flag2: flag2 });
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
runCachedTestOnly('cached all() uses fresh values if there has been an update', async (wrapper, core) => {
|
|
222
|
+
const flag1v1 = { key: 'flag1', version: 1 };
|
|
223
|
+
const flag1v2 = { key: 'flag1', version: 2 };
|
|
224
|
+
const flag2v1 = { key: 'flag2', version: 1 };
|
|
225
|
+
const flag2v2 = { key: 'flag2', version: 2 };
|
|
226
|
+
|
|
227
|
+
const allData = { features: { flag1: flag1v1, flag2: flag2v2 } };
|
|
228
|
+
|
|
229
|
+
await promisifySingle(wrapper.init)(allData);
|
|
230
|
+
expect(core.data).toEqual(allData);
|
|
231
|
+
|
|
232
|
+
// make a change to flag1 using the wrapper - this should flush the cache
|
|
233
|
+
await promisifySingle(wrapper.upsert)(features, flag1v2);
|
|
234
|
+
// make a change to flag2 that bypasses the cache
|
|
235
|
+
core.forceSet(features, flag2v2);
|
|
236
|
+
|
|
237
|
+
// we should now see both changes since the cache was flushed
|
|
238
|
+
var items = await promisifySingle(wrapper.all)(features);
|
|
239
|
+
expect(items).toEqual({ flag1: flag1v2, flag2: flag2v2 });
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
runCachedAndUncachedTests('upsert() - successful', async (wrapper, core, isCached) => {
|
|
243
|
+
const flagv1 = { key: 'flag', version: 1 };
|
|
244
|
+
const flagv2 = { key: 'flag', version: 2 };
|
|
245
|
+
|
|
246
|
+
await promisifySingle(wrapper.upsert)(features, flagv1);
|
|
247
|
+
expect(core.data[features.namespace][flagv1.key]).toEqual(flagv1);
|
|
248
|
+
|
|
249
|
+
await promisifySingle(wrapper.upsert)(features, flagv2);
|
|
250
|
+
expect(core.data[features.namespace][flagv1.key]).toEqual(flagv2);
|
|
251
|
+
|
|
252
|
+
// if we have a cache, verify that the new item is now cached by writing a different value
|
|
253
|
+
// to the underlying data - get() should still return the cached item
|
|
254
|
+
if (isCached) {
|
|
255
|
+
const flagv3 = { key: 'flag', version: 3 };
|
|
256
|
+
core.forceSet(features, flagv3);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
var item = await promisifySingle(wrapper.get)(features, flagv1.key);
|
|
260
|
+
expect(item).toEqual(flagv2);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
runCachedAndUncachedTests('upsert() - error', async (wrapper, core, isCached) => {
|
|
264
|
+
const flagv1 = { key: 'flag', version: 1 };
|
|
265
|
+
const flagv2 = { key: 'flag', version: 2 };
|
|
266
|
+
|
|
267
|
+
await promisifySingle(wrapper.upsert)(features, flagv1);
|
|
268
|
+
expect(core.data[features.namespace][flagv1.key]).toEqual(flagv1);
|
|
269
|
+
|
|
270
|
+
core.upsertError = new Error('sorry');
|
|
271
|
+
|
|
272
|
+
await promisifySingle(wrapper.upsert)(features, flagv2);
|
|
273
|
+
expect(core.data[features.namespace][flagv1.key]).toEqual(flagv1);
|
|
274
|
+
|
|
275
|
+
// if we have a cache, verify that the old item is still cached by writing a different value
|
|
276
|
+
// to the underlying data - get() should still return the cached item
|
|
277
|
+
if (isCached) {
|
|
278
|
+
const flagv3 = { key: 'flag', version: 3 };
|
|
279
|
+
core.forceSet(features, flagv3);
|
|
280
|
+
var item = await promisifySingle(wrapper.get)(features, flagv1.key);
|
|
281
|
+
expect(item).toEqual(flagv1);
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
runCachedTestOnly('cached upsert() - unsuccessful', async (wrapper, core) => {
|
|
286
|
+
const flagv1 = { key: 'flag', version: 1 };
|
|
287
|
+
const flagv2 = { key: 'flag', version: 2 };
|
|
288
|
+
|
|
289
|
+
core.forceSet(features, flagv2); // this is now in the underlying data, but not in the cache
|
|
290
|
+
|
|
291
|
+
await promisifySingle(wrapper.upsert)(features, flagv1);
|
|
292
|
+
expect(core.data[features.namespace][flagv1.key]).toEqual(flagv2); // value in store remains the same
|
|
293
|
+
|
|
294
|
+
// the cache should now contain flagv2 - check this by making another change that bypasses
|
|
295
|
+
// the cache, and verifying that get() uses the cached value instead
|
|
296
|
+
const flagv3 = { key: 'flag', version: 3 };
|
|
297
|
+
core.forceSet(features, flagv3);
|
|
298
|
+
|
|
299
|
+
var item = await promisifySingle(wrapper.get)(features, flagv1.key);
|
|
300
|
+
expect(item).toEqual(flagv2);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
runCachedAndUncachedTests('delete()', async (wrapper, core, isCached) => {
|
|
304
|
+
const flagv1 = { key: 'flag', version: 1 };
|
|
305
|
+
const flagv2 = { key: 'flag', version: 2, deleted: true };
|
|
306
|
+
const flagv3 = { key: 'flag', version: 3 };
|
|
307
|
+
|
|
308
|
+
core.forceSet(features, flagv1);
|
|
309
|
+
|
|
310
|
+
var item = await promisifySingle(wrapper.get)(features, flagv1.key);
|
|
311
|
+
expect(item).toEqual(flagv1);
|
|
312
|
+
|
|
313
|
+
await promisifySingle(wrapper.delete)(features, flagv1.key, flagv2.version);
|
|
314
|
+
|
|
315
|
+
expect(core.data[features.namespace][flagv1.key]).toEqual(flagv2);
|
|
316
|
+
|
|
317
|
+
// make a change to the flag that bypasses the cache
|
|
318
|
+
core.forceSet(features, flagv3);
|
|
319
|
+
|
|
320
|
+
var item = await promisifySingle(wrapper.get)(features, flagv1.key);
|
|
321
|
+
expect(item).toEqual(isCached ? null : flagv3);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
describe('initialized()', function() {
|
|
325
|
+
it('calls underlying initialized() only if not already inited', async () => {
|
|
326
|
+
const core = MockCore();
|
|
327
|
+
const wrapper = new CachingStoreWrapper(core, 0);
|
|
328
|
+
|
|
329
|
+
var value = await promisifySingle(wrapper.initialized)();
|
|
330
|
+
expect(value).toEqual(false);
|
|
331
|
+
expect(core.initQueriedCount).toEqual(1);
|
|
332
|
+
|
|
333
|
+
core.inited = true;
|
|
334
|
+
|
|
335
|
+
value = await promisifySingle(wrapper.initialized)();
|
|
336
|
+
expect(value).toEqual(true);
|
|
337
|
+
expect(core.initQueriedCount).toEqual(2);
|
|
338
|
+
|
|
339
|
+
core.inited = false; // this should have no effect since we already returned true
|
|
340
|
+
|
|
341
|
+
value = await promisifySingle(wrapper.initialized)();
|
|
342
|
+
expect(value).toEqual(true);
|
|
343
|
+
expect(core.initQueriedCount).toEqual(2);
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it('will not call initialized() if init() has been called', async () => {
|
|
347
|
+
const core = MockCore();
|
|
348
|
+
const wrapper = new CachingStoreWrapper(core, 0);
|
|
349
|
+
|
|
350
|
+
var value = await promisifySingle(wrapper.initialized)();
|
|
351
|
+
expect(value).toEqual(false);
|
|
352
|
+
expect(core.initQueriedCount).toEqual(1);
|
|
353
|
+
|
|
354
|
+
const allData = { features: {} };
|
|
355
|
+
await promisifySingle(wrapper.init)(allData);
|
|
356
|
+
|
|
357
|
+
value = await promisifySingle(wrapper.initialized)();
|
|
358
|
+
expect(value).toEqual(true);
|
|
359
|
+
expect(core.initQueriedCount).toEqual(1);
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
it('can cache false result', async () => {
|
|
363
|
+
const core = MockCore();
|
|
364
|
+
const wrapper = new CachingStoreWrapper(core, 1); // cache TTL = 1 second
|
|
365
|
+
|
|
366
|
+
var value = await promisifySingle(wrapper.initialized)();
|
|
367
|
+
expect(value).toEqual(false);
|
|
368
|
+
expect(core.initQueriedCount).toEqual(1);
|
|
369
|
+
|
|
370
|
+
core.inited = true;
|
|
371
|
+
|
|
372
|
+
value = await promisifySingle(wrapper.initialized)();
|
|
373
|
+
expect(value).toEqual(false);
|
|
374
|
+
expect(core.initQueriedCount).toEqual(1);
|
|
375
|
+
|
|
376
|
+
await sleepAsync(1100);
|
|
377
|
+
|
|
378
|
+
value = await promisifySingle(wrapper.initialized)();
|
|
379
|
+
expect(value).toEqual(true);
|
|
380
|
+
expect(core.initQueriedCount).toEqual(2);
|
|
381
|
+
});
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
describe('close()', function() {
|
|
385
|
+
runCachedAndUncachedTests('closes underlying store', async (wrapper, core) => {
|
|
386
|
+
wrapper.close();
|
|
387
|
+
expect(core.closed).toBe(true);
|
|
388
|
+
});
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
describe('core that uses initOrdered()', function() {
|
|
392
|
+
runCachedAndUncachedTests('receives properly ordered data for init', async (wrapper, core) => {
|
|
393
|
+
var dependencyOrderingTestData = {};
|
|
394
|
+
dependencyOrderingTestData[features.namespace] = {
|
|
395
|
+
a: { key: "a", prerequisites: [ { key: "b" }, { key: "c" } ] },
|
|
396
|
+
b: { key: "b", prerequisites: [ { key: "c" }, { key: "e" } ] },
|
|
397
|
+
c: { key: "c" },
|
|
398
|
+
d: { key: "d" },
|
|
399
|
+
e: { key: "e" },
|
|
400
|
+
f: { key: "f" }
|
|
401
|
+
};
|
|
402
|
+
dependencyOrderingTestData[segments.namespace] = {
|
|
403
|
+
o: { key: "o" }
|
|
404
|
+
};
|
|
405
|
+
await promisifySingle(wrapper.init)(dependencyOrderingTestData);
|
|
406
|
+
|
|
407
|
+
var receivedData = core.data;
|
|
408
|
+
expect(receivedData.length).toEqual(2);
|
|
409
|
+
|
|
410
|
+
// Segments should always come first
|
|
411
|
+
expect(receivedData[0].kind).toEqual(segments);
|
|
412
|
+
expect(receivedData[0].items.length).toEqual(1);
|
|
413
|
+
|
|
414
|
+
// Features should be ordered so that a flag always appears after its prerequisites, if any
|
|
415
|
+
expect(receivedData[1].kind).toEqual(features);
|
|
416
|
+
var featuresMap = dependencyOrderingTestData[features.namespace];
|
|
417
|
+
var featuresList = receivedData[1].items;
|
|
418
|
+
expect(featuresList.length).toEqual(Object.keys(featuresMap).length);
|
|
419
|
+
for (var itemIndex in featuresList) {
|
|
420
|
+
var item = featuresList[itemIndex];
|
|
421
|
+
(item.prerequisites || []).forEach(function(prereq) {
|
|
422
|
+
var prereqKey = prereq.key;
|
|
423
|
+
var prereqItem = featuresMap[prereqKey];
|
|
424
|
+
var prereqIndex = featuresList.indexOf(prereqItem);
|
|
425
|
+
if (prereqIndex > itemIndex) {
|
|
426
|
+
var allKeys = featuresList.map(f => f.key);
|
|
427
|
+
throw new Error(item.key + " depends on " + prereqKey + ", but " + item.key +
|
|
428
|
+
" was listed first; keys in order are [" + allKeys.join(", ") + "]");
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
}, MockOrderedCore);
|
|
433
|
+
});
|
|
434
|
+
});
|