@@ -22,6 +22,7 @@ import { IKeyValueAdapter } from "./IKeyValueAdapter.js";
2222import { JsonKeyValueAdapter } from "./JsonKeyValueAdapter.js" ;
2323import { DEFAULT_STARTUP_TIMEOUT_IN_MS } from "./StartupOptions.js" ;
2424import { 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" ;
2526import { Disposable } from "./common/disposable.js" ;
2627import { base64Helper , jsonSorter , getCryptoModule } from "./common/utils.js" ;
2728import {
@@ -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
0 commit comments