Skip to content

Commit 357daf4

Browse files
authored
Simplify fetching logic (#176)
- Remove retry-logic that was too complicated. We will no longer cache negative responses at all. - Increase expire time from 7 to 30 days. - Don't block on LiveSatisfaction initialization
1 parent 329434a commit 357daf4

File tree

9 files changed

+144
-250
lines changed

9 files changed

+144
-250
lines changed

packages/browser-sdk/src/client.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ export class BucketClient {
7373
private httpClient: HttpClient;
7474

7575
private liveSatisfaction: LiveSatisfaction | undefined;
76+
private liveSatisfactionInit: Promise<void> | undefined;
7677
private featuresClient: FeaturesClient;
7778

7879
constructor(
@@ -135,11 +136,16 @@ export class BucketClient {
135136
* Must be called before calling other SDK methods.
136137
*/
137138
async initialize() {
138-
const inits = [this.featuresClient.initialize()];
139139
if (this.liveSatisfaction) {
140-
inits.push(this.liveSatisfaction.initialize());
140+
// do not block on live satisfaction initialization
141+
this.liveSatisfactionInit = this.liveSatisfaction
142+
.initialize()
143+
.catch((e) => {
144+
this.logger.error("error initializing live satisfaction", e);
145+
});
141146
}
142-
await Promise.all(inits);
147+
148+
await this.featuresClient.initialize();
143149

144150
this.logger.debug(
145151
`initialized with key "${this.publishableKey}" and options`,
@@ -312,8 +318,10 @@ export class BucketClient {
312318
return this.featuresClient.getFeatures();
313319
}
314320

315-
stop() {
321+
async stop() {
316322
if (this.liveSatisfaction) {
323+
// ensure fully initialized before stopping
324+
await this.liveSatisfactionInit;
317325
this.liveSatisfaction.stop();
318326
}
319327
}

packages/browser-sdk/src/feature/featureCache.ts

Lines changed: 3 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,7 @@ interface StorageItem {
88
interface cacheEntry {
99
expireAt: number;
1010
staleAt: number;
11-
success: boolean; // we also want to cache failures to avoid the UI waiting and spamming the API
12-
features: APIFeaturesResponse | undefined;
13-
attemptCount: number;
11+
features: APIFeaturesResponse;
1412
}
1513

1614
// Parse and validate an API feature response
@@ -41,10 +39,8 @@ export function parseAPIFeaturesResponse(
4139
}
4240

4341
export interface CacheResult {
44-
features: APIFeaturesResponse | undefined;
42+
features: APIFeaturesResponse;
4543
stale: boolean;
46-
success: boolean;
47-
attemptCount: number;
4844
}
4945

5046
export class FeatureCache {
@@ -69,13 +65,9 @@ export class FeatureCache {
6965
set(
7066
key: string,
7167
{
72-
success,
7368
features,
74-
attemptCount,
7569
}: {
76-
success: boolean;
77-
features?: APIFeaturesResponse;
78-
attemptCount: number;
70+
features: APIFeaturesResponse;
7971
},
8072
) {
8173
let cacheData: CacheData = {};
@@ -93,8 +85,6 @@ export class FeatureCache {
9385
expireAt: Date.now() + this.expireTimeMs,
9486
staleAt: Date.now() + this.staleTimeMs,
9587
features,
96-
success,
97-
attemptCount,
9888
} satisfies cacheEntry;
9989

10090
cacheData = Object.fromEntries(
@@ -118,9 +108,7 @@ export class FeatureCache {
118108
) {
119109
return {
120110
features: cachedResponse[key].features,
121-
success: cachedResponse[key].success,
122111
stale: cachedResponse[key].staleAt < Date.now(),
123-
attemptCount: cachedResponse[key].attemptCount,
124112
};
125113
}
126114
}
@@ -145,8 +133,6 @@ function validateCacheData(cacheDataInput: any) {
145133
if (
146134
typeof cacheEntry.expireAt !== "number" ||
147135
typeof cacheEntry.staleAt !== "number" ||
148-
typeof cacheEntry.success !== "boolean" ||
149-
typeof cacheEntry.attemptCount !== "number" ||
150136
(cacheEntry.features && !parseAPIFeaturesResponse(cacheEntry.features))
151137
) {
152138
return;
@@ -155,9 +141,7 @@ function validateCacheData(cacheDataInput: any) {
155141
cacheData[key] = {
156142
expireAt: cacheEntry.expireAt,
157143
staleAt: cacheEntry.staleAt,
158-
success: cacheEntry.success,
159144
features: cacheEntry.features,
160-
attemptCount: cacheEntry.attemptCount,
161145
};
162146
}
163147
return cacheData;

packages/browser-sdk/src/feature/features.ts

Lines changed: 50 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -25,21 +25,18 @@ export type FeaturesOptions = {
2525
fallbackFeatures?: string[];
2626
timeoutMs?: number;
2727
staleWhileRevalidate?: boolean;
28-
failureRetryAttempts?: number | false;
2928
};
3029

3130
type Config = {
3231
fallbackFeatures: string[];
3332
timeoutMs: number;
3433
staleWhileRevalidate: boolean;
35-
failureRetryAttempts: number | false;
3634
};
3735

3836
export 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

110107
export 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

113110
const 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

packages/browser-sdk/src/feature/maskedProxy.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ export default function maskedProxy<T extends object, K extends keyof T, O>(
44
) {
55
return new Proxy(obj, {
66
get(target: T, prop) {
7+
if (typeof prop === "symbol") {
8+
return target[prop as K];
9+
}
710
return valueFunc(target, prop as K);
811
},
912
set(_target, prop, _value) {

packages/browser-sdk/src/feedback/feedback.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,7 @@ export class LiveSatisfaction {
229229
this.logger.error("feedback prompting already initialized");
230230
return;
231231
}
232+
this.initialized = true;
232233

233234
const channel = await this.getChannel();
234235
if (!channel) return;

packages/browser-sdk/test/featureCache.test.ts

Lines changed: 2 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -48,31 +48,17 @@ describe("cache", () => {
4848
test("caches items", async () => {
4949
const { cache } = newCache();
5050

51-
cache.set("key", { success: true, features, attemptCount: 1 });
51+
cache.set("key", { features });
5252
expect(cache.get("key")).toEqual({
5353
stale: false,
54-
success: true,
5554
features,
56-
attemptCount: 1,
57-
} satisfies CacheResult);
58-
});
59-
60-
test("caches unsuccessful items", async () => {
61-
const { cache } = newCache();
62-
63-
cache.set("key", { success: false, features, attemptCount: 1 });
64-
expect(cache.get("key")).toEqual({
65-
stale: false,
66-
success: false,
67-
features,
68-
attemptCount: 1,
6955
} satisfies CacheResult);
7056
});
7157

7258
test("sets stale", async () => {
7359
const { cache } = newCache();
7460

75-
cache.set("key", { success: true, features, attemptCount: 1 });
61+
cache.set("key", { features });
7662

7763
vitest.advanceTimersByTime(TEST_STALE_MS + 1);
7864

@@ -84,17 +70,13 @@ describe("cache", () => {
8470
const { cache, cacheItem } = newCache();
8571

8672
cache.set("first key", {
87-
success: true,
8873
features,
89-
attemptCount: 1,
9074
});
9175
expect(cacheItem[0]).not.toBeNull();
9276
vitest.advanceTimersByTime(TEST_EXPIRE_MS + 1);
9377

9478
cache.set("other key", {
95-
success: true,
9679
features,
97-
attemptCount: 1,
9880
});
9981

10082
const item = cache.get("key");

0 commit comments

Comments
 (0)