@@ -74,11 +74,16 @@ function nameOfConfigServiceStatus(value: ConfigServiceStatus): string {
74
74
return ConfigServiceStatus [ value ] as string ;
75
75
}
76
76
77
+ type ConfigRefreshOperation = {
78
+ promise : Promise < [ FetchResult , ProjectConfig ] > ;
79
+ latestConfig : ProjectConfig ;
80
+ } ;
81
+
77
82
export abstract class ConfigServiceBase < TOptions extends OptionsBase > {
78
83
private status : ConfigServiceStatus ;
79
84
80
85
private pendingCacheSyncUp : Promise < ProjectConfig > | null = null ;
81
- private pendingFetch : Promise < FetchResult > | null = null ;
86
+ private pendingConfigRefresh : ConfigRefreshOperation | null = null ;
82
87
83
88
protected readonly cacheKey : string ;
84
89
@@ -119,25 +124,57 @@ export abstract class ConfigServiceBase<TOptions extends OptionsBase> {
119
124
}
120
125
121
126
protected async refreshConfigCoreAsync ( latestConfig : ProjectConfig , isInitiatedByUser : boolean ) : Promise < [ FetchResult , ProjectConfig ] > {
122
- const fetchResult = await this . fetchAsync ( latestConfig ) ;
123
-
124
- let configChanged = false ;
125
- const success = fetchResult . status === FetchStatus . Fetched ;
126
- if ( success
127
- || fetchResult . config . timestamp > latestConfig . timestamp && ( ! fetchResult . config . isEmpty || latestConfig . isEmpty ) ) {
128
- await this . options . cache . set ( this . cacheKey , fetchResult . config ) ;
127
+ let refreshOperation = this . pendingConfigRefresh ;
129
128
130
- configChanged = success && ! ProjectConfig . equals ( fetchResult . config , latestConfig ) ;
131
- latestConfig = fetchResult . config ;
129
+ if ( refreshOperation ) {
130
+ const { promise, latestConfig : knownLatestConfig } = refreshOperation ;
131
+ if ( latestConfig . timestamp > knownLatestConfig . timestamp && ( ! latestConfig . isEmpty || knownLatestConfig . isEmpty ) ) {
132
+ refreshOperation . latestConfig = latestConfig ;
133
+ }
134
+ return promise ;
132
135
}
133
136
134
- this . onConfigFetched ( fetchResult , isInitiatedByUser ) ;
137
+ refreshOperation = { latestConfig } as ConfigRefreshOperation ;
138
+ refreshOperation . promise = ( async ( refreshOperation : ConfigRefreshOperation ) => {
139
+ const fetchResult = await this . fetchAsync ( refreshOperation . latestConfig ) ;
140
+
141
+ // NOTE: Further joiners may obtain more up-to-date configs from the external cache, and update
142
+ // operation.latestConfig before the operation completes, but those updates will be ignored.
143
+ // In other words, the operation may not return the most recent config obtained during its execution.
144
+ // However, this is acceptable, especially if we consider that reading and writing the external cache is
145
+ // not synchronized, which means that a more recent config can be overwritten with a stale one.
146
+ // (We don't make any effort to synchronize external cache access as that would be extremely hard,
147
+ // and we expect the "stuttering" resulting from this race condition to be temporary only.)
148
+ let { latestConfig } = refreshOperation ;
149
+
150
+ const success = fetchResult . status === FetchStatus . Fetched ;
151
+ if ( success
152
+ || fetchResult . config . timestamp > latestConfig . timestamp && ( ! fetchResult . config . isEmpty || latestConfig . isEmpty ) ) {
153
+ await this . options . cache . set ( this . cacheKey , fetchResult . config ) ;
154
+
155
+ latestConfig = fetchResult . config ;
156
+ }
135
157
136
- if ( configChanged ) {
137
- this . onConfigChanged ( fetchResult . config ) ;
138
- }
158
+ return [ fetchResult , latestConfig ] ;
159
+ } ) ( refreshOperation ) ;
160
+
161
+ const refreshAndFinish = refreshOperation . promise
162
+ . finally ( ( ) => this . pendingConfigRefresh = null )
163
+ . then ( refreshResult => {
164
+ const [ fetchResult ] = refreshResult ;
139
165
140
- return [ fetchResult , latestConfig ] ;
166
+ this . onConfigFetched ( fetchResult , isInitiatedByUser ) ;
167
+
168
+ if ( fetchResult . status === FetchStatus . Fetched ) {
169
+ this . onConfigChanged ( fetchResult . config ) ;
170
+ }
171
+
172
+ return refreshResult ;
173
+ } ) ;
174
+
175
+ this . pendingConfigRefresh = refreshOperation ;
176
+
177
+ return refreshAndFinish ;
141
178
}
142
179
143
180
protected onConfigFetched ( fetchResult : FetchResult , isInitiatedByUser : boolean ) : void {
@@ -150,12 +187,7 @@ export abstract class ConfigServiceBase<TOptions extends OptionsBase> {
150
187
this . options . hooks . emit ( "configChanged" , newConfig . config ?? new Config ( { } ) ) ;
151
188
}
152
189
153
- private fetchAsync ( lastConfig : ProjectConfig ) : Promise < FetchResult > {
154
- return this . pendingFetch ??= this . fetchLogicAsync ( lastConfig )
155
- . finally ( ( ) => this . pendingFetch = null ) ;
156
- }
157
-
158
- private async fetchLogicAsync ( lastConfig : ProjectConfig ) : Promise < FetchResult > {
190
+ private async fetchAsync ( lastConfig : ProjectConfig ) : Promise < FetchResult > {
159
191
const options = this . options ;
160
192
options . logger . debug ( "ConfigServiceBase.fetchLogicAsync() called." ) ;
161
193
0 commit comments