Repository: open-feature/js-sdk
Pull request #541 (2023-08-17 19:22:06)

feat: STALE state, minor event changes
* added the "STALE" provider state
* added "providerName" to EventDetails
* run handlers immediately if provider in corresponding state
* fixes a few lint issues

Fixes: https://github.com/open-feature/js-sdk/issues/562
Fixes: https://github.com/open-feature/js-sdk/issues/539

toddbaert: Now that 0.7.0 is released marked this ready.

Patch:
diff --git a/packages/client/README.md b/packages/client/README.md
index 4e9c9aad..c1a98ec2 100644
--- a/packages/client/README.md
+++ b/packages/client/README.md
@@ -13,8 +13,8 @@
 <!-- x-hide-in-docs-end -->
 <!-- The 'github-badges' class is used in the docs -->
 <p align="center" class="github-badges">
-  <a href="https://github.com/open-feature/spec/tree/v0.6.0">
-    <img alt="Specification" src="https://img.shields.io/static/v1?label=specification&message=v0.6.0&color=yellow&style=for-the-badge" />
+  <a href="https://github.com/open-feature/spec/tree/v0.7.0">
+    <img alt="Specification" src="https://img.shields.io/static/v1?label=specification&message=v0.7.0&color=yellow&style=for-the-badge" />
   </a>
   <!-- x-release-please-start-version -->
   <a href="https://github.com/open-feature/js-sdk/releases/tag/web-sdk-v0.4.0">
