Skip to content

Commit c094801

Browse files
update
1 parent b0ab944 commit c094801

File tree

2 files changed

+84
-62
lines changed

2 files changed

+84
-62
lines changed

src/AzureAppConfigurationImpl.ts

Lines changed: 83 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,17 @@ import { ETAG_LOOKUP_HEADER } from "./EtagUrlPipelinePolicy.js";
4040

4141
type PagedSettingSelector = SettingSelector & {
4242
pageEtags?: string[];
43+
};
44+
45+
type SettingSelectorCollection = {
46+
selectors: PagedSettingSelector[];
4347

4448
/**
45-
* The etag which has changed after the last refresh. This is used to break the CDN cache.
49+
* The etag which has changed after the last refresh. This is used to append to the request url for breaking the CDN cache.
4650
* It can either be a page etag or etag of a watched setting.
4751
*/
48-
latestEtag?: string;
49-
};
52+
etagToBreakCdnCache?: string;
53+
}
5054

5155
export class AzureAppConfigurationImpl implements AzureAppConfiguration {
5256
/**
@@ -85,22 +89,17 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
8589
/**
8690
* Selectors of key-values obtained from @see AzureAppConfigurationOptions.selectors
8791
*/
88-
#kvSelectors: PagedSettingSelector[] = [];
92+
#kvSelectorCollection: SettingSelectorCollection = { selectors: [] };
8993
/**
9094
* Selectors of feature flags obtained from @see AzureAppConfigurationOptions.featureFlagOptions.selectors
9195
*/
92-
#ffSelectors: PagedSettingSelector[] = [];
96+
#ffSelectorCollection: SettingSelectorCollection = { selectors: [] };
9397

9498
// Load balancing
9599
#lastSuccessfulEndpoint: string = "";
96100

97101
// CDN
98102
#isCdnUsed: boolean;
99-
/**
100-
* The etag of a watched setting which has changed after the last refresh. This is used to break the CDN cache.
101-
* This property will not be used when using key value collection based refresh. It could only be used during updateWatchedKeyValuesEtag and refreshKeyValues.
102-
*/
103-
#latestEtag?: string;
104103

105104
constructor(
106105
clientManager: ConfigurationClientManager,
@@ -145,12 +144,12 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
145144
this.#kvRefreshTimer = new RefreshTimer(this.#kvRefreshInterval);
146145
}
147146

148-
this.#kvSelectors = getValidKeyValueSelectors(options?.selectors);
147+
this.#kvSelectorCollection.selectors = getValidKeyValueSelectors(options?.selectors);
149148

150149
// feature flag options
151150
if (options?.featureFlagOptions?.enabled) {
152151
// validate feature flag selectors
153-
this.#ffSelectors = getValidFeatureFlagSelectors(options.featureFlagOptions.selectors);
152+
this.#ffSelectorCollection.selectors = getValidFeatureFlagSelectors(options.featureFlagOptions.selectors);
154153

155154
if (options.featureFlagOptions.refresh?.enabled) {
156155
const { refreshIntervalInMs } = options.featureFlagOptions.refresh;
@@ -233,19 +232,36 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
233232
*/
234233
async load() {
235234
await this.#loadSelectedAndWatchedKeyValues();
236-
if (this.#watchAll) {
237-
this.#kvSelectors.forEach(selector => selector.latestEtag = selector.pageEtags ? selector.pageEtags[0] : undefined);
238-
} else if (this.#refreshEnabled) {
239-
this.#latestEtag = this.#sentinels.find(s => s.etag !== undefined)?.etag;
240-
}
241235

242236
if (this.#featureFlagEnabled) {
243237
await this.#loadFeatureFlags();
238+
}
239+
240+
if (this.#isCdnUsed) {
241+
if (this.#watchAll) { // collection monitoring based refresh
242+
// use the first page etag of the first kv selector
243+
const defaultSelector = this.#kvSelectorCollection.selectors.find(s => s.pageEtags !== undefined);
244+
if (defaultSelector && defaultSelector.pageEtags!.length > 0) {
245+
this.#kvSelectorCollection.etagToBreakCdnCache = defaultSelector.pageEtags![0];
246+
} else {
247+
this.#kvSelectorCollection.etagToBreakCdnCache = undefined;
248+
}
249+
} else if (this.#refreshEnabled) { // watched settings based refresh
250+
// use the etag of the first watched setting (sentinel)
251+
this.#kvSelectorCollection.etagToBreakCdnCache = this.#sentinels.find(s => s.etag !== undefined)?.etag;
252+
}
253+
244254
if (this.#featureFlagRefreshEnabled) {
245-
this.#ffSelectors.forEach(selector => selector.latestEtag = selector.pageEtags ? selector.pageEtags[0] : undefined);
255+
const defaultSelector = this.#ffSelectorCollection.selectors.find(s => s.pageEtags !== undefined);
256+
if (defaultSelector && defaultSelector.pageEtags!.length > 0) {
257+
this.#ffSelectorCollection.etagToBreakCdnCache = defaultSelector.pageEtags![0];
258+
} else {
259+
this.#ffSelectorCollection.etagToBreakCdnCache = undefined;
260+
}
246261
}
247262
}
248-
// Mark all settings have loaded at startup.
263+
264+
// mark all settings have loaded at startup.
249265
this.#isInitialLoadCompleted = true;
250266
}
251267

@@ -369,12 +385,12 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
369385
* If false, loads key-value using the key-value selectors. Defaults to false.
370386
*/
371387
async #loadConfigurationSettings(loadFeatureFlag: boolean = false): Promise<ConfigurationSetting[]> {
372-
const selectors = loadFeatureFlag ? this.#ffSelectors : this.#kvSelectors;
388+
const selectorCollection = loadFeatureFlag ? this.#ffSelectorCollection : this.#kvSelectorCollection;
373389
const funcToExecute = async (client) => {
374390
const loadedSettings: ConfigurationSetting[] = [];
375391
// deep copy selectors to avoid modification if current client fails
376-
const selectorsToUpdate = JSON.parse(
377-
JSON.stringify(selectors)
392+
const selectorsToUpdate: PagedSettingSelector[] = JSON.parse(
393+
JSON.stringify(selectorCollection.selectors)
378394
);
379395

380396
for (const selector of selectorsToUpdate) {
@@ -383,19 +399,12 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
383399
labelFilter: selector.labelFilter,
384400
};
385401

402+
// If cdn is used, add etag to request header so that the pipeline policy can retrieve and append it to the request URL
386403
if (this.#isCdnUsed) {
387-
// If cdn is used, add etag to request header so that the pipeline policy can retrieve and append it to the request URL
388-
if (this.#watchAll && selector.latestEtag) {
389-
listOptions = {
390-
...listOptions,
391-
requestOptions: { customHeaders: { [ETAG_LOOKUP_HEADER]: selector.latestEtag }}
392-
};
393-
} else if (this.#latestEtag) {
394-
listOptions = {
395-
...listOptions,
396-
requestOptions: { customHeaders: { [ETAG_LOOKUP_HEADER]: this.#latestEtag }}
397-
};
398-
}
404+
listOptions = {
405+
...listOptions,
406+
requestOptions: { customHeaders: { [ETAG_LOOKUP_HEADER]: selectorCollection.etagToBreakCdnCache ?? "" }}
407+
};
399408
}
400409

401410
const pageEtags: string[] = [];
@@ -405,21 +414,21 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
405414
listOptions
406415
).byPage();
407416
for await (const page of pageIterator) {
408-
pageEtags.push(page.etag ?? "");
417+
pageEtags.push(page.etag ?? ""); // pageEtags is string[]
409418
for (const setting of page.items) {
410419
if (loadFeatureFlag === isFeatureFlag(setting)) {
411420
loadedSettings.push(setting);
412421
}
413422
}
414423
}
424+
425+
if (pageEtags.length === 0) {
426+
console.warn(`No page is found in the response of listing key-value selector: key=${selector.keyFilter} and label=${selector.labelFilter}.`);
427+
}
415428
selector.pageEtags = pageEtags;
416429
}
417430

418-
if (loadFeatureFlag) {
419-
this.#ffSelectors = selectorsToUpdate;
420-
} else {
421-
this.#kvSelectors = selectorsToUpdate;
422-
}
431+
selectorCollection.selectors = selectorsToUpdate;
423432
return loadedSettings;
424433
};
425434

@@ -457,15 +466,13 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
457466
if (matchedSetting) {
458467
sentinel.etag = matchedSetting.etag;
459468
} else {
460-
// Send a request to retrieve key-value since it may be either not loaded or loaded with a different label or different casing
469+
// Send a request to retrieve watched key-value since it may be either not loaded or loaded with a different selector
461470
// If cdn is used, add etag to request header so that the pipeline policy can retrieve and append it to the request URL
462-
const getOptions = this.#isCdnUsed && this.#latestEtag ? { requestOptions: { customHeaders: { [ETAG_LOOKUP_HEADER]: this.#latestEtag }}} : {};
471+
const getOptions = this.#isCdnUsed ?
472+
{ requestOptions: { customHeaders: { [ETAG_LOOKUP_HEADER]: this.#kvSelectorCollection.etagToBreakCdnCache ?? "" } } } :
473+
{};
463474
const response = await this.#getConfigurationSetting(sentinel, {...getOptions, onlyIfChanged: false}); // always send non-conditional request
464-
if (response) {
465-
sentinel.etag = response.etag;
466-
} else {
467-
sentinel.etag = undefined;
468-
}
475+
sentinel.etag = response?.etag;
469476
}
470477
}
471478
}
@@ -510,18 +517,20 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
510517
// try refresh if any of watched settings is changed.
511518
let needRefresh = false;
512519
if (this.#watchAll) {
513-
needRefresh = await this.#checkConfigurationSettingsChange(this.#kvSelectors);
520+
needRefresh = await this.#checkConfigurationSettingsChange(this.#kvSelectorCollection);
514521
}
515522
for (const sentinel of this.#sentinels.values()) {
516523
// If cdn is used, add etag to request header so that the pipeline policy can retrieve and append it to the request URL
517-
const getOptions = this.#isCdnUsed && this.#latestEtag ? { requestOptions: { customHeaders: { [ETAG_LOOKUP_HEADER]: this.#latestEtag }}} : {};
518-
const response = await this.#getConfigurationSetting(sentinel, {...getOptions, onlyIfChanged: !this.#isCdnUsed}); // if CDN is used, do not send conditional request
524+
const getOptions = this.#isCdnUsed ?
525+
{ requestOptions: { customHeaders: { [ETAG_LOOKUP_HEADER]: this.#kvSelectorCollection.etagToBreakCdnCache ?? "" } } } :
526+
{};
527+
const response = await this.#getConfigurationSetting(sentinel, { ...getOptions, onlyIfChanged: !this.#isCdnUsed }); // if CDN is used, do not send conditional request
519528

520529
if ((response?.statusCode === 200 && sentinel.etag !== response?.etag) ||
521530
(response === undefined && sentinel.etag !== undefined) // deleted
522531
) {
523532
sentinel.etag = response?.etag;// update etag of the sentinel
524-
this.#latestEtag = response?.etag; // record the last changed etag
533+
this.#kvSelectorCollection.etagToBreakCdnCache = sentinel.etag;
525534
needRefresh = true;
526535
break;
527536
}
@@ -545,7 +554,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
545554
return Promise.resolve(false);
546555
}
547556

548-
const needRefresh = await this.#checkConfigurationSettingsChange(this.#ffSelectors);
557+
const needRefresh = await this.#checkConfigurationSettingsChange(this.#ffSelectorCollection);
549558
if (needRefresh) {
550559
await this.#loadFeatureFlags();
551560
}
@@ -556,40 +565,53 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
556565

557566
/**
558567
* Checks whether the key-value collection has changed.
559-
* @param selectors - The @see PagedSettingSelector of the kev-value collection.
568+
* @param selectorCollection - The @see SettingSelectorCollection of the kev-value collection.
560569
* @returns true if key-value collection has changed, false otherwise.
561570
*/
562-
async #checkConfigurationSettingsChange(selectors: PagedSettingSelector[]): Promise<boolean> {
571+
async #checkConfigurationSettingsChange(selectorCollection: SettingSelectorCollection): Promise<boolean> {
563572
const funcToExecute = async (client) => {
564-
for (const selector of selectors) {
565-
const listOptions: ListConfigurationSettingsOptions = {
573+
for (const selector of selectorCollection.selectors) {
574+
let listOptions: ListConfigurationSettingsOptions = {
566575
keyFilter: selector.keyFilter,
567-
labelFilter: selector.labelFilter,
568-
...(!this.#isCdnUsed && { pageEtags: selector.pageEtags }), // if CDN is used, do not send conditional request
569-
// If cdn is used, add etag to request header so that the pipeline policy can retrieve and append it to the request URL
570-
...(this.#isCdnUsed && selector.latestEtag && { requestOptions: { customHeaders: { [ETAG_LOOKUP_HEADER]: selector.latestEtag }}})
576+
labelFilter: selector.labelFilter
571577
};
572578

579+
if (this.#isCdnUsed) {
580+
// If cdn is used, add etag to request header so that the pipeline policy can retrieve and append it to the request URL
581+
listOptions = {
582+
...listOptions,
583+
requestOptions: { customHeaders: { [ETAG_LOOKUP_HEADER]: selectorCollection.etagToBreakCdnCache ?? "" } }};
584+
} else {
585+
// send conditional request if cdn is not used
586+
listOptions = { ...listOptions, pageEtags: selector.pageEtags };
587+
}
588+
573589
const pageIterator = listConfigurationSettingsWithTrace(
574590
this.#requestTraceOptions,
575591
client,
576592
listOptions
577593
).byPage();
578594

579595
if (selector.pageEtags === undefined || selector.pageEtags.length === 0) {
596+
selectorCollection.etagToBreakCdnCache = undefined;
580597
return true; // no etag, always refresh
581598
}
582599

583600
let i = 0;
584601
for await (const page of pageIterator) {
585602
if (i > selector.pageEtags.length + 1 || // new page
586603
(page._response.status === 200 && page.etag !== selector.pageEtags[i])) { // page changed
587-
selector.latestEtag = page.etag; // record the last changed etag
604+
if (this.#isCdnUsed) {
605+
selectorCollection.etagToBreakCdnCache = page.etag;
606+
}
588607
return true;
589608
}
590609
i++;
591610
}
592611
if (i !== selector.pageEtags.length) { // page removed
612+
if (this.#isCdnUsed) {
613+
selectorCollection.etagToBreakCdnCache = selector.pageEtags[i];
614+
}
593615
return true;
594616
}
595617
}

src/EtagUrlPipelinePolicy.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export class EtagUrlPipelinePolicy implements PipelinePolicy {
2020
request.headers.delete(ETAG_LOOKUP_HEADER);
2121

2222
const url = new URL(request.url);
23-
url.searchParams.append("etag", etag);
23+
url.searchParams.append("_", etag); // _ is a dummy query parameter to break the CDN cache
2424
request.url = url.toString();
2525
}
2626

0 commit comments

Comments
 (0)