@@ -41,6 +41,7 @@ import { FeatureFlagTracingOptions } from "./requestTracing/FeatureFlagTracingOp
4141import { KeyFilter , LabelFilter , SettingSelector } from "./types.js" ;
4242import { ConfigurationClientManager } from "./ConfigurationClientManager.js" ;
4343import { getFixedBackoffDuration , calculateDynamicBackoffDuration } from "./failover.js" ;
44+ import { FailoverError , OperationError , isFailoverableError } from "./error.js" ;
4445
4546type PagedSettingSelector = SettingSelector & {
4647 /**
@@ -50,7 +51,6 @@ type PagedSettingSelector = SettingSelector & {
5051} ;
5152
5253const DEFAULT_STARTUP_TIMEOUT = 100 * 1000 ; // 100 seconds in milliseconds
53- const MAX_STARTUP_TIMEOUT = 60 * 60 * 1000 ; // 60 minutes in milliseconds
5454
5555export class AzureAppConfigurationImpl implements AzureAppConfiguration {
5656 /**
@@ -127,10 +127,10 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
127127 } else {
128128 for ( const setting of watchedSettings ) {
129129 if ( setting . key . includes ( "*" ) || setting . key . includes ( "," ) ) {
130- throw new Error ( "The characters '*' and ',' are not supported in key of watched settings." ) ;
130+ throw new RangeError ( "The characters '*' and ',' are not supported in key of watched settings." ) ;
131131 }
132132 if ( setting . label ?. includes ( "*" ) || setting . label ?. includes ( "," ) ) {
133- throw new Error ( "The characters '*' and ',' are not supported in label of watched settings." ) ;
133+ throw new RangeError ( "The characters '*' and ',' are not supported in label of watched settings." ) ;
134134 }
135135 this . #sentinels. push ( setting ) ;
136136 }
@@ -139,7 +139,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
139139 // custom refresh interval
140140 if ( refreshIntervalInMs !== undefined ) {
141141 if ( refreshIntervalInMs < MIN_REFRESH_INTERVAL_IN_MS ) {
142- throw new Error ( `The refresh interval cannot be less than ${ MIN_REFRESH_INTERVAL_IN_MS } milliseconds.` ) ;
142+ throw new RangeError ( `The refresh interval cannot be less than ${ MIN_REFRESH_INTERVAL_IN_MS } milliseconds.` ) ;
143143 } else {
144144 this . #kvRefreshInterval = refreshIntervalInMs ;
145145 }
@@ -157,7 +157,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
157157 // custom refresh interval
158158 if ( refreshIntervalInMs !== undefined ) {
159159 if ( refreshIntervalInMs < MIN_REFRESH_INTERVAL_IN_MS ) {
160- throw new Error ( `The feature flag refresh interval cannot be less than ${ MIN_REFRESH_INTERVAL_IN_MS } milliseconds.` ) ;
160+ throw new RangeError ( `The feature flag refresh interval cannot be less than ${ MIN_REFRESH_INTERVAL_IN_MS } milliseconds.` ) ;
161161 } else {
162162 this . #ffRefreshInterval = refreshIntervalInMs ;
163163 }
@@ -233,17 +233,29 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
233233 * Loads the configuration store for the first time.
234234 */
235235 async load ( ) {
236- const startupTimeout = this . #options?. startupOptions ?. timeoutInMs ?? DEFAULT_STARTUP_TIMEOUT ;
237- let timer ;
236+ const startupTimeout : number = this . #options?. startupOptions ?. timeoutInMs ?? DEFAULT_STARTUP_TIMEOUT ;
237+ const abortController = new AbortController ( ) ;
238+ const abortSignal = abortController . signal ;
239+ let timeoutId ;
238240 try {
241+ // Promise.race will be settled when the first promise in the list is settled
242+ // It will not cancel the remaining promises in the list.
243+ // To avoid memory leaks, we need to cancel other promises when one promise is settled.
239244 await Promise . race ( [
240- new Promise ( ( _ , reject ) => timer = setTimeout ( ( ) => reject ( new Error ( "Load operation timed out." ) ) , startupTimeout ) ) ,
241- this . #initialize( )
245+ this . #initializeWithRetryPolicy( abortSignal ) ,
246+ // this promise will be rejected after timeout
247+ new Promise ( ( _ , reject ) => {
248+ timeoutId = setTimeout ( ( ) => {
249+ abortController . abort ( ) ; // abort the initialization promise
250+ reject ( new Error ( "Load operation timed out." ) ) ;
251+ } ,
252+ startupTimeout ) ;
253+ } )
242254 ] ) ;
243255 } catch ( error ) {
244256 throw new Error ( `Failed to load: ${ error . message } ` ) ;
245257 } finally {
246- clearTimeout ( timer ) ;
258+ clearTimeout ( timeoutId ) ; // cancel the timeout promise
247259 }
248260 }
249261
@@ -254,7 +266,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
254266 const separator = options ?. separator ?? "." ;
255267 const validSeparators = [ "." , "," , ";" , "-" , "_" , "__" , "/" , ":" ] ;
256268 if ( ! validSeparators . includes ( separator ) ) {
257- throw new Error ( `Invalid separator '${ separator } '. Supported values: ${ validSeparators . map ( s => `'${ s } '` ) . join ( ", " ) } .` ) ;
269+ throw new RangeError ( `Invalid separator '${ separator } '. Supported values: ${ validSeparators . map ( s => `'${ s } '` ) . join ( ", " ) } .` ) ;
258270 }
259271
260272 // construct hierarchical data object from map
@@ -267,22 +279,22 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
267279 const segment = segments [ i ] ;
268280 // undefined or empty string
269281 if ( ! segment ) {
270- throw new Error ( `invalid key: ${ key } `) ;
282+ throw new OperationError ( `Failed to construct configuration object: Invalid key: ${ key } `) ;
271283 }
272284 // create path if not exist
273285 if ( current [ segment ] === undefined ) {
274286 current [ segment ] = { } ;
275287 }
276288 // The path has been occupied by a non-object value, causing ambiguity.
277289 if ( typeof current [ segment ] !== "object" ) {
278- throw new Error ( `Ambiguity occurs when constructing configuration object from key '${ key } ', value '${ value } '. The path '${ segments . slice ( 0 , i + 1 ) . join ( separator ) } ' has been occupied.` ) ;
290+ throw new OperationError ( `Ambiguity occurs when constructing configuration object from key '${ key } ', value '${ value } '. The path '${ segments . slice ( 0 , i + 1 ) . join ( separator ) } ' has been occupied.` ) ;
279291 }
280292 current = current [ segment ] ;
281293 }
282294
283295 const lastSegment = segments [ segments . length - 1 ] ;
284296 if ( current [ lastSegment ] !== undefined ) {
285- throw new Error ( `Ambiguity occurs when constructing configuration object from key '${ key } ', value '${ value } '. The key should not be part of another key.` ) ;
297+ throw new OperationError ( `Ambiguity occurs when constructing configuration object from key '${ key } ', value '${ value } '. The key should not be part of another key.` ) ;
286298 }
287299 // set value to the last segment
288300 current [ lastSegment ] = value ;
@@ -295,7 +307,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
295307 */
296308 async refresh ( ) : Promise < void > {
297309 if ( ! this . #refreshEnabled && ! this . #featureFlagRefreshEnabled) {
298- throw new Error ( "Refresh is not enabled for key-values or feature flags." ) ;
310+ throw new OperationError ( "Refresh is not enabled for key-values or feature flags." ) ;
299311 }
300312
301313 if ( this . #refreshInProgress) {
@@ -314,7 +326,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
314326 */
315327 onRefresh ( listener : ( ) => any , thisArg ?: any ) : Disposable {
316328 if ( ! this . #refreshEnabled && ! this . #featureFlagRefreshEnabled) {
317- throw new Error ( "Refresh is not enabled for key-values or feature flags." ) ;
329+ throw new OperationError ( "Refresh is not enabled for key-values or feature flags." ) ;
318330 }
319331
320332 const boundedListener = listener . bind ( thisArg ) ;
@@ -332,12 +344,11 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
332344 /**
333345 * Initializes the configuration provider.
334346 */
335- async #initialize ( ) {
347+ async #initializeWithRetryPolicy ( abortSignal : AbortSignal ) {
336348 if ( ! this . #isInitialLoadCompleted) {
337349 await this . #inspectFmPackage( ) ;
338- const retryEnabled = this . #options?. startupOptions ?. retryEnabled ?? true ; // enable startup retry by default
339350 const startTimestamp = Date . now ( ) ;
340- while ( startTimestamp + MAX_STARTUP_TIMEOUT > Date . now ( ) ) {
351+ while ( ! abortSignal . aborted ) {
341352 try {
342353 await this . #loadSelectedAndWatchedKeyValues( ) ;
343354 if ( this . #featureFlagEnabled) {
@@ -347,24 +358,17 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
347358 this . #isInitialLoadCompleted = true ;
348359 break ;
349360 } catch ( error ) {
350- if ( retryEnabled ) {
351- const timeElapsed = Date . now ( ) - startTimestamp ;
352- let postAttempts = 0 ;
353- let backoffDuration = getFixedBackoffDuration ( timeElapsed ) ;
354- if ( backoffDuration === undefined ) {
355- postAttempts += 1 ;
356- backoffDuration = calculateDynamicBackoffDuration ( postAttempts ) ;
357- }
358- await new Promise ( resolve => setTimeout ( resolve , backoffDuration ) ) ;
359- console . warn ( "Failed to load configuration settings at startup. Retrying..." ) ;
360- } else {
361- throw error ;
361+ const timeElapsed = Date . now ( ) - startTimestamp ;
362+ let postAttempts = 0 ;
363+ let backoffDuration = getFixedBackoffDuration ( timeElapsed ) ;
364+ if ( backoffDuration === undefined ) {
365+ postAttempts += 1 ;
366+ backoffDuration = calculateDynamicBackoffDuration ( postAttempts ) ;
362367 }
368+ await new Promise ( resolve => setTimeout ( resolve , backoffDuration ) ) ;
369+ console . warn ( "Failed to load configuration settings at startup. Retrying..." ) ;
363370 }
364371 }
365- if ( ! this . #isInitialLoadCompleted) {
366- throw new Error ( "Load operation exceeded the maximum startup timeout limitation." ) ;
367- }
368372 }
369373 }
370374
@@ -687,7 +691,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
687691 }
688692
689693 this . #clientManager. refreshClients ( ) ;
690- throw new Error ( "All clients failed to get configuration settings." ) ;
694+ throw new FailoverError ( "All fallback clients failed to get configuration settings." ) ;
691695 }
692696
693697 async #processKeyValues( setting : ConfigurationSetting < string > ) : Promise < [ string , unknown ] > {
@@ -719,7 +723,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
719723 async #parseFeatureFlag( setting : ConfigurationSetting < string > ) : Promise < any > {
720724 const rawFlag = setting . value ;
721725 if ( rawFlag === undefined ) {
722- throw new Error ( "The value of configuration setting cannot be undefined." ) ;
726+ throw new RangeError ( "The value of configuration setting cannot be undefined." ) ;
723727 }
724728 const featureFlag = JSON . parse ( rawFlag ) ;
725729
@@ -943,13 +947,13 @@ function getValidSelectors(selectors: SettingSelector[]): SettingSelector[] {
943947 return uniqueSelectors . map ( selectorCandidate => {
944948 const selector = { ...selectorCandidate } ;
945949 if ( ! selector . keyFilter ) {
946- throw new Error ( "Key filter cannot be null or empty." ) ;
950+ throw new RangeError ( "Key filter cannot be null or empty." ) ;
947951 }
948952 if ( ! selector . labelFilter ) {
949953 selector . labelFilter = LabelFilter . Null ;
950954 }
951955 if ( selector . labelFilter . includes ( "*" ) || selector . labelFilter . includes ( "," ) ) {
952- throw new Error ( "The characters '*' and ',' are not supported in label filters." ) ;
956+ throw new RangeError ( "The characters '*' and ',' are not supported in label filters." ) ;
953957 }
954958 return selector ;
955959 } ) ;
@@ -973,9 +977,3 @@ function getValidFeatureFlagSelectors(selectors?: SettingSelector[]): SettingSel
973977 } ) ;
974978 return getValidSelectors ( selectors ) ;
975979}
976-
977- function isFailoverableError ( error : any ) : boolean {
978- // ENOTFOUND: DNS lookup failed, ENOENT: no such file or directory
979- return isRestError ( error ) && ( error . code === "ENOTFOUND" || error . code === "ENOENT" ||
980- ( error . statusCode !== undefined && ( error . statusCode === 401 || error . statusCode === 403 || error . statusCode === 408 || error . statusCode === 429 || error . statusCode >= 500 ) ) ) ;
981- }
0 commit comments