Skip to content

Commit 5087206

Browse files
update
1 parent 9d63b7c commit 5087206

File tree

7 files changed

+171
-128
lines changed

7 files changed

+171
-128
lines changed

src/appConfigurationImpl.ts

Lines changed: 101 additions & 85 deletions
Large diffs are not rendered by default.

src/refresh/refreshOptions.ts

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

4-
import { WatchedSetting } from "../watchedSetting.js";
4+
import { WatchedSetting } from "../types.js";
55

66
export const DEFAULT_REFRESH_INTERVAL_IN_MS = 30_000;
77
export const MIN_REFRESH_INTERVAL_IN_MS = 1_000;

src/types.ts

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT license.
33

4+
import { ConfigurationSettingId } from "@azure/app-configuration";
5+
46
/**
57
* SettingSelector is used to select key-values from Azure App Configuration based on keys and labels.
68
*/
@@ -58,7 +60,7 @@ export enum KeyFilter {
5860
* Matches all key-values.
5961
*/
6062
Any = "*"
61-
}
63+
};
6264

6365
/**
6466
* LabelFilter is used to filter key-values based on labels.
@@ -68,7 +70,7 @@ export enum LabelFilter {
6870
* Matches key-values without a label.
6971
*/
7072
Null = "\0"
71-
}
73+
};
7274

7375
/**
7476
* TagFilter is used to filter key-values based on tags.
@@ -78,4 +80,26 @@ export enum TagFilter {
7880
* Represents empty tag value.
7981
*/
8082
Null = ""
83+
};
84+
85+
export type WatchedSetting = {
86+
/**
87+
* The key for this setting.
88+
*/
89+
key: string;
90+
91+
/**
92+
* The label for this setting.
93+
* Leaving this undefined means this setting does not have a label.
94+
*/
95+
label?: string;
8196
}
97+
98+
export type SettingWatcher = {
99+
etag?: string;
100+
timestamp: Date;
101+
}
102+
103+
export type PagedSettingsWatcher = SettingSelector & {
104+
pageWatchers?: SettingWatcher[]
105+
};

src/watchedSetting.ts

Lines changed: 0 additions & 18 deletions
This file was deleted.

test/cdn.test.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -180,10 +180,10 @@ describe("loadFromAzureFrontDoor", function() {
180180
]));
181181

182182
stub.onCall(2).returns(getCachedIterator([
183-
{ items: [ff_updated], response: { status: 200, headers: createTimestampHeaders("2025-09-07T00:00:00Z") } }
183+
{ items: [ff_updated], response: { status: 200, headers: createTimestampHeaders("2025-09-07T00:00:03Z") } }
184184
]));
185185
stub.onCall(3).returns(getCachedIterator([
186-
{ items: [ff_updated], response: { status: 200, headers: createTimestampHeaders("2025-09-07T00:00:00Z") } }
186+
{ items: [ff_updated], response: { status: 200, headers: createTimestampHeaders("2025-09-07T00:00:03Z") } }
187187
]));
188188

