Skip to content

Commit 42a3612

Browse files
committed
Refactor config validation.
1 parent f7252ba commit 42a3612

4 files changed

Lines changed: 137 additions & 51 deletions

File tree

packages/sdk/browser/src/BrowserClient.ts

Lines changed: 29 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,9 @@ import {
44
BROWSER_DATA_SYSTEM_DEFAULTS,
55
browserFdv1Endpoints,
66
Configuration,
7-
dataSystemValidators,
87
FlagManager,
98
Hook,
109
internal,
11-
LDClientDataSystemOptions,
1210
LDClientImpl,
1311
LDContext,
1412
LDEmitter,
@@ -22,7 +20,6 @@ import {
2220
Platform,
2321
readFlagsFromBootstrap,
2422
safeRegisterDebugOverridePlugins,
25-
validateOptions,
2623
} from '@launchdarkly/js-client-sdk-common';
2724

2825
import { getHref } from './BrowserApi';
@@ -82,53 +79,34 @@ class BrowserClientImpl extends LDClientImpl {
8279
const { eventUrlTransformer } = validatedBrowserOptions;
8380
const endpoints = browserFdv1Endpoints(clientSideId);
8481

85-
const validatedDataSystem =
86-
options.dataSystem !== undefined
87-
? (validateOptions(
88-
options.dataSystem,
89-
dataSystemValidators,
90-
BROWSER_DATA_SYSTEM_DEFAULTS as unknown as Record<string, unknown>,
91-
logger,
92-
'dataSystem',
93-
) as unknown as LDClientDataSystemOptions)
94-
: undefined;
95-
96-
const dataManagerFactory =
97-
validatedDataSystem !== undefined
98-
? (
99-
flagManager: FlagManager,
100-
configuration: Configuration,
101-
baseHeaders: LDHeaders,
102-
emitter: LDEmitter,
103-
_diagnosticsManager?: internal.DiagnosticsManager,
104-
) =>
105-
new BrowserFDv2DataManager(
106-
platform,
107-
flagManager,
108-
clientSideId,
109-
configuration,
110-
baseHeaders,
111-
emitter,
112-
)
113-
: (
114-
flagManager: FlagManager,
115-
configuration: Configuration,
116-
baseHeaders: LDHeaders,
117-
emitter: LDEmitter,
118-
diagnosticsManager?: internal.DiagnosticsManager,
119-
) =>
120-
new BrowserDataManager(
121-
platform,
122-
flagManager,
123-
clientSideId,
124-
configuration,
125-
validatedBrowserOptions,
126-
endpoints.polling,
127-
endpoints.streaming,
128-
baseHeaders,
129-
emitter,
130-
diagnosticsManager,
131-
);
82+
const dataManagerFactory = (
83+
flagManager: FlagManager,
84+
configuration: Configuration,
85+
baseHeaders: LDHeaders,
86+
emitter: LDEmitter,
87+
diagnosticsManager?: internal.DiagnosticsManager,
88+
) =>
89+
configuration.dataSystem
90+
? new BrowserFDv2DataManager(
91+
platform,
92+
flagManager,
93+
clientSideId,
94+
configuration,
95+
baseHeaders,
96+
emitter,
97+
)
98+
: new BrowserDataManager(
99+
platform,
100+
flagManager,
101+
clientSideId,
102+
configuration,
103+
validatedBrowserOptions,
104+
endpoints.polling,
105+
endpoints.streaming,
106+
baseHeaders,
107+
emitter,
108+
diagnosticsManager,
109+
);
132110

133111
super(clientSideId, autoEnvAttributes, platform, baseOptionsWithDefaults, dataManagerFactory, {
134112
// This logic is derived from https://github.com/launchdarkly/js-sdk-common/blob/main/src/PersistentFlagStore.js
@@ -139,6 +117,7 @@ class BrowserClientImpl extends LDClientImpl {
139117
includeAuthorizationHeader: false,
140118
highTimeoutThreshold: 5,
141119
userAgentHeaderName: 'x-launchdarkly-user-agent',
120+
dataSystemDefaults: BROWSER_DATA_SYSTEM_DEFAULTS,
142121
trackEventModifier: (event: internal.InputCustomEvent) =>
143122
new internal.InputCustomEvent(
144123
event.context,

packages/sdk/react-native/src/ReactNativeLDClient.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
LDEmitter,
1212
LDHeaders,
1313
LDPluginEnvironmentMetadata,
14+
MOBILE_DATA_SYSTEM_DEFAULTS,
1415
mobileFdv1Endpoints,
1516
} from '@launchdarkly/js-client-sdk-common';
1617

@@ -65,6 +66,7 @@ export default class ReactNativeLDClient extends LDClientImpl {
6566
getImplementationHooks: (_environmentMetadata: LDPluginEnvironmentMetadata) =>
6667
internal.safeGetHooks(logger, _environmentMetadata, validatedRnOptions.plugins),
6768
credentialType: 'mobileKey',
69+
dataSystemDefaults: MOBILE_DATA_SYSTEM_DEFAULTS,
6870
};
6971

7072
const platform = createPlatform(logger, options, validatedRnOptions.storage);

packages/shared/sdk-client/__tests__/configuration/Configuration.test.ts

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,3 +178,92 @@ it('does not wrap already safe loggers', () => {
178178
const config = new ConfigurationImpl({ logger });
179179
expect(config.logger).toBe(logger);
180180
});
181+
182+
describe('dataSystem validation', () => {
183+
it('does not set dataSystem when not provided', () => {
184+
const config = new ConfigurationImpl(
185+
{},
186+
{
187+
getImplementationHooks: () => [],
188+
credentialType: 'clientSideId',
189+
dataSystemDefaults: {
190+
initialConnectionMode: 'one-shot',
191+
automaticModeSwitching: false,
192+
},
193+
},
194+
);
195+
expect(config.dataSystem).toBeUndefined();
196+
});
197+
198+
it('validates dataSystem with platform defaults when provided as empty object', () => {
199+
const config = new ConfigurationImpl(
200+
// @ts-ignore dataSystem is @internal
201+
{ dataSystem: {} },
202+
{
203+
getImplementationHooks: () => [],
204+
credentialType: 'clientSideId',
205+
dataSystemDefaults: {
206+
initialConnectionMode: 'one-shot',
207+
automaticModeSwitching: false,
208+
},
209+
},
210+
);
211+
expect(config.dataSystem).toBeDefined();
212+
expect(config.dataSystem!.initialConnectionMode).toBe('one-shot');
213+
expect(config.dataSystem!.automaticModeSwitching).toBe(false);
214+
});
215+
216+
it('validates dataSystem with user overrides applied over platform defaults', () => {
217+
const config = new ConfigurationImpl(
218+
// @ts-ignore dataSystem is @internal
219+
{ dataSystem: { initialConnectionMode: 'polling' } },
220+
{
221+
getImplementationHooks: () => [],
222+
credentialType: 'mobileKey',
223+
dataSystemDefaults: {
224+
initialConnectionMode: 'streaming',
225+
backgroundConnectionMode: 'background',
226+
automaticModeSwitching: true,
227+
},
228+
},
229+
);
230+
expect(config.dataSystem).toBeDefined();
231+
expect(config.dataSystem!.initialConnectionMode).toBe('polling');
232+
expect(config.dataSystem!.backgroundConnectionMode).toBe('background');
233+
expect(config.dataSystem!.automaticModeSwitching).toBe(true);
234+
});
235+
236+
it('warns and falls back to default for invalid dataSystem sub-fields', () => {
237+
console.error = jest.fn();
238+
const config = new ConfigurationImpl(
239+
// @ts-ignore dataSystem is @internal
240+
{ dataSystem: { initialConnectionMode: 'turbo' } },
241+
{
242+
getImplementationHooks: () => [],
243+
credentialType: 'clientSideId',
244+
dataSystemDefaults: {
245+
initialConnectionMode: 'one-shot',
246+
automaticModeSwitching: false,
247+
},
248+
},
249+
);
250+
expect(config.dataSystem).toBeDefined();
251+
expect(config.dataSystem!.initialConnectionMode).toBe('one-shot');
252+
expect(console.error).toHaveBeenCalledWith(
253+
expect.stringContaining('dataSystem.initialConnectionMode'),
254+
);
255+
});
256+
257+
it('does not deep-validate dataSystem when dataSystemDefaults is not provided', () => {
258+
const config = new ConfigurationImpl(
259+
// @ts-ignore dataSystem is @internal
260+
{ dataSystem: { initialConnectionMode: 'polling' } },
261+
{
262+
getImplementationHooks: () => [],
263+
credentialType: 'clientSideId',
264+
},
265+
);
266+
// Without defaults, deep validation is skipped — raw object from basic validator
267+
expect(config.dataSystem).toBeDefined();
268+
});
269+
});

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

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,12 @@ import {
1010
} from '@launchdarkly/js-sdk-common';
1111

1212
import { Hook, type LDOptions } from '../api';
13-
import type { LDClientDataSystemOptions } from '../api/datasource/LDClientDataSystemOptions';
13+
import type {
14+
LDClientDataSystemOptions,
15+
PlatformDataSystemDefaults,
16+
} from '../api/datasource/LDClientDataSystemOptions';
1417
import { LDInspection } from '../api/LDInspection';
18+
import { dataSystemValidators } from '../datasource/LDClientDataSystemOptions';
1519
import validateOptions from './validateOptions';
1620
import validators from './validators';
1721

@@ -22,6 +26,7 @@ export interface LDClientInternalOptions extends internal.LDInternalOptions {
2226
getImplementationHooks: (environmentMetadata: LDPluginEnvironmentMetadata) => Hook[];
2327
credentialType: 'clientSideId' | 'mobileKey';
2428
getLegacyStorageKeys?: () => string[];
29+
dataSystemDefaults?: PlatformDataSystemDefaults;
2530
}
2631

2732
export interface Configuration {
@@ -182,5 +187,16 @@ export default class ConfigurationImpl implements Configuration {
182187

183188
this.credentialType = internalOptions.credentialType;
184189
this.getImplementationHooks = internalOptions.getImplementationHooks;
190+
191+
// Deep-validate dataSystem if present, using platform-specific defaults.
192+
if (pristineOptions.dataSystem !== undefined && internalOptions.dataSystemDefaults) {
193+
this.dataSystem = validateOptions(
194+
pristineOptions.dataSystem,
195+
dataSystemValidators,
196+
internalOptions.dataSystemDefaults as unknown as Record<string, unknown>,
197+
this.logger,
198+
'dataSystem',
199+
) as unknown as LDClientDataSystemOptions;
200+
}
185201
}
186202
}

0 commit comments

Comments
 (0)