From 905b5930f3667c6cf37a3cbc7416c3edd8b3cf3c Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Fri, 6 Jun 2025 15:22:12 -0700 Subject: [PATCH 1/4] feat: Add plugin support for node. --- .../__tests__/LDClientNode.plugin.test.ts | 259 ++++++++++++++++++ packages/sdk/server-node/src/LDClientNode.ts | 65 ++++- packages/sdk/server-node/src/api/LDOptions.ts | 19 ++ packages/sdk/server-node/src/api/LDPlugin.ts | 12 + packages/sdk/server-node/src/index.ts | 8 +- .../shared/sdk-server/src/LDClientImpl.ts | 22 +- .../src/createPluginEnvironmentMetadata.ts | 35 +++ .../sdk-server/src/options/Configuration.ts | 18 +- .../src/options/ServerInternalOptions.ts | 7 + 9 files changed, 426 insertions(+), 19 deletions(-) create mode 100644 packages/sdk/server-node/__tests__/LDClientNode.plugin.test.ts create mode 100644 packages/sdk/server-node/src/api/LDOptions.ts create mode 100644 packages/sdk/server-node/src/api/LDPlugin.ts create mode 100644 packages/shared/sdk-server/src/createPluginEnvironmentMetadata.ts create mode 100644 packages/shared/sdk-server/src/options/ServerInternalOptions.ts diff --git a/packages/sdk/server-node/__tests__/LDClientNode.plugin.test.ts b/packages/sdk/server-node/__tests__/LDClientNode.plugin.test.ts new file mode 100644 index 0000000000..6801fd4424 --- /dev/null +++ b/packages/sdk/server-node/__tests__/LDClientNode.plugin.test.ts @@ -0,0 +1,259 @@ +import { integrations, LDContext, LDLogger } from '@launchdarkly/js-server-sdk-common'; + +import { LDOptions } from '../src/api/LDOptions'; +import { LDPlugin } from '../src/api/LDPlugin'; +import LDClientNode from '../src/LDClientNode'; +import NodeInfo from '../src/platform/NodeInfo'; + +// Test for plugin registration +it('registers plugins and executes hooks during initialization', async () => { + const logger: LDLogger = { + debug: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + }; + + const mockHook: integrations.Hook = { + getMetadata(): integrations.HookMetadata { + return { + name: 'test-hook', + }; + }, + beforeEvaluation: jest.fn(() => ({})), + afterEvaluation: jest.fn(() => ({})), + }; + + const mockPlugin: LDPlugin = { + getMetadata: () => ({ name: 'test-plugin' }), + register: jest.fn(), + getHooks: () => [mockHook], + }; + + const client = new LDClientNode('test', { offline: true, logger, plugins: [mockPlugin] }); + + // Verify the plugin was registered + expect(mockPlugin.register).toHaveBeenCalled(); + + // Now test that hooks work by calling identify and variation + const context: LDContext = { key: 'user-key', kind: 'user' }; + + await client.variation('flag-key', context, false); + + expect(mockHook.beforeEvaluation).toHaveBeenCalledWith( + { + context, + defaultValue: false, + flagKey: 'flag-key', + method: 'LDClient.variation', + environmentId: undefined, + }, + {}, + ); + + expect(mockHook.afterEvaluation).toHaveBeenCalled(); +}); + +// Test for multiple plugins with hooks +it('registers multiple plugins and executes all hooks', async () => { + const logger: LDLogger = { + debug: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + }; + + const mockHook1: integrations.Hook = { + getMetadata(): integrations.HookMetadata { + return { + name: 'test-hook-1', + }; + }, + beforeEvaluation: jest.fn(() => ({})), + afterEvaluation: jest.fn(() => ({})), + }; + + const mockHook2: integrations.Hook = { + getMetadata(): integrations.HookMetadata { + return { + name: 'test-hook-2', + }; + }, + beforeEvaluation: jest.fn(() => ({})), + afterEvaluation: jest.fn(() => ({})), + }; + + const mockPlugin1: LDPlugin = { + getMetadata: () => ({ name: 'test-plugin-1' }), + register: jest.fn(), + getHooks: () => [mockHook1], + }; + + const mockPlugin2: LDPlugin = { + getMetadata: () => ({ name: 'test-plugin-2' }), + register: jest.fn(), + getHooks: () => [mockHook2], + }; + + const client = new LDClientNode('test', { + offline: true, + logger, + plugins: [mockPlugin1, mockPlugin2], + }); + + // Verify plugins were registered + expect(mockPlugin1.register).toHaveBeenCalled(); + expect(mockPlugin2.register).toHaveBeenCalled(); + + // Test that both hooks work + const context: LDContext = { key: 'user-key', kind: 'user' }; + await client.variation('flag-key', context, false); + + expect(mockHook1.beforeEvaluation).toHaveBeenCalled(); + expect(mockHook1.afterEvaluation).toHaveBeenCalled(); + expect(mockHook2.beforeEvaluation).toHaveBeenCalled(); + expect(mockHook2.afterEvaluation).toHaveBeenCalled(); +}); + +it('passes correct environmentMetadata to plugin getHooks and register functions', async () => { + const logger: LDLogger = { + debug: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + }; + + const mockHook: integrations.Hook = { + getMetadata(): integrations.HookMetadata { + return { + name: 'test-hook', + }; + }, + beforeEvaluation: jest.fn(() => ({})), + afterEvaluation: jest.fn(() => ({})), + }; + + const mockPlugin: LDPlugin = { + getMetadata: () => ({ name: 'test-plugin' }), + register: jest.fn(), + getHooks: jest.fn(() => [mockHook]), + }; + + const options: LDOptions = { + wrapperName: 'test-wrapper', + wrapperVersion: '2.0.0', + application: { + id: 'test-app', + name: 'TestApp', + version: '3.0.0', + versionName: '3', + }, + offline: true, + logger, + plugins: [mockPlugin], + }; + + // eslint-disable-next-line no-new + new LDClientNode('test', options); + const platformInfo = new NodeInfo(options); + const sdkData = platformInfo.sdkData(); + expect(sdkData.name).toBeDefined(); + expect(sdkData.version).toBeDefined(); + + // Verify getHooks was called with correct environmentMetadata + expect(mockPlugin.getHooks).toHaveBeenCalledWith({ + sdk: { + name: sdkData.userAgentBase, + version: sdkData.version, + wrapperName: options.wrapperName, + wrapperVersion: options.wrapperVersion, + }, + application: { + id: options.application?.id, + name: options.application?.name, + version: options.application?.version, + versionName: options.application?.versionName, + }, + sdkKey: 'test', + }); + + // Verify register was called with correct environmentMetadata + expect(mockPlugin.register).toHaveBeenCalledWith( + expect.any(Object), // client + { + sdk: { + name: sdkData.userAgentBase, + version: sdkData.version, + wrapperName: options.wrapperName, + wrapperVersion: options.wrapperVersion, + }, + application: { + id: options.application?.id, + version: options.application?.version, + name: options.application?.name, + versionName: options.application?.versionName, + }, + sdkKey: 'test', + }, + ); +}); + +it('passes correct environmentMetadata without optional fields', async () => { + const logger: LDLogger = { + debug: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + }; + + const mockHook: integrations.Hook = { + getMetadata(): integrations.HookMetadata { + return { + name: 'test-hook', + }; + }, + beforeEvaluation: jest.fn(() => ({})), + afterEvaluation: jest.fn(() => ({})), + }; + + const mockPlugin: LDPlugin = { + getMetadata: () => ({ name: 'test-plugin' }), + register: jest.fn(), + getHooks: jest.fn(() => [mockHook]), + }; + + const options: LDOptions = { + offline: true, + logger, + plugins: [mockPlugin], + }; + + // eslint-disable-next-line no-new + new LDClientNode('test', options); + + const platformInfo = new NodeInfo(options); + const sdkData = platformInfo.sdkData(); + expect(sdkData.name).toBeDefined(); + expect(sdkData.version).toBeDefined(); + + // Verify getHooks was called with correct environmentMetadata + expect(mockPlugin.getHooks).toHaveBeenCalledWith({ + sdk: { + name: sdkData.userAgentBase, + version: sdkData.version, + }, + sdkKey: 'test', + }); + + // Verify register was called with correct environmentMetadata + expect(mockPlugin.register).toHaveBeenCalledWith( + expect.any(Object), // client + { + sdk: { + name: sdkData.userAgentBase, + version: sdkData.version, + }, + sdkKey: 'test', + }, + ); +}); diff --git a/packages/sdk/server-node/src/LDClientNode.ts b/packages/sdk/server-node/src/LDClientNode.ts index d6e322d8cc..9262a83ecb 100644 --- a/packages/sdk/server-node/src/LDClientNode.ts +++ b/packages/sdk/server-node/src/LDClientNode.ts @@ -4,24 +4,53 @@ import { format } from 'util'; import { BasicLogger, + internal, LDClientImpl, - LDOptions, + LDPluginEnvironmentMetadata, + Platform, SafeLogger, + TypeValidators, } from '@launchdarkly/js-server-sdk-common'; +import { LDClientCallbacks } from '@launchdarkly/js-server-sdk-common/dist/LDClientImpl'; +import { ServerInternalOptions } from '@launchdarkly/js-server-sdk-common/dist/options/ServerInternalOptions'; -import { BigSegmentStoreStatusProvider } from './api'; +import { BigSegmentStoreStatusProvider, LDClient } from './api'; +import { LDOptions } from './api/LDOptions'; +import { LDPlugin } from './api/LDPlugin'; import BigSegmentStoreStatusProviderNode from './BigSegmentsStoreStatusProviderNode'; import { Emits } from './Emits'; import NodePlatform from './platform/NodePlatform'; -class ClientEmitter extends EventEmitter {} - /** - * @ignore + * @internal + * Extend the base client implementation with an event emitter. + * + * The LDClientNode implementation must satisfy the LDClient interface, + * which is why we extend the base client implementation with an event emitter + * and then inherit from that. This adds everything we need to the implementation + * to comply with the interface. + * + * This allows re-use of the `Emits` mixin for this and big segments. */ -class LDClientNode extends LDClientImpl { +class ClientBaseWithEmitter extends LDClientImpl { emitter: EventEmitter; + constructor( + sdkKey: string, + platform: Platform, + options: LDOptions, + callbacks: LDClientCallbacks, + internalOptions?: ServerInternalOptions, + ) { + super(sdkKey, platform, options, callbacks, internalOptions); + this.emitter = new EventEmitter(); + } +} + +/** + * @ignore + */ +class LDClientNode extends Emits(ClientBaseWithEmitter) implements LDClient { bigSegmentStoreStatusProvider: BigSegmentStoreStatusProvider; constructor(sdkKey: string, options: LDOptions) { @@ -32,9 +61,19 @@ class LDClientNode extends LDClientImpl { formatter: format, }); - const emitter = new ClientEmitter(); - const logger = options.logger ? new SafeLogger(options.logger, fallbackLogger) : fallbackLogger; + const emitter = new EventEmitter(); + + const pluginValidator = TypeValidators.createTypeArray('LDPlugin', {}); + const plugins: LDPlugin[] = []; + if (options.plugins) { + if (pluginValidator.is(options.plugins)) { + plugins.push(...options.plugins); + } else { + logger.warn('Could not validate plugins.'); + } + } + super( sdkKey, new NodePlatform({ ...options, logger }), @@ -65,13 +104,21 @@ class LDClientNode extends LDClientImpl { name === 'update' || (typeof name === 'string' && name.startsWith('update:')), ), }, + { + getImplementationHooks: (environmentMetadata: LDPluginEnvironmentMetadata) => + internal.safeGetHooks(logger, environmentMetadata, plugins), + }, ); + // TODO: It would be good if we could re-arrange this emitter situation so we don't have to + // create two emitters. It isn't harmful, but it isn't ideal. this.emitter = emitter; this.bigSegmentStoreStatusProvider = new BigSegmentStoreStatusProviderNode( this.bigSegmentStatusProviderInternal, ) as BigSegmentStoreStatusProvider; + + internal.safeRegisterPlugins(logger, this.environmentMetadata, this, plugins); } } -export default Emits(LDClientNode); +export default LDClientNode; diff --git a/packages/sdk/server-node/src/api/LDOptions.ts b/packages/sdk/server-node/src/api/LDOptions.ts new file mode 100644 index 0000000000..2a5decf164 --- /dev/null +++ b/packages/sdk/server-node/src/api/LDOptions.ts @@ -0,0 +1,19 @@ +import { LDOptions as LDOptionsCommon } from '@launchdarkly/js-server-sdk-common'; + +import { LDPlugin } from './LDPlugin'; + +/** + * LaunchDarkly initialization options. + * + * @privateRemarks + * The plugins implementation is SDK specific, so these options exist to extend the base options + * with the node specific plugin configuration. + */ +export interface LDOptions extends LDOptionsCommon { + /** + * A list of plugins to be used with the SDK. + * + * Plugin support is currently experimental and subject to change. + */ + plugins?: LDPlugin[]; +} diff --git a/packages/sdk/server-node/src/api/LDPlugin.ts b/packages/sdk/server-node/src/api/LDPlugin.ts new file mode 100644 index 0000000000..cc074943b6 --- /dev/null +++ b/packages/sdk/server-node/src/api/LDPlugin.ts @@ -0,0 +1,12 @@ +import { integrations, LDPluginBase } from '@launchdarkly/js-server-sdk-common'; + +import { LDClient } from './LDClient'; + +/** + * Interface for plugins to the LaunchDarkly SDK. + * + * @privateRemarks + * The plugin interface must be in the leaf-sdk implementations to ensure it uses the correct LDClient intrface. + * The LDClient in the shared server code doesn't match the LDClient interface of the individual SDKs. + */ +export interface LDPlugin extends LDPluginBase {} diff --git a/packages/sdk/server-node/src/index.ts b/packages/sdk/server-node/src/index.ts index d6e713d3d6..bce3718b48 100644 --- a/packages/sdk/server-node/src/index.ts +++ b/packages/sdk/server-node/src/index.ts @@ -8,14 +8,10 @@ * * @packageDocumentation */ -import { - BasicLogger, - BasicLoggerOptions, - LDLogger, - LDOptions, -} from '@launchdarkly/js-server-sdk-common'; +import { BasicLogger, BasicLoggerOptions, LDLogger } from '@launchdarkly/js-server-sdk-common'; import { LDClient } from './api/LDClient'; +import { LDOptions } from './api/LDOptions'; import LDClientImpl from './LDClientNode'; export * from '@launchdarkly/js-server-sdk-common'; diff --git a/packages/shared/sdk-server/src/LDClientImpl.ts b/packages/shared/sdk-server/src/LDClientImpl.ts index 6d9096b54c..4a83a367aa 100644 --- a/packages/shared/sdk-server/src/LDClientImpl.ts +++ b/packages/shared/sdk-server/src/LDClientImpl.ts @@ -10,6 +10,7 @@ import { LDEvaluationDetail, LDEvaluationDetailTyped, LDLogger, + LDPluginEnvironmentMetadata, LDTimeoutError, Platform, subsystem, @@ -32,6 +33,7 @@ import { BigSegmentStoreMembership } from './api/interfaces'; import { LDWaitForInitializationOptions } from './api/LDWaitForInitializationOptions'; import BigSegmentsManager from './BigSegmentsManager'; import BigSegmentStoreStatusProvider from './BigSegmentStatusProviderImpl'; +import { createPluginEnvironmentMetadata } from './createPluginEnvironmentMetadata'; import { createStreamListeners } from './data_sources/createStreamListeners'; import DataSourceUpdates from './data_sources/DataSourceUpdates'; import PollingProcessor from './data_sources/PollingProcessor'; @@ -52,6 +54,7 @@ import HookRunner from './hooks/HookRunner'; import MigrationOpEventToInputEvent from './MigrationOpEventConversion'; import MigrationOpTracker from './MigrationOpTracker'; import Configuration from './options/Configuration'; +import { ServerInternalOptions } from './options/ServerInternalOptions'; import VersionedDataKinds from './store/VersionedDataKinds'; const { ClientMessages, ErrorKinds, NullEventProcessor } = internal; @@ -129,6 +132,11 @@ export default class LDClientImpl implements LDClient { private _hookRunner: HookRunner; + /** + * Derived classes need to use this data to register plugins. + */ + protected environmentMetadata: LDPluginEnvironmentMetadata; + public get logger(): LDLogger | undefined { return this._logger; } @@ -147,7 +155,7 @@ export default class LDClientImpl implements LDClient { private _platform: Platform, options: LDOptions, callbacks: LDClientCallbacks, - internalOptions?: internal.LDInternalOptions, + internalOptions?: ServerInternalOptions, ) { this._onError = callbacks.onError; this._onFailed = callbacks.onFailed; @@ -156,7 +164,17 @@ export default class LDClientImpl implements LDClient { const { onUpdate, hasEventListeners } = callbacks; const config = new Configuration(options, internalOptions); - this._hookRunner = new HookRunner(config.logger, config.hooks || []); + this.environmentMetadata = createPluginEnvironmentMetadata(_platform, _sdkKey, config); + + const hooks: Hook[] = []; + if (config.hooks) { + hooks.push(...config.hooks); + } + config.getImplementationHooks(this.environmentMetadata).forEach((hook) => { + hooks.push(hook); + }); + + this._hookRunner = new HookRunner(config.logger, hooks); if (!_sdkKey && !config.offline) { throw new Error('You must configure the client with an SDK key'); diff --git a/packages/shared/sdk-server/src/createPluginEnvironmentMetadata.ts b/packages/shared/sdk-server/src/createPluginEnvironmentMetadata.ts new file mode 100644 index 0000000000..8b6005c0f6 --- /dev/null +++ b/packages/shared/sdk-server/src/createPluginEnvironmentMetadata.ts @@ -0,0 +1,35 @@ +import { LDPluginEnvironmentMetadata, Platform } from '@launchdarkly/js-sdk-common'; + +import Configuration from './options/Configuration'; + +/** + * Mutable utility type to allow building up a readonly object from a mutable one. + */ +type Mutable = { + -readonly [P in keyof T]: Mutable; +}; + +export function createPluginEnvironmentMetadata( + _platform: Platform, + _sdkKey: string, + config: Configuration, +) { + const environmentMetadata: Mutable = { + sdk: { + name: _platform.info.sdkData().userAgentBase!, + version: _platform.info.sdkData().version!, + }, + sdkKey: _sdkKey, + }; + + if (_platform.info.sdkData().wrapperName) { + environmentMetadata.sdk.wrapperName = _platform.info.sdkData().wrapperName; + } + if (_platform.info.sdkData().wrapperVersion) { + environmentMetadata.sdk.wrapperVersion = _platform.info.sdkData().wrapperVersion; + } + if (config.applicationInfo) { + environmentMetadata.application = config.applicationInfo; + } + return environmentMetadata; +} diff --git a/packages/shared/sdk-server/src/options/Configuration.ts b/packages/shared/sdk-server/src/options/Configuration.ts index 96d7494143..9f2a4cde38 100644 --- a/packages/shared/sdk-server/src/options/Configuration.ts +++ b/packages/shared/sdk-server/src/options/Configuration.ts @@ -1,8 +1,8 @@ import { ApplicationTags, - internal, LDClientContext, LDLogger, + LDPluginEnvironmentMetadata, NumberWithMinimum, OptionMessages, ServiceEndpoints, @@ -16,6 +16,7 @@ import { LDBigSegmentsOptions, LDOptions, LDProxyOptions, LDTLSOptions } from '. import { Hook } from '../api/integrations'; import { LDDataSourceUpdates, LDFeatureStore } from '../api/subsystems'; import InMemoryFeatureStore from '../store/InMemoryFeatureStore'; +import { ServerInternalOptions } from './ServerInternalOptions'; import { ValidatedOptions } from './ValidatedOptions'; // Once things are internal to the implementation of the SDK we can depend on @@ -219,7 +220,18 @@ export default class Configuration { public readonly enableEventCompression: boolean; - constructor(options: LDOptions = {}, internalOptions: internal.LDInternalOptions = {}) { + public readonly getImplementationHooks: ( + environmentMetadata: LDPluginEnvironmentMetadata, + ) => Hook[]; + + public readonly applicationInfo?: { + id?: string; + version?: string; + name?: string; + versionName?: string; + }; + + constructor(options: LDOptions = {}, internalOptions: ServerInternalOptions = {}) { // The default will handle undefined, but not null. // Because we can be called from JS we need to be extra defensive. // eslint-disable-next-line no-param-reassign @@ -288,5 +300,7 @@ export default class Configuration { this.hooks = validatedOptions.hooks; this.enableEventCompression = validatedOptions.enableEventCompression; + this.getImplementationHooks = internalOptions.getImplementationHooks ?? (() => []); + this.applicationInfo = validatedOptions.application; } } diff --git a/packages/shared/sdk-server/src/options/ServerInternalOptions.ts b/packages/shared/sdk-server/src/options/ServerInternalOptions.ts new file mode 100644 index 0000000000..dd1de40357 --- /dev/null +++ b/packages/shared/sdk-server/src/options/ServerInternalOptions.ts @@ -0,0 +1,7 @@ +import { internal, LDPluginEnvironmentMetadata } from '@launchdarkly/js-sdk-common'; + +import { Hook } from '../integrations'; + +export interface ServerInternalOptions extends internal.LDInternalOptions { + getImplementationHooks?: (environmentMetadata: LDPluginEnvironmentMetadata) => Hook[]; +} From 8be82b75763e2eaa0540a6a3c8ecbd94e26558f2 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Mon, 9 Jun 2025 12:55:03 -0700 Subject: [PATCH 2/4] Remove use of emits for node client. --- packages/sdk/server-node/src/LDClientNode.ts | 110 +++++++++++++------ 1 file changed, 76 insertions(+), 34 deletions(-) diff --git a/packages/sdk/server-node/src/LDClientNode.ts b/packages/sdk/server-node/src/LDClientNode.ts index 9262a83ecb..e0affc654f 100644 --- a/packages/sdk/server-node/src/LDClientNode.ts +++ b/packages/sdk/server-node/src/LDClientNode.ts @@ -7,51 +7,22 @@ import { internal, LDClientImpl, LDPluginEnvironmentMetadata, - Platform, SafeLogger, TypeValidators, } from '@launchdarkly/js-server-sdk-common'; -import { LDClientCallbacks } from '@launchdarkly/js-server-sdk-common/dist/LDClientImpl'; -import { ServerInternalOptions } from '@launchdarkly/js-server-sdk-common/dist/options/ServerInternalOptions'; import { BigSegmentStoreStatusProvider, LDClient } from './api'; import { LDOptions } from './api/LDOptions'; import { LDPlugin } from './api/LDPlugin'; import BigSegmentStoreStatusProviderNode from './BigSegmentsStoreStatusProviderNode'; -import { Emits } from './Emits'; import NodePlatform from './platform/NodePlatform'; -/** - * @internal - * Extend the base client implementation with an event emitter. - * - * The LDClientNode implementation must satisfy the LDClient interface, - * which is why we extend the base client implementation with an event emitter - * and then inherit from that. This adds everything we need to the implementation - * to comply with the interface. - * - * This allows re-use of the `Emits` mixin for this and big segments. - */ -class ClientBaseWithEmitter extends LDClientImpl { - emitter: EventEmitter; - - constructor( - sdkKey: string, - platform: Platform, - options: LDOptions, - callbacks: LDClientCallbacks, - internalOptions?: ServerInternalOptions, - ) { - super(sdkKey, platform, options, callbacks, internalOptions); - this.emitter = new EventEmitter(); - } -} - /** * @ignore */ -class LDClientNode extends Emits(ClientBaseWithEmitter) implements LDClient { +class LDClientNode extends LDClientImpl implements LDClient { bigSegmentStoreStatusProvider: BigSegmentStoreStatusProvider; + emitter: EventEmitter; constructor(sdkKey: string, options: LDOptions) { const fallbackLogger = new BasicLogger({ @@ -109,16 +80,87 @@ class LDClientNode extends Emits(ClientBaseWithEmitter) implements LDClient { internal.safeGetHooks(logger, environmentMetadata, plugins), }, ); - // TODO: It would be good if we could re-arrange this emitter situation so we don't have to - // create two emitters. It isn't harmful, but it isn't ideal. - this.emitter = emitter; + this.emitter = emitter; this.bigSegmentStoreStatusProvider = new BigSegmentStoreStatusProviderNode( this.bigSegmentStatusProviderInternal, ) as BigSegmentStoreStatusProvider; internal.safeRegisterPlugins(logger, this.environmentMetadata, this, plugins); } + + // #region: EventEmitter + + on(eventName: string | symbol, listener: (...args: any[]) => void): this { + this.emitter.on(eventName, listener); + return this; + } + + addListener(eventName: string | symbol, listener: (...args: any[]) => void): this { + this.emitter.addListener(eventName, listener); + return this; + } + + once(eventName: string | symbol, listener: (...args: any[]) => void): this { + this.emitter.once(eventName, listener); + return this; + } + + removeListener(eventName: string | symbol, listener: (...args: any[]) => void): this { + this.emitter.removeListener(eventName, listener); + return this; + } + + off(eventName: string | symbol, listener: (...args: any) => void): this { + this.emitter.off(eventName, listener); + return this; + } + + removeAllListeners(event?: string | symbol): this { + this.emitter.removeAllListeners(event); + return this; + } + + setMaxListeners(n: number): this { + this.emitter.setMaxListeners(n); + return this; + } + + getMaxListeners(): number { + return this.emitter.getMaxListeners(); + } + + listeners(eventName: string | symbol): Function[] { + return this.emitter.listeners(eventName); + } + + rawListeners(eventName: string | symbol): Function[] { + return this.emitter.rawListeners(eventName); + } + + emit(eventName: string | symbol, ...args: any[]): boolean { + return this.emitter.emit(eventName, args); + } + + listenerCount(eventName: string | symbol): number { + return this.emitter.listenerCount(eventName); + } + + prependListener(eventName: string | symbol, listener: (...args: any[]) => void): this { + this.emitter.prependListener(eventName, listener); + return this; + } + + prependOnceListener(eventName: string | symbol, listener: (...args: any[]) => void): this { + this.emitter.prependOnceListener(eventName, listener); + return this; + } + + eventNames(): (string | symbol)[] { + return this.emitter.eventNames(); + } + + // #endregion } export default LDClientNode; From b2e6c413dcaba1f16dd8e0cbe25796d54d2db2ef Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Mon, 16 Jun 2025 14:42:40 -0700 Subject: [PATCH 3/4] Explicitly export LDOptions --- packages/sdk/server-node/src/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/sdk/server-node/src/index.ts b/packages/sdk/server-node/src/index.ts index bce3718b48..db9ef00199 100644 --- a/packages/sdk/server-node/src/index.ts +++ b/packages/sdk/server-node/src/index.ts @@ -15,6 +15,8 @@ import { LDOptions } from './api/LDOptions'; import LDClientImpl from './LDClientNode'; export * from '@launchdarkly/js-server-sdk-common'; +// Override common options with node specific options. +export { LDOptions }; // To replace the exports from `export *` we need to name them. // So the below exports replace them with the Node specific variants. From 0c41534f5a54f697eb4aa8fe9775af17a11f409d Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Mon, 16 Jun 2025 15:16:28 -0700 Subject: [PATCH 4/4] Filter to base options to prevent warnings. --- packages/sdk/server-node/src/LDClientNode.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/sdk/server-node/src/LDClientNode.ts b/packages/sdk/server-node/src/LDClientNode.ts index e0affc654f..0b8cbc3a0e 100644 --- a/packages/sdk/server-node/src/LDClientNode.ts +++ b/packages/sdk/server-node/src/LDClientNode.ts @@ -45,10 +45,13 @@ class LDClientNode extends LDClientImpl implements LDClient { } } + const baseOptions = { ...options, logger }; + delete baseOptions.plugins; + super( sdkKey, new NodePlatform({ ...options, logger }), - { ...options, logger }, + baseOptions, { onError: (err: Error) => { if (emitter.listenerCount('error')) {