189189
const appConfig = await loadFromAzureFrontDoor(createMockedAzureFrontDoorEndpoint(), {
@@ -209,6 +209,7 @@ describe("loadFromAzureFrontDoor", function() {
209209
});
210210

211211
it("should keep refreshing key value until cache expires", async () => {
212+
let refreshSuccessfulCount = 0;
212213
const sentinel = createMockedKeyValue({ key: "sentinel", value: "initial value" });
213214
const sentinel_updated = createMockedKeyValue({ key: "sentinel", value: "updated value" });
214215
const kv1 = createMockedKeyValue({ key: "app.key1", value: "value1" });
@@ -221,9 +222,7 @@ describe("loadFromAzureFrontDoor", function() {
221222
getStub.onCall(0).returns(Promise.resolve({ statusCode: 200, _response: { headers: createTimestampHeaders("2025-09-07T00:00:00Z") }, ...sentinel } as any));
222223
getStub.onCall(1).returns(Promise.resolve({ statusCode: 200, _response: { headers: createTimestampHeaders("2025-09-07T00:00:01Z") }, ...sentinel_updated } as any));
223224
getStub.onCall(2).returns(Promise.resolve({ statusCode: 200, _response: { headers: createTimestampHeaders("2025-09-07T00:00:01Z") }, ...sentinel_updated } as any));
224-
225-
getStub.onCall(3).returns(Promise.resolve({ statusCode: 200, _response: { headers: createTimestampHeaders("2025-09-07T00:00:01Z") }, ...sentinel_updated } as any));
226-
getStub.onCall(4).returns(Promise.resolve({ statusCode: 200, _response: { headers: createTimestampHeaders("2025-09-07T00:00:01Z") }, ...sentinel_updated } as any));
225+
getStub.onCall(4).returns(Promise.resolve({ statusCode: 200, _response: { headers: createTimestampHeaders("2025-09-07T00:00:00Z") }, ...sentinel } as any)); // server old value from another cache server
227226

228227
listStub.onCall(0).returns(getCachedIterator([
229228
{ items: [kv1, kv2], response: { status: 200, headers: createTimestampHeaders("2025-09-07T00:00:00Z") } }
@@ -246,19 +245,30 @@ describe("loadFromAzureFrontDoor", function() {
246245
}
247246
});
248247

248+
appConfig.onRefresh(() => {
249+
refreshSuccessfulCount++;
250+
});
251+
249252
expect(appConfig.get("app.key2")).to.equal("value2");
250253

251254
await sleepInMs(1500);
252255
await appConfig.refresh();
253256

254257
// cdn cache hasn't expired, even if the sentinel key changed, key2 should still return the old value
255258
expect(appConfig.get("app.key2")).to.equal("value2");
259+
expect(refreshSuccessfulCount).to.equal(0);
256260

257261
await sleepInMs(1500);
258262
await appConfig.refresh();
263+
expect(refreshSuccessfulCount).to.equal(1);
259264

260265
// cdn cache has expired, key2 should return the updated value even if sentinel remains the same
261266
expect(appConfig.get("app.key2")).to.equal("value2-updated");
267+
268+
await sleepInMs(1500);
269+
// even if the sentinel is different from previous value, but the timestamp is older, it should not trigger refresh
270+
await appConfig.refresh();
271+
expect(refreshSuccessfulCount).to.equal(1);
262272
});
263273
});
264274
/* eslint-ensable @typescript-eslint/no-unused-expressions */

test/refresh.test.ts

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ describe("dynamic refresh", function () {
144144
}
145145
});
146146
expect(listKvRequestCount).eq(1);
147-
expect(getKvRequestCount).eq(0);
147+
expect(getKvRequestCount).eq(1);
148148
expect(settings).not.undefined;
149149
expect(settings.get("app.settings.fontColor")).eq("red");
150150
expect(settings.get("app.settings.fontSize")).eq("40");
@@ -156,13 +156,13 @@ describe("dynamic refresh", function () {
156156
await settings.refresh();
157157
expect(settings.get("app.settings.fontColor")).eq("red");
158158
expect(listKvRequestCount).eq(1); // no more request should be sent during the refresh interval
159-
expect(getKvRequestCount).eq(0); // no more request should be sent during the refresh interval
159+
expect(getKvRequestCount).eq(1); // no more request should be sent during the refresh interval
160160

161161
// after refreshInterval, should really refresh
162162
await sleepInMs(2 * 1000 + 1);
163163
await settings.refresh();
164164
expect(listKvRequestCount).eq(2);
165-
expect(getKvRequestCount).eq(1);
165+
expect(getKvRequestCount).eq(2);
166166
expect(settings.get("app.settings.fontColor")).eq("blue");
167167
});
168168

@@ -178,7 +178,7 @@ describe("dynamic refresh", function () {
178178
}
179179
});
180180
expect(listKvRequestCount).eq(1);
181-
expect(getKvRequestCount).eq(0);
181+
expect(getKvRequestCount).eq(1);
182182
expect(settings).not.undefined;
183183
expect(settings.get("app.settings.fontColor")).eq("red");
184184
expect(settings.get("app.settings.fontSize")).eq("40");
@@ -192,7 +192,7 @@ describe("dynamic refresh", function () {
192192
await sleepInMs(2 * 1000 + 1);
193193
await settings.refresh();
194194
expect(listKvRequestCount).eq(2);
195-
expect(getKvRequestCount).eq(2); // one conditional request to detect change and one request as part of loading all kvs (because app.settings.fontColor doesn't exist in the response of listKv request)
195+
expect(getKvRequestCount).eq(2); // one conditional request to detect change
196196
expect(settings.get("app.settings.fontColor")).eq(undefined);
197197
});
198198

@@ -208,7 +208,7 @@ describe("dynamic refresh", function () {
208208
}
209209
});
210210
expect(listKvRequestCount).eq(1);
211-
expect(getKvRequestCount).eq(0);
211+
expect(getKvRequestCount).eq(1);
212212
expect(settings).not.undefined;
213213
expect(settings.get("app.settings.fontColor")).eq("red");
214214
expect(settings.get("app.settings.fontSize")).eq("40");
@@ -217,7 +217,7 @@ describe("dynamic refresh", function () {
217217
await sleepInMs(2 * 1000 + 1);
218218
await settings.refresh();
219219
expect(listKvRequestCount).eq(1);
220-
expect(getKvRequestCount).eq(1);
220+
expect(getKvRequestCount).eq(2);
221221
expect(settings.get("app.settings.fontSize")).eq("40");
222222
});
223223

@@ -234,19 +234,19 @@ describe("dynamic refresh", function () {
234234
}
235235
});
236236
expect(listKvRequestCount).eq(1);
237-
expect(getKvRequestCount).eq(0);
237+
expect(getKvRequestCount).eq(2); // two getKv requests for two watched settings
238238
expect(settings).not.undefined;
239239
expect(settings.get("app.settings.fontColor")).eq("red");
240240
expect(settings.get("app.settings.fontSize")).eq("40");
241241

