Skip to content

Commit bebf549

Browse files
update
1 parent 414a8c8 commit bebf549

File tree

7 files changed

+88
-54
lines changed

7 files changed

+88
-54
lines changed

package-lock.json

Lines changed: 15 additions & 15 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@
6767
"playwright": "^1.55.0"
6868
},
6969
"dependencies": {
70-
"@azure/app-configuration": "^1.9.0",
70+
"@azure/app-configuration": "^1.9.2",
7171
"@azure/core-rest-pipeline": "^1.6.0",
7272
"@azure/identity": "^4.2.1",
7373
"@azure/keyvault-secrets": "^4.7.0",

src/appConfigurationImpl.ts

Lines changed: 27 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ import { ConfigurationClientManager } from "./configurationClientManager.js";
6868
import { getFixedBackoffDuration, getExponentialBackoffDuration } from "./common/backoffUtils.js";
6969
import { InvalidOperationError, ArgumentError, isFailoverableError, isInputError } from "./common/errors.js";
7070
import { ErrorMessages } from "./common/errorMessages.js";
71-
import { TIMESTAMP_HEADER } from "./cdn/constants.js";
71+
import { SERVER_TIMESTAMP_HEADER } from "./cdn/constants.js";
7272

7373
const MIN_DELAY_FOR_UNHANDLED_FAILURE = 5_000; // 5 seconds
7474

@@ -130,17 +130,17 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
130130
// Load balancing
131131
#lastSuccessfulEndpoint: string = "";
132132

133-
// CDN
134-
#isCdnUsed: boolean = false;
133+
// Azure Front Door
134+
#isAfdUsed: boolean = false;
135135

136136
constructor(
137137
clientManager: ConfigurationClientManager,
138138
options: AzureAppConfigurationOptions | undefined,
139-
isCdnUsed: boolean
139+
isAfdUsed: boolean
140140
) {
141141
this.#options = options;
142142
this.#clientManager = clientManager;
143-
this.#isCdnUsed = isCdnUsed;
143+
this.#isAfdUsed = isAfdUsed;
144144

145145
// enable request tracing if not opt-out
146146
this.#requestTracingEnabled = requestTracingEnabled();
@@ -169,7 +169,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
169169
if (setting.label?.includes("*") || setting.label?.includes(",")) {
170170
throw new ArgumentError(ErrorMessages.INVALID_WATCHED_SETTINGS_LABEL);
171171
}
172-
this.#sentinels.set(setting, { etag: undefined, lastServerResponseTime: new Date(0) });
172+
this.#sentinels.set(setting, { etag: undefined });
173173
}
174174
}
175175

@@ -229,7 +229,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
229229
featureFlagTracing: this.#featureFlagTracing,
230230
fmVersion: this.#fmVersion,
231231
aiConfigurationTracing: this.#aiConfigurationTracing,
232-
isAfdUsed: this.#isCdnUsed
232+
isAfdUsed: this.#isAfdUsed
233233
};
234234
}
235235

@@ -592,10 +592,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
592592
for (const watchedSetting of this.#sentinels.keys()) {
593593
const configurationSettingId: ConfigurationSettingId = { key: watchedSetting.key, label: watchedSetting.label };
594594
const response = await this.#getConfigurationSetting(configurationSettingId, { onlyIfChanged: false });
595-
this.#sentinels.set(watchedSetting, {
596-
etag: isRestError(response) ? undefined : response.etag,
597-
lastServerResponseTime: this.#getResponseTimestamp(response)
598-
});
595+
this.#sentinels.set(watchedSetting, { etag: response?.etag });
599596
}
600597
}
601598

@@ -647,22 +644,19 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
647644

