Skip to content

Commit 6eb196a

Browse files
committed
feat!: Replace prefetch behavior with simple TTL cache
Previously, the LDClient would issue a call to prime the store with data, which would be retained for the lifetime of the variation or all flags call. This priming call is being removed in favor of a simple TTL cache. The cache will be populated on the initial usage of the SDK, and then periodically as it is detected to be expired. The TTL can be configured with: - Positive value representing the time to cache the value - 0 to cache the value indefinitely. This allows a customer to initialize the SDK within an EdgeWorker handler, and get a "snapshot" of the view for the lifetime of the SDK. - Negative value representing no cache. This value is highly discouraged as usage restrictions in Akamai make it ineffective.
1 parent 3e5c0e8 commit 6eb196a

File tree

12 files changed

+220
-269
lines changed

12 files changed

+220
-269
lines changed
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import { AsyncStoreFacade, LDFeatureStore } from '@launchdarkly/js-server-sdk-common';
2+
3+
import { EdgeFeatureStore, EdgeProvider } from '../../src/featureStore';
4+
import * as testData from '../testData.json';
5+
6+
describe('EdgeFeatureStore', () => {
7+
const sdkKey = 'sdkKey';
8+
const mockLogger = {
9+
error: jest.fn(),
10+
warn: jest.fn(),
11+
info: jest.fn(),
12+
debug: jest.fn(),
13+
};
14+
const mockEdgeProvider: EdgeProvider = {
15+
get: jest.fn(),
16+
};
17+
const mockGet = mockEdgeProvider.get as jest.Mock;
18+
let featureStore: LDFeatureStore;
19+
let asyncFeatureStore: AsyncStoreFacade;
20+
21+
describe('with infinite cache', () => {
22+
beforeEach(() => {
23+
mockGet.mockImplementation(() => Promise.resolve(JSON.stringify(testData)));
24+
featureStore = new EdgeFeatureStore(
25+
mockEdgeProvider,
26+
sdkKey,
27+
'MockEdgeProvider',
28+
mockLogger,
29+
0,
30+
);
31+
asyncFeatureStore = new AsyncStoreFacade(featureStore);
32+
});
33+
34+
afterEach(() => {
35+
jest.resetAllMocks();
36+
});
37+
38+
it('will cache the initial request', async () => {
39+
await asyncFeatureStore.get({ namespace: 'features' }, 'testFlag1');
40+
await asyncFeatureStore.get({ namespace: 'features' }, 'testFlag1');
41+
await asyncFeatureStore.all({ namespace: 'features' });
42+
43+
expect(mockGet).toHaveBeenCalledTimes(1);
44+
});
45+
});
46+
47+
describe('with cache disabled', () => {
48+
beforeEach(() => {
49+
mockGet.mockImplementation(() => Promise.resolve(JSON.stringify(testData)));
50+
featureStore = new EdgeFeatureStore(
51+
mockEdgeProvider,
52+
sdkKey,
53+
'MockEdgeProvider',
54+
mockLogger,
55+
-1,
56+
);
57+
asyncFeatureStore = new AsyncStoreFacade(featureStore);
58+
});
59+
60+
afterEach(() => {
61+
jest.resetAllMocks();
62+
});
63+
64+
it('caches nothing', async () => {
65+
await asyncFeatureStore.get({ namespace: 'features' }, 'testFlag1');
66+
await asyncFeatureStore.get({ namespace: 'features' }, 'testFlag1');
67+
await asyncFeatureStore.all({ namespace: 'features' });
68+
69+
expect(mockGet).toHaveBeenCalledTimes(3);
70+
});
71+
});
72+
73+
describe('with finite cache', () => {
74+
beforeEach(() => {
75+
mockGet.mockImplementation(() => Promise.resolve(JSON.stringify(testData)));
76+
featureStore = new EdgeFeatureStore(
77+
mockEdgeProvider,
78+
sdkKey,
79+
'MockEdgeProvider',
80+
mockLogger,
81+
100,
82+
);
83+
asyncFeatureStore = new AsyncStoreFacade(featureStore);
84+
});
85+
86+
afterEach(() => {
87+
jest.resetAllMocks();
88+
});
89+
90+
it('expires are configured duration', async () => {
91+
jest.spyOn(Date, 'now').mockImplementation(() => 0);
92+
await asyncFeatureStore.get({ namespace: 'features' }, 'testFlag1');
93+
await asyncFeatureStore.get({ namespace: 'features' }, 'testFlag1');
94+
await asyncFeatureStore.all({ namespace: 'features' });
95+
96+
expect(mockGet).toHaveBeenCalledTimes(1);
97+
98+
jest.spyOn(Date, 'now').mockImplementation(() => 99);
99+
await asyncFeatureStore.get({ namespace: 'features' }, 'testFlag1');
100+
await asyncFeatureStore.get({ namespace: 'features' }, 'testFlag1');
101+
await asyncFeatureStore.all({ namespace: 'features' });
102+
103+
expect(mockGet).toHaveBeenCalledTimes(1);
104+
105+
jest.spyOn(Date, 'now').mockImplementation(() => 100);
106+
await asyncFeatureStore.get({ namespace: 'features' }, 'testFlag1');
107+
await asyncFeatureStore.get({ namespace: 'features' }, 'testFlag1');
108+
await asyncFeatureStore.all({ namespace: 'features' });
109+
110+
expect(mockGet).toHaveBeenCalledTimes(2);
111+
});
112+
});
113+
});

