From a0901700ff04c0a26b526e3210b05ecf10d24736 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Thu, 19 May 2022 16:26:36 -0700 Subject: [PATCH 1/7] Work on initial scaffold. --- .../src/BigSegmentsStoreStatusProviderNode.ts | 26 +++++ platform-node/src/LDClientNode.ts | 96 +++++++++++++++ platform-node/src/api/LDClient.ts | 22 ++++ platform-node/src/api/index.ts | 2 + .../BigSegmentStoreStatusProvider.ts | 8 +- platform-node/src/api/interfaces/index.ts | 1 + platform-node/src/index.ts | 25 +--- .../src/{ => platform}/NodeCrypto.ts | 0 .../src/{ => platform}/NodeFilesystem.ts | 0 platform-node/src/{ => platform}/NodeInfo.ts | 2 +- .../src/{ => platform}/NodePlatform.ts | 0 .../src/{ => platform}/NodeRequests.ts | 0 .../src/{ => platform}/NodeResponse.ts | 0 .../src/BigSegmentStatusProvider.ts | 36 ++++++ server-sdk-common/src/LDClientImpl.ts | 109 ++++++++++++++++++ server-sdk-common/src/api/LDClient.ts | 45 +------- server-sdk-common/src/api/interfaces/index.ts | 1 - server-sdk-common/src/index.ts | 4 + 18 files changed, 304 insertions(+), 73 deletions(-) create mode 100644 platform-node/src/BigSegmentsStoreStatusProviderNode.ts create mode 100644 platform-node/src/LDClientNode.ts create mode 100644 platform-node/src/api/LDClient.ts create mode 100644 platform-node/src/api/index.ts rename {server-sdk-common => platform-node}/src/api/interfaces/BigSegmentStoreStatusProvider.ts (86%) create mode 100644 platform-node/src/api/interfaces/index.ts rename platform-node/src/{ => platform}/NodeCrypto.ts (100%) rename platform-node/src/{ => platform}/NodeFilesystem.ts (100%) rename platform-node/src/{ => platform}/NodeInfo.ts (94%) rename platform-node/src/{ => platform}/NodePlatform.ts (100%) rename platform-node/src/{ => platform}/NodeRequests.ts (100%) rename platform-node/src/{ => platform}/NodeResponse.ts (100%) create mode 100644 server-sdk-common/src/BigSegmentStatusProvider.ts create mode 100644 server-sdk-common/src/LDClientImpl.ts diff --git a/platform-node/src/BigSegmentsStoreStatusProviderNode.ts b/platform-node/src/BigSegmentsStoreStatusProviderNode.ts new file mode 100644 index 0000000000..0a4b03cd5c --- /dev/null +++ b/platform-node/src/BigSegmentsStoreStatusProviderNode.ts @@ -0,0 +1,26 @@ +import { LDClientImpl } from '@launchdarkly/js-server-sdk-common'; +import { BigSegmentStoreStatus } from '@launchdarkly/js-server-sdk-common/dist/api/interfaces'; +import EventEmitter from 'events'; +import { BigSegmentStoreStatusProvider } from './api/interfaces/BigSegmentStoreStatusProvider'; + +export default class BigSegmentStoreStatusProviderNode + extends EventEmitter implements BigSegmentStoreStatusProvider { + clientBase: LDClientImpl; + + constructor(clientBase: LDClientImpl) { + super(); + this.clientBase = clientBase; + + clientBase.bigSegmentStoreStatusProvider.setStatusHandler((status) => { + this.emit('change', status); + }); + } + + getStatus(): BigSegmentStoreStatus | undefined { + return this.clientBase.bigSegmentStoreStatusProvider.getStatus(); + } + + requireStatus(): Promise { + return this.clientBase.bigSegmentStoreStatusProvider.requireStatus(); + } +} diff --git a/platform-node/src/LDClientNode.ts b/platform-node/src/LDClientNode.ts new file mode 100644 index 0000000000..b34d16660f --- /dev/null +++ b/platform-node/src/LDClientNode.ts @@ -0,0 +1,96 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/* eslint-disable class-methods-use-this */ +import { + LDClientImpl, LDContext, LDEvaluationDetail, LDFlagsState, LDFlagsStateOptions, LDOptions, +} from '@launchdarkly/js-server-sdk-common'; + +import EventEmitter from 'events'; +import { LDClient } from './api/LDClient'; +import BigSegmentStoreStatusProviderNode from './BigSegmentsStoreStatusProviderNode'; +import { BigSegmentStoreStatusProvider } from './api/interfaces/BigSegmentStoreStatusProvider'; +import NodePlatform from './platform/NodePlatform'; + +export default class LDClientNode extends EventEmitter implements LDClient { + /** + * The LDClientImpl is included through composition, instead of inheritance, + * for a couple of reasons. + * 1. This allows the node client to implement EventEmitter, where otherwise + * it would be unable to from single inheritance. EventEmitter is not included + * in common because it is node specific. + * + * 2. This allows conversions of some types compared to the base implementation. + * Such as returning a node `LDClient` instead of the base `LDClient` interface. + * This interface extends EventEmitter to support #1. + */ + private client: LDClientImpl; + + constructor(options: LDOptions) { + super(); + // TODO: Remember to parse the options before providing them to the platform. + this.client = new LDClientImpl(new NodePlatform(options)); + // Extend the BigSegmentStoreStatusProvider from the common client to allow + // for use of the event emitter. + this.bigSegmentStoreStatusProvider = new BigSegmentStoreStatusProviderNode(this.client); + } + + bigSegmentStoreStatusProvider: BigSegmentStoreStatusProvider; + + initialized(): boolean { + return this.client.initialized(); + } + + async waitForInitialization(): Promise { + await this.client.waitForInitialization(); + return this; + } + + variation( + key: string, + context: LDContext, + defaultValue: any, + callback?: (err: any, res: any) => void, + ): Promise { + return this.client.variation(key, context, defaultValue, callback); + } + + variationDetail( + key: string, + context: LDContext, + defaultValue: any, + callback?: (err: any, res: LDEvaluationDetail) => void, + ): Promise { + return this.client.variationDetail(key, context, defaultValue, callback); + } + + allFlagsState( + context: LDContext, + options?: LDFlagsStateOptions, + callback?: (err: Error, res: LDFlagsState) => void, + ): Promise { + return this.client.allFlagsState(context, options, callback); + } + + secureModeHash(context: LDContext): string { + return this.client.secureModeHash(context); + } + + close(): void { + this.client.close(); + } + + isOffline(): boolean { + return this.client.isOffline(); + } + + track(key: string, context: LDContext, data?: any, metricValue?: number): void { + return this.client.track(key, context, data, metricValue); + } + + identify(context: LDContext): void { + return this.client.identify(context); + } + + flush(callback?: (err: Error, res: boolean) => void): Promise { + return this.client.flush(callback); + } +} diff --git a/platform-node/src/api/LDClient.ts b/platform-node/src/api/LDClient.ts new file mode 100644 index 0000000000..e36c8d94c7 --- /dev/null +++ b/platform-node/src/api/LDClient.ts @@ -0,0 +1,22 @@ +import { LDClient as LDClientCommon } from '@launchdarkly/js-server-sdk-common'; +import EventEmitter from 'events'; +import { BigSegmentStoreStatusProvider } from './interfaces/BigSegmentStoreStatusProvider'; + +/** + * The LaunchDarkly SDK client object. + * + * Create this object with [[init]]. Applications should configure the client at startup time and + * continue to use it throughout the lifetime of the application, rather than creating instances on + * the fly. + * + */ +export interface LDClient extends LDClientCommon, EventEmitter { + /** + * A mechanism for tracking the status of a Big Segment store. + * + * This object has methods for checking whether the Big Segment store is (as far as the SDK + * knows) currently operational and tracking changes in this status. See + * {@link interfaces.BigSegmentStoreStatusProvider} for more about this functionality. + */ + readonly bigSegmentStoreStatusProvider: BigSegmentStoreStatusProvider; +} diff --git a/platform-node/src/api/index.ts b/platform-node/src/api/index.ts new file mode 100644 index 0000000000..5ace2345c0 --- /dev/null +++ b/platform-node/src/api/index.ts @@ -0,0 +1,2 @@ +export * from './LDClient'; +export * from './interfaces'; diff --git a/server-sdk-common/src/api/interfaces/BigSegmentStoreStatusProvider.ts b/platform-node/src/api/interfaces/BigSegmentStoreStatusProvider.ts similarity index 86% rename from server-sdk-common/src/api/interfaces/BigSegmentStoreStatusProvider.ts rename to platform-node/src/api/interfaces/BigSegmentStoreStatusProvider.ts index 45a512db30..0ae8846d8a 100644 --- a/server-sdk-common/src/api/interfaces/BigSegmentStoreStatusProvider.ts +++ b/platform-node/src/api/interfaces/BigSegmentStoreStatusProvider.ts @@ -1,5 +1,5 @@ -import { EventEmitter } from 'events'; -import { BigSegmentStoreStatus } from './BigSegmentStoreStatus'; +import EventEmitter from 'events'; +import { interfaces } from '@launchdarkly/js-server-sdk-common'; /** * An interface for querying the status of a Big Segment store. @@ -27,12 +27,12 @@ export interface BigSegmentStoreStatusProvider extends EventEmitter { * @returns a {@link BigSegmentStoreStatus}, or `undefined` if the SDK has not yet queried the * Big Segment store status */ - getStatus(): BigSegmentStoreStatus | undefined; + getStatus(): interfaces.BigSegmentStoreStatus | undefined; /** * Gets the current status of the store, querying it if the status has not already been queried. * * @returns a Promise for the status of the store */ - requireStatus(): Promise; + requireStatus(): Promise; } diff --git a/platform-node/src/api/interfaces/index.ts b/platform-node/src/api/interfaces/index.ts new file mode 100644 index 0000000000..aaf6c352a2 --- /dev/null +++ b/platform-node/src/api/interfaces/index.ts @@ -0,0 +1 @@ +export * from './BigSegmentStoreStatusProvider'; \ No newline at end of file diff --git a/platform-node/src/index.ts b/platform-node/src/index.ts index 897f20d7d7..0567f419eb 100644 --- a/platform-node/src/index.ts +++ b/platform-node/src/index.ts @@ -1,24 +1,3 @@ -/* eslint-disable no-console */ -// This file contains temporary code for testing. -import NodePlatform from './NodePlatform'; +export * from '@launchdarkly/js-server-sdk-common'; -const platform = new NodePlatform({ - tlsParams: { - checkServerIdentity: (name) => { - console.log('GOT A IDENTITY CHECK', name); - return undefined; - }, - }, -}); - -async function runTest() { - console.log('Making request'); - const res = await platform.requests.fetch('https://www.google.com'); - console.log('Got res', res); - const body = await res.text(); - console.log('Body', body); -} - -runTest(); - -console.log(platform.info.sdkData()); +export { LDClient } from './api'; diff --git a/platform-node/src/NodeCrypto.ts b/platform-node/src/platform/NodeCrypto.ts similarity index 100% rename from platform-node/src/NodeCrypto.ts rename to platform-node/src/platform/NodeCrypto.ts diff --git a/platform-node/src/NodeFilesystem.ts b/platform-node/src/platform/NodeFilesystem.ts similarity index 100% rename from platform-node/src/NodeFilesystem.ts rename to platform-node/src/platform/NodeFilesystem.ts diff --git a/platform-node/src/NodeInfo.ts b/platform-node/src/platform/NodeInfo.ts similarity index 94% rename from platform-node/src/NodeInfo.ts rename to platform-node/src/platform/NodeInfo.ts index 72ee63aeab..03d9e198ca 100644 --- a/platform-node/src/NodeInfo.ts +++ b/platform-node/src/platform/NodeInfo.ts @@ -4,7 +4,7 @@ import { PlatformData, SdkData } from '@launchdarkly/js-server-sdk-common/dist/p import * as os from 'os'; -import * as packageJson from '../package.json'; +import * as packageJson from '../../package.json'; function processPlatformName(name: string): string { switch (name) { diff --git a/platform-node/src/NodePlatform.ts b/platform-node/src/platform/NodePlatform.ts similarity index 100% rename from platform-node/src/NodePlatform.ts rename to platform-node/src/platform/NodePlatform.ts diff --git a/platform-node/src/NodeRequests.ts b/platform-node/src/platform/NodeRequests.ts similarity index 100% rename from platform-node/src/NodeRequests.ts rename to platform-node/src/platform/NodeRequests.ts diff --git a/platform-node/src/NodeResponse.ts b/platform-node/src/platform/NodeResponse.ts similarity index 100% rename from platform-node/src/NodeResponse.ts rename to platform-node/src/platform/NodeResponse.ts diff --git a/server-sdk-common/src/BigSegmentStatusProvider.ts b/server-sdk-common/src/BigSegmentStatusProvider.ts new file mode 100644 index 0000000000..3bf42038ae --- /dev/null +++ b/server-sdk-common/src/BigSegmentStatusProvider.ts @@ -0,0 +1,36 @@ +/* eslint-disable class-methods-use-this */ +import { BigSegmentStoreStatus } from './api/interfaces'; + +type StatusHandler = (status: BigSegmentStoreStatus) => void; + +export default class BigSegmentStoreStatusProvider { + private onStatus?: StatusHandler; + + /** + * Gets the current status of the store, if known. + * + * @returns a {@link BigSegmentStoreStatus}, or `undefined` if the SDK has not yet queried the + * Big Segment store status + */ + getStatus(): BigSegmentStoreStatus | undefined { + return undefined; + } + + /** + * Gets the current status of the store, querying it if the status has not already been queried. + * + * @returns a Promise for the status of the store + */ + async requireStatus(): Promise { + return Promise.reject(); + } + + /** + * Set the status handler. Only one handler can be registered and this will replace the existing + * handler. + * @param handler + */ + setStatusHandler(handler: StatusHandler) { + this.onStatus = handler; + } +} diff --git a/server-sdk-common/src/LDClientImpl.ts b/server-sdk-common/src/LDClientImpl.ts new file mode 100644 index 0000000000..27d1b92050 --- /dev/null +++ b/server-sdk-common/src/LDClientImpl.ts @@ -0,0 +1,109 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/* eslint-disable class-methods-use-this */ +import { + LDClient, LDContext, LDEvaluationDetail, LDFlagsState, LDFlagsStateOptions, +} from './api'; +import BigSegmentStoreStatusProvider from './BigSegmentStatusProvider'; +import { Platform } from './platform'; + +enum EventTypes { + /** + * Emitted on errors other than a failure to initialize. + */ + Error = 'error', + /** + * Emitted when the SDK fails to initialize. + */ + Failed = 'failed', + /** + * Emitted when the SDK is ready to use. + */ + Ready = 'ready', +} + +enum InitState { + Initializing, + Initialized, + Failed, +} + +export default class LDClientImpl implements LDClient { + private platform: Platform; + + private initState: InitState = InitState.Initializing; + + /** + * Intended for use by platform specific client implementations. + * + * It is not included in the main interface because it requires the use of + * a platform event system. For node this would be an EventEmitter, for other + * platforms it would likely be an EventTarget. + */ + public bigSegmentStoreStatusProvider = new BigSegmentStoreStatusProvider(); + + constructor(targetPlatform: Platform) { + this.platform = targetPlatform; + } + + initialized(): boolean { + return this.initState === InitState.Initialized; + } + + waitForInitialization(): Promise { + throw new Error('Method not implemented.'); + } + + variation( + key: string, + context: LDContext, + defaultValue: any, + callback?: (err: any, res: any) => void, + ): Promise { + throw new Error('Method not implemented.'); + } + + variationDetail( + key: string, + context: LDContext, + defaultValue: any, + callback?: (err: any, res: LDEvaluationDetail) => void, + ): Promise { + throw new Error('Method not implemented.'); + } + + allFlagsState( + context: LDContext, + options?: LDFlagsStateOptions, + callback?: (err: Error, res: LDFlagsState) => void, + ): Promise { + throw new Error('Method not implemented.'); + } + + secureModeHash(context: LDContext): string { + throw new Error('Method not implemented.'); + } + + close(): void { + throw new Error('Method not implemented.'); + } + + isOffline(): boolean { + throw new Error('Method not implemented.'); + } + + track(key: string, context: LDContext, data?: any, metricValue?: number): void { + throw new Error('Method not implemented.'); + } + + identify(context: LDContext): void { + throw new Error('Method not implemented.'); + } + + flush(callback?: (err: Error, res: boolean) => void): Promise { + throw new Error('Method not implemented.'); + } + + on(event: string | symbol, listener: (...args: any[]) => void): this { + throw new Error('Method not implemented.'); + } +} diff --git a/server-sdk-common/src/api/LDClient.ts b/server-sdk-common/src/api/LDClient.ts index 6a53d11fa4..ada8704a04 100644 --- a/server-sdk-common/src/api/LDClient.ts +++ b/server-sdk-common/src/api/LDClient.ts @@ -1,6 +1,4 @@ -import { EventEmitter } from 'events'; import { LDContext } from './LDContext'; -import { BigSegmentStoreStatusProvider } from './interfaces/BigSegmentStoreStatusProvider'; import { LDEvaluationDetail } from './data/LDEvaluationDetail'; import { LDFlagsState } from './data/LDFlagsState'; import { LDFlagsStateOptions } from './data/LDFlagsStateOptions'; @@ -13,15 +11,8 @@ import { LDFlagValue } from './data/LDFlagValue'; * continue to use it throughout the lifetime of the application, rather than creating instances on * the fly. * - * Note that `LDClient` inherits from `EventEmitter`, so you can use the standard `on()`, `once()`, - * and `off()` methods to receive events. The standard `EventEmitter` methods are not documented - * here; see the - * {@link https://nodejs.org/api/events.html#events_class_eventemitter|Node API documentation}. For - * a description of events you can listen for, see [[on]]. - * - * @see {@link https://docs.launchdarkly.com/sdk/server-side/node-js|SDK Reference Guide} */ -export interface LDClient extends EventEmitter { +export interface LDClient { /** * Tests whether the client has completed initialization. * @@ -241,23 +232,9 @@ export interface LDClient extends EventEmitter { */ flush(callback?: (err: Error, res: boolean) => void): Promise; - /** - * A mechanism for tracking the status of a Big Segment store. - * - * This object has methods for checking whether the Big Segment store is (as far as the SDK - * knows) currently operational and tracking changes in this status. See - * {@link interfaces.BigSegmentStoreStatusProvider} for more about this functionality. - */ - readonly bigSegmentStoreStatusProvider: BigSegmentStoreStatusProvider; - /** * Registers an event listener that will be called when the client triggers some type of event. * - * This is the standard `on` method inherited from Node's `EventEmitter`; see the - * {@link https://nodejs.org/api/events.html#events_class_eventemitter|Node API docs} for more - * details on how to manage event listeners. Here is a description of the event types defined by - * `LDClient`. - * * - `"ready"`: Sent only once, when the client has successfully connected to LaunchDarkly. * Alternately, you can detect this with [[waitForInitialization]]. * - `"failed"`: Sent only once, if the client has permanently failed to connect to LaunchDarkly. @@ -275,24 +252,4 @@ export interface LDClient extends EventEmitter { * @param listener the function to call when the event happens */ on(event: string | symbol, listener: (...args: any[]) => void): this; - - // The following are symbols that LDClient inherits from EventEmitter, which we are declaring - // again here only so that we can use @ignore to exclude them from the generated docs. - // Unfortunately it does not seem possible to exclude these inherited methods en masse without - // using a Typedoc plugin. - /** @ignore */ addListener(event: string | symbol, listener: (...args: any[]) => void): this; - /** @ignore */ emit(event: string | symbol, ...args: any[]): boolean; - /** @ignore */ eventNames(): Array; - /** @ignore */ getMaxListeners(): number; - /** @ignore */ listenerCount(type: string | symbol): number; - /** @ignore */ listeners(event: string | symbol): Function[]; - /** @ignore */ prependListener(event: string | symbol, listener: (...args: any[]) => void): this; - /** @ignore */ prependOnceListener(event: string | symbol, listener: - (...args: any[]) => void): this; - /** @ignore */ rawListeners(event: string | symbol): Function[]; - /** @ignore */ removeAllListeners(event?: string | symbol): this; - /** @ignore */ removeListener(event: string | symbol, listener: (...args: any[]) => void): this; - /** @ignore */ setMaxListeners(n: number): this; - /** @ignore */ once(event: string | symbol, listener: (...args: any[]) => void): this; - /** @ignore */ off(event: string | symbol, listener: (...args: any[]) => void): this; } diff --git a/server-sdk-common/src/api/interfaces/index.ts b/server-sdk-common/src/api/interfaces/index.ts index 827cee6e7b..1a4a141cde 100644 --- a/server-sdk-common/src/api/interfaces/index.ts +++ b/server-sdk-common/src/api/interfaces/index.ts @@ -2,7 +2,6 @@ export * from './BigSegmentStore'; export * from './BigSegmentStoreMembership'; export * from './BigSegmentStoreMetadata'; export * from './BigSegmentStoreStatus'; -export * from './BigSegmentStoreStatusProvider'; export * from './DataCollection'; export * from './DataKind'; export * from './FullDataSet'; diff --git a/server-sdk-common/src/index.ts b/server-sdk-common/src/index.ts index e349fcd5f6..2a3603259f 100644 --- a/server-sdk-common/src/index.ts +++ b/server-sdk-common/src/index.ts @@ -1,2 +1,6 @@ +import LDClientImpl from './LDClientImpl'; + export * as platform from './platform'; export * from './api'; + +export { LDClientImpl }; From 0b470eab6d55b529f666bbd953458bb632c0c35e Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Thu, 19 May 2022 16:31:17 -0700 Subject: [PATCH 2/7] Update index exports. --- platform-node/src/index.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/platform-node/src/index.ts b/platform-node/src/index.ts index 0567f419eb..c1fecbeeab 100644 --- a/platform-node/src/index.ts +++ b/platform-node/src/index.ts @@ -1,3 +1,9 @@ +import LDClientImpl from './LDClientNode'; + export * from '@launchdarkly/js-server-sdk-common'; -export { LDClient } from './api'; +// To replace the exports from `export *` we need to name them. +// So the below exports replace them with the Node specific variants. + +export { LDClient, BigSegmentStoreStatusProvider } from './api'; +export { LDClientImpl }; From 93fbac63cf6625d2e03a719c4023aa0a2458ec2c Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Thu, 19 May 2022 16:39:43 -0700 Subject: [PATCH 3/7] Add an extra blank line. --- platform-node/src/api/interfaces/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform-node/src/api/interfaces/index.ts b/platform-node/src/api/interfaces/index.ts index aaf6c352a2..153474b58c 100644 --- a/platform-node/src/api/interfaces/index.ts +++ b/platform-node/src/api/interfaces/index.ts @@ -1 +1 @@ -export * from './BigSegmentStoreStatusProvider'; \ No newline at end of file +export * from './BigSegmentStoreStatusProvider'; From 8be08daaa960319bae0285db3ed629b4b3ab1267 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Mon, 23 May 2022 10:20:03 -0700 Subject: [PATCH 4/7] Attempt using mixin for event emitter. --- platform-node/__tests__/NodeInfo_test.ts | 2 +- platform-node/__tests__/NodeRequests_test.ts | 2 +- .../src/BigSegmentsStoreStatusProviderNode.ts | 29 ++---- platform-node/src/Emits.ts | 74 ++++++++++++++ platform-node/src/LDClientNode.ts | 97 +++---------------- platform-node/src/api/LDClient.ts | 6 +- ...rovider.ts => BigSegmentStatusProvider.ts} | 2 +- platform-node/src/api/interfaces/index.ts | 2 +- platform-node/src/index.ts | 5 +- ...der.ts => BigSegmentStatusProviderImpl.ts} | 6 +- server-sdk-common/src/LDClientImpl.ts | 4 +- .../BigSegmentStoreStatusProvider.ts | 30 ++++++ server-sdk-common/src/api/interfaces/index.ts | 1 + server-sdk-common/src/index.ts | 2 + 14 files changed, 141 insertions(+), 121 deletions(-) create mode 100644 platform-node/src/Emits.ts rename platform-node/src/api/interfaces/{BigSegmentStoreStatusProvider.ts => BigSegmentStatusProvider.ts} (95%) rename server-sdk-common/src/{BigSegmentStatusProvider.ts => BigSegmentStatusProviderImpl.ts} (78%) create mode 100644 server-sdk-common/src/api/interfaces/BigSegmentStoreStatusProvider.ts diff --git a/platform-node/__tests__/NodeInfo_test.ts b/platform-node/__tests__/NodeInfo_test.ts index 18c527e4ba..0567413c76 100644 --- a/platform-node/__tests__/NodeInfo_test.ts +++ b/platform-node/__tests__/NodeInfo_test.ts @@ -1,5 +1,5 @@ import * as os from 'os'; -import NodeInfo from '../src/NodeInfo'; +import NodeInfo from '../src/platform/NodeInfo'; describe('given an information instance', () => { const info = new NodeInfo(); diff --git a/platform-node/__tests__/NodeRequests_test.ts b/platform-node/__tests__/NodeRequests_test.ts index bf21e2358e..4cdb820811 100644 --- a/platform-node/__tests__/NodeRequests_test.ts +++ b/platform-node/__tests__/NodeRequests_test.ts @@ -1,6 +1,6 @@ import * as http from 'http'; -import NodeRequests from '../src/NodeRequests'; +import NodeRequests from '../src/platform/NodeRequests'; const PORT = '3000'; const TEXT_RESPONSE = 'Test Text'; diff --git a/platform-node/src/BigSegmentsStoreStatusProviderNode.ts b/platform-node/src/BigSegmentsStoreStatusProviderNode.ts index 0a4b03cd5c..24ccdc61fa 100644 --- a/platform-node/src/BigSegmentsStoreStatusProviderNode.ts +++ b/platform-node/src/BigSegmentsStoreStatusProviderNode.ts @@ -1,26 +1,9 @@ -import { LDClientImpl } from '@launchdarkly/js-server-sdk-common'; -import { BigSegmentStoreStatus } from '@launchdarkly/js-server-sdk-common/dist/api/interfaces'; +import { BigSegmentStoreStatusProviderImpl } from '@launchdarkly/js-server-sdk-common'; import EventEmitter from 'events'; -import { BigSegmentStoreStatusProvider } from './api/interfaces/BigSegmentStoreStatusProvider'; +import { Emits } from './Emits'; -export default class BigSegmentStoreStatusProviderNode - extends EventEmitter implements BigSegmentStoreStatusProvider { - clientBase: LDClientImpl; - - constructor(clientBase: LDClientImpl) { - super(); - this.clientBase = clientBase; - - clientBase.bigSegmentStoreStatusProvider.setStatusHandler((status) => { - this.emit('change', status); - }); - } - - getStatus(): BigSegmentStoreStatus | undefined { - return this.clientBase.bigSegmentStoreStatusProvider.getStatus(); - } - - requireStatus(): Promise { - return this.clientBase.bigSegmentStoreStatusProvider.requireStatus(); - } +class BigSegmentStoreStatusProviderNode extends BigSegmentStoreStatusProviderImpl { + emitter: EventEmitter = new EventEmitter(); } + +export default Emits(BigSegmentStoreStatusProviderNode); diff --git a/platform-node/src/Emits.ts b/platform-node/src/Emits.ts new file mode 100644 index 0000000000..57addc03f9 --- /dev/null +++ b/platform-node/src/Emits.ts @@ -0,0 +1,74 @@ +import EventEmitter from 'events'; + +export type EventableConstructor = new (...args: any[]) => T; +export type Eventable = EventableConstructor<{ emitter: EventEmitter }>; + +export function Emits(Base: TBase) { + return class WithEvents extends Base { + 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(); + } + }; +} \ No newline at end of file diff --git a/platform-node/src/LDClientNode.ts b/platform-node/src/LDClientNode.ts index b34d16660f..6204e6cfb0 100644 --- a/platform-node/src/LDClientNode.ts +++ b/platform-node/src/LDClientNode.ts @@ -1,96 +1,25 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable class-methods-use-this */ +// eslint-disable-next-line max-classes-per-file import { - LDClientImpl, LDContext, LDEvaluationDetail, LDFlagsState, LDFlagsStateOptions, LDOptions, + LDClientImpl, LDOptions, } from '@launchdarkly/js-server-sdk-common'; import EventEmitter from 'events'; -import { LDClient } from './api/LDClient'; -import BigSegmentStoreStatusProviderNode from './BigSegmentsStoreStatusProviderNode'; -import { BigSegmentStoreStatusProvider } from './api/interfaces/BigSegmentStoreStatusProvider'; import NodePlatform from './platform/NodePlatform'; +import { Emits } from './Emits'; +import BigSegmentStoreStatusProviderNode from './BigSegmentsStoreStatusProviderNode'; -export default class LDClientNode extends EventEmitter implements LDClient { - /** - * The LDClientImpl is included through composition, instead of inheritance, - * for a couple of reasons. - * 1. This allows the node client to implement EventEmitter, where otherwise - * it would be unable to from single inheritance. EventEmitter is not included - * in common because it is node specific. - * - * 2. This allows conversions of some types compared to the base implementation. - * Such as returning a node `LDClient` instead of the base `LDClient` interface. - * This interface extends EventEmitter to support #1. - */ - private client: LDClientImpl; - - constructor(options: LDOptions) { - super(); - // TODO: Remember to parse the options before providing them to the platform. - this.client = new LDClientImpl(new NodePlatform(options)); - // Extend the BigSegmentStoreStatusProvider from the common client to allow - // for use of the event emitter. - this.bigSegmentStoreStatusProvider = new BigSegmentStoreStatusProviderNode(this.client); - } - - bigSegmentStoreStatusProvider: BigSegmentStoreStatusProvider; - - initialized(): boolean { - return this.client.initialized(); - } - - async waitForInitialization(): Promise { - await this.client.waitForInitialization(); - return this; - } - - variation( - key: string, - context: LDContext, - defaultValue: any, - callback?: (err: any, res: any) => void, - ): Promise { - return this.client.variation(key, context, defaultValue, callback); - } - - variationDetail( - key: string, - context: LDContext, - defaultValue: any, - callback?: (err: any, res: LDEvaluationDetail) => void, - ): Promise { - return this.client.variationDetail(key, context, defaultValue, callback); - } - - allFlagsState( - context: LDContext, - options?: LDFlagsStateOptions, - callback?: (err: Error, res: LDFlagsState) => void, - ): Promise { - return this.client.allFlagsState(context, options, callback); - } - - secureModeHash(context: LDContext): string { - return this.client.secureModeHash(context); - } - - close(): void { - this.client.close(); - } - - isOffline(): boolean { - return this.client.isOffline(); - } - - track(key: string, context: LDContext, data?: any, metricValue?: number): void { - return this.client.track(key, context, data, metricValue); - } +class LDClientNode extends LDClientImpl { + emitter: EventEmitter = new EventEmitter(); - identify(context: LDContext): void { - return this.client.identify(context); - } + override bigSegmentStoreStatusProvider: + InstanceType; - flush(callback?: (err: Error, res: boolean) => void): Promise { - return this.client.flush(callback); + constructor(options: LDOptions) { + super(new NodePlatform(options)); + this.bigSegmentStoreStatusProvider = new BigSegmentStoreStatusProviderNode(); } } + +export default Emits(LDClientNode); diff --git a/platform-node/src/api/LDClient.ts b/platform-node/src/api/LDClient.ts index e36c8d94c7..8e2488dc68 100644 --- a/platform-node/src/api/LDClient.ts +++ b/platform-node/src/api/LDClient.ts @@ -1,6 +1,6 @@ import { LDClient as LDClientCommon } from '@launchdarkly/js-server-sdk-common'; -import EventEmitter from 'events'; -import { BigSegmentStoreStatusProvider } from './interfaces/BigSegmentStoreStatusProvider'; +import { BigSegmentStoreStatusProvider } from './interfaces'; + /** * The LaunchDarkly SDK client object. @@ -10,7 +10,7 @@ import { BigSegmentStoreStatusProvider } from './interfaces/BigSegmentStoreStatu * the fly. * */ -export interface LDClient extends LDClientCommon, EventEmitter { +export interface LDClient extends LDClientCommon { /** * A mechanism for tracking the status of a Big Segment store. * diff --git a/platform-node/src/api/interfaces/BigSegmentStoreStatusProvider.ts b/platform-node/src/api/interfaces/BigSegmentStatusProvider.ts similarity index 95% rename from platform-node/src/api/interfaces/BigSegmentStoreStatusProvider.ts rename to platform-node/src/api/interfaces/BigSegmentStatusProvider.ts index 0ae8846d8a..13678396a4 100644 --- a/platform-node/src/api/interfaces/BigSegmentStoreStatusProvider.ts +++ b/platform-node/src/api/interfaces/BigSegmentStatusProvider.ts @@ -20,7 +20,7 @@ import { interfaces } from '@launchdarkly/js-server-sdk-common'; * type of the status change event is `"change"`, and its value is the same value that would be * returned by {@link getStatus}. */ -export interface BigSegmentStoreStatusProvider extends EventEmitter { +export interface BigSegmentStoreStatusProvider { /** * Gets the current status of the store, if known. * diff --git a/platform-node/src/api/interfaces/index.ts b/platform-node/src/api/interfaces/index.ts index 153474b58c..1ad24aaa45 100644 --- a/platform-node/src/api/interfaces/index.ts +++ b/platform-node/src/api/interfaces/index.ts @@ -1 +1 @@ -export * from './BigSegmentStoreStatusProvider'; +export * from '@launchdarkly/js-server-sdk-common/src/api/interfaces/BigSegmentStoreStatusProvider'; diff --git a/platform-node/src/index.ts b/platform-node/src/index.ts index c1fecbeeab..a51e695836 100644 --- a/platform-node/src/index.ts +++ b/platform-node/src/index.ts @@ -1,9 +1,10 @@ import LDClientImpl from './LDClientNode'; +import BigSegmentStoreStatusProviderNode from './BigSegmentsStoreStatusProviderNode'; export * from '@launchdarkly/js-server-sdk-common'; // To replace the exports from `export *` we need to name them. // So the below exports replace them with the Node specific variants. -export { LDClient, BigSegmentStoreStatusProvider } from './api'; -export { LDClientImpl }; +export { LDClient } from './api'; +export { LDClientImpl, BigSegmentStoreStatusProviderNode }; diff --git a/server-sdk-common/src/BigSegmentStatusProvider.ts b/server-sdk-common/src/BigSegmentStatusProviderImpl.ts similarity index 78% rename from server-sdk-common/src/BigSegmentStatusProvider.ts rename to server-sdk-common/src/BigSegmentStatusProviderImpl.ts index 3bf42038ae..ddc9e184d4 100644 --- a/server-sdk-common/src/BigSegmentStatusProvider.ts +++ b/server-sdk-common/src/BigSegmentStatusProviderImpl.ts @@ -1,9 +1,9 @@ /* eslint-disable class-methods-use-this */ -import { BigSegmentStoreStatus } from './api/interfaces'; +import { BigSegmentStoreStatusProvider, BigSegmentStoreStatus } from './api/interfaces'; type StatusHandler = (status: BigSegmentStoreStatus) => void; -export default class BigSegmentStoreStatusProvider { +export default class BigSegmentStoreStatusProviderImpl implements BigSegmentStoreStatusProvider { private onStatus?: StatusHandler; /** @@ -30,7 +30,7 @@ export default class BigSegmentStoreStatusProvider { * handler. * @param handler */ - setStatusHandler(handler: StatusHandler) { + protected setStatusHandler(handler: StatusHandler) { this.onStatus = handler; } } diff --git a/server-sdk-common/src/LDClientImpl.ts b/server-sdk-common/src/LDClientImpl.ts index 27d1b92050..99a6f0dcb1 100644 --- a/server-sdk-common/src/LDClientImpl.ts +++ b/server-sdk-common/src/LDClientImpl.ts @@ -3,7 +3,7 @@ import { LDClient, LDContext, LDEvaluationDetail, LDFlagsState, LDFlagsStateOptions, } from './api'; -import BigSegmentStoreStatusProvider from './BigSegmentStatusProvider'; +import BigSegmentStoreStatusProvider from './BigSegmentStatusProviderImpl'; import { Platform } from './platform'; enum EventTypes { @@ -39,7 +39,7 @@ export default class LDClientImpl implements LDClient { * a platform event system. For node this would be an EventEmitter, for other * platforms it would likely be an EventTarget. */ - public bigSegmentStoreStatusProvider = new BigSegmentStoreStatusProvider(); + bigSegmentStoreStatusProvider = new BigSegmentStoreStatusProvider(); constructor(targetPlatform: Platform) { this.platform = targetPlatform; diff --git a/server-sdk-common/src/api/interfaces/BigSegmentStoreStatusProvider.ts b/server-sdk-common/src/api/interfaces/BigSegmentStoreStatusProvider.ts new file mode 100644 index 0000000000..e181ecfc47 --- /dev/null +++ b/server-sdk-common/src/api/interfaces/BigSegmentStoreStatusProvider.ts @@ -0,0 +1,30 @@ +import { BigSegmentStoreStatus } from './BigSegmentStoreStatus'; + +/** + * An interface for querying the status of a Big Segment store. + * + * The Big Segment store is the component that receives information about Big Segments, normally + * from a database populated by the LaunchDarkly Relay Proxy. Big Segments are a specific type of + * user segments. For more information, read the LaunchDarkly documentation: + * https://docs.launchdarkly.com/home/users/big-segments + * + * An implementation of this interface is returned by + * {@link LDClient.bigSegmentStoreStatusProvider}. Application code never needs to implement this + * interface. + */ +export interface BigSegmentStoreStatusProvider { + /** + * Gets the current status of the store, if known. + * + * @returns a {@link BigSegmentStoreStatus}, or `undefined` if the SDK has not yet queried the + * Big Segment store status + */ + getStatus(): BigSegmentStoreStatus | undefined; + + /** + * Gets the current status of the store, querying it if the status has not already been queried. + * + * @returns a Promise for the status of the store + */ + requireStatus(): Promise; +} diff --git a/server-sdk-common/src/api/interfaces/index.ts b/server-sdk-common/src/api/interfaces/index.ts index 1a4a141cde..73c35c13b5 100644 --- a/server-sdk-common/src/api/interfaces/index.ts +++ b/server-sdk-common/src/api/interfaces/index.ts @@ -10,3 +10,4 @@ export * from './PersistentDataStore'; export * from './PersistentDataStoreBase'; export * from './PersistentDataStoreNonAtomic'; export * from './VersionedData'; +export * from './BigSegmentStoreStatusProvider'; diff --git a/server-sdk-common/src/index.ts b/server-sdk-common/src/index.ts index 2a3603259f..432a9d7a4c 100644 --- a/server-sdk-common/src/index.ts +++ b/server-sdk-common/src/index.ts @@ -1,6 +1,8 @@ import LDClientImpl from './LDClientImpl'; +import BigSegmentStoreStatusProviderImpl from './BigSegmentStatusProviderImpl'; export * as platform from './platform'; export * from './api'; export { LDClientImpl }; +export { BigSegmentStoreStatusProviderImpl }; From 5302683ee1c73a584c3455bf3aaa96d584cf8bbe Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Mon, 23 May 2022 10:24:11 -0700 Subject: [PATCH 5/7] Add implementation for emitting events for big segment status. --- .../src/BigSegmentsStoreStatusProviderNode.ts | 6 +++++- .../src/BigSegmentStatusProviderImpl.ts | 15 +++++---------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/platform-node/src/BigSegmentsStoreStatusProviderNode.ts b/platform-node/src/BigSegmentsStoreStatusProviderNode.ts index 24ccdc61fa..ebd4e1005c 100644 --- a/platform-node/src/BigSegmentsStoreStatusProviderNode.ts +++ b/platform-node/src/BigSegmentsStoreStatusProviderNode.ts @@ -1,9 +1,13 @@ -import { BigSegmentStoreStatusProviderImpl } from '@launchdarkly/js-server-sdk-common'; +import { BigSegmentStoreStatusProviderImpl, interfaces } from '@launchdarkly/js-server-sdk-common'; import EventEmitter from 'events'; import { Emits } from './Emits'; class BigSegmentStoreStatusProviderNode extends BigSegmentStoreStatusProviderImpl { emitter: EventEmitter = new EventEmitter(); + + override dispatch(eventType: string, status: interfaces.BigSegmentStoreStatus) { + this.emitter.emit(eventType, status); + } } export default Emits(BigSegmentStoreStatusProviderNode); diff --git a/server-sdk-common/src/BigSegmentStatusProviderImpl.ts b/server-sdk-common/src/BigSegmentStatusProviderImpl.ts index ddc9e184d4..fa457409e8 100644 --- a/server-sdk-common/src/BigSegmentStatusProviderImpl.ts +++ b/server-sdk-common/src/BigSegmentStatusProviderImpl.ts @@ -1,11 +1,7 @@ /* eslint-disable class-methods-use-this */ import { BigSegmentStoreStatusProvider, BigSegmentStoreStatus } from './api/interfaces'; -type StatusHandler = (status: BigSegmentStoreStatus) => void; - export default class BigSegmentStoreStatusProviderImpl implements BigSegmentStoreStatusProvider { - private onStatus?: StatusHandler; - /** * Gets the current status of the store, if known. * @@ -26,11 +22,10 @@ export default class BigSegmentStoreStatusProviderImpl implements BigSegmentStor } /** - * Set the status handler. Only one handler can be registered and this will replace the existing - * handler. - * @param handler + * This should be overridden by derived implementations. + * @param eventType + * @param status */ - protected setStatusHandler(handler: StatusHandler) { - this.onStatus = handler; - } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + dispatch(eventType: string, status: BigSegmentStoreStatus) {} } From 263fccdefcd92326224d5274d8cb84c8936369b0 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Mon, 23 May 2022 10:25:45 -0700 Subject: [PATCH 6/7] Add documentation. --- platform-node/src/Emits.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/platform-node/src/Emits.ts b/platform-node/src/Emits.ts index 57addc03f9..7cbbf919f6 100644 --- a/platform-node/src/Emits.ts +++ b/platform-node/src/Emits.ts @@ -3,6 +3,12 @@ import EventEmitter from 'events'; export type EventableConstructor = new (...args: any[]) => T; export type Eventable = EventableConstructor<{ emitter: EventEmitter }>; +/** + * Adds the implementation of an event emitter to something that contains + * a field of `emitter` with type `EventEmitter`. + * @param Base The class to derive the mixin from. + * @returns A class extending the base with an event emitter. + */ export function Emits(Base: TBase) { return class WithEvents extends Base { addListener(eventName: string | symbol, listener: (...args: any[]) => void): this { @@ -71,4 +77,4 @@ export function Emits(Base: TBase) { return this.emitter.eventNames(); } }; -} \ No newline at end of file +} From 831b964933bddfa9da2fb3c8ff81a551ff1706a7 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Mon, 23 May 2022 10:34:32 -0700 Subject: [PATCH 7/7] Fix lint and interface hierarchy. --- platform-node/src/LDClientNode.ts | 3 --- platform-node/src/api/LDClient.ts | 4 ++-- platform-node/src/api/interfaces/BigSegmentStatusProvider.ts | 2 +- platform-node/src/api/interfaces/index.ts | 2 +- 4 files changed, 4 insertions(+), 7 deletions(-) diff --git a/platform-node/src/LDClientNode.ts b/platform-node/src/LDClientNode.ts index 6204e6cfb0..8079063093 100644 --- a/platform-node/src/LDClientNode.ts +++ b/platform-node/src/LDClientNode.ts @@ -1,6 +1,3 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ -/* eslint-disable class-methods-use-this */ -// eslint-disable-next-line max-classes-per-file import { LDClientImpl, LDOptions, } from '@launchdarkly/js-server-sdk-common'; diff --git a/platform-node/src/api/LDClient.ts b/platform-node/src/api/LDClient.ts index 8e2488dc68..5c7f331556 100644 --- a/platform-node/src/api/LDClient.ts +++ b/platform-node/src/api/LDClient.ts @@ -1,7 +1,7 @@ import { LDClient as LDClientCommon } from '@launchdarkly/js-server-sdk-common'; +import EventEmitter from 'events'; import { BigSegmentStoreStatusProvider } from './interfaces'; - /** * The LaunchDarkly SDK client object. * @@ -10,7 +10,7 @@ import { BigSegmentStoreStatusProvider } from './interfaces'; * the fly. * */ -export interface LDClient extends LDClientCommon { +export interface LDClient extends LDClientCommon, EventEmitter { /** * A mechanism for tracking the status of a Big Segment store. * diff --git a/platform-node/src/api/interfaces/BigSegmentStatusProvider.ts b/platform-node/src/api/interfaces/BigSegmentStatusProvider.ts index 13678396a4..0ae8846d8a 100644 --- a/platform-node/src/api/interfaces/BigSegmentStatusProvider.ts +++ b/platform-node/src/api/interfaces/BigSegmentStatusProvider.ts @@ -20,7 +20,7 @@ import { interfaces } from '@launchdarkly/js-server-sdk-common'; * type of the status change event is `"change"`, and its value is the same value that would be * returned by {@link getStatus}. */ -export interface BigSegmentStoreStatusProvider { +export interface BigSegmentStoreStatusProvider extends EventEmitter { /** * Gets the current status of the store, if known. * diff --git a/platform-node/src/api/interfaces/index.ts b/platform-node/src/api/interfaces/index.ts index 1ad24aaa45..19659eab65 100644 --- a/platform-node/src/api/interfaces/index.ts +++ b/platform-node/src/api/interfaces/index.ts @@ -1 +1 @@ -export * from '@launchdarkly/js-server-sdk-common/src/api/interfaces/BigSegmentStoreStatusProvider'; +export * from './BigSegmentStatusProvider';