648645
// try refresh if any of watched settings is changed.
649646
let needRefresh = false;
650-
let changedSentinel;
651-
let changedSentinelWatcher;
647+
let changedSentinel: WatchedSetting | undefined;
648+
let changedSentinelWatcher: SettingWatcher | undefined;
652649
if (this.#watchAll) {
653650
needRefresh = await this.#checkConfigurationSettingsChange(this.#kvSelectors);
654651
} else {
655652
for (const watchedSetting of this.#sentinels.keys()) {
656653
const configurationSettingId: ConfigurationSettingId = { key: watchedSetting.key, label: watchedSetting.label };
657-
const response: GetConfigurationSettingResponse | RestError =
654+
const response: GetConfigurationSettingResponse | undefined =
658655
await this.#getConfigurationSetting(configurationSettingId, { onlyIfChanged: true });
659656

660657
const watcher: SettingWatcher = this.#sentinels.get(watchedSetting)!; // watcher should always exist for sentinels
661-
const isDeleted = isRestError(response) && watcher.etag !== undefined; // previously existed, now deleted
662-
const isChanged =
663-
!isRestError(response) &&
664-
response.statusCode === 200 &&
665-
watcher.etag !== response.etag; // etag changed
658+
const isDeleted = response === undefined && watcher.etag !== undefined; // previously existed, now deleted
659+
const isChanged = response && response.statusCode === 200 && watcher.etag !== response.etag; // etag changed
666660

667661
if (isDeleted || isChanged) {
668662
changedSentinel = watchedSetting;
@@ -747,7 +741,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
747741
tagsFilter: selector.tagFilters
748742
};
749743

750-
if (!this.#isCdnUsed) {
744+
if (!this.#isAfdUsed) {
751745
// if CDN is not used, add page etags to the listOptions to send conditional request
752746
listOptions.pageEtags = pageWatchers.map(w => w.etag ?? "") ;
753747
}
@@ -761,11 +755,13 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
761755
let i = 0;
762756
for await (const page of pageIterator) {
763757
const timestamp = this.#getResponseTimestamp(page);
764-
// when conditional request is sent, the response will be 304 if not changed
765-
if (i >= pageWatchers.length || // new page
766-
(timestamp > pageWatchers[i].lastServerResponseTime && // up to date
767-
page._response.status === 200 && // page changed
768-
page.etag !== pageWatchers[i].etag)) {
758+
if (i >= pageWatchers.length) {
759+
return true;
760+
}
761+
762+
const lastServerResponseTime = pageWatchers[i].lastServerResponseTime;
763+
const isUpToDate = lastServerResponseTime ? timestamp > lastServerResponseTime : true;
764+
if (isUpToDate && page._response.status === 200 && page.etag !== pageWatchers[i].etag) {
769765
return true;
770766
}
771767
i++;
@@ -779,9 +775,9 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
779775
}
780776

781777
/**
782-
* Gets a configuration setting by key and label. If the setting is not found, return the error instead of throwing it.
778+
* Gets a configuration setting by key and label. If the setting is not found, return undefined instead of throwing an error.
783779
*/
784-
async #getConfigurationSetting(configurationSettingId: ConfigurationSettingId, getOptions?: GetConfigurationSettingOptions): Promise<GetConfigurationSettingResponse | RestError> {
780+
async #getConfigurationSetting(configurationSettingId: ConfigurationSettingId, getOptions?: GetConfigurationSettingOptions): Promise<GetConfigurationSettingResponse | undefined> {
785781
const funcToExecute = async (client) => {
786782
return getConfigurationSettingWithTrace(
787783
this.#requestTraceOptions,
@@ -791,13 +787,12 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
791787
);
792788
};
793789

794-
let response: GetConfigurationSettingResponse | RestError;
790+
let response: GetConfigurationSettingResponse | undefined;
795791
try {
796792
response = await this.#executeWithFailoverPolicy(funcToExecute);
797793
} catch (error) {
798794
if (isRestError(error) && error.statusCode === 404) {
799-
// configuration setting not found, return the error
800-
return error;
795+
response = undefined;
801796
} else {
802797
throw error;
803798
}
@@ -1151,9 +1146,9 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
11511146
#getResponseTimestamp(response: GetConfigurationSettingResponse | ListConfigurationSettingPage | RestError): Date {
11521147
let header: string | undefined;
11531148
if (isRestError(response)) {
1154-
header = response.response?.headers?.get(TIMESTAMP_HEADER) ?? undefined;
1149+
header = response.response?.headers?.get(SERVER_TIMESTAMP_HEADER) ?? undefined;
11551150
} else {
1156-
header = response._response?.headers?.get(TIMESTAMP_HEADER) ?? undefined;
1151+
header = response._response?.headers?.get(SERVER_TIMESTAMP_HEADER) ?? undefined;
11571152
}
11581153
return header ? new Date(header) : new Date();
11591154
}

src/cdn/constants.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT license.
33

4-
export const TIMESTAMP_HEADER = "x-ms-date";
4+
export const SERVER_TIMESTAMP_HEADER = "x-ms-date";

src/load.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,8 @@ export async function load(
5050
}
5151

5252
try {
53-
const isCdnUsed: boolean = credentialOrOptions === emptyTokenCredential;
54-
const appConfiguration = new AzureAppConfigurationImpl(clientManager, options, isCdnUsed);
53+
const isAfdUsed: boolean = credentialOrOptions === emptyTokenCredential;
54+
const appConfiguration = new AzureAppConfigurationImpl(clientManager, options, isAfdUsed);
5555
await appConfiguration.load();
5656
return appConfiguration;
5757
} catch (error) {

src/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ export type WatchedSetting = {
9595

9696
export type SettingWatcher = {
9797
etag?: string;
98-
lastServerResponseTime: Date;
98+
lastServerResponseTime?: Date;
9999
}
100100

101101
export type PagedSettingsWatcher = SettingSelector & {

test/afd.test.ts

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,13 @@ import { AppConfigurationClient } from "@azure/app-configuration";
1111
import { load, loadFromAzureFrontDoor } from "../src/index.js";
1212
import { ErrorMessages } from "../src/common/errorMessages.js";
1313
import { createMockedKeyValue, createMockedFeatureFlag, HttpRequestHeadersPolicy, getCachedIterator, sinon, restoreMocks, createMockedConnectionString, createMockedAzureFrontDoorEndpoint, sleepInMs } from "./utils/testHelper.js";
14-
import { TIMESTAMP_HEADER } from "../src/cdn/constants.js";
14+
import { SERVER_TIMESTAMP_HEADER } from "../src/cdn/constants.js";
1515
import { isBrowser } from "../src/requestTracing/utils.js";
1616

1717
function createTimestampHeaders(timestamp: string | Date) {
1818
const value = timestamp instanceof Date ? timestamp.toUTCString() : new Date(timestamp).toUTCString();
1919
return {
20-
get: (name: string) => name.toLowerCase() === TIMESTAMP_HEADER ? value : undefined
20+
get: (name: string) => name.toLowerCase() === SERVER_TIMESTAMP_HEADER ? value : undefined
2121
};
2222
}
2323

@@ -229,5 +229,44 @@ describe("loadFromAzureFrontDoor", function() {
229229
expect(featureFlags[0].id).to.equal("Beta");
230230
expect(featureFlags[0].enabled).to.equal(false);
231231
});
232+
233+
it("should not refresh if the response is stale", async () => {
234+
const kv1 = createMockedKeyValue({ key: "app.key1", value: "value1" });
235+
const kv1_stale = createMockedKeyValue({ key: "app.key1", value: "stale-value" });
236+
const kv1_new = createMockedKeyValue({ key: "app.key1", value: "new-value" });
237+
238+
const stub = sinon.stub(AppConfigurationClient.prototype, "listConfigurationSettings");
239+
stub.onCall(0).returns(getCachedIterator([
240+
{ items: [kv1], response: { status: 200, headers: createTimestampHeaders("2025-09-07T00:00:01Z") } }
241+
]));
242+
243+
stub.onCall(1).returns(getCachedIterator([
244+
{ items: [kv1_stale], response: { status: 200, headers: createTimestampHeaders("2025-09-07T00:00:00Z") } } // stale response, should not trigger refresh
245+
]));
246+
stub.onCall(2).returns(getCachedIterator([
247+
{ items: [kv1_new], response: { status: 200, headers: createTimestampHeaders("2025-09-07T00:00:02Z") } } // new response, should trigger refresh
248+
]));
249+
stub.onCall(3).returns(getCachedIterator([
250+
{ items: [kv1_new], response: { status: 200, headers: createTimestampHeaders("2025-09-07T00:00:02Z") } }
251+
]));
252+
253+
const appConfig = await loadFromAzureFrontDoor(createMockedAzureFrontDoorEndpoint(), {
254+
selectors: [{ keyFilter: "app.*" }],
255+
refreshOptions: {
256+
enabled: true,
257+
refreshIntervalInMs: 1000
258+
}
259+
}); // 1 call listConfigurationSettings
260+
261+
expect(appConfig.get("app.key1")).to.equal("value1");
262+
263+
await sleepInMs(1500);
264+
await appConfig.refresh(); // 1 call listConfigurationSettings for watching changes
265+
expect(appConfig.get("app.key1")).to.equal("value1"); // value should not be updated
266+
267+
await sleepInMs(1500);
268+
await appConfig.refresh(); // 1 call listConfigurationSettings for watching changes and 1 call for reloading
269+
expect(appConfig.get("app.key1")).to.equal("new-value");
270+
});
232271
});
233272
/* eslint-ensable @typescript-eslint/no-unused-expressions */

0 commit comments

Comments
 (0)