242242
// change setting
243243
addSetting("app.settings.bgColor", "white");
244-
updateSetting("app.settings.fontSize", "50");
244+
updateSetting("app.settings.fontColor", "blue");
245245
await sleepInMs(2 * 1000 + 1);
246246
await settings.refresh();
247247
expect(listKvRequestCount).eq(2);
248-
expect(getKvRequestCount).eq(2); // two getKv request for two watched settings
249-
expect(settings.get("app.settings.fontSize")).eq("50");
248+
expect(getKvRequestCount).eq(3);
249+
expect(settings.get("app.settings.fontColor")).eq("blue");
250250
expect(settings.get("app.settings.bgColor")).eq("white");
251251
});
252252

@@ -284,7 +284,7 @@ describe("dynamic refresh", function () {
284284
await sleepInMs(2 * 1000 + 1);
285285
await settings.refresh(); // should continue to refresh even if sentinel key doesn't change now
286286
expect(listKvRequestCount).eq(2);
287-
expect(getKvRequestCount).eq(4);
287+
expect(getKvRequestCount).eq(3);
288288
expect(settings.get("app.settings.bgColor")).eq("white");
289289
});
290290

@@ -370,7 +370,7 @@ describe("dynamic refresh", function () {
370370
}
371371
});
372372
expect(listKvRequestCount).eq(1);
373-
expect(getKvRequestCount).eq(1); // app.settings.bgColor doesn't exist in the response of listKv request, so an additional getKv request is made to get it.
373+
expect(getKvRequestCount).eq(1);
374374
expect(settings).not.undefined;
375375
expect(settings.get("app.settings.fontColor")).eq("red");
376376
expect(settings.get("app.settings.fontSize")).eq("40");
@@ -464,7 +464,7 @@ describe("dynamic refresh", function () {
464464
}
465465
});
466466
expect(listKvRequestCount).eq(1);
467-
expect(getKvRequestCount).eq(0);
467+
expect(getKvRequestCount).eq(1);
468468
expect(settings).not.undefined;
469469
expect(settings.get("app.settings.fontColor")).eq("red");
470470

@@ -477,12 +477,12 @@ describe("dynamic refresh", function () {
477477
settings.refresh(); // refresh "concurrently"
478478
}
479479
expect(listKvRequestCount).to.be.at.most(2);
480-
expect(getKvRequestCount).to.be.at.most(1);
480+
expect(getKvRequestCount).to.be.at.most(2);
481481

482482
await sleepInMs(1000); // wait for all 5 refresh attempts to finish
483483

484484
expect(listKvRequestCount).eq(2);
485-
expect(getKvRequestCount).eq(1);
485+
expect(getKvRequestCount).eq(2);
486486
expect(settings.get("app.settings.fontColor")).eq("blue");
487487
});
488488

test/requestTracing.test.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@ import * as chai from "chai";
66
import chaiAsPromised from "chai-as-promised";
77
chai.use(chaiAsPromised);
88
const expect = chai.expect;
9+
import { AppConfigurationClient } from "@azure/app-configuration";
910
import { HttpRequestHeadersPolicy, createMockedConnectionString, createMockedKeyValue, createMockedFeatureFlag, createMockedTokenCredential, mockAppConfigurationClientListConfigurationSettings, restoreMocks, sinon, sleepInMs } from "./utils/testHelper.js";
1011
import { ConfigurationClientManager } from "../src/configurationClientManager.js";
1112
import { load, loadFromAzureFrontDoor } from "../src/index.js";
1213
import { isBrowser } from "../src/requestTracing/utils.js";
14+
import { get } from "http";
1315

1416
const CORRELATION_CONTEXT_HEADER_NAME = "Correlation-Context";
1517

@@ -211,16 +213,25 @@ describe("request tracing", function () {
211213
value: "red"
212214
}].map(createMockedKeyValue)]);
213215

216+
const sentinel = createMockedKeyValue({ key: "sentinel", value: "initial value" });
217+
const getStub = sinon.stub(AppConfigurationClient.prototype, "getConfigurationSetting");
218+
getStub.onCall(0).returns(Promise.resolve({ statusCode: 200, ...sentinel } as any));
219+
214220
const settings = await load(createMockedConnectionString(fakeEndpoint), {
215221
clientOptions,
216222
refreshOptions: {
217223
enabled: true,
218224
refreshIntervalInMs: 1_000,
219225
watchedSettings: [{
220-
key: "app.settings.fontColor"
226+
key: "sentinel"
221227
}]
222228
}
223229
});
230+
231+
expect(settings.get("app.settings.fontColor")).eq("red"); // initial load should succeed
232+
// we only mocked getConfigurationSetting for initial load, so the watch request during refresh will still use the SDK's pipeline, then the headerPolicy can capture the headers
233+
getStub.restore();
234+
224235
await sleepInMs(1_000 + 1_000);
225236
try {
226237
await settings.refresh();

0 commit comments

Comments
 (0)