Skip to content

Commit 53cb736

Browse files
authored
feat!: Update dependencies and encapsulate client construction. (#35)
Updates to latest OpenFeature SDK. Updates to latest LD SDK. Addresses #34 Adds support for OpenFeature events. Adds support for provider shutdown. Addresses #15.
1 parent d879893 commit 53cb736

File tree

9 files changed

+717
-180
lines changed

9 files changed

+717
-180
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ jobs:
1616
matrix:
1717
variations: [
1818
{os: ubuntu-latest, node: latest},
19-
{os: ubuntu-latest, node: 14},
19+
{os: ubuntu-latest, node: 16},
2020
{os: windows-latest, node: latest}
2121
]
2222

README.md

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,30 +14,46 @@ This provider is a beta version and should not be considered ready for productio
1414

1515
## Supported Node versions
1616

17-
This version of the LaunchDarkly OpenFeature provider is compatible with Node.js versions 14 and above.
17+
This version of the LaunchDarkly OpenFeature provider is compatible with Node.js versions 16 and above.
1818

1919
## Getting started
2020

2121
### Installation
2222

2323
```
24-
npm install @openfeature/js-sdk
24+
npm install @openfeature/server-sdk
2525
npm install @launchdarkly/node-server-sdk
2626
npm install @launchdarkly/openfeature-node-server
2727
```
2828

2929
### Usage
3030
```
31-
import { OpenFeature } from '@openfeature/js-sdk';
31+
import { OpenFeature } from '@openfeature/server-sdk';
3232
import { init } from '@launchdarkly/node-server-sdk';
3333
import { LaunchDarklyProvider } from '@launchdarkly/openfeature-node-server';
3434
35+
const ldProvider = new LaunchDarklyProvider('<your-sdk-key>', {/* LDOptions here */});
36+
OpenFeature.setProvider(ldProvider);
3537
36-
const ldClient = init('<your-sdk-key>');
37-
await ldClient.waitForInitialization();
38-
OpenFeature.setProvider(new LaunchDarklyProvider(ldClient));
39-
const client = OpenFeature.getClient();
40-
const value = await client.getBooleanValue('app-enabled', false, {targetingKey: 'my-key'});
38+
// If you need access to the LDClient, then you can use ldProvider.getClient()
39+
40+
// Evaluations before the provider indicates it is ready may get default values with a
41+
// CLIENT_NOT_READY reason.
42+
OpenFeature.addHandler(ProviderEvents.Ready, (eventDetails) => {
43+
const client = OpenFeature.getClient();
44+
const value = await client.getBooleanValue('app-enabled', false, {targetingKey: 'my-key'});
45+
});
46+
47+
// The LaunchDarkly provider supports the ProviderEvents.ConfigurationChanged event.
48+
// The provider will emit this event for any flag key that may have changed (each event will contain
49+
// a single key in the `flagsChanged` field).
50+
OpenFeature.addHandler(ProviderEvents.Ready, (eventDetails) => {
51+
console.log(`Changed ${eventDetails.flagsChanged}`);
52+
});
53+
54+
// When the LaunchDarkly provider is closed it will flush the events on the LDClient instance.
55+
// This can be useful for short lived processes.
56+
await OpenFeature.close();
4157
```
4258

4359
Refer to the [SDK reference guide](https://docs.launchdarkly.com/sdk/server-side/node-js) for instructions on getting started with using the SDK.

__tests__/LaunchDarklyProvider.test.ts

Lines changed: 65 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,85 @@
1-
import { OpenFeature, Client, ErrorCode } from '@openfeature/js-sdk';
2-
import { LDClient } from '@launchdarkly/node-server-sdk';
1+
import {
2+
OpenFeature, Client, ErrorCode, ProviderStatus, ProviderEvents,
3+
} from '@openfeature/server-sdk';
4+
import {
5+
LDClient, LDClientContext, integrations,
6+
} from '@launchdarkly/node-server-sdk';
37
import { LaunchDarklyProvider } from '../src';
48
import translateContext from '../src/translateContext';
59
import TestLogger from './TestLogger';
610

711
const basicContext = { targetingKey: 'the-key' };
812
const testFlagKey = 'a-key';
913

14+
it('can be initialized', async () => {
15+
const ldProvider = new LaunchDarklyProvider('sdk-key', { offline: true });
16+
await ldProvider.initialize({});
17+
18+
expect(ldProvider.status).toEqual(ProviderStatus.READY);
19+
await ldProvider.onClose();
20+
});
21+
22+
it('can fail to initialize client', async () => {
23+
const ldProvider = new LaunchDarklyProvider('sdk-key', {
24+
updateProcessor: (
25+
clientContext: LDClientContext,
26+
dataSourceUpdates: any,
27+
initSuccessHandler: VoidFunction,
28+
errorHandler?: (e: Error) => void,
29+
) => ({
30+
start: () => {
31+
setTimeout(() => errorHandler?.({ code: 401 } as any), 20);
32+
},
33+
}),
34+
sendEvents: false,
35+
});
36+
try {
37+
await ldProvider.initialize({});
38+
} catch (e) {
39+
expect((e as Error).message).toEqual('Authentication failed. Double check your SDK key.');
40+
}
41+
expect(ldProvider.status).toEqual(ProviderStatus.ERROR);
42+
});
43+
44+
it('emits events for flag changes', async () => {
45+
const td = new integrations.TestData();
46+
const ldProvider = new LaunchDarklyProvider('sdk-key', {
47+
updateProcessor: td.getFactory(),
48+
sendEvents: false,
49+
});
50+
let count = 0;
51+
ldProvider.events.addHandler(ProviderEvents.ConfigurationChanged, (eventDetail) => {
52+
expect(eventDetail?.flagsChanged).toEqual(['flagA']);
53+
count += 1;
54+
});
55+
td.update(td.flag('flagA').valueForAll('B'));
56+
expect(await ldProvider.getClient()
57+
.stringVariation('flagA', { key: 'test-key' }, 'A'))
58+
.toEqual('B');
59+
expect(count).toEqual(1);
60+
await ldProvider.onClose();
61+
});
62+
1063
describe('given a mock LaunchDarkly client', () => {
1164
let ldClient: LDClient;
1265
let ofClient: Client;
66+
let ldProvider: LaunchDarklyProvider;
1367
const logger: TestLogger = new TestLogger();
1468

1569
beforeEach(() => {
16-
ldClient = {
17-
variationDetail: jest.fn(),
18-
} as any;
19-
OpenFeature.setProvider(new LaunchDarklyProvider(ldClient, { logger }));
70+
ldProvider = new LaunchDarklyProvider('sdk-key', { logger, offline: true });
71+
ldClient = ldProvider.getClient();
72+
OpenFeature.setProvider(ldProvider);
73+
2074
ofClient = OpenFeature.getClient();
2175
logger.reset();
2276
});
2377

78+
afterEach(async () => {
79+
await ldProvider.onClose();
80+
jest.restoreAllMocks();
81+
});
82+
2483
it('calls the client correctly for boolean variations', async () => {
2584
ldClient.variationDetail = jest.fn(async () => ({
2685
value: true,

package.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,11 @@
2020
],
2121
"license": "Apache-2.0",
2222
"peerDependencies": {
23-
"@openfeature/js-sdk": "^1.0.0",
24-
"@launchdarkly/node-server-sdk": "8.x"
23+
"@openfeature/server-sdk": "^1.6.3",
24+
"@launchdarkly/node-server-sdk": "9.x"
2525
},
2626
"devDependencies": {
27-
"@openfeature/js-sdk": "^1.0.0",
27+
"@openfeature/server-sdk": "^1.6.3",
2828
"@types/jest": "^27.4.1",
2929
"@typescript-eslint/eslint-plugin": "^5.22.0",
3030
"@typescript-eslint/parser": "^5.22.0",
@@ -34,7 +34,7 @@
3434
"eslint-plugin-import": "^2.26.0",
3535
"jest": "^27.5.1",
3636
"jest-junit": "^14.0.1",
37-
"@launchdarkly/node-server-sdk": "8.x",
37+
"@launchdarkly/node-server-sdk": "9.x",
3838
"ts-jest": "^27.1.4",
3939
"typescript": "^4.7.4"
4040
}

src/LaunchDarklyProvider.ts

Lines changed: 77 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,20 @@ import {
22
ErrorCode,
33
EvaluationContext, FlagValue, Hook,
44
JsonValue,
5-
Provider, ProviderMetadata, ResolutionDetails, StandardResolutionReasons,
6-
} from '@openfeature/js-sdk';
5+
OpenFeatureEventEmitter,
6+
Provider,
7+
ProviderEvents,
8+
ProviderMetadata,
9+
ProviderStatus,
10+
ResolutionDetails,
11+
StandardResolutionReasons,
12+
} from '@openfeature/server-sdk';
713
import {
8-
basicLogger, LDClient, LDLogger,
14+
basicLogger, init, LDClient, LDLogger, LDOptions,
915
} from '@launchdarkly/node-server-sdk';
10-
import { LaunchDarklyProviderOptions } from './LaunchDarklyProviderOptions';
1116
import translateContext from './translateContext';
1217
import translateResult from './translateResult';
18+
import SafeLogger from './SafeLogger';
1319

1420
/**
1521
* Create a ResolutionDetails for an evaluation that produced a type different
@@ -31,20 +37,67 @@ function wrongTypeResult<T>(value: T): ResolutionDetails<T> {
3137
export default class LaunchDarklyProvider implements Provider {
3238
private readonly logger: LDLogger;
3339

40+
private readonly client: LDClient;
41+
42+
private readonly clientConstructionError: any;
43+
3444
readonly metadata: ProviderMetadata = {
3545
name: 'launchdarkly-node-provider',
3646
};
3747

48+
private innerStatus: ProviderStatus = ProviderStatus.NOT_READY;
49+
50+
public readonly events = new OpenFeatureEventEmitter();
51+
52+
/**
53+
* Get the status of the LaunchDarkly provider.
54+
*/
55+
public get status() {
56+
return this.innerStatus;
57+
}
58+
3859
/**
3960
* Construct a {@link LaunchDarklyProvider}.
4061
* @param client The LaunchDarkly client instance to use.
4162
*/
42-
constructor(private readonly client: LDClient, options: LaunchDarklyProviderOptions = {}) {
63+
constructor(sdkKey: string, options: LDOptions = {}) {
4364
if (options.logger) {
44-
this.logger = options.logger;
65+
this.logger = new SafeLogger(options.logger, basicLogger({ level: 'info' }));
4566
} else {
4667
this.logger = basicLogger({ level: 'info' });
4768
}
69+
try {
70+
this.client = init(sdkKey, {
71+
...options,
72+
wrapperName: 'open-feature/node-server',
73+
// The wrapper version should be kept on its own line to allow easy updates using
74+
// release-please.
75+
wrapperVersion: '0.4.0', // x-release-please-version
76+
});
77+
this.client.on('update', ({ key }: { key: string }) => this.events.emit(ProviderEvents.ConfigurationChanged, { flagsChanged: [key] }));
78+
} catch (e) {
79+
this.clientConstructionError = e;
80+
this.logger.error(`Encountered unrecoverable initialization error, ${e}`);
81+
this.innerStatus = ProviderStatus.ERROR;
82+
}
83+
}
84+
85+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
86+
async initialize(context?: EvaluationContext): Promise<void> {
87+
if (!this.client) {
88+
// The client could not be constructed.
89+
if (this.clientConstructionError) {
90+
throw this.clientConstructionError;
91+
}
92+
throw new Error('Unknown problem encountered during initialization');
93+
}
94+
try {
95+
await this.client.waitForInitialization();
96+
this.innerStatus = ProviderStatus.READY;
97+
} catch (e) {
98+
this.innerStatus = ProviderStatus.ERROR;
99+
throw e;
100+
}
48101
}
49102

50103
/**
@@ -169,4 +222,22 @@ export default class LaunchDarklyProvider implements Provider {
169222
private translateContext(context: EvaluationContext) {
170223
return translateContext(this.logger, context);
171224
}
225+
226+
/**
227+
* Get the LDClient instance used by this provider.
228+
*
229+
* @returns The client for this provider.
230+
*/
231+
public getClient(): LDClient {
232+
return this.client;
233+
}
234+
235+
/**
236+
* Called by OpenFeature when it needs to close the provider. This will flush
237+
* events from the LDClient and then close it.
238+
*/
239+
async onClose(): Promise<void> {
240+
await this.client.flush();
241+
this.client.close();
242+
}
172243
}

src/LaunchDarklyProviderOptions.ts

Lines changed: 0 additions & 23 deletions
This file was deleted.

src/translateContext.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { EvaluationContext, EvaluationContextValue } from '@openfeature/js-sdk';
1+
import { EvaluationContext, EvaluationContextValue } from '@openfeature/server-sdk';
22
import {
33
LDContext, LDContextCommon, LDLogger, LDSingleKindContext,
44
} from '@launchdarkly/node-server-sdk';

src/translateResult.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ErrorCode, ResolutionDetails } from '@openfeature/js-sdk';
1+
import { ErrorCode, ResolutionDetails } from '@openfeature/server-sdk';
22
import { LDEvaluationDetail } from '@launchdarkly/node-server-sdk';
33

44
/**

0 commit comments

Comments
 (0)