Repository: open-feature/js-sdk (ts)

Issue:
web-sdk: onContextChange not called for named provider
### Observed behavior
I am not sure to understand how do you change the context for a named provider?

Let's say I am doing something like this :
```javascript
await OpenFeature.setContext({targetingKey: 'foo'});
OpenFeature.setProvider('my-provider', provider);
const client = await OpenFeature.getClient('my-provider');
//...
await OpenFeature.setContext({targetingKey: 'bar'});
```

### Expected Behavior
I was expecting the function `onContextChange` of the provider to be called when changing the context.
But when looking at the code https://github.com/open-feature/js-sdk/blob/5725194393329cb65cf82a7a76c4ed2bd5d7bcf0/packages/client/src/open-feature.ts#L37-L41

 it seems that this function is called only for the default provider.

Is it something expected? Did I miss something to be sure that `onContextChange` is call on the provider.


Pull request:
fix: onContextChanged not running for named providers
<!-- Please use this template for your pull request. -->
<!-- Please use the sections that you need and delete other sections -->

## This PR
<!-- add the description of the PR here -->

Fixes context propagation for the static-context-paradigm as found in #488.

### Related Issues
<!-- add here the GitHub issue that this PR resolves if applicable -->

Fixes #488

### Notes
<!-- any additional notes for this PR -->

### Follow-up Tasks
<!-- anything that is related to this PR but not done here should be noted under this section -->
<!-- if there is a need for a new issue, please link it here -->



### How to test
<!-- if applicable, add testing instructions under this section -->



Patch:
diff --git a/packages/client/src/open-feature.ts b/packages/client/src/open-feature.ts
index 675fc53d..c161cb5a 100644
--- a/packages/client/src/open-feature.ts
+++ b/packages/client/src/open-feature.ts
@@ -37,7 +37,17 @@ export class OpenFeatureAPI extends OpenFeatureCommonAPI<Provider> implements Ma
   async setContext(context: EvaluationContext): Promise<void> {
     const oldContext = this._context;
     this._context = context;
-    await this._defaultProvider?.onContextChange?.(oldContext, context);
+
+    const allProviders = [this._defaultProvider, ...this._clientProviders.values()];
+    await Promise.all(
+      allProviders.map(async (provider) => {
+        try {
+          return await provider.onContextChange?.(oldContext, context);
+        } catch (err) {
+          this._logger?.error(`Error running context change handler of provider ${provider.metadata.name}:`, err);
+        }
+      })
+    );
   }
 
   getContext(): EvaluationContext {


Test patch:
diff --git a/packages/client/test/evaluation-context.spec.ts b/packages/client/test/evaluation-context.spec.ts
new file mode 100644
index 00000000..79e91198
--- /dev/null
+++ b/packages/client/test/evaluation-context.spec.ts
@@ -0,0 +1,99 @@
+import { EvaluationContext, JsonValue, OpenFeature, Provider, ProviderMetadata, ResolutionDetails } from '../src';
+
+class MockProvider implements Provider {
+  readonly metadata: ProviderMetadata;
+
+  constructor(options?: { name?: string }) {
+    this.metadata = { name: options?.name ?? 'mock-provider' };
+  }
+
+  // eslint-disable-next-line @typescript-eslint/no-unused-vars
+  onContextChange(oldContext: EvaluationContext, newContext: EvaluationContext): Promise<void> {
+    return Promise.resolve();
+  }
+
+  resolveBooleanEvaluation(): ResolutionDetails<boolean> {
+    throw new Error('Not implemented');
+  }
+
+  resolveNumberEvaluation(): ResolutionDetails<number> {
+    throw new Error('Not implemented');
+  }
+
+  resolveObjectEvaluation<T extends JsonValue>(): ResolutionDetails<T> {
+    throw new Error('Not implemented');
+  }
+
+  resolveStringEvaluation(): ResolutionDetails<string> {
+    throw new Error('Not implemented');
+  }
+}
+
+describe('Evaluation Context', () => {
+  describe('Requirement 3.2.2', () => {
+    it('the API MUST have a method for setting the global evaluation context', () => {
+      const context: EvaluationContext = { property1: false };
+      OpenFeature.setContext(context);
+      expect(OpenFeature.getContext()).toEqual(context);
+    });
+  });
+
+  describe('Requirement 3.2.4', () => {
+    describe('when the global evaluation context is set, the on context changed handler MUST run', () => {
+      it('on all registered providers', async () => {
+        // Set initial context
+        const context: EvaluationContext = { property1: false };
+        await OpenFeature.setContext(context);
+
+        // Set some providers
+        const defaultProvider = new MockProvider();
+        const provider1 = new MockProvider();
+        const provider2 = new MockProvider();
+
+        OpenFeature.setProvider(defaultProvider);
+        OpenFeature.setProvider('client1', provider1);
+        OpenFeature.setProvider('client2', provider2);
+
+        // Spy on context changed handlers of all providers
+        const contextChangedSpys = [defaultProvider, provider1, provider2].map((provider) =>
+          jest.spyOn(provider, 'onContextChange')
+        );
+
+        // Change context
+        const newContext: EvaluationContext = { property1: true, property2: 'prop2' };
+        await OpenFeature.setContext(newContext);
+
+        contextChangedSpys.forEach((spy) => expect(spy).toHaveBeenCalledWith(context, newContext));
+      });
+
+      it('on all registered providers even if one fails', async () => {
+        // Set initial context
+        const context: EvaluationContext = { property1: false };
+        await OpenFeature.setContext(context);
+
+        // Set some providers
+        const defaultProvider = new MockProvider();
+        const provider1 = new MockProvider();
+        const provider2 = new MockProvider();
+
+        OpenFeature.setProvider(defaultProvider);
+        OpenFeature.setProvider('client1', provider1);
+        OpenFeature.setProvider('client2', provider2);
+
+        // Spy on context changed handlers of all providers
+        const contextChangedSpys = [defaultProvider, provider1, provider2].map((provider) =>
+          jest.spyOn(provider, 'onContextChange')
+        );
+
+        // Let first handler fail
+        contextChangedSpys[0].mockImplementation(() => Promise.reject(new Error('Error')));
+
+        // Change context
+        const newContext: EvaluationContext = { property1: true, property2: 'prop2' };
+        await OpenFeature.setContext(newContext);
+
+        contextChangedSpys.forEach((spy) => expect(spy).toHaveBeenCalledWith(context, newContext));
+      });
+    });
+  });
+});