Skip to content

Commit be2ee75

Browse files
fix: improving payload processing overhead when payload does not change (#918)
**Requirements** - [X] I have added test coverage for new or changed functionality - [X] I have followed the repository's [pull request submission guidelines](../blob/main/CONTRIBUTING.md#submitting-pull-requests) - [X] I have validated my changes against all supported platform versions **Related issues** #907 **Describe the solution you've provided** Vercel's Edge Config SDK returns the same object reference if the payload has not changed. A custom implementation of EdgeFeatureStore was added to take advantage of this and not re-reprocess the KV payload if it has not changed.
1 parent 2285894 commit be2ee75

File tree

6 files changed

+500
-3
lines changed

6 files changed

+500
-3
lines changed
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
import { AsyncStoreFacade, LDFeatureStore } from '@launchdarkly/js-server-sdk-common-edge';
2+
import * as edgeExports from '@launchdarkly/js-server-sdk-common-edge';
3+
4+
import { EdgeFeatureStore } from '../../src/api/EdgeFeatureStore';
5+
import mockEdgeProvider from '../utils/mockEdgeProvider';
6+
import * as testData from './testData.json';
7+
8+
describe('EdgeFeatureStore', () => {
9+
const sdkKey = 'sdkKey';
10+
const kvKey = `LD-Env-${sdkKey}`;
11+
const mockLogger = {
12+
error: jest.fn(),
13+
warn: jest.fn(),
14+
info: jest.fn(),
15+
debug: jest.fn(),
16+
};
17+
const mockGet = mockEdgeProvider.get as jest.Mock;
18+
let featureStore: LDFeatureStore;
19+
let asyncFeatureStore: AsyncStoreFacade;
20+
21+
beforeEach(() => {
22+
featureStore = new EdgeFeatureStore(mockEdgeProvider, sdkKey, 'MockEdgeProvider', mockLogger);
23+
asyncFeatureStore = new AsyncStoreFacade(featureStore);
24+
mockGet.mockImplementation(() => Promise.resolve(testData));
25+
});
26+
27+
afterEach(() => {
28+
jest.resetAllMocks();
29+
});
30+
31+
describe('get', () => {
32+
test('get flag', async () => {
33+
const flag = await asyncFeatureStore.get({ namespace: 'features' }, 'testFlag1');
34+
35+
expect(mockGet).toHaveBeenCalledWith(kvKey);
36+
expect(flag).toMatchObject(testData.flags.testFlag1);
37+
});
38+
39+
test('invalid flag key', async () => {
40+
const flag = await asyncFeatureStore.get({ namespace: 'features' }, 'invalid');
41+
42+
expect(flag).toBeUndefined();
43+
});
44+
45+
test('get segment', async () => {
46+
const segment = await asyncFeatureStore.get({ namespace: 'segments' }, 'testSegment1');
47+
48+
expect(mockGet).toHaveBeenCalledWith(kvKey);
49+
expect(segment).toMatchObject(testData.segments.testSegment1);
50+
});
51+
52+
test('invalid segment key', async () => {
53+
const segment = await asyncFeatureStore.get({ namespace: 'segments' }, 'invalid');
54+
55+
expect(segment).toBeUndefined();
56+
});
57+
58+
test('invalid kv key', async () => {
59+
mockGet.mockImplementation(() => Promise.resolve(null));
60+
const flag = await asyncFeatureStore.get({ namespace: 'features' }, 'testFlag1');
61+
62+
expect(flag).toBeNull();
63+
});
64+
65+
test('get multiple flags with same payload', async () => {
66+
const reviveSpy = jest.spyOn(edgeExports, 'reviveFullPayload');
67+
68+
const flag1 = await asyncFeatureStore.get({ namespace: 'features' }, 'testFlag1');
69+
const flag2 = await asyncFeatureStore.get({ namespace: 'features' }, 'testFlag2');
70+
const flag3 = await asyncFeatureStore.get({ namespace: 'features' }, 'testFlag3');
71+
72+
expect(mockGet).toHaveBeenCalledTimes(3);
73+
expect(reviveSpy).toHaveBeenCalledTimes(1);
74+
expect(flag1).toMatchObject(testData.flags.testFlag1);
75+
expect(flag2).toMatchObject(testData.flags.testFlag2);
76+
expect(flag3).toMatchObject(testData.flags.testFlag3);
77+
78+
reviveSpy.mockRestore();
79+
});
80+
81+
test('get multiple flags with changing payload', async () => {
82+
const changedFlag2 = {
83+
...testData.flags.testFlag2,
84+
version: testData.flags.testFlag2.version + 1,
85+
};
86+
const changedFlag3 = {
87+
...testData.flags.testFlag3,
88+
version: testData.flags.testFlag3.version + 1,
89+
};
90+
91+
mockGet.mockImplementationOnce(() => Promise.resolve(testData));
92+
mockGet.mockImplementationOnce(() =>
93+
// New payload object reference
94+
Promise.resolve({
95+
...testData,
96+
flags: { ...testData.flags, testFlag2: { ...changedFlag2 } },
97+
}),
98+
);
99+
mockGet.mockImplementationOnce(() =>
100+
// New payload object reference
101+
Promise.resolve({
102+
...testData,
103+
flags: { ...testData.flags, testFlag3: { ...changedFlag3 } },
104+
}),
105+
);
106+
const reviveSpy = jest.spyOn(edgeExports, 'reviveFullPayload');
107+
108+
const flag1 = await asyncFeatureStore.get({ namespace: 'features' }, 'testFlag1');
109+
const flag2 = await asyncFeatureStore.get({ namespace: 'features' }, 'testFlag2');
110+
const flag3 = await asyncFeatureStore.get({ namespace: 'features' }, 'testFlag3');
111+
112+
expect(mockGet).toHaveBeenCalledTimes(3);
113+
expect(reviveSpy).toHaveBeenCalledTimes(3);
114+
expect(flag1).toMatchObject(testData.flags.testFlag1);
115+
expect(flag2).toMatchObject(changedFlag2);
116+
expect(flag3).toMatchObject(changedFlag3);
117+
118+
reviveSpy.mockRestore();
119+
});
120+
});
121+
122+
describe('all', () => {
123+
test('all flags', async () => {
124+
const flags = await asyncFeatureStore.all({ namespace: 'features' });
125+
126+
expect(mockGet).toHaveBeenCalledWith(kvKey);
127+
expect(flags).toMatchObject(testData.flags);
128+
});
129+
130+
test('all segments', async () => {
131+
const segment = await asyncFeatureStore.all({ namespace: 'segments' });
132+
133+
expect(mockGet).toHaveBeenCalledWith(kvKey);
134+
expect(segment).toMatchObject(testData.segments);
135+
});
136+
137+
test('invalid DataKind', async () => {
138+
const flag = await asyncFeatureStore.all({ namespace: 'InvalidDataKind' });
139+
140+
expect(flag).toEqual({});
141+
});
142+
143+
test('invalid kv key', async () => {
144+
mockGet.mockImplementation(() => Promise.resolve(null));
145+
const segment = await asyncFeatureStore.all({ namespace: 'segments' });
146+
147+
expect(segment).toEqual({});
148+
});
149+
});
150+
151+
describe('initialized', () => {
152+
test('is initialized', async () => {
153+
const isInitialized = await asyncFeatureStore.initialized();
154+
155+
expect(mockGet).toHaveBeenCalledWith(kvKey);
156+
expect(isInitialized).toBeTruthy();
157+
});
158+
159+
test('not initialized', async () => {
160+
mockGet.mockImplementation(() => Promise.resolve(null));
161+
const isInitialized = await asyncFeatureStore.initialized();
162+
163+
expect(mockGet).toHaveBeenCalledWith(kvKey);
164+
expect(isInitialized).toBeFalsy();
165+
});
166+
});
167+
168+
describe('init & getDescription', () => {
169+
test('init', (done) => {
170+
const cb = jest.fn(() => {
171+
done();
172+
});
173+
featureStore.init(testData, cb);
174+
});
175+
176+
test('getDescription', async () => {
177+
const description = featureStore.getDescription?.();
178+
179+
expect(description).toEqual('MockEdgeProvider');
180+
});
181+
});
182+
});
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
{
2+
"flags": {
3+
"testFlag1": {
4+
"key": "testFlag1",
5+
"on": true,
6+
"prerequisites": [],
7+
"targets": [],
8+
"rules": [
9+
{
10+
"variation": 1,
11+
"id": "rule1",
12+
"clauses": [
13+
{
14+
"contextKind": "user",
15+
"attribute": "/email",
16+
"op": "contains",
17+
"values": ["gmail"],
18+
"negate": false
19+
}
20+
],
21+
"trackEvents": false,
22+
"rollout": {
23+
"bucketBy": "bucket",
24+
"variations": [{ "variation": 1, "weight": 100 }]
25+
}
26+
}
27+
],
28+
"fallthrough": {
29+
"variation": 0
30+
},
31+
"offVariation": 1,
32+
"variations": [true, false],
33+
"clientSideAvailability": {
34+
"usingMobileKey": true,
35+
"usingEnvironmentId": true
36+
},
37+
"clientSide": true,
38+
"salt": "aef830243d6640d0a973be89988e008d",
39+
"trackEvents": false,
40+
"trackEventsFallthrough": false,
41+
"debugEventsUntilDate": 2000,
42+
"version": 2,
43+
"deleted": false
44+
},
45+
"testFlag2": {
46+
"key": "testFlag2",
47+
"on": true,
48+
"prerequisites": [],
49+
"targets": [],
50+
"rules": [],
51+
"fallthrough": {
52+
"variation": 0,
53+
"rollout": {
54+
"bucketBy": "bucket",
55+
"variations": [{ "variation": 1, "weight": 100 }],
56+
"contextKind:": "user",
57+
"attribute": "/email"
58+
}
59+
},
60+
"offVariation": 1,
61+
"variations": [true, false],
62+
"clientSideAvailability": {
63+
"usingMobileKey": true,
64+
"usingEnvironmentId": true
65+
},
66+
"clientSide": true,
67+
"salt": "aef830243d6640d0a973be89988e008d",
68+
"trackEvents": false,
69+
"trackEventsFallthrough": false,
70+
"debugEventsUntilDate": 2000,
71+
"version": 2,
72+
"deleted": false
73+
},
74+
"testFlag3": {
75+
"key": "testFlag3",
76+
"on": true,
77+
"prerequisites": [],
78+
"targets": [],
79+
"rules": [
80+
{
81+
"variation": 1,
82+
"id": "rule1",
83+
"clauses": [
84+
{
85+
"op": "segmentMatch",
86+
"values": ["testSegment1"],
87+
"negate": false
88+
}
89+
],
90+
"trackEvents": false
91+
}
92+
],
93+
"fallthrough": {
94+
"variation": 0
95+
},
96+
"offVariation": 1,
97+
"variations": [true, false],
98+
"clientSideAvailability": {
99+
"usingMobileKey": true,
100+
"usingEnvironmentId": true
101+
},
102+
"clientSide": true,
103+
"salt": "aef830243d6640d0a973be89988e008d",
104+
"trackEvents": false,
105+
"trackEventsFallthrough": false,
106+
"debugEventsUntilDate": 2000,
107+
"version": 2,
108+
"deleted": false
109+
}
110+
},
111+
"segments": {
112+
"testSegment1": {
113+
"name": "testSegment1",
114+
"tags": [],
115+
"creationDate": 1676063792158,
116+
"key": "testSegment1",
117+
"included": [],
118+
"excluded": [],
119+
"includedContexts": [],
120+
"excludedContexts": [],
121+
"_links": {
122+
"parent": { "href": "/api/v2/segments/default/test", "type": "application/json" },
123+
"self": {
124+
"href": "/api/v2/segments/default/test/beta-users-1",
125+
"type": "application/json"
126+
},
127+
"site": { "href": "/default/test/segments/beta-users-1", "type": "text/html" }
128+
},
129+
"rules": [
130+
{
131+
"id": "rule-country",
132+
"clauses": [
133+
{
134+
"attribute": "country",
135+
"op": "in",
136+
"values": ["australia"],
137+
"negate": false
138+
}
139+
]
140+
}
141+
],
142+
"version": 1,
143+
"deleted": false,
144+
"_access": { "denied": [], "allowed": [] },
145+
"generation": 1
146+
},
147+
"testSegment2": {
148+
"name": "testSegment2",
149+
"tags": [],
150+
"creationDate": 1676063792158,
151+
"key": "testSegment2",
152+
"included": [],
153+
"excluded": [],
154+
"includedContexts": [],
155+
"excludedContexts": [],
156+
"_links": {
157+
"parent": { "href": "/api/v2/segments/default/test", "type": "application/json" },
158+
"self": {
159+
"href": "/api/v2/segments/default/test/beta-users-1",
160+
"type": "application/json"
161+
},
162+
"site": { "href": "/default/test/segments/beta-users-1", "type": "text/html" }
163+
},
164+
"rules": [],
165+
"version": 1,
166+
"deleted": false,
167+
"_access": { "denied": [], "allowed": [] },
168+
"generation": 1
169+
}
170+
}
171+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { EdgeProvider } from '../../src/api';
2+
3+
const mockEdgeProvider: EdgeProvider = {
4+
get: jest.fn(),
5+
};
6+
7+
export default mockEdgeProvider;

0 commit comments

Comments
 (0)