Skip to content

Commit 7d6299f

Browse files
authored
feat: FDv2 Cache Initializer (#1147)
<!-- CURSOR_SUMMARY --> > [!NOTE] > **Medium Risk** > Touches persistence and FDv2 data-source plumbing by adding new cached-data paths and new storage keys, which could affect startup behavior and cache correctness across contexts. Changes are well-covered by new unit tests but still impact core flag loading/poll timing logic. > > **Overview** > Adds an FDv2 `CacheInitializer` that loads cached flag evaluations from persistent storage (including legacy-key fallback), returns them as a changeSet without a selector, and supports early shutdown via `close()`. > > Introduces cache “freshness” metadata: `FlagPersistence` now writes/evicts `{contextKey}_freshness` records (timestamp + context hash), `readFreshness` validates them, and `FDv2SourceResult.changeSet` can carry an optional `freshness` value; also adds `calculatePollDelay` to derive the next poll delay from freshness. > > Refactors/expands tests to cover the new cache initializer, freshness persistence/eviction, and updates existing storage tests for the additional storage writes; shared test helpers are centralized in `flagManagerTestHelpers.ts`. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit ccae06f. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> <!-- devin-review-badge-begin --> --- <a href="https://app.devin.ai/review/launchdarkly/js-core/pull/1147" target="_blank"> <picture> <source media="(prefers-color-scheme: dark)" srcset="https://static.devin.ai/assets/gh-open-in-devin-review-dark.svg?v=1"> <img src="https://static.devin.ai/assets/gh-open-in-devin-review-light.svg?v=1" alt="Open with Devin"> </picture> </a> <!-- devin-review-badge-end -->
1 parent e46769b commit 7d6299f

13 files changed

Lines changed: 957 additions & 252 deletions

File tree

packages/shared/sdk-client/__tests__/LDClientImpl.storage.test.ts

Lines changed: 33 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,7 @@ describe('sdk-client storage', () => {
250250
await jest.runAllTimersAsync();
251251

252252
expect(ldc.allFlags()).not.toHaveProperty('dev-test-flag');
253-
expect(mockPlatform.storage.set).toHaveBeenCalledTimes(2);
253+
expect(mockPlatform.storage.set).toHaveBeenCalledTimes(3);
254254
expect(mockPlatform.storage.set).toHaveBeenNthCalledWith(
255255
1,
256256
indexStorageKey,
@@ -291,7 +291,7 @@ describe('sdk-client storage', () => {
291291
await jest.runAllTimersAsync();
292292

293293
expect(ldc.allFlags()).toMatchObject({ 'another-dev-test-flag': false });
294-
expect(mockPlatform.storage.set).toHaveBeenCalledTimes(2);
294+
expect(mockPlatform.storage.set).toHaveBeenCalledTimes(3);
295295
expect(mockPlatform.storage.set).toHaveBeenNthCalledWith(
296296
1,
297297
indexStorageKey,
@@ -373,7 +373,7 @@ describe('sdk-client storage', () => {
373373
await ldc.identify(context);
374374
await jest.runAllTimersAsync();
375375

376-
expect(mockPlatform.storage.set).toHaveBeenCalledTimes(2);
376+
expect(mockPlatform.storage.set).toHaveBeenCalledTimes(3);
377377
expect(mockPlatform.storage.set).toHaveBeenNthCalledWith(
378378
1,
379379
indexStorageKey,
@@ -421,7 +421,11 @@ describe('sdk-client storage', () => {
421421
await changePromise;
422422
await jest.runAllTimersAsync();
423423

424-
const flagsInStorage = JSON.parse(mockPlatform.storage.set.mock.lastCall[1]) as Flags;
424+
const flagsInStorage = JSON.parse(
425+
mockPlatform.storage.set.mock.calls
426+
.filter(([key]: [string]) => key === flagStorageKey)
427+
.pop()![1],
428+
) as Flags;
425429
expect(ldc.allFlags()).toMatchObject({ 'dev-test-flag': true });
426430
expect(flagsInStorage['dev-test-flag'].reason).toEqual({
427431
kind: 'RULE_MATCH',
@@ -455,9 +459,13 @@ describe('sdk-client storage', () => {
455459
await changePromise;
456460
await jest.runAllTimersAsync();
457461

458-
const flagsInStorage = JSON.parse(mockPlatform.storage.set.mock.lastCall[1]) as Flags;
462+
const flagsInStorage = JSON.parse(
463+
mockPlatform.storage.set.mock.calls
464+
.filter(([key]: [string]) => key === flagStorageKey)
465+
.pop()![1],
466+
) as Flags;
459467
expect(ldc.allFlags()).toMatchObject({ 'dev-test-flag': false });
460-
expect(mockPlatform.storage.set).toHaveBeenCalledTimes(4);
468+
expect(mockPlatform.storage.set).toHaveBeenCalledTimes(6);
461469
expect(flagsInStorage['dev-test-flag'].version).toEqual(patchResponse.version);
462470
expect(emitter.emit).toHaveBeenCalledWith('change', context, ['dev-test-flag']);
463471
});
@@ -482,13 +490,12 @@ describe('sdk-client storage', () => {
482490
await changePromise;
483491
await jest.runAllTimersAsync();
484492

485-
const flagsInStorage = JSON.parse(mockPlatform.storage.set.mock.lastCall[1]) as Flags;
493+
const flagsInStorage = JSON.parse(
494+
mockPlatform.storage.set.mock.calls
495+
.filter(([key]: [string]) => key === flagStorageKey)
496+
.pop()![1],
497+
) as Flags;
486498
expect(ldc.allFlags()).toHaveProperty('another-dev-test-flag');
487-
expect(mockPlatform.storage.set).toHaveBeenNthCalledWith(
488-
4,
489-
flagStorageKey,
490-
expect.stringContaining(JSON.stringify(patchResponse)),
491-
);
492499
expect(flagsInStorage).toHaveProperty('another-dev-test-flag');
493500
expect(emitter.emit).toHaveBeenCalledWith('change', context, ['another-dev-test-flag']);
494501
});
@@ -516,7 +523,7 @@ describe('sdk-client storage', () => {
516523
await jest.runAllTimersAsync();
517524

518525
// the initial put is resulting in two sets, one for the index and one for the flag data
519-
expect(mockPlatform.storage.set).toHaveBeenCalledTimes(2);
526+
expect(mockPlatform.storage.set).toHaveBeenCalledTimes(3);
520527
expect(emitter.emit).not.toHaveBeenCalledWith('change');
521528

522529
// this is defaultPutResponse
@@ -556,13 +563,12 @@ describe('sdk-client storage', () => {
556563
await changePromise;
557564
await jest.runAllTimersAsync();
558565

559-
const flagsInStorage = JSON.parse(mockPlatform.storage.set.mock.lastCall[1]) as Flags;
566+
const flagsInStorage = JSON.parse(
567+
mockPlatform.storage.set.mock.calls
568+
.filter(([key]: [string]) => key === flagStorageKey)
569+
.pop()![1],
570+
) as Flags;
560571
expect(ldc.allFlags()).not.toHaveProperty('dev-test-flag');
561-
expect(mockPlatform.storage.set).toHaveBeenNthCalledWith(
562-
4,
563-
flagStorageKey,
564-
expect.stringContaining('dev-test-flag'),
565-
);
566572
expect(flagsInStorage['dev-test-flag']).toMatchObject({ ...deleteResponse, deleted: true });
567573
expect(emitter.emit).toHaveBeenCalledWith('change', context, ['dev-test-flag']);
568574
});
@@ -591,7 +597,7 @@ describe('sdk-client storage', () => {
591597

592598
expect(ldc.allFlags()).toHaveProperty('dev-test-flag');
593599
// the initial put is resulting in two sets, one for the index and one for the flag data
594-
expect(mockPlatform.storage.set).toHaveBeenCalledTimes(2);
600+
expect(mockPlatform.storage.set).toHaveBeenCalledTimes(3);
595601
expect(emitter.emit).not.toHaveBeenCalledWith('change');
596602
});
597603

@@ -619,7 +625,7 @@ describe('sdk-client storage', () => {
619625

620626
expect(ldc.allFlags()).toHaveProperty('dev-test-flag');
621627
// the initial put is resulting in two sets, one for the index and one for the flag data
622-
expect(mockPlatform.storage.set).toHaveBeenCalledTimes(2);
628+
expect(mockPlatform.storage.set).toHaveBeenCalledTimes(3);
623629
expect(emitter.emit).not.toHaveBeenCalledWith('change');
624630
});
625631

@@ -645,9 +651,13 @@ describe('sdk-client storage', () => {
645651
await changePromise;
646652
await jest.runAllTimersAsync();
647653

648-
const flagsInStorage = JSON.parse(mockPlatform.storage.set.mock.lastCall[1]) as Flags;
654+
const flagsInStorage = JSON.parse(
655+
mockPlatform.storage.set.mock.calls
656+
.filter(([key]: [string]) => key === flagStorageKey)
657+
.pop()![1],
658+
) as Flags;
649659

650-
expect(mockPlatform.storage.set).toHaveBeenCalledTimes(4); // two index saves and two flag saves
660+
expect(mockPlatform.storage.set).toHaveBeenCalledTimes(6); // two index saves and two flag saves
651661
expect(flagsInStorage['does-not-exist']).toMatchObject({ ...deleteResponse, deleted: true });
652662
expect(emitter.emit).toHaveBeenCalledWith('change', context, ['does-not-exist']);
653663
});

0 commit comments

Comments
 (0)