Skip to content

Commit fdd30e2

Browse files
refresh based on page etag
1 parent 7db665f commit fdd30e2

File tree

3 files changed

+83
-83
lines changed

3 files changed

+83
-83
lines changed

rollup.config.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import dts from "rollup-plugin-dts";
44

55
export default [
66
{
7-
external: ["@azure/app-configuration", "@azure/keyvault-secrets", "@azure/core-rest-pipeline", "crypto"],
7+
external: ["@azure/app-configuration", "@azure/keyvault-secrets", "@azure/core-rest-pipeline", "crypto", "dns/promises"],
88
input: "src/index.ts",
99
output: [
1010
{

src/AzureAppConfigurationImpl.ts

Lines changed: 77 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
7676
#featureFlagRefreshTimer: RefreshTimer;
7777

7878
// selectors
79+
#keyValueSelectors: PagedSettingSelector[] = [];
7980
#featureFlagSelectors: PagedSettingSelector[] = [];
8081

8182
constructor(
@@ -93,35 +94,38 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
9394
}
9495

9596
if (options?.refreshOptions?.enabled) {
96-
const { watchedSettings, refreshIntervalInMs } = options.refreshOptions;
97-
// validate watched settings
98-
if (watchedSettings === undefined || watchedSettings.length === 0) {
99-
throw new Error("Refresh is enabled but no watched settings are specified.");
97+
const { watchedSettings, refreshIntervalInMs, watchAll } = options.refreshOptions;
98+
// validate refresh options
99+
if (watchAll !== true) {
100+
if (watchedSettings === undefined || watchedSettings.length === 0) {
101+
throw new Error("Refresh is enabled but no watched settings are specified.");
102+
} else {
103+
for (const setting of watchedSettings) {
104+
if (setting.key.includes("*") || setting.key.includes(",")) {
105+
throw new Error("The characters '*' and ',' are not supported in key of watched settings.");
106+
}
107+
if (setting.label?.includes("*") || setting.label?.includes(",")) {
108+
throw new Error("The characters '*' and ',' are not supported in label of watched settings.");
109+
}
110+
this.#sentinels.push(setting);
111+
}
112+
}
113+
} else if (watchedSettings && watchedSettings.length > 0) {
114+
throw new Error("Watched settings should not be specified when registerAll is enabled.");
100115
}
101-
102116
// custom refresh interval
103117
if (refreshIntervalInMs !== undefined) {
104118
if (refreshIntervalInMs < MIN_REFRESH_INTERVAL_IN_MS) {
105119
throw new Error(`The refresh interval cannot be less than ${MIN_REFRESH_INTERVAL_IN_MS} milliseconds.`);
106-
107120
} else {
108121
this.#refreshInterval = refreshIntervalInMs;
109122
}
110123
}
111-
112-
for (const setting of watchedSettings) {
113-
if (setting.key.includes("*") || setting.key.includes(",")) {
114-
throw new Error("The characters '*' and ',' are not supported in key of watched settings.");
115-
}
116-
if (setting.label?.includes("*") || setting.label?.includes(",")) {
117-
throw new Error("The characters '*' and ',' are not supported in label of watched settings.");
118-
}
119-
this.#sentinels.push(setting);
120-
}
121-
122124
this.#refreshTimer = new RefreshTimer(this.#refreshInterval);
123125
}
124126

127+
this.#keyValueSelectors = getValidKeyValueSelectors(options?.selectors);
128+
125129
// feature flag options
126130
if (options?.featureFlagOptions?.enabled) {
127131
// validate feature flag selectors
@@ -184,6 +188,10 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
184188
return !!this.#options?.refreshOptions?.enabled;
185189
}
186190

191+
get #watchAll(): boolean {
192+
return !!this.#options?.refreshOptions?.watchAll;
193+
}
194+
187195
get #featureFlagEnabled(): boolean {
188196
return !!this.#options?.featureFlagOptions?.enabled;
189197
}
@@ -228,29 +236,42 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
228236
throw new Error("All clients failed to get configuration settings.");
229237
}
230238

231-
async #loadSelectedKeyValues(): Promise<ConfigurationSetting[]> {
232-
// validate selectors
233-
const selectors = getValidKeyValueSelectors(this.#options?.selectors);
234-
239+
async #loadConfigurationSettings(loadFeatureFlag: boolean = false): Promise<ConfigurationSetting[]> {
240+
const selectors = loadFeatureFlag ? this.#featureFlagSelectors : this.#keyValueSelectors;
235241
const funcToExecute = async (client) => {
236242
const loadedSettings: ConfigurationSetting[] = [];
237-
for (const selector of selectors) {
243+
// deep copy selectors to avoid modification if current client fails
244+
const selectorsToUpdate = JSON.parse(
245+
JSON.stringify(selectors)
246+
);
247+
248+
for (const selector of selectorsToUpdate) {
238249
const listOptions: ListConfigurationSettingsOptions = {
239250
keyFilter: selector.keyFilter,
240251
labelFilter: selector.labelFilter
241252
};
242253

243-
const settings = listConfigurationSettingsWithTrace(
254+
const pageEtags: string[] = [];
255+
const pageIterator = listConfigurationSettingsWithTrace(
244256
this.#requestTraceOptions,
245257
client,
246258
listOptions
247-
);
248-
249-
for await (const setting of settings) {
250-
if (!isFeatureFlag(setting)) { // exclude feature flags
251-
loadedSettings.push(setting);
259+
).byPage();
260+
for await (const page of pageIterator) {
261+
pageEtags.push(page.etag ?? "");
262+
for (const setting of page.items) {
263+
if (loadFeatureFlag === isFeatureFlag(setting)) {
264+
loadedSettings.push(setting);
265+
}
252266
}
253267
}
268+
selector.pageEtags = pageEtags;
269+
}
270+
271+
if (loadFeatureFlag) {
272+
this.#featureFlagSelectors = selectorsToUpdate;
273+
} else {
274+
this.#keyValueSelectors = selectorsToUpdate;
254275
}
255276
return loadedSettings;
256277
};
@@ -262,10 +283,6 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
262283
* Update 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.
263284
*/
264285
async #updateWatchedKeyValuesEtag(existingSettings: ConfigurationSetting[]): Promise<void> {
265-
if (!this.#refreshEnabled) {
266-
return;
267-
}
268-
269286
for (const sentinel of this.#sentinels) {
270287
const matchedSetting = existingSettings.find(s => s.key === sentinel.key && s.label === sentinel.label);
271288
if (matchedSetting) {
@@ -285,8 +302,10 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
285302

286303
async #loadSelectedAndWatchedKeyValues() {
287304
const keyValues: [key: string, value: unknown][] = [];
288-
const loadedSettings = await this.#loadSelectedKeyValues();
289-
await this.#updateWatchedKeyValuesEtag(loadedSettings);
305+
const loadedSettings = await this.#loadConfigurationSettings();
306+
if (this.#refreshEnabled && !this.#watchAll) {
307+
await this.#updateWatchedKeyValuesEtag(loadedSettings);
308+
}
290309

291310
// process key-values, watched settings have higher priority
292311
for (const setting of loadedSettings) {
@@ -309,42 +328,8 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
309328
}
310329

311330
async #loadFeatureFlags() {
312-
// Temporary map to store feature flags, key is the key of the setting, value is the raw value of the setting
313-
const funcToExecute = async (client) => {
314-
const featureFlagSettings: ConfigurationSetting[] = [];
315-
// deep copy selectors to avoid modification if current client fails
316-
const selectors = JSON.parse(
317-
JSON.stringify(this.#featureFlagSelectors)
318-
);
319-
320-
for (const selector of selectors) {
321-
const listOptions: ListConfigurationSettingsOptions = {
322-
keyFilter: `${featureFlagPrefix}${selector.keyFilter}`,
323-
labelFilter: selector.labelFilter
324-
};
325-
326-
const pageEtags: string[] = [];
327-
const pageIterator = listConfigurationSettingsWithTrace(
328-
this.#requestTraceOptions,
329-
client,
330-
listOptions
331-
).byPage();
332-
for await (const page of pageIterator) {
333-
pageEtags.push(page.etag ?? "");
334-
for (const setting of page.items) {
335-
if (isFeatureFlag(setting)) {
336-
featureFlagSettings.push(setting);
337-
}
338-
}
339-
}
340-
selector.pageEtags = pageEtags;
341-
}
342-
343-
this.#featureFlagSelectors = selectors;
344-
return featureFlagSettings;
345-
};
346-
347-
const featureFlagSettings = await this.#executeWithFailoverPolicy(funcToExecute) as ConfigurationSetting[];
331+
const loadFeatureFlag = true;
332+
const featureFlagSettings = await this.#loadConfigurationSettings(loadFeatureFlag);
348333

349334
// parse feature flags
350335
const featureFlags = await Promise.all(
@@ -458,6 +443,9 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
458443

459444
// try refresh if any of watched settings is changed.
460445
let needRefresh = false;
446+
if (this.#watchAll) {
447+
needRefresh = await this.#checkKeyValueCollectionChanged(this.#keyValueSelectors);
448+
}
461449
for (const sentinel of this.#sentinels.values()) {
462450
const response = await this.#getConfigurationSetting(sentinel, {
463451
onlyIfChanged: true
@@ -490,11 +478,20 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
490478
return Promise.resolve(false);
491479
}
492480

493-
// check if any feature flag is changed
481+
const needRefresh = await this.#checkKeyValueCollectionChanged(this.#featureFlagSelectors);
482+
if (needRefresh) {
483+
await this.#loadFeatureFlags();
484+
}
485+
486+
this.#featureFlagRefreshTimer.reset();
487+
return Promise.resolve(needRefresh);
488+
}
489+
490+
async #checkKeyValueCollectionChanged(selectors: PagedSettingSelector[]): Promise<boolean> {
494491
const funcToExecute = async (client) => {
495-
for (const selector of this.#featureFlagSelectors) {
492+
for (const selector of selectors) {
496493
const listOptions: ListConfigurationSettingsOptions = {
497-
keyFilter: `${featureFlagPrefix}${selector.keyFilter}`,
494+
keyFilter: selector.keyFilter,
498495
labelFilter: selector.labelFilter,
499496
pageEtags: selector.pageEtags
500497
};
@@ -514,13 +511,8 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
514511
return false;
515512
};
516513

517-
const needRefresh: boolean = await this.#executeWithFailoverPolicy(funcToExecute);
518-
if (needRefresh) {
519-
await this.#loadFeatureFlags();
520-
}
521-
522-
this.#featureFlagRefreshTimer.reset();
523-
return Promise.resolve(needRefresh);
514+
const isChanged = await this.#executeWithFailoverPolicy(funcToExecute);
515+
return isChanged;
524516
}
525517

526518
onRefresh(listener: () => any, thisArg?: any): Disposable {
@@ -813,18 +805,21 @@ function getValidSelectors(selectors: SettingSelector[]): SettingSelector[] {
813805
}
814806

815807
function getValidKeyValueSelectors(selectors?: SettingSelector[]): SettingSelector[] {
816-
if (!selectors || selectors.length === 0) {
808+
if (selectors === undefined || selectors.length === 0) {
817809
// Default selector: key: *, label: \0
818810
return [{ keyFilter: KeyFilter.Any, labelFilter: LabelFilter.Null }];
819811
}
820812
return getValidSelectors(selectors);
821813
}
822814

823815
function getValidFeatureFlagSelectors(selectors?: SettingSelector[]): SettingSelector[] {
824-
if (!selectors || selectors.length === 0) {
816+
if (selectors === undefined || selectors.length === 0) {
825817
// selectors must be explicitly provided.
826818
throw new Error("Feature flag selectors must be provided.");
827819
} else {
820+
selectors.forEach(selector => {
821+
selector.keyFilter = `${featureFlagPrefix}${selector.keyFilter}`;
822+
});
828823
return getValidSelectors(selectors);
829824
}
830825
}

src/RefreshOptions.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ export interface RefreshOptions {
2424
* Any modifications to watched settings will refresh all settings loaded by the configuration provider when refresh() is called.
2525
*/
2626
watchedSettings?: WatchedSetting[];
27+
28+
/**
29+
* Specifies whether all configuration settings will be watched for changes on the server.
30+
*/
31+
watchAll?: boolean;
2732
}
2833

2934
export interface FeatureFlagRefreshOptions {

0 commit comments

Comments
 (0)