Skip to content

Commit 8ab2ee4

Browse files
authored
feat: refactors the implementation of context caching. You can now s… (#531)
…pecify the maxCachedContexts (default 5) in the LDOptions for client-side SDKs. **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 TODOs: - [x] FlagManager and FlagPersistence tests - [x] need to do some validation on platforms and manual examination of storage - [x] implement data migration instructions in FlagPersistence.ts - [x] need to validate migration of existing data - [x] [update various namespace concat usages to use static helper funcs](https://github.com/launchdarkly/js-core/pull/531/files#diff-2ea53243d0cda26506ba1e932279540f87359ef3647357de41dd6961553b3908R6) - [x] [discuss immutability of return values](https://github.com/launchdarkly/js-core/pull/531/files#diff-d292bcc848729d6a937835ad9b3aefb2217a4ab9057a1b9caa22eb32b6412cbdR22) - [x] [add tests for namespace hashing and utf8 encoding](https://github.com/launchdarkly/js-core/pull/531/files#diff-2ea53243d0cda26506ba1e932279540f87359ef3647357de41dd6961553b3908R23) - [x] [verify deleteFlag handling](https://github.com/launchdarkly/js-core/pull/531/files#diff-5ab8d43c9c238bf0ca76fae92d46229147658df4640f421264a1f57eebfdde79R215) **Related issues** SC-247373 **Describe the solution you've provided** Introduces FlagManager, FlagUpdater, FlagPersistence, and flag CacheIndex for creating a LRU for cached flag evaluations. This follows the implementations of other client SDKs.
1 parent fc4645e commit 8ab2ee4

33 files changed

+1837
-275
lines changed

packages/shared/common/__tests__/Context.test.ts

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,3 +177,151 @@ describe('given a multi-kind context', () => {
177177
expect(context?.kindsAndKeys).toEqual({ org: 'OrgKey', user: 'User%:/Key' });
178178
});
179179
});
180+
181+
describe('given a user context with private attributes', () => {
182+
const input = Context.fromLDContext({
183+
key: 'testKey',
184+
name: 'testName',
185+
custom: { cat: 'calico', dog: 'lab' },
186+
anonymous: true,
187+
privateAttributeNames: ['/a/b/c', 'cat', 'custom/dog'],
188+
});
189+
190+
const expected = {
191+
key: 'testKey',
192+
kind: 'user',
193+
name: 'testName',
194+
cat: 'calico',
195+
dog: 'lab',
196+
anonymous: true,
197+
_meta: {
198+
privateAttributes: ['/a/b/c', 'cat', 'custom/dog'],
199+
},
200+
};
201+
202+
it('it can convert from LDContext to Context and back to LDContext', () => {
203+
expect(Context.toLDContext(input)).toEqual(expected);
204+
});
205+
});
206+
207+
describe('given a user context without private attributes', () => {
208+
const input = Context.fromLDContext({
209+
key: 'testKey',
210+
name: 'testName',
211+
custom: { cat: 'calico', dog: 'lab' },
212+
anonymous: true,
213+
});
214+
215+
const expected = {
216+
key: 'testKey',
217+
kind: 'user',
218+
name: 'testName',
219+
cat: 'calico',
220+
dog: 'lab',
221+
anonymous: true,
222+
};
223+
224+
it('it can convert from LDContext to Context and back to LDContext', () => {
225+
expect(Context.toLDContext(input)).toEqual(expected);
226+
});
227+
});
228+
229+
describe('given a single context with private attributes', () => {
230+
const input = Context.fromLDContext({
231+
kind: 'org',
232+
key: 'testKey',
233+
name: 'testName',
234+
cat: 'calico',
235+
dog: 'lab',
236+
anonymous: true,
237+
_meta: {
238+
privateAttributes: ['/a/b/c', 'cat', 'dog'],
239+
},
240+
});
241+
242+
const expected = {
243+
kind: 'org',
244+
key: 'testKey',
245+
name: 'testName',
246+
cat: 'calico',
247+
dog: 'lab',
248+
anonymous: true,
249+
_meta: {
250+
privateAttributes: ['/a/b/c', 'cat', 'dog'],
251+
},
252+
};
253+
254+
it('it can convert from LDContext to Context and back to LDContext', () => {
255+
expect(Context.toLDContext(input)).toEqual(expected);
256+
});
257+
});
258+
259+
describe('given a single context without meta', () => {
260+
const input = Context.fromLDContext({
261+
kind: 'org',
262+
key: 'testKey',
263+
name: 'testName',
264+
cat: 'calico',
265+
dog: 'lab',
266+
anonymous: true,
267+
});
268+
269+
const expected = {
270+
kind: 'org',
271+
key: 'testKey',
272+
name: 'testName',
273+
cat: 'calico',
274+
dog: 'lab',
275+
anonymous: true,
276+
};
277+
278+
it('it can convert from LDContext to Context and back to LDContext', () => {
279+
expect(Context.toLDContext(input)).toEqual(expected);
280+
});
281+
});
282+
283+
describe('given a multi context', () => {
284+
const input = Context.fromLDContext({
285+
kind: 'multi',
286+
org: {
287+
key: 'testKey',
288+
name: 'testName',
289+
cat: 'calico',
290+
dog: 'lab',
291+
anonymous: true,
292+
_meta: {
293+
privateAttributes: ['/a/b/c', 'cat', 'custom/dog'],
294+
},
295+
},
296+
customer: {
297+
key: 'testKey',
298+
name: 'testName',
299+
bird: 'party parrot',
300+
chicken: 'hen',
301+
},
302+
});
303+
304+
const expected = {
305+
kind: 'multi',
306+
org: {
307+
key: 'testKey',
308+
name: 'testName',
309+
cat: 'calico',
310+
dog: 'lab',
311+
anonymous: true,
312+
_meta: {
313+
privateAttributes: ['/a/b/c', 'cat', 'custom/dog'],
314+
},
315+
},
316+
customer: {
317+
key: 'testKey',
318+
name: 'testName',
319+
bird: 'party parrot',
320+
chicken: 'hen',
321+
},
322+
};
323+
324+
it('it can convert from LDContext to Context and back to LDContext', () => {
325+
expect(Context.toLDContext(input)).toEqual(expected);
326+
});
327+
});

