Skip to content

Commit 5d80ccc

Browse files
committed
load watched settings if not coverred by selectors
1 parent 506c8a6 commit 5d80ccc

File tree

2 files changed

+123
-35
lines changed

2 files changed

+123
-35
lines changed

src/AzureAppConfigurationImpl.ts

Lines changed: 85 additions & 34 deletions
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 { AppConfigurationClient, ConfigurationSetting, ConfigurationSettingId, GetConfigurationSettingResponse, ListConfigurationSettingsOptions } from "@azure/app-configuration";
4+
import { AppConfigurationClient, ConfigurationSetting, ConfigurationSettingId, GetConfigurationSettingOptions, GetConfigurationSettingResponse, ListConfigurationSettingsOptions } from "@azure/app-configuration";
55
import { RestError } from "@azure/core-rest-pipeline";
66
import { AzureAppConfiguration } from "./AzureAppConfiguration";
77
import { AzureAppConfigurationOptions } from "./AzureAppConfigurationOptions";
@@ -15,6 +15,11 @@ import { CorrelationContextHeaderName, RequestType } from "./requestTracing/cons
1515
import { createCorrelationContextHeader, requestTracingEnabled } from "./requestTracing/utils";
1616
import { KeyFilter, LabelFilter, SettingSelector } from "./types";
1717

18+
type KeyValueIdentifier = string; // key::label
19+
function toKeyValueIderntifier(key: string, label: string | undefined): KeyValueIdentifier {
20+
return `${key}::${label ?? ""}`;
21+
}
22+
1823
export class AzureAppConfigurationImpl extends Map<string, any> implements AzureAppConfiguration {
1924
#adapters: IKeyValueAdapter[] = [];
2025
/**
@@ -29,7 +34,7 @@ export class AzureAppConfigurationImpl extends Map<string, any> implements Azure
2934
// Refresh
3035
#refreshInterval: number = DefaultRefreshIntervalInMs;
3136
#onRefreshListeners: Array<() => any> = [];
32-
#sentinels: ConfigurationSettingId[];
37+
#sentinels: Map<KeyValueIdentifier, ConfigurationSettingId> = new Map();
3338
#refreshTimer: RefreshTimer;
3439

3540
constructor(
@@ -64,15 +69,16 @@ export class AzureAppConfigurationImpl extends Map<string, any> implements Azure
6469
}
6570
}
6671

67-
this.#sentinels = watchedSettings.map(setting => {
72+
for (const setting of watchedSettings) {
6873
if (setting.key.includes("*") || setting.key.includes(",")) {
6974
throw new Error("The characters '*' and ',' are not supported in key of watched settings.");
7075
}
7176
if (setting.label?.includes("*") || setting.label?.includes(",")) {
7277
throw new Error("The characters '*' and ',' are not supported in label of watched settings.");
7378
}
74-
return { ...setting };
75-
});
79+
const id = toKeyValueIderntifier(setting.key, setting.label);
80+
this.#sentinels.set(id, setting);
81+
}
7682

7783
this.#refreshTimer = new RefreshTimer(this.#refreshInterval);
7884
}
@@ -88,8 +94,8 @@ export class AzureAppConfigurationImpl extends Map<string, any> implements Azure
8894
return !!this.#options?.refreshOptions?.enabled;
8995
}
9096

91-
async load(requestType: RequestType = RequestType.Startup) {
92-
const keyValues: [key: string, value: unknown][] = [];
97+
async #loadSelectedKeyValues(requestType: RequestType): Promise<Map<KeyValueIdentifier, ConfigurationSetting>> {
98+
const loadedSettings = new Map<string, ConfigurationSetting>();
9399

94100
// validate selectors
95101
const selectors = getValidSelectors(this.#options?.selectors);
@@ -108,19 +114,57 @@ export class AzureAppConfigurationImpl extends Map<string, any> implements Azure
108114
const settings = this.#client.listConfigurationSettings(listOptions);
109115

110116
for await (const setting of settings) {
111-
if (setting.key) {
112-
const keyValuePair = await this.#processKeyValues(setting);
113-
keyValues.push(keyValuePair);
114-
}
115-
// update etag of sentinels if refresh is enabled
116-
if (this.#refreshEnabled) {
117-
const matchedSentinel = this.#sentinels.find(s => s.key === setting.key && s.label === setting.label);
118-
if (matchedSentinel) {
119-
matchedSentinel.etag = setting.etag;
120-
}
117+
const id = toKeyValueIderntifier(setting.key, setting.label);
118+
loadedSettings.set(id, setting);
119+
}
120+
}
121+
return loadedSettings;
122+
}
123+
124+
/**
125+
* Load watched key-values from Azure App Configuration service if not coverred by the selectors. Update etag of sentinels.
126+
*/
127+
async #loadWatchedKeyValues(requestType: RequestType, existingSettings: Map<KeyValueIdentifier, ConfigurationSetting>): Promise<Map<KeyValueIdentifier, ConfigurationSetting>> {
128+
const watchedSettings = new Map<KeyValueIdentifier, ConfigurationSetting>();
129+
130+
if (!this.#refreshEnabled) {
131+
return watchedSettings;
132+
}
133+
134+
for (const [id, sentinel] of this.#sentinels) {
135+
const matchedSetting = existingSettings.get(id);
136+
if (matchedSetting) {
137+
watchedSettings.set(id, matchedSetting);
138+
sentinel.etag = matchedSetting.etag;
139+
} else {
140+
// Send a request to retrieve key-value since it may be either not loaded or loaded with a different label or different casing
141+
const { key, label } = sentinel;
142+
const response = await this.#getConfigurationSettingWithTrace(requestType, { key, label });
143+
if (response) {
144+
watchedSettings.set(id, response);
145+
sentinel.etag = response.etag;
146+
} else {
147+
sentinel.etag = undefined;
121148
}
122149
}
123150
}
151+
return watchedSettings;
152+
}
153+
154+
async load(requestType: RequestType = RequestType.Startup) {
155+
const keyValues: [key: string, value: unknown][] = [];
156+
157+
const loadedSettings = await this.#loadSelectedKeyValues(requestType);
158+
const watchedSettings = await this.#loadWatchedKeyValues(requestType, loadedSettings);
159+
160+
// process key-values, watched settings have higher priority
161+
for (const setting of [...loadedSettings.values(), ...watchedSettings.values()]) {
162+
if (setting.key) {
163+
const [key, value] = await this.#processKeyValues(setting);
164+
keyValues.push([key, value]);
165+
}
166+
}
167+
124168
this.clear(); // clear existing key-values in case of configuration setting deletion
125169
for (const [k, v] of keyValues) {
126170
this.set(k, v);
@@ -142,22 +186,10 @@ export class AzureAppConfigurationImpl extends Map<string, any> implements Azure
142186

143187
// try refresh if any of watched settings is changed.
144188
let needRefresh = false;
145-
for (const sentinel of this.#sentinels) {
146-
let response: GetConfigurationSettingResponse | undefined;
147-
try {
148-
response = await this.#client.getConfigurationSetting(sentinel, {
149-
onlyIfChanged: true,
150-
requestOptions: {
151-
customHeaders: this.#customHeaders(RequestType.Watch)
152-
}
153-
});
154-
} catch (error) {
155-
if (error instanceof RestError && error.statusCode === 404) {
156-
response = undefined;
157-
} else {
158-
throw error;
159-
}
160-
}
189+
for (const sentinel of this.#sentinels.values()) {
190+
const response = await this.#getConfigurationSettingWithTrace(RequestType.Watch, sentinel, {
191+
onlyIfChanged: true
192+
});
161193

