Repository: open-feature/js-sdk
Pull request #731 (2023-12-14 16:31:44)

feat: add PROVIDER_CONTEXT_CHANGED event (web-sdk only)
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](https://github.com/open-feature/spec/pull/200) 

Fixes: https://github.com/open-feature/js-sdk/issues/729

toddbaert: Great points @beeme1mr , please re-review.
toddbaert: > Please address the lint issues when you have a moment.

done

Patch:
diff --git a/packages/client/src/client/open-feature-client.ts b/packages/client/src/client/open-feature-client.ts
index f8b0a15f..84fdc79c 100644
--- a/packages/client/src/client/open-feature-client.ts
+++ b/packages/client/src/client/open-feature-client.ts
@@ -10,7 +10,6 @@ import {
   JsonValue,
   Logger,
   OpenFeatureError,
-  ProviderEvents,
   ProviderStatus,
   ResolutionDetails,
   SafeLogger,
@@ -18,6 +17,7 @@ import {
   statusMatchesEvent
 } from '@openfeature/core';
 import { FlagEvaluationOptions } from '../evaluation';
+import { ProviderEvents } from '../events';
 import { InternalEventEmitter } from '../events/internal/internal-event-emitter';
 import { Hook } from '../hooks';
 import { OpenFeature } from '../open-feature';
@@ -54,7 +54,7 @@ export class OpenFeatureClient implements Client {
     return this.providerAccessor()?.status || ProviderStatus.READY;
   }
 
-  addHandler<T extends ProviderEvents>(eventType: T, handler: EventHandler<T>): void {
+  addHandler(eventType: ProviderEvents, handler: EventHandler): void {
     this.emitterAccessor().addHandler(eventType, handler);
     const shouldRunNow = statusMatchesEvent(eventType, this._provider.status);
 
@@ -68,7 +68,7 @@ export class OpenFeatureClient implements Client {
     }
   }
 
-  removeHandler<T extends ProviderEvents>(notificationType: T, handler: EventHandler<T>): void {
+  removeHandler(notificationType: ProviderEvents, handler: EventHandler): void {
     this.emitterAccessor().removeHandler(notificationType, handler);
   }
 
diff --git a/packages/client/src/events/events.ts b/packages/client/src/events/events.ts
new file mode 100644
index 00000000..71c2d624
--- /dev/null
+++ b/packages/client/src/events/events.ts
@@ -0,0 +1,8 @@
+import { ClientProviderEvents } from '@openfeature/core';
+
+export { ClientProviderEvents as ProviderEvents};
+
+/**
+ * A subset of events that can be directly emitted by providers.
+ */
+export type ProviderEmittableEvents = Exclude<ClientProviderEvents, ClientProviderEvents.ContextChanged>;
\ No newline at end of file
diff --git a/packages/client/src/events/index.ts b/packages/client/src/events/index.ts
index 4377e48f..6093005e 100644
--- a/packages/client/src/events/index.ts
+++ b/packages/client/src/events/index.ts
@@ -1,1 +1,2 @@
-export * from './open-feature-event-emitter';
\ No newline at end of file
+export * from './open-feature-event-emitter';
+export * from './events';
\ No newline at end of file
diff --git a/packages/client/src/events/internal/internal-event-emitter.ts b/packages/client/src/events/internal/internal-event-emitter.ts
index e2ce1c5a..6d9b1daa 100644
--- a/packages/client/src/events/internal/internal-event-emitter.ts
+++ b/packages/client/src/events/internal/internal-event-emitter.ts
@@ -1,8 +1,9 @@
 import { CommonEventDetails, GenericEventEmitter } from '@openfeature/core';
+import { ProviderEvents } from '../events';
 
 /**
  * The InternalEventEmitter is not exported publicly and should only be used within the SDK. It extends the
  * OpenFeatureEventEmitter to include additional properties that can be included
  * in the event details.
  */
-export abstract class InternalEventEmitter extends GenericEventEmitter<CommonEventDetails> {};
\ No newline at end of file
+export abstract class InternalEventEmitter extends GenericEventEmitter<ProviderEvents, CommonEventDetails> {};
\ No newline at end of file
diff --git a/packages/client/src/events/open-feature-event-emitter.ts b/packages/client/src/events/open-feature-event-emitter.ts
index f9b1b08b..dbfdebd8 100644
--- a/packages/client/src/events/open-feature-event-emitter.ts
+++ b/packages/client/src/events/open-feature-event-emitter.ts
@@ -1,6 +1,6 @@
 import { GenericEventEmitter } from '@openfeature/core';
 import EventEmitter from 'events';
-
+import { ProviderEmittableEvents } from './events';
 /**
  * The OpenFeatureEventEmitter can be used by provider developers to emit
  * events at various parts of the provider lifecycle.
@@ -8,7 +8,7 @@ import EventEmitter from 'events';
  * NOTE: Ready and error events are automatically emitted by the SDK based on
  * the result of the initialize method.
  */
-export class OpenFeatureEventEmitter extends GenericEventEmitter {
+export class OpenFeatureEventEmitter extends GenericEventEmitter<ProviderEmittableEvents> {
   protected readonly eventEmitter = new EventEmitter({ captureRejections: true });
 
   constructor() {
diff --git a/packages/client/src/open-feature.ts b/packages/client/src/open-feature.ts
index 0e85046b..1aa50243 100644
--- a/packages/client/src/open-feature.ts
+++ b/packages/client/src/open-feature.ts
@@ -1,14 +1,15 @@
 import {
   EvaluationContext,
+  GenericEventEmitter,
   ManageContext,
   OpenFeatureCommonAPI,
   objectOrUndefined,
   stringOrUndefined,
 } from '@openfeature/core';
 import { Client, OpenFeatureClient } from './client';
-import { NOOP_PROVIDER, Provider } from './provider';
-import { OpenFeatureEventEmitter } from './events';
+import { OpenFeatureEventEmitter, ProviderEvents } from './events';
 import { Hook } from './hooks';
+import { NOOP_PROVIDER, Provider } from './provider';
 
 // use a symbol as a key for the global singleton
 const GLOBAL_OPENFEATURE_API_KEY = Symbol.for('@openfeature/web-sdk/api');
@@ -19,7 +20,7 @@ type OpenFeatureGlobal = {
 const _globalThis = globalThis as OpenFeatureGlobal;
 
 export class OpenFeatureAPI extends OpenFeatureCommonAPI<Provider, Hook> implements ManageContext<Promise<void>> {
-  protected _events = new OpenFeatureEventEmitter();
+  protected _events: GenericEventEmitter<ProviderEvents> = new OpenFeatureEventEmitter();
   protected _defaultProvider: Provider = NOOP_PROVIDER;
   protected _createEventEmitter = () => new OpenFeatureEventEmitter();
   protected _namedProviderContext: Map<string, EvaluationContext> = new Map();
@@ -72,7 +73,7 @@ export class OpenFeatureAPI extends OpenFeatureCommonAPI<Provider, Hook> impleme
       if (provider) {
         const oldContext = this.getContext(clientName);
         this._namedProviderContext.set(clientName, context);
-        await this.runProviderContextChangeHandler(provider, oldContext, context);
+        await this.runProviderContextChangeHandler(clientName, provider, oldContext, context);
       } else {
         this._namedProviderContext.set(clientName, context);
       }
@@ -89,7 +90,7 @@ export class OpenFeatureAPI extends OpenFeatureCommonAPI<Provider, Hook> impleme
 
       const allProviders = [this._defaultProvider, ...providersWithoutContextOverride];
       await Promise.all(
-        allProviders.map((provider) => this.runProviderContextChangeHandler(provider, oldContext, context)),
+        allProviders.map((provider) => this.runProviderContextChangeHandler(undefined, provider, oldContext, context)),
       );
     }
   }
@@ -136,7 +137,7 @@ export class OpenFeatureAPI extends OpenFeatureCommonAPI<Provider, Hook> impleme
         const oldContext = this.getContext(clientName);
         this._namedProviderContext.delete(clientName);
         const newContext = this.getContext();
-        await this.runProviderContextChangeHandler(provider, oldContext, newContext);
+        await this.runProviderContextChangeHandler(clientName, provider, oldContext, newContext);
       } else {
         this._namedProviderContext.delete(clientName);
       }
@@ -190,15 +191,24 @@ export class OpenFeatureAPI extends OpenFeatureCommonAPI<Provider, Hook> impleme
   }
 
   private async runProviderContextChangeHandler(
+    clientName: string | undefined,
     provider: Provider,
     oldContext: EvaluationContext,
     newContext: EvaluationContext,
   ): Promise<void> {
-    try {
-      return await provider.onContextChange?.(oldContext, newContext);
-    } catch (err) {
-      this._logger?.error(`Error running ${provider.metadata.name}'s context change handler:`, err);
-    }
+      const providerName = provider.metadata.name;
+      return provider.onContextChange?.(oldContext, newContext).then(() => {
+        this.getAssociatedEventEmitters(clientName).forEach((emitter) => {
+          emitter?.emit(ProviderEvents.ContextChanged, { clientName, providerName });
+        });
+        this._events?.emit(ProviderEvents.ContextChanged, { clientName, providerName });
+      }).catch((err) => {
+        this._logger?.error(`Error running ${provider.metadata.name}'s context change handler:`, err);
+        this.getAssociatedEventEmitters(clientName).forEach((emitter) => {
+          emitter?.emit(ProviderEvents.Error, { clientName, providerName, message: err?.message, });
+        });
+        this._events?.emit(ProviderEvents.Error, { clientName, providerName, message: err?.message, });
+      });
   }
 }
 
diff --git a/packages/client/src/provider/in-memory-provider/in-memory-provider.ts b/packages/client/src/provider/in-memory-provider/in-memory-provider.ts
index 46a8165f..9261830b 100644
--- a/packages/client/src/provider/in-memory-provider/in-memory-provider.ts
+++ b/packages/client/src/provider/in-memory-provider/in-memory-provider.ts
@@ -6,14 +6,13 @@ import {
   JsonValue,
   Logger,
   OpenFeatureError,
-  ProviderEvents,
   ResolutionDetails,
   StandardResolutionReasons,
   TypeMismatchError,
   ProviderStatus,
 } from '@openfeature/core';
 import { Provider } from '../provider';
-import { OpenFeatureEventEmitter } from '../../events';
+import { OpenFeatureEventEmitter, ProviderEvents } from '../../events';
 import { FlagConfiguration, Flag } from './flag-configuration';
 import { VariantNotFoundError } from './variant-not-found-error';
 
diff --git a/packages/server/src/client/open-feature-client.ts b/packages/server/src/client/open-feature-client.ts
index 35322269..dc5ad4bf 100644
--- a/packages/server/src/client/open-feature-client.ts
+++ b/packages/server/src/client/open-feature-client.ts
@@ -11,18 +11,18 @@ import {
   Logger,
   ManageContext,
   OpenFeatureError,
-  ProviderEvents,
   ResolutionDetails,
   SafeLogger,
   StandardResolutionReasons,
   statusMatchesEvent,
 } from '@openfeature/core';
 import { FlagEvaluationOptions } from '../evaluation';
+import { ProviderEvents } from '../events';
+import { InternalEventEmitter } from '../events/internal/internal-event-emitter';
+import { Hook } from '../hooks';
 import { OpenFeature } from '../open-feature';
 import { Provider } from '../provider';
 import { Client } from './client';
-import { InternalEventEmitter } from '../events/internal/internal-event-emitter';
-import { Hook } from '../hooks';
 
 type OpenFeatureClientOptions = {
   name?: string;
@@ -54,7 +54,7 @@ export class OpenFeatureClient implements Client, ManageContext<OpenFeatureClien
     };
   }
 
-  addHandler<T extends ProviderEvents>(eventType: T, handler: EventHandler<T>): void {
+  addHandler(eventType: ProviderEvents, handler: EventHandler): void {
     this.emitterAccessor().addHandler(eventType, handler);
     const shouldRunNow = statusMatchesEvent(eventType, this._provider.status);
 
@@ -68,7 +68,7 @@ export class OpenFeatureClient implements Client, ManageContext<OpenFeatureClien
     }
   }
 
-  removeHandler<T extends ProviderEvents>(eventType: T, handler: EventHandler<T>) {
+  removeHandler(eventType: ProviderEvents, handler: EventHandler) {
     this.emitterAccessor().removeHandler(eventType, handler);
   }
 
diff --git a/packages/server/src/events/events.ts b/packages/server/src/events/events.ts
new file mode 100644
index 00000000..4d7205a5
--- /dev/null
+++ b/packages/server/src/events/events.ts
@@ -0,0 +1,3 @@
+import { ServerProviderEvents } from '@openfeature/core';
+
+export { ServerProviderEvents as ProviderEvents };
\ No newline at end of file
diff --git a/packages/server/src/events/index.ts b/packages/server/src/events/index.ts
index 47d1dbc6..6093005e 100644
--- a/packages/server/src/events/index.ts
+++ b/packages/server/src/events/index.ts
@@ -1,1 +1,2 @@
 export * from './open-feature-event-emitter';
+export * from './events';
\ No newline at end of file
diff --git a/packages/server/src/events/internal/internal-event-emitter.ts b/packages/server/src/events/internal/internal-event-emitter.ts
index e2ce1c5a..663a8af0 100644
--- a/packages/server/src/events/internal/internal-event-emitter.ts
+++ b/packages/server/src/events/internal/internal-event-emitter.ts
@@ -1,8 +1,9 @@
-import { CommonEventDetails, GenericEventEmitter } from '@openfeature/core';
+import { GenericEventEmitter } from '@openfeature/core';
+import { ProviderEvents } from '../events';
 
 /**
  * The InternalEventEmitter is not exported publicly and should only be used within the SDK. It extends the
  * OpenFeatureEventEmitter to include additional properties that can be included
  * in the event details.
  */
-export abstract class InternalEventEmitter extends GenericEventEmitter<CommonEventDetails> {};
\ No newline at end of file
+export abstract class InternalEventEmitter extends GenericEventEmitter<ProviderEvents> {};
\ No newline at end of file
diff --git a/packages/server/src/events/open-feature-event-emitter.ts b/packages/server/src/events/open-feature-event-emitter.ts
index 6237bb4f..3e98cb44 100644
--- a/packages/server/src/events/open-feature-event-emitter.ts
+++ b/packages/server/src/events/open-feature-event-emitter.ts
@@ -1,5 +1,6 @@
 import { GenericEventEmitter } from '@openfeature/core';
 import EventEmitter from 'events';
+import { ProviderEvents } from './events';
 
 /**
  * The OpenFeatureEventEmitter can be used by provider developers to emit
@@ -8,7 +9,7 @@ import EventEmitter from 'events';
  * NOTE: Ready and error events are automatically emitted by the SDK based on
  * the result of the initialize method.
  */
-export class OpenFeatureEventEmitter extends GenericEventEmitter {
+export class OpenFeatureEventEmitter extends GenericEventEmitter<ProviderEvents> {
   protected readonly eventEmitter = new EventEmitter({ captureRejections: true });
 
    constructor() {
diff --git a/packages/server/src/provider/in-memory-provider/in-memory-provider.ts b/packages/server/src/provider/in-memory-provider/in-memory-provider.ts
index a52b06c2..7c76cdd8 100644
--- a/packages/server/src/provider/in-memory-provider/in-memory-provider.ts
+++ b/packages/server/src/provider/in-memory-provider/in-memory-provider.ts
@@ -6,7 +6,6 @@ import {
   JsonValue,
   Logger,
   OpenFeatureError,
-  ProviderEvents,
   ResolutionDetails,
   StandardResolutionReasons,
   TypeMismatchError
@@ -14,7 +13,7 @@ import {
 import { Provider } from '../provider';
 import { Flag, FlagConfiguration } from './flag-configuration';
 import { VariantFoundError } from './variant-not-found-error';
-import { OpenFeatureEventEmitter } from '../..';
+import { OpenFeatureEventEmitter, ProviderEvents } from '../..';
 
 /**
  * A simple OpenFeature provider intended for demos and as a test stub.
diff --git a/packages/shared/src/events/event-utils.ts b/packages/shared/src/events/event-utils.ts
index eff6cab7..d538c85d 100644
--- a/packages/shared/src/events/event-utils.ts
+++ b/packages/shared/src/events/event-utils.ts
@@ -1,20 +1,20 @@
 import { ProviderStatus } from '../provider';
-import { ProviderEvents } from './events';
+import { AllProviderEvents, AnyProviderEvent } from './events';
 
 const eventStatusMap = {
-    [ProviderStatus.READY]: ProviderEvents.Ready,
-    [ProviderStatus.ERROR]: ProviderEvents.Error,
-    [ProviderStatus.STALE]: ProviderEvents.Stale,
+    [ProviderStatus.READY]: AllProviderEvents.Ready,
+    [ProviderStatus.ERROR]: AllProviderEvents.Error,
+    [ProviderStatus.STALE]: AllProviderEvents.Stale,
     [ProviderStatus.NOT_READY]: undefined,
 };
 
 /**
  * Returns true if the provider's status corresponds to the event.
  * If the provider's status is not defined, it matches READY.
- * @param {ProviderEvents} event event to match
+ * @param {AnyProviderEvent} event event to match
  * @param {ProviderStatus} status  status of provider
  * @returns {boolean} boolean indicating if the provider status corresponds to the event.
  */
-export const statusMatchesEvent = (event: ProviderEvents, status?: ProviderStatus): boolean => {
-    return (!status && event === ProviderEvents.Ready) || eventStatusMap[status!] === event;
+export const statusMatchesEvent = <T extends AnyProviderEvent>(event: T, status?: ProviderStatus): boolean => {
+    return (!status && event === AllProviderEvents.Ready) || eventStatusMap[status!] === event;
 };
\ No newline at end of file
diff --git a/packages/shared/src/events/eventing.ts b/packages/shared/src/events/eventing.ts
index cc556c2f..8b72cae8 100644
--- a/packages/shared/src/events/eventing.ts
+++ b/packages/shared/src/events/eventing.ts
@@ -1,4 +1,4 @@
-import { ProviderEvents } from './events';
+import { AllProviderEvents, AnyProviderEvent } from './events';
 
 export type EventMetadata = {
   [key: string]: string | boolean | number;
@@ -20,40 +20,39 @@ export type StaleEvent = CommonEventProps;
 export type ConfigChangeEvent = CommonEventProps & { flagsChanged?: string[] };
 
 type EventMap = {
-  [ProviderEvents.Ready]: ReadyEvent;
-  [ProviderEvents.Error]: ErrorEvent;
-  [ProviderEvents.Stale]: StaleEvent;
-  [ProviderEvents.ConfigurationChanged]: ConfigChangeEvent;
+  [AllProviderEvents.Ready]: ReadyEvent;
+  [AllProviderEvents.Error]: ErrorEvent;
+  [AllProviderEvents.Stale]: StaleEvent;
+  [AllProviderEvents.ContextChanged]: CommonEventProps;
+  [AllProviderEvents.ConfigurationChanged]: ConfigChangeEvent;
 };
 
-export type EventContext<
-  T extends ProviderEvents,
-  U extends Record<string, unknown> = Record<string, unknown>
-> = EventMap[T] & U;
+export type EventContext<  U extends Record<string, unknown> = Record<string, unknown>
+> = EventMap[AllProviderEvents] & U;
 
-export type EventDetails<T extends ProviderEvents> = EventContext<T> & CommonEventDetails;
-export type EventHandler<T extends ProviderEvents> = (eventDetails?: EventDetails<T>) => Promise<unknown> | unknown;
+export type EventDetails = EventContext & CommonEventDetails;
+export type EventHandler = (eventDetails?: EventDetails) => Promise<unknown> | unknown;
 
 export interface Eventing {
   /**
    * Adds a handler for the given provider event type.
    * The handlers are called in the order they have been added.
-   * @param {ProviderEvents} eventType The provider event type to listen to
+   * @param {AnyProviderEvent} eventType The provider event type to listen to
    * @param {EventHandler} handler The handler to run on occurrence of the event type
    */
-  addHandler<T extends ProviderEvents>(eventType: T, handler: EventHandler<T>): void;
+  addHandler(eventType: AnyProviderEvent, handler: EventHandler): void;
 
   /**
    * Removes a handler for the given provider event type.
-   * @param {ProviderEvents} eventType The provider event type to remove the listener for
+   * @param {AnyProviderEvent} eventType The provider event type to remove the listener for
    * @param {EventHandler} handler The handler to remove for the provider event type
    */
-  removeHandler<T extends ProviderEvents>(eventType: T, handler: EventHandler<T>): void;
+  removeHandler(eventType: AnyProviderEvent, handler: EventHandler): void;
 
   /**
    * Gets the current handlers for the given provider event type.
-   * @param {ProviderEvents} eventType The provider event type to get the current handlers for
+   * @param {AnyProviderEvent} eventType The provider event type to get the current handlers for
    * @returns {EventHandler[]} The handlers currently attached to the given provider event type
    */
-  getHandlers<T extends ProviderEvents>(eventType: T): EventHandler<T>[];
+  getHandlers(eventType: AnyProviderEvent): EventHandler[];
 }
diff --git a/packages/shared/src/events/events.ts b/packages/shared/src/events/events.ts
index 5cecaa15..e6101e97 100644
--- a/packages/shared/src/events/events.ts
+++ b/packages/shared/src/events/events.ts
@@ -1,4 +1,36 @@
-export enum ProviderEvents {
+// TODO: with TypeScript 5+, we can use computed string properties,
+// so we can extract all of these into a shared set of string consts and use that in both enums
+// for now we have duplicated them.
+
+/**
+ * An enumeration of possible events for server-sdk providers.
+ */
+export enum ServerProviderEvents {
+  /**
+   * The provider is ready to evaluate flags.
+   */
+  Ready = 'PROVIDER_READY',
+
+  /**
+   * The provider is in an error state.
+   */
+  Error = 'PROVIDER_ERROR',
+
+  /**
+   * The flag configuration in the source-of-truth has changed.
+   */
+  ConfigurationChanged = 'PROVIDER_CONFIGURATION_CHANGED',
+
+  /**
+   * The provider's cached state is no longer valid and may not be up-to-date with the source of truth.
+   */
+  Stale = 'PROVIDER_STALE',
+}
+
+/**
+ * An enumeration of possible events for web-sdk providers.
+ */
+export enum ClientProviderEvents {
   /**
    * The provider is ready to evaluate flags.
    */
@@ -14,8 +46,24 @@ export enum ProviderEvents {
    */
   ConfigurationChanged = 'PROVIDER_CONFIGURATION_CHANGED',
 
+  /**
+   * The context associated with the provider has changed, and the provider has reconciled it's associated state.
+   */
+  ContextChanged = 'PROVIDER_CONTEXT_CHANGED',
+
   /**
    * The provider's cached state is no longer valid and may not be up-to-date with the source of truth.
    */
   Stale = 'PROVIDER_STALE',
 }
+
+
+/* alias because in many cases, we iterate over all possible events in code,
+so we have to do this on ClientProviderEvents to be exhaustive */
+export { ClientProviderEvents as AllProviderEvents };
+
+/**
+ * A type representing any possible ProviderEvent (server or client side).
+ * If you are implementing a hook or provider, you probably want to import `ProviderEvents` from the respective SDK.
+ */
+export type AnyProviderEvent = ServerProviderEvents | ClientProviderEvents;
diff --git a/packages/shared/src/events/generic-event-emitter.ts b/packages/shared/src/events/generic-event-emitter.ts
index 6c30b1d7..1ba01a9c 100644
--- a/packages/shared/src/events/generic-event-emitter.ts
+++ b/packages/shared/src/events/generic-event-emitter.ts
@@ -1,30 +1,31 @@
 import { Logger, ManageLogger, SafeLogger } from '../logger';
 import { EventContext, EventDetails, EventHandler } from './eventing';
-import { ProviderEvents } from './events';
+import { AnyProviderEvent } from './events';
 
 /**
  * The GenericEventEmitter should only be used within the SDK. It supports additional properties that can be included
  * in the event details.
  */
-export abstract class GenericEventEmitter<AdditionalContext extends Record<string, unknown> = Record<string, unknown>>
-  implements ManageLogger<GenericEventEmitter<AdditionalContext>>
+export abstract class GenericEventEmitter<E extends AnyProviderEvent, AdditionalContext extends Record<string, unknown> = Record<string, unknown>>
+  implements ManageLogger<GenericEventEmitter<E, AdditionalContext>>
 {
   protected abstract readonly eventEmitter: PlatformEventEmitter;
 
   // eslint-disable-next-line @typescript-eslint/no-explicit-any
-  private readonly _handlers = new WeakMap<EventHandler<any>, EventHandler<any>>();
+  private readonly _handlers = new WeakMap<EventHandler, EventHandler>();
   private _eventLogger?: Logger;
 
   constructor(private readonly globalLogger?: () => Logger) {}
 
-  emit<T extends ProviderEvents>(eventType: T, context?: EventContext<T, AdditionalContext>): void {
+  // here we use E, to restrict the events a provider can manually emit (PROVIDER_CONTEXT_CHANGED is emitted by the SDK)
+  emit(eventType: E, context?: EventContext): void {
     this.eventEmitter.emit(eventType, context);
   }
 
-  addHandler<T extends ProviderEvents>(eventType: T, handler: EventHandler<T>): void {
+  addHandler(eventType: AnyProviderEvent, handler: EventHandler): void {
     // The handlers have to be wrapped with an async function because if a synchronous functions throws an error,
     // the other handlers will not run.
-    const asyncHandler = async (details?: EventDetails<T>) => {
+    const asyncHandler = async (details?: EventDetails) => {
       await handler(details);    
     };
     // The async handler has to be written to the map, because we need to get the wrapper function when deleting a listener
@@ -32,9 +33,9 @@ export abstract class GenericEventEmitter<AdditionalContext extends Record<strin
     this.eventEmitter.on(eventType, asyncHandler);
   }
 
-  removeHandler<T extends ProviderEvents>(eventType: T, handler: EventHandler<T>): void {
+  removeHandler(eventType: AnyProviderEvent, handler: EventHandler): void {
     // Get the wrapper function for this handler, to delete it from the event emitter
-    const asyncHandler = this._handlers.get(handler) as EventHandler<T> | undefined;
+    const asyncHandler = this._handlers.get(handler) as EventHandler | undefined;
 
     if (!asyncHandler) {
       return;
@@ -43,7 +44,7 @@ export abstract class GenericEventEmitter<AdditionalContext extends Record<strin
     this.eventEmitter.removeListener(eventType, asyncHandler);
   }
 
-  removeAllHandlers(eventType?: ProviderEvents): void {
+  removeAllHandlers(eventType?: AnyProviderEvent): void {
     // If giving undefined, the listeners are not removed, so we have to check explicitly
     if (eventType) {
       this.eventEmitter.removeAllListeners(eventType);
@@ -52,8 +53,8 @@ export abstract class GenericEventEmitter<AdditionalContext extends Record<strin
     }
   }
 
-  getHandlers<T extends ProviderEvents>(eventType: T): EventHandler<T>[] {
-    return this.eventEmitter.listeners(eventType) as EventHandler<T>[];
+  getHandlers(eventType: AnyProviderEvent): EventHandler[] {
+    return this.eventEmitter.listeners(eventType) as EventHandler[];
   }
 
   setLogger(logger: Logger): this {
diff --git a/packages/shared/src/open-feature.ts b/packages/shared/src/open-feature.ts
index 313e31fa..45715434 100644
--- a/packages/shared/src/open-feature.ts
+++ b/packages/shared/src/open-feature.ts
@@ -1,9 +1,16 @@
 import { GeneralError } from './errors';
 import { EvaluationContext } from './evaluation';
-import { EventDetails, EventHandler, Eventing, ProviderEvents, statusMatchesEvent } from './events';
-import { GenericEventEmitter } from './events';
+import {
+  AllProviderEvents,
+  AnyProviderEvent,
+  EventDetails,
+  EventHandler,
+  Eventing,
+  GenericEventEmitter,
+  statusMatchesEvent,
+} from './events';
 import { isDefined } from './filter';
-import { EvaluationLifeCycle, BaseHook } from './hooks';
+import { BaseHook, EvaluationLifeCycle } from './hooks';
 import { DefaultLogger, Logger, ManageLogger, SafeLogger } from './logger';
 import { CommonProvider, ProviderMetadata, ProviderStatus } from './provider';
 import { objectOrUndefined, stringOrUndefined } from './type-guards';
@@ -12,18 +19,17 @@ import { Paradigm } from './types';
 export abstract class OpenFeatureCommonAPI<P extends CommonProvider = CommonProvider, H extends BaseHook = BaseHook>
   implements Eventing, EvaluationLifeCycle<OpenFeatureCommonAPI<P>>, ManageLogger<OpenFeatureCommonAPI<P>>
 {
-  protected abstract _createEventEmitter(): GenericEventEmitter;
+  protected abstract _createEventEmitter(): GenericEventEmitter<AnyProviderEvent>;
   protected abstract _defaultProvider: P;
-  protected abstract readonly _events: GenericEventEmitter;
+  protected abstract readonly _events: GenericEventEmitter<AnyProviderEvent>;
 
   protected _hooks: H[] = [];
   protected _context: EvaluationContext = {};
   protected _logger: Logger = new DefaultLogger();
 
-  private readonly _clientEventHandlers: Map<string | undefined, [ProviderEvents, EventHandler<ProviderEvents>][]> =
-    new Map();
+  private readonly _clientEventHandlers: Map<string | undefined, [AnyProviderEvent, EventHandler][]> = new Map();
   protected _clientProviders: Map<string, P> = new Map();
-  protected _clientEvents: Map<string | undefined, GenericEventEmitter> = new Map();
+  protected _clientEvents: Map<string | undefined, GenericEventEmitter<AnyProviderEvent>> = new Map();
   protected _runsOn: Paradigm;
 
   constructor(category: Paradigm) {
@@ -71,10 +77,10 @@ export abstract class OpenFeatureCommonAPI<P extends CommonProvider = CommonProv
    * Adds a handler for the given provider event type.
    * The handlers are called in the order they have been added.
    * API (global) events run for all providers.
-   * @param {ProviderEvents} eventType The provider event type to listen to
+   * @param {AnyProviderEvent} eventType The provider event type to listen to
    * @param {EventHandler} handler The handler to run on occurrence of the event type
    */
-  addHandler<T extends ProviderEvents>(eventType: T, handler: EventHandler<T>): void {
+  addHandler<T extends AnyProviderEvent>(eventType: T, handler: EventHandler): void {
     [...new Map([[undefined, this._defaultProvider]]), ...this._clientProviders].forEach((keyProviderTuple) => {
       const clientName = keyProviderTuple[0];
       const provider = keyProviderTuple[1];
@@ -95,19 +101,19 @@ export abstract class OpenFeatureCommonAPI<P extends CommonProvider = CommonProv
 
   /**
    * Removes a handler for the given provider event type.
-   * @param {ProviderEvents} eventType The provider event type to remove the listener for
+   * @param {AnyProviderEvent} eventType The provider event type to remove the listener for
    * @param {EventHandler} handler The handler to remove for the provider event type
    */
-  removeHandler<T extends ProviderEvents>(eventType: T, handler: EventHandler<T>): void {
+  removeHandler<T extends AnyProviderEvent>(eventType: T, handler: EventHandler): void {
     this._events.removeHandler(eventType, handler);
   }
 
   /**
    * Gets the current handlers for the given provider event type.
-   * @param {ProviderEvents} eventType The provider event type to get the current handlers for
+   * @param {AnyProviderEvent} eventType The provider event type to get the current handlers for
    * @returns {EventHandler[]} The handlers currently attached to the given provider event type
    */
-  getHandlers<T extends ProviderEvents>(eventType: T): EventHandler<T>[] {
+  getHandlers<T extends AnyProviderEvent>(eventType: T): EventHandler[] {
     return this._events.getHandlers(eventType);
   }
 
@@ -206,23 +212,23 @@ export abstract class OpenFeatureCommonAPI<P extends CommonProvider = CommonProv
         ?.then(() => {
           // fetch the most recent event emitters, some may have been added during init
           this.getAssociatedEventEmitters(clientName).forEach((emitter) => {
-            emitter?.emit(ProviderEvents.Ready, { clientName, providerName });
+            emitter?.emit(AllProviderEvents.Ready, { clientName, providerName });
           });
-          this._events?.emit(ProviderEvents.Ready, { clientName, providerName });
+          this._events?.emit(AllProviderEvents.Ready, { clientName, providerName });
         })
         ?.catch((error) => {
           this.getAssociatedEventEmitters(clientName).forEach((emitter) => {
-            emitter?.emit(ProviderEvents.Error, { clientName, providerName, message: error.message });
+            emitter?.emit(AllProviderEvents.Error, { clientName, providerName, message: error?.message });
           });
-          this._events?.emit(ProviderEvents.Error, { clientName, providerName, message: error.message });
+          this._events?.emit(AllProviderEvents.Error, { clientName, providerName, message: error?.message });
           // rethrow after emitting error events, so that public methods can control error handling
           throw error;
         });
     } else {
       emitters.forEach((emitter) => {
-        emitter?.emit(ProviderEvents.Ready, { clientName, providerName });
+        emitter?.emit(AllProviderEvents.Ready, { clientName, providerName });
       });
-      this._events?.emit(ProviderEvents.Ready, { clientName, providerName });
+      this._events?.emit(AllProviderEvents.Ready, { clientName, providerName });
     }
 
     if (clientName) {
@@ -249,7 +255,7 @@ export abstract class OpenFeatureCommonAPI<P extends CommonProvider = CommonProv
     return this._clientProviders.get(name) ?? this._defaultProvider;
   }
 
-  protected buildAndCacheEventEmitterForClient(name?: string): GenericEventEmitter {
+  protected buildAndCacheEventEmitterForClient(name?: string): GenericEventEmitter<AnyProviderEvent> {
     const emitter = this._clientEvents.get(name);
 
     if (emitter) {
@@ -261,7 +267,7 @@ export abstract class OpenFeatureCommonAPI<P extends CommonProvider = CommonProv
     this._clientEvents.set(name, newEmitter);
 
     const clientProvider = this.getProviderForClient(name);
-    Object.values<ProviderEvents>(ProviderEvents).forEach(
+    Object.values<AllProviderEvents>(AllProviderEvents).forEach(
       (eventType) =>
         clientProvider.events?.addHandler(eventType, async (details) => {
           newEmitter.emit(eventType, { ...details, clientName: name, providerName: clientProvider.metadata.name });
@@ -271,7 +277,7 @@ export abstract class OpenFeatureCommonAPI<P extends CommonProvider = CommonProv
     return newEmitter;
   }
 
-  private getUnboundEmitters(): GenericEventEmitter[] {
+  private getUnboundEmitters(): GenericEventEmitter<AnyProviderEvent>[] {
     const namedProviders = [...this._clientProviders.keys()];
     const eventEmitterNames = [...this._clientEvents.keys()].filter(isDefined);
     const unboundEmitterNames = eventEmitterNames.filter((name) => !namedProviders.includes(name));
@@ -283,7 +289,7 @@ export abstract class OpenFeatureCommonAPI<P extends CommonProvider = CommonProv
     ].filter(isDefined);
   }
 
-  private getAssociatedEventEmitters(clientName: string | undefined) {
+  protected getAssociatedEventEmitters(clientName: string | undefined) {
     return clientName ? [this.buildAndCacheEventEmitterForClient(clientName)] : this.getUnboundEmitters();
   }
 
@@ -291,17 +297,15 @@ export abstract class OpenFeatureCommonAPI<P extends CommonProvider = CommonProv
     oldProvider: P,
     newProvider: P,
     clientName: string | undefined,
-    emitters: (GenericEventEmitter | undefined)[],
+    emitters: (GenericEventEmitter<AnyProviderEvent> | undefined)[],
   ) {
     this._clientEventHandlers
       .get(clientName)
       ?.forEach((eventHandler) => oldProvider.events?.removeHandler(...eventHandler));
 
     // iterate over the event types
-    const newClientHandlers = Object.values<ProviderEvents>(ProviderEvents).map<
-      [ProviderEvents, EventHandler<ProviderEvents>]
-    >((eventType) => {
-      const handler = async (details?: EventDetails<ProviderEvents>) => {
+    const newClientHandlers = Object.values(AllProviderEvents).map<[AllProviderEvents, EventHandler]>((eventType) => {
+      const handler = async (details?: EventDetails) => {
         // on each event type, fire the associated handlers
         emitters.forEach((emitter) => {
           emitter?.emit(eventType, { ...details, clientName, providerName: newProvider.metadata.name });
diff --git a/packages/shared/src/provider/provider.ts b/packages/shared/src/provider/provider.ts
index 84ef8d48..61bf19ad 100644
--- a/packages/shared/src/provider/provider.ts
+++ b/packages/shared/src/provider/provider.ts
@@ -1,5 +1,5 @@
 import { EvaluationContext } from '../evaluation';
-import { GenericEventEmitter } from '../events';
+import { AnyProviderEvent, GenericEventEmitter } from '../events';
 import { Metadata, Paradigm } from '../types';
 
 /**
@@ -57,7 +57,7 @@ export interface CommonProvider {
    * An event emitter for ProviderEvents.
    * @see ProviderEvents
    */
-  events?: GenericEventEmitter;
+  events?: GenericEventEmitter<AnyProviderEvent>;
 
   /**
    * A function used to shut down the provider.


Test patch:
diff --git a/packages/client/test/events.spec.ts b/packages/client/test/events.spec.ts
index a46a0b5c..6b7ff0bd 100644
--- a/packages/client/test/events.spec.ts
+++ b/packages/client/test/events.spec.ts
@@ -1,3 +1,4 @@
+import { EventDetails } from '@openfeature/core';
 import { v4 as uuid } from 'uuid';
 import {
   JsonValue,
@@ -13,6 +14,7 @@ import {
 } from '../src';
 
 const TIMEOUT = 1000;
+const ERR_MESSAGE = 'fake err';
 
 class MockProvider implements Provider {
   readonly metadata: ProviderMetadata;
@@ -20,24 +22,27 @@ class MockProvider implements Provider {
   readonly runsOn = 'client';
   private hasInitialize: boolean;
   private failOnInit: boolean;
-  private initDelay?: number;
+  private failOnContextChange: boolean;
+  private asyncDelay?: number;
   private enableEvents: boolean;
   status?: ProviderStatus = undefined;
 
   constructor(options?: {
     hasInitialize?: boolean;
     initialStatus?: ProviderStatus;
-    initDelay?: number;
+    asyncDelay?: number;
     enableEvents?: boolean;
     failOnInit?: boolean;
+    failOnContextChange?: boolean;
     name?: string;
   }) {
     this.metadata = { name: options?.name ?? 'mock-provider' };
     this.hasInitialize = options?.hasInitialize ?? true;
     this.status = options?.initialStatus ?? ProviderStatus.NOT_READY;
-    this.initDelay = options?.initDelay ?? 0;
+    this.asyncDelay = options?.asyncDelay ?? 0;
     this.enableEvents = options?.enableEvents ?? true;
     this.failOnInit = options?.failOnInit ?? false;
+    this.failOnContextChange = options?.failOnContextChange ?? false;
 
     if (this.enableEvents) {
       this.events = new OpenFeatureEventEmitter();
@@ -45,9 +50,9 @@ class MockProvider implements Provider {
 
     if (this.hasInitialize) {
       this.initialize = jest.fn(async () => {
-        await new Promise((resolve) => setTimeout(resolve, this.initDelay));
+        await new Promise((resolve) => setTimeout(resolve, this.asyncDelay));
         if (this.failOnInit) {
-          throw new Error('Provider initialization failed');
+          throw new Error(ERR_MESSAGE);
         }
 
         this.status = ProviderStatus.READY;
@@ -57,6 +62,16 @@ class MockProvider implements Provider {
 
   initialize: jest.Mock<Promise<void>, []> | undefined;
 
+  async onContextChange(): Promise<void> {
+      return new Promise((resolve, reject) => setTimeout(() => {
+        if (this.failOnContextChange) {
+          reject(new Error(ERR_MESSAGE));
+        } else {
+          resolve();
+        }
+      }, this.asyncDelay));
+  }
+
   resolveBooleanEvaluation(): ResolutionDetails<boolean> {
     throw new Error('Not implemented');
   }
@@ -312,7 +327,7 @@ describe('Events', () => {
       const provider = new MockProvider({
         name: 'race',
         initialStatus: ProviderStatus.NOT_READY,
-        initDelay: TIMEOUT / 2,
+        asyncDelay: TIMEOUT / 2,
       });
 
       // set the default provider
@@ -507,10 +522,10 @@ describe('Events', () => {
       describe('Handlers attached after the provider is already in the associated state, MUST run immediately.', () => {
         it('Ready', (done) => {
           const provider = new MockProvider({ initialStatus: ProviderStatus.READY });
-  
+
           OpenFeature.setProvider(clientId, provider);
           expect(provider.initialize).not.toHaveBeenCalled();
-  
+
           OpenFeature.addHandler(ProviderEvents.Ready, () => {
             done();
           });
@@ -518,10 +533,10 @@ describe('Events', () => {
 
         it('Error', (done) => {
           const provider = new MockProvider({ initialStatus: ProviderStatus.ERROR });
-  
+
           OpenFeature.setProvider(clientId, provider);
           expect(provider.initialize).not.toHaveBeenCalled();
-  
+
           OpenFeature.addHandler(ProviderEvents.Error, () => {
             done();
           });
@@ -529,10 +544,10 @@ describe('Events', () => {
 
         it('Stale', (done) => {
           const provider = new MockProvider({ initialStatus: ProviderStatus.STALE });
-  
+
           OpenFeature.setProvider(clientId, provider);
           expect(provider.initialize).not.toHaveBeenCalled();
-  
+
           OpenFeature.addHandler(ProviderEvents.Stale, () => {
             done();
           });
@@ -545,10 +560,10 @@ describe('Events', () => {
         it('Ready', (done) => {
           const provider = new MockProvider({ initialStatus: ProviderStatus.READY });
           const client = OpenFeature.getClient(clientId);
-  
+
           OpenFeature.setProvider(clientId, provider);
           expect(provider.initialize).not.toHaveBeenCalled();
-  
+
           client.addHandler(ProviderEvents.Ready, () => {
             done();
           });
@@ -557,10 +572,10 @@ describe('Events', () => {
         it('Error', (done) => {
           const provider = new MockProvider({ initialStatus: ProviderStatus.ERROR });
           const client = OpenFeature.getClient(clientId);
-  
+
           OpenFeature.setProvider(clientId, provider);
           expect(provider.initialize).not.toHaveBeenCalled();
-  
+
           client.addHandler(ProviderEvents.Error, () => {
             done();
           });
@@ -569,10 +584,10 @@ describe('Events', () => {
         it('Stale', (done) => {
           const provider = new MockProvider({ initialStatus: ProviderStatus.STALE });
           const client = OpenFeature.getClient(clientId);
-  
+
           OpenFeature.setProvider(clientId, provider);
           expect(provider.initialize).not.toHaveBeenCalled();
-  
+
           client.addHandler(ProviderEvents.Stale, () => {
             done();
           });
@@ -580,4 +595,140 @@ describe('Events', () => {
       });
     });
   });
+
+  describe('Requirement 5.3.4.2, 5.3.4.3', () => {
+    describe('API', () => {
+      describe('context set for same client', () => {
+        it("If the provider's `on context changed` function terminates normally, associated `PROVIDER_CONTEXT_CHANGED` handlers MUST run.", (done) => {
+          const provider = new MockProvider({ initialStatus: ProviderStatus.READY });
+
+          OpenFeature.setProvider(clientId, provider);
+          OpenFeature.setContext(clientId, {});
+
+          const handler = (details?: EventDetails) => {
+            try {
+              expect(details?.clientName).toEqual(clientId);
+              expect(details?.providerName).toEqual(provider.metadata.name);
+              done();
+              OpenFeature.removeHandler(ProviderEvents.ContextChanged, handler);
+            } catch (e) {
+              done(e);
+            }
+          };
+
+          OpenFeature.addHandler(ProviderEvents.ContextChanged, handler);
+        });
+
+        it("If the provider's `on context changed` function terminates abnormally, associated `PROVIDER_ERROR` handlers MUST run.", (done) => {
+          const provider = new MockProvider({ initialStatus: ProviderStatus.READY, failOnContextChange: true });
+
+          OpenFeature.setProvider(clientId, provider);
+          OpenFeature.setContext(clientId, {});
+
+          const handler = (details?: EventDetails) => {
+            try {
+              expect(details?.clientName).toEqual(clientId);
+              expect(details?.providerName).toEqual(provider.metadata.name);
+              done();
+              OpenFeature.removeHandler(ProviderEvents.Error, handler);
+            } catch (e) {
+              done(e);
+            }
+          };
+
+          OpenFeature.addHandler(ProviderEvents.Error, handler);
+        });
+      });
+
+      describe('context set for different client', () => {
+        it("If the provider's `on context changed` function terminates normally, associated `PROVIDER_CONTEXT_CHANGED` handlers MUST run.", (done) => {
+          const provider = new MockProvider({ initialStatus: ProviderStatus.READY });
+
+          OpenFeature.setProvider(clientId, provider);
+
+          const handler = (details?: EventDetails) => {
+            try {
+              expect(details?.clientName).toBeUndefined();
+              expect(details?.providerName).toEqual(provider.metadata.name);
+              OpenFeature.removeHandler(ProviderEvents.ContextChanged, handler);
+              done();
+            } catch (e) {
+              done(e);
+            }
+          };
+
+          OpenFeature.addHandler(ProviderEvents.ContextChanged, handler);
+          OpenFeature.setContext({});
+
+        });
+
+        it("If the provider's `on context changed` function terminates abnormally, associated `PROVIDER_ERROR` handlers MUST run.", (done) => {
+          const provider = new MockProvider({ initialStatus: ProviderStatus.READY, failOnContextChange: true });
+
+          OpenFeature.setProvider(clientId, provider);
+
+          const handler = (details?: EventDetails) => {
+            try {
+              expect(details?.clientName).toBeUndefined();
+              expect(details?.providerName).toEqual(provider.metadata.name);
+              expect(details?.message).toEqual(ERR_MESSAGE);
+              OpenFeature.removeHandler(ProviderEvents.Error, handler);
+              done();
+            } catch (e) {
+              done(e);
+            }
+          };
+
+          OpenFeature.addHandler(ProviderEvents.Error, handler);
+          OpenFeature.setContext({});
+        });
+      });
+    });
+
+    describe('client', () => {
+      it("If the provider's `on context changed` function terminates normally, associated `PROVIDER_CONTEXT_CHANGED` handlers MUST run.", (done) => {
+        const provider = new MockProvider({ initialStatus: ProviderStatus.READY });
+        const client = OpenFeature.getClient(clientId);
+
+        OpenFeature.setProvider(clientId, provider);
+
+        const handler = (details?: EventDetails) => {
+          try {
+            expect(details?.clientName).toEqual(clientId);
+            expect(details?.providerName).toEqual(provider.metadata.name);
+            OpenFeature.removeHandler(ProviderEvents.ContextChanged, handler);
+            done();
+          } catch (e) {
+            done(e);
+          }
+        };
+
+        client.addHandler(ProviderEvents.ContextChanged, handler);
+        OpenFeature.setContext(clientId, {});
+      });
+
+      it("If the provider's `on context changed` function terminates abnormally, associated `PROVIDER_ERROR` handlers MUST run.", (done) => {
+        const provider = new MockProvider({ initialStatus: ProviderStatus.READY, failOnContextChange: true });
+        const client = OpenFeature.getClient(clientId);
+
+        OpenFeature.setProvider(clientId, provider);
+
+        const handler = (details?: EventDetails) => {
+          try {
+            expect(details?.clientName).toEqual(clientId);
+            expect(details?.providerName).toEqual(provider.metadata.name);
+            expect(details?.message).toEqual(ERR_MESSAGE);
+            OpenFeature.removeHandler(ProviderEvents.Error, handler);
+            done();
+          } catch (e) {
+            done(e);
+          }
+        };
+
+        client.addHandler(ProviderEvents.Error, handler);
+        OpenFeature.setContext(clientId, {});
+
+      });
+    });
+  });
 });
diff --git a/packages/client/test/in-memory-provider.spec.ts b/packages/client/test/in-memory-provider.spec.ts
index 23854cbc..c1481681 100644
--- a/packages/client/test/in-memory-provider.spec.ts
+++ b/packages/client/test/in-memory-provider.spec.ts
@@ -1,5 +1,4 @@
-import { FlagNotFoundError, GeneralError, ProviderEvents, ProviderStatus, StandardResolutionReasons, TypeMismatchError } from '@openfeature/core';
-import { InMemoryProvider } from '../src';
+import { FlagNotFoundError, GeneralError, InMemoryProvider, ProviderEvents, ProviderStatus, StandardResolutionReasons, TypeMismatchError } from '../src';
 import { FlagConfiguration } from '../src/provider/in-memory-provider/flag-configuration';
 import { VariantNotFoundError } from '../src/provider/in-memory-provider/variant-not-found-error';
 
diff --git a/packages/server/e2e/step-definitions/evaluation.spec.ts b/packages/server/e2e/step-definitions/evaluation.spec.ts
index 957bd8fa..33e349a2 100644
--- a/packages/server/e2e/step-definitions/evaluation.spec.ts
+++ b/packages/server/e2e/step-definitions/evaluation.spec.ts
@@ -1,15 +1,15 @@
-import { defineFeature, loadFeature } from 'jest-cucumber';
 import {
-  JsonValue,
-  JsonObject,
-  EvaluationDetails,
   EvaluationContext,
+  EvaluationDetails,
+  JsonObject,
+  JsonValue,
   ResolutionDetails,
   StandardResolutionReasons,
-  ProviderEvents,
 } from '@openfeature/core';
-import { OpenFeature, InMemoryProvider } from '../../src';
+import { defineFeature, loadFeature } from 'jest-cucumber';
+import { InMemoryProvider, OpenFeature, ProviderEvents } from '../../src';
 import flagConfiguration from './flags-config';
+
 // load the feature file.
 const feature = loadFeature('packages/server/e2e/features/evaluation.feature');
 
@@ -17,7 +17,7 @@ const feature = loadFeature('packages/server/e2e/features/evaluation.feature');
 const client = OpenFeature.getClient();
 
 const givenAnOpenfeatureClientIsRegisteredWithCacheDisabled = (
-  given: (stepMatcher: string, stepDefinitionCallback: () => void) => void
+  given: (stepMatcher: string, stepDefinitionCallback: () => void) => void,
 ) => {
   OpenFeature.setProvider(
     new InMemoryProvider(flagConfiguration),
@@ -28,7 +28,7 @@ const givenAnOpenfeatureClientIsRegisteredWithCacheDisabled = (
 defineFeature(feature, (test) => {
   beforeAll((done) => {
     client.addHandler(ProviderEvents.Ready, async () => {
-        done();
+      done();
     });
   });
 
@@ -47,7 +47,7 @@ defineFeature(feature, (test) => {
       async (key: string, defaultValue: string) => {
         flagKey = key;
         value = await client.getBooleanValue(flagKey, defaultValue === 'true');
-      }
+      },
     );
 
     then(/^the resolved boolean value should be "(.*)"$/, (expectedValue: string) => {
@@ -66,7 +66,7 @@ defineFeature(feature, (test) => {
       async (key: string, defaultValue: string) => {
         flagKey = key;
         value = await client.getStringValue(flagKey, defaultValue);
-      }
+      },
     );
 
     then(/^the resolved string value should be "(.*)"$/, (expectedValue: string) => {
@@ -85,7 +85,7 @@ defineFeature(feature, (test) => {
       async (key: string, defaultValue: string) => {
         flagKey = key;
         value = await client.getNumberValue(flagKey, Number.parseInt(defaultValue));
-      }
+      },
     );
 
     then(/^the resolved integer value should be (\d+)$/, (expectedValue: string) => {
@@ -104,7 +104,7 @@ defineFeature(feature, (test) => {
       async (key: string, defaultValue: string) => {
         flagKey = key;
         value = await client.getNumberValue(flagKey, Number.parseFloat(defaultValue));
-      }
+      },
     );
 
     then(/^the resolved float value should be (\d+\.?\d*)$/, (expectedValue: string) => {
@@ -130,7 +130,7 @@ defineFeature(feature, (test) => {
         expect(jsonObject[field1]).toEqual(boolValue === 'true');
         expect(jsonObject[field2]).toEqual(stringValue);
         expect(jsonObject[field3]).toEqual(Number.parseInt(intValue));
-      }
+      },
     );
   });
 
@@ -145,7 +145,7 @@ defineFeature(feature, (test) => {
       async (key: string, defaultValue: string) => {
         flagKey = key;
         details = await client.getBooleanDetails(flagKey, defaultValue === 'true');
-      }
+      },
     );
 
     then(
@@ -154,7 +154,7 @@ defineFeature(feature, (test) => {
         expect(details.value).toEqual(expectedValue === 'true');
         expect(details.variant).toEqual(expectedVariant);
         expect(details.reason).toEqual(expectedReason);
-      }
+      },
     );
   });
 
@@ -169,7 +169,7 @@ defineFeature(feature, (test) => {
       async (key: string, defaultValue: string) => {
         flagKey = key;
         details = await client.getStringDetails(flagKey, defaultValue);
-      }
+      },
     );
 
     then(
@@ -178,7 +178,7 @@ defineFeature(feature, (test) => {
         expect(details.value).toEqual(expectedValue);
         expect(details.variant).toEqual(expectedVariant);
         expect(details.reason).toEqual(expectedReason);
-      }
+      },
     );
   });
 
@@ -193,7 +193,7 @@ defineFeature(feature, (test) => {
       async (key: string, defaultValue: string) => {
         flagKey = key;
         details = await client.getNumberDetails(flagKey, Number.parseInt(defaultValue));
-      }
+      },
     );
 
     then(
@@ -202,7 +202,7 @@ defineFeature(feature, (test) => {
         expect(details.value).toEqual(Number.parseInt(expectedValue));
         expect(details.variant).toEqual(expectedVariant);
         expect(details.reason).toEqual(expectedReason);
-      }
+      },
     );
   });
 
@@ -217,7 +217,7 @@ defineFeature(feature, (test) => {
       async (key: string, defaultValue: string) => {
         flagKey = key;
         details = await client.getNumberDetails(flagKey, Number.parseFloat(defaultValue));
-      }
+      },
     );
 
     then(
@@ -226,7 +226,7 @@ defineFeature(feature, (test) => {
         expect(details.value).toEqual(Number.parseFloat(expectedValue));
         expect(details.variant).toEqual(expectedVariant);
         expect(details.reason).toEqual(expectedReason);
-      }
+      },
     );
   });
 
@@ -249,7 +249,7 @@ defineFeature(feature, (test) => {
         expect(jsonObject[field1]).toEqual(boolValue === 'true');
         expect(jsonObject[field2]).toEqual(stringValue);
         expect(jsonObject[field3]).toEqual(Number.parseInt(intValue));
-      }
+      },
     );
 
     and(
@@ -257,7 +257,7 @@ defineFeature(feature, (test) => {
       (expectedVariant: string, expectedReason: string) => {
         expect(details.variant).toEqual(expectedVariant);
         expect(details.reason).toEqual(expectedReason);
-      }
+      },
     );
   });
 
@@ -278,13 +278,13 @@ defineFeature(feature, (test) => {
         stringValue1: string,
         stringValue2: string,
         intValue: string,
-        boolValue: string
+        boolValue: string,
       ) => {
         context[stringField1] = stringValue1;
         context[stringField2] = stringValue2;
         context[intField] = Number.parseInt(intValue);
         context[boolField] = boolValue === 'true';
-      }
+      },
     );
 
     and(
@@ -292,7 +292,7 @@ defineFeature(feature, (test) => {
       async (key: string, defaultValue: string) => {
         flagKey = key;
         value = await client.getStringValue(flagKey, defaultValue, context);
-      }
+      },
     );
 
     then(/^the resolved string response should be "(.*)"$/, (expectedValue: string) => {
@@ -318,7 +318,7 @@ defineFeature(feature, (test) => {
         flagKey = key;
         fallbackValue = defaultValue;
         details = await client.getStringDetails(flagKey, defaultValue);
-      }
+      },
     );
 
     then(/^the default string value should be returned$/, () => {
@@ -330,7 +330,7 @@ defineFeature(feature, (test) => {
       (errorCode: string) => {
         expect(details.reason).toEqual(StandardResolutionReasons.ERROR);
         expect(details.errorCode).toEqual(errorCode);
-      }
+      },
     );
   });
 
@@ -347,7 +347,7 @@ defineFeature(feature, (test) => {
         flagKey = key;
         fallbackValue = Number.parseInt(defaultValue);
         details = await client.getNumberDetails(flagKey, Number.parseInt(defaultValue));
-      }
+      },
     );
 
     then(/^the default integer value should be returned$/, () => {
@@ -359,7 +359,7 @@ defineFeature(feature, (test) => {
       (errorCode: string) => {
         expect(details.reason).toEqual(StandardResolutionReasons.ERROR);
         expect(details.errorCode).toEqual(errorCode);
-      }
+      },
     );
   });
 });
diff --git a/packages/server/test/in-memory-provider.spec.ts b/packages/server/test/in-memory-provider.spec.ts
index 46c9d96c..8266e08e 100644
--- a/packages/server/test/in-memory-provider.spec.ts
+++ b/packages/server/test/in-memory-provider.spec.ts
@@ -1,5 +1,4 @@
-import { FlagNotFoundError, ProviderEvents, StandardResolutionReasons, TypeMismatchError } from '@openfeature/core';
-import { InMemoryProvider } from '../src';
+import { FlagNotFoundError, InMemoryProvider, ProviderEvents, StandardResolutionReasons, TypeMismatchError } from '../src';
 import { VariantFoundError } from '../src/provider/in-memory-provider/variant-not-found-error';
 
 describe(InMemoryProvider, () => {
diff --git a/packages/shared/test/events.spec.ts b/packages/shared/test/events.spec.ts
index 9742f786..ff4120ae 100644
--- a/packages/shared/test/events.spec.ts
+++ b/packages/shared/test/events.spec.ts
@@ -1,8 +1,8 @@
 import EventEmitter from 'events';
-import { GenericEventEmitter, Logger, ProviderEvents, ReadyEvent } from '../src';
+import { GenericEventEmitter, Logger, AllProviderEvents, ReadyEvent, AnyProviderEvent } from '../src';
 
 // create concrete class to test the abstract functionality.
-class TestEventEmitter extends GenericEventEmitter {
+class TestEventEmitter extends GenericEventEmitter<AnyProviderEvent> {
   protected readonly eventEmitter = new EventEmitter({ captureRejections: true });
 
   constructor() {
@@ -19,8 +19,8 @@ describe('GenericEventEmitter', () => {
       const emitter = new TestEventEmitter();
 
       const handler1 = jest.fn();
-      emitter.addHandler(ProviderEvents.Ready, handler1);
-      emitter.emit(ProviderEvents.Ready);
+      emitter.addHandler(AllProviderEvents.Ready, handler1);
+      emitter.emit(AllProviderEvents.Ready);
 
       expect(handler1).toHaveBeenCalledTimes(1);
     });
@@ -32,11 +32,11 @@ describe('GenericEventEmitter', () => {
       const handler2 = jest.fn();
       const handler3 = jest.fn();
 
-      emitter.addHandler(ProviderEvents.Ready, handler1);
-      emitter.addHandler(ProviderEvents.Ready, handler2);
-      emitter.addHandler(ProviderEvents.Error, handler3);
+      emitter.addHandler(AllProviderEvents.Ready, handler1);
+      emitter.addHandler(AllProviderEvents.Ready, handler2);
+      emitter.addHandler(AllProviderEvents.Error, handler3);
 
-      emitter.emit(ProviderEvents.Ready);
+      emitter.emit(AllProviderEvents.Ready);
 
       expect(handler1).toHaveBeenCalledTimes(1);
       expect(handler2).toHaveBeenCalledTimes(1);
@@ -56,18 +56,18 @@ describe('GenericEventEmitter', () => {
       const emitter = new TestEventEmitter();
       emitter.setLogger(logger);
 
-      emitter.addHandler(ProviderEvents.Ready, async () => {
+      emitter.addHandler(AllProviderEvents.Ready, async () => {
         throw Error();
       });
-      emitter.emit(ProviderEvents.Ready);
+      emitter.emit(AllProviderEvents.Ready);
     });
 
     it('trigger handler for event type', function () {
       const emitter = new TestEventEmitter();
 
       const handler1 = jest.fn();
-      emitter.addHandler(ProviderEvents.Ready, handler1);
-      emitter.emit(ProviderEvents.Ready);
+      emitter.addHandler(AllProviderEvents.Ready, handler1);
+      emitter.emit(AllProviderEvents.Ready);
 
       expect(handler1).toHaveBeenCalledTimes(1);
     });
@@ -77,8 +77,8 @@ describe('GenericEventEmitter', () => {
       const emitter = new TestEventEmitter();
 
       const handler1 = jest.fn();
-      emitter.addHandler(ProviderEvents.Ready, handler1);
-      emitter.emit(ProviderEvents.Ready, event);
+      emitter.addHandler(AllProviderEvents.Ready, handler1);
+      emitter.emit(AllProviderEvents.Ready, event);
 
       expect(handler1).toHaveBeenNthCalledWith(1, event);
     });
@@ -90,11 +90,11 @@ describe('GenericEventEmitter', () => {
       const handler2 = jest.fn();
       const handler3 = jest.fn();
 
-      emitter.addHandler(ProviderEvents.Ready, handler1);
-      emitter.addHandler(ProviderEvents.Ready, handler2);
-      emitter.addHandler(ProviderEvents.Error, handler3);
+      emitter.addHandler(AllProviderEvents.Ready, handler1);
+      emitter.addHandler(AllProviderEvents.Ready, handler2);
+      emitter.addHandler(AllProviderEvents.Error, handler3);
 
-      emitter.emit(ProviderEvents.Ready);
+      emitter.emit(AllProviderEvents.Ready);
 
       expect(handler1).toHaveBeenCalledTimes(1);
       expect(handler2).toHaveBeenCalledTimes(1);
@@ -107,11 +107,11 @@ describe('GenericEventEmitter', () => {
       const emitter = new TestEventEmitter();
 
       const handler1 = jest.fn();
-      emitter.addHandler(ProviderEvents.Ready, handler1);
+      emitter.addHandler(AllProviderEvents.Ready, handler1);
 
-      emitter.emit(ProviderEvents.Ready);
-      emitter.removeHandler(ProviderEvents.Ready, handler1);
-      emitter.emit(ProviderEvents.Ready);
+      emitter.emit(AllProviderEvents.Ready);
+      emitter.removeHandler(AllProviderEvents.Ready, handler1);
+      emitter.emit(AllProviderEvents.Ready);
 
       expect(handler1).toHaveBeenCalledTimes(1);
     });
@@ -123,12 +123,12 @@ describe('GenericEventEmitter', () => {
 
       const handler1 = jest.fn();
       const handler2 = jest.fn();
-      emitter.addHandler(ProviderEvents.Ready, handler1);
-      emitter.addHandler(ProviderEvents.Error, handler2);
+      emitter.addHandler(AllProviderEvents.Ready, handler1);
+      emitter.addHandler(AllProviderEvents.Error, handler2);
 
-      emitter.removeAllHandlers(ProviderEvents.Ready);
-      emitter.emit(ProviderEvents.Ready);
-      emitter.emit(ProviderEvents.Error);
+      emitter.removeAllHandlers(AllProviderEvents.Ready);
+      emitter.emit(AllProviderEvents.Ready);
+      emitter.emit(AllProviderEvents.Error);
 
       expect(handler1).toHaveBeenCalledTimes(0);
       expect(handler2).toHaveBeenCalledTimes(1);
@@ -139,12 +139,12 @@ describe('GenericEventEmitter', () => {
 
       const handler1 = jest.fn();
       const handler2 = jest.fn();
-      emitter.addHandler(ProviderEvents.Ready, handler1);
-      emitter.addHandler(ProviderEvents.Error, handler2);
+      emitter.addHandler(AllProviderEvents.Ready, handler1);
+      emitter.addHandler(AllProviderEvents.Error, handler2);
 
-      emitter.emit(ProviderEvents.Ready);
-      emitter.removeAllHandlers(ProviderEvents.Ready);
-      emitter.emit(ProviderEvents.Ready);
+      emitter.emit(AllProviderEvents.Ready);
+      emitter.removeAllHandlers(AllProviderEvents.Ready);
+      emitter.emit(AllProviderEvents.Ready);
 
       expect(handler1).toHaveBeenCalledTimes(1);
       expect(handler2).toHaveBeenCalledTimes(0);
@@ -155,14 +155,14 @@ describe('GenericEventEmitter', () => {
 
       const handler1 = jest.fn();
       const handler2 = jest.fn();
-      emitter.addHandler(ProviderEvents.Ready, handler1);
-      emitter.addHandler(ProviderEvents.Error, handler2);
+      emitter.addHandler(AllProviderEvents.Ready, handler1);
+      emitter.addHandler(AllProviderEvents.Error, handler2);
 
-      emitter.emit(ProviderEvents.Ready);
-      emitter.emit(ProviderEvents.Error);
+      emitter.emit(AllProviderEvents.Ready);
+      emitter.emit(AllProviderEvents.Error);
       emitter.removeAllHandlers();
-      emitter.emit(ProviderEvents.Ready);
-      emitter.emit(ProviderEvents.Error);
+      emitter.emit(AllProviderEvents.Ready);
+      emitter.emit(AllProviderEvents.Error);
 
       expect(handler1).toHaveBeenCalledTimes(1);
       expect(handler2).toHaveBeenCalledTimes(1);