Skip to content

Commit 17a33c7

Browse files
Add support for isExpired in configuration store to gate CDN requests… (#61)
* Add support for isExpired in configuration store to gate CDN requests (FF-2048) * Add info message * 3.0.4
1 parent f94eb93 commit 17a33c7

File tree

8 files changed

+58
-1
lines changed

8 files changed

+58
-1
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@eppo/js-client-sdk-common",
3-
"version": "3.0.3",
3+
"version": "3.0.4",
44
"description": "Eppo SDK for client-side JavaScript applications (base for both web and react native)",
55
"main": "dist/index.js",
66
"files": [

src/client/eppo-client.spec.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -639,6 +639,24 @@ describe('EppoClient E2E test', () => {
639639
expect(variation).toBe(pi);
640640
});
641641

642+
it('Does not fetch configurations if the configuration store is unexpired', async () => {
643+
class MockStore extends MemoryOnlyConfigurationStore<Flag | ObfuscatedFlag> {
644+
async isExpired(): Promise<boolean> {
645+
return false;
646+
}
647+
}
648+
649+
client = new EppoClient(new MockStore(), requestConfiguration);
650+
client.setIsGracefulFailureMode(false);
651+
// no configuration loaded
652+
let variation = client.getNumericAssignment(flagKey, subject, {}, 0.0);
653+
expect(variation).toBe(0.0);
654+
// have client fetch configurations
655+
await client.fetchFlagConfigurations();
656+
variation = client.getNumericAssignment(flagKey, subject, {}, 0.0);
657+
expect(variation).toBe(0.0);
658+
});
659+
642660
it.each([
643661
{ pollAfterSuccessfulInitialization: false },
644662
{ pollAfterSuccessfulInitialization: true },

src/client/eppo-client.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,14 @@ export default class EppoClient implements IEppoClient {
152152
this.requestPoller.stop();
153153
}
154154

155+
const isExpired = await this.configurationStore.isExpired();
156+
if (!isExpired) {
157+
logger.info(
158+
'[Eppo SDK] Configuration store is not expired. Skipping fetching flag configurations',
159+
);
160+
return;
161+
}
162+
155163
// todo: consider injecting the IHttpClient interface
156164
const httpClient = new FetchHttpClient(
157165
this.configurationRequestParameters.baseUrl || DEFAULT_BASE_URL,

src/configuration-store/configuration-store.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export interface IConfigurationStore<T> {
2828
get(key: string): T | null;
2929
getKeys(): string[];
3030
isInitialized(): boolean;
31+
isExpired(): Promise<boolean>;
3132
setEntries(entries: Record<string, T>): Promise<void>;
3233
}
3334

@@ -40,6 +41,7 @@ export interface ISyncStore<T> {
4041

4142
export interface IAsyncStore<T> {
4243
isInitialized(): boolean;
44+
isExpired(): Promise<boolean>;
4345
getEntries(): Promise<Record<string, T>>;
4446
setEntries(entries: Record<string, T>): Promise<void>;
4547
}

src/configuration-store/hybrid.store.spec.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ describe('HybridConfigurationStore', () => {
1717
asyncStoreMock = {
1818
getEntries: jest.fn(),
1919
isInitialized: jest.fn(),
20+
isExpired: jest.fn(),
2021
setEntries: jest.fn(),
2122
};
2223

@@ -35,6 +36,21 @@ describe('HybridConfigurationStore', () => {
3536
});
3637
});
3738

39+
describe('isExpired', () => {
40+
it("is the persistent store's expired value", async () => {
41+
(asyncStoreMock.isExpired as jest.Mock).mockResolvedValue(true);
42+
expect(await store.isExpired()).toBe(true);
43+
44+
(asyncStoreMock.isExpired as jest.Mock).mockResolvedValue(false);
45+
expect(await store.isExpired()).toBe(false);
46+
});
47+
48+
it('is true without a persistent store', async () => {
49+
const mixedStoreWithNull = new HybridConfigurationStore(syncStoreMock, null);
50+
expect(await mixedStoreWithNull.isExpired()).toBe(true);
51+
});
52+
});
53+
3854
describe('isInitialized', () => {
3955
it('should return true if both stores are initialized', () => {
4056
(syncStoreMock.isInitialized as jest.Mock).mockReturnValue(true);

src/configuration-store/hybrid.store.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@ export class HybridConfigurationStore<T> implements IConfigurationStore<T> {
4040
return this.servingStore.isInitialized() && (this.persistentStore?.isInitialized() ?? true);
4141
}
4242

43+
public async isExpired(): Promise<boolean> {
44+
const isExpired = (await this.persistentStore?.isExpired()) ?? true;
45+
return isExpired;
46+
}
47+
4348
public get(key: string): T | null {
4449
if (!this.servingStore.isInitialized()) {
4550
logger.warn(`${loggerPrefix} getting a value from a ServingStore that is not initialized.`);

src/configuration-store/memory.store.spec.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ describe('MemoryOnlyConfigurationStore', () => {
1212
expect(memoryStore.getKeys()).toEqual([]);
1313
});
1414

15+
it('is always expired', async () => {
16+
expect(await memoryStore.isExpired()).toBe(true);
17+
});
18+
1519
it('should return null for non-existent keys', () => {
1620
expect(memoryStore.get('nonexistent')).toBeNull();
1721
});

src/configuration-store/memory.store.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@ export class MemoryOnlyConfigurationStore<T> implements IConfigurationStore<T> {
4848
return this.servingStore.getKeys();
4949
}
5050

51+
async isExpired(): Promise<boolean> {
52+
return true;
53+
}
54+
5155
isInitialized(): boolean {
5256
return this.initialized;
5357
}

0 commit comments

Comments
 (0)