11// Copyright (c) Microsoft Corporation.
22// Licensed under the MIT license.
33
4- import { AppConfigurationClient , ConfigurationSetting , ConfigurationSettingId , GetConfigurationSettingResponse , ListConfigurationSettingsOptions } from "@azure/app-configuration" ;
4+ import { AppConfigurationClient , ConfigurationSetting , ConfigurationSettingId , GetConfigurationSettingOptions , GetConfigurationSettingResponse , ListConfigurationSettingsOptions } from "@azure/app-configuration" ;
55import { RestError } from "@azure/core-rest-pipeline" ;
66import { AzureAppConfiguration } from "./AzureAppConfiguration" ;
77import { AzureAppConfigurationOptions } from "./AzureAppConfigurationOptions" ;
@@ -15,6 +15,11 @@ import { CorrelationContextHeaderName, RequestType } from "./requestTracing/cons
1515import { createCorrelationContextHeader , requestTracingEnabled } from "./requestTracing/utils" ;
1616import { KeyFilter , LabelFilter , SettingSelector } from "./types" ;
1717
18+ type KeyValueIdentifier = string ; // key::label
19+ function toKeyValueIderntifier ( key : string , label : string | undefined ) : KeyValueIdentifier {
20+ return `${ key } ::${ label ?? "" } ` ;
21+ }
22+
1823export class AzureAppConfigurationImpl extends Map < string , any > implements AzureAppConfiguration {
1924 #adapters: IKeyValueAdapter [ ] = [ ] ;
2025 /**
@@ -29,7 +34,7 @@ export class AzureAppConfigurationImpl extends Map<string, any> implements Azure
2934 // Refresh
3035 #refreshInterval: number = DefaultRefreshIntervalInMs ;
3136 #onRefreshListeners: Array < ( ) => any > = [ ] ;
32- #sentinels: ConfigurationSettingId [ ] ;
37+ #sentinels: Map < KeyValueIdentifier , ConfigurationSettingId > = new Map ( ) ;
3338 #refreshTimer: RefreshTimer ;
3439
3540 constructor (
@@ -64,15 +69,16 @@ export class AzureAppConfigurationImpl extends Map<string, any> implements Azure
6469 }
6570 }
6671
67- this . #sentinels = watchedSettings . map ( setting => {
72+ for ( const setting of watchedSettings ) {
6873 if ( setting . key . includes ( "*" ) || setting . key . includes ( "," ) ) {
6974 throw new Error ( "The characters '*' and ',' are not supported in key of watched settings." ) ;
7075 }
7176 if ( setting . label ?. includes ( "*" ) || setting . label ?. includes ( "," ) ) {
7277 throw new Error ( "The characters '*' and ',' are not supported in label of watched settings." ) ;
7378 }
74- return { ...setting } ;
75- } ) ;
79+ const id = toKeyValueIderntifier ( setting . key , setting . label ) ;
80+ this . #sentinels. set ( id , setting ) ;
81+ }
7682
7783 this . #refreshTimer = new RefreshTimer ( this . #refreshInterval) ;
7884 }
@@ -88,8 +94,8 @@ export class AzureAppConfigurationImpl extends Map<string, any> implements Azure
8894 return ! ! this . #options?. refreshOptions ?. enabled ;
8995 }
9096
91- async load ( requestType : RequestType = RequestType . Startup ) {
92- const keyValues : [ key : string , value : unknown ] [ ] = [ ] ;
97+ async #loadSelectedKeyValues ( requestType : RequestType ) : Promise < Map < KeyValueIdentifier , ConfigurationSetting > > {
98+ const loadedSettings = new Map < string , ConfigurationSetting > ( ) ;
9399
94100 // validate selectors
95101 const selectors = getValidSelectors ( this . #options?. selectors ) ;
@@ -108,19 +114,57 @@ export class AzureAppConfigurationImpl extends Map<string, any> implements Azure
108114 const settings = this . #client. listConfigurationSettings ( listOptions ) ;
109115
110116 for await ( const setting of settings ) {
111- if ( setting . key ) {
112- const keyValuePair = await this . #processKeyValues( setting ) ;
113- keyValues . push ( keyValuePair ) ;
114- }
115- // update etag of sentinels if refresh is enabled
116- if ( this . #refreshEnabled) {
117- const matchedSentinel = this . #sentinels. find ( s => s . key === setting . key && s . label === setting . label ) ;
118- if ( matchedSentinel ) {
119- matchedSentinel . etag = setting . etag ;
120- }
117+ const id = toKeyValueIderntifier ( setting . key , setting . label ) ;
118+ loadedSettings . set ( id , setting ) ;
119+ }
120+ }
121+ return loadedSettings ;
122+ }
123+
124+ /**
125+ * Load watched key-values from Azure App Configuration service if not coverred by the selectors. Update etag of sentinels.
126+ */
127+ async #loadWatchedKeyValues( requestType : RequestType , existingSettings : Map < KeyValueIdentifier , ConfigurationSetting > ) : Promise < Map < KeyValueIdentifier , ConfigurationSetting > > {
128+ const watchedSettings = new Map < KeyValueIdentifier , ConfigurationSetting > ( ) ;
129+
130+ if ( ! this . #refreshEnabled) {
131+ return watchedSettings ;
132+ }
133+
134+ for ( const [ id , sentinel ] of this . #sentinels) {
135+ const matchedSetting = existingSettings . get ( id ) ;
136+ if ( matchedSetting ) {
137+ watchedSettings . set ( id , matchedSetting ) ;
138+ sentinel . etag = matchedSetting . etag ;
139+ } else {
140+ // Send a request to retrieve key-value since it may be either not loaded or loaded with a different label or different casing
141+ const { key, label } = sentinel ;
142+ const response = await this . #getConfigurationSettingWithTrace( requestType , { key, label } ) ;
143+ if ( response ) {
144+ watchedSettings . set ( id , response ) ;
145+ sentinel . etag = response . etag ;
146+ } else {
147+ sentinel . etag = undefined ;
121148 }
122149 }
123150 }
151+ return watchedSettings ;
152+ }
153+
154+ async load ( requestType : RequestType = RequestType . Startup ) {
155+ const keyValues : [ key : string , value : unknown ] [ ] = [ ] ;
156+
157+ const loadedSettings = await this . #loadSelectedKeyValues( requestType ) ;
158+ const watchedSettings = await this . #loadWatchedKeyValues( requestType , loadedSettings ) ;
159+
160+ // process key-values, watched settings have higher priority
161+ for ( const setting of [ ...loadedSettings . values ( ) , ...watchedSettings . values ( ) ] ) {
162+ if ( setting . key ) {
163+ const [ key , value ] = await this . #processKeyValues( setting ) ;
164+ keyValues . push ( [ key , value ] ) ;
165+ }
166+ }
167+
124168 this . clear ( ) ; // clear existing key-values in case of configuration setting deletion
125169 for ( const [ k , v ] of keyValues ) {
126170 this . set ( k , v ) ;
@@ -142,22 +186,10 @@ export class AzureAppConfigurationImpl extends Map<string, any> implements Azure
142186
143187 // try refresh if any of watched settings is changed.
144188 let needRefresh = false ;
145- for ( const sentinel of this . #sentinels) {
146- let response : GetConfigurationSettingResponse | undefined ;
147- try {
148- response = await this . #client. getConfigurationSetting ( sentinel , {
149- onlyIfChanged : true ,
150- requestOptions : {
151- customHeaders : this . #customHeaders( RequestType . Watch )
152- }
153- } ) ;
154- } catch ( error ) {
155- if ( error instanceof RestError && error . statusCode === 404 ) {
156- response = undefined ;
157- } else {
158- throw error ;
159- }
160- }
189+ for ( const sentinel of this . #sentinels. values ( ) ) {
190+ const response = await this . #getConfigurationSettingWithTrace( RequestType . Watch , sentinel , {
191+ onlyIfChanged : true
192+ } ) ;
161193
162194 if ( response === undefined || response . statusCode === 200 ) {
163195 // sentinel deleted / changed.
@@ -235,6 +267,25 @@ export class AzureAppConfigurationImpl extends Map<string, any> implements Azure
235267 headers [ CorrelationContextHeaderName ] = createCorrelationContextHeader ( this . #options, requestType ) ;
236268 return headers ;
237269 }
270+
271+ async #getConfigurationSettingWithTrace( requestType : RequestType , configurationSettingId : ConfigurationSettingId , options ?: GetConfigurationSettingOptions ) : Promise < GetConfigurationSettingResponse | undefined > {
272+ let response : GetConfigurationSettingResponse | undefined ;
273+ try {
274+ response = await this . #client. getConfigurationSetting ( configurationSettingId , {
275+ ...options ?? { } ,
276+ requestOptions : {
277+ customHeaders : this . #customHeaders( requestType )
278+ }
279+ } ) ;
280+ } catch ( error ) {
281+ if ( error instanceof RestError && error . statusCode === 404 ) {
282+ response = undefined ;
283+ } else {
284+ throw error ;
285+ }
286+ }
287+ return response ;
288+ }
238289}
239290
240291function getValidSelectors ( selectors ?: SettingSelector [ ] ) {
@@ -266,4 +317,4 @@ function getValidSelectors(selectors?: SettingSelector[]) {
266317 }
267318 return selector ;
268319 } ) ;
269- }
320+ }
0 commit comments