Skip to content

Commit 047ad58

Browse files
Merge branch 'preview' of https://github.com/Azure/AppConfiguration-JavaScriptProvider into zhiyuanliang/enforce-api-version-for-cdn
2 parents 26ff70f + 6d1b6a1 commit 047ad58

17 files changed

+306
-121
lines changed

src/AzureAppConfigurationImpl.ts

Lines changed: 99 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { IKeyValueAdapter } from "./IKeyValueAdapter.js";
2222
import { JsonKeyValueAdapter } from "./JsonKeyValueAdapter.js";
2323
import { DEFAULT_STARTUP_TIMEOUT_IN_MS } from "./StartupOptions.js";
2424
import { DEFAULT_REFRESH_INTERVAL_IN_MS, MIN_REFRESH_INTERVAL_IN_MS } from "./refresh/refreshOptions.js";
25+
import { MIN_SECRET_REFRESH_INTERVAL_IN_MS } from "./keyvault/KeyVaultOptions.js";
2526
import { Disposable } from "./common/disposable.js";
2627
import { base64Helper, jsonSorter, getCryptoModule } from "./common/utils.js";
2728
import {
@@ -112,16 +113,22 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
112113
/**
113114
* Aka watched settings.
114115
*/
116+
#refreshEnabled: boolean = false;
115117
#sentinels: ConfigurationSettingId[] = [];
116118
#watchAll: boolean = false;
117119
#kvRefreshInterval: number = DEFAULT_REFRESH_INTERVAL_IN_MS;
118120
#kvRefreshTimer: RefreshTimer;
119121

120122
// Feature flags
123+
#featureFlagEnabled: boolean = false;
124+
#featureFlagRefreshEnabled: boolean = false;
121125
#ffRefreshInterval: number = DEFAULT_REFRESH_INTERVAL_IN_MS;
122126
#ffRefreshTimer: RefreshTimer;
123127

124128
// Key Vault references
129+
#secretRefreshEnabled: boolean = false;
130+
#secretReferences: ConfigurationSetting[] = []; // cached key vault references
131+
#secretRefreshTimer: RefreshTimer;
125132
#resolveSecretsInParallel: boolean = false;
126133

127134
/**
@@ -155,11 +162,12 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
155162
this.#featureFlagTracing = new FeatureFlagTracingOptions();
156163
}
157164

158-
if (options?.trimKeyPrefixes) {
165+
if (options?.trimKeyPrefixes !== undefined) {
159166
this.#sortedTrimKeyPrefixes = [...options.trimKeyPrefixes].sort((a, b) => b.localeCompare(a));
160167
}
161168

162-
if (options?.refreshOptions?.enabled) {
169+
if (options?.refreshOptions?.enabled === true) {
170+
this.#refreshEnabled = true;
163171
const { refreshIntervalInMs, watchedSettings } = options.refreshOptions;
164172
if (watchedSettings === undefined || watchedSettings.length === 0) {
165173
this.#watchAll = true; // if no watched settings is specified, then watch all
@@ -179,9 +187,8 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
179187
if (refreshIntervalInMs !== undefined) {
180188
if (refreshIntervalInMs < MIN_REFRESH_INTERVAL_IN_MS) {
181189
throw new RangeError(`The refresh interval cannot be less than ${MIN_REFRESH_INTERVAL_IN_MS} milliseconds.`);
182-
} else {
183-
this.#kvRefreshInterval = refreshIntervalInMs;
184190
}
191+
this.#kvRefreshInterval = refreshIntervalInMs;
185192
}
186193
this.#kvRefreshTimer = new RefreshTimer(this.#kvRefreshInterval);
187194
}
@@ -191,44 +198,40 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
191198

192199
// feature flag options
193200
if (options?.featureFlagOptions?.enabled) {
194-
// validate feature flag selectors
201+
this.#featureFlagEnabled = true;
202+
// validate feature flag selectors, only load feature flags when enabled
195203
this.#ffSelectorCollection.selectors = getValidFeatureFlagSelectors(options.featureFlagOptions.selectors);
196204

197-
if (options.featureFlagOptions.refresh?.enabled) {
205+
if (options.featureFlagOptions.refresh?.enabled === true) {
206+
this.#featureFlagRefreshEnabled = true;
198207
const { refreshIntervalInMs } = options.featureFlagOptions.refresh;
199208
// custom refresh interval
200209
if (refreshIntervalInMs !== undefined) {
201210
if (refreshIntervalInMs < MIN_REFRESH_INTERVAL_IN_MS) {
202211
throw new RangeError(`The feature flag refresh interval cannot be less than ${MIN_REFRESH_INTERVAL_IN_MS} milliseconds.`);
203-
} else {
204-
this.#ffRefreshInterval = refreshIntervalInMs;
205212
}
213+
this.#ffRefreshInterval = refreshIntervalInMs;
206214
}
207215

208216
this.#ffRefreshTimer = new RefreshTimer(this.#ffRefreshInterval);
209217
}
210218
}
211219

212-
if (options?.keyVaultOptions?.parallelSecretResolutionEnabled) {
213-
this.#resolveSecretsInParallel = options.keyVaultOptions.parallelSecretResolutionEnabled;
220+
if (options?.keyVaultOptions !== undefined) {
221+
const { secretRefreshIntervalInMs } = options.keyVaultOptions;
222+
if (secretRefreshIntervalInMs !== undefined) {
223+
if (secretRefreshIntervalInMs < MIN_SECRET_REFRESH_INTERVAL_IN_MS) {
224+
throw new RangeError(`The Key Vault secret refresh interval cannot be less than ${MIN_SECRET_REFRESH_INTERVAL_IN_MS} milliseconds.`);
225+
}
226+
this.#secretRefreshEnabled = true;
227+
this.#secretRefreshTimer = new RefreshTimer(secretRefreshIntervalInMs);
228+
}
229+
this.#resolveSecretsInParallel = options.keyVaultOptions.parallelSecretResolutionEnabled ?? false;
214230
}
215-
216-
this.#adapters.push(new AzureKeyVaultKeyValueAdapter(options?.keyVaultOptions));
231+
this.#adapters.push(new AzureKeyVaultKeyValueAdapter(options?.keyVaultOptions, this.#secretRefreshTimer));
217232
this.#adapters.push(new JsonKeyValueAdapter());
218233
}
219234

220-
get #refreshEnabled(): boolean {
221-
return !!this.#options?.refreshOptions?.enabled;
222-
}
223-
224-
get #featureFlagEnabled(): boolean {
225-
return !!this.#options?.featureFlagOptions?.enabled;
226-
}
227-
228-
get #featureFlagRefreshEnabled(): boolean {
229-
return this.#featureFlagEnabled && !!this.#options?.featureFlagOptions?.refresh?.enabled;
230-
}
231-
232235
get #requestTraceOptions(): RequestTracingOptions {
233236
return {
234237
enabled: this.#requestTracingEnabled,
@@ -365,8 +368,8 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
365368
* Refreshes the configuration.
366369
*/
367370
async refresh(): Promise<void> {
368-
if (!this.#refreshEnabled && !this.#featureFlagRefreshEnabled) {
369-
throw new InvalidOperationError("Refresh is not enabled for key-values or feature flags.");
371+
if (!this.#refreshEnabled && !this.#featureFlagRefreshEnabled && !this.#secretRefreshEnabled) {
372+
throw new InvalidOperationError("Refresh is not enabled for key-values, feature flags or Key Vault secrets.");
370373
}
371374

372375
if (this.#refreshInProgress) {
@@ -384,8 +387,8 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
384387
* Registers a callback function to be called when the configuration is refreshed.
385388
*/
386389
onRefresh(listener: () => any, thisArg?: any): Disposable {
387-
if (!this.#refreshEnabled && !this.#featureFlagRefreshEnabled) {
388-
throw new InvalidOperationError("Refresh is not enabled for key-values or feature flags.");
390+
if (!this.#refreshEnabled && !this.#featureFlagRefreshEnabled && !this.#secretRefreshEnabled) {
391+
throw new InvalidOperationError("Refresh is not enabled for key-values, feature flags or Key Vault secrets.");
389392
}
390393

391394
const boundedListener = listener.bind(thisArg);
@@ -453,8 +456,20 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
453456

454457
async #refreshTasks(): Promise<void> {
455458
const refreshTasks: Promise<boolean>[] = [];
456-
if (this.#refreshEnabled) {
457-
refreshTasks.push(this.#refreshKeyValues());
459+
if (this.#refreshEnabled || this.#secretRefreshEnabled) {
460+
refreshTasks.push(
461+
this.#refreshKeyValues()
462+
.then(keyValueRefreshed => {
463+
// Only refresh secrets if key values didn't change and secret refresh is enabled
464+
// If key values are refreshed, all secret references will be refreshed as well.
465+
if (!keyValueRefreshed && this.#secretRefreshEnabled) {
466+
// Returns the refreshSecrets promise directly.
467+
// in a Promise chain, this automatically flattens nested Promises without requiring await.
468+
return this.#refreshSecrets();
469+
}
470+
return keyValueRefreshed;
471+
})
472+
);
458473
}
459474
if (this.#featureFlagRefreshEnabled) {
460475
refreshTasks.push(this.#refreshFeatureFlags());
@@ -566,35 +581,32 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
566581
* Loads selected key-values and watched settings (sentinels) for refresh from App Configuration to the local configuration.
567582
*/
568583
async #loadSelectedAndWatchedKeyValues() {
584+
this.#secretReferences = []; // clear all cached key vault reference configuration settings
569585
const keyValues: [key: string, value: unknown][] = [];
570586
const loadedSettings: ConfigurationSetting[] = await this.#loadConfigurationSettings();
571587
if (this.#refreshEnabled && !this.#watchAll) {
572588
await this.#updateWatchedKeyValuesEtag(loadedSettings);
573589
}
574590

575591
if (this.#requestTracingEnabled && this.#aiConfigurationTracing !== undefined) {
576-
// Reset old AI configuration tracing in order to track the information present in the current response from server.
592+
// reset old AI configuration tracing in order to track the information present in the current response from server
577593
this.#aiConfigurationTracing.reset();
578594
}
579595

580-
const secretResolutionPromises: Promise<void>[] = [];
581596
for (const setting of loadedSettings) {
582-
if (this.#resolveSecretsInParallel && isSecretReference(setting)) {
583-
// secret references are resolved asynchronously to improve performance
584-
const secretResolutionPromise = this.#processKeyValue(setting)
585-
.then(([key, value]) => {
586-
keyValues.push([key, value]);
587-
});
588-
secretResolutionPromises.push(secretResolutionPromise);
597+
if (isSecretReference(setting)) {
598+
this.#secretReferences.push(setting); // cache secret references for resolve/refresh secret separately
589599
continue;
590600
}
591601
// adapt configuration settings to key-values
592602
const [key, value] = await this.#processKeyValue(setting);
593603
keyValues.push([key, value]);
594604
}
595-
if (secretResolutionPromises.length > 0) {
596-
// wait for all secret resolution promises to be resolved
597-
await Promise.all(secretResolutionPromises);
605+
606+
if (this.#secretReferences.length > 0) {
607+
await this.#resolveSecretReferences(this.#secretReferences, (key, value) => {
608+
keyValues.push([key, value]);
609+
});
598610
}
599611

600612
this.#clearLoadedKeyValues(); // clear existing key-values in case of configuration setting deletion
@@ -664,7 +676,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
664676
*/
665677
async #refreshKeyValues(): Promise<boolean> {
666678
// if still within refresh interval/backoff, return
667-
if (!this.#kvRefreshTimer.canRefresh()) {
679+
if (this.#kvRefreshTimer === undefined || !this.#kvRefreshTimer.canRefresh()) {
668680
return Promise.resolve(false);
669681
}
670682

@@ -702,6 +714,9 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
702714
}
703715

704716
if (needRefresh) {
717+
for (const adapter of this.#adapters) {
718+
await adapter.onChangeDetected();
719+
}
705720
await this.#loadSelectedAndWatchedKeyValues();
706721
}
707722

@@ -715,7 +730,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
715730
*/
716731
async #refreshFeatureFlags(): Promise<boolean> {
717732
// if still within refresh interval/backoff, return
718-
if (!this.#ffRefreshTimer.canRefresh()) {
733+
if (this.#ffRefreshInterval === undefined || !this.#ffRefreshTimer.canRefresh()) {
719734
return Promise.resolve(false);
720735
}
721736

@@ -728,6 +743,25 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
728743
return Promise.resolve(needRefresh);
729744
}
730745

746+
async #refreshSecrets(): Promise<boolean> {
747+
// if still within refresh interval/backoff, return
748+
if (this.#secretRefreshTimer === undefined || !this.#secretRefreshTimer.canRefresh()) {
749+
return Promise.resolve(false);
750+
}
751+
752+
// if no cached key vault references, return
753+
if (this.#secretReferences.length === 0) {
754+
return Promise.resolve(false);
755+
}
756+
757+
await this.#resolveSecretReferences(this.#secretReferences, (key, value) => {
758+
this.#configMap.set(key, value);
759+
});
760+
761+
this.#secretRefreshTimer.reset();
762+
return Promise.resolve(true);
763+
}
764+
731765
/**
732766
* Checks whether the key-value collection has changed.
733767
* @param selectorCollection - The @see SettingSelectorCollection of the kev-value collection.
@@ -887,6 +921,27 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
887921
throw new Error("All fallback clients failed to get configuration settings.");
888922
}
889923

924+
async #resolveSecretReferences(secretReferences: ConfigurationSetting[], resultHandler: (key: string, value: unknown) => void): Promise<void> {
925+
if (this.#resolveSecretsInParallel) {
926+
const secretResolutionPromises: Promise<void>[] = [];
927+
for (const setting of secretReferences) {
928+
const secretResolutionPromise = this.#processKeyValue(setting)
929+
.then(([key, value]) => {
930+
resultHandler(key, value);
931+
});
932+
secretResolutionPromises.push(secretResolutionPromise);
933+
}
934+
935+
// Wait for all secret resolution promises to be resolved
936+
await Promise.all(secretResolutionPromises);
937+
} else {
938+
for (const setting of secretReferences) {
939+
const [key, value] = await this.#processKeyValue(setting);
940+
resultHandler(key, value);
941+
}
942+
}
943+
}
944+
890945
async #processKeyValue(setting: ConfigurationSetting<string>): Promise<[string, unknown]> {
891946
this.#setAIConfigurationTracing(setting);
892947

src/ConfigurationClientManager.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { ArgumentError } from "./common/error.js";
1212

1313
// Configuration client retry options
1414
const CLIENT_MAX_RETRIES = 2;
15-
const CLIENT_MAX_RETRY_DELAY = 60_000; // 1 minute in milliseconds
15+
const CLIENT_MAX_RETRY_DELAY_IN_MS = 60_000;
1616

1717
const TCP_ORIGIN_KEY_NAME = "_origin._tcp";
1818
const ALT_KEY_NAME = "_alt";
@@ -21,9 +21,9 @@ const ENDPOINT_KEY_NAME = "Endpoint";
2121
const ID_KEY_NAME = "Id";
2222
const SECRET_KEY_NAME = "Secret";
2323
const TRUSTED_DOMAIN_LABELS = [".azconfig.", ".appconfig."];
24-
const FALLBACK_CLIENT_EXPIRE_INTERVAL = 60 * 60 * 1000; // 1 hour in milliseconds
25-
const MINIMAL_CLIENT_REFRESH_INTERVAL = 30_000; // 30 seconds in milliseconds
26-
const DNS_RESOLVER_TIMEOUT = 3_000; // 3 seconds in milliseconds, in most cases, dns resolution should be within 200 milliseconds
24+
const FALLBACK_CLIENT_EXPIRE_INTERVAL_IN_MS = 60 * 60 * 1000;
25+
const MINIMAL_CLIENT_REFRESH_INTERVAL_IN_MS = 30_000;
26+
const DNS_RESOLVER_TIMEOUT_IN_MS = 3_000;
2727
const DNS_RESOLVER_TRIES = 2;
2828
const MAX_ALTNATIVE_SRV_COUNT = 10;
2929

@@ -120,11 +120,11 @@ export class ConfigurationClientManager {
120120
const currentTime = Date.now();
121121
// Filter static clients whose backoff time has ended
122122
let availableClients = this.#staticClients.filter(client => client.backoffEndTime <= currentTime);
123-
if (currentTime >= this.#lastFallbackClientRefreshAttempt + MINIMAL_CLIENT_REFRESH_INTERVAL &&
123+
if (currentTime >= this.#lastFallbackClientRefreshAttempt + MINIMAL_CLIENT_REFRESH_INTERVAL_IN_MS &&
124124
(!this.#dynamicClients ||
125125
// All dynamic clients are in backoff means no client is available
126126
this.#dynamicClients.every(client => currentTime < client.backoffEndTime) ||
127-
currentTime >= this.#lastFallbackClientUpdateTime + FALLBACK_CLIENT_EXPIRE_INTERVAL)) {
127+
currentTime >= this.#lastFallbackClientUpdateTime + FALLBACK_CLIENT_EXPIRE_INTERVAL_IN_MS)) {
128128
await this.#discoverFallbackClients(this.endpoint.hostname);
129129
return availableClients.concat(this.#dynamicClients);
130130
}
@@ -142,7 +142,7 @@ export class ConfigurationClientManager {
142142
async refreshClients() {
143143
const currentTime = Date.now();
144144
if (this.#isFailoverable &&
145-
currentTime >= this.#lastFallbackClientRefreshAttempt + MINIMAL_CLIENT_REFRESH_INTERVAL) {
145+
currentTime >= this.#lastFallbackClientRefreshAttempt + MINIMAL_CLIENT_REFRESH_INTERVAL_IN_MS) {
146146
await this.#discoverFallbackClients(this.endpoint.hostname);
147147
}
148148
}
@@ -185,7 +185,7 @@ export class ConfigurationClientManager {
185185

186186
try {
187187
// https://nodejs.org/api/dns.html#dnspromisesresolvesrvhostname
188-
const resolver = new this.#dns.Resolver({timeout: DNS_RESOLVER_TIMEOUT, tries: DNS_RESOLVER_TRIES});
188+
const resolver = new this.#dns.Resolver({timeout: DNS_RESOLVER_TIMEOUT_IN_MS, tries: DNS_RESOLVER_TRIES});
189189
// On success, resolveSrv() returns an array of SrvRecord
190190
// On failure, resolveSrv() throws an error with code 'ENOTFOUND'.
191191
const originRecords = await resolver.resolveSrv(`${TCP_ORIGIN_KEY_NAME}.${host}`); // look up SRV records for the origin host
@@ -266,7 +266,7 @@ function getClientOptions(options?: AzureAppConfigurationOptions): AppConfigurat
266266
// retry options
267267
const defaultRetryOptions = {
268268
maxRetries: CLIENT_MAX_RETRIES,
269-
maxRetryDelayInMs: CLIENT_MAX_RETRY_DELAY,
269+
maxRetryDelayInMs: CLIENT_MAX_RETRY_DELAY_IN_MS,
270270
};
271271
const retryOptions = Object.assign({}, defaultRetryOptions, options?.clientOptions?.retryOptions);
272272

src/IKeyValueAdapter.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,9 @@ export interface IKeyValueAdapter {
1313
* This method process the original configuration setting, and returns processed key and value in an array.
1414
*/
1515
processKeyValue(setting: ConfigurationSetting): Promise<[string, unknown]>;
16+
17+
/**
18+
* This method is called when a change is detected in the configuration setting.
19+
*/
20+
onChangeDetected(): Promise<void>;
1621
}

src/JsonKeyValueAdapter.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,8 @@ export class JsonKeyValueAdapter implements IKeyValueAdapter {
3535
}
3636
return [setting.key, parsedValue];
3737
}
38+
39+
async onChangeDetected(): Promise<void> {
40+
return;
41+
}
3842
}

src/StartupOptions.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-
export const DEFAULT_STARTUP_TIMEOUT_IN_MS = 100 * 1000; // 100 seconds in milliseconds
4+
export const DEFAULT_STARTUP_TIMEOUT_IN_MS = 100_000;
55

66
export interface StartupOptions {
77
/**

0 commit comments

Comments
 (0)