Skip to content

Commit 4ec140a

Browse files
committed
list feature flags with pageEtags
1 parent 6411a30 commit 4ec140a

File tree

2 files changed

+77
-28
lines changed

2 files changed

+77
-28
lines changed

src/AzureAppConfigurationImpl.ts

Lines changed: 76 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,13 @@ import { RefreshTimer } from "./refresh/RefreshTimer";
1515
import { getConfigurationSettingWithTrace, listConfigurationSettingsWithTrace, requestTracingEnabled } from "./requestTracing/utils";
1616
import { KeyFilter, LabelFilter, SettingSelector } from "./types";
1717

18+
type PagedSettingSelector = SettingSelector & {
19+
/**
20+
* Key: page eTag, Value: feature flag configurations
21+
*/
22+
pageEtags?: string[];
23+
};
24+
1825
export class AzureAppConfigurationImpl implements AzureAppConfiguration {
1926
/**
2027
* Hosting key-value pairs in the configuration store.
@@ -45,6 +52,9 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
4552
#featureFlagRefreshInterval: number = DEFAULT_REFRESH_INTERVAL_IN_MS;
4653
#featureFlagRefreshTimer: RefreshTimer;
4754

55+
// selectors
56+
#featureFlagSelectors: PagedSettingSelector[] = [];
57+
4858
constructor(
4959
client: AppConfigurationClient,
5060
options: AzureAppConfigurationOptions | undefined
@@ -90,19 +100,23 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
90100
}
91101

92102
// feature flag options
93-
if (options?.featureFlagOptions?.enabled && options.featureFlagOptions.refresh?.enabled) {
94-
const { refreshIntervalInMs } = options.featureFlagOptions.refresh;
95-
96-
// custom refresh interval
97-
if (refreshIntervalInMs !== undefined) {
98-
if (refreshIntervalInMs < MIN_REFRESH_INTERVAL_IN_MS) {
99-
throw new Error(`The feature flag refresh interval cannot be less than ${MIN_REFRESH_INTERVAL_IN_MS} milliseconds.`);
100-
} else {
101-
this.#featureFlagRefreshInterval = refreshIntervalInMs;
103+
if (options?.featureFlagOptions?.enabled) {
104+
// validate feature flag selectors
105+
this.#featureFlagSelectors = getValidFeatureFlagSelectors(options.featureFlagOptions.selectors);
106+
107+
if (options.featureFlagOptions.refresh?.enabled) {
108+
const { refreshIntervalInMs } = options.featureFlagOptions.refresh;
109+
// custom refresh interval
110+
if (refreshIntervalInMs !== undefined) {
111+
if (refreshIntervalInMs < MIN_REFRESH_INTERVAL_IN_MS) {
112+
throw new Error(`The feature flag refresh interval cannot be less than ${MIN_REFRESH_INTERVAL_IN_MS} milliseconds.`);
113+
} else {
114+
this.#featureFlagRefreshInterval = refreshIntervalInMs;
115+
}
102116
}
103-
}
104117

105-
this.#featureFlagRefreshTimer = new RefreshTimer(this.#featureFlagRefreshInterval);
118+
this.#featureFlagRefreshTimer = new RefreshTimer(this.#featureFlagRefreshInterval);
119+
}
106120
}
107121

108122
this.#adapters.push(new AzureKeyVaultKeyValueAdapter(options?.keyVaultOptions));
@@ -233,7 +247,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
233247
}
234248

235249
async #clearLoadedKeyValues() {
236-
for(const key of this.#configMap.keys()) {
250+
for (const key of this.#configMap.keys()) {
237251
if (key !== FEATURE_MANAGEMENT_KEY_NAME) {
238252
this.#configMap.delete(key);
239253
}
@@ -243,22 +257,31 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
243257
async #loadFeatureFlags() {
244258
// Temporary map to store feature flags, key is the key of the setting, value is the raw value of the setting
245259
const featureFlagsMap = new Map<string, any>();
246-
const featureFlagSelectors = getValidFeatureFlagSelectors(this.#options?.featureFlagOptions?.selectors);
247-
for (const selector of featureFlagSelectors) {
260+
for (const selector of this.#featureFlagSelectors) {
248261
const listOptions: ListConfigurationSettingsOptions = {
249262
keyFilter: `${featureFlagPrefix}${selector.keyFilter}`,
250263
labelFilter: selector.labelFilter
251264
};
252-
const settings = listConfigurationSettingsWithTrace(
265+
266+
const pageEtags: string[] = [];
267+
const pageIterator = listConfigurationSettingsWithTrace(
253268
this.#requestTraceOptions,
254269
this.#client,
255270
listOptions
256-
);
257-
for await (const setting of settings) {
258-
if (isFeatureFlag(setting)) {
259-
featureFlagsMap.set(setting.key, setting.value);
271+
).byPage();
272+
for await (const page of pageIterator) {
273+
if (page._response.status === 200) {
274+
if (page.etag) {
275+
pageEtags.push(page.etag);
276+
}
277+
}
278+
for (const setting of page.items) {
279+
if (isFeatureFlag(setting)) {
280+
featureFlagsMap.set(setting.key, setting.value);
281+
}
260282
}
261283
}
284+
selector.pageEtags = pageEtags;
262285
}
263286

264287
// parse feature flags
@@ -410,16 +433,41 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
410433
return Promise.resolve(false);
411434
}
412435

413-
try {
414-
// TODO: instead of refreshing all feature flags, only refresh the changed ones with etag
415-
await this.#loadFeatureFlags();
416-
this.#featureFlagRefreshTimer.reset();
417-
} catch (error) {
418-
// if refresh failed, backoff
419-
this.#featureFlagRefreshTimer.backoff();
420-
throw error;
436+
// check if any feature flag is changed
437+
let needRefresh = false;
438+
for (const selector of this.#featureFlagSelectors) {
439+
const listOptions: ListConfigurationSettingsOptions = {
440+
keyFilter: `${featureFlagPrefix}${selector.keyFilter}`,
441+
labelFilter: selector.labelFilter,
442+
pageEtags: selector.pageEtags
443+
};
444+
const pageIterator = listConfigurationSettingsWithTrace(
445+
this.#requestTraceOptions,
446+
this.#client,
447+
listOptions
448+
).byPage();
449+
for await (const page of pageIterator) {
450+
if (page._response.status === 200) { // created or changed
451+
needRefresh = true;
452+
break;
453+
}
454+
// TODO: handle page deleted?
455+
}
421456
}
422-
return Promise.resolve(true);
457+
458+
if (needRefresh) {
459+
try {
460+
await this.#loadFeatureFlags();
461+
this.#featureFlagRefreshTimer.reset();
462+
} catch (error) {
463+
// if refresh failed, backoff
464+
this.#featureFlagRefreshTimer.backoff();
465+
throw error;
466+
}
467+
return Promise.resolve(true);
468+
}
469+
470+
return Promise.resolve(false);
423471
}
424472

425473
onRefresh(listener: () => any, thisArg?: any): Disposable {

test/utils/testHelper.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ const TEST_CLIENT_ID = "00000000-0000-0000-0000-000000000000";
1414
const TEST_TENANT_ID = "00000000-0000-0000-0000-000000000000";
1515
const TEST_CLIENT_SECRET = "0000000000000000000000000000000000000000";
1616

17+
// TODO: mock client.listConfigurationSettings().byPage() to test pagination
1718
function mockAppConfigurationClientListConfigurationSettings(kvList: ConfigurationSetting[]) {
1819
function* testKvSetGenerator(kvs: any[]) {
1920
yield* kvs;

0 commit comments

Comments
 (0)