Skip to content

Commit 3db6927

Browse files
feat!: implement events and shutdown for spec 0.6.0 (open-feature#422)
Signed-off-by: Lukas Reining <[email protected]>
1 parent 64c7d3a commit 3db6927

File tree

4 files changed

+193
-41
lines changed

4 files changed

+193
-41
lines changed

libs/providers/config-cat/README.md

+24-15
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,24 @@ This provider is an implementation for [ConfigCat](https://configcat.com) a mana
88
$ npm install @openfeature/config-cat-provider
99
```
1010

11+
#### Required peer dependencies
12+
13+
The OpenFeature SDK is required as peer dependency.
14+
15+
The minimum required version of `@openfeature/js-sdk` currently is `1.3.0`.
16+
17+
The minimum required version of `configcat-js` currently is `7.0.0`.
18+
19+
```
20+
$ npm install @openfeature/js-sdk configcat-js
21+
```
22+
1123
## Usage
1224

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

15-
It can either be created by passing the ConfigCat SDK options to ```ConfigCatProvider.create``` or injecting a ConfigCat
16-
SDK client into ```ConfigCatProvider.createFromClient```.
27+
It can either be created by passing the ConfigCat SDK options to ```ConfigCatProvider.create``` or
28+
the ```ConfigCatProvider``` constructor.
1729

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

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

25-
const provider = OpenFeature.setProvider(ConfigCatProvider.create('<sdk_key>'));
37+
const provider = ConfigCatProvider.create('<sdk_key>');
2638
OpenFeature.setProvider(provider);
2739
```
2840

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

41-
### Example injecting a client
42-
43-
```javascript
44-
import { ConfigCatProvider } from '@openfeature/config-cat-provider';
45-
import * as configcat from 'configcat-js';
46-
47-
const configCatClient = configcat.getClient("<sdk_key>")
48-
const provider = ConfigCatProvider.createFromClient(configCatClient);
49-
50-
OpenFeature.setProvider(provider);
51-
```
52-
5353
## Evaluation Context
5454

5555
ConfigCat only supports string values in its "evaluation
@@ -110,6 +110,15 @@ User:
110110
}
111111
```
112112

113+
## Events
114+
115+
The ConfigCat provider emits the
116+
following [OpenFeature events](https://openfeature.dev/specification/types#provider-events):
117+
118+
- PROVIDER_READY
119+
- PROVIDER_ERROR
120+
- PROVIDER_CONFIGURATION_CHANGED
121+
113122
## Building
114123

115124
Run `nx package providers-config-cat` to build the library.

libs/providers/config-cat/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"current-version": "echo $npm_package_version"
88
},
99
"peerDependencies": {
10-
"@openfeature/js-sdk": "^1.0.0",
10+
"@openfeature/js-sdk": "^1.3.0",
1111
"configcat-js": "^7.0.0 || ^8.0.0"
1212
}
1313
}

libs/providers/config-cat/src/lib/config-cat-provider.spec.ts

+95-11
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,22 @@
11
import { ConfigCatProvider } from './config-cat-provider';
2-
import { ParseError, TypeMismatchError } from '@openfeature/js-sdk';
2+
import { ParseError, ProviderEvents, TypeMismatchError } from '@openfeature/js-sdk';
33
import {
4-
IConfigCatClient,
5-
getClient,
4+
createConsoleLogger,
65
createFlagOverridesFromMap,
6+
HookEvents,
7+
ISetting,
8+
LogLevel,
79
OverrideBehaviour,
8-
createConsoleLogger,
10+
PollingMode,
911
} from 'configcat-js';
10-
import { LogLevel } from 'configcat-common';
12+
13+
import { IEventEmitter } from 'configcat-common/lib/EventEmitter';
1114

1215
describe('ConfigCatProvider', () => {
13-
const targetingKey = "abc";
16+
const targetingKey = 'abc';
1417

15-
let client: IConfigCatClient;
1618
let provider: ConfigCatProvider;
19+
let configCatEmitter: IEventEmitter<HookEvents>;
1720

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

29-
beforeAll(() => {
30-
client = getClient('__key__', undefined, {
32+
beforeAll(async () => {
33+
provider = ConfigCatProvider.create('__key__', PollingMode.ManualPoll, {
3134
logger: createConsoleLogger(LogLevel.Off),
3235
offline: true,
3336
flagOverrides: createFlagOverridesFromMap(values, OverrideBehaviour.LocalOnly),
3437
});
35-
provider = ConfigCatProvider.createFromClient(client);
38+
39+
await provider.initialize();
40+
41+
// Currently there is no option to get access to the event emitter
42+
configCatEmitter = (provider.configCatClient as any).options.hooks;
3643
});
3744

3845
afterAll(() => {
39-
client.dispose();
46+
provider.onClose();
4047
});
4148

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

53+
it('should dispose the configcat client on provider closing', async () => {
54+
const newProvider = ConfigCatProvider.create('__another_key__', PollingMode.AutoPoll, {
55+
logger: createConsoleLogger(LogLevel.Off),
56+
offline: true,
57+
flagOverrides: createFlagOverridesFromMap(values, OverrideBehaviour.LocalOnly),
58+
});
59+
60+
await newProvider.initialize();
61+
62+
if (!newProvider.configCatClient) {
63+
throw Error('No ConfigCat client');
64+
}
65+
66+
const clientDisposeSpy = jest.spyOn(newProvider.configCatClient, 'dispose');
67+
await newProvider.onClose();
68+
69+
expect(clientDisposeSpy).toHaveBeenCalled();
70+
});
71+
72+
describe('events', () => {
73+
it('should emit PROVIDER_READY event', () => {
74+
const handler = jest.fn();
75+
provider.events.addHandler(ProviderEvents.Ready, handler);
76+
configCatEmitter.emit('clientReady');
77+
expect(handler).toHaveBeenCalled();
78+
});
79+
80+
it('should emit PROVIDER_READY event on initialization', async () => {
81+
const newProvider = ConfigCatProvider.create('__another_key__', PollingMode.ManualPoll, {
82+
logger: createConsoleLogger(LogLevel.Off),
83+
offline: true,
84+
flagOverrides: createFlagOverridesFromMap(values, OverrideBehaviour.LocalOnly),
85+
});
86+
87+
const handler = jest.fn();
88+
newProvider.events.addHandler(ProviderEvents.Ready, handler);
89+
await newProvider.initialize();
90+
91+
expect(handler).toHaveBeenCalled();
92+
});
93+
94+
it('should emit PROVIDER_READY event without options', async () => {
95+
const newProvider = ConfigCatProvider.create('__yet_another_key__', PollingMode.ManualPoll);
96+
97+
const handler = jest.fn();
98+
newProvider.events.addHandler(ProviderEvents.Ready, handler);
99+
await newProvider.initialize();
100+
101+
expect(handler).toHaveBeenCalled();
102+
});
103+
104+
it('should emit PROVIDER_CONFIGURATION_CHANGED event', () => {
105+
const handler = jest.fn();
106+
const eventData = { settings: { myFlag: {} as ISetting } };
107+
108+
provider.events.addHandler(ProviderEvents.ConfigurationChanged, handler);
109+
configCatEmitter.emit('configChanged', eventData);
110+
111+
expect(handler).toHaveBeenCalledWith({
112+
flagsChanged: ['myFlag'],
113+
});
114+
});
115+
116+
it('should emit PROVIDER_ERROR event', () => {
117+
const handler = jest.fn();
118+
const eventData: [string, unknown] = ['error', { error: 'error' }];
119+
120+
provider.events.addHandler(ProviderEvents.Error, handler);
121+
configCatEmitter.emit('clientError', ...eventData);
122+
123+
expect(handler).toHaveBeenCalledWith({
124+
message: eventData[0],
125+
metadata: eventData[1],
126+
});
127+
});
128+
});
129+
46130
describe('method resolveBooleanEvaluation', () => {
47131
it('should return default value for missing value', async () => {
48132
const value = await provider.resolveBooleanEvaluation('nonExistent', false, { targetingKey });

libs/providers/config-cat/src/lib/config-cat-provider.ts

+73-14
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,89 @@
11
import {
22
EvaluationContext,
3+
GeneralError,
34
JsonValue,
5+
OpenFeatureEventEmitter,
46
ParseError,
57
Provider,
8+
ProviderEvents,
69
ResolutionDetails,
710
ResolutionReason,
811
StandardResolutionReasons,
912
TypeMismatchError,
1013
} from '@openfeature/js-sdk';
11-
import * as configcat from 'configcat-js';
12-
import { IConfigCatClient } from 'configcat-js';
13-
import { SettingValue } from 'configcat-common/lib/RolloutEvaluator';
14+
import { getClient, IConfigCatClient, IEvaluationDetails, SettingValue } from 'configcat-js';
1415
import { transformContext } from './context-transformer';
1516

1617
export class ConfigCatProvider implements Provider {
17-
private client: IConfigCatClient;
18+
private readonly clientParameters: Parameters<typeof getClient>;
19+
public readonly events = new OpenFeatureEventEmitter();
20+
private client?: IConfigCatClient;
1821

19-
private constructor(client: IConfigCatClient) {
20-
this.client = client;
22+
public metadata = {
23+
name: ConfigCatProvider.name,
24+
};
25+
26+
constructor(...params: Parameters<typeof getClient>) {
27+
this.clientParameters = params;
2128
}
2229

23-
public static create(...params: Parameters<typeof configcat.getClient>) {
24-
return new ConfigCatProvider(configcat.getClient(...params));
30+
public static create(...params: Parameters<typeof getClient>) {
31+
return new ConfigCatProvider(...params);
2532
}
2633

27-
public static createFromClient(client: IConfigCatClient) {
28-
return new ConfigCatProvider(client);
34+
public async initialize(): Promise<void> {
35+
return new Promise((resolve) => {
36+
const originalParameters = this.clientParameters;
37+
originalParameters[2] ??= {};
38+
39+
const options = originalParameters[2];
40+
const oldSetupHooks = options.setupHooks;
41+
42+
options.setupHooks = (hooks) => {
43+
oldSetupHooks?.(hooks);
44+
45+
// After resolving, once, we can simply emit events the next time
46+
hooks.once('clientReady', () => {
47+
hooks.on('clientReady', () => this.events.emit(ProviderEvents.Ready));
48+
this.events.emit(ProviderEvents.Ready);
49+
resolve();
50+
});
51+
52+
hooks.on('configChanged', (projectConfig) =>
53+
this.events.emit(ProviderEvents.ConfigurationChanged, {
54+
flagsChanged: Object.keys(projectConfig.settings),
55+
})
56+
);
57+
58+
hooks.on('clientError', (message: string, error) =>
59+
this.events.emit(ProviderEvents.Error, {
60+
message: message,
61+
metadata: error,
62+
})
63+
);
64+
};
65+
66+
this.client = getClient(...originalParameters);
67+
});
2968
}
3069

31-
metadata = {
32-
name: ConfigCatProvider.name,
33-
};
70+
public get configCatClient() {
71+
return this.client;
72+
}
73+
74+
public async onClose(): Promise<void> {
75+
await this.client?.dispose();
76+
}
3477

3578
async resolveBooleanEvaluation(
3679
flagKey: string,
3780
defaultValue: boolean,
3881
context: EvaluationContext
3982
): Promise<ResolutionDetails<boolean>> {
83+
if (!this.client) {
84+
throw new GeneralError('Provider is not initialized');
85+
}
86+
4087
const { value, ...evaluationData } = await this.client.getValueDetailsAsync<SettingValue>(
4188
flagKey,
4289
undefined,
@@ -55,6 +102,10 @@ export class ConfigCatProvider implements Provider {
55102
defaultValue: string,
56103
context: EvaluationContext
57104
): Promise<ResolutionDetails<string>> {
105+
if (!this.client) {
106+
throw new GeneralError('Provider is not initialized');
107+
}
108+
58109
const { value, ...evaluationData } = await this.client.getValueDetailsAsync<SettingValue>(
59110
flagKey,
60111
undefined,
@@ -73,6 +124,10 @@ export class ConfigCatProvider implements Provider {
73124
defaultValue: number,
74125
context: EvaluationContext
75126
): Promise<ResolutionDetails<number>> {
127+
if (!this.client) {
128+
throw new GeneralError('Provider is not initialized');
129+
}
130+
76131
const { value, ...evaluationData } = await this.client.getValueDetailsAsync<SettingValue>(
77132
flagKey,
78133
undefined,
@@ -91,6 +146,10 @@ export class ConfigCatProvider implements Provider {
91146
defaultValue: U,
92147
context: EvaluationContext
93148
): Promise<ResolutionDetails<U>> {
149+
if (!this.client) {
150+
throw new GeneralError('Provider is not initialized');
151+
}
152+
94153
const { value, ...evaluationData } = await this.client.getValueDetailsAsync(
95154
flagKey,
96155
undefined,
@@ -125,7 +184,7 @@ export class ConfigCatProvider implements Provider {
125184

126185
function toResolutionDetails<U extends JsonValue>(
127186
value: U,
128-
data: Omit<configcat.IEvaluationDetails, 'value'>,
187+
data: Omit<IEvaluationDetails, 'value'>,
129188
reason?: ResolutionReason
130189
): ResolutionDetails<U> {
131190
const matchedRule = Boolean(data.matchedEvaluationRule || data.matchedEvaluationPercentageRule);

0 commit comments

Comments
 (0)