Skip to content

Commit 7906bbe

Browse files
authored
feat: add PROVIDER_CONTEXT_CHANGED event (web-sdk only) (#731)
This PR: - adds `PROVIDER_CONTEXT_CHANGED` events, which, in the static paradigm, can be used to inform the SDK that the flags should be re-evaluated (important for UI repaints in React, for instance (note this event is only available in the web-sdk) - runs the associated `PROVIDER_CONTEXT_CHANGED` handlers if the provider's context handler function ran successfully or `PROVIDER_ERROR` handlers otherwise. - adds associated tests A decent amount of this is just typing magic to reduce duplicated code while making the new event only available in the web-sdk. See: [associated spec change](open-feature/spec#200) Fixes: #729 --------- Signed-off-by: Todd Baert <[email protected]>
1 parent 696bf4a commit 7906bbe

24 files changed

+413
-188
lines changed

packages/client/src/client/open-feature-client.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,14 @@ import {
1010
JsonValue,
1111
Logger,
1212
OpenFeatureError,
13-
ProviderEvents,
1413
ProviderStatus,
1514
ResolutionDetails,
1615
SafeLogger,
1716
StandardResolutionReasons,
1817
statusMatchesEvent
1918
} from '@openfeature/core';
2019
import { FlagEvaluationOptions } from '../evaluation';
20+
import { ProviderEvents } from '../events';
2121
import { InternalEventEmitter } from '../events/internal/internal-event-emitter';
2222
import { Hook } from '../hooks';
2323
import { OpenFeature } from '../open-feature';
@@ -54,7 +54,7 @@ export class OpenFeatureClient implements Client {
5454
return this.providerAccessor()?.status || ProviderStatus.READY;
5555
}
5656

57-
addHandler<T extends ProviderEvents>(eventType: T, handler: EventHandler<T>): void {
57+
addHandler(eventType: ProviderEvents, handler: EventHandler): void {
5858
this.emitterAccessor().addHandler(eventType, handler);
5959
const shouldRunNow = statusMatchesEvent(eventType, this._provider.status);
6060

@@ -68,7 +68,7 @@ export class OpenFeatureClient implements Client {
6868
}
6969
}
7070

71-
removeHandler<T extends ProviderEvents>(notificationType: T, handler: EventHandler<T>): void {
71+
removeHandler(notificationType: ProviderEvents, handler: EventHandler): void {
7272
this.emitterAccessor().removeHandler(notificationType, handler);
7373
}
7474

packages/client/src/events/events.ts

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { ClientProviderEvents } from '@openfeature/core';
2+
3+
export { ClientProviderEvents as ProviderEvents};
4+
5+
/**
6+
* A subset of events that can be directly emitted by providers.
7+
*/
8+
export type ProviderEmittableEvents = Exclude<ClientProviderEvents, ClientProviderEvents.ContextChanged>;

packages/client/src/events/index.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
export * from './open-feature-event-emitter';
1+
export * from './open-feature-event-emitter';
2+
export * from './events';
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { CommonEventDetails, GenericEventEmitter } from '@openfeature/core';
2+
import { ProviderEvents } from '../events';
23

34
/**
45
* The InternalEventEmitter is not exported publicly and should only be used within the SDK. It extends the
56
* OpenFeatureEventEmitter to include additional properties that can be included
67
* in the event details.
78
*/
8-
export abstract class InternalEventEmitter extends GenericEventEmitter<CommonEventDetails> {};
9+
export abstract class InternalEventEmitter extends GenericEventEmitter<ProviderEvents, CommonEventDetails> {};

packages/client/src/events/open-feature-event-emitter.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import { GenericEventEmitter } from '@openfeature/core';
22
import EventEmitter from 'events';
3-
3+
import { ProviderEmittableEvents } from './events';
44
/**
55
* The OpenFeatureEventEmitter can be used by provider developers to emit
66
* events at various parts of the provider lifecycle.
77
*
88
* NOTE: Ready and error events are automatically emitted by the SDK based on
99
* the result of the initialize method.
1010
*/
11-
export class OpenFeatureEventEmitter extends GenericEventEmitter {
11+
export class OpenFeatureEventEmitter extends GenericEventEmitter<ProviderEmittableEvents> {
1212
protected readonly eventEmitter = new EventEmitter({ captureRejections: true });
1313

1414
constructor() {

packages/client/src/open-feature.ts

+21-11
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
import {
22
EvaluationContext,
3+
GenericEventEmitter,
34
ManageContext,
45
OpenFeatureCommonAPI,
56
objectOrUndefined,
67
stringOrUndefined,
78
} from '@openfeature/core';
89
import { Client, OpenFeatureClient } from './client';
9-
import { NOOP_PROVIDER, Provider } from './provider';
10-
import { OpenFeatureEventEmitter } from './events';
10+
import { OpenFeatureEventEmitter, ProviderEvents } from './events';
1111
import { Hook } from './hooks';
12+
import { NOOP_PROVIDER, Provider } from './provider';
1213

1314
// use a symbol as a key for the global singleton
1415
const GLOBAL_OPENFEATURE_API_KEY = Symbol.for('@openfeature/web-sdk/api');
@@ -19,7 +20,7 @@ type OpenFeatureGlobal = {
1920
const _globalThis = globalThis as OpenFeatureGlobal;
2021

2122
export class OpenFeatureAPI extends OpenFeatureCommonAPI<Provider, Hook> implements ManageContext<Promise<void>> {
22-
protected _events = new OpenFeatureEventEmitter();
23+
protected _events: GenericEventEmitter<ProviderEvents> = new OpenFeatureEventEmitter();
2324
protected _defaultProvider: Provider = NOOP_PROVIDER;
2425
protected _createEventEmitter = () => new OpenFeatureEventEmitter();
2526
protected _namedProviderContext: Map<string, EvaluationContext> = new Map();
@@ -72,7 +73,7 @@ export class OpenFeatureAPI extends OpenFeatureCommonAPI<Provider, Hook> impleme
7273
if (provider) {
7374
const oldContext = this.getContext(clientName);
7475
this._namedProviderContext.set(clientName, context);
75-
await this.runProviderContextChangeHandler(provider, oldContext, context);
76+
await this.runProviderContextChangeHandler(clientName, provider, oldContext, context);
7677
} else {
7778
this._namedProviderContext.set(clientName, context);
7879
}
@@ -89,7 +90,7 @@ export class OpenFeatureAPI extends OpenFeatureCommonAPI<Provider, Hook> impleme
8990

9091
const allProviders = [this._defaultProvider, ...providersWithoutContextOverride];
9192
await Promise.all(
92-
allProviders.map((provider) => this.runProviderContextChangeHandler(provider, oldContext, context)),
93+
allProviders.map((provider) => this.runProviderContextChangeHandler(undefined, provider, oldContext, context)),
9394
);
9495
}
9596
}
@@ -136,7 +137,7 @@ export class OpenFeatureAPI extends OpenFeatureCommonAPI<Provider, Hook> impleme
136137
const oldContext = this.getContext(clientName);
137138
this._namedProviderContext.delete(clientName);
138139
const newContext = this.getContext();
139-
await this.runProviderContextChangeHandler(provider, oldContext, newContext);
140+
await this.runProviderContextChangeHandler(clientName, provider, oldContext, newContext);
140141
} else {
141142
this._namedProviderContext.delete(clientName);
142143
}
@@ -190,15 +191,24 @@ export class OpenFeatureAPI extends OpenFeatureCommonAPI<Provider, Hook> impleme
190191
}
191192

192193
private async runProviderContextChangeHandler(
194+
clientName: string | undefined,
193195
provider: Provider,
194196
oldContext: EvaluationContext,
195197
newContext: EvaluationContext,
196198
): Promise<void> {
197-
try {
198-
return await provider.onContextChange?.(oldContext, newContext);
199-
} catch (err) {
200-
this._logger?.error(`Error running ${provider.metadata.name}'s context change handler:`, err);
201-
}
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+
});
202212
}
203213
}
204214

packages/client/src/provider/in-memory-provider/in-memory-provider.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,13 @@ import {
66
JsonValue,
77
Logger,
88
OpenFeatureError,
9-
ProviderEvents,
109
ResolutionDetails,
1110
StandardResolutionReasons,
1211
TypeMismatchError,
1312
ProviderStatus,
1413
} from '@openfeature/core';
1514
import { Provider } from '../provider';
16-
import { OpenFeatureEventEmitter } from '../../events';
15+
import { OpenFeatureEventEmitter, ProviderEvents } from '../../events';
1716
import { FlagConfiguration, Flag } from './flag-configuration';
1817
import { VariantNotFoundError } from './variant-not-found-error';
1918

0 commit comments

Comments
 (0)