Skip to content

Commit 431fce0

Browse files
committed
fix: disable cache was not working
1 parent d6da2d4 commit 431fce0

8 files changed

Lines changed: 258 additions & 2 deletions

File tree

packages/shared/sdk-client/__tests__/flag-manager/FlagManager.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ describe('FlagManager override tests', () => {
2525
mockPlatform,
2626
TEST_SDK_KEY,
2727
TEST_MAX_CACHED_CONTEXTS,
28+
false,
2829
mockLogger,
2930
);
3031
});

packages/shared/sdk-client/__tests__/flag-manager/FlagPersistence.test.ts

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ describe('FlagPersistence tests', () => {
2828
makeMockPlatform(makeMemoryStorage(), makeMockCrypto()),
2929
TEST_NAMESPACE,
3030
5,
31+
false,
3132
flagStore,
3233
createFlagUpdater(flagStore, mockLogger),
3334
mockLogger,
@@ -45,6 +46,7 @@ describe('FlagPersistence tests', () => {
4546
makeMockPlatform(makeCorruptStorage(), makeMockCrypto()),
4647
TEST_NAMESPACE,
4748
5,
49+
false,
4850
flagStore,
4951
createFlagUpdater(flagStore, mockLogger),
5052
mockLogger,
@@ -72,6 +74,7 @@ describe('FlagPersistence tests', () => {
7274
makeMockPlatform(makeMemoryStorage(), makeMockCrypto()),
7375
TEST_NAMESPACE,
7476
5,
77+
false,
7578
flagStore,
7679
flagUpdater,
7780
mockLogger,
@@ -100,6 +103,7 @@ describe('FlagPersistence tests', () => {
100103
makeMockPlatform(memoryStorage, makeMockCrypto()),
101104
TEST_NAMESPACE,
102105
5,
106+
false,
103107
flagStore,
104108
flagUpdater,
105109
mockLogger,
@@ -129,6 +133,7 @@ describe('FlagPersistence tests', () => {
129133
mockPlatform,
130134
TEST_NAMESPACE,
131135
5,
136+
false,
132137
flagStore,
133138
flagUpdater,
134139
mockLogger,
@@ -165,6 +170,7 @@ describe('FlagPersistence tests', () => {
165170
mockPlatform,
166171
TEST_NAMESPACE,
167172
1,
173+
false,
168174
flagStore,
169175
flagUpdater,
170176
mockLogger,
@@ -213,6 +219,7 @@ describe('FlagPersistence tests', () => {
213219
mockPlatform,
214220
TEST_NAMESPACE,
215221
1,
222+
false,
216223
flagStore,
217224
flagUpdater,
218225
mockLogger,
@@ -244,6 +251,7 @@ describe('FlagPersistence tests', () => {
244251
mockPlatform,
245252
TEST_NAMESPACE,
246253
5,
254+
false,
247255
flagStore,
248256
flagUpdater,
249257
mockLogger,
@@ -277,6 +285,7 @@ describe('FlagPersistence tests', () => {
277285
mockPlatform,
278286
TEST_NAMESPACE,
279287
5,
288+
false,
280289
flagStore,
281290
flagUpdater,
282291
mockLogger,
@@ -305,6 +314,165 @@ describe('FlagPersistence tests', () => {
305314
expect(await memoryStorage.get(contextDataKey)).toContain('"version":2');
306315
});
307316

317+
it('does not write to storage when maxCachedContexts is 0', async () => {
318+
const memoryStorage = makeMemoryStorage();
319+
const mockPlatform = makeMockPlatform(memoryStorage, makeMockCrypto());
320+
const flagStore = createDefaultFlagStore();
321+
const mockLogger = makeMockLogger();
322+
const flagUpdater = createFlagUpdater(flagStore, mockLogger);
323+
324+
const fpUnderTest = new FlagPersistence(
325+
mockPlatform,
326+
TEST_NAMESPACE,
327+
0,
328+
false,
329+
flagStore,
330+
flagUpdater,
331+
mockLogger,
332+
);
333+
334+
const context = Context.fromLDContext({ kind: 'org', key: 'TestyPizza' });
335+
const flags = { flagA: { version: 1, flag: makeMockFlag() } };
336+
337+
await fpUnderTest.init(context, flags);
338+
339+
const contextDataKey = await namespaceForContextData(mockPlatform.crypto, TEST_NAMESPACE, context);
340+
expect(await memoryStorage.get(contextDataKey)).toBeNull();
341+
});
342+
343+
it('clears previously cached data when maxCachedContexts is 0', async () => {
344+
const memoryStorage = makeMemoryStorage();
345+
const crypto = makeMockCrypto();
346+
const mockPlatform = makeMockPlatform(memoryStorage, crypto);
347+
const flagStore = createDefaultFlagStore();
348+
const mockLogger = makeMockLogger();
349+
const flagUpdater = createFlagUpdater(flagStore, mockLogger);
350+
351+
const contextA = Context.fromLDContext({ kind: 'org', key: 'TestyPizza' });
352+
const storageKeyA = await namespaceForContextData(crypto, TEST_NAMESPACE, contextA);
353+
const indexKey = await namespaceForContextIndex(TEST_NAMESPACE);
354+
355+
// Pre-populate storage as if a prior session had maxCachedContexts > 0
356+
const indexJson = JSON.stringify({ index: [{ id: storageKeyA, timestamp: 1 }] });
357+
await memoryStorage.set(indexKey, indexJson);
358+
await memoryStorage.set(storageKeyA, JSON.stringify({ flagA: makeMockFlag() }));
359+
360+
const fpUnderTest = new FlagPersistence(
361+
mockPlatform,
362+
TEST_NAMESPACE,
363+
0,
364+
false,
365+
flagStore,
366+
flagUpdater,
367+
mockLogger,
368+
);
369+
370+
const flags = { flagA: { version: 1, flag: makeMockFlag() } };
371+
await fpUnderTest.init(contextA, flags);
372+
373+
// Existing entry must have been evicted
374+
expect(await memoryStorage.get(storageKeyA)).toBeNull();
375+
// Index must be saved as empty
376+
const savedIndex = JSON.parse((await memoryStorage.get(indexKey))!);
377+
expect(savedIndex.index).toHaveLength(0);
378+
});
379+
380+
it('does not load from storage when maxCachedContexts is 0', async () => {
381+
const memoryStorage = makeMemoryStorage();
382+
const mockPlatform = makeMockPlatform(memoryStorage, makeMockCrypto());
383+
const flagStore = createDefaultFlagStore();
384+
const mockLogger = makeMockLogger();
385+
const flagUpdater = createFlagUpdater(flagStore, mockLogger);
386+
387+
// First write data to storage using a normal FlagPersistence
388+
const writeFp = new FlagPersistence(
389+
mockPlatform,
390+
TEST_NAMESPACE,
391+
5,
392+
false,
393+
flagStore,
394+
flagUpdater,
395+
mockLogger,
396+
);
397+
const context = Context.fromLDContext({ kind: 'org', key: 'TestyPizza' });
398+
const flags = { flagA: { version: 1, flag: makeMockFlag() } };
399+
await writeFp.init(context, flags);
400+
401+
// Now try to load with maxCachedContexts: 0
402+
const fpUnderTest = new FlagPersistence(
403+
mockPlatform,
404+
TEST_NAMESPACE,
405+
0,
406+
false,
407+
flagStore,
408+
flagUpdater,
409+
mockLogger,
410+
);
411+
const didLoad = await fpUnderTest.loadCached(context);
412+
expect(didLoad).toEqual(false);
413+
});
414+
415+
it('does not write to storage when disableCache is true', async () => {
416+
const memoryStorage = makeMemoryStorage();
417+
const mockPlatform = makeMockPlatform(memoryStorage, makeMockCrypto());
418+
const flagStore = createDefaultFlagStore();
419+
const mockLogger = makeMockLogger();
420+
const flagUpdater = createFlagUpdater(flagStore, mockLogger);
421+
422+
const fpUnderTest = new FlagPersistence(
423+
mockPlatform,
424+
TEST_NAMESPACE,
425+
5,
426+
true,
427+
flagStore,
428+
flagUpdater,
429+
mockLogger,
430+
);
431+
432+
const context = Context.fromLDContext({ kind: 'org', key: 'TestyPizza' });
433+
const flags = { flagA: { version: 1, flag: makeMockFlag() } };
434+
435+
await fpUnderTest.init(context, flags);
436+
437+
const contextDataKey = await namespaceForContextData(mockPlatform.crypto, TEST_NAMESPACE, context);
438+
expect(await memoryStorage.get(contextDataKey)).toBeNull();
439+
});
440+
441+
it('does not load from storage when disableCache is true', async () => {
442+
const memoryStorage = makeMemoryStorage();
443+
const mockPlatform = makeMockPlatform(memoryStorage, makeMockCrypto());
444+
const flagStore = createDefaultFlagStore();
445+
const mockLogger = makeMockLogger();
446+
const flagUpdater = createFlagUpdater(flagStore, mockLogger);
447+
448+
// First write data to storage using a normal FlagPersistence
449+
const writeFp = new FlagPersistence(
450+
mockPlatform,
451+
TEST_NAMESPACE,
452+
5,
453+
false,
454+
flagStore,
455+
flagUpdater,
456+
mockLogger,
457+
);
458+
const context = Context.fromLDContext({ kind: 'org', key: 'TestyPizza' });
459+
const flags = { flagA: { version: 1, flag: makeMockFlag() } };
460+
await writeFp.init(context, flags);
461+
462+
// Now try to load with disableCache: true
463+
const fpUnderTest = new FlagPersistence(
464+
mockPlatform,
465+
TEST_NAMESPACE,
466+
5,
467+
true,
468+
flagStore,
469+
flagUpdater,
470+
mockLogger,
471+
);
472+
const didLoad = await fpUnderTest.loadCached(context);
473+
expect(didLoad).toEqual(false);
474+
});
475+
308476
test('upsert ignores inactive context', async () => {
309477
const memoryStorage = makeMemoryStorage();
310478
const mockPlatform = makeMockPlatform(memoryStorage, makeMockCrypto());
@@ -316,6 +484,7 @@ describe('FlagPersistence tests', () => {
316484
mockPlatform,
317485
TEST_NAMESPACE,
318486
5,
487+
false,
319488
flagStore,
320489
flagUpdater,
321490
mockLogger,
@@ -350,6 +519,55 @@ describe('FlagPersistence tests', () => {
350519
expect(await memoryStorage.get(activeContextDataKey)).not.toBeNull();
351520
expect(await memoryStorage.get(inactiveContextDataKey)).toBeNull();
352521
});
522+
523+
it('does not write to storage when current context is pruned due to equal timestamps', async () => {
524+
const memoryStorage = makeMemoryStorage();
525+
const crypto = makeMockCrypto();
526+
const mockPlatform = makeMockPlatform(memoryStorage, crypto);
527+
const flagStore = createDefaultFlagStore();
528+
const mockLogger = makeMockLogger();
529+
const flagUpdater = createFlagUpdater(flagStore, mockLogger);
530+
531+
const contextA = Context.fromLDContext({ kind: 'org', key: 'TestyPizza' });
532+
const contextB = Context.fromLDContext({ kind: 'user', key: 'TestyUser' });
533+
534+
const storageKeyA = await namespaceForContextData(crypto, TEST_NAMESPACE, contextA);
535+
const storageKeyB = await namespaceForContextData(crypto, TEST_NAMESPACE, contextB);
536+
const indexKey = await namespaceForContextIndex(TEST_NAMESPACE);
537+
538+
// Pre-populate storage: index with A before B (same timestamp t=1), and B's flag data
539+
const indexJson = JSON.stringify({
540+
index: [
541+
{ id: storageKeyA, timestamp: 1 },
542+
{ id: storageKeyB, timestamp: 1 },
543+
],
544+
});
545+
await memoryStorage.set(indexKey, indexJson);
546+
await memoryStorage.set(storageKeyB, JSON.stringify({ flagB: makeMockFlag() }));
547+
548+
const fpUnderTest = new FlagPersistence(
549+
mockPlatform,
550+
TEST_NAMESPACE,
551+
1,
552+
false,
553+
flagStore,
554+
flagUpdater,
555+
mockLogger,
556+
() => 1,
557+
);
558+
559+
const flags = { flagA: { version: 1, flag: makeMockFlag() } };
560+
await fpUnderTest.init(contextA, flags);
561+
562+
// A was in the pruned list — must not be re-written to storage
563+
expect(await memoryStorage.get(storageKeyA)).toBeNull();
564+
// B was not pruned — its existing data should be untouched
565+
expect(await memoryStorage.get(storageKeyB)).not.toBeNull();
566+
// Index should contain only B
567+
const savedIndex = JSON.parse((await memoryStorage.get(indexKey))!);
568+
expect(savedIndex.index).toHaveLength(1);
569+
expect(savedIndex.index[0].id).toBe(storageKeyB);
570+
});
353571
});
354572

355573
describe('FlagPersistence freshness', () => {
@@ -363,6 +581,7 @@ describe('FlagPersistence freshness', () => {
363581
makeMockPlatform(memoryStorage, crypto),
364582
TEST_NAMESPACE,
365583
5,
584+
false,
366585
flagStore,
367586
createFlagUpdater(flagStore, mockLogger),
368587
mockLogger,

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ export default class LDClientImpl implements LDClient, LDClientIdentifyResult {
129129
this.platform,
130130
sdkKey,
131131
this._config.maxCachedContexts,
132+
this._config.disableCache,
132133
this._config.logger,
133134
);
134135
this._diagnosticsManager = createDiagnosticsManager(sdkKey, this._config, platform);

packages/shared/sdk-client/src/api/LDOptions.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,4 +279,13 @@ export interface LDOptions {
279279
* @defaultValue false.
280280
*/
281281
cleanOldPersistentData?: boolean;
282+
283+
/**
284+
* Set to true to completely disable the persistent flag cache. When disabled,
285+
* flags are never read from or written to local storage. This takes precedence
286+
* over `maxCachedContexts`.
287+
*
288+
* @defaultValue false
289+
*/
290+
disableCache?: boolean;
282291
}

packages/shared/sdk-client/src/configuration/Configuration.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export interface LDClientInternalOptions extends internal.LDInternalOptions {
2626
export interface Configuration {
2727
readonly logger: LDLogger;
2828
readonly maxCachedContexts: number;
29+
readonly disableCache: boolean;
2930
readonly capacity: number;
3031
readonly diagnosticRecordingInterval: number;
3132
readonly flushInterval: number;
@@ -87,6 +88,7 @@ export default class ConfigurationImpl implements Configuration {
8788
private readonly streamUri = DEFAULT_STREAM;
8889

8990
public readonly maxCachedContexts = 5;
91+
public readonly disableCache: boolean = false;
9092

9193
public readonly capacity = 100;
9294
public readonly diagnosticRecordingInterval = 900;

packages/shared/sdk-client/src/configuration/validators.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ const validators: Record<keyof LDOptions, TypeValidator> = {
3535
hooks: TypeValidators.createTypeArray('Hook[]', {}),
3636
inspectors: TypeValidators.createTypeArray('LDInspection', {}),
3737
cleanOldPersistentData: TypeValidators.Boolean,
38+
disableCache: TypeValidators.Boolean,
3839
};
3940

4041
export default validators;

0 commit comments

Comments
 (0)