@@ -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 } from "./common/utils.js" ;
2728import {
@@ -99,16 +100,22 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
99100 /**
100101 * Aka watched settings.
101102 */
103+ #refreshEnabled: boolean = false ;
102104 #sentinels: ConfigurationSettingId [ ] = [ ] ;
103105 #watchAll: boolean = false ;
104106 #kvRefreshInterval: number = DEFAULT_REFRESH_INTERVAL_IN_MS ;
105107 #kvRefreshTimer: RefreshTimer ;
106108
107109 // Feature flags
110+ #featureFlagEnabled: boolean = false ;
111+ #featureFlagRefreshEnabled: boolean = false ;
108112 #ffRefreshInterval: number = DEFAULT_REFRESH_INTERVAL_IN_MS ;
109113 #ffRefreshTimer: RefreshTimer ;
110114
111115 // Key Vault references
116+ #secretRefreshEnabled: boolean = false ;
117+ #secretReferences: ConfigurationSetting [ ] = [ ] ; // cached key vault references
118+ #secretRefreshTimer: RefreshTimer ;
112119 #resolveSecretsInParallel: boolean = false ;
113120
114121 /**
@@ -137,14 +144,15 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
137144 this . #featureFlagTracing = new FeatureFlagTracingOptions ( ) ;
138145 }
139146
140- if ( options ?. trimKeyPrefixes ) {
147+ if ( options ?. trimKeyPrefixes !== undefined ) {
141148 this . #sortedTrimKeyPrefixes = [ ...options . trimKeyPrefixes ] . sort ( ( a , b ) => b . localeCompare ( a ) ) ;
142149 }
143150
144151 // if no selector is specified, always load key values using the default selector: key="*" and label="\0"
145152 this . #kvSelectors = getValidKeyValueSelectors ( options ?. selectors ) ;
146153
147- if ( options ?. refreshOptions ?. enabled ) {
154+ if ( options ?. refreshOptions ?. enabled === true ) {
155+ this . #refreshEnabled = true ;
148156 const { refreshIntervalInMs, watchedSettings } = options . refreshOptions ;
149157 if ( watchedSettings === undefined || watchedSettings . length === 0 ) {
150158 this . #watchAll = true ; // if no watched settings is specified, then watch all
@@ -164,53 +172,48 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
164172 if ( refreshIntervalInMs !== undefined ) {
165173 if ( refreshIntervalInMs < MIN_REFRESH_INTERVAL_IN_MS ) {
166174 throw new RangeError ( `The refresh interval cannot be less than ${ MIN_REFRESH_INTERVAL_IN_MS } milliseconds.` ) ;
167- } else {
168- this . #kvRefreshInterval = refreshIntervalInMs ;
169175 }
176+ this . #kvRefreshInterval = refreshIntervalInMs ;
170177 }
171178 this . #kvRefreshTimer = new RefreshTimer ( this . #kvRefreshInterval) ;
172179 }
173180
174181 // feature flag options
175- if ( options ?. featureFlagOptions ?. enabled ) {
182+ if ( options ?. featureFlagOptions ?. enabled === true ) {
183+ this . #featureFlagEnabled = true ;
176184 // validate feature flag selectors, only load feature flags when enabled
177185 this . #ffSelectors = getValidFeatureFlagSelectors ( options . featureFlagOptions . selectors ) ;
178186
179- if ( options . featureFlagOptions . refresh ?. enabled ) {
187+ if ( options . featureFlagOptions . refresh ?. enabled === true ) {
188+ this . #featureFlagRefreshEnabled = true ;
180189 const { refreshIntervalInMs } = options . featureFlagOptions . refresh ;
181190 // custom refresh interval
182191 if ( refreshIntervalInMs !== undefined ) {
183192 if ( refreshIntervalInMs < MIN_REFRESH_INTERVAL_IN_MS ) {
184193 throw new RangeError ( `The feature flag refresh interval cannot be less than ${ MIN_REFRESH_INTERVAL_IN_MS } milliseconds.` ) ;
185- } else {
186- this . #ffRefreshInterval = refreshIntervalInMs ;
187194 }
195+ this . #ffRefreshInterval = refreshIntervalInMs ;
188196 }
189197
190198 this . #ffRefreshTimer = new RefreshTimer ( this . #ffRefreshInterval) ;
191199 }
192200 }
193201
194- if ( options ?. keyVaultOptions ?. parallelSecretResolutionEnabled ) {
195- this . #resolveSecretsInParallel = options . keyVaultOptions . parallelSecretResolutionEnabled ;
202+ if ( options ?. keyVaultOptions !== undefined ) {
203+ const { secretRefreshIntervalInMs } = options . keyVaultOptions ;
204+ if ( secretRefreshIntervalInMs !== undefined ) {
205+ if ( secretRefreshIntervalInMs < MIN_SECRET_REFRESH_INTERVAL_IN_MS ) {
206+ throw new RangeError ( `The Key Vault secret refresh interval cannot be less than ${ MIN_SECRET_REFRESH_INTERVAL_IN_MS } milliseconds.` ) ;
207+ }
208+ this . #secretRefreshEnabled = true ;
209+ this . #secretRefreshTimer = new RefreshTimer ( secretRefreshIntervalInMs ) ;
210+ }
211+ this . #resolveSecretsInParallel = options . keyVaultOptions . parallelSecretResolutionEnabled ?? false ;
196212 }
197-
198- this . #adapters. push ( new AzureKeyVaultKeyValueAdapter ( options ?. keyVaultOptions ) ) ;
213+ this . #adapters. push ( new AzureKeyVaultKeyValueAdapter ( options ?. keyVaultOptions , this . #secretRefreshTimer) ) ;
199214 this . #adapters. push ( new JsonKeyValueAdapter ( ) ) ;
200215 }
201216
202- get #refreshEnabled( ) : boolean {
203- return ! ! this . #options?. refreshOptions ?. enabled ;
204- }
205-
206- get #featureFlagEnabled( ) : boolean {
207- return ! ! this . #options?. featureFlagOptions ?. enabled ;
208- }
209-
210- get #featureFlagRefreshEnabled( ) : boolean {
211- return this . #featureFlagEnabled && ! ! this . #options?. featureFlagOptions ?. refresh ?. enabled ;
212- }
213-
214217 get #requestTraceOptions( ) : RequestTracingOptions {
215218 return {
216219 enabled : this . #requestTracingEnabled,
@@ -345,8 +348,8 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
345348 * Refreshes the configuration.
346349 */
347350 async refresh ( ) : Promise < void > {
348- if ( ! this . #refreshEnabled && ! this . #featureFlagRefreshEnabled) {
349- throw new InvalidOperationError ( "Refresh is not enabled for key-values or feature flags." ) ;
351+ if ( ! this . #refreshEnabled && ! this . #featureFlagRefreshEnabled && ! this . #secretRefreshEnabled ) {
352+ throw new InvalidOperationError ( "Refresh is not enabled for key-values, feature flags or Key Vault secrets ." ) ;
350353 }
351354
352355 if ( this . #refreshInProgress) {
@@ -364,8 +367,8 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
364367 * Registers a callback function to be called when the configuration is refreshed.
365368 */
366369 onRefresh ( listener : ( ) => any , thisArg ?: any ) : Disposable {
367- if ( ! this . #refreshEnabled && ! this . #featureFlagRefreshEnabled) {
368- throw new InvalidOperationError ( "Refresh is not enabled for key-values or feature flags." ) ;
370+ if ( ! this . #refreshEnabled && ! this . #featureFlagRefreshEnabled && ! this . #secretRefreshEnabled ) {
371+ throw new InvalidOperationError ( "Refresh is not enabled for key-values, feature flags or Key Vault secrets ." ) ;
369372 }
370373
371374 const boundedListener = listener . bind ( thisArg ) ;
@@ -433,8 +436,20 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
433436
434437 async #refreshTasks( ) : Promise < void > {
435438 const refreshTasks : Promise < boolean > [ ] = [ ] ;
436- if ( this . #refreshEnabled) {
437- refreshTasks . push ( this . #refreshKeyValues( ) ) ;
439+ if ( this . #refreshEnabled || this . #secretRefreshEnabled) {
440+ refreshTasks . push (
441+ this . #refreshKeyValues( )
442+ . then ( keyValueRefreshed => {
443+ // Only refresh secrets if key values didn't change and secret refresh is enabled
444+ // If key values are refreshed, all secret references will be refreshed as well.
445+ if ( ! keyValueRefreshed && this . #secretRefreshEnabled) {
446+ // Returns the refreshSecrets promise directly.
447+ // in a Promise chain, this automatically flattens nested Promises without requiring await.
448+ return this . #refreshSecrets( ) ;
449+ }
450+ return keyValueRefreshed ;
451+ } )
452+ ) ;
438453 }
439454 if ( this . #featureFlagRefreshEnabled) {
440455 refreshTasks . push ( this . #refreshFeatureFlags( ) ) ;
@@ -538,35 +553,32 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
538553 * Loads selected key-values and watched settings (sentinels) for refresh from App Configuration to the local configuration.
539554 */
540555 async #loadSelectedAndWatchedKeyValues( ) {
556+ this . #secretReferences = [ ] ; // clear all cached key vault reference configuration settings
541557 const keyValues : [ key : string , value : unknown ] [ ] = [ ] ;
542558 const loadedSettings : ConfigurationSetting [ ] = await this . #loadConfigurationSettings( ) ;
543559 if ( this . #refreshEnabled && ! this . #watchAll) {
544560 await this . #updateWatchedKeyValuesEtag( loadedSettings ) ;
545561 }
546562
547563 if ( this . #requestTracingEnabled && this . #aiConfigurationTracing !== undefined ) {
548- // Reset old AI configuration tracing in order to track the information present in the current response from server.
564+ // reset old AI configuration tracing in order to track the information present in the current response from server
549565 this . #aiConfigurationTracing. reset ( ) ;
550566 }
551567
552- const secretResolutionPromises : Promise < void > [ ] = [ ] ;
553568 for ( const setting of loadedSettings ) {
554- if ( this . #resolveSecretsInParallel && isSecretReference ( setting ) ) {
555- // secret references are resolved asynchronously to improve performance
556- const secretResolutionPromise = this . #processKeyValue( setting )
557- . then ( ( [ key , value ] ) => {
558- keyValues . push ( [ key , value ] ) ;
559- } ) ;
560- secretResolutionPromises . push ( secretResolutionPromise ) ;
569+ if ( isSecretReference ( setting ) ) {
570+ this . #secretReferences. push ( setting ) ; // cache secret references for resolve/refresh secret separately
561571 continue ;
562572 }
563573 // adapt configuration settings to key-values
564574 const [ key , value ] = await this . #processKeyValue( setting ) ;
565575 keyValues . push ( [ key , value ] ) ;
566576 }
567- if ( secretResolutionPromises . length > 0 ) {
568- // wait for all secret resolution promises to be resolved
569- await Promise . all ( secretResolutionPromises ) ;
577+
578+ if ( this . #secretReferences. length > 0 ) {
579+ await this . #resolveSecretReferences( this . #secretReferences, ( key , value ) => {
580+ keyValues . push ( [ key , value ] ) ;
581+ } ) ;
570582 }
571583
572584 this . #clearLoadedKeyValues( ) ; // clear existing key-values in case of configuration setting deletion
@@ -634,7 +646,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
634646 */
635647 async #refreshKeyValues( ) : Promise < boolean > {
636648 // if still within refresh interval/backoff, return
637- if ( ! this . #kvRefreshTimer. canRefresh ( ) ) {
649+ if ( this . #kvRefreshTimer === undefined || ! this . #kvRefreshTimer. canRefresh ( ) ) {
638650 return Promise . resolve ( false ) ;
639651 }
640652
@@ -658,6 +670,9 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
658670 }
659671
660672 if ( needRefresh ) {
673+ for ( const adapter of this . #adapters) {
674+ await adapter . onChangeDetected ( ) ;
675+ }
661676 await this . #loadSelectedAndWatchedKeyValues( ) ;
662677 }
663678
@@ -671,7 +686,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
671686 */
672687 async #refreshFeatureFlags( ) : Promise < boolean > {
673688 // if still within refresh interval/backoff, return
674- if ( ! this . #ffRefreshTimer. canRefresh ( ) ) {
689+ if ( this . #ffRefreshInterval === undefined || ! this . #ffRefreshTimer. canRefresh ( ) ) {
675690 return Promise . resolve ( false ) ;
676691 }
677692
@@ -684,6 +699,25 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
684699 return Promise . resolve ( needRefresh ) ;
685700 }
686701
702+ async #refreshSecrets( ) : Promise < boolean > {
703+ // if still within refresh interval/backoff, return
704+ if ( this . #secretRefreshTimer === undefined || ! this . #secretRefreshTimer. canRefresh ( ) ) {
705+ return Promise . resolve ( false ) ;
706+ }
707+
708+ // if no cached key vault references, return
709+ if ( this . #secretReferences. length === 0 ) {
710+ return Promise . resolve ( false ) ;
711+ }
712+
713+ await this . #resolveSecretReferences( this . #secretReferences, ( key , value ) => {
714+ this . #configMap. set ( key , value ) ;
715+ } ) ;
716+
717+ this . #secretRefreshTimer. reset ( ) ;
718+ return Promise . resolve ( true ) ;
719+ }
720+
687721 /**
688722 * Checks whether the key-value collection has changed.
689723 * @param selectors - The @see PagedSettingSelector of the kev-value collection.
@@ -812,6 +846,27 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
812846 throw new Error ( "All fallback clients failed to get configuration settings." ) ;
813847 }
814848
849+ async #resolveSecretReferences( secretReferences : ConfigurationSetting [ ] , resultHandler : ( key : string , value : unknown ) => void ) : Promise < void > {
850+ if ( this . #resolveSecretsInParallel) {
851+ const secretResolutionPromises : Promise < void > [ ] = [ ] ;
852+ for ( const setting of secretReferences ) {
853+ const secretResolutionPromise = this . #processKeyValue( setting )
854+ . then ( ( [ key , value ] ) => {
855+ resultHandler ( key , value ) ;
856+ } ) ;
857+ secretResolutionPromises . push ( secretResolutionPromise ) ;
858+ }
859+
860+ // Wait for all secret resolution promises to be resolved
861+ await Promise . all ( secretResolutionPromises ) ;
862+ } else {
863+ for ( const setting of secretReferences ) {
864+ const [ key , value ] = await this . #processKeyValue( setting ) ;
865+ resultHandler ( key , value ) ;
866+ }
867+ }
868+ }
869+
815870 async #processKeyValue( setting : ConfigurationSetting < string > ) : Promise < [ string , unknown ] > {
816871 this . #setAIConfigurationTracing( setting ) ;
817872
0 commit comments