packages/shared/akamai-edgeworker-sdk/__tests__/featureStore/cacheableStore.test.ts

Lines changed: 0 additions & 101 deletions
This file was deleted.

packages/shared/akamai-edgeworker-sdk/__tests__/featureStore/index.test.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,13 @@ describe('EdgeFeatureStore', () => {
2121

2222
beforeEach(() => {
2323
mockGet.mockImplementation(() => Promise.resolve(JSON.stringify(testData)));
24-
featureStore = new EdgeFeatureStore(mockEdgeProvider, sdkKey, 'MockEdgeProvider', mockLogger);
24+
featureStore = new EdgeFeatureStore(
25+
mockEdgeProvider,
26+
sdkKey,
27+
'MockEdgeProvider',
28+
mockLogger,
29+
0,
30+
);
2531
asyncFeatureStore = new AsyncStoreFacade(featureStore);
2632
});
2733

packages/shared/akamai-edgeworker-sdk/__tests__/index.test.ts

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ const createClient = (sdkKey: string, mockLogger: LDLogger, mockEdgeProvider: Ed
66
sdkKey,
77
options: {
88
logger: mockLogger,
9+
cacheTtlMs: 0,
910
},
1011
featureStoreProvider: mockEdgeProvider,
1112
platformName: 'platform-name',
@@ -40,22 +41,14 @@ describe('EdgeWorker', () => {
4041
it('should call edge providers get method only once', async () => {
4142
const client = createClient(sdkKey, mockLogger, mockEdgeProvider);
4243
await client.waitForInitialization();
43-
await client.allFlagsState({ kind: 'multi', l: { key: 'key' } });
44-
45-
expect(mockGet).toHaveBeenCalledTimes(1);
46-
});
47-
48-
it('should call edge providers get method only 3 times', async () => {
49-
const client = createClient(sdkKey, mockLogger, mockEdgeProvider);
50-
await client.waitForInitialization();
5144

5245
const context: LDMultiKindContext = { kind: 'multi', l: { key: 'key' } };
5346

5447
await client.allFlagsState(context, { clientSideOnly: true });
5548
await client.variation('testFlag1', context, false);
5649
await client.variationDetail('testFlag1', context, false);
5750

58-
expect(mockGet).toHaveBeenCalledTimes(3);
51+
expect(mockGet).toHaveBeenCalledTimes(1);
5952
});
6053

6154
it('should successfully return data for allFlagsState', async () => {

packages/shared/akamai-edgeworker-sdk/__tests__/platform/requests.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import EdgeRequests from '../../src/platform/requests';
55
const TEXT_RESPONSE = '';
66
const JSON_RESPONSE = {};
77

8-
describe('given a default instance of requets', () => {
8+
describe('given a default instance of requests', () => {
99
const requests = new EdgeRequests();
1010

1111
describe('fetch', () => {

packages/shared/akamai-edgeworker-sdk/__tests__/utils/validateOptions.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ const mockOptions = ({
2626
}) => {
2727
const mockLogger = logger ?? BasicLogger.get();
2828
const mockFeatureStore =
29-
featureStore ?? new EdgeFeatureStore(edgeProvider, SDK_KEY, 'validationTest', mockLogger);
29+
featureStore ?? new EdgeFeatureStore(edgeProvider, SDK_KEY, 'validationTest', mockLogger, 0);
3030

3131
return {
3232
featureStore: allowEmptyFS ? undefined : mockFeatureStore,

packages/shared/akamai-edgeworker-sdk/src/api/LDClient.ts

Lines changed: 3 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,9 @@
22
import {
33
LDClientImpl,
44
LDClient as LDClientType,
5-
LDContext,
6-
LDEvaluationDetail,
7-
LDFlagsState,
8-
LDFlagsStateOptions,
9-
LDFlagValue,
105
LDOptions,
116
} from '@launchdarkly/js-server-sdk-common';
127

13-
import CacheableStoreProvider from '../featureStore/cacheableStoreProvider';
148
import EdgePlatform from '../platform';
159
import { createCallbacks, createOptions } from '../utils';
1610

@@ -20,58 +14,21 @@ export interface CustomLDOptions extends LDOptions {}
2014
* The LaunchDarkly Akamai SDK edge client object.
2115
*/
2216
class LDClient extends LDClientImpl {
23-
private _cacheableStoreProvider!: CacheableStoreProvider;
24-
2517
// sdkKey is only used to query featureStore, not to initialize with LD servers
26-
constructor(
27-
sdkKey: string,
28-
platform: EdgePlatform,
29-
options: LDOptions,
30-
storeProvider: CacheableStoreProvider,
31-
) {
18+
constructor(sdkKey: string, platform: EdgePlatform, options: LDOptions) {
3219
const finalOptions = createOptions(options);
3320
super(sdkKey, platform, finalOptions, createCallbacks(finalOptions.logger));
34-
this._cacheableStoreProvider = storeProvider;
3521
}
3622

3723
override initialized(): boolean {
3824
return true;
3925
}
4026

4127
override waitForInitialization(): Promise<LDClientType> {
42-
// we need to resolve the promise immediately because Akamai's runtime doesnt
43-
// have a setimeout so everything executes synchronously.
28+
// we need to resolve the promise immediately because Akamai's runtime doesn't
29+
// have a setTimeout so everything executes synchronously.
4430
return Promise.resolve(this);
4531
}
46-
47-
override async variation(
48-
key: string,
49-
context: LDContext,
50-
defaultValue: LDFlagValue,
51-
callback?: (err: any, res: LDFlagValue) => void,
52-
): Promise<LDFlagValue> {
53-
await this._cacheableStoreProvider.prefetchPayloadFromOriginStore();
54-
return super.variation(key, context, defaultValue, callback);
55-
}
56-
57-
override async variationDetail(
58-
key: string,
59-
context: LDContext,
60-
defaultValue: LDFlagValue,
61-
callback?: (err: any, res: LDEvaluationDetail) => void,
62-
): Promise<LDEvaluationDetail> {
63-
await this._cacheableStoreProvider.prefetchPayloadFromOriginStore();
64-
return super.variationDetail(key, context, defaultValue, callback);
65-
}
66-
67-
override async allFlagsState(
68-
context: LDContext,
69-
options?: LDFlagsStateOptions,
70-
callback?: (err: Error | null, res: LDFlagsState) => void,
71-
): Promise<LDFlagsState> {
72-
await this._cacheableStoreProvider.prefetchPayloadFromOriginStore();
73-
return super.allFlagsState(context, options, callback);
74-
}
7532
}
7633

7734
export default LDClient;
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
export class CacheItem {
2+
private _cachedAt: number;
3+
constructor(public readonly value: any) {
4+
this._cachedAt = Date.now();
5+
}
6+
7+
fresh(ttl: number): boolean {
8+
return Date.now() - this._cachedAt < ttl;
9+
}
10+
}

0 commit comments

Comments
 (0)