Skip to content

Commit a8ceda2

Browse files
Todd AndersonTodd Anderson
authored andcommitted
chore: experimental FDv2 configuration hooked up
1 parent 2694cae commit a8ceda2

File tree

13 files changed

+807
-60
lines changed

13 files changed

+807
-60
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)