From e0f35e3d998ba3219238664dd60aa942a9cdb57c Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 21 Jan 2026 21:55:57 +0000 Subject: [PATCH 1/5] refactor(sdk-client): replace ConfigurationImpl class with createConfiguration factory function SDK-1705: Refactor ConfigurationImpl to follow factory function pattern - Add createConfiguration() factory function that returns Configuration interface - Update LDClientImpl to use createConfiguration() instead of new ConfigurationImpl() - Update exports in configuration/index.ts to include createConfiguration - Keep ConfigurationImpl class for backwards compatibility - Update test files to use createConfiguration() - Update tests to check serviceEndpoints properties instead of internal class properties This change follows the CONTRIBUTING.md guidelines for preferring interfaces over classes, which results in better bundle size and minification potential. Co-Authored-By: Steven Zhang --- .../configuration/Configuration.test.ts | 38 ++-- .../__tests__/context/addAutoEnv.test.ts | 8 +- .../createDiagnosticsInitConfig.test.ts | 6 +- .../shared/sdk-client/src/LDClientImpl.ts | 4 +- .../src/configuration/Configuration.ts | 170 ++++++++++++++++++ .../sdk-client/src/configuration/index.ts | 2 + 6 files changed, 201 insertions(+), 27 deletions(-) diff --git a/packages/shared/sdk-client/__tests__/configuration/Configuration.test.ts b/packages/shared/sdk-client/__tests__/configuration/Configuration.test.ts index 3f437855a..e0ccf7502 100644 --- a/packages/shared/sdk-client/__tests__/configuration/Configuration.test.ts +++ b/packages/shared/sdk-client/__tests__/configuration/Configuration.test.ts @@ -1,7 +1,7 @@ /* eslint-disable no-console */ import { createSafeLogger } from '@launchdarkly/js-sdk-common'; -import ConfigurationImpl from '../../src/configuration/Configuration'; +import { createConfiguration } from '../../src/configuration/Configuration'; describe('Configuration', () => { beforeEach(() => { @@ -10,17 +10,15 @@ describe('Configuration', () => { }); it('has valid default values', () => { - const config = new ConfigurationImpl(); + const config = createConfiguration(); expect(config).toMatchObject({ allAttributesPrivate: false, - baseUri: 'https://clientsdk.launchdarkly.com', capacity: 100, debug: false, diagnosticOptOut: false, diagnosticRecordingInterval: 900, withReasons: false, - eventsUri: 'https://events.launchdarkly.com', flushInterval: 30, logger: expect.anything(), maxCachedContexts: 5, @@ -28,28 +26,32 @@ describe('Configuration', () => { sendEvents: true, sendLDHeaders: true, streamInitialReconnectDelay: 1, - streamUri: 'https://clientstream.launchdarkly.com', useReport: false, }); + // Verify service endpoints have correct default values + expect(config.serviceEndpoints.polling).toBe('https://clientsdk.launchdarkly.com'); + expect(config.serviceEndpoints.streaming).toBe('https://clientstream.launchdarkly.com'); + expect(config.serviceEndpoints.events).toBe('https://events.launchdarkly.com'); expect(console.error).not.toHaveBeenCalled(); }); it('allows specifying valid wrapperName', () => { - const config = new ConfigurationImpl({ wrapperName: 'test' }); + const config = createConfiguration({ wrapperName: 'test' }); expect(config).toMatchObject({ wrapperName: 'test' }); }); it('warns and ignored invalid keys', () => { // @ts-ignore - const config = new ConfigurationImpl({ baseballUri: 1 }); + const config = createConfiguration({ baseballUri: 1 }); + // @ts-ignore expect(config.baseballUri).toBeUndefined(); expect(console.error).toHaveBeenCalledWith(expect.stringContaining('unknown config option')); }); it('converts boolean types', () => { // @ts-ignore - const config = new ConfigurationImpl({ sendEvents: 0 }); + const config = createConfiguration({ sendEvents: 0 }); expect(config.sendEvents).toBeFalsy(); expect(console.error).toHaveBeenCalledWith( @@ -59,7 +61,7 @@ describe('Configuration', () => { it('ignores wrong type for number and logs appropriately', () => { // @ts-ignore - const config = new ConfigurationImpl({ capacity: true }); + const config = createConfiguration({ capacity: true }); expect(config.capacity).toEqual(100); expect(console.error).toHaveBeenCalledWith( @@ -68,7 +70,7 @@ describe('Configuration', () => { }); it('enforces minimum flushInterval', () => { - const config = new ConfigurationImpl({ flushInterval: 1 }); + const config = createConfiguration({ flushInterval: 1 }); expect(config.flushInterval).toEqual(2); expect(console.error).toHaveBeenNthCalledWith( @@ -78,14 +80,14 @@ describe('Configuration', () => { }); it('allows setting a valid maxCachedContexts', () => { - const config = new ConfigurationImpl({ maxCachedContexts: 3 }); + const config = createConfiguration({ maxCachedContexts: 3 }); expect(config.maxCachedContexts).toBeDefined(); expect(console.error).not.toHaveBeenCalled(); }); it('enforces minimum maxCachedContext', () => { - const config = new ConfigurationImpl({ maxCachedContexts: -1 }); + const config = createConfiguration({ maxCachedContexts: -1 }); expect(config.maxCachedContexts).toBeDefined(); expect(console.error).toHaveBeenNthCalledWith( @@ -101,16 +103,16 @@ describe('Configuration', () => { ['kebab-case-works'], ['snake_case_works'], ])('allow setting valid payload filter keys', (filter) => { - const config = new ConfigurationImpl({ payloadFilterKey: filter }); - expect(config.payloadFilterKey).toEqual(filter); + const config = createConfiguration({ payloadFilterKey: filter }); + expect(config.serviceEndpoints.payloadFilterKey).toEqual(filter); expect(console.error).toHaveBeenCalledTimes(0); }); it.each([['invalid-@-filter'], ['_invalid-filter'], ['-invalid-filter']])( 'ignores invalid filters and logs a warning', (filter) => { - const config = new ConfigurationImpl({ payloadFilterKey: filter }); - expect(config.payloadFilterKey).toBeUndefined(); + const config = createConfiguration({ payloadFilterKey: filter }); + expect(config.serviceEndpoints.payloadFilterKey).toBeUndefined(); expect(console.error).toHaveBeenNthCalledWith( 1, expect.stringMatching(/should be of type string matching/i), @@ -134,7 +136,7 @@ it('makes a safe logger', () => { throw new Error('bad'); }, }; - const config = new ConfigurationImpl({ + const config = createConfiguration({ logger: badLogger, }); @@ -160,6 +162,6 @@ it('does not wrap already safe loggers', () => { throw new Error('bad'); }, }); - const config = new ConfigurationImpl({ logger }); + const config = createConfiguration({ logger }); expect(config.logger).toBe(logger); }); diff --git a/packages/shared/sdk-client/__tests__/context/addAutoEnv.test.ts b/packages/shared/sdk-client/__tests__/context/addAutoEnv.test.ts index cc0759d86..3d65d1fee 100644 --- a/packages/shared/sdk-client/__tests__/context/addAutoEnv.test.ts +++ b/packages/shared/sdk-client/__tests__/context/addAutoEnv.test.ts @@ -7,7 +7,7 @@ import { LDUser, } from '@launchdarkly/js-sdk-common'; -import { Configuration, ConfigurationImpl } from '../../src/configuration'; +import { Configuration, createConfiguration } from '../../src/configuration'; import { addApplicationInfo, addAutoEnv, @@ -37,7 +37,7 @@ describe('automatic environment attributes', () => { beforeEach(() => { ({ crypto, info } = mockPlatform); (crypto.randomUUID as jest.Mock).mockResolvedValue('test-device-key-1'); - config = new ConfigurationImpl({ logger }); + config = createConfiguration({ logger }); }); afterEach(() => { @@ -342,7 +342,7 @@ describe('automatic environment attributes', () => { describe('addApplicationInfo', () => { test('add id, version, name, versionName', async () => { - config = new ConfigurationImpl({ + config = createConfiguration({ applicationInfo: { id: 'com.from-config.ld', version: '2.2.2', @@ -435,7 +435,7 @@ describe('automatic environment attributes', () => { info.platformData = jest .fn() .mockReturnValueOnce({ ld_application: { version: null, locale: '' } }); - config = new ConfigurationImpl({ applicationInfo: { version: '1.2.3' } }); + config = createConfiguration({ applicationInfo: { version: '1.2.3' } }); const ldApplication = await addApplicationInfo(mockPlatform, config); expect(ldApplication).toBeUndefined(); diff --git a/packages/shared/sdk-client/__tests__/diagnostics/createDiagnosticsInitConfig.test.ts b/packages/shared/sdk-client/__tests__/diagnostics/createDiagnosticsInitConfig.test.ts index 004dd0f7b..35f6bd218 100644 --- a/packages/shared/sdk-client/__tests__/diagnostics/createDiagnosticsInitConfig.test.ts +++ b/packages/shared/sdk-client/__tests__/diagnostics/createDiagnosticsInitConfig.test.ts @@ -1,6 +1,6 @@ import { secondsToMillis } from '@launchdarkly/js-sdk-common'; -import { ConfigurationImpl } from '../../src/configuration'; +import { createConfiguration } from '../../src/configuration'; import createDiagnosticsInitConfig, { type DiagnosticsInitConfig, } from '../../src/diagnostics/createDiagnosticsInitConfig'; @@ -9,7 +9,7 @@ describe('createDiagnosticsInitConfig', () => { let initConfig: DiagnosticsInitConfig; beforeEach(() => { - initConfig = createDiagnosticsInitConfig(new ConfigurationImpl()); + initConfig = createDiagnosticsInitConfig(createConfiguration()); }); test('defaults', () => { @@ -29,7 +29,7 @@ describe('createDiagnosticsInitConfig', () => { test('non-default config', () => { const custom = createDiagnosticsInitConfig( - new ConfigurationImpl({ + createConfiguration({ baseUri: 'https://dev.ld.com', streamUri: 'https://stream.ld.com', eventsUri: 'https://events.ld.com', diff --git a/packages/shared/sdk-client/src/LDClientImpl.ts b/packages/shared/sdk-client/src/LDClientImpl.ts index 9bec7272b..3cfb944dd 100644 --- a/packages/shared/sdk-client/src/LDClientImpl.ts +++ b/packages/shared/sdk-client/src/LDClientImpl.ts @@ -37,7 +37,7 @@ import { import { LDEvaluationDetail, LDEvaluationDetailTyped } from './api/LDEvaluationDetail'; import { LDIdentifyOptions } from './api/LDIdentifyOptions'; import { createAsyncTaskQueue } from './async/AsyncTaskQueue'; -import { Configuration, ConfigurationImpl, LDClientInternalOptions } from './configuration'; +import { Configuration, createConfiguration, LDClientInternalOptions } from './configuration'; import { addAutoEnv } from './context/addAutoEnv'; import { ActiveContextTracker, @@ -114,7 +114,7 @@ export default class LDClientImpl implements LDClient, LDClientIdentifyResult { throw new Error('Platform must implement Encoding because btoa is required.'); } - this._config = new ConfigurationImpl(options, internalOptions); + this._config = createConfiguration(options, internalOptions); this.logger = this._config.logger; this._baseHeaders = defaultHeaders( diff --git a/packages/shared/sdk-client/src/configuration/Configuration.ts b/packages/shared/sdk-client/src/configuration/Configuration.ts index ead620ac3..544dc7b74 100644 --- a/packages/shared/sdk-client/src/configuration/Configuration.ts +++ b/packages/shared/sdk-client/src/configuration/Configuration.ts @@ -76,6 +76,176 @@ function ensureSafeLogger(logger?: LDLogger): LDLogger { return createSafeLogger(logger); } +interface ConfigurationValues { + logger: LDLogger; + baseUri: string; + eventsUri: string; + streamUri: string; + maxCachedContexts: number; + capacity: number; + diagnosticRecordingInterval: number; + flushInterval: number; + streamInitialReconnectDelay: number; + allAttributesPrivate: boolean; + debug: boolean; + diagnosticOptOut: boolean; + sendEvents: boolean; + sendLDHeaders: boolean; + useReport: boolean; + withReasons: boolean; + privateAttributes: string[]; + applicationInfo?: { + id?: string; + version?: string; + name?: string; + versionName?: string; + }; + bootstrap?: LDFlagSet; + requestHeaderTransform?: (headers: Map) => Map; + stream?: boolean; + hash?: string; + wrapperName?: string; + wrapperVersion?: string; + pollInterval: number; + hooks: Hook[]; + inspectors: LDInspection[]; + payloadFilterKey?: string; + [index: string]: any; +} + +function validateTypesAndNames( + pristineOptions: LDOptions, + values: ConfigurationValues, + _logger: LDLogger, +): string[] { + const errors: string[] = []; + + Object.entries(pristineOptions).forEach(([k, v]) => { + const validator = validators[k as keyof LDOptions]; + + if (validator) { + if (!validator.is(v)) { + const validatorType = validator.getType(); + + if (validatorType === 'boolean') { + errors.push(OptionMessages.wrongOptionTypeBoolean(k, typeof v)); + // eslint-disable-next-line no-param-reassign + values[k] = !!v; + } else if (validatorType === 'boolean | undefined | null') { + errors.push(OptionMessages.wrongOptionTypeBoolean(k, typeof v)); + + if (typeof v !== 'boolean' && typeof v !== 'undefined' && v !== null) { + // eslint-disable-next-line no-param-reassign + values[k] = !!v; + } + } else if (validator instanceof NumberWithMinimum && TypeValidators.Number.is(v)) { + const { min } = validator as NumberWithMinimum; + errors.push(OptionMessages.optionBelowMinimum(k, v, min)); + // eslint-disable-next-line no-param-reassign + values[k] = min; + } else { + errors.push(OptionMessages.wrongOptionType(k, validator.getType(), typeof v)); + } + } else if (k === 'logger') { + // Logger already assigned. + } else { + // if an option is explicitly null, coerce to undefined + // eslint-disable-next-line no-param-reassign + values[k] = v ?? undefined; + } + } else { + errors.push(OptionMessages.unknownOption(k)); + } + }); + + return errors; +} + +export function createConfiguration( + pristineOptions: LDOptions = {}, + internalOptions: LDClientInternalOptions = { + getImplementationHooks: () => [], + credentialType: 'mobileKey', + }, +): Configuration { + const logger = ensureSafeLogger(pristineOptions.logger); + + const values: ConfigurationValues = { + logger, + baseUri: DEFAULT_POLLING, + eventsUri: ServiceEndpoints.DEFAULT_EVENTS, + streamUri: DEFAULT_STREAM, + maxCachedContexts: 5, + capacity: 100, + diagnosticRecordingInterval: 900, + flushInterval: 30, + streamInitialReconnectDelay: 1, + allAttributesPrivate: false, + debug: false, + diagnosticOptOut: false, + sendEvents: true, + sendLDHeaders: true, + useReport: false, + withReasons: false, + privateAttributes: [], + pollInterval: DEFAULT_POLLING_INTERVAL, + hooks: [], + inspectors: [], + }; + + const errors = validateTypesAndNames(pristineOptions, values, logger); + errors.forEach((e: string) => logger.warn(e)); + + const serviceEndpoints = new ServiceEndpoints( + values.streamUri, + values.baseUri, + values.eventsUri, + internalOptions.analyticsEventPath, + internalOptions.diagnosticEventPath, + internalOptions.includeAuthorizationHeader, + values.payloadFilterKey, + ); + + const useReport = pristineOptions.useReport ?? false; + const tags = new ApplicationTags({ application: values.applicationInfo, logger }); + const userAgentHeaderName = internalOptions.userAgentHeaderName ?? 'user-agent'; + const trackEventModifier = internalOptions.trackEventModifier ?? ((event) => event); + const { credentialType, getImplementationHooks } = internalOptions; + + return { + logger, + maxCachedContexts: values.maxCachedContexts, + capacity: values.capacity, + diagnosticRecordingInterval: values.diagnosticRecordingInterval, + flushInterval: values.flushInterval, + streamInitialReconnectDelay: values.streamInitialReconnectDelay, + allAttributesPrivate: values.allAttributesPrivate, + debug: values.debug, + diagnosticOptOut: values.diagnosticOptOut, + sendEvents: values.sendEvents, + sendLDHeaders: values.sendLDHeaders, + useReport, + withReasons: values.withReasons, + privateAttributes: values.privateAttributes, + tags, + applicationInfo: values.applicationInfo, + bootstrap: values.bootstrap, + requestHeaderTransform: values.requestHeaderTransform, + stream: values.stream, + hash: values.hash, + wrapperName: values.wrapperName, + wrapperVersion: values.wrapperVersion, + serviceEndpoints, + pollInterval: values.pollInterval, + userAgentHeaderName, + trackEventModifier, + hooks: values.hooks, + inspectors: values.inspectors, + credentialType, + getImplementationHooks, + }; +} + export default class ConfigurationImpl implements Configuration { public readonly logger: LDLogger = createSafeLogger(); diff --git a/packages/shared/sdk-client/src/configuration/index.ts b/packages/shared/sdk-client/src/configuration/index.ts index cdb5c1344..9477fda88 100644 --- a/packages/shared/sdk-client/src/configuration/index.ts +++ b/packages/shared/sdk-client/src/configuration/index.ts @@ -1,5 +1,6 @@ import ConfigurationImpl, { Configuration, + createConfiguration, DEFAULT_POLLING, DEFAULT_STREAM, LDClientInternalOptions, @@ -8,6 +9,7 @@ import ConfigurationImpl, { export { Configuration, ConfigurationImpl, + createConfiguration, LDClientInternalOptions, DEFAULT_POLLING, DEFAULT_STREAM, From fa7731a67083279ed0c2378e871e81ae9212e5e9 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 21 Jan 2026 22:05:51 +0000 Subject: [PATCH 2/5] refactor(sdk-client): remove ConfigurationImpl class entirely SDK-1705: Complete removal of ConfigurationImpl class - Remove ConfigurationImpl class from Configuration.ts - Update exports in index.ts to remove ConfigurationImpl - All usages now use createConfiguration() factory function This completes the refactoring to follow the CONTRIBUTING.md guidelines for preferring interfaces over classes. Co-Authored-By: Steven Zhang --- .../src/configuration/Configuration.ts | 139 ------------------ .../sdk-client/src/configuration/index.ts | 3 +- 2 files changed, 1 insertion(+), 141 deletions(-) diff --git a/packages/shared/sdk-client/src/configuration/Configuration.ts b/packages/shared/sdk-client/src/configuration/Configuration.ts index 544dc7b74..33eba37a2 100644 --- a/packages/shared/sdk-client/src/configuration/Configuration.ts +++ b/packages/shared/sdk-client/src/configuration/Configuration.ts @@ -245,142 +245,3 @@ export function createConfiguration( getImplementationHooks, }; } - -export default class ConfigurationImpl implements Configuration { - public readonly logger: LDLogger = createSafeLogger(); - - // Naming conventions is not followed for these lines because the config validation - // accesses members based on the keys of the options. (sdk-763) - // eslint-disable-next-line @typescript-eslint/naming-convention - private readonly baseUri = DEFAULT_POLLING; - // eslint-disable-next-line @typescript-eslint/naming-convention - private readonly eventsUri = ServiceEndpoints.DEFAULT_EVENTS; - // eslint-disable-next-line @typescript-eslint/naming-convention - private readonly streamUri = DEFAULT_STREAM; - - public readonly maxCachedContexts = 5; - - public readonly capacity = 100; - public readonly diagnosticRecordingInterval = 900; - public readonly flushInterval = 30; - public readonly streamInitialReconnectDelay = 1; - - public readonly allAttributesPrivate: boolean = false; - public readonly debug: boolean = false; - public readonly diagnosticOptOut: boolean = false; - public readonly sendEvents: boolean = true; - public readonly sendLDHeaders: boolean = true; - - public readonly useReport: boolean = false; - public readonly withReasons: boolean = false; - - public readonly privateAttributes: string[] = []; - - public readonly tags: ApplicationTags; - public readonly applicationInfo?: { - id?: string; - version?: string; - name?: string; - versionName?: string; - }; - public readonly bootstrap?: LDFlagSet; - - // TODO: implement requestHeaderTransform - public readonly requestHeaderTransform?: (headers: Map) => Map; - public readonly stream?: boolean; - public readonly hash?: string; - public readonly wrapperName?: string; - public readonly wrapperVersion?: string; - - public readonly serviceEndpoints: ServiceEndpoints; - - public readonly pollInterval: number = DEFAULT_POLLING_INTERVAL; - - public readonly userAgentHeaderName: 'user-agent' | 'x-launchdarkly-user-agent'; - - public readonly hooks: Hook[] = []; - - public readonly inspectors: LDInspection[] = []; - - public readonly trackEventModifier: ( - event: internal.InputCustomEvent, - ) => internal.InputCustomEvent; - - public readonly credentialType: 'clientSideId' | 'mobileKey'; - public readonly getImplementationHooks: ( - environmentMetadata: LDPluginEnvironmentMetadata, - ) => Hook[]; - - // Allow indexing Configuration by a string - [index: string]: any; - - constructor( - pristineOptions: LDOptions = {}, - internalOptions: LDClientInternalOptions = { - getImplementationHooks: () => [], - credentialType: 'mobileKey', - }, - ) { - this.logger = ensureSafeLogger(pristineOptions.logger); - const errors = this._validateTypesAndNames(pristineOptions); - errors.forEach((e: string) => this.logger.warn(e)); - - this.serviceEndpoints = new ServiceEndpoints( - this.streamUri, - this.baseUri, - this.eventsUri, - internalOptions.analyticsEventPath, - internalOptions.diagnosticEventPath, - internalOptions.includeAuthorizationHeader, - pristineOptions.payloadFilterKey, - ); - this.useReport = pristineOptions.useReport ?? false; - - this.tags = new ApplicationTags({ application: this.applicationInfo, logger: this.logger }); - this.userAgentHeaderName = internalOptions.userAgentHeaderName ?? 'user-agent'; - this.trackEventModifier = internalOptions.trackEventModifier ?? ((event) => event); - - this.credentialType = internalOptions.credentialType; - this.getImplementationHooks = internalOptions.getImplementationHooks; - } - - private _validateTypesAndNames(pristineOptions: LDOptions): string[] { - const errors: string[] = []; - - Object.entries(pristineOptions).forEach(([k, v]) => { - const validator = validators[k as keyof LDOptions]; - - if (validator) { - if (!validator.is(v)) { - const validatorType = validator.getType(); - - if (validatorType === 'boolean') { - errors.push(OptionMessages.wrongOptionTypeBoolean(k, typeof v)); - this[k] = !!v; - } else if (validatorType === 'boolean | undefined | null') { - errors.push(OptionMessages.wrongOptionTypeBoolean(k, typeof v)); - - if (typeof v !== 'boolean' && typeof v !== 'undefined' && v !== null) { - this[k] = !!v; - } - } else if (validator instanceof NumberWithMinimum && TypeValidators.Number.is(v)) { - const { min } = validator as NumberWithMinimum; - errors.push(OptionMessages.optionBelowMinimum(k, v, min)); - this[k] = min; - } else { - errors.push(OptionMessages.wrongOptionType(k, validator.getType(), typeof v)); - } - } else if (k === 'logger') { - // Logger already assigned. - } else { - // if an option is explicitly null, coerce to undefined - this[k] = v ?? undefined; - } - } else { - errors.push(OptionMessages.unknownOption(k)); - } - }); - - return errors; - } -} diff --git a/packages/shared/sdk-client/src/configuration/index.ts b/packages/shared/sdk-client/src/configuration/index.ts index 9477fda88..58d7716ad 100644 --- a/packages/shared/sdk-client/src/configuration/index.ts +++ b/packages/shared/sdk-client/src/configuration/index.ts @@ -1,4 +1,4 @@ -import ConfigurationImpl, { +import { Configuration, createConfiguration, DEFAULT_POLLING, @@ -8,7 +8,6 @@ import ConfigurationImpl, { export { Configuration, - ConfigurationImpl, createConfiguration, LDClientInternalOptions, DEFAULT_POLLING, From efe9fac303c9215e4fc1f17736a8d0468d37002c Mon Sep 17 00:00:00 2001 From: Steven Zhang Date: Fri, 23 Jan 2026 11:37:54 -0600 Subject: [PATCH 3/5] fix: actually reduce module size --- .../src/configuration/Configuration.ts | 114 ++++++------------ 1 file changed, 39 insertions(+), 75 deletions(-) diff --git a/packages/shared/sdk-client/src/configuration/Configuration.ts b/packages/shared/sdk-client/src/configuration/Configuration.ts index 33eba37a2..3576efddb 100644 --- a/packages/shared/sdk-client/src/configuration/Configuration.ts +++ b/packages/shared/sdk-client/src/configuration/Configuration.ts @@ -68,14 +68,6 @@ const DEFAULT_STREAM: string = 'https://clientstream.launchdarkly.com'; export { DEFAULT_POLLING, DEFAULT_STREAM }; -function ensureSafeLogger(logger?: LDLogger): LDLogger { - if (logger instanceof SafeLogger) { - return logger; - } - // Even if logger is not defined this will produce a valid logger. - return createSafeLogger(logger); -} - interface ConfigurationValues { logger: LDLogger; baseUri: string; @@ -113,11 +105,43 @@ interface ConfigurationValues { [index: string]: any; } -function validateTypesAndNames( - pristineOptions: LDOptions, - values: ConfigurationValues, - _logger: LDLogger, -): string[] { +export function createConfiguration( + pristineOptions: LDOptions = {}, + internalOptions: LDClientInternalOptions = { + getImplementationHooks: () => [], + credentialType: 'mobileKey', + }, +): Configuration { + // Ensures that the logger is a SafeLogger. + // This should account for the case where the logger is not defined. + const logger = + pristineOptions.logger instanceof SafeLogger + ? pristineOptions.logger + : createSafeLogger(pristineOptions.logger); + + const values: ConfigurationValues = { + logger, + baseUri: DEFAULT_POLLING, + eventsUri: ServiceEndpoints.DEFAULT_EVENTS, + streamUri: DEFAULT_STREAM, + maxCachedContexts: 5, + capacity: 100, + diagnosticRecordingInterval: 900, + flushInterval: 30, + streamInitialReconnectDelay: 1, + allAttributesPrivate: false, + debug: false, + diagnosticOptOut: false, + sendEvents: true, + sendLDHeaders: true, + useReport: false, + withReasons: false, + privateAttributes: [], + pollInterval: DEFAULT_POLLING_INTERVAL, + hooks: [], + inspectors: [], + }; + const errors: string[] = []; Object.entries(pristineOptions).forEach(([k, v]) => { @@ -158,42 +182,6 @@ function validateTypesAndNames( } }); - return errors; -} - -export function createConfiguration( - pristineOptions: LDOptions = {}, - internalOptions: LDClientInternalOptions = { - getImplementationHooks: () => [], - credentialType: 'mobileKey', - }, -): Configuration { - const logger = ensureSafeLogger(pristineOptions.logger); - - const values: ConfigurationValues = { - logger, - baseUri: DEFAULT_POLLING, - eventsUri: ServiceEndpoints.DEFAULT_EVENTS, - streamUri: DEFAULT_STREAM, - maxCachedContexts: 5, - capacity: 100, - diagnosticRecordingInterval: 900, - flushInterval: 30, - streamInitialReconnectDelay: 1, - allAttributesPrivate: false, - debug: false, - diagnosticOptOut: false, - sendEvents: true, - sendLDHeaders: true, - useReport: false, - withReasons: false, - privateAttributes: [], - pollInterval: DEFAULT_POLLING_INTERVAL, - hooks: [], - inspectors: [], - }; - - const errors = validateTypesAndNames(pristineOptions, values, logger); errors.forEach((e: string) => logger.warn(e)); const serviceEndpoints = new ServiceEndpoints( @@ -206,41 +194,17 @@ export function createConfiguration( values.payloadFilterKey, ); - const useReport = pristineOptions.useReport ?? false; const tags = new ApplicationTags({ application: values.applicationInfo, logger }); const userAgentHeaderName = internalOptions.userAgentHeaderName ?? 'user-agent'; const trackEventModifier = internalOptions.trackEventModifier ?? ((event) => event); const { credentialType, getImplementationHooks } = internalOptions; return { - logger, - maxCachedContexts: values.maxCachedContexts, - capacity: values.capacity, - diagnosticRecordingInterval: values.diagnosticRecordingInterval, - flushInterval: values.flushInterval, - streamInitialReconnectDelay: values.streamInitialReconnectDelay, - allAttributesPrivate: values.allAttributesPrivate, - debug: values.debug, - diagnosticOptOut: values.diagnosticOptOut, - sendEvents: values.sendEvents, - sendLDHeaders: values.sendLDHeaders, - useReport, - withReasons: values.withReasons, - privateAttributes: values.privateAttributes, - tags, - applicationInfo: values.applicationInfo, - bootstrap: values.bootstrap, - requestHeaderTransform: values.requestHeaderTransform, - stream: values.stream, - hash: values.hash, - wrapperName: values.wrapperName, - wrapperVersion: values.wrapperVersion, + ...values, serviceEndpoints, - pollInterval: values.pollInterval, + tags, userAgentHeaderName, trackEventModifier, - hooks: values.hooks, - inspectors: values.inspectors, credentialType, getImplementationHooks, }; From 8baac7f4ee91b6a9869a98d26e5b2abe33567741 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Mon, 26 Jan 2026 22:11:05 +0000 Subject: [PATCH 4/5] refactor(sdk-client): optimize createConfiguration to reduce bundle size Use object spread instead of explicit property mapping in the return statement to reduce the generated JavaScript code size. Before optimization: +716 bytes raw, +86 bytes brotli After optimization: -319 bytes raw, -35 bytes brotli (vs main) Co-Authored-By: Steven Zhang --- .../src/configuration/Configuration.ts | 62 +++++-------------- 1 file changed, 17 insertions(+), 45 deletions(-) diff --git a/packages/shared/sdk-client/src/configuration/Configuration.ts b/packages/shared/sdk-client/src/configuration/Configuration.ts index 33eba37a2..be0173bfa 100644 --- a/packages/shared/sdk-client/src/configuration/Configuration.ts +++ b/packages/shared/sdk-client/src/configuration/Configuration.ts @@ -196,52 +196,24 @@ export function createConfiguration( const errors = validateTypesAndNames(pristineOptions, values, logger); errors.forEach((e: string) => logger.warn(e)); - const serviceEndpoints = new ServiceEndpoints( - values.streamUri, - values.baseUri, - values.eventsUri, - internalOptions.analyticsEventPath, - internalOptions.diagnosticEventPath, - internalOptions.includeAuthorizationHeader, - values.payloadFilterKey, - ); - - const useReport = pristineOptions.useReport ?? false; - const tags = new ApplicationTags({ application: values.applicationInfo, logger }); - const userAgentHeaderName = internalOptions.userAgentHeaderName ?? 'user-agent'; - const trackEventModifier = internalOptions.trackEventModifier ?? ((event) => event); - const { credentialType, getImplementationHooks } = internalOptions; + // Remove internal URI properties that shouldn't be on the final Configuration + const { baseUri, eventsUri, streamUri, payloadFilterKey, ...configValues } = values; return { - logger, - maxCachedContexts: values.maxCachedContexts, - capacity: values.capacity, - diagnosticRecordingInterval: values.diagnosticRecordingInterval, - flushInterval: values.flushInterval, - streamInitialReconnectDelay: values.streamInitialReconnectDelay, - allAttributesPrivate: values.allAttributesPrivate, - debug: values.debug, - diagnosticOptOut: values.diagnosticOptOut, - sendEvents: values.sendEvents, - sendLDHeaders: values.sendLDHeaders, - useReport, - withReasons: values.withReasons, - privateAttributes: values.privateAttributes, - tags, - applicationInfo: values.applicationInfo, - bootstrap: values.bootstrap, - requestHeaderTransform: values.requestHeaderTransform, - stream: values.stream, - hash: values.hash, - wrapperName: values.wrapperName, - wrapperVersion: values.wrapperVersion, - serviceEndpoints, - pollInterval: values.pollInterval, - userAgentHeaderName, - trackEventModifier, - hooks: values.hooks, - inspectors: values.inspectors, - credentialType, - getImplementationHooks, + ...configValues, + tags: new ApplicationTags({ application: values.applicationInfo, logger }), + serviceEndpoints: new ServiceEndpoints( + streamUri, + baseUri, + eventsUri, + internalOptions.analyticsEventPath, + internalOptions.diagnosticEventPath, + internalOptions.includeAuthorizationHeader, + payloadFilterKey, + ), + userAgentHeaderName: internalOptions.userAgentHeaderName ?? 'user-agent', + trackEventModifier: internalOptions.trackEventModifier ?? ((event) => event), + credentialType: internalOptions.credentialType, + getImplementationHooks: internalOptions.getImplementationHooks, }; } From 1988be651fa504c814492513ab8c5c53ebfb4377 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 27 Jan 2026 17:00:16 +0000 Subject: [PATCH 5/5] refactor(sdk-client): inline validation logic into createConfiguration Removes the separate validateTypesAndNames function and inlines the validation logic directly into createConfiguration since it's only used in one place. This further reduces the bundle size. Additional savings: ~443 bytes raw, ~54 bytes brotli Co-Authored-By: Steven Zhang --- .../src/configuration/Configuration.ts | 84 ++++++++----------- 1 file changed, 34 insertions(+), 50 deletions(-) diff --git a/packages/shared/sdk-client/src/configuration/Configuration.ts b/packages/shared/sdk-client/src/configuration/Configuration.ts index be0173bfa..a137b91c2 100644 --- a/packages/shared/sdk-client/src/configuration/Configuration.ts +++ b/packages/shared/sdk-client/src/configuration/Configuration.ts @@ -113,54 +113,6 @@ interface ConfigurationValues { [index: string]: any; } -function validateTypesAndNames( - pristineOptions: LDOptions, - values: ConfigurationValues, - _logger: LDLogger, -): string[] { - const errors: string[] = []; - - Object.entries(pristineOptions).forEach(([k, v]) => { - const validator = validators[k as keyof LDOptions]; - - if (validator) { - if (!validator.is(v)) { - const validatorType = validator.getType(); - - if (validatorType === 'boolean') { - errors.push(OptionMessages.wrongOptionTypeBoolean(k, typeof v)); - // eslint-disable-next-line no-param-reassign - values[k] = !!v; - } else if (validatorType === 'boolean | undefined | null') { - errors.push(OptionMessages.wrongOptionTypeBoolean(k, typeof v)); - - if (typeof v !== 'boolean' && typeof v !== 'undefined' && v !== null) { - // eslint-disable-next-line no-param-reassign - values[k] = !!v; - } - } else if (validator instanceof NumberWithMinimum && TypeValidators.Number.is(v)) { - const { min } = validator as NumberWithMinimum; - errors.push(OptionMessages.optionBelowMinimum(k, v, min)); - // eslint-disable-next-line no-param-reassign - values[k] = min; - } else { - errors.push(OptionMessages.wrongOptionType(k, validator.getType(), typeof v)); - } - } else if (k === 'logger') { - // Logger already assigned. - } else { - // if an option is explicitly null, coerce to undefined - // eslint-disable-next-line no-param-reassign - values[k] = v ?? undefined; - } - } else { - errors.push(OptionMessages.unknownOption(k)); - } - }); - - return errors; -} - export function createConfiguration( pristineOptions: LDOptions = {}, internalOptions: LDClientInternalOptions = { @@ -193,8 +145,40 @@ export function createConfiguration( inspectors: [], }; - const errors = validateTypesAndNames(pristineOptions, values, logger); - errors.forEach((e: string) => logger.warn(e)); + // Validate options and update values + Object.entries(pristineOptions).forEach(([k, v]) => { + const validator = validators[k as keyof LDOptions]; + + if (validator) { + if (!validator.is(v)) { + const validatorType = validator.getType(); + + if (validatorType === 'boolean') { + logger.warn(OptionMessages.wrongOptionTypeBoolean(k, typeof v)); + values[k] = !!v; + } else if (validatorType === 'boolean | undefined | null') { + logger.warn(OptionMessages.wrongOptionTypeBoolean(k, typeof v)); + + if (typeof v !== 'boolean' && typeof v !== 'undefined' && v !== null) { + values[k] = !!v; + } + } else if (validator instanceof NumberWithMinimum && TypeValidators.Number.is(v)) { + const { min } = validator as NumberWithMinimum; + logger.warn(OptionMessages.optionBelowMinimum(k, v, min)); + values[k] = min; + } else { + logger.warn(OptionMessages.wrongOptionType(k, validator.getType(), typeof v)); + } + } else if (k === 'logger') { + // Logger already assigned. + } else { + // if an option is explicitly null, coerce to undefined + values[k] = v ?? undefined; + } + } else { + logger.warn(OptionMessages.unknownOption(k)); + } + }); // Remove internal URI properties that shouldn't be on the final Configuration const { baseUri, eventsUri, streamUri, payloadFilterKey, ...configValues } = values;