From 57498a9ed30441e5d33224b899fe1735d4024dd3 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Wed, 28 Oct 2026 21:37:32 +0000 Subject: [PATCH 1/2] fix: Build the overrides-only all flags state from the override layer Before the client is initialized and while the store is not initialized, allFlagsState read the merged view and reported a valid state whenever it returned any flag and a source was configured. A store that holds LaunchDarkly flags without being initialized therefore produced a valid state full of LaunchDarkly data when the layer was empty, and LaunchDarkly flags next to the overridden ones when it was not. The overrides-only state is now built from the layer's flags, and a layer without flags leaves the state as it is without an override source. --- .../__tests__/LDClientImpl.overrides.test.ts | 62 +++++++++++++++++++ .../shared/sdk-server/src/LDClientImpl.ts | 41 ++++++------ 2 files changed, 83 insertions(+), 20 deletions(-) diff --git a/packages/shared/sdk-server/__tests__/LDClientImpl.overrides.test.ts b/packages/shared/sdk-server/__tests__/LDClientImpl.overrides.test.ts index 54a0248a59..b05b1d3ebd 100644 --- a/packages/shared/sdk-server/__tests__/LDClientImpl.overrides.test.ts +++ b/packages/shared/sdk-server/__tests__/LDClientImpl.overrides.test.ts @@ -398,6 +398,68 @@ describe('given an uninitialized client over a persistent store that does not an }); }); +// A persistent store that holds LaunchDarkly flags but reports that it is not initialized, as a +// store that has data without the marker that a complete data set was written. +function storeWithFlagsButNotInitialized(flags: Record): LDFeatureStore { + return { + get: (_kind, key, callback) => callback(_kind.namespace === 'features' ? flags[key] : null), + all: (kind, callback) => callback(kind.namespace === 'features' ? { ...flags } : {}), + init: (_data, callback) => callback(), + delete: (_kind, _key, _version, callback) => callback(), + upsert: (_kind, _data, callback) => callback(), + initialized: (callback) => callback(false), + close: () => {}, + getDescription: () => 'not initialized', + } as LDFeatureStore; +} + +describe('given an uninitialized client over a store that holds flags but is not initialized', () => { + const storeFlags = { 'ld-flag': singleValueFlag('ld-flag', 'ld-value') }; + let client: LDClientImpl; + let logger: MockLogger; + + const makeClient = (overrides?: TestOverrideSource) => + makeFDv2Client(makeFDv2Platform(), { + logger, + dataSystem: { overrides, persistentStore: storeWithFlagsButNotInitialized(storeFlags) }, + }); + + beforeEach(() => { + logger = makeMockLogger(); + }); + + afterEach(() => { + client.close(); + }); + + it('reports the same invalid all flags state with an empty override layer as without a source', async () => { + client = makeClient(); + const withoutSource = await client.allFlagsState(user); + client.close(); + + client = makeClient(new TestOverrideSource()); + const withEmptySource = await client.allFlagsState(user); + + // Configuring a source that supplies nothing changes nothing. + expect(withoutSource.valid).toBe(false); + expect(withEmptySource.toJSON()).toEqual(withoutSource.toJSON()); + expect(warningsMatching(logger, /Data store not available/)).toEqual(2); + expect(warningsMatching(logger, /returning only flags from the override layer/)).toEqual(0); + }); + + it('reports only the flags of the override layer, not the flags in the store', async () => { + client = makeClient( + new TestOverrideSource({ flags: [singleValueFlag('overridden-flag', true)] }), + ); + + const state = await client.allFlagsState(user); + + expect(state.valid).toBe(true); + expect(state.allValues()).toEqual({ 'overridden-flag': true }); + expect(warningsMatching(logger, /returning only flags from the override layer/)).toEqual(1); + }); +}); + describe('given a client whose initialization fails and an override source with an asynchronous initial load', () => { let source: TestOverrideSource; let client: LDClientImpl; diff --git a/packages/shared/sdk-server/src/LDClientImpl.ts b/packages/shared/sdk-server/src/LDClientImpl.ts index 6f773ce53d..1d968bb5d9 100644 --- a/packages/shared/sdk-server/src/LDClientImpl.ts +++ b/packages/shared/sdk-server/src/LDClientImpl.ts @@ -1343,27 +1343,28 @@ export default class LDClientImpl implements LDClient { doEval(true); return; } - // No LaunchDarkly data is available. The read returns only the flags that the override - // layer holds, and the result decides the state: a layer without flags gives the state - // nothing to report. - this._readStore.all(VersionedDataKinds.Features, (allFlags) => { - if (this._overrideLayer && Object.keys(allFlags).length > 0) { - if (!this._allFlagsStateOverridesOnlyWarningLogged) { - this._allFlagsStateOverridesOnlyWarningLogged = true; - this._logger?.warn( - 'Called allFlagsState before client initialization; returning only flags from' + - ' the override layer. This message is logged once.', - ); - } - evaluateAll(true, allFlags); - return; + // No LaunchDarkly data is available. When the override layer holds flags, the state + // contains only those flags, read from the layer itself rather than through the overlay: + // a store that is not initialized can still hold LaunchDarkly flags, and a state marked + // valid must not present them. A layer without flags leaves the state as it is without an + // override source. + const overrideFlags = this._overrideLayer?.all(VersionedDataKinds.Features); + if (overrideFlags && Object.keys(overrideFlags).length > 0) { + if (!this._allFlagsStateOverridesOnlyWarningLogged) { + this._allFlagsStateOverridesOnlyWarningLogged = true; + this._logger?.warn( + 'Called allFlagsState before client initialization; returning only flags from' + + ' the override layer. This message is logged once.', + ); } - this._logger?.warn( - 'Called allFlagsState before client initialization. Data store not available; ' + - 'returning empty state', - ); - evaluateAll(false, allFlags); - }); + evaluateAll(true, overrideFlags); + return; + } + this._logger?.warn( + 'Called allFlagsState before client initialization. Data store not available; ' + + 'returning empty state', + ); + doEval(false); }); }); } From 4d267016669ea9c383f799bc9c98607cedb4c65f Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Thu, 29 Oct 2026 16:02:41 +0000 Subject: [PATCH 2/2] fix: Keep the store's flags in the uninitialized all flags state when overrides are present Without a source, an uninitialized store's flags are evaluated into an invalid state. Building the state from the override layer alone dropped them whenever the layer held flags, so configuring overrides removed flags from the state. The read goes through the overlay again, so the state holds the store's flags with the override entries taking precedence; whether the layer holds flags still decides the validity, so an empty layer leaves the state exactly as without a source. --- .../__tests__/LDClientImpl.overrides.test.ts | 34 ++++++++++---- .../shared/sdk-server/src/LDClientImpl.ts | 44 ++++++++++--------- 2 files changed, 48 insertions(+), 30 deletions(-) diff --git a/packages/shared/sdk-server/__tests__/LDClientImpl.overrides.test.ts b/packages/shared/sdk-server/__tests__/LDClientImpl.overrides.test.ts index b05b1d3ebd..7fabcc6581 100644 --- a/packages/shared/sdk-server/__tests__/LDClientImpl.overrides.test.ts +++ b/packages/shared/sdk-server/__tests__/LDClientImpl.overrides.test.ts @@ -113,7 +113,7 @@ describe('given an uninitialized client with an override source', () => { }); }); - it('reports only the override layer in the all flags state and warns once', async () => { + it('reports the override layer in the all flags state of an empty store and warns once', async () => { const state = await client.allFlagsState(user, { withReasons: true }); expect(state.valid).toBe(true); @@ -124,7 +124,7 @@ describe('given an uninitialized client with an override source', () => { }); await client.allFlagsState(user); - expect(warningsMatching(logger, /returning only flags from the override layer/)).toEqual(1); + expect(warningsMatching(logger, /returning flags from the override layer/)).toEqual(1); }); it('reports an invalid empty all flags state when the override layer holds only segments', async () => { @@ -137,7 +137,7 @@ describe('given an uninitialized client with an override source', () => { expect(state.valid).toBe(false); expect(state.allValues()).toEqual({}); expect(warningsMatching(logger, /Data store not available/)).toEqual(1); - expect(warningsMatching(logger, /returning only flags from the override layer/)).toEqual(0); + expect(warningsMatching(logger, /returning flags from the override layer/)).toEqual(0); }); it('reports an invalid empty all flags state when the override layer is empty', async () => { @@ -414,7 +414,10 @@ function storeWithFlagsButNotInitialized(flags: Record): LDFeatureS } describe('given an uninitialized client over a store that holds flags but is not initialized', () => { - const storeFlags = { 'ld-flag': singleValueFlag('ld-flag', 'ld-value') }; + const storeFlags = { + 'ld-flag': singleValueFlag('ld-flag', 'ld-value'), + 'other-ld-flag': singleValueFlag('other-ld-flag', 'other-value'), + }; let client: LDClientImpl; let logger: MockLogger; @@ -444,19 +447,32 @@ describe('given an uninitialized client over a store that holds flags but is not expect(withoutSource.valid).toBe(false); expect(withEmptySource.toJSON()).toEqual(withoutSource.toJSON()); expect(warningsMatching(logger, /Data store not available/)).toEqual(2); - expect(warningsMatching(logger, /returning only flags from the override layer/)).toEqual(0); + expect(warningsMatching(logger, /returning flags from the override layer/)).toEqual(0); }); - it('reports only the flags of the override layer, not the flags in the store', async () => { + it('reports the flags in the store with the override layer taking precedence', async () => { + // Without a source the state holds the store's flags. Overrides add to it and win for a key + // they share with the store; they never take a flag away. client = makeClient( - new TestOverrideSource({ flags: [singleValueFlag('overridden-flag', true)] }), + new TestOverrideSource({ + flags: [ + singleValueFlag('overridden-flag', true), + singleValueFlag('ld-flag', 'override-value'), + ], + }), ); const state = await client.allFlagsState(user); expect(state.valid).toBe(true); - expect(state.allValues()).toEqual({ 'overridden-flag': true }); - expect(warningsMatching(logger, /returning only flags from the override layer/)).toEqual(1); + expect(state.allValues()).toEqual({ + 'ld-flag': 'override-value', + 'other-ld-flag': 'other-value', + 'overridden-flag': true, + }); + expect( + warningsMatching(logger, /returning flags from the override layer and the data store/), + ).toEqual(1); }); }); diff --git a/packages/shared/sdk-server/src/LDClientImpl.ts b/packages/shared/sdk-server/src/LDClientImpl.ts index 1d968bb5d9..1f4541ba8c 100644 --- a/packages/shared/sdk-server/src/LDClientImpl.ts +++ b/packages/shared/sdk-server/src/LDClientImpl.ts @@ -1343,28 +1343,30 @@ export default class LDClientImpl implements LDClient { doEval(true); return; } - // No LaunchDarkly data is available. When the override layer holds flags, the state - // contains only those flags, read from the layer itself rather than through the overlay: - // a store that is not initialized can still hold LaunchDarkly flags, and a state marked - // valid must not present them. A layer without flags leaves the state as it is without an - // override source. - const overrideFlags = this._overrideLayer?.all(VersionedDataKinds.Features); - if (overrideFlags && Object.keys(overrideFlags).length > 0) { - if (!this._allFlagsStateOverridesOnlyWarningLogged) { - this._allFlagsStateOverridesOnlyWarningLogged = true; - this._logger?.warn( - 'Called allFlagsState before client initialization; returning only flags from' + - ' the override layer. This message is logged once.', - ); + // No LaunchDarkly data is available. Without overrides, the state is invalid and holds + // whatever flags the store has. Overrides only add to that, never remove: the read goes + // through the overlay, so the state holds the store's flags with the override layer's + // entries taking precedence. Whether the layer holds flags decides the validity, so a + // layer without flags leaves the state exactly as it is without an override source. + this._readStore.all(VersionedDataKinds.Features, (allFlags) => { + const overrideFlags = this._overrideLayer?.all(VersionedDataKinds.Features); + if (overrideFlags && Object.keys(overrideFlags).length > 0) { + if (!this._allFlagsStateOverridesOnlyWarningLogged) { + this._allFlagsStateOverridesOnlyWarningLogged = true; + this._logger?.warn( + 'Called allFlagsState before client initialization; returning flags from the' + + ' override layer and the data store. This message is logged once.', + ); + } + evaluateAll(true, allFlags); + return; } - evaluateAll(true, overrideFlags); - return; - } - this._logger?.warn( - 'Called allFlagsState before client initialization. Data store not available; ' + - 'returning empty state', - ); - doEval(false); + this._logger?.warn( + 'Called allFlagsState before client initialization. Data store not available; ' + + 'returning empty state', + ); + evaluateAll(false, allFlags); + }); }); }); }