@@ -13,9 +13,10 @@ import {
1313 isSecretReference ,
1414 GetSnapshotOptions ,
1515 GetSnapshotResponse ,
16- KnownSnapshotComposition
16+ KnownSnapshotComposition ,
17+ ListConfigurationSettingPage
1718} from "@azure/app-configuration" ;
18- import { isRestError } from "@azure/core-rest-pipeline" ;
19+ import { isRestError , RestError } from "@azure/core-rest-pipeline" ;
1920import { AzureAppConfiguration , ConfigurationObjectConstructionOptions } from "./appConfiguration.js" ;
2021import { AzureAppConfigurationOptions } from "./appConfigurationOptions.js" ;
2122import { IKeyValueAdapter } from "./keyValueAdapter.js" ;
@@ -66,6 +67,7 @@ import { ConfigurationClientManager } from "./configurationClientManager.js";
6667import { getFixedBackoffDuration , getExponentialBackoffDuration } from "./common/backoffUtils.js" ;
6768import { InvalidOperationError , ArgumentError , isFailoverableError , isInputError } from "./common/errors.js" ;
6869import { ErrorMessages } from "./common/errorMessages.js" ;
70+ import { TIMESTAMP_HEADER } from "./cdn/constants.js" ;
6971
7072const MIN_DELAY_FOR_UNHANDLED_FAILURE = 5_000 ; // 5 seconds
7173
@@ -106,12 +108,16 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
106108 #watchAll: boolean = false ;
107109 #kvRefreshInterval: number = DEFAULT_REFRESH_INTERVAL_IN_MS ;
108110 #kvRefreshTimer: RefreshTimer ;
111+ #lastKvChangeDetected: Date = new Date ( 0 ) ;
112+ #kvRefreshIncompleted: boolean = false ;
109113
110114 // Feature flags
111115 #featureFlagEnabled: boolean = false ;
112116 #featureFlagRefreshEnabled: boolean = false ;
113117 #ffRefreshInterval: number = DEFAULT_REFRESH_INTERVAL_IN_MS ;
114118 #ffRefreshTimer: RefreshTimer ;
119+ #lastFfChangeDetected: Date = new Date ( 0 ) ;
120+ #ffRefreshIncompleted: boolean = false ;
115121
116122 // Key Vault references
117123 #secretRefreshEnabled: boolean = false ;
@@ -131,12 +137,17 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
131137 // Load balancing
132138 #lastSuccessfulEndpoint: string = "" ;
133139
140+ // CDN
141+ #isCdnUsed: boolean = false ;
142+
134143 constructor (
135144 clientManager : ConfigurationClientManager ,
136145 options : AzureAppConfigurationOptions | undefined ,
146+ isCdnUsed : boolean
137147 ) {
138148 this . #options = options ;
139149 this . #clientManager = clientManager ;
150+ this . #isCdnUsed = isCdnUsed ;
140151
141152 // enable request tracing if not opt-out
142153 this . #requestTracingEnabled = requestTracingEnabled ( ) ;
@@ -224,7 +235,8 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
224235 isFailoverRequest : this . #isFailoverRequest,
225236 featureFlagTracing : this . #featureFlagTracing,
226237 fmVersion : this . #fmVersion,
227- aiConfigurationTracing : this . #aiConfigurationTracing
238+ aiConfigurationTracing : this . #aiConfigurationTracing,
239+ isCdnUsed : this . #isCdnUsed
228240 } ;
229241 }
230242
@@ -498,6 +510,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
498510 JSON . stringify ( selectors )
499511 ) ;
500512
513+ let upToDate : boolean = true ;
501514 for ( const selector of selectorsToUpdate ) {
502515 if ( selector . snapshotName === undefined ) {
503516 const listOptions : ListConfigurationSettingsOptions = {
@@ -519,6 +532,9 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
519532 loadedSettings . set ( setting . key , setting ) ;
520533 }
521534 }
535+ const timestamp = this . #getResponseTimestamp( page ) ;
536+ // all pages must be later than last change detected to be considered up-to-date
537+ upToDate &&= ( timestamp > ( loadFeatureFlag ? this . #lastFfChangeDetected : this . #lastKvChangeDetected) ) ;
522538 }
523539 selector . pageEtags = pageEtags ;
524540 } else { // snapshot selector
@@ -547,8 +563,10 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
547563
548564 if ( loadFeatureFlag ) {
549565 this . #ffSelectors = selectorsToUpdate ;
566+ this . #ffRefreshIncompleted = ! upToDate ;
550567 } else {
551568 this . #kvSelectors = selectorsToUpdate ;
569+ this . #kvRefreshIncompleted = ! upToDate ;
552570 }
553571 return Array . from ( loadedSettings . values ( ) ) ;
554572 } ;
@@ -605,11 +623,11 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
605623 } else {
606624 // Send a request to retrieve key-value since it may be either not loaded or loaded with a different label or different casing
607625 const { key, label } = sentinel ;
608- const response = await this . #getConfigurationSetting( { key, label } ) ;
609- if ( response ) {
610- sentinel . etag = response . etag ;
611- } else {
626+ const response = await this . #getConfigurationSetting( { key, label } , { onlyIfChanged : false } ) ;
627+ if ( isRestError ( response ) ) { // watched key not found
612628 sentinel . etag = undefined ;
629+ } else {
630+ sentinel . etag = response . etag ;
613631 }
614632 }
615633 }
@@ -661,22 +679,36 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
661679 let needRefresh = false ;
662680 if ( this . #watchAll) {
663681 needRefresh = await this . #checkConfigurationSettingsChange( this . #kvSelectors) ;
664- }
665- for ( const sentinel of this . #sentinels. values ( ) ) {
666- const response = await this . #getConfigurationSetting( sentinel , {
667- onlyIfChanged : true
668- } ) ;
669-
670- if ( response ?. statusCode === 200 // created or changed
671- || ( response === undefined && sentinel . etag !== undefined ) // deleted
672- ) {
673- sentinel . etag = response ?. etag ; // update etag of the sentinel
674- needRefresh = true ;
675- break ;
682+ } else {
683+ const getOptions : GetConfigurationSettingOptions = {
684+ // send conditional request only when CDN is not used
685+ onlyIfChanged : ! this . #isCdnUsed
686+ } ;
687+ for ( const sentinel of this . #sentinels. values ( ) ) {
688+ const response : GetConfigurationSettingResponse | RestError =
689+ await this . #getConfigurationSetting( sentinel , getOptions ) ;
690+
691+ if ( isRestError ( response ) ) { // sentinel key not found
692+ if ( sentinel . etag !== undefined ) {
693+ // previously existed, now deleted
694+ sentinel . etag = undefined ;
695+ const timestamp = this . #getResponseTimestamp( response ) ;
696+ if ( timestamp > this . #lastKvChangeDetected) {
697+ this . #lastKvChangeDetected = timestamp ;
698+ }
699+ needRefresh = true ;
700+ break ;
701+ }
702+ } else if ( response . statusCode === 200 && sentinel . etag !== response ?. etag ) {
703+ // change detected
704+ sentinel . etag = response ?. etag ; // update etag of the sentinel
705+ needRefresh = true ;
706+ break ;
707+ }
676708 }
677709 }
678710
679- if ( needRefresh ) {
711+ if ( needRefresh || this . #kvRefreshIncompleted ) {
680712 for ( const adapter of this . #adapters) {
681713 await adapter . onChangeDetected ( ) ;
682714 }
@@ -697,8 +729,9 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
697729 return Promise . resolve ( false ) ;
698730 }
699731
700- const needRefresh = await this . #checkConfigurationSettingsChange( this . #ffSelectors) ;
701- if ( needRefresh ) {
732+ const refreshFeatureFlag = true ;
733+ const needRefresh = await this . #checkConfigurationSettingsChange( this . #ffSelectors, refreshFeatureFlag ) ;
734+ if ( needRefresh || this . #ffRefreshIncompleted) {
702735 await this . #loadFeatureFlags( ) ;
703736 }
704737
@@ -730,7 +763,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
730763 * @param selectors - The @see PagedSettingSelector of the kev-value collection.
731764 * @returns true if key-value collection has changed, false otherwise.
732765 */
733- async #checkConfigurationSettingsChange( selectors : PagedSettingSelector [ ] ) : Promise < boolean > {
766+ async #checkConfigurationSettingsChange( selectors : PagedSettingSelector [ ] , refreshFeatureFlag : boolean = false ) : Promise < boolean > {
734767 const funcToExecute = async ( client ) => {
735768 for ( const selector of selectors ) {
736769 if ( selector . snapshotName ) { // skip snapshot selector
@@ -739,20 +772,41 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
739772 const listOptions : ListConfigurationSettingsOptions = {
740773 keyFilter : selector . keyFilter ,
741774 labelFilter : selector . labelFilter ,
742- tagsFilter : selector . tagFilters ,
743- pageEtags : selector . pageEtags
775+ tagsFilter : selector . tagFilters
744776 } ;
745777
778+ if ( ! this . #isCdnUsed) {
779+ // if CDN is not used, add page etags to the listOptions to send conditional request
780+ listOptions . pageEtags = selector . pageEtags ;
781+ }
782+
746783 const pageIterator = listConfigurationSettingsWithTrace (
747784 this . #requestTraceOptions,
748785 client ,
749786 listOptions
750787 ) . byPage ( ) ;
751788
789+ if ( selector . pageEtags === undefined || selector . pageEtags . length === 0 ) {
790+ return true ; // no etag is retrieved from previous request, always refresh
791+ }
792+
793+ let i = 0 ;
752794 for await ( const page of pageIterator ) {
753- if ( page . _response . status === 200 ) { // created or changed
795+ if ( i >= selector . pageEtags . length || // new page
796+ ( page . _response . status === 200 && page . etag !== selector . pageEtags [ i ] ) ) { // page changed
797+ const timestamp = this . #getResponseTimestamp( page ) ;
798+ if ( refreshFeatureFlag ) {
799+ if ( timestamp > this . #lastFfChangeDetected) {
800+ this . #lastFfChangeDetected = timestamp ;
801+ }
802+ } else {
803+ if ( timestamp > this . #lastKvChangeDetected) {
804+ this . #lastKvChangeDetected = timestamp ;
805+ }
806+ }
754807 return true ;
755808 }
809+ i ++ ;
756810 }
757811 }
758812 return false ;
@@ -763,9 +817,9 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
763817 }
764818
765819 /**
766- * Gets a configuration setting by key and label.If the setting is not found, return undefine instead of throwing an error .
820+ * Gets a configuration setting by key and label. If the setting is not found, return the error instead of throwing it .
767821 */
768- async #getConfigurationSetting( configurationSettingId : ConfigurationSettingId , customOptions ?: GetConfigurationSettingOptions ) : Promise < GetConfigurationSettingResponse | undefined > {
822+ async #getConfigurationSetting( configurationSettingId : ConfigurationSettingId , customOptions ?: GetConfigurationSettingOptions ) : Promise < GetConfigurationSettingResponse | RestError > {
769823 const funcToExecute = async ( client ) => {
770824 return getConfigurationSettingWithTrace (
771825 this . #requestTraceOptions,
@@ -775,12 +829,13 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
775829 ) ;
776830 } ;
777831
778- let response : GetConfigurationSettingResponse | undefined ;
832+ let response : GetConfigurationSettingResponse | RestError ;
779833 try {
780834 response = await this . #executeWithFailoverPolicy( funcToExecute ) ;
781835 } catch ( error ) {
782836 if ( isRestError ( error ) && error . statusCode === 404 ) {
783- response = undefined ;
837+ // configuration setting not found, return the error
838+ return error ;
784839 } else {
785840 throw error ;
786841 }
@@ -1088,6 +1143,16 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
10881143 return first15Bytes . toString ( "base64url" ) ;
10891144 }
10901145 }
1146+
1147+ #getResponseTimestamp( response : GetConfigurationSettingResponse | ListConfigurationSettingPage | RestError ) : Date {
1148+ let header : string | undefined ;
1149+ if ( isRestError ( response ) ) {
1150+ header = response . response ?. headers . get ( TIMESTAMP_HEADER ) ?? undefined ;
1151+ } else {
1152+ header = response . _response . headers . get ( TIMESTAMP_HEADER ) ?? undefined ;
1153+ }
1154+ return header ? new Date ( header ) : new Date ( ) ;
1155+ }
10911156}
10921157
10931158function getValidSettingSelectors ( selectors : SettingSelector [ ] ) : SettingSelector [ ] {
0 commit comments