Skip to content

Commit eae1812

Browse files
tanderson-ldTodd Anderson
andauthored
chore: experimental FDv2 configuration hooked up (#853)
**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 adds new changes to LdClientImpl to utilize FDv2 as an experimental feature. This PR also moves changes for FDv2 from the temporary holding branch to main. --------- Co-authored-by: Todd Anderson <tanderson@Todds-MacBook-Pro.local>
1 parent 6a2ce65 commit eae1812

File tree

12 files changed

+966
-145
lines changed

12 files changed

+966
-145
lines changed

packages/shared/common/__tests__/subsystem/DataSystem/CompositeDataSource.test.ts

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
TransitionConditions,
1010
} from '../../../src/datasource/CompositeDataSource';
1111
import { DataSourceErrorKind } from '../../../src/datasource/DataSourceErrorKinds';
12-
import { LDFlagDeliveryFallbackError } from '../../../src/datasource/errors';
12+
import { LDFlagDeliveryFallbackError, LDPollingError } from '../../../src/datasource/errors';
1313

1414
function makeDataSourceFactory(internal: DataSource): LDDataSourceFactory {
1515
return () => internal;
@@ -107,6 +107,73 @@ it('handles initializer getting basis, switching to synchronizer', async () => {
107107
expect(statusCallback).toHaveBeenNthCalledWith(4, DataSourceState.Valid, undefined);
108108
});
109109

110+
it('handles initializer getting error and switches to synchronizer 1', async () => {
111+
const mockInitializer1: DataSource = {
112+
start: jest
113+
.fn()
114+
.mockImplementation(
115+
(
116+
_dataCallback: (basis: boolean, data: any) => void,
117+
_statusCallback: (status: DataSourceState, err?: any) => void,
118+
) => {
119+
_statusCallback(DataSourceState.Initializing);
120+
_statusCallback(
121+
DataSourceState.Closed,
122+
new LDPollingError(DataSourceErrorKind.ErrorResponse, 'polling error'),
123+
);
124+
},
125+
),
126+
stop: jest.fn(),
127+
};
128+
129+
const mockSynchronizer1Data = { key: 'sync1' };
130+
const mockSynchronizer1 = {
131+
start: jest
132+
.fn()
133+
.mockImplementation(
134+
(
135+
_dataCallback: (basis: boolean, data: any) => void,
136+
_statusCallback: (status: DataSourceState, err?: any) => void,
137+
) => {
138+
_statusCallback(DataSourceState.Initializing);
139+
_statusCallback(DataSourceState.Valid, null); // this should lead to recovery
140+
_dataCallback(true, mockSynchronizer1Data);
141+
},
142+
),
143+
stop: jest.fn(),
144+
};
145+
146+
const underTest = new CompositeDataSource(
147+
[makeDataSourceFactory(mockInitializer1)],
148+
[makeDataSourceFactory(mockSynchronizer1)],
149+
[],
150+
undefined,
151+
makeTestTransitionConditions(),
152+
makeZeroBackoff(),
153+
);
154+
155+
let callback;
156+
const statusCallback = jest.fn();
157+
await new Promise<void>((resolve) => {
158+
callback = jest.fn((_: boolean, data: any) => {
159+
if (data === mockSynchronizer1Data) {
160+
resolve();
161+
}
162+
});
163+
164+
underTest.start(callback, statusCallback);
165+
});
166+
167+
expect(mockInitializer1.start).toHaveBeenCalledTimes(1);
168+
expect(mockSynchronizer1.start).toHaveBeenCalledTimes(1);
169+
expect(callback).toHaveBeenCalledTimes(1);
170+
expect(callback).toHaveBeenNthCalledWith(1, true, { key: 'sync1' });
171+
expect(statusCallback).toHaveBeenCalledTimes(3);
172+
expect(statusCallback).toHaveBeenNthCalledWith(1, DataSourceState.Initializing, undefined);
173+
expect(statusCallback).toHaveBeenNthCalledWith(2, DataSourceState.Interrupted, expect.anything()); // sync1 error
174+
expect(statusCallback).toHaveBeenNthCalledWith(3, DataSourceState.Valid, undefined); // sync1 got data
175+
});
176+
110177
it('handles initializer getting basis, switches to synchronizer 1, falls back to synchronizer 2, recovers to synchronizer 1', async () => {
111178
const mockInitializer1: DataSource = {
112179
start: jest

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
11
// TODO: refactor client-sdk to use this enum
2+
/**
3+
* @experimental
4+
* This feature is not stable and not subject to any backwards compatibility guarantees or semantic
5+
* versioning. It is not suitable for production usage.
6+
*/
27
export enum DataSourceState {
38
// Positive confirmation of connection/data receipt
49
Valid,
@@ -10,6 +15,11 @@ export enum DataSourceState {
1015
Closed,
1116
}
1217

18+
/**
19+
* @experimental
20+
* This feature is not stable and not subject to any backwards compatibility guarantees or semantic
21+
* versioning. It is not suitable for production usage.
22+
*/
1323
export interface DataSource {
1424
/**
1525
* May be called any number of times, if already started, has no effect
@@ -30,4 +40,9 @@ export interface DataSource {
3040
stop(): void;
3141
}
3242

43+
/**
44+
* @experimental
45+
* This feature is not stable and not subject to any backwards compatibility guarantees or semantic
46+
* versioning. It is not suitable for production usage.
47+
*/
3348
export type LDDataSourceFactory = () => DataSource;

packages/shared/common/src/datasource/CompositeDataSource.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,12 @@ export class CompositeDataSource implements DataSource {
288288
break;
289289
case 'fallback':
290290
default:
291+
// if asked to fallback after using all init factories, switch to sync factories
292+
if (this._initPhaseActive && this._initFactories.pos() >= this._initFactories.length()) {
293+
this._initPhaseActive = false;
294+
this._syncFactories.reset();
295+
}
296+
291297
if (this._initPhaseActive) {
292298
isPrimary = this._initFactories.pos() === 0;
293299
factory = this._initFactories.next();

packages/shared/sdk-server/__tests__/options/Configuration.test.ts

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import { LDOptions } from '../../src';
1+
import { DataSourceOptions, isStandardOptions, LDFeatureStore, LDOptions } from '../../src';
22
import Configuration from '../../src/options/Configuration';
3+
import InMemoryFeatureStore from '../../src/store/InMemoryFeatureStore';
34
import TestLogger, { LogLevel } from '../Logger';
45

56
function withLogger(options: LDOptions): LDOptions {
@@ -13,7 +14,7 @@ function logger(options: LDOptions): TestLogger {
1314
describe.each([undefined, null, 'potat0', 17, [], {}])('constructed without options', (input) => {
1415
it('should have default options', () => {
1516
// JavaScript is not going to stop you from calling this with whatever
16-
// you want. So we need to tell TS to ingore our bad behavior.
17+
// you want. So we need to tell TS to ignore our bad behavior.
1718
// @ts-ignore
1819
const config = new Configuration(input);
1920

@@ -42,6 +43,7 @@ describe.each([undefined, null, 'potat0', 17, [], {}])('constructed without opti
4243
expect(config.wrapperVersion).toBeUndefined();
4344
expect(config.hooks).toBeUndefined();
4445
expect(config.payloadFilterKey).toBeUndefined();
46+
expect(config.dataSystem).toBeUndefined();
4547
});
4648
});
4749

@@ -408,4 +410,66 @@ describe('when setting different options', () => {
408410
},
409411
]);
410412
});
413+
414+
it('drops invalid datasystem data source options and replaces with defaults', () => {
415+
const config = new Configuration(
416+
withLogger({
417+
dataSystem: { dataSource: { bogus: 'myBogusOptions' } as unknown as DataSourceOptions },
418+
}),
419+
);
420+
expect(isStandardOptions(config.dataSystem!.dataSource)).toEqual(true);
421+
logger(config).expectMessages([
422+
{
423+
level: LogLevel.Warn,
424+
matches: /Config option "dataSource" should be of type DataSourceOptions/,
425+
},
426+
]);
427+
});
428+
429+
it('validates the datasystem persistent store is a factory or object', () => {
430+
const config1 = new Configuration(
431+
withLogger({
432+
dataSystem: {
433+
persistentStore: () => new InMemoryFeatureStore(),
434+
},
435+
}),
436+
);
437+
expect(isStandardOptions(config1.dataSystem!.dataSource)).toEqual(true);
438+
expect(logger(config1).getCount()).toEqual(0);
439+
440+
const config2 = new Configuration(
441+
withLogger({
442+
dataSystem: {
443+
persistentStore: 'bogus type' as unknown as LDFeatureStore,
444+
},
445+
}),
446+
);
447+
expect(isStandardOptions(config2.dataSystem!.dataSource)).toEqual(true);
448+
logger(config2).expectMessages([
449+
{
450+
level: LogLevel.Warn,
451+
matches: /Config option "persistentStore" should be of type LDFeatureStore/,
452+
},
453+
]);
454+
});
455+
456+
it('provides reasonable defaults when datasystem is provided, but some options are missing', () => {
457+
const config = new Configuration(
458+
withLogger({
459+
dataSystem: {},
460+
}),
461+
);
462+
expect(isStandardOptions(config.dataSystem!.dataSource)).toEqual(true);
463+
expect(logger(config).getCount()).toEqual(0);
464+
});
465+
466+
it('provides reasonable defaults within the dataSystem.dataSource options when they are missing', () => {
467+
const config = new Configuration(
468+
withLogger({
469+
dataSystem: { dataSource: { dataSourceOptionsType: 'standard' } },
470+
}),
471+
);
472+
expect(isStandardOptions(config.dataSystem!.dataSource)).toEqual(true);
473+
expect(logger(config).getCount()).toEqual(0);
474+
});
411475
});

0 commit comments

Comments
 (0)