Skip to content

Commit 1e18d66

Browse files
tanderson-ldTodd Anderson
and
Todd Anderson
authored
chore: adds LDTransactionalFeatureStore, LDTransactionalDataSourceUpdates, and FDv2 DataSource impls. (#833)
**Requirements** - [x] I have added test coverage for new or changed functionality - [x] I have followed the repository's [pull request submission guidelines](../blob/main/CONTRIBUTING.md#submitting-pull-requests) - [x] I have validated my changes against all supported platform versions This PR moves changes for FDv2 from the temporary holding branch to main. All picked code has been previously reviewed and was picked from the temporary holding branch using file pick commands. Resolved conflicts with @abarker-launchdarkly's recent initMetadata changes. Please verify those conflict resolutions @abarker-launchdarkly . --------- Co-authored-by: Todd Anderson <[email protected]>
1 parent 681ec66 commit 1e18d66

21 files changed

+1244
-90
lines changed

packages/shared/common/src/api/subsystem/DataSystem/DataSource.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export interface DataSource {
2323
): void;
2424

2525
/**
26-
* May be called any number of times, if already stopped, has no effect.
26+
* May be called any number of times, if already stopped, has no effect. Datasource will not make any additional callbacks after stop returns.
2727
*/
2828
stop(): void;
2929
}

packages/shared/sdk-server/__tests__/data_sources/DataSourceUpdates.test.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ import { AsyncQueue } from 'launchdarkly-js-test-helpers';
22

33
import { internal } from '@launchdarkly/js-sdk-common';
44

5-
import { LDFeatureStore } from '../../src/api/subsystems';
5+
import { LDTransactionalFeatureStore } from '../../src/api/subsystems';
66
import promisify from '../../src/async/promisify';
7-
import DataSourceUpdates from '../../src/data_sources/DataSourceUpdates';
7+
import DataSourceUpdates from '../../src/data_sources/TransactionalDataSourceUpdates';
88
import InMemoryFeatureStore from '../../src/store/InMemoryFeatureStore';
99
import VersionedDataKinds from '../../src/store/VersionedDataKinds';
1010

@@ -13,21 +13,28 @@ type InitMetadata = internal.InitMetadata;
1313
it('passes initialization metadata to underlying feature store', () => {
1414
const metadata: InitMetadata = { environmentId: '12345' };
1515
const store = new InMemoryFeatureStore();
16-
store.init = jest.fn();
16+
store.applyChanges = jest.fn();
1717
const updates = new DataSourceUpdates(
1818
store,
1919
() => false,
2020
() => {},
2121
);
2222
updates.init({}, () => {}, metadata);
23-
expect(store.init).toHaveBeenCalledTimes(1);
24-
expect(store.init).toHaveBeenNthCalledWith(1, expect.any(Object), expect.any(Function), metadata);
23+
expect(store.applyChanges).toHaveBeenCalledTimes(1);
24+
expect(store.applyChanges).toHaveBeenNthCalledWith(
25+
1,
26+
true,
27+
expect.any(Object),
28+
expect.any(Function),
29+
metadata,
30+
undefined,
31+
);
2532
});
2633

