Repository: open-feature/js-sdk
Pull request #825 (2024-02-21 16:50:46)

chore(deps): update dependency eslint-plugin-check-file to v2.8.0
[![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [eslint-plugin-check-file](https://togithub.com/dukeluo/eslint-plugin-check-file) | [`2.6.2` -> `2.8.0`](https://renovatebot.com/diffs/npm/eslint-plugin-check-file/2.6.2/2.8.0) | [![age](https://developer.mend.io/api/mc/badges/age/npm/eslint-plugin-check-file/2.8.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/eslint-plugin-check-file/2.8.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/eslint-plugin-check-file/2.6.2/2.8.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/eslint-plugin-check-file/2.6.2/2.8.0?slim=true)](https://docs.renovatebot.com/merge-confidence/) |

---

### Release Notes

<details>
<summary>dukeluo/eslint-plugin-check-file (eslint-plugin-check-file)</summary>

### [`v2.8.0`](https://togithub.com/dukeluo/eslint-plugin-check-file/blob/HEAD/CHANGELOG.md#280---2024-04-14)

[Compare Source](https://togithub.com/dukeluo/eslint-plugin-check-file/compare/v2.7.1...v2.8.0)

##### Added

-   support flat config for ESLint v9.0.0

### [`v2.7.1`](https://togithub.com/dukeluo/eslint-plugin-check-file/blob/HEAD/CHANGELOG.md#271---2024-02-24)

[Compare Source](https://togithub.com/dukeluo/eslint-plugin-check-file/compare/v2.7.0...v2.7.1)

##### Fixed

-   the rule `filename-blocklist` can set the suggested glob pattern as empty string when `errorMessage` is set

### [`v2.7.0`](https://togithub.com/dukeluo/eslint-plugin-check-file/blob/HEAD/CHANGELOG.md#270---2024-02-18)

[Compare Source](https://togithub.com/dukeluo/eslint-plugin-check-file/compare/v2.6.2...v2.7.0)

##### Added

-   the rule `filename-naming-convention` can set `errorMessage` property to customize the error message
-   the rule `filename-blocklist` can set `errorMessage` property to customize the error message

##### Changed

-   Node.js version should >= 18

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Enabled.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://developer.mend.io/github/open-feature/js-sdk).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy4yMDAuMCIsInVwZGF0ZWRJblZlciI6IjM3LjMwMS40IiwidGFyZ2V0QnJhbmNoIjoibWFpbiJ9-->



Patch:
diff --git a/package-lock.json b/package-lock.json
index 9a57cd20..a5289668 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -4625,16 +4625,16 @@
       }
     },
     "node_modules/eslint-plugin-check-file": {
-      "version": "2.6.2",
-      "resolved": "https://registry.npmjs.org/eslint-plugin-check-file/-/eslint-plugin-check-file-2.6.2.tgz",
-      "integrity": "sha512-z3Rur4JjOdNH0fia1IH7JQseo9NLuFVtw9j8P6z2c5XmXWemH7/qGpmMB8XbOt9bJBNpmPlNAGJty9b3EervPw==",
+      "version": "2.8.0",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-check-file/-/eslint-plugin-check-file-2.8.0.tgz",
+      "integrity": "sha512-FvvafMTam2WJYH9uj+FuMxQ1y+7jY3Z6P9T4j2214cH0FBxNzTcmeCiGTj1Lxp3mI6kbbgsXvmgewvf+llKYyw==",
       "dev": true,
       "dependencies": {
         "is-glob": "^4.0.3",
         "micromatch": "^4.0.5"
       },
       "engines": {
-        "node": "14.x || >= 16"
+        "node": ">=18"
       },
       "funding": {
         "type": "ko_fi",
@@ -13516,9 +13516,9 @@
       }
     },
     "node_modules/typedoc": {
-      "version": "0.25.12",
-      "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.25.12.tgz",
-      "integrity": "sha512-F+qhkK2VoTweDXd1c42GS/By2DvI2uDF4/EpG424dTexSHdtCH52C6IcAvMA6jR3DzAWZjHpUOW+E02kyPNUNw==",
+      "version": "0.25.13",
+      "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.25.13.tgz",
+      "integrity": "sha512-pQqiwiJ+Z4pigfOnnysObszLiU3mVLWAExSPf+Mu06G/qsc3wzbuM56SZQvONhHLncLUhYzOVkjFFpFfL5AzhQ==",
       "dev": true,
       "dependencies": {
         "lunr": "^2.3.9",
@@ -14164,7 +14164,7 @@
     },
     "packages/react": {
       "name": "@openfeature/react-sdk",
-      "version": "0.3.3",
+      "version": "0.3.4",
       "license": "Apache-2.0",
       "devDependencies": {
         "@openfeature/core": "*",
diff --git a/packages/client/src/open-feature.ts b/packages/client/src/open-feature.ts
index 82f1a4b3..c0bcde6c 100644
--- a/packages/client/src/open-feature.ts
+++ b/packages/client/src/open-feature.ts
@@ -231,14 +231,20 @@ export class OpenFeatureAPI
 
     try {
       if (typeof wrapper.provider.onContextChange === 'function') {
-        wrapper.incrementPendingContextChanges();
-        wrapper.status = this._statusEnumType.RECONCILING;
-        this.getAssociatedEventEmitters(domain).forEach((emitter) => {
-          emitter?.emit(ProviderEvents.Reconciling, { domain, providerName });
-        });
-        this._apiEmitter?.emit(ProviderEvents.Reconciling, { domain, providerName });
-        await wrapper.provider.onContextChange(oldContext, newContext);
-        wrapper.decrementPendingContextChanges();
+        const maybePromise = wrapper.provider.onContextChange(oldContext, newContext);
+
+        // only reconcile if the onContextChange method returns a promise
+        if (typeof maybePromise?.then === 'function') {
+          wrapper.incrementPendingContextChanges();
+          wrapper.status = this._statusEnumType.RECONCILING;
+          this.getAssociatedEventEmitters(domain).forEach((emitter) => {
+            emitter?.emit(ProviderEvents.Reconciling, { domain, providerName });
+          });
+          this._apiEmitter?.emit(ProviderEvents.Reconciling, { domain, providerName });
+
+          await maybePromise;
+          wrapper.decrementPendingContextChanges();
+        }
       }
       // only run the event handlers, and update the state if the onContextChange method succeeded
       wrapper.status = this._statusEnumType.READY;
diff --git a/packages/client/src/provider/provider.ts b/packages/client/src/provider/provider.ts
index 8b590366..6b51ec73 100644
--- a/packages/client/src/provider/provider.ts
+++ b/packages/client/src/provider/provider.ts
@@ -19,12 +19,18 @@ export interface Provider extends CommonProvider<ClientProviderStatus> {
   readonly hooks?: Hook[];
 
   /**
-   * A handler function to reconcile changes when the static context.
+   * A handler function to reconcile changes made to the static context.
    * Called by the SDK when the context is changed.
+   *
+   * Returning a promise will put the provider in the RECONCILING state and
+   * emit the ProviderEvents.Reconciling event.
+   *
+   * Return void will avoid putting the provider in the RECONCILING state and
+   * **not** emit the ProviderEvents.Reconciling event.
    * @param oldContext
    * @param newContext
    */
-  onContextChange?(oldContext: EvaluationContext, newContext: EvaluationContext): Promise<void>;
+  onContextChange?(oldContext: EvaluationContext, newContext: EvaluationContext): Promise<void> | void;
 
   /**
    * Resolve a boolean flag and its evaluation details.


Test patch:
diff --git a/packages/client/test/events.spec.ts b/packages/client/test/events.spec.ts
index 5272694f..51ceceb8 100644
--- a/packages/client/test/events.spec.ts
+++ b/packages/client/test/events.spec.ts
@@ -22,11 +22,12 @@ class MockProvider implements Provider {
   readonly runsOn = 'client';
   private hasInitialize: boolean;
   private hasContextChanged: boolean;
+  private asyncContextChangedHandler: boolean;
   private failOnInit: boolean;
   private failOnContextChange: boolean;
   private asyncDelay?: number;
   private enableEvents: boolean;
-  onContextChange?: () => Promise<void>;
+  onContextChange?: () => Promise<void> | void;
   initialize?: () => Promise<void>;
 
   constructor(options?: {
@@ -35,6 +36,7 @@ class MockProvider implements Provider {
     enableEvents?: boolean;
     failOnInit?: boolean;
     hasContextChanged?: boolean;
+    asyncContextChangedHandler?: boolean;
     failOnContextChange?: boolean;
     name?: string;
   }) {
@@ -45,6 +47,7 @@ class MockProvider implements Provider {
     this.enableEvents = options?.enableEvents ?? true;
     this.failOnInit = options?.failOnInit ?? false;
     this.failOnContextChange = options?.failOnContextChange ?? false;
+    this.asyncContextChangedHandler = options?.asyncContextChangedHandler ?? true;
     if (this.hasContextChanged) {
       this.onContextChange = this.changeHandler;
     }
@@ -80,15 +83,19 @@ class MockProvider implements Provider {
   }
 
   private changeHandler() {
-    return new Promise<void>((resolve, reject) =>
-      setTimeout(() => {
-        if (this.failOnContextChange) {
-          reject(new Error(ERR_MESSAGE));
-        } else {
-          resolve();
-        }
-      }, this.asyncDelay),
-    );
+    if (this.asyncContextChangedHandler) {
+      return new Promise<void>((resolve, reject) =>
+        setTimeout(() => {
+          if (this.failOnContextChange) {
+            reject(new Error(ERR_MESSAGE));
+          } else {
+            resolve();
+          }
+        }, this.asyncDelay),
+      );
+    } else if (this.failOnContextChange) {
+      throw new Error(ERR_MESSAGE);
+    }
   }
 }
 
@@ -598,6 +605,25 @@ describe('Events', () => {
 
           expect(handler).toHaveBeenCalledTimes(2);
         });
+
+        it('Reconciling events are not emitted for synchronous onContextChange operations', async () => {
+          const provider = new MockProvider({
+            hasInitialize: false,
+            hasContextChanged: true,
+            asyncContextChangedHandler: false,
+          });
+
+          const reconcileHandler = jest.fn(() => {});
+          const changedEventHandler = jest.fn(() => {});
+
+          await OpenFeature.setProviderAndWait(domain, provider);
+          OpenFeature.addHandler(ProviderEvents.Reconciling, reconcileHandler);
+          OpenFeature.addHandler(ProviderEvents.ContextChanged, changedEventHandler);
+          await OpenFeature.setContext(domain, {});
+
+          expect(reconcileHandler).not.toHaveBeenCalled();
+          expect(changedEventHandler).toHaveBeenCalledTimes(1);
+        });
       });
 
       describe('provider has no context changed handler', () => {
@@ -615,7 +641,7 @@ describe('Events', () => {
         });
       });
     });
-    
+
     describe('client', () => {
       describe('provider has context changed handler', () => {
         it('Stale and ContextChanged are emitted', async () => {
@@ -810,12 +836,9 @@ describe('Events', () => {
         };
 
         client.addHandler(ProviderEvents.ContextChanged, handler);
-        
+
         // update context change twice
-        await Promise.all([
-          OpenFeature.setContext(domain, {}),
-          OpenFeature.setContext(domain, {}),
-        ]);
+        await Promise.all([OpenFeature.setContext(domain, {}), OpenFeature.setContext(domain, {})]);
 
         // should only have run once
         expect(runs).toEqual(1);