Skip to content

Commit 76d614f

Browse files
fix: Improve serialization/deserialization overhead with EdgeFeatureStore (#914)
**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 already returns the JSON.parsed payload but it was being JSON.stringified and JSON.parsed an additional time which was adding overhead. EdgeFeatureStore has been improved to be able to handle either stringified or parsed data so that this extra serialization/deserialization step doesn't need to occur.
1 parent de783b9 commit 76d614f

File tree

5 files changed

+116
-24
lines changed

5 files changed

+116
-24
lines changed

packages/sdk/vercel/src/index.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,10 @@ export type { LDClient };
4949
export const init = (sdkKey: string, edgeConfig: EdgeConfigClient, options: LDOptions = {}) => {
5050
const logger = options.logger ?? BasicLogger.get();
5151

52-
// vercel does not support string gets so we have to stringify it
5352
const edgeProvider: EdgeProvider = {
5453
get: async (rootKey: string) => {
55-
const json = await edgeConfig.get(rootKey);
56-
return json ? JSON.stringify(json) : null;
54+
const json = await edgeConfig.get<Record<string, any>>(rootKey);
55+
return json || null;
5756
},
5857
};
5958

packages/shared/sdk-server-edge/__tests__/api/EdgeFeatureStore.test.ts

Lines changed: 82 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ describe('EdgeFeatureStore', () => {
1818
let asyncFeatureStore: AsyncStoreFacade;
1919

2020
beforeEach(() => {
21-
mockGet.mockImplementation(() => Promise.resolve(JSON.stringify(testData)));
2221
featureStore = new EdgeFeatureStore(mockEdgeProvider, sdkKey, 'MockEdgeProvider', mockLogger);
2322
asyncFeatureStore = new AsyncStoreFacade(featureStore);
2423
});
@@ -27,7 +26,50 @@ describe('EdgeFeatureStore', () => {
2726
jest.resetAllMocks();
2827
});
2928

30-
describe('get', () => {
29+
describe('get (string payload)', () => {
30+
beforeEach(() => {
31+
mockGet.mockImplementation(() => Promise.resolve(JSON.stringify(testData)));
32+
});
33+
34+
test('get flag', async () => {
35+
const flag = await asyncFeatureStore.get({ namespace: 'features' }, 'testFlag1');
36+
37+
expect(mockGet).toHaveBeenCalledWith(kvKey);
38+
expect(flag).toMatchObject(testData.flags.testFlag1);
39+
});
40+
41+
test('invalid flag key', async () => {
42+
const flag = await asyncFeatureStore.get({ namespace: 'features' }, 'invalid');
43+
44+
expect(flag).toBeUndefined();
45+
});
46+
47+
test('get segment', async () => {
48+
const segment = await asyncFeatureStore.get({ namespace: 'segments' }, 'testSegment1');
49+
50+
expect(mockGet).toHaveBeenCalledWith(kvKey);
51+
expect(segment).toMatchObject(testData.segments.testSegment1);
52+
});
53+
54+
test('invalid segment key', async () => {
55+
const segment = await asyncFeatureStore.get({ namespace: 'segments' }, 'invalid');
56+
57+
expect(segment).toBeUndefined();
58+
});
59+
60+
test('invalid kv key', async () => {
61+
mockGet.mockImplementation(() => Promise.resolve(null));
62+
const flag = await asyncFeatureStore.get({ namespace: 'features' }, 'testFlag1');
63+
64+
expect(flag).toBeNull();
65+
});
66+
});
67+
68+
describe('get (object payload)', () => {
69+
beforeEach(() => {
70+
mockGet.mockImplementation(() => Promise.resolve(testData));
71+
});
72+
3173
test('get flag', async () => {
3274
const flag = await asyncFeatureStore.get({ namespace: 'features' }, 'testFlag1');
3375

@@ -62,7 +104,44 @@ describe('EdgeFeatureStore', () => {
62104
});
63105
});
64106

65-
describe('all', () => {
107+
describe('all (string payload)', () => {
108+
beforeEach(() => {
109+
mockGet.mockImplementation(() => Promise.resolve(JSON.stringify(testData)));
110+
});
111+
112+
test('all flags', async () => {
113+
const flags = await asyncFeatureStore.all({ namespace: 'features' });
114+
115+
expect(mockGet).toHaveBeenCalledWith(kvKey);
116+
expect(flags).toMatchObject(testData.flags);
117+
});
118+
119+
test('all segments', async () => {
120+
const segment = await asyncFeatureStore.all({ namespace: 'segments' });
121+
122+
expect(mockGet).toHaveBeenCalledWith(kvKey);
123+
expect(segment).toMatchObject(testData.segments);
124+
});
125+
126+
test('invalid DataKind', async () => {
127+
const flag = await asyncFeatureStore.all({ namespace: 'InvalidDataKind' });
128+
129+
expect(flag).toEqual({});
130+
});
131+
132+
test('invalid kv key', async () => {
133+
mockGet.mockImplementation(() => Promise.resolve(null));
134+
const segment = await asyncFeatureStore.all({ namespace: 'segments' });
135+
136+
expect(segment).toEqual({});
137+
});
138+
});
139+
140+
describe('all (object payload)', () => {
141+
beforeEach(() => {
142+
mockGet.mockImplementation(() => Promise.resolve(testData));
143+
});
144+
66145
test('all flags', async () => {
67146
const flags = await asyncFeatureStore.all({ namespace: 'features' });
68147

packages/shared/sdk-server-edge/src/api/EdgeFeatureStore.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@ import type {
66
LDFeatureStoreKindData,
77
LDLogger,
88
} from '@launchdarkly/js-server-sdk-common';
9-
import { deserializePoll, noop } from '@launchdarkly/js-server-sdk-common';
9+
import { deserializePoll, noop, reviveFullPayload } from '@launchdarkly/js-server-sdk-common';
1010

1111
import Cache from './cache';
1212

1313
export interface EdgeProvider {
14-
get: (rootKey: string) => Promise<string | null | undefined>;
14+
get: (rootKey: string) => Promise<string | Record<string, any> | null | undefined>;
1515
}
1616

1717
export class EdgeFeatureStore implements LDFeatureStore {
@@ -96,7 +96,11 @@ export class EdgeFeatureStore implements LDFeatureStore {
9696
throw new Error(`${this._rootKey} is not found in KV.`);
9797
}
9898

99-
payload = deserializePoll(providerData);
99+
payload =
100+
typeof providerData === 'string'
101+
? deserializePoll(providerData)
102+
: reviveFullPayload(providerData);
103+
100104
if (!payload) {
101105
throw new Error(`Error deserializing ${this._rootKey}`);
102106
}
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import AsyncStoreFacade from './AsyncStoreFacade';
22
import AsyncTransactionalStoreFacade from './AsyncTransactionalStoreFacade';
33
import PersistentDataStoreWrapper from './PersistentDataStoreWrapper';
4-
import { deserializePoll } from './serialization';
4+
import { deserializePoll, reviveFullPayload } from './serialization';
55
import TransactionalFeatureStore from './TransactionalFeatureStore';
66

77
export {
@@ -10,4 +10,5 @@ export {
1010
PersistentDataStoreWrapper,
1111
TransactionalFeatureStore,
1212
deserializePoll,
13+
reviveFullPayload,
1314
};

packages/shared/sdk-server/src/store/serialization.ts

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,27 @@ function tryParse(data: string): any {
237237
}
238238
}
239239

240+
/**
241+
* This function is intended for usage inside LaunchDarkly SDKs.
242+
* This function should NOT be used by customer applications.
243+
* This function may be changed or removed without a major version.
244+
*
245+
* @param payload Payload data from launchdarkly.
246+
* @returns The revived and processed data.
247+
*/
248+
export function reviveFullPayload(payload: Record<string, any>): FlagsAndSegments {
249+
const flagsAndSegments = payload as FlagsAndSegments;
250+
Object.values(flagsAndSegments?.flags || []).forEach((flag) => {
251+
processFlag(flag);
252+
});
253+
254+
Object.values(flagsAndSegments?.segments || []).forEach((segment) => {
255+
processSegment(segment);
256+
});
257+
258+
return flagsAndSegments;
259+
}
260+
240261
/**
241262
* @internal
242263
*/
@@ -253,13 +274,7 @@ export function deserializeAll(data: string): AllData | undefined {
253274
return undefined;
254275
}
255276

256-
Object.values(parsed?.data?.flags || []).forEach((flag) => {
257-
processFlag(flag);
258-
});
259-
260-
Object.values(parsed?.data?.segments || []).forEach((segment) => {
261-
processSegment(segment);
262-
});
277+
reviveFullPayload(parsed?.data);
263278
return parsed;
264279
}
265280

@@ -278,13 +293,7 @@ export function deserializePoll(data: string): FlagsAndSegments | undefined {
278293
return undefined;
279294
}
280295

281-
Object.values(parsed?.flags || []).forEach((flag) => {
282-
processFlag(flag);
283-
});
284-
285-
Object.values(parsed?.segments || []).forEach((segment) => {
286-
processSegment(segment);
287-
});
296+
reviveFullPayload(parsed);
288297
return parsed;
289298
}
290299

0 commit comments

Comments
 (0)