diff --git a/packages/shared/sdk-client/__tests__/datasource/fdv2/FDv2DataSource.test.ts b/packages/shared/sdk-client/__tests__/datasource/fdv2/FDv2DataSource.test.ts index 815ec9261f..285ee0833f 100644 --- a/packages/shared/sdk-client/__tests__/datasource/fdv2/FDv2DataSource.test.ts +++ b/packages/shared/sdk-client/__tests__/datasource/fdv2/FDv2DataSource.test.ts @@ -1180,3 +1180,49 @@ it('stops initializer chain when a transfer-none changeSet triggers fdv1 fallbac expect(dataCallback).toHaveBeenCalledWith(fdv1Payload); ds.close(); }); + +// -- FDv1 fallback re-trigger guard (regression: SDK-2617) -- + +it('does not re-trigger fallback when the fdv1 synchronizer itself yields a fallback-flagged result', async () => { + const dataCallback = jest.fn(); + const statusManager = makeStatusManager(); + const logger = makeLogger(); + + const fdv2Payload = makePayload({ state: 'fdv2-selector' }); + const fdv1PayloadA = makePayload({ state: 'fdv1-a' }); + const fdv1PayloadB = makePayload({ state: 'fdv1-b' }); + + let fdv2Created = 0; + const fdv2Factory = jest.fn(() => { + fdv2Created += 1; + return makeMockSynchronizer([changeSet(fdv2Payload, { fdv1Fallback: true, fdv1FallbackTtlMs: 0 })]); + }); + const fdv1Sync = makeMockSynchronizer([ + changeSet(fdv1PayloadA, { fdv1Fallback: true }), + changeSet(fdv1PayloadB, { fdv1Fallback: false }), + ]); + + const slots: SynchronizerSlot[] = [ + createSynchronizerSlot({ create: fdv2Factory }), + createSynchronizerSlot({ create: () => fdv1Sync }, { isFDv1Fallback: true }), + ]; + + const ds = createFDv2DataSource({ + initializerFactories: [], + synchronizerSlots: slots, + dataCallback, + statusManager, + selectorGetter: noSelector, + logger, + }); + + await ds.start(); + + await statusManager.waitForState('VALID', 3); + expect(dataCallback).toHaveBeenCalledWith(fdv1PayloadA); + expect(dataCallback).toHaveBeenCalledWith(fdv1PayloadB); + expect(fdv2Created).toBe(1); + + ds.close(); +}); + diff --git a/packages/shared/sdk-client/src/datasource/fdv2/FDv2DataSource.ts b/packages/shared/sdk-client/src/datasource/fdv2/FDv2DataSource.ts index fba90989a7..76fa2fbac0 100644 --- a/packages/shared/sdk-client/src/datasource/fdv2/FDv2DataSource.ts +++ b/packages/shared/sdk-client/src/datasource/fdv2/FDv2DataSource.ts @@ -132,6 +132,11 @@ export function createFDv2DataSource(config: FDv2DataSourceConfig): FDv2DataSour } function handleFdv1Fallback(result: FDv2SourceResult): boolean { + // Guard: if the FDv1 fallback synchronizer itself produces a result flagged + // fdv1Fallback, do not re-run the fallback machinery - we are already on FDv1. + if (sourceManager.isCurrentSynchronizerFDv1Fallback) { + return false; + } if (result.fdv1Fallback && sourceManager.hasFDv1Fallback()) { sourceManager.fdv1Fallback(); return true; @@ -255,7 +260,7 @@ export function createFDv2DataSource(config: FDv2DataSourceConfig): FDv2DataSour logger?.debug('Fallback condition active for current synchronizer.'); } - // try/finally ensures conditions are closed on all code paths. + // Conditions hold timers; close them even if the inner loop throws or breaks early. let synchronizerRunning = true; try { while (!closed && synchronizerRunning) {