diff --git a/packages/client/test/events.spec.ts b/packages/client/test/events.spec.ts index 364febe11..c737c9858 100644 --- a/packages/client/test/events.spec.ts +++ b/packages/client/test/events.spec.ts @@ -1,6 +1,7 @@ import { EventDetails, JsonValue, + NOOP_PROVIDER, OpenFeature, OpenFeatureEventEmitter, Provider, @@ -76,6 +77,10 @@ describe('Events', () => { clientId = uuid(); }); + beforeEach(() => { + OpenFeature.setProvider(NOOP_PROVIDER); + }); + describe('Requirement 5.1.1', () => { describe('provider implements events', () => { it('The provider defines a mechanism for signalling the occurrence of an event`PROVIDER_READY`', (done) => { @@ -192,6 +197,45 @@ describe('Events', () => { OpenFeature.setProvider(clientId, provider); }); + it('anonymous provider with named client should run', (done) => { + const defaultProvider = new MockProvider({ + failOnInit: false, + initialStatus: ProviderStatus.NOT_READY, + name: 'default', + }); + const unboundName = 'some-new-unbound-name'; + + // get a client using the default because it has not other mapping + const unBoundClient = OpenFeature.getClient(unboundName); + unBoundClient.addHandler(ProviderEvents.ConfigurationChanged, () => { + done(); + }); + + // set the default provider + OpenFeature.setProvider(defaultProvider); + + // fire events + defaultProvider.events?.emit(ProviderEvents.ConfigurationChanged); + }); + + it('anonymous provider with named client should run init events', (done) => { + const defaultProvider = new MockProvider({ + failOnInit: false, + initialStatus: ProviderStatus.NOT_READY, + name: 'default', + }); + const unboundName = 'some-other-unbound-name'; + + // get a client using the default because it has not other mapping + const unBoundClient = OpenFeature.getClient(unboundName); + unBoundClient.addHandler(ProviderEvents.Ready, () => { + done(); + }); + + // set the default provider + OpenFeature.setProvider(defaultProvider); + }); + it('un-bound client event handlers still run after new provider set', (done) => { const defaultProvider = new MockProvider({ name: 'default' }); const namedProvider = new MockProvider(); diff --git a/packages/server/test/events.spec.ts b/packages/server/test/events.spec.ts index 691d6aa43..90a5c7b78 100644 --- a/packages/server/test/events.spec.ts +++ b/packages/server/test/events.spec.ts @@ -10,6 +10,7 @@ import { ResolutionDetails, } from '../src'; import { v4 as uuid } from 'uuid'; +import { NOOP_PROVIDER } from '../src'; class MockProvider implements Provider { readonly metadata: ProviderMetadata; @@ -79,6 +80,10 @@ describe('Events', () => { clientId = uuid(); }); + beforeEach(() => { + OpenFeature.setProvider(NOOP_PROVIDER); + }); + describe('Requirement 5.1.1', () => { describe('provider implements events', () => { it('The provider defines a mechanism for signalling the occurrence of an event`PROVIDER_READY`', (done) => { @@ -195,6 +200,45 @@ describe('Events', () => { OpenFeature.setProvider(clientId, provider); }); + it('anonymous provider with named client should run', (done) => { + const defaultProvider = new MockProvider({ + failOnInit: false, + initialStatus: ProviderStatus.NOT_READY, + name: 'default', + }); + const unboundName = 'some-new-unbound-name'; + + // get a client using the default because it has not other mapping + const unBoundClient = OpenFeature.getClient(unboundName); + unBoundClient.addHandler(ProviderEvents.ConfigurationChanged, () => { + done(); + }); + + // set the default provider + OpenFeature.setProvider(defaultProvider); + + // fire events + defaultProvider.events?.emit(ProviderEvents.ConfigurationChanged); + }); + + it('anonymous provider with named client should run init events', (done) => { + const defaultProvider = new MockProvider({ + failOnInit: false, + initialStatus: ProviderStatus.NOT_READY, + name: 'default', + }); + const unboundName = 'some-other-unbound-name'; + + // get a client using the default because it has not other mapping + const unBoundClient = OpenFeature.getClient(unboundName); + unBoundClient.addHandler(ProviderEvents.Ready, () => { + done(); + }); + + // set the default provider + OpenFeature.setProvider(defaultProvider); + }); + it('un-bound client event handlers still run after new provider set', (done) => { const defaultProvider = new MockProvider({ name: 'default' }); const namedProvider = new MockProvider(); diff --git a/packages/shared/src/filter.ts b/packages/shared/src/filter.ts new file mode 100644 index 000000000..66b8a995f --- /dev/null +++ b/packages/shared/src/filter.ts @@ -0,0 +1,9 @@ +/** + * Checks if a value is not null or undefined and returns it as type assertion + * @template T + * @param {T} input The value to check + * @returns If the value is not null or undefined + */ +export function isDefined(input?: T | null | undefined): input is T { + return typeof input !== 'undefined' && input !== null; +} diff --git a/packages/shared/src/open-feature.ts b/packages/shared/src/open-feature.ts index a414423c7..7bc31dce6 100644 --- a/packages/shared/src/open-feature.ts +++ b/packages/shared/src/open-feature.ts @@ -10,6 +10,7 @@ import { } from './types'; import { EventDetails, EventHandler, Eventing, OpenFeatureEventEmitter, ProviderEvents } from './events'; import { objectOrUndefined, stringOrUndefined } from './type-guards'; +import { isDefined } from './filter'; export abstract class OpenFeatureCommonAPI

implements Eventing { protected _transactionContextPropagator: TransactionContextPropagator = NOOP_TRANSACTION_CONTEXT_PROPAGATOR; @@ -104,21 +105,28 @@ export abstract class OpenFeatureCommonAPI

{ - clientEmitter.emit(ProviderEvents.Ready, { clientName }); + emitters.forEach((emitter) => { + emitter?.emit(ProviderEvents.Ready, { clientName }); + }); this._events?.emit(ProviderEvents.Ready, { clientName }); }) ?.catch((error) => { - clientEmitter.emit(ProviderEvents.Error, { clientName, message: error.message }); + emitters.forEach((emitter) => { + emitter?.emit(ProviderEvents.Error, { clientName, message: error.message }); + }); this._events?.emit(ProviderEvents.Error, { clientName, message: error.message }); }); } else { - clientEmitter.emit(ProviderEvents.Ready, { clientName }); + emitters.forEach((emitter) => { + emitter?.emit(ProviderEvents.Ready, { clientName }); + }); this._events?.emit(ProviderEvents.Ready, { clientName }); } @@ -128,7 +136,7 @@ export abstract class OpenFeatureCommonAPI

!namedProviders.includes(name)); + return unboundEmitterNames.map((name) => this._clientEvents.get(name)).filter(isDefined); + } + private transferListeners( oldProvider: P, newProvider: P, clientName: string | undefined, - clientEmitter: OpenFeatureEventEmitter + emitters: (OpenFeatureEventEmitter | undefined)[] ) { this._clientEventHandlers .get(clientName) @@ -182,7 +197,9 @@ export abstract class OpenFeatureCommonAPI

{ const handler = async (details?: EventDetails) => { // on each event type, fire the associated handlers - clientEmitter.emit(eventType, { ...details, clientName }); + emitters.forEach((emitter) => { + emitter?.emit(eventType, { ...details, clientName }); + }); this._events.emit(eventType, { ...details, clientName }); };