Skip to content

Commit

Permalink
feat!: implement events and shutdown for spec 0.6.0 (#422)
Browse files Browse the repository at this point in the history
Signed-off-by: Lukas Reining <[email protected]>
  • Loading branch information
lukas-reining authored Jun 27, 2023
1 parent 64c7d3a commit 3db6927
Show file tree
Hide file tree
Showing 4 changed files with 193 additions and 41 deletions.
39 changes: 24 additions & 15 deletions libs/providers/config-cat/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,24 @@ This provider is an implementation for [ConfigCat](https://configcat.com) a mana
$ npm install @openfeature/config-cat-provider
```

#### Required peer dependencies

The OpenFeature SDK is required as peer dependency.

The minimum required version of `@openfeature/js-sdk` currently is `1.3.0`.

The minimum required version of `configcat-js` currently is `7.0.0`.

```
$ npm install @openfeature/js-sdk configcat-js
```

## Usage

The ConfigCat provider uses the [ConfigCat Javascript SDK](https://configcat.com/docs/sdk-reference/js/).

It can either be created by passing the ConfigCat SDK options to ```ConfigCatProvider.create``` or injecting a ConfigCat
SDK client into ```ConfigCatProvider.createFromClient```.
It can either be created by passing the ConfigCat SDK options to ```ConfigCatProvider.create``` or
the ```ConfigCatProvider``` constructor.

The available options can be found in the [ConfigCat Javascript SDK docs](https://configcat.com/docs/sdk-reference/js/).

Expand All @@ -22,7 +34,7 @@ The available options can be found in the [ConfigCat Javascript SDK docs](https:
```javascript
import { ConfigCatProvider } from '@openfeature/config-cat-provider';

const provider = OpenFeature.setProvider(ConfigCatProvider.create('<sdk_key>'));
const provider = ConfigCatProvider.create('<sdk_key>');
OpenFeature.setProvider(provider);
```

Expand All @@ -38,18 +50,6 @@ const provider = ConfigCatProvider.create('<sdk_key>', PollingMode.LazyLoad, {
OpenFeature.setProvider(provider);
```

### Example injecting a client

```javascript
import { ConfigCatProvider } from '@openfeature/config-cat-provider';
import * as configcat from 'configcat-js';

const configCatClient = configcat.getClient("<sdk_key>")
const provider = ConfigCatProvider.createFromClient(configCatClient);

OpenFeature.setProvider(provider);
```

## Evaluation Context

ConfigCat only supports string values in its "evaluation
Expand Down Expand Up @@ -110,6 +110,15 @@ User:
}
```

## Events

The ConfigCat provider emits the
following [OpenFeature events](https://openfeature.dev/specification/types#provider-events):

- PROVIDER_READY
- PROVIDER_ERROR
- PROVIDER_CONFIGURATION_CHANGED

## Building

Run `nx package providers-config-cat` to build the library.
Expand Down
2 changes: 1 addition & 1 deletion libs/providers/config-cat/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"current-version": "echo $npm_package_version"
},
"peerDependencies": {
"@openfeature/js-sdk": "^1.0.0",
"@openfeature/js-sdk": "^1.3.0",
"configcat-js": "^7.0.0 || ^8.0.0"
}
}
106 changes: 95 additions & 11 deletions libs/providers/config-cat/src/lib/config-cat-provider.spec.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
import { ConfigCatProvider } from './config-cat-provider';
import { ParseError, TypeMismatchError } from '@openfeature/js-sdk';
import { ParseError, ProviderEvents, TypeMismatchError } from '@openfeature/js-sdk';
import {
IConfigCatClient,
getClient,
createConsoleLogger,
createFlagOverridesFromMap,
HookEvents,
ISetting,
LogLevel,
OverrideBehaviour,
createConsoleLogger,
PollingMode,
} from 'configcat-js';
import { LogLevel } from 'configcat-common';

import { IEventEmitter } from 'configcat-common/lib/EventEmitter';

describe('ConfigCatProvider', () => {
const targetingKey = "abc";
const targetingKey = 'abc';

let client: IConfigCatClient;
let provider: ConfigCatProvider;
let configCatEmitter: IEventEmitter<HookEvents>;

const values = {
booleanFalse: false,
Expand All @@ -26,23 +29,104 @@ describe('ConfigCatProvider', () => {
jsonPrimitive: JSON.stringify(123),
};

beforeAll(() => {
client = getClient('__key__', undefined, {
beforeAll(async () => {
provider = ConfigCatProvider.create('__key__', PollingMode.ManualPoll, {
logger: createConsoleLogger(LogLevel.Off),
offline: true,
flagOverrides: createFlagOverridesFromMap(values, OverrideBehaviour.LocalOnly),
});
provider = ConfigCatProvider.createFromClient(client);

await provider.initialize();

// Currently there is no option to get access to the event emitter
configCatEmitter = (provider.configCatClient as any).options.hooks;
});

afterAll(() => {
client.dispose();
provider.onClose();
});

it('should be an instance of ConfigCatProvider', () => {
expect(provider).toBeInstanceOf(ConfigCatProvider);
});

it('should dispose the configcat client on provider closing', async () => {
const newProvider = ConfigCatProvider.create('__another_key__', PollingMode.AutoPoll, {
logger: createConsoleLogger(LogLevel.Off),
offline: true,
flagOverrides: createFlagOverridesFromMap(values, OverrideBehaviour.LocalOnly),
});

await newProvider.initialize();

if (!newProvider.configCatClient) {
throw Error('No ConfigCat client');
}

const clientDisposeSpy = jest.spyOn(newProvider.configCatClient, 'dispose');
await newProvider.onClose();

expect(clientDisposeSpy).toHaveBeenCalled();
});

describe('events', () => {
it('should emit PROVIDER_READY event', () => {
const handler = jest.fn();
provider.events.addHandler(ProviderEvents.Ready, handler);
configCatEmitter.emit('clientReady');
expect(handler).toHaveBeenCalled();
});

it('should emit PROVIDER_READY event on initialization', async () => {
const newProvider = ConfigCatProvider.create('__another_key__', PollingMode.ManualPoll, {
logger: createConsoleLogger(LogLevel.Off),
offline: true,
flagOverrides: createFlagOverridesFromMap(values, OverrideBehaviour.LocalOnly),
});

const handler = jest.fn();
newProvider.events.addHandler(ProviderEvents.Ready, handler);
await newProvider.initialize();

expect(handler).toHaveBeenCalled();
});

it('should emit PROVIDER_READY event without options', async () => {
const newProvider = ConfigCatProvider.create('__yet_another_key__', PollingMode.ManualPoll);

const handler = jest.fn();
newProvider.events.addHandler(ProviderEvents.Ready, handler);
await newProvider.initialize();

expect(handler).toHaveBeenCalled();
});

it('should emit PROVIDER_CONFIGURATION_CHANGED event', () => {
const handler = jest.fn();
const eventData = { settings: { myFlag: {} as ISetting } };

provider.events.addHandler(ProviderEvents.ConfigurationChanged, handler);
configCatEmitter.emit('configChanged', eventData);

expect(handler).toHaveBeenCalledWith({
flagsChanged: ['myFlag'],
});
});

it('should emit PROVIDER_ERROR event', () => {
const handler = jest.fn();
const eventData: [string, unknown] = ['error', { error: 'error' }];

provider.events.addHandler(ProviderEvents.Error, handler);
configCatEmitter.emit('clientError', ...eventData);

expect(handler).toHaveBeenCalledWith({
message: eventData[0],
metadata: eventData[1],
});
});
});

describe('method resolveBooleanEvaluation', () => {
it('should return default value for missing value', async () => {
const value = await provider.resolveBooleanEvaluation('nonExistent', false, { targetingKey });
Expand Down
87 changes: 73 additions & 14 deletions libs/providers/config-cat/src/lib/config-cat-provider.ts
Original file line number Diff line number Diff line change
@@ -1,42 +1,89 @@
import {
EvaluationContext,
GeneralError,
JsonValue,
OpenFeatureEventEmitter,
ParseError,
Provider,
ProviderEvents,
ResolutionDetails,
ResolutionReason,
StandardResolutionReasons,
TypeMismatchError,
} from '@openfeature/js-sdk';
import * as configcat from 'configcat-js';
import { IConfigCatClient } from 'configcat-js';
import { SettingValue } from 'configcat-common/lib/RolloutEvaluator';
import { getClient, IConfigCatClient, IEvaluationDetails, SettingValue } from 'configcat-js';
import { transformContext } from './context-transformer';

export class ConfigCatProvider implements Provider {
private client: IConfigCatClient;
private readonly clientParameters: Parameters<typeof getClient>;
public readonly events = new OpenFeatureEventEmitter();
private client?: IConfigCatClient;

private constructor(client: IConfigCatClient) {
this.client = client;
public metadata = {
name: ConfigCatProvider.name,
};

constructor(...params: Parameters<typeof getClient>) {
this.clientParameters = params;
}

public static create(...params: Parameters<typeof configcat.getClient>) {
return new ConfigCatProvider(configcat.getClient(...params));
public static create(...params: Parameters<typeof getClient>) {
return new ConfigCatProvider(...params);
}

public static createFromClient(client: IConfigCatClient) {
return new ConfigCatProvider(client);
public async initialize(): Promise<void> {
return new Promise((resolve) => {
const originalParameters = this.clientParameters;
originalParameters[2] ??= {};

const options = originalParameters[2];
const oldSetupHooks = options.setupHooks;

options.setupHooks = (hooks) => {
oldSetupHooks?.(hooks);

// After resolving, once, we can simply emit events the next time
hooks.once('clientReady', () => {
hooks.on('clientReady', () => this.events.emit(ProviderEvents.Ready));
this.events.emit(ProviderEvents.Ready);
resolve();
});

hooks.on('configChanged', (projectConfig) =>
this.events.emit(ProviderEvents.ConfigurationChanged, {
flagsChanged: Object.keys(projectConfig.settings),
})
);

hooks.on('clientError', (message: string, error) =>
this.events.emit(ProviderEvents.Error, {
message: message,
metadata: error,
})
);
};

this.client = getClient(...originalParameters);
});
}

metadata = {
name: ConfigCatProvider.name,
};
public get configCatClient() {
return this.client;
}

public async onClose(): Promise<void> {
await this.client?.dispose();
}

async resolveBooleanEvaluation(
flagKey: string,
defaultValue: boolean,
context: EvaluationContext
): Promise<ResolutionDetails<boolean>> {
if (!this.client) {
throw new GeneralError('Provider is not initialized');
}

const { value, ...evaluationData } = await this.client.getValueDetailsAsync<SettingValue>(
flagKey,
undefined,
Expand All @@ -55,6 +102,10 @@ export class ConfigCatProvider implements Provider {
defaultValue: string,
context: EvaluationContext
): Promise<ResolutionDetails<string>> {
if (!this.client) {
throw new GeneralError('Provider is not initialized');
}

const { value, ...evaluationData } = await this.client.getValueDetailsAsync<SettingValue>(
flagKey,
undefined,
Expand All @@ -73,6 +124,10 @@ export class ConfigCatProvider implements Provider {
defaultValue: number,
context: EvaluationContext
): Promise<ResolutionDetails<number>> {
if (!this.client) {
throw new GeneralError('Provider is not initialized');
}

const { value, ...evaluationData } = await this.client.getValueDetailsAsync<SettingValue>(
flagKey,
undefined,
Expand All @@ -91,6 +146,10 @@ export class ConfigCatProvider implements Provider {
defaultValue: U,
context: EvaluationContext
): Promise<ResolutionDetails<U>> {
if (!this.client) {
throw new GeneralError('Provider is not initialized');
}

const { value, ...evaluationData } = await this.client.getValueDetailsAsync(
flagKey,
undefined,
Expand Down Expand Up @@ -125,7 +184,7 @@ export class ConfigCatProvider implements Provider {

function toResolutionDetails<U extends JsonValue>(
value: U,
data: Omit<configcat.IEvaluationDetails, 'value'>,
data: Omit<IEvaluationDetails, 'value'>,
reason?: ResolutionReason
): ResolutionDetails<U> {
const matchedRule = Boolean(data.matchedEvaluationRule || data.matchedEvaluationPercentageRule);
Expand Down

0 comments on commit 3db6927

Please sign in to comment.