2734
describe.each([true, false])(
2835
'given a DataSourceUpdates with in memory store and change listeners: %s',
2936
(listen) => {
30-
let store: LDFeatureStore;
37+
let store: LDTransactionalFeatureStore;
3138
let updates: DataSourceUpdates;
3239

3340
const queue = new AsyncQueue<string>();
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { subsystem } from '../../src';
2+
import OneShotInitializerFDv2 from '../../src/data_sources/OneShotInitializerFDv2';
3+
import Requestor from '../../src/data_sources/Requestor';
4+
import TestLogger from '../Logger';
5+
6+
describe('given a one shot initializer', () => {
7+
const requestor = {
8+
requestAllData: jest.fn(),
9+
};
10+
const allEvents = {
11+
events: [
12+
{
13+
event: 'server-intent',
14+
data: { payloads: [{ code: 'xfer-full', id: 'mockId' }] },
15+
},
16+
{
17+
event: 'put-object',
18+
data: {
19+
kind: 'flag',
20+
key: 'flagA',
21+
version: 123,
22+
object: { objectFieldA: 'objectValueA' },
23+
},
24+
},
25+
{
26+
event: 'payload-transferred',
27+
data: { state: 'mockState', version: 1 },
28+
},
29+
],
30+
};
31+
const jsonData = JSON.stringify(allEvents);
32+
33+
let initializer: OneShotInitializerFDv2;
34+
const mockDataCallback = jest.fn();
35+
const mockStatusCallback = jest.fn();
36+
let testLogger: TestLogger;
37+
38+
beforeEach(() => {
39+
testLogger = new TestLogger();
40+
initializer = new OneShotInitializerFDv2(requestor as unknown as Requestor, testLogger);
41+
});
42+
43+
afterEach(() => {
44+
initializer.stop();
45+
jest.restoreAllMocks();
46+
});
47+
48+
it('makes no requests before being started', () => {
49+
expect(requestor.requestAllData).not.toHaveBeenCalled();
50+
});
51+
52+
it('polls immediately on start', () => {
53+
initializer.start(mockDataCallback, mockStatusCallback);
54+
expect(requestor.requestAllData).toHaveBeenCalledTimes(1);
55+
expect(mockDataCallback).not.toHaveBeenCalled();
56+
expect(mockStatusCallback).toHaveBeenNthCalledWith(1, subsystem.DataSourceState.Initializing);
57+
});
58+
59+
it('calls callback on success', () => {
60+
requestor.requestAllData = jest.fn((cb) => cb(undefined, jsonData));
61+
initializer.start(mockDataCallback, mockStatusCallback);
62+
expect(mockDataCallback).toHaveBeenNthCalledWith(1, true, {
63+
basis: true,
64+
id: `mockId`,
65+
state: `mockState`,
66+
updates: [
67+
{
68+
kind: `flag`,
69+
key: `flagA`,
70+
version: 123,
71+
object: { objectFieldA: 'objectValueA' },
72+
},
73+
],
74+
version: 1,
75+
});
76+
});
77+
});

packages/shared/sdk-server/__tests__/data_sources/PollingProcessor.test.ts

Lines changed: 23 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,12 @@
1-
import { ClientContext } from '@launchdarkly/js-sdk-common';
2-
31
import { LDFeatureStore } from '../../src';
42
import PollingProcessor from '../../src/data_sources/PollingProcessor';
53
import Requestor from '../../src/data_sources/Requestor';
6-
import Configuration from '../../src/options/Configuration';
74
import AsyncStoreFacade from '../../src/store/AsyncStoreFacade';
85
import InMemoryFeatureStore from '../../src/store/InMemoryFeatureStore';
96
import VersionedDataKinds from '../../src/store/VersionedDataKinds';
10-
import { createBasicPlatform } from '../createBasicPlatform';
117
import TestLogger, { LogLevel } from '../Logger';
128

13-
describe('given an event processor', () => {
9+
describe('given a polling processor', () => {
1410
const requestor = {
1511
requestAllData: jest.fn(),
1612
};
@@ -23,24 +19,19 @@ describe('given an event processor', () => {
2319

2420
let store: LDFeatureStore;
2521
let storeFacade: AsyncStoreFacade;
26-
let config: Configuration;
2722
let processor: PollingProcessor;
2823
let initSuccessHandler: jest.Mock;
2924

3025
beforeEach(() => {
3126
store = new InMemoryFeatureStore();
3227
storeFacade = new AsyncStoreFacade(store);
33-
config = new Configuration({
34-
featureStore: store,
35-
pollInterval: longInterval,
36-
logger: new TestLogger(),
37-
});
3828
initSuccessHandler = jest.fn();
3929

4030
processor = new PollingProcessor(
41-
config,
4231
requestor as unknown as Requestor,
43-
config.featureStoreFactory(new ClientContext('', config, createBasicPlatform())),
32+
longInterval,
33+
store,
34+
new TestLogger(),
4435
initSuccessHandler,
4536
);
4637
});
@@ -99,27 +90,22 @@ describe('given a polling processor with a short poll duration', () => {
9990
const jsonData = JSON.stringify(allData);
10091

10192
let store: LDFeatureStore;
102-
let config: Configuration;
93+
let testLogger: TestLogger;
10394
let processor: PollingProcessor;
10495
let initSuccessHandler: jest.Mock;
10596
let errorHandler: jest.Mock;
10697

10798
beforeEach(() => {
10899
store = new InMemoryFeatureStore();
109-
config = new Configuration({
110-
featureStore: store,
111-
pollInterval: shortInterval,
112-
logger: new TestLogger(),
113-
});
100+
testLogger = new TestLogger();
114101
initSuccessHandler = jest.fn();
115102
errorHandler = jest.fn();
116103

117-
// Configuration will not let us set this as low as needed for the test.
118-
Object.defineProperty(config, 'pollInterval', { value: 0.1 });
119104
processor = new PollingProcessor(
120-
config,
121105
requestor as unknown as Requestor,
122-
config.featureStoreFactory(new ClientContext('', config, createBasicPlatform())),
106+
shortInterval,
107+
store,
108+
testLogger,
123109
initSuccessHandler,
124110
errorHandler,
125111
);
@@ -130,14 +116,21 @@ describe('given a polling processor with a short poll duration', () => {
130116
jest.resetAllMocks();
131117
});
132118

133-
it('polls repeatedly', (done) => {
134-
requestor.requestAllData = jest.fn((cb) => cb(undefined, jsonData));
119+
it('polls repeatedly', async () => {
120+
const expectedCalls = new Promise<void>((resolve) => {
121+
let callCount = 0;
122+
requestor.requestAllData = jest.fn((cb) => {
123+
cb(undefined, jsonData);
124+
callCount += 1;
125+
if (callCount >= 10) {
126+
resolve();
127+
}
128+
});
135129

136-
processor.start();
137-
setTimeout(() => {
138-
expect(requestor.requestAllData.mock.calls.length).toBeGreaterThanOrEqual(4);
139-
done();
140-
}, 500);
130+
processor.start();
131+
});
132+
133+
await expectedCalls;
141134
});
142135

143136
it.each<number | jest.DoneCallback>([400, 408, 429, 500, 503])(
@@ -158,7 +151,6 @@ describe('given a polling processor with a short poll duration', () => {
158151
expect(errorHandler).not.toBeCalled();
159152
setTimeout(() => {
160153
expect(requestor.requestAllData.mock.calls.length).toBeGreaterThanOrEqual(2);
161-
const testLogger = config.logger as TestLogger;
162154
expect(testLogger.getCount(LogLevel.Error)).toBe(0);
163155
expect(testLogger.getCount(LogLevel.Warn)).toBeGreaterThan(2);
164156
(done as jest.DoneCallback)();
@@ -176,7 +168,6 @@ describe('given a polling processor with a short poll duration', () => {
176168

177169
setTimeout(() => {
178170
expect(requestor.requestAllData.mock.calls.length).toBeGreaterThanOrEqual(2);
179-
const testLogger = config.logger as TestLogger;
180171
expect(testLogger.getCount(LogLevel.Error)).toBeGreaterThan(2);
181172
(done as jest.DoneCallback)();
182173
}, 300);
@@ -199,7 +190,6 @@ describe('given a polling processor with a short poll duration', () => {
199190

200191
setTimeout(() => {
201192
expect(requestor.requestAllData.mock.calls.length).toBe(1);
202-
const testLogger = config.logger as TestLogger;
203193
expect(testLogger.getCount(LogLevel.Error)).toBe(1);
204194
(done as jest.DoneCallback)();
205195
}, 300);

0 commit comments

Comments
 (0)