@@ -25,21 +25,18 @@ export type FeaturesOptions = {
2525 fallbackFeatures ?: string [ ] ;
2626 timeoutMs ?: number ;
2727 staleWhileRevalidate ?: boolean ;
28- failureRetryAttempts ?: number | false ;
2928} ;
3029
3130type Config = {
3231 fallbackFeatures : string [ ] ;
3332 timeoutMs : number ;
3433 staleWhileRevalidate : boolean ;
35- failureRetryAttempts : number | false ;
3634} ;
3735
3836export const DEFAULT_FEATURES_CONFIG : Config = {
3937 fallbackFeatures : [ ] ,
4038 timeoutMs : 5000 ,
4139 staleWhileRevalidate : false ,
42- failureRetryAttempts : false ,
4340} ;
4441
4542// Deep merge two objects.
@@ -108,7 +105,7 @@ export function clearFeatureCache() {
108105}
109106
110107export const FEATURES_STALE_MS = 60000 ; // turn stale after 60 seconds, optionally reevaluate in the background
111- export const FEATURES_EXPIRE_MS = 7 * 24 * 60 * 60 * 1000 ; // expire entirely after 7 days
108+ export const FEATURES_EXPIRE_MS = 30 * 24 * 60 * 60 * 1000 ; // expire entirely after 30 days
112109
113110const localStorageCacheKey = `__bucket_features` ;
114111
@@ -147,6 +144,10 @@ export class FeaturesClient {
147144
148145 async initialize ( ) {
149146 const features = ( await this . maybeFetchFeatures ( ) ) || { } ;
147+ this . setFeatures ( features ) ;
148+ }
149+
150+ private setFeatures ( features : APIFeaturesResponse ) {
150151 const proxiedFeatures = maskedProxy ( features , ( fs , key ) => {
151152 this . sendCheckEvent ( {
152153 key,
@@ -177,40 +178,60 @@ export class FeaturesClient {
177178 }
178179
179180 private async maybeFetchFeatures ( ) : Promise < APIFeaturesResponse | undefined > {
180- const cachedItem = this . cache . get ( this . fetchParams ( ) . toString ( ) ) ;
181-
182- // if there's no cached item OR the cached item is a failure and we haven't retried
183- // too many times yet - fetch now
184- if (
185- ! cachedItem ||
186- ( ! cachedItem . success &&
187- ( this . config . failureRetryAttempts === false ||
188- cachedItem . attemptCount < this . config . failureRetryAttempts ) )
189- ) {
190- return await this . fetchFeatures ( ) ;
191- }
181+ const cacheKey = this . fetchParams ( ) . toString ( ) ;
182+ const cachedItem = this . cache . get ( cacheKey ) ;
183+
184+ if ( cachedItem ) {
185+ if ( ! cachedItem . stale ) return cachedItem . features ;
192186
193- // cachedItem is a success or a failed attempt that we've retried too many times
194- if ( cachedItem . stale ) {
195187 // serve successful stale cache if `staleWhileRevalidate` is enabled
196- if ( this . config . staleWhileRevalidate && cachedItem . success ) {
197- // re-fetch in the background, return last successful value
198- this . fetchFeatures ( ) . catch ( ( ) => {
199- // we don't care about the result, we just want to re-fetch
200- } ) ;
188+ if ( this . config . staleWhileRevalidate ) {
189+ // re-fetch in the background, but immediately return last successful value
190+ this . fetchFeatures ( )
191+ . then ( ( features ) => {
192+ if ( ! features ) return ;
193+
194+ this . cache . set ( cacheKey , {
195+ features,
196+ } ) ;
197+ this . setFeatures ( features ) ;
198+ } )
199+ . catch ( ( ) => {
200+ // we don't care about the result, we just want to re-fetch
201+ } ) ;
201202 return cachedItem . features ;
202203 }
204+ }
205+
206+ // if there's no cached item or there is a stale one but `staleWhileRevalidate` is disabled
207+ // try fetching a new one
208+ const fetchedFeatures = await this . fetchFeatures ( ) ;
209+
210+ if ( fetchedFeatures ) {
211+ this . cache . set ( cacheKey , {
212+ features : fetchedFeatures ,
213+ } ) ;
214+
215+ return fetchedFeatures ;
216+ }
203217
204- return await this . fetchFeatures ( ) ;
218+ if ( cachedItem ) {
219+ // fetch failed, return stale cache
220+ return cachedItem . features ;
205221 }
206222
207- // serve cached items if not stale and not expired
208- return cachedItem . features ;
223+ // fetch failed, nothing cached => return fallbacks
224+ return this . config . fallbackFeatures . reduce ( ( acc , key ) => {
225+ acc [ key ] = {
226+ key,
227+ isEnabled : true ,
228+ } ;
229+ return acc ;
230+ } , { } as APIFeaturesResponse ) ;
209231 }
210232
211- public async fetchFeatures ( ) : Promise < APIFeaturesResponse > {
233+ public async fetchFeatures ( ) : Promise < APIFeaturesResponse | undefined > {
212234 const params = this . fetchParams ( ) ;
213- const cacheKey = params . toString ( ) ;
214235 try {
215236 const res = await this . httpClient . get ( {
216237 path : "/features/enabled" ,
@@ -238,41 +259,10 @@ export class FeaturesClient {
238259 throw new Error ( "unable to validate response" ) ;
239260 }
240261
241- this . cache . set ( cacheKey , {
242- success : true ,
243- features : typeRes . features ,
244- attemptCount : 0 ,
245- } ) ;
246-
247262 return typeRes . features ;
248263 } catch ( e ) {
249264 this . logger . error ( "error fetching features: " , e ) ;
250-
251- const current = this . cache . get ( cacheKey ) ;
252- if ( current ) {
253- // if there is a previous failure cached, increase the attempt count
254- this . cache . set ( cacheKey , {
255- success : current . success ,
256- features : current . features ,
257- attemptCount : current . attemptCount + 1 ,
258- } ) ;
259- } else {
260- // otherwise cache if the request failed and there is no previous version to extend
261- // to avoid having the UI wait and spam the API
262- this . cache . set ( cacheKey , {
263- success : false ,
264- features : undefined ,
265- attemptCount : 1 ,
266- } ) ;
267- }
268-
269- return this . config . fallbackFeatures . reduce ( ( acc , key ) => {
270- acc [ key ] = {
271- key,
272- isEnabled : true ,
273- } ;
274- return acc ;
275- } , { } as APIFeaturesResponse ) ;
265+ return ;
276266 }
277267 }
278268
0 commit comments