diff --git a/packages/client/src/client/open-feature-client.ts b/packages/client/src/client/open-feature-client.ts
index 20442e7d..fb22f578 100644
--- a/packages/client/src/client/open-feature-client.ts
+++ b/packages/client/src/client/open-feature-client.ts
@@ -1,5 +1,3 @@
-import { Client } from './client';
-import { Provider } from '../provider';
 import {
   ClientMetadata,
   ErrorCode,
@@ -20,8 +18,10 @@ import {
   SafeLogger,
   StandardResolutionReasons,
 } from '@openfeature/shared';
-import { OpenFeature } from '../open-feature';
 import { FlagEvaluationOptions } from '../evaluation';
+import { OpenFeature } from '../open-feature';
+import { Provider } from '../provider';
+import { Client } from './client';
 
 type OpenFeatureClientOptions = {
   name?: string;
@@ -56,7 +56,7 @@ export class OpenFeatureClient implements Client {
     if (eventType === ProviderEvents.Ready && providerReady) {
       // run immediately, we're ready.
       try {
-        handler({ clientName: this.metadata.name });
+        handler({ clientName: this.metadata.name, providerName: this._provider.metadata.name });
       } catch (err) {
         this._logger?.error('Error running event handler:', err);
       }
diff --git a/packages/server/README.md b/packages/server/README.md
index 44fcfa83..f0600cdd 100644
--- a/packages/server/README.md
+++ b/packages/server/README.md
@@ -13,8 +13,8 @@
 <!-- x-hide-in-docs-end -->
 <!-- The 'github-badges' class is used in the docs -->
 <p align="center" class="github-badges">
-  <a href="https://github.com/open-feature/spec/tree/v0.6.0">
-    <img alt="Specification" src="https://img.shields.io/static/v1?label=specification&message=v0.6.0&color=yellow&style=for-the-badge" />
+  <a href="https://github.com/open-feature/spec/tree/v0.7.0">
+    <img alt="Specification" src="https://img.shields.io/static/v1?label=specification&message=v0.7.0&color=yellow&style=for-the-badge" />
   </a>
   <!-- x-release-please-start-version -->
   <a href="https://github.com/open-feature/js-sdk/releases/tag/js-sdk-v1.4.2">
diff --git a/packages/server/src/client/open-feature-client.ts b/packages/server/src/client/open-feature-client.ts
index e6331a12..9e2d4ef2 100644
--- a/packages/server/src/client/open-feature-client.ts
+++ b/packages/server/src/client/open-feature-client.ts
@@ -6,23 +6,23 @@ import {
   EventHandler,
   FlagValue,
   FlagValueType,
+  Hook,
   HookContext,
+  InternalEventEmitter,
   JsonValue,
   Logger,
+  ManageContext,
   OpenFeatureError,
-  InternalEventEmitter,
   ProviderEvents,
-  ProviderStatus,
   ResolutionDetails,
-  StandardResolutionReasons,
   SafeLogger,
-  Hook,
-  ManageContext,
+  StandardResolutionReasons,
+  statusMatchesEvent
 } from '@openfeature/shared';
+import { FlagEvaluationOptions } from '../evaluation';
 import { OpenFeature } from '../open-feature';
-import { Client } from './client';
 import { Provider } from '../provider';
-import { FlagEvaluationOptions } from '../evaluation';
+import { Client } from './client';
 
 type OpenFeatureClientOptions = {
   name?: string;
@@ -56,12 +56,12 @@ export class OpenFeatureClient implements Client, ManageContext<OpenFeatureClien
 
   addHandler<T extends ProviderEvents>(eventType: T, handler: EventHandler<T>): void {
     this.emitterAccessor().addHandler(eventType, handler);
-    const providerReady = !this._provider.status || this._provider.status === ProviderStatus.READY;
+    const shouldRunNow = statusMatchesEvent(eventType, this._provider.status);
 
-    if (eventType === ProviderEvents.Ready && providerReady) {
-      // run immediately, we're ready.
+    if (shouldRunNow) {
+      // run immediately, we're in the matching state
       try {
-        handler({ clientName: this.metadata.name });
+        handler({ clientName: this.metadata.name, providerName: this._provider.metadata.name });
       } catch (err) {
         this._logger?.error('Error running event handler:', err);
       }
diff --git a/packages/shared/src/events/event-utils.ts b/packages/shared/src/events/event-utils.ts
new file mode 100644
index 00000000..eff6cab7
--- /dev/null
+++ b/packages/shared/src/events/event-utils.ts
@@ -0,0 +1,20 @@
+import { ProviderStatus } from '../provider';
+import { ProviderEvents } from './events';
+
+const eventStatusMap = {
+    [ProviderStatus.READY]: ProviderEvents.Ready,
+    [ProviderStatus.ERROR]: ProviderEvents.Error,
+    [ProviderStatus.STALE]: ProviderEvents.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 {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;
+};
\ No newline at end of file
diff --git a/packages/shared/src/events/eventing.ts b/packages/shared/src/events/eventing.ts
index 6f2d62ac..cc556c2f 100644
--- a/packages/shared/src/events/eventing.ts
+++ b/packages/shared/src/events/eventing.ts
@@ -5,6 +5,7 @@ export type EventMetadata = {
 };
 
 export type CommonEventDetails = {
+  providerName: string;
   clientName?: string;
 };
 
diff --git a/packages/shared/src/events/index.ts b/packages/shared/src/events/index.ts
index ec3d42fe..c32213c3 100644
--- a/packages/shared/src/events/index.ts
+++ b/packages/shared/src/events/index.ts
@@ -1,3 +1,4 @@
-export * from './events';
+export * from './event-utils';
 export * from './eventing';
+export * from './events';
 export * from './open-feature-event-emitter';
diff --git a/packages/shared/src/events/open-feature-event-emitter.ts b/packages/shared/src/events/open-feature-event-emitter.ts
index a143e220..56cf4d84 100644
--- a/packages/shared/src/events/open-feature-event-emitter.ts
+++ b/packages/shared/src/events/open-feature-event-emitter.ts
@@ -1,7 +1,7 @@
-import { Logger, ManageLogger, SafeLogger } from '../logger';
 import EventEmitter from 'events';
+import { Logger, ManageLogger, SafeLogger } from '../logger';
+import { CommonEventDetails, EventContext, EventDetails, EventHandler } from './eventing';
 import { ProviderEvents } from './events';
-import { EventContext, EventDetails, EventHandler, CommonEventDetails } from './eventing';
 
 abstract class GenericEventEmitter<AdditionalContext extends Record<string, unknown> = Record<string, unknown>>
   implements ManageLogger<GenericEventEmitter<AdditionalContext>>
@@ -24,8 +24,8 @@ abstract class GenericEventEmitter<AdditionalContext extends Record<string, unkn
   addHandler<T extends ProviderEvents>(eventType: T, handler: EventHandler<T>): 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 (context?: EventDetails<T>) => {
-      await handler(context);
+    const asyncHandler = async (details?: EventDetails<T>) => {
+      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
     this._handlers.set(handler, asyncHandler);
diff --git a/packages/shared/src/filter.ts b/packages/shared/src/filter.ts
index 66b8a995..f0ab489f 100644
--- a/packages/shared/src/filter.ts
+++ b/packages/shared/src/filter.ts
@@ -2,7 +2,7 @@
  * 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
+ * @returns {T} If the value is not null or undefined
  */
 export function isDefined<T>(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 b4a15853..8ca60106 100644
--- a/packages/shared/src/open-feature.ts
+++ b/packages/shared/src/open-feature.ts
@@ -1,6 +1,6 @@
 import { GeneralError } from './errors';
 import { EvaluationContext, FlagValue } from './evaluation';
-import { EventDetails, EventHandler, Eventing, ProviderEvents } from './events';
+import { EventDetails, EventHandler, Eventing, ProviderEvents, statusMatchesEvent } from './events';
 import { InternalEventEmitter } from './events/open-feature-event-emitter';
 import { isDefined } from './filter';
 import { EvaluationLifeCycle, Hook } from './hooks';
@@ -70,11 +70,26 @@ 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.
-   * When changing the provider, the currently attached handlers will listen to the events of the new provider.
+   * API (global) events run for all providers.
    * @param {ProviderEvents} 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 {
+    [...new Map([[undefined, this._defaultProvider]]), ...this._clientProviders].forEach((keyProviderTuple) => {
+      const clientName = keyProviderTuple[0];
+      const provider = keyProviderTuple[1];
+      const shouldRunNow = statusMatchesEvent(eventType, keyProviderTuple[1].status);
+
+      if (shouldRunNow) {
+        // run immediately, we're in the matching state
+        try {
+          handler({ clientName, providerName: provider.metadata.name });
+        } catch (err) {
+          this._logger?.error('Error running event handler:', err);
+        }
+      }
+    });
+    
     this._events.addHandler(eventType, handler);
   }
 
@@ -124,6 +139,7 @@ export abstract class OpenFeatureCommonAPI<P extends CommonProvider = CommonProv
     }
 
     const oldProvider = this.getProviderForClient(clientName);
+    const providerName = provider.metadata.name;
 
     // ignore no-ops
     if (oldProvider === provider) {
@@ -143,7 +159,7 @@ export abstract class OpenFeatureCommonAPI<P extends CommonProvider = CommonProv
     if (typeof provider.initialize === 'function' && provider.status === undefined) {
       const activeLogger = this._logger || console;
       activeLogger.warn(
-        `Provider ${provider?.metadata?.name} implements 'initialize' but not 'status'. Please implement 'status'.`
+        `Provider ${providerName} implements 'initialize' but not 'status'. Please implement 'status'.`
       );
     }
 
@@ -153,21 +169,21 @@ 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 });
+            emitter?.emit(ProviderEvents.Ready, { clientName, providerName });
           });
-          this._events?.emit(ProviderEvents.Ready, { clientName });
+          this._events?.emit(ProviderEvents.Ready, { clientName, providerName });
         })
         ?.catch((error) => {
           this.getAssociatedEventEmitters(clientName).forEach((emitter) => {
-            emitter?.emit(ProviderEvents.Error, { clientName, message: error.message });
+            emitter?.emit(ProviderEvents.Error, { clientName, providerName,  message: error.message });
           });
-          this._events?.emit(ProviderEvents.Error, { clientName, message: error.message });
+          this._events?.emit(ProviderEvents.Error, { clientName, providerName, message: error.message });
         });
     } else {
       emitters.forEach((emitter) => {
-        emitter?.emit(ProviderEvents.Ready, { clientName });
+        emitter?.emit(ProviderEvents.Ready, { clientName, providerName });
       });
-      this._events?.emit(ProviderEvents.Ready, { clientName });
+      this._events?.emit(ProviderEvents.Ready, { clientName, providerName });
     }
 
     if (clientName) {
@@ -208,7 +224,7 @@ export abstract class OpenFeatureCommonAPI<P extends CommonProvider = CommonProv
     const clientProvider = this.getProviderForClient(name);
     Object.values<ProviderEvents>(ProviderEvents).forEach((eventType) =>
       clientProvider.events?.addHandler(eventType, async (details) => {
-        newEmitter.emit(eventType, { ...details, clientName: name });
+        newEmitter.emit(eventType, { ...details, clientName: name, providerName: clientProvider.metadata.name });
       })
     );
 
@@ -248,9 +264,9 @@ export abstract class OpenFeatureCommonAPI<P extends CommonProvider = CommonProv
       const handler = async (details?: EventDetails<ProviderEvents>) => {
         // on each event type, fire the associated handlers
         emitters.forEach((emitter) => {
-          emitter?.emit(eventType, { ...details, clientName });
+          emitter?.emit(eventType, { ...details, clientName, providerName: newProvider.metadata.name });
         });
-        this._events.emit(eventType, { ...details, clientName });
+        this._events.emit(eventType, { ...details, clientName, providerName: newProvider.metadata.name });
       };
 
       return [eventType, handler];
diff --git a/packages/shared/src/provider/provider.ts b/packages/shared/src/provider/provider.ts
index df42ec33..e67b78c3 100644
--- a/packages/shared/src/provider/provider.ts
+++ b/packages/shared/src/provider/provider.ts
@@ -1,7 +1,6 @@
-import { OpenFeatureEventEmitter } from '../events';
-import { Metadata } from '../types';
 import { EvaluationContext } from '../evaluation';
-import { Paradigm } from '../types';
+import { OpenFeatureEventEmitter } from '../events';
+import { Metadata, Paradigm } from '../types';
 
 /**
  * The state of the provider.
@@ -21,6 +20,11 @@ export enum ProviderStatus {
    * The provider is in an error state and unable to evaluate flags.
    */
   ERROR = 'ERROR',
+
+  /**
+   * The provider's cached state is no longer valid and may not be up-to-date with the source of truth.
+   */
+  STALE = 'STALE',
 }
 
 /**


Test patch:
diff --git a/packages/client/test/client.spec.ts b/packages/client/test/client.spec.ts
index ac8e3c96..fb9d1fc1 100644
--- a/packages/client/test/client.spec.ts
+++ b/packages/client/test/client.spec.ts
@@ -1,16 +1,16 @@
 import {
   Client,
-  Provider,
   ErrorCode,
   EvaluationDetails,
-  JsonValue,
+  FlagNotFoundError,
   JsonArray,
   JsonObject,
+  JsonValue,
+  OpenFeature,
+  OpenFeatureClient,
+  Provider,
   ResolutionDetails,
   StandardResolutionReasons,
-  FlagNotFoundError,
-  OpenFeatureClient,
-  OpenFeature,
 } from '../src';
 
 const BOOLEAN_VALUE = true;
@@ -46,7 +46,6 @@ const MOCK_PROVIDER: Provider = {
   metadata: {
     name: 'mock',
   },
-
   events: undefined,
   hooks: [],
   initialize(): Promise<void> {
@@ -370,7 +369,6 @@ describe('Evaluation details structure', () => {
       metadata: {
         name: 'error-mock',
       },
-
       resolveNumberEvaluation: jest.fn((): Promise<ResolutionDetails<number>> => {
         throw new Error(NON_OPEN_FEATURE_ERROR_MESSAGE); // throw a non-open-feature error
       }),
diff --git a/packages/client/test/events.spec.ts b/packages/client/test/events.spec.ts
index 83082eb6..9efa1dcf 100644
--- a/packages/client/test/events.spec.ts
+++ b/packages/client/test/events.spec.ts
@@ -1,3 +1,4 @@
+import { v4 as uuid } from 'uuid';
 import {
   JsonValue,
   NOOP_PROVIDER,
@@ -10,7 +11,6 @@ import {
   ResolutionDetails,
   StaleEvent,
 } from '../src';
-import { v4 as uuid } from 'uuid';
 
 const TIMEOUT = 1000;
 
@@ -82,7 +82,9 @@ describe('Events', () => {
     jest.clearAllMocks();
     clientId = uuid();
     // hacky, but it's helpful to clear the handlers between tests
+    /* eslint-disable @typescript-eslint/no-explicit-any */
     (OpenFeature as any)._clientEventHandlers = new Map();
+    /* eslint-disable @typescript-eslint/no-explicit-any */
     (OpenFeature as any)._clientEvents = new Map();  });
 
   beforeEach(() => {
@@ -166,24 +168,23 @@ describe('Events', () => {
       const provider = new MockProvider();
       const client = OpenFeature.getClient(clientId);
 
-      let clientHandlerRan = false;
-      let apiHandlerRan = false;
-
-      client.addHandler(ProviderEvents.Ready, () => {
-        clientHandlerRan = true;
-        if (clientHandlerRan && apiHandlerRan) {
-          done();
-        }
-      });
-
-      OpenFeature.addHandler(ProviderEvents.Ready, () => {
-        apiHandlerRan = true;
-        if (clientHandlerRan && apiHandlerRan) {
-          done();
-        }
+      Promise.all([
+        new Promise<void>((resolve) => {
+          client.addHandler(ProviderEvents.Error, () => {
+            resolve();
+          });
+        }),
+        new Promise<void>((resolve) => {
+          OpenFeature.addHandler(ProviderEvents.Error, () => {
+            resolve();
+          });
+        })
+      ]).then(() => {
+        done();
       });
 
       OpenFeature.setProvider(clientId, provider);
+      provider.events?.emit(ProviderEvents.Error);
     });
   });
 
@@ -344,11 +345,13 @@ describe('Events', () => {
   });
 
   describe('Requirement 5.2.3,', () => {
-    it('The event details contain the client name associated with the event in the API', (done) => {
-      const provider = new MockProvider();
+    it('The event details MUST contain the provider name associated with the event.', (done) => {
+      const providerName = '5.2.3';
+      const provider = new MockProvider({ name: providerName });
       const client = OpenFeature.getClient(clientId);
 
       client.addHandler(ProviderEvents.Ready, (details) => {
+        expect(details?.providerName).toEqual(providerName);
         expect(details?.clientName).toEqual(clientId);
         done();
       });
@@ -493,20 +496,31 @@ describe('Events', () => {
   });
 
   describe('Requirement 5.3.3', () => {
-    it('`PROVIDER_READY` handlers added after the provider is already in a ready state MUST run immediately.', (done) => {
-      const provider = new MockProvider();
-      const client = OpenFeature.getClient(clientId);
-
-      OpenFeature.setProvider(clientId, provider);
-      expect(provider.initialize).toHaveBeenCalled();
-
-      let handlerCalled = false;
-      client.addHandler(ProviderEvents.Ready, () => {
-        if (!handlerCalled) {
-          handlerCalled = true;
-          done();
-        }
+    describe('API', () => {
+      it('Handlers attached after the provider is already in the associated state, MUST run immediately.', (done) => {
+        const provider = new MockProvider({ initialStatus: ProviderStatus.ERROR });
+  
+        OpenFeature.setProvider(clientId, provider);
+        expect(provider.initialize).not.toHaveBeenCalled();
+  
+        OpenFeature.addHandler(ProviderEvents.Error, () => {
+            done();
+        });
       });
     });
+
+    describe('client', () => {
+      it('Handlers attached after the provider is already in the associated state, MUST run immediately.', (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();
+        });
+      });
+    });    
   });
 });
diff --git a/packages/server/test/client.spec.ts b/packages/server/test/client.spec.ts
index f50cf84e..2eb74e88 100644
--- a/packages/server/test/client.spec.ts
+++ b/packages/server/test/client.spec.ts
@@ -7,11 +7,11 @@ import {
   JsonArray,
   JsonObject,
   JsonValue,
-  ResolutionDetails,
-  StandardResolutionReasons,
-  Provider,
   OpenFeature,
   OpenFeatureClient,
+  Provider,
+  ResolutionDetails,
+  StandardResolutionReasons,
   TransactionContext,
   TransactionContextPropagator,
 } from '../src';
@@ -348,7 +348,6 @@ describe('OpenFeatureClient', () => {
         metadata: {
           name: 'error-mock',
         },
-
         resolveNumberEvaluation: jest.fn((): Promise<ResolutionDetails<number>> => {
           throw new Error(NON_OPEN_FEATURE_ERROR_MESSAGE); // throw a non-open-feature error
         }),
diff --git a/packages/server/test/events.spec.ts b/packages/server/test/events.spec.ts
index 340691e1..9e891630 100644
--- a/packages/server/test/events.spec.ts
+++ b/packages/server/test/events.spec.ts
@@ -1,5 +1,7 @@
+import { v4 as uuid } from 'uuid';
 import {
   JsonValue,
+  NOOP_PROVIDER,
   OpenFeature,
   OpenFeatureEventEmitter,
   Provider,
@@ -7,10 +9,8 @@ import {
   ProviderMetadata,
   ProviderStatus,
   ResolutionDetails,
-  NOOP_PROVIDER,
   StaleEvent,
 } from '../src';
-import { v4 as uuid } from 'uuid';
 
 const TIMEOUT = 1000;
 
@@ -85,7 +85,9 @@ describe('Events', () => {
     jest.clearAllMocks();
     clientId = uuid();
     // hacky, but it's helpful to clear the handlers between tests
+    /* eslint-disable @typescript-eslint/no-explicit-any */
     (OpenFeature as any)._clientEventHandlers = new Map();
+    /* eslint-disable @typescript-eslint/no-explicit-any */
     (OpenFeature as any)._clientEvents = new Map();
   });
 
@@ -170,24 +172,23 @@ describe('Events', () => {
       const provider = new MockProvider();
       const client = OpenFeature.getClient(clientId);
 
-      let clientHandlerRan = false;
-      let apiHandlerRan = false;
-
-      client.addHandler(ProviderEvents.Ready, () => {
-        clientHandlerRan = true;
-        if (clientHandlerRan && apiHandlerRan) {
-          done();
-        }
-      });
-
-      OpenFeature.addHandler(ProviderEvents.Ready, () => {
-        apiHandlerRan = true;
-        if (clientHandlerRan && apiHandlerRan) {
-          done();
-        }
+      Promise.all([
+        new Promise<void>((resolve) => {
+          client.addHandler(ProviderEvents.Error, () => {
+            resolve();
+          });
+        }),
+        new Promise<void>((resolve) => {
+          OpenFeature.addHandler(ProviderEvents.Error, () => {
+            resolve();
+          });
+        })
+      ]).then(() => {
+        done();
       });
 
       OpenFeature.setProvider(clientId, provider);
+      provider.events?.emit(ProviderEvents.Error);
     });
   });
 
@@ -348,11 +349,13 @@ describe('Events', () => {
   });
 
   describe('Requirement 5.2.3,', () => {
-    it('The event details contain the client name associated with the event in the API', (done) => {
-      const provider = new MockProvider();
+    it('The event details MUST contain the provider name associated with the event.', (done) => {
+      const providerName = '5.2.3';
+      const provider = new MockProvider({ name: providerName });
       const client = OpenFeature.getClient(clientId);
 
       client.addHandler(ProviderEvents.Ready, (details) => {
+        expect(details?.providerName).toEqual(providerName);
         expect(details?.clientName).toEqual(clientId);
         done();
       });
@@ -497,20 +500,31 @@ describe('Events', () => {
   });
 
   describe('Requirement 5.3.3', () => {
-    it('`PROVIDER_READY` handlers added after the provider is already in a ready state MUST run immediately.', (done) => {
-      const provider = new MockProvider();
-      const client = OpenFeature.getClient(clientId);
-
-      OpenFeature.setProvider(clientId, provider);
-      expect(provider.initialize).toHaveBeenCalled();
-
-      let handlerCalled = false;
-      client.addHandler(ProviderEvents.Ready, () => {
-        if (!handlerCalled) {
-          handlerCalled = true;
-          done();
-        }
+    describe('API', () => {
+      it('Handlers attached after the provider is already in the associated state, MUST run immediately.', (done) => {
+        const provider = new MockProvider({ initialStatus: ProviderStatus.ERROR });
+  
+        OpenFeature.setProvider(clientId, provider);
+        expect(provider.initialize).not.toHaveBeenCalled();
+  
+        OpenFeature.addHandler(ProviderEvents.Error, () => {
+            done();
+        });
       });
     });
+
+    describe('client', () => {
+      it('Handlers attached after the provider is already in the associated state, MUST run immediately.', (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();
+        });
+      });
+    });    
   });
 });