Skip to content

Commit f4597af

Browse files
authored
fix: some handlers fail to run (#753)
fixed an issue where some handlers would fail to run if the associated provider did not have a onContextChange method --------- Signed-off-by: Todd Baert <[email protected]>
1 parent b6adbba commit f4597af

File tree

3 files changed

+102
-39
lines changed

3 files changed

+102
-39
lines changed

packages/client/src/open-feature.ts

+35-17
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ const GLOBAL_OPENFEATURE_API_KEY = Symbol.for('@openfeature/web-sdk/api');
1717
type OpenFeatureGlobal = {
1818
[GLOBAL_OPENFEATURE_API_KEY]?: OpenFeatureAPI;
1919
};
20+
type NameProviderRecord = {
21+
name?: string;
22+
provider: Provider;
23+
}
24+
2025
const _globalThis = globalThis as OpenFeatureGlobal;
2126

2227
export class OpenFeatureAPI extends OpenFeatureCommonAPI<Provider, Hook> implements ManageContext<Promise<void>> {
@@ -81,16 +86,23 @@ export class OpenFeatureAPI extends OpenFeatureCommonAPI<Provider, Hook> impleme
8186
const oldContext = this._context;
8287
this._context = context;
8388

84-
const providersWithoutContextOverride = Array.from(this._clientProviders.entries())
89+
// collect all providers that are using the default context (not mapped to a name)
90+
const defaultContextNameProviders: NameProviderRecord[] = Array.from(this._clientProviders.entries())
8591
.filter(([name]) => !this._namedProviderContext.has(name))
86-
.reduce<Provider[]>((acc, [, provider]) => {
87-
acc.push(provider);
92+
.reduce<NameProviderRecord[]>((acc, [name, provider]) => {
93+
acc.push({ name, provider });
8894
return acc;
8995
}, []);
9096

91-
const allProviders = [this._defaultProvider, ...providersWithoutContextOverride];
97+
const allProviders: NameProviderRecord[] = [
98+
// add in the default (no name)
99+
{ name: undefined, provider: this._defaultProvider },
100+
...defaultContextNameProviders,
101+
];
92102
await Promise.all(
93-
allProviders.map((provider) => this.runProviderContextChangeHandler(undefined, provider, oldContext, context)),
103+
allProviders.map((tuple) =>
104+
this.runProviderContextChangeHandler(tuple.name, tuple.provider, oldContext, context),
105+
),
94106
);
95107
}
96108
}
@@ -196,19 +208,25 @@ export class OpenFeatureAPI extends OpenFeatureCommonAPI<Provider, Hook> impleme
196208
oldContext: EvaluationContext,
197209
newContext: EvaluationContext,
198210
): Promise<void> {
199-
const providerName = provider.metadata.name;
200-
return provider.onContextChange?.(oldContext, newContext).then(() => {
201-
this.getAssociatedEventEmitters(clientName).forEach((emitter) => {
202-
emitter?.emit(ProviderEvents.ContextChanged, { clientName, providerName });
203-
});
204-
this._events?.emit(ProviderEvents.ContextChanged, { clientName, providerName });
205-
}).catch((err) => {
206-
this._logger?.error(`Error running ${provider.metadata.name}'s context change handler:`, err);
207-
this.getAssociatedEventEmitters(clientName).forEach((emitter) => {
208-
emitter?.emit(ProviderEvents.Error, { clientName, providerName, message: err?.message, });
209-
});
210-
this._events?.emit(ProviderEvents.Error, { clientName, providerName, message: err?.message, });
211+
const providerName = provider.metadata.name;
212+
try {
213+
await provider.onContextChange?.(oldContext, newContext);
214+
215+
// only run the event handlers if the onContextChange method succeeded
216+
this.getAssociatedEventEmitters(clientName).forEach((emitter) => {
217+
emitter?.emit(ProviderEvents.ContextChanged, { clientName, providerName });
211218
});
219+
this._events?.emit(ProviderEvents.ContextChanged, { clientName, providerName });
220+
} catch (err) {
221+
// run error handlers instead
222+
const error = err as Error | undefined;
223+
const message = `Error running ${provider?.metadata?.name}'s context change handler: ${error?.message}`;
224+
this._logger?.error(`${message}`, err);
225+
this.getAssociatedEventEmitters(clientName).forEach((emitter) => {
226+
emitter?.emit(ProviderEvents.Error, { clientName, providerName, message });
227+
});
228+
this._events?.emit(ProviderEvents.Error, { clientName, providerName, message });
229+
}
212230
}
213231
}
214232

packages/client/test/events.spec.ts

+60-22
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,15 @@ class MockProvider implements Provider {
2626
private asyncDelay?: number;
2727
private enableEvents: boolean;
2828
status?: ProviderStatus = undefined;
29+
onContextChange?: () => Promise<void>;
2930

3031
constructor(options?: {
3132
hasInitialize?: boolean;
3233
initialStatus?: ProviderStatus;
3334
asyncDelay?: number;
3435
enableEvents?: boolean;
3536
failOnInit?: boolean;
37+
noContextChanged?: boolean;
3638
failOnContextChange?: boolean;
3739
name?: string;
3840
}) {
@@ -43,6 +45,9 @@ class MockProvider implements Provider {
4345
this.enableEvents = options?.enableEvents ?? true;
4446
this.failOnInit = options?.failOnInit ?? false;
4547
this.failOnContextChange = options?.failOnContextChange ?? false;
48+
if (!options?.noContextChanged) {
49+
this.onContextChange = this.changeHandler;
50+
}
4651

4752
if (this.enableEvents) {
4853
this.events = new OpenFeatureEventEmitter();
@@ -62,16 +67,6 @@ class MockProvider implements Provider {
6267

6368
initialize: jest.Mock<Promise<void>, []> | undefined;
6469

65-
async onContextChange(): Promise<void> {
66-
return new Promise((resolve, reject) => setTimeout(() => {
67-
if (this.failOnContextChange) {
68-
reject(new Error(ERR_MESSAGE));
69-
} else {
70-
resolve();
71-
}
72-
}, this.asyncDelay));
73-
}
74-
7570
resolveBooleanEvaluation(): ResolutionDetails<boolean> {
7671
throw new Error('Not implemented');
7772
}
@@ -87,6 +82,18 @@ class MockProvider implements Provider {
8782
resolveStringEvaluation(): ResolutionDetails<string> {
8883
throw new Error('Not implemented');
8984
}
85+
86+
private changeHandler() {
87+
return new Promise<void>((resolve, reject) =>
88+
setTimeout(() => {
89+
if (this.failOnContextChange) {
90+
reject(new Error(ERR_MESSAGE));
91+
} else {
92+
resolve();
93+
}
94+
}, this.asyncDelay),
95+
);
96+
}
9097
}
9198

9299
describe('Events', () => {
@@ -96,6 +103,7 @@ describe('Events', () => {
96103

97104
afterEach(async () => {
98105
await OpenFeature.clearProviders();
106+
OpenFeature.clearHandlers();
99107
jest.clearAllMocks();
100108
clientId = uuid();
101109
// hacky, but it's helpful to clear the handlers between tests
@@ -610,7 +618,6 @@ describe('Events', () => {
610618
expect(details?.clientName).toEqual(clientId);
611619
expect(details?.providerName).toEqual(provider.metadata.name);
612620
done();
613-
OpenFeature.removeHandler(ProviderEvents.ContextChanged, handler);
614621
} catch (e) {
615622
done(e);
616623
}
@@ -630,7 +637,6 @@ describe('Events', () => {
630637
expect(details?.clientName).toEqual(clientId);
631638
expect(details?.providerName).toEqual(provider.metadata.name);
632639
done();
633-
OpenFeature.removeHandler(ProviderEvents.Error, handler);
634640
} catch (e) {
635641
done(e);
636642
}
@@ -643,15 +649,25 @@ describe('Events', () => {
643649
describe('context set for different client', () => {
644650
it("If the provider's `on context changed` function terminates normally, associated `PROVIDER_CONTEXT_CHANGED` handlers MUST run.", (done) => {
645651
const provider = new MockProvider({ initialStatus: ProviderStatus.READY });
652+
let runCount = 0;
646653

647654
OpenFeature.setProvider(clientId, provider);
648655

656+
// expect 2 runs, since 2 providers are impacted by this context change (global)
649657
const handler = (details?: EventDetails) => {
650658
try {
651-
expect(details?.clientName).toBeUndefined();
652-
expect(details?.providerName).toEqual(provider.metadata.name);
653-
OpenFeature.removeHandler(ProviderEvents.ContextChanged, handler);
654-
done();
659+
runCount++;
660+
// one run should be global
661+
if (details?.clientName === undefined) {
662+
expect(details?.providerName).toEqual(OpenFeature.getProviderMetadata().name);
663+
} else if (details?.clientName === clientId) {
664+
// one run should be for client
665+
expect(details?.clientName).toEqual(clientId);
666+
expect(details?.providerName).toEqual(provider.metadata.name);
667+
}
668+
if (runCount == 2) {
669+
done();
670+
}
655671
} catch (e) {
656672
done(e);
657673
}
@@ -669,10 +685,10 @@ describe('Events', () => {
669685

670686
const handler = (details?: EventDetails) => {
671687
try {
672-
expect(details?.clientName).toBeUndefined();
688+
// expect only one error run, because only one provider throws
689+
expect(details?.clientName).toEqual(clientId);
673690
expect(details?.providerName).toEqual(provider.metadata.name);
674-
expect(details?.message).toEqual(ERR_MESSAGE);
675-
OpenFeature.removeHandler(ProviderEvents.Error, handler);
691+
expect(details?.message).toBeTruthy();
676692
done();
677693
} catch (e) {
678694
done(e);
@@ -696,7 +712,6 @@ describe('Events', () => {
696712
try {
697713
expect(details?.clientName).toEqual(clientId);
698714
expect(details?.providerName).toEqual(provider.metadata.name);
699-
OpenFeature.removeHandler(ProviderEvents.ContextChanged, handler);
700715
done();
701716
} catch (e) {
702717
done(e);
@@ -717,8 +732,7 @@ describe('Events', () => {
717732
try {
718733
expect(details?.clientName).toEqual(clientId);
719734
expect(details?.providerName).toEqual(provider.metadata.name);
720-
expect(details?.message).toEqual(ERR_MESSAGE);
721-
OpenFeature.removeHandler(ProviderEvents.Error, handler);
735+
expect(details?.message).toBeTruthy();
722736
done();
723737
} catch (e) {
724738
done(e);
@@ -730,5 +744,29 @@ describe('Events', () => {
730744

731745
});
732746
});
747+
748+
describe('provider', () => {
749+
describe('has no onContextChange handler', () => {
750+
it('runs API ContextChanged event handler', (done) => {
751+
const noChangeHandlerProvider = 'noChangeHandlerProvider';
752+
const provider = new MockProvider({ initialStatus: ProviderStatus.READY, noContextChanged: true });
753+
754+
OpenFeature.setProvider(noChangeHandlerProvider, provider);
755+
OpenFeature.setContext(noChangeHandlerProvider, {});
756+
757+
const handler = (details?: EventDetails) => {
758+
try {
759+
expect(details?.clientName).toEqual(noChangeHandlerProvider);
760+
expect(details?.providerName).toEqual(provider.metadata.name);
761+
done();
762+
} catch (e) {
763+
done(e);
764+
}
765+
};
766+
767+
OpenFeature.addHandler(ProviderEvents.ContextChanged, handler);
768+
});
769+
});
770+
});
733771
});
734772
});

packages/shared/src/open-feature.ts

+7
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,13 @@ export abstract class OpenFeatureCommonAPI<P extends CommonProvider = CommonProv
108108
this._events.removeHandler(eventType, handler);
109109
}
110110

111+
/**
112+
* Removes all event handlers.
113+
*/
114+
clearHandlers(): void {
115+
this._events.removeAllHandlers();
116+
}
117+
111118
/**
112119
* Gets the current handlers for the given provider event type.
113120
* @param {AnyProviderEvent} eventType The provider event type to get the current handlers for

0 commit comments

Comments
 (0)