162194
if (response === undefined || response.statusCode === 200) {
163195
// sentinel deleted / changed.
@@ -235,6 +267,25 @@ export class AzureAppConfigurationImpl extends Map<string, any> implements Azure
235267
headers[CorrelationContextHeaderName] = createCorrelationContextHeader(this.#options, requestType);
236268
return headers;
237269
}
270+
271+
async #getConfigurationSettingWithTrace(requestType: RequestType, configurationSettingId: ConfigurationSettingId, options?: GetConfigurationSettingOptions): Promise<GetConfigurationSettingResponse | undefined> {
272+
let response: GetConfigurationSettingResponse | undefined;
273+
try {
274+
response = await this.#client.getConfigurationSetting(configurationSettingId, {
275+
...options ?? {},
276+
requestOptions: {
277+
customHeaders: this.#customHeaders(requestType)
278+
}
279+
});
280+
} catch (error) {
281+
if (error instanceof RestError && error.statusCode === 404) {
282+
response = undefined;
283+
} else {
284+
throw error;
285+
}
286+
}
287+
return response;
288+
}
238289
}
239290

240291
function getValidSelectors(selectors?: SettingSelector[]) {
@@ -266,4 +317,4 @@ function getValidSelectors(selectors?: SettingSelector[]) {
266317
}
267318
return selector;
268319
});
269-
}
320+
}

test/refresh.test.ts

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ describe("dynamic refresh", function () {
2828
beforeEach(() => {
2929
mockedKVs = [
3030
{ value: "red", key: "app.settings.fontColor" },
31-
{ value: "40", key: "app.settings.fontSize" }
31+
{ value: "40", key: "app.settings.fontSize" },
32+
{ value: "30", key: "app.settings.fontSize", label: "prod" }
3233
].map(createMockedKeyValue);
3334
mockAppConfigurationClientListConfigurationSettings(mockedKVs);
3435
mockAppConfigurationClientGetConfigurationSetting(mockedKVs)
@@ -255,4 +256,40 @@ describe("dynamic refresh", function () {
255256
expect(count).eq(1);
256257
});
257258

259+
it("should also load watched settings if not specified in selectors", async () => {
260+
const connectionString = createMockedConnectionString();
261+
const settings = await load(connectionString, {
262+
selectors: [
263+
{ keyFilter: "app.settings.fontColor" }
264+
],
265+
refreshOptions: {
266+
enabled: true,
267+
refreshIntervalInMs: 2000,
268+
watchedSettings: [
269+
{ key: "app.settings.fontSize" }
270+
]
271+
}
272+
});
273+
expect(settings).not.undefined;
274+
expect(settings.get("app.settings.fontColor")).eq("red");
275+
expect(settings.get("app.settings.fontSize")).eq("40");
276+
});
277+
278+
it("watched settings have higher priority than selectors", async () => {
279+
const connectionString = createMockedConnectionString();
280+
const settings = await load(connectionString, {
281+
selectors: [
282+
{ keyFilter: "app.settings.*" }
283+
],
284+
refreshOptions: {
285+
enabled: true,
286+
refreshIntervalInMs: 2000,
287+
watchedSettings: [
288+
{ key: "app.settings.fontSize", label: "prod" }
289+
]
290+
}
291+
});
292+
expect(settings).not.undefined;
293+
expect(settings.get("app.settings.fontSize")).eq("30");
294+
});
258295
});

0 commit comments

Comments
 (0)