Skip to content

chore: experimental FDv2 configuration hooked up #853

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: ta/fdv2-code-move-part3
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
TransitionConditions,
} from '../../../src/datasource/CompositeDataSource';
import { DataSourceErrorKind } from '../../../src/datasource/DataSourceErrorKinds';
import { LDFlagDeliveryFallbackError } from '../../../src/datasource/errors';
import { LDFlagDeliveryFallbackError, LDPollingError } from '../../../src/datasource/errors';

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

it('handles initializer getting error and switches to synchronizer 1', async () => {
const mockInitializer1: DataSource = {
start: jest
.fn()
.mockImplementation(
(
_dataCallback: (basis: boolean, data: any) => void,
_statusCallback: (status: DataSourceState, err?: any) => void,
) => {
_statusCallback(DataSourceState.Initializing);
_statusCallback(
DataSourceState.Closed,
new LDPollingError(DataSourceErrorKind.ErrorResponse, 'polling error'),
);
},
),
stop: jest.fn(),
};

const mockSynchronizer1Data = { key: 'sync1' };
const mockSynchronizer1 = {
start: jest
.fn()
.mockImplementation(
(
_dataCallback: (basis: boolean, data: any) => void,
_statusCallback: (status: DataSourceState, err?: any) => void,
) => {
_statusCallback(DataSourceState.Initializing);
_statusCallback(DataSourceState.Valid, null); // this should lead to recovery
_dataCallback(true, mockSynchronizer1Data);
},
),
stop: jest.fn(),
};

const underTest = new CompositeDataSource(
[makeDataSourceFactory(mockInitializer1)],
[makeDataSourceFactory(mockSynchronizer1)],
[],
undefined,
makeTestTransitionConditions(),
makeZeroBackoff(),
);

let callback;
const statusCallback = jest.fn();
await new Promise<void>((resolve) => {
callback = jest.fn((_: boolean, data: any) => {
if (data === mockSynchronizer1Data) {
resolve();
}
});

underTest.start(callback, statusCallback);
});

expect(mockInitializer1.start).toHaveBeenCalledTimes(1);
expect(mockSynchronizer1.start).toHaveBeenCalledTimes(1);
expect(callback).toHaveBeenCalledTimes(1);
expect(callback).toHaveBeenNthCalledWith(1, true, { key: 'sync1' });
expect(statusCallback).toHaveBeenCalledTimes(3);
expect(statusCallback).toHaveBeenNthCalledWith(1, DataSourceState.Initializing, undefined);
expect(statusCallback).toHaveBeenNthCalledWith(2, DataSourceState.Interrupted, expect.anything()); // sync1 error
expect(statusCallback).toHaveBeenNthCalledWith(3, DataSourceState.Valid, undefined); // sync1 got data
});

it('handles initializer getting basis, switches to synchronizer 1, falls back to synchronizer 2, recovers to synchronizer 1', async () => {
const mockInitializer1: DataSource = {
start: jest
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
// TODO: refactor client-sdk to use this enum
/**
* @experimental
* This feature is not stable and not subject to any backwards compatibility guarantees or semantic
* versioning. It is not suitable for production usage.
*/
export enum DataSourceState {
// Positive confirmation of connection/data receipt
Valid,
Expand All @@ -10,6 +15,11 @@ export enum DataSourceState {
Closed,
}

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

/**
* @experimental
* This feature is not stable and not subject to any backwards compatibility guarantees or semantic
* versioning. It is not suitable for production usage.
*/
export type LDDataSourceFactory = () => DataSource;
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,12 @@ export class CompositeDataSource implements DataSource {
break;
case 'fallback':
default:
// if asked to fallback after using all init factories, switch to sync factories
if (this._initPhaseActive && this._initFactories.pos() >= this._initFactories.length()) {
this._initPhaseActive = false;
this._syncFactories.reset();
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For reviewers: Bugfix found during testing with intermittent wifi. Error in last initializer was not leading to fallback to synchronizer.


if (this._initPhaseActive) {
isPrimary = this._initFactories.pos() === 0;
factory = this._initFactories.next();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { LDOptions } from '../../src';
import { DataSourceOptions, isStandardOptions, LDFeatureStore, LDOptions } from '../../src';
import Configuration from '../../src/options/Configuration';
import InMemoryFeatureStore from '../../src/store/InMemoryFeatureStore';
import TestLogger, { LogLevel } from '../Logger';

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

Expand Down Expand Up @@ -42,6 +43,7 @@ describe.each([undefined, null, 'potat0', 17, [], {}])('constructed without opti
expect(config.wrapperVersion).toBeUndefined();
expect(config.hooks).toBeUndefined();
expect(config.payloadFilterKey).toBeUndefined();
expect(config.dataSystem).toBeUndefined();
});
});

Expand Down Expand Up @@ -408,4 +410,66 @@ describe('when setting different options', () => {
},
]);
});

it('drops invalid datasystem data source options and replaces with defaults', () => {
const config = new Configuration(
withLogger({
dataSystem: { dataSource: { bogus: 'myBogusOptions' } as unknown as DataSourceOptions },
}),
);
expect(isStandardOptions(config.dataSystem!.dataSource)).toEqual(true);
logger(config).expectMessages([
{
level: LogLevel.Warn,
matches: /Config option "dataSource" should be of type DataSourceOptions/,
},
]);
});

it('validates the datasystem persistent store is a factory or object', () => {
const config1 = new Configuration(
withLogger({
dataSystem: {
persistentStore: () => new InMemoryFeatureStore(),
},
}),
);
expect(isStandardOptions(config1.dataSystem!.dataSource)).toEqual(true);
expect(logger(config1).getCount()).toEqual(0);

const config2 = new Configuration(
withLogger({
dataSystem: {
persistentStore: 'bogus type' as unknown as LDFeatureStore,
},
}),
);
expect(isStandardOptions(config2.dataSystem!.dataSource)).toEqual(true);
logger(config2).expectMessages([
{
level: LogLevel.Warn,
matches: /Config option "persistentStore" should be of type LDFeatureStore/,
},
]);
});

it('provides reasonable defaults when datasystem is provided, but some options are missing', () => {
const config = new Configuration(
withLogger({
dataSystem: {},
}),
);
expect(isStandardOptions(config.dataSystem!.dataSource)).toEqual(true);
expect(logger(config).getCount()).toEqual(0);
});

it('provides reasonable defaults within the dataSystem.dataSource options when they are missing', () => {
const config = new Configuration(
withLogger({
dataSystem: { dataSource: { dataSourceOptionsType: 'standard' } },
}),
);
expect(isStandardOptions(config.dataSystem!.dataSource)).toEqual(true);
expect(logger(config).getCount()).toEqual(0);
});
});
Loading
Loading