11// Copyright (c) Microsoft Corporation.
22// Licensed under the MIT license.
33
4- import { AppConfigurationClient , ConfigurationSetting , ListConfigurationSettingsOptions } from "@azure/app-configuration" ;
4+ import { AppConfigurationClient , ConfigurationSetting , ConfigurationSettingId , ListConfigurationSettingsOptions } from "@azure/app-configuration" ;
55import { AzureAppConfiguration } from "./AzureAppConfiguration" ;
66import { AzureAppConfigurationOptions } from "./AzureAppConfigurationOptions" ;
77import { IKeyValueAdapter } from "./IKeyValueAdapter" ;
@@ -24,9 +24,10 @@ export class AzureAppConfigurationImpl extends Map<string, unknown> implements A
2424 private sortedTrimKeyPrefixes : string [ ] | undefined ;
2525 private readonly requestTracingEnabled : boolean ;
2626 // Refresh
27- private refreshIntervalInMs : number ;
28- private onRefreshListeners : LinkedList < ( ) => any > ;
27+ private refreshIntervalInMs : number | undefined ;
28+ private onRefreshListeners : LinkedList < ( ) => any > | undefined ;
2929 private lastUpdateTimestamp : number ;
30+ private sentinels : ConfigurationSettingId [ ] | undefined ;
3031
3132 constructor (
3233 private client : AppConfigurationClient ,
@@ -52,6 +53,15 @@ export class AzureAppConfigurationImpl extends Map<string, unknown> implements A
5253 this . refreshIntervalInMs = refreshIntervalInMs ;
5354 }
5455 }
56+
57+ this . sentinels = options . refreshOptions . watchedSettings ?. map ( setting => {
58+ const key = setting . key ;
59+ const label = setting . label ;
60+ if ( key . includes ( "*" ) || label ?. includes ( "*" ) ) {
61+ throw new Error ( "Wildcard key or label filters are not supported for refresh." ) ;
62+ }
63+ return { key, label } ;
64+ } ) ;
5565 }
5666
5767 // TODO: should add more adapters to process different type of values
@@ -84,6 +94,11 @@ export class AzureAppConfigurationImpl extends Map<string, unknown> implements A
8494 const keyValuePair = await this . processKeyValues ( setting ) ;
8595 keyValues . push ( keyValuePair ) ;
8696 }
97+ // update etag of sentinels
98+ const matchedSentinel = this . sentinels ?. find ( s => s . key === setting . key && ( s . label ?? null ) === setting . label ) ; // Workaround: as undefined label represents the same with null.
99+ if ( matchedSentinel ) {
100+ matchedSentinel . etag = setting . etag ;
101+ }
87102 }
88103 }
89104 for ( const [ k , v ] of keyValues ) {
@@ -94,7 +109,7 @@ export class AzureAppConfigurationImpl extends Map<string, unknown> implements A
94109
95110 public async refresh ( ) : Promise < void > {
96111 // if no refreshOptions set, return
97- if ( this . options ?. refreshOptions === undefined || this . options . refreshOptions . watchedSettings . length === 0 ) {
112+ if ( this . sentinels === undefined || this . sentinels . length === 0 || this . refreshIntervalInMs === undefined ) {
98113 return Promise . resolve ( ) ;
99114 }
100115 // if still within refresh interval, return
@@ -104,26 +119,35 @@ export class AzureAppConfigurationImpl extends Map<string, unknown> implements A
104119 }
105120
106121 // try refresh if any of watched settings is changed.
107- // TODO: watchedSettings as optional, etag based refresh if not specified.
108122 let needRefresh = false ;
109- for ( const watchedSetting of this . options . refreshOptions . watchedSettings ) {
110- const response = await this . client . getConfigurationSetting ( watchedSetting ) ;
111- const [ key , value ] = await this . processKeyValues ( response ) ;
112- if ( value !== this . get ( key ) ) {
123+ for ( const sentinel of this . sentinels ) {
124+ const response = await this . client . getConfigurationSetting ( sentinel , {
125+ onlyIfChanged : true
126+ // TODO: do we trace this request by adding custom headers?
127+ } ) ;
128+ if ( response . statusCode !== 304 ) { // TODO: can be more robust, e.g. === 200?
129+ // sentinel changed.
130+ sentinel . etag = response . etag ; // update etag of the sentinel
113131 needRefresh = true ;
114132 break ;
115133 }
116134 }
117135 if ( needRefresh ) {
118136 await this . load ( RequestType . Watch ) ;
119137 // run callbacks in async
120- for ( const listener of this . onRefreshListeners ) {
121- listener ( ) ;
138+ if ( this . onRefreshListeners !== undefined ) {
139+ for ( const listener of this . onRefreshListeners ) {
140+ listener ( ) ;
141+ }
122142 }
123143 }
124144 }
125145
126146 public onRefresh ( listener : ( ) => any , thisArg ?: any ) : Disposable {
147+ if ( this . onRefreshListeners === undefined ) {
148+ // TODO: Add unit tests
149+ throw new Error ( "Illegal operation because refreshOptions is not provided on loading." ) ;
150+ }
127151 const boundedListener = listener . bind ( thisArg ) ;
128152 const remove = this . onRefreshListeners . push ( boundedListener ) ;
129153 return new Disposable ( remove ) ;
0 commit comments