diff --git a/package-lock.json b/package-lock.json index 2aa07eba..87cf916a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5149,14 +5149,14 @@ "license": "MIT" }, "node_modules/tinyglobby": { - "version": "0.2.14", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", - "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", "dev": true, "license": "MIT", "dependencies": { - "fdir": "^6.4.4", - "picomatch": "^4.0.2" + "fdir": "^6.5.0", + "picomatch": "^4.0.3" }, "engines": { "node": ">=12.0.0" @@ -5319,9 +5319,9 @@ } }, "node_modules/vite": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.4.tgz", - "integrity": "sha512-X5QFK4SGynAeeIt+A7ZWnApdUyHYm+pzv/8/A57LqSGcI88U6R6ipOs3uCesdc6yl7nl+zNO0t8LmqAdXcQihw==", + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.5.tgz", + "integrity": "sha512-4cKBO9wR75r0BeIWWWId9XK9Lj6La5X846Zw9dFfzMRw38IlTk2iCcUt6hsyiDRcPidc55ZParFYDXi0nXOeLQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5330,7 +5330,7 @@ "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", - "tinyglobby": "^0.2.14" + "tinyglobby": "^0.2.15" }, "bin": { "vite": "bin/vite.js" diff --git a/src/appConfigurationImpl.ts b/src/appConfigurationImpl.ts index 6918e2fe..7452d2ba 100644 --- a/src/appConfigurationImpl.ts +++ b/src/appConfigurationImpl.ts @@ -598,21 +598,19 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { * Updates etag of watched settings from loaded data. If a watched setting is not covered by any selector, a request will be sent to retrieve it. */ async #updateWatchedKeyValuesEtag(existingSettings: ConfigurationSetting[]): Promise { + const updatedSentinels: ConfigurationSettingId[] = []; for (const sentinel of this.#sentinels) { const matchedSetting = existingSettings.find(s => s.key === sentinel.key && s.label === sentinel.label); if (matchedSetting) { - sentinel.etag = matchedSetting.etag; + updatedSentinels.push( {...sentinel, etag: matchedSetting.etag} ); } else { // Send a request to retrieve key-value since it may be either not loaded or loaded with a different label or different casing const { key, label } = sentinel; const response = await this.#getConfigurationSetting({ key, label }); - if (response) { - sentinel.etag = response.etag; - } else { - sentinel.etag = undefined; - } + updatedSentinels.push( {...sentinel, etag: response?.etag} ); } } + this.#sentinels = updatedSentinels; } /** @@ -670,7 +668,6 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { if (response?.statusCode === 200 // created or changed || (response === undefined && sentinel.etag !== undefined) // deleted ) { - sentinel.etag = response?.etag;// update etag of the sentinel needRefresh = true; break; } diff --git a/test/refresh.test.ts b/test/refresh.test.ts index 26f869de..6d4aea89 100644 --- a/test/refresh.test.ts +++ b/test/refresh.test.ts @@ -25,11 +25,19 @@ function addSetting(key: string, value: any) { } let listKvRequestCount = 0; +let failNextListKv = false; const listKvCallback = () => { + if (failNextListKv) { + throw new Error("Intended error for test"); + } listKvRequestCount++; }; let getKvRequestCount = 0; +let failNextGetKv = false; const getKvCallback = () => { + if (failNextGetKv) { + throw new Error("Intended error for test"); + } getKvRequestCount++; }; @@ -40,6 +48,7 @@ describe("dynamic refresh", function () { { value: "red", key: "app.settings.fontColor" }, { value: "40", key: "app.settings.fontSize" }, { value: "30", key: "app.settings.fontSize", label: "prod" }, + { value: "someValue", key: "sentinel" }, { value: "someValue", key: "TestTagKey", tags: { "env": "dev" } } ].map(createMockedKeyValue); mockAppConfigurationClientListConfigurationSettings([mockedKVs], listKvCallback); @@ -48,6 +57,8 @@ describe("dynamic refresh", function () { afterEach(() => { restoreMocks(); + failNextListKv = false; + failNextGetKv = false; listKvRequestCount = 0; getKvRequestCount = 0; }); @@ -239,6 +250,44 @@ describe("dynamic refresh", function () { expect(settings.get("app.settings.bgColor")).eq("white"); }); + it("should continue to refresh when previous refresh-all attempt failed", async () => { + const connectionString = createMockedConnectionString(); + const settings = await load(connectionString, { + selectors: [ + { keyFilter: "app.settings.*" } + ], + refreshOptions: { + enabled: true, + refreshIntervalInMs: 2000, + watchedSettings: [ + { key: "sentinel" } + ] + } + }); + expect(settings.get("app.settings.fontSize")).eq("40"); + expect(settings.get("app.settings.fontColor")).eq("red"); + expect(settings.get("sentinel")).to.be.undefined; + expect(listKvRequestCount).eq(1); + expect(getKvRequestCount).eq(1); // one getKv request for sentinel key + + // change setting + addSetting("app.settings.bgColor", "white"); + updateSetting("sentinel", "updatedValue"); + failNextListKv = true; // force next listConfigurationSettings request to fail + await sleepInMs(2 * 1000 + 1); + await settings.refresh(); // even if the provider detects the sentinel key change, this refresh will fail, so we won't get the updated value of sentinel + expect(listKvRequestCount).eq(1); + expect(getKvRequestCount).eq(2); + expect(settings.get("app.settings.bgColor")).to.be.undefined; + + failNextListKv = false; + await sleepInMs(2 * 1000 + 1); + await settings.refresh(); // should continue to refresh even if sentinel key doesn't change now + expect(listKvRequestCount).eq(2); + expect(getKvRequestCount).eq(4); + expect(settings.get("app.settings.bgColor")).eq("white"); + }); + it("should execute callbacks on successful refresh", async () => { const connectionString = createMockedConnectionString(); const settings = await load(connectionString, {