1
1
// Copyright (c) Microsoft Corporation.
2
2
// Licensed under the MIT license.
3
3
4
- import { AppConfigurationClient , ConfigurationSetting , ConfigurationSettingId , GetConfigurationSettingResponse , ListConfigurationSettingsOptions } from "@azure/app-configuration" ;
4
+ import { AppConfigurationClient , ConfigurationSetting , ConfigurationSettingId , GetConfigurationSettingOptions , GetConfigurationSettingResponse , ListConfigurationSettingsOptions } from "@azure/app-configuration" ;
5
5
import { RestError } from "@azure/core-rest-pipeline" ;
6
6
import { AzureAppConfiguration } from "./AzureAppConfiguration" ;
7
7
import { AzureAppConfigurationOptions } from "./AzureAppConfigurationOptions" ;
@@ -15,6 +15,11 @@ import { CorrelationContextHeaderName, RequestType } from "./requestTracing/cons
15
15
import { createCorrelationContextHeader , requestTracingEnabled } from "./requestTracing/utils" ;
16
16
import { KeyFilter , LabelFilter , SettingSelector } from "./types" ;
17
17
18
+ type KeyValueIdentifier = string ; // key::label
19
+ function toKeyValueIderntifier ( key : string , label : string | undefined ) : KeyValueIdentifier {
20
+ return `${ key } ::${ label ?? "" } ` ;
21
+ }
22
+
18
23
export class AzureAppConfigurationImpl extends Map < string , any > implements AzureAppConfiguration {
19
24
#adapters: IKeyValueAdapter [ ] = [ ] ;
20
25
/**
@@ -29,7 +34,7 @@ export class AzureAppConfigurationImpl extends Map<string, any> implements Azure
29
34
// Refresh
30
35
#refreshInterval: number = DefaultRefreshIntervalInMs ;
31
36
#onRefreshListeners: Array < ( ) => any > = [ ] ;
32
- #sentinels: ConfigurationSettingId [ ] ;
37
+ #sentinels: Map < KeyValueIdentifier , ConfigurationSettingId > = new Map ( ) ;
33
38
#refreshTimer: RefreshTimer ;
34
39
35
40
constructor (
@@ -64,15 +69,16 @@ export class AzureAppConfigurationImpl extends Map<string, any> implements Azure
64
69
}
65
70
}
66
71
67
- this . #sentinels = watchedSettings . map ( setting => {
72
+ for ( const setting of watchedSettings ) {
68
73
if ( setting . key . includes ( "*" ) || setting . key . includes ( "," ) ) {
69
74
throw new Error ( "The characters '*' and ',' are not supported in key of watched settings." ) ;
70
75
}
71
76
if ( setting . label ?. includes ( "*" ) || setting . label ?. includes ( "," ) ) {
72
77
throw new Error ( "The characters '*' and ',' are not supported in label of watched settings." ) ;
73
78
}
74
- return { ...setting } ;
75
- } ) ;
79
+ const id = toKeyValueIderntifier ( setting . key , setting . label ) ;
80
+ this . #sentinels. set ( id , setting ) ;
81
+ }
76
82
77
83
this . #refreshTimer = new RefreshTimer ( this . #refreshInterval) ;
78
84
}
@@ -88,8 +94,8 @@ export class AzureAppConfigurationImpl extends Map<string, any> implements Azure
88
94
return ! ! this . #options?. refreshOptions ?. enabled ;
89
95
}
90
96
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 > ( ) ;
93
99
94
100
// validate selectors
95
101
const selectors = getValidSelectors ( this . #options?. selectors ) ;
@@ -108,19 +114,57 @@ export class AzureAppConfigurationImpl extends Map<string, any> implements Azure
108
114
const settings = this . #client. listConfigurationSettings ( listOptions ) ;
109
115
110
116
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 ;
121
148
}
122
149
}
123
150
}
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
+
124
168
this . clear ( ) ; // clear existing key-values in case of configuration setting deletion
125
169
for ( const [ k , v ] of keyValues ) {
126
170
this . set ( k , v ) ;
@@ -142,22 +186,10 @@ export class AzureAppConfigurationImpl extends Map<string, any> implements Azure
142
186
143
187
// try refresh if any of watched settings is changed.
144
188
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
+ } ) ;
161
193
162
194
if ( response === undefined || response . statusCode === 200 ) {
163
195
// sentinel deleted / changed.
@@ -235,6 +267,25 @@ export class AzureAppConfigurationImpl extends Map<string, any> implements Azure
235
267
headers [ CorrelationContextHeaderName ] = createCorrelationContextHeader ( this . #options, requestType ) ;
236
268
return headers ;
237
269
}
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
+ }
238
289
}
239
290
240
291
function getValidSelectors ( selectors ?: SettingSelector [ ] ) {
@@ -266,4 +317,4 @@ function getValidSelectors(selectors?: SettingSelector[]) {
266
317
}
267
318
return selector ;
268
319
} ) ;
269
- }
320
+ }
0 commit comments