Skip to content

Commit fb69b9d

Browse files
fix: named client events (#472)
* fixed issues where named client wouldnt get events from a default provider Signed-off-by: Todd Baert <[email protected]> Signed-off-by: Lukas Reining <[email protected]> Co-authored-by: Lukas Reining <[email protected]>
1 parent d26c4ee commit fb69b9d

File tree

4 files changed

+121
-7
lines changed

4 files changed

+121
-7
lines changed

Diff for: packages/client/test/events.spec.ts

+44
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {
22
EventDetails,
33
JsonValue,
4+
NOOP_PROVIDER,
45
OpenFeature,
56
OpenFeatureEventEmitter,
67
Provider,
@@ -76,6 +77,10 @@ describe('Events', () => {
7677
clientId = uuid();
7778
});
7879

80+
beforeEach(() => {
81+
OpenFeature.setProvider(NOOP_PROVIDER);
82+
});
83+
7984
describe('Requirement 5.1.1', () => {
8085
describe('provider implements events', () => {
8186
it('The provider defines a mechanism for signalling the occurrence of an event`PROVIDER_READY`', (done) => {
@@ -192,6 +197,45 @@ describe('Events', () => {
192197
OpenFeature.setProvider(clientId, provider);
193198
});
194199

200+
it('anonymous provider with named client should run', (done) => {
201+
const defaultProvider = new MockProvider({
202+
failOnInit: false,
203+
initialStatus: ProviderStatus.NOT_READY,
204+
name: 'default',
205+
});
206+
const unboundName = 'some-new-unbound-name';
207+
208+
// get a client using the default because it has not other mapping
209+
const unBoundClient = OpenFeature.getClient(unboundName);
210+
unBoundClient.addHandler(ProviderEvents.ConfigurationChanged, () => {
211+
done();
212+
});
213+
214+
// set the default provider
215+
OpenFeature.setProvider(defaultProvider);
216+
217+
// fire events
218+
defaultProvider.events?.emit(ProviderEvents.ConfigurationChanged);
219+
});
220+
221+
it('anonymous provider with named client should run init events', (done) => {
222+
const defaultProvider = new MockProvider({
223+
failOnInit: false,
224+
initialStatus: ProviderStatus.NOT_READY,
225+
name: 'default',
226+
});
227+
const unboundName = 'some-other-unbound-name';
228+
229+
// get a client using the default because it has not other mapping
230+
const unBoundClient = OpenFeature.getClient(unboundName);
231+
unBoundClient.addHandler(ProviderEvents.Ready, () => {
232+
done();
233+
});
234+
235+
// set the default provider
236+
OpenFeature.setProvider(defaultProvider);
237+
});
238+
195239
it('un-bound client event handlers still run after new provider set', (done) => {
196240
const defaultProvider = new MockProvider({ name: 'default' });
197241
const namedProvider = new MockProvider();

Diff for: packages/server/test/events.spec.ts

+44
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
ResolutionDetails,
1111
} from '../src';
1212
import { v4 as uuid } from 'uuid';
13+
import { NOOP_PROVIDER } from '../src';
1314

1415
class MockProvider implements Provider {
1516
readonly metadata: ProviderMetadata;
@@ -79,6 +80,10 @@ describe('Events', () => {
7980
clientId = uuid();
8081
});
8182

83+
beforeEach(() => {
84+
OpenFeature.setProvider(NOOP_PROVIDER);
85+
});
86+
8287
describe('Requirement 5.1.1', () => {
8388
describe('provider implements events', () => {
8489
it('The provider defines a mechanism for signalling the occurrence of an event`PROVIDER_READY`', (done) => {
@@ -195,6 +200,45 @@ describe('Events', () => {
195200
OpenFeature.setProvider(clientId, provider);
196201
});
197202

203+
it('anonymous provider with named client should run', (done) => {
204+
const defaultProvider = new MockProvider({
205+
failOnInit: false,
206+
initialStatus: ProviderStatus.NOT_READY,
207+
name: 'default',
208+
});
209+
const unboundName = 'some-new-unbound-name';
210+
211+
// get a client using the default because it has not other mapping
212+
const unBoundClient = OpenFeature.getClient(unboundName);
213+
unBoundClient.addHandler(ProviderEvents.ConfigurationChanged, () => {
214+
done();
215+
});
216+
217+
// set the default provider
218+
OpenFeature.setProvider(defaultProvider);
219+
220+
// fire events
221+
defaultProvider.events?.emit(ProviderEvents.ConfigurationChanged);
222+
});
223+
224+
it('anonymous provider with named client should run init events', (done) => {
225+
const defaultProvider = new MockProvider({
226+
failOnInit: false,
227+
initialStatus: ProviderStatus.NOT_READY,
228+
name: 'default',
229+
});
230+
const unboundName = 'some-other-unbound-name';
231+
232+
// get a client using the default because it has not other mapping
233+
const unBoundClient = OpenFeature.getClient(unboundName);
234+
unBoundClient.addHandler(ProviderEvents.Ready, () => {
235+
done();
236+
});
237+
238+
// set the default provider
239+
OpenFeature.setProvider(defaultProvider);
240+
});
241+
198242
it('un-bound client event handlers still run after new provider set', (done) => {
199243
const defaultProvider = new MockProvider({ name: 'default' });
200244
const namedProvider = new MockProvider();

Diff for: packages/shared/src/filter.ts

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/**
2+
* Checks if a value is not null or undefined and returns it as type assertion
3+
* @template T
4+
* @param {T} input The value to check
5+
* @returns If the value is not null or undefined
6+
*/
7+
export function isDefined<T>(input?: T | null | undefined): input is T {
8+
return typeof input !== 'undefined' && input !== null;
9+
}

Diff for: packages/shared/src/open-feature.ts

+24-7
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
} from './types';
1111
import { EventDetails, EventHandler, Eventing, OpenFeatureEventEmitter, ProviderEvents } from './events';
1212
import { objectOrUndefined, stringOrUndefined } from './type-guards';
13+
import { isDefined } from './filter';
1314

1415
export abstract class OpenFeatureCommonAPI<P extends CommonProvider = CommonProvider> implements Eventing {
1516
protected _transactionContextPropagator: TransactionContextPropagator = NOOP_TRANSACTION_CONTEXT_PROPAGATOR;
@@ -104,21 +105,28 @@ export abstract class OpenFeatureCommonAPI<P extends CommonProvider = CommonProv
104105
return this;
105106
}
106107

107-
const clientEmitter = this.getAndCacheEventEmitterForClient(clientName);
108+
// get the named emitter, or if this is the default provider, get all event emitters not associated with a provider
109+
const emitters = clientName ? [this.getAndCacheEventEmitterForClient(clientName)] : this.getUnboundEmitters();
108110

109111
if (typeof provider.initialize === 'function') {
110112
provider
111113
.initialize?.(this._context)
112114
?.then(() => {
113-
clientEmitter.emit(ProviderEvents.Ready, { clientName });
115+
emitters.forEach((emitter) => {
116+
emitter?.emit(ProviderEvents.Ready, { clientName });
117+
});
114118
this._events?.emit(ProviderEvents.Ready, { clientName });
115119
})
116120
?.catch((error) => {
117-
clientEmitter.emit(ProviderEvents.Error, { clientName, message: error.message });
121+
emitters.forEach((emitter) => {
122+
emitter?.emit(ProviderEvents.Error, { clientName, message: error.message });
123+
});
118124
this._events?.emit(ProviderEvents.Error, { clientName, message: error.message });
119125
});
120126
} else {
121-
clientEmitter.emit(ProviderEvents.Ready, { clientName });
127+
emitters.forEach((emitter) => {
128+
emitter?.emit(ProviderEvents.Ready, { clientName });
129+
});
122130
this._events?.emit(ProviderEvents.Ready, { clientName });
123131
}
124132

@@ -128,7 +136,7 @@ export abstract class OpenFeatureCommonAPI<P extends CommonProvider = CommonProv
128136
this._defaultProvider = provider;
129137
}
130138

131-
this.transferListeners(oldProvider, provider, clientName, clientEmitter);
139+
this.transferListeners(oldProvider, provider, clientName, emitters);
132140

133141
// Do not close a provider that is bound to any client
134142
if (![...this._clientProviders.values(), this._defaultProvider].includes(oldProvider)) {
@@ -167,11 +175,18 @@ export abstract class OpenFeatureCommonAPI<P extends CommonProvider = CommonProv
167175
return newEmitter;
168176
}
169177

178+
private getUnboundEmitters(): OpenFeatureEventEmitter[] {
179+
const namedProviders = [...this._clientProviders.keys()];
180+
const eventEmitterNames = [...this._clientEvents.keys()].filter(isDefined);
181+
const unboundEmitterNames = eventEmitterNames.filter((name) => !namedProviders.includes(name));
182+
return unboundEmitterNames.map((name) => this._clientEvents.get(name)).filter(isDefined);
183+
}
184+
170185
private transferListeners(
171186
oldProvider: P,
172187
newProvider: P,
173188
clientName: string | undefined,
174-
clientEmitter: OpenFeatureEventEmitter
189+
emitters: (OpenFeatureEventEmitter | undefined)[]
175190
) {
176191
this._clientEventHandlers
177192
.get(clientName)
@@ -182,7 +197,9 @@ export abstract class OpenFeatureCommonAPI<P extends CommonProvider = CommonProv
182197
(eventType) => {
183198
const handler = async (details?: EventDetails) => {
184199
// on each event type, fire the associated handlers
185-
clientEmitter.emit(eventType, { ...details, clientName });
200+
emitters.forEach((emitter) => {
201+
emitter?.emit(eventType, { ...details, clientName });
202+
});
186203
this._events.emit(eventType, { ...details, clientName });
187204
};
188205

0 commit comments

Comments
 (0)