@@ -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  { 
2728    FEATURE_FLAGS_KEY_NAME , 
@@ -91,16 +92,22 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
9192    /** 
9293     * Aka watched settings. 
9394     */ 
95+     #refreshEnabled: boolean  =  false ; 
9496    #sentinels: ConfigurationSettingId [ ]  =  [ ] ; 
9597    #watchAll: boolean  =  false ; 
9698    #kvRefreshInterval: number  =  DEFAULT_REFRESH_INTERVAL_IN_MS ; 
9799    #kvRefreshTimer: RefreshTimer ; 
98100
99101    // Feature flags 
102+     #featureFlagEnabled: boolean  =  false ; 
103+     #featureFlagRefreshEnabled: boolean  =  false ; 
100104    #ffRefreshInterval: number  =  DEFAULT_REFRESH_INTERVAL_IN_MS ; 
101105    #ffRefreshTimer: RefreshTimer ; 
102106
103107    // Key Vault references 
108+     #secretRefreshEnabled: boolean  =  false ; 
109+     #secretReferences: ConfigurationSetting [ ]  =  [ ] ;  // cached key vault references 
110+     #secretRefreshTimer: RefreshTimer ; 
104111    #resolveSecretsInParallel: boolean  =  false ; 
105112
106113    /** 
@@ -129,14 +136,15 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
129136            this . #featureFlagTracing =  new  FeatureFlagTracingOptions ( ) ; 
130137        } 
131138
132-         if  ( options ?. trimKeyPrefixes )  { 
139+         if  ( options ?. trimKeyPrefixes   !==   undefined )  { 
133140            this . #sortedTrimKeyPrefixes =  [ ...options . trimKeyPrefixes ] . sort ( ( a ,  b )  =>  b . localeCompare ( a ) ) ; 
134141        } 
135142
136143        // if no selector is specified, always load key values using the default selector: key="*" and label="\0" 
137144        this . #kvSelectors =  getValidKeyValueSelectors ( options ?. selectors ) ; 
138145
139-         if  ( options ?. refreshOptions ?. enabled )  { 
146+         if  ( options ?. refreshOptions ?. enabled  ===  true )  { 
147+             this . #refreshEnabled =  true ; 
140148            const  {  refreshIntervalInMs,  watchedSettings }  =  options . refreshOptions ; 
141149            if  ( watchedSettings  ===  undefined  ||  watchedSettings . length  ===  0 )  { 
142150                this . #watchAll =  true ;  // if no watched settings is specified, then watch all 
@@ -156,53 +164,48 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
156164            if  ( refreshIntervalInMs  !==  undefined )  { 
157165                if  ( refreshIntervalInMs  <  MIN_REFRESH_INTERVAL_IN_MS )  { 
158166                    throw  new  RangeError ( `The refresh interval cannot be less than ${ MIN_REFRESH_INTERVAL_IN_MS }   milliseconds.` ) ; 
159-                 }  else  { 
160-                     this . #kvRefreshInterval =  refreshIntervalInMs ; 
161167                } 
168+                 this . #kvRefreshInterval =  refreshIntervalInMs ; 
162169            } 
163170            this . #kvRefreshTimer =  new  RefreshTimer ( this . #kvRefreshInterval) ; 
164171        } 
165172
166173        // feature flag options 
167-         if  ( options ?. featureFlagOptions ?. enabled )  { 
174+         if  ( options ?. featureFlagOptions ?. enabled  ===  true )  { 
175+             this . #featureFlagEnabled =  true ; 
168176            // validate feature flag selectors, only load feature flags when enabled 
169177            this . #ffSelectors =  getValidFeatureFlagSelectors ( options . featureFlagOptions . selectors ) ; 
170178
171-             if  ( options . featureFlagOptions . refresh ?. enabled )  { 
179+             if  ( options . featureFlagOptions . refresh ?. enabled  ===  true )  { 
180+                 this . #featureFlagRefreshEnabled =  true ; 
172181                const  {  refreshIntervalInMs }  =  options . featureFlagOptions . refresh ; 
173182                // custom refresh interval 
174183                if  ( refreshIntervalInMs  !==  undefined )  { 
175184                    if  ( refreshIntervalInMs  <  MIN_REFRESH_INTERVAL_IN_MS )  { 
176185                        throw  new  RangeError ( `The feature flag refresh interval cannot be less than ${ MIN_REFRESH_INTERVAL_IN_MS }   milliseconds.` ) ; 
177-                     }  else  { 
178-                         this . #ffRefreshInterval =  refreshIntervalInMs ; 
179186                    } 
187+                     this . #ffRefreshInterval =  refreshIntervalInMs ; 
180188                } 
181189
182190                this . #ffRefreshTimer =  new  RefreshTimer ( this . #ffRefreshInterval) ; 
183191            } 
184192        } 
185193
186-         if  ( options ?. keyVaultOptions ?. parallelSecretResolutionEnabled )  { 
187-             this . #resolveSecretsInParallel =  options . keyVaultOptions . parallelSecretResolutionEnabled ; 
194+         if  ( options ?. keyVaultOptions  !==  undefined )  { 
195+             const  {  secretRefreshIntervalInMs }  =  options . keyVaultOptions ; 
196+             if  ( secretRefreshIntervalInMs  !==  undefined )  { 
197+                 if  ( secretRefreshIntervalInMs  <  MIN_SECRET_REFRESH_INTERVAL_IN_MS )  { 
198+                     throw  new  RangeError ( `The Key Vault secret refresh interval cannot be less than ${ MIN_SECRET_REFRESH_INTERVAL_IN_MS }   milliseconds.` ) ; 
199+                 } 
200+                 this . #secretRefreshEnabled =  true ; 
201+                 this . #secretRefreshTimer =  new  RefreshTimer ( secretRefreshIntervalInMs ) ; 
202+             } 
203+             this . #resolveSecretsInParallel =  options . keyVaultOptions . parallelSecretResolutionEnabled  ??  false ; 
188204        } 
189- 
190-         this . #adapters. push ( new  AzureKeyVaultKeyValueAdapter ( options ?. keyVaultOptions ) ) ; 
205+         this . #adapters. push ( new  AzureKeyVaultKeyValueAdapter ( options ?. keyVaultOptions ,  this . #secretRefreshTimer) ) ; 
191206        this . #adapters. push ( new  JsonKeyValueAdapter ( ) ) ; 
192207    } 
193208
194-     get  #refreshEnabled( ) : boolean  { 
195-         return  ! ! this . #options?. refreshOptions ?. enabled ; 
196-     } 
197- 
198-     get  #featureFlagEnabled( ) : boolean  { 
199-         return  ! ! this . #options?. featureFlagOptions ?. enabled ; 
200-     } 
201- 
202-     get  #featureFlagRefreshEnabled( ) : boolean  { 
203-         return  this . #featureFlagEnabled &&  ! ! this . #options?. featureFlagOptions ?. refresh ?. enabled ; 
204-     } 
205- 
206209    get  #requestTraceOptions( ) : RequestTracingOptions  { 
207210        return  { 
208211            enabled : this . #requestTracingEnabled, 
@@ -337,8 +340,8 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
337340     * Refreshes the configuration. 
338341     */ 
339342    async  refresh ( ) : Promise < void >  { 
340-         if  ( ! this . #refreshEnabled &&  ! this . #featureFlagRefreshEnabled)  { 
341-             throw  new  InvalidOperationError ( "Refresh is not enabled for key-values or  feature flags." ) ; 
343+         if  ( ! this . #refreshEnabled &&  ! this . #featureFlagRefreshEnabled  &&   ! this . #secretRefreshEnabled )  { 
344+             throw  new  InvalidOperationError ( "Refresh is not enabled for key-values,  feature flags or Key Vault secrets ." ) ; 
342345        } 
343346
344347        if  ( this . #refreshInProgress)  { 
@@ -356,8 +359,8 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
356359     * Registers a callback function to be called when the configuration is refreshed. 
357360     */ 
358361    onRefresh ( listener : ( )  =>  any ,  thisArg ?: any ) : Disposable  { 
359-         if  ( ! this . #refreshEnabled &&  ! this . #featureFlagRefreshEnabled)  { 
360-             throw  new  InvalidOperationError ( "Refresh is not enabled for key-values or  feature flags." ) ; 
362+         if  ( ! this . #refreshEnabled &&  ! this . #featureFlagRefreshEnabled  &&   ! this . #secretRefreshEnabled )  { 
363+             throw  new  InvalidOperationError ( "Refresh is not enabled for key-values,  feature flags or Key Vault secrets ." ) ; 
361364        } 
362365
363366        const  boundedListener  =  listener . bind ( thisArg ) ; 
@@ -425,8 +428,20 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
425428
426429    async  #refreshTasks( ) : Promise < void >  { 
427430        const  refreshTasks : Promise < boolean > [ ]  =  [ ] ; 
428-         if  ( this . #refreshEnabled)  { 
429-             refreshTasks . push ( this . #refreshKeyValues( ) ) ; 
431+         if  ( this . #refreshEnabled ||  this . #secretRefreshEnabled)  { 
432+             refreshTasks . push ( 
433+                 this . #refreshKeyValues( ) 
434+                 . then ( keyValueRefreshed  =>  { 
435+                     // Only refresh secrets if key values didn't change and secret refresh is enabled 
436+                     // If key values are refreshed, all secret references will be refreshed as well. 
437+                     if  ( ! keyValueRefreshed  &&  this . #secretRefreshEnabled)  { 
438+                         // Returns the refreshSecrets promise directly. 
439+                         // in a Promise chain, this automatically flattens nested Promises without requiring await. 
440+                         return  this . #refreshSecrets( ) ; 
441+                     } 
442+                     return  keyValueRefreshed ; 
443+                 } ) 
444+             ) ; 
430445        } 
431446        if  ( this . #featureFlagRefreshEnabled)  { 
432447            refreshTasks . push ( this . #refreshFeatureFlags( ) ) ; 
@@ -530,35 +545,32 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
530545     * Loads selected key-values and watched settings (sentinels) for refresh from App Configuration to the local configuration. 
531546     */ 
532547    async  #loadSelectedAndWatchedKeyValues( )  { 
548+         this . #secretReferences =  [ ] ;  // clear all cached key vault reference configuration settings 
533549        const  keyValues : [ key : string ,  value : unknown ] [ ]  =  [ ] ; 
534550        const  loadedSettings : ConfigurationSetting [ ]  =  await  this . #loadConfigurationSettings( ) ; 
535551        if  ( this . #refreshEnabled &&  ! this . #watchAll)  { 
536552            await  this . #updateWatchedKeyValuesEtag( loadedSettings ) ; 
537553        } 
538554
539555        if  ( this . #requestTracingEnabled &&  this . #aiConfigurationTracing !==  undefined )  { 
540-             // Reset  old AI configuration tracing in order to track the information present in the current response from server.  
556+             // reset  old AI configuration tracing in order to track the information present in the current response from server 
541557            this . #aiConfigurationTracing. reset ( ) ; 
542558        } 
543559
544-         const  secretResolutionPromises : Promise < void > [ ]  =  [ ] ; 
545560        for  ( const  setting  of  loadedSettings )  { 
546-             if  ( this . #resolveSecretsInParallel &&  isSecretReference ( setting ) )  { 
547-                 // secret references are resolved asynchronously to improve performance 
548-                 const  secretResolutionPromise  =  this . #processKeyValue( setting ) 
549-                     . then ( ( [ key ,  value ] )  =>  { 
550-                         keyValues . push ( [ key ,  value ] ) ; 
551-                     } ) ; 
552-                 secretResolutionPromises . push ( secretResolutionPromise ) ; 
561+             if  ( isSecretReference ( setting ) )  { 
562+                 this . #secretReferences. push ( setting ) ;  // cache secret references for resolve/refresh secret separately 
553563                continue ; 
554564            } 
555565            // adapt configuration settings to key-values 
556566            const  [ key ,  value ]  =  await  this . #processKeyValue( setting ) ; 
557567            keyValues . push ( [ key ,  value ] ) ; 
558568        } 
559-         if  ( secretResolutionPromises . length  >  0 )  { 
560-             // wait for all secret resolution promises to be resolved 
561-             await  Promise . all ( secretResolutionPromises ) ; 
569+ 
570+         if  ( this . #secretReferences. length  >  0 )  { 
571+             await  this . #resolveSecretReferences( this . #secretReferences,  ( key ,  value )  =>  { 
572+                 keyValues . push ( [ key ,  value ] ) ; 
573+             } ) ; 
562574        } 
563575
564576        this . #clearLoadedKeyValues( ) ;  // clear existing key-values in case of configuration setting deletion 
@@ -626,7 +638,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
626638     */ 
627639    async  #refreshKeyValues( ) : Promise < boolean >  { 
628640        // if still within refresh interval/backoff, return 
629-         if  ( ! this . #kvRefreshTimer. canRefresh ( ) )  { 
641+         if  ( this . #kvRefreshTimer  ===   undefined   ||   ! this . #kvRefreshTimer. canRefresh ( ) )  { 
630642            return  Promise . resolve ( false ) ; 
631643        } 
632644
@@ -650,6 +662,9 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
650662        } 
651663
652664        if  ( needRefresh )  { 
665+             for  ( const  adapter  of  this . #adapters)  { 
666+                 await  adapter . onChangeDetected ( ) ; 
667+             } 
653668            await  this . #loadSelectedAndWatchedKeyValues( ) ; 
654669        } 
655670
@@ -663,7 +678,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
663678     */ 
664679    async  #refreshFeatureFlags( ) : Promise < boolean >  { 
665680        // if still within refresh interval/backoff, return 
666-         if  ( ! this . #ffRefreshTimer. canRefresh ( ) )  { 
681+         if  ( this . #ffRefreshInterval  ===   undefined   ||   ! this . #ffRefreshTimer. canRefresh ( ) )  { 
667682            return  Promise . resolve ( false ) ; 
668683        } 
669684
@@ -676,6 +691,25 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
676691        return  Promise . resolve ( needRefresh ) ; 
677692    } 
678693
694+     async  #refreshSecrets( ) : Promise < boolean >  { 
695+         // if still within refresh interval/backoff, return 
696+         if  ( this . #secretRefreshTimer ===  undefined  ||  ! this . #secretRefreshTimer. canRefresh ( ) )  { 
697+             return  Promise . resolve ( false ) ; 
698+         } 
699+ 
700+         // if no cached key vault references, return 
701+         if  ( this . #secretReferences. length  ===  0 )  { 
702+             return  Promise . resolve ( false ) ; 
703+         } 
704+ 
705+         await  this . #resolveSecretReferences( this . #secretReferences,  ( key ,  value )  =>  { 
706+             this . #configMap. set ( key ,  value ) ; 
707+         } ) ; 
708+ 
709+         this . #secretRefreshTimer. reset ( ) ; 
710+         return  Promise . resolve ( true ) ; 
711+     } 
712+ 
679713    /** 
680714     * Checks whether the key-value collection has changed. 
681715     * @param  selectors - The @see PagedSettingSelector of the kev-value collection. 
@@ -804,6 +838,27 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
804838        throw  new  Error ( "All fallback clients failed to get configuration settings." ) ; 
805839    } 
806840
841+     async  #resolveSecretReferences( secretReferences : ConfigurationSetting [ ] ,  resultHandler : ( key : string ,  value : unknown )  =>  void ) : Promise < void >  { 
842+         if  ( this . #resolveSecretsInParallel)  { 
843+             const  secretResolutionPromises : Promise < void > [ ]  =  [ ] ; 
844+             for  ( const  setting  of  secretReferences )  { 
845+                 const  secretResolutionPromise  =  this . #processKeyValue( setting ) 
846+                     . then ( ( [ key ,  value ] )  =>  { 
847+                         resultHandler ( key ,  value ) ; 
848+                     } ) ; 
849+                 secretResolutionPromises . push ( secretResolutionPromise ) ; 
850+             } 
851+ 
852+             // Wait for all secret resolution promises to be resolved 
853+             await  Promise . all ( secretResolutionPromises ) ; 
854+         }  else  { 
855+             for  ( const  setting  of  secretReferences )  { 
856+                 const  [ key ,  value ]  =  await  this . #processKeyValue( setting ) ; 
857+                 resultHandler ( key ,  value ) ; 
858+             } 
859+         } 
860+     } 
861+ 
807862    async  #processKeyValue( setting : ConfigurationSetting < string > ) : Promise < [ string ,  unknown ] >  { 
808863        this . #setAIConfigurationTracing( setting ) ; 
809864
0 commit comments