packages/shared/common/src/Context.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,11 @@ function legacyToSingleKind(user: LDUser): LDSingleKindContext {
140140
if (user.country !== null && user.country !== undefined) {
141141
singleKindContext.country = user.country;
142142
}
143+
if (user.privateAttributeNames !== null && user.privateAttributeNames !== undefined) {
144+
singleKindContext._meta = {
145+
privateAttributes: user.privateAttributeNames,
146+
};
147+
}
143148

144149
// We are not pulling private attributes over because we will serialize
145150
// those from attribute references for events.
@@ -338,6 +343,31 @@ export default class Context {
338343
return Context.contextForError('unknown', 'Context was not of a valid kind');
339344
}
340345

346+
/**
347+
* Creates a {@link LDContext} from a {@link Context}.
348+
* @param context to be converted
349+
* @returns an {@link LDContext} if input was valid, otherwise undefined
350+
*/
351+
public static toLDContext(context: Context): LDContext | undefined {
352+
if (!context.valid) {
353+
return undefined;
354+
}
355+
356+
const contexts = context.getContexts();
357+
if (!context.isMulti) {
358+
return contexts[0][1];
359+
}
360+
const result: LDMultiKindContext = {
361+
kind: 'multi',
362+
};
363+
contexts.forEach((kindAndContext) => {
364+
const kind = kindAndContext[0];
365+
const nestedContext = kindAndContext[1];
366+
result[kind] = nestedContext;
367+
});
368+
return result;
369+
}
370+
341371
/**
342372
* Attempt to get a value for the given context kind using the given reference.
343373
* @param reference The reference to the value to get.

packages/shared/common/src/api/platform/Storage.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,25 @@
1+
/**
2+
* Interface for a data store that holds feature flag data and other SDK
3+
* properties in a serialized form.
4+
*
5+
* This interface should be used for platform-specific integrations that store
6+
* data somewhere other than in memory. Each data item is uniquely identified by
7+
* a string typically constructed following a namespacing structure that
8+
* is then encoded.
9+
*
10+
* Implementations may not throw exceptions.
11+
*
12+
* The SDK assumes that the persistence is only being used by a single instance
13+
* of the SDK per SDK key (two different SDK instances, with 2 different SDK
14+
* keys could use the same persistence instance).
15+
*
16+
* The SDK, with correct usage, will not have overlapping writes to the same
17+
* key.
18+
*
19+
* This interface does not depend on the ability to list the contents of the
20+
* store or namespaces. This is to maintain the simplicity of implementing a
21+
* key-value store on many platforms.
22+
*/
123
export interface Storage {
224
get: (key: string) => Promise<string | null>;
325
set: (key: string, value: string) => Promise<void>;

packages/shared/mocks/src/crypto.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ export let hasher: Hasher;
66
export const setupCrypto = () => {
77
let counter = 0;
88
hasher = {
9-
update: jest.fn(),
9+
update: jest.fn(() => hasher),
1010
digest: jest.fn(() => '1234567890123456'),
1111
};
1212

packages/shared/sdk-client/src/LDClientImpl.events.test.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import {
99
import { InputCustomEvent, InputIdentifyEvent } from '@launchdarkly/js-sdk-common/dist/internal';
1010
import {
1111
basicPlatform,
12-
hasher,
1312
logger,
1413
MockEventProcessor,
1514
setupMockStreamingProcessor,
@@ -59,7 +58,6 @@ describe('sdk-client object', () => {
5958
);
6059
setupMockStreamingProcessor(false, defaultPutResponse);
6160
basicPlatform.crypto.randomUUID.mockReturnValue('random1');
62-
hasher.digest.mockReturnValue('digested1');
6361

6462
ldc = new LDClientImpl(testSdkKey, AutoEnvAttributes.Enabled, basicPlatform, {
6563
logger,

0 commit comments

Comments
 (0)