Skip to content

Commit ae4de36

Browse files
committed
feat: Add a module for increased backward compatibility.
1 parent ae2b5bb commit ae4de36

File tree

14 files changed

+769
-29
lines changed

14 files changed

+769
-29
lines changed
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
import { jest } from '@jest/globals';
2+
3+
import { LDContext, LDFlagSet, LDLogger } from '@launchdarkly/js-client-sdk-common';
4+
5+
const mockBrowserClient = {
6+
identify: jest.fn(),
7+
allFlags: jest.fn(),
8+
close: jest.fn(),
9+
flush: jest.fn(),
10+
_emitter: jest.fn(() => ({
11+
emit: jest.fn(),
12+
on: jest.fn(),
13+
off: jest.fn(),
14+
})),
15+
};
16+
17+
jest.unstable_mockModule('../../src/BrowserClient', () => ({
18+
__esModule: true,
19+
BrowserClient: jest.fn(),
20+
}));
21+
22+
const { default: LDClientCompatImpl } = await import('../../src/compat/LDClientCompatImpl');
23+
24+
describe('given a LDClientCompatImpl client with mocked browser client', () => {
25+
// @ts-ignore
26+
let client: LDClientCompatImpl;
27+
// let mockBrowserClient: jest.Mocked<BrowserClient>;
28+
let mockLogger: LDLogger;
29+
30+
beforeEach(() => {
31+
// mockBrowserClient = {
32+
// identify: jest.fn(),
33+
// allFlags: jest.fn(),
34+
// close: jest.fn(),
35+
// flush: jest.fn(),
36+
// } as unknown as jest.Mocked<BrowserClient>;
37+
38+
// (BrowserClient as jest.MockedClass<typeof BrowserClient>).mockImplementation(
39+
// () => mockBrowserClient,
40+
// );
41+
mockLogger = {
42+
error: jest.fn(),
43+
warn: jest.fn(),
44+
info: jest.fn(),
45+
debug: jest.fn(),
46+
};
47+
client = new LDClientCompatImpl(
48+
'env-key',
49+
{ kind: 'user', key: 'user-key' },
50+
{ logger: mockLogger },
51+
);
52+
});
53+
54+
it('should return a promise from identify when no callback is provided', async () => {
55+
const context: LDContext = { kind: 'user', key: 'new-user' };
56+
const mockFlags: LDFlagSet = { flag1: true, flag2: false };
57+
// @ts-ignore
58+
mockBrowserClient.identify.mockResolvedValue(undefined);
59+
mockBrowserClient.allFlags.mockReturnValue(mockFlags);
60+
61+
const result = await client.identify(context);
62+
63+
expect(mockBrowserClient.identify).toHaveBeenCalledWith(context, { hash: undefined });
64+
expect(result).toEqual(mockFlags);
65+
});
66+
67+
it('should call the callback when provided to identify', (done) => {
68+
const context: LDContext = { kind: 'user', key: 'new-user' };
69+
const mockFlags: LDFlagSet = { flag1: true, flag2: false };
70+
// @ts-ignore
71+
mockBrowserClient.identify.mockResolvedValue(undefined);
72+
mockBrowserClient.allFlags.mockReturnValue(mockFlags);
73+
74+
// @ts-ignore
75+
client.identify(context, undefined, (err, flags) => {
76+
expect(err).toBeNull();
77+
expect(flags).toEqual(mockFlags);
78+
done();
79+
});
80+
});
81+
82+
it('should return a promise from close when no callback is provided', async () => {
83+
// @ts-ignore
84+
mockBrowserClient.close.mockResolvedValue();
85+
86+
await expect(client.close()).resolves.toBeUndefined();
87+
expect(mockBrowserClient.close).toHaveBeenCalled();
88+
});
89+
90+
it('should call the callback when provided to close', (done) => {
91+
// @ts-ignore
92+
mockBrowserClient.close.mockResolvedValue();
93+
94+
client.close(() => {
95+
expect(mockBrowserClient.close).toHaveBeenCalled();
96+
done();
97+
});
98+
});
99+
100+
it('should return a promise from flush when no callback is provided', async () => {
101+
// @ts-ignore
102+
mockBrowserClient.flush.mockResolvedValue({ result: true });
103+
104+
await expect(client.flush()).resolves.toBeUndefined();
105+
expect(mockBrowserClient.flush).toHaveBeenCalled();
106+
});
107+
108+
it('should call the callback when provided to flush', (done) => {
109+
// @ts-ignore
110+
mockBrowserClient.flush.mockResolvedValue({ result: true });
111+
112+
client.flush(() => {
113+
expect(mockBrowserClient.flush).toHaveBeenCalled();
114+
done();
115+
});
116+
});
117+
118+
// it('should resolve immediately if the client is already initialized', async () => {
119+
// mockBrowserClient.waitForInitialization.mockResolvedValue(undefined);
120+
121+
// await expect(client.waitForInitialization()).resolves.toBeUndefined();
122+
// expect(mockBrowserClient.waitForInitialization).toHaveBeenCalledWith({ noTimeout: true });
123+
// });
124+
125+
// it('should log a warning when no timeout is specified for waitForInitialization', async () => {
126+
// mockBrowserClient.waitForInitialization.mockResolvedValue(undefined);
127+
128+
// await client.waitForInitialization();
129+
130+
// expect(mockLogger.warn).toHaveBeenCalledWith(
131+
// expect.stringContaining('The waitForInitialization function was called without a timeout specified.')
132+
// );
133+
// });
134+
135+
// it('should apply a timeout when specified for waitForInitialization', async () => {
136+
// mockBrowserClient.waitForInitialization.mockResolvedValue(undefined);
137+
138+
// await client.waitForInitialization(5);
139+
140+
// expect(mockBrowserClient.waitForInitialization).toHaveBeenCalledWith({ timeout: 5 });
141+
// });
142+
143+
// it('should reject with a timeout error when initialization takes too long', async () => {
144+
// mockBrowserClient.waitForInitialization.mockRejectedValue(new Error('Timeout'));
145+
146+
// await expect(client.waitForInitialization(1)).rejects.toThrow('Timeout');
147+
// expect(mockLogger.error).toHaveBeenCalledWith(expect.stringContaining('waitForInitialization timed out'));
148+
// });
149+
});

packages/sdk/browser/jest.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@ export default {
55
testEnvironment: 'jest-environment-jsdom',
66
testPathIgnorePatterns: ['./dist', './src'],
77
testMatch: ['**.test.ts'],
8+
setupFiles: ['./setup-jest.js'],
89
};

packages/sdk/browser/package.json

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,16 @@
1717
"sdk"
1818
],
1919
"exports": {
20-
"types": "./dist/src/index.d.ts",
21-
"require": "./dist/index.cjs.js",
22-
"import": "./dist/index.es.js"
20+
".": {
21+
"types": "./dist/src/index.d.ts",
22+
"require": "./dist/index.cjs.js",
23+
"import": "./dist/index.es.js"
24+
},
25+
"./compat": {
26+
"types": "./dist/src/compat/index.d.ts",
27+
"require": "./dist/compat.cjs.js",
28+
"import": "./dist/compat.es.js"
29+
}
2330
},
2431
"type": "module",
2532
"files": [

packages/sdk/browser/rollup.config.js

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,17 @@ import terser from '@rollup/plugin-terser';
55
import typescript from '@rollup/plugin-typescript';
66
import { visualizer } from 'rollup-plugin-visualizer';
77

8-
const getSharedConfig = (format, file) => ({
9-
input: 'src/index.ts',
10-
output: [
11-
{
12-
format: format,
13-
sourcemap: true,
14-
file: file,
15-
},
16-
],
17-
onwarn: (warning) => {
18-
if (warning.code !== 'CIRCULAR_DEPENDENCY') {
19-
console.error(`(!) ${warning.message}`);
20-
}
8+
const getSharedConfig = (format, extension) => ({
9+
input: {
10+
index: 'src/index.ts',
11+
compat: 'src/compat/index.ts'
12+
},
13+
output:
14+
{
15+
format: format,
16+
sourcemap: true,
17+
dir: 'dist',
18+
entryFileNames: '[name]' + extension
2119
},
2220
});
2321

@@ -35,7 +33,7 @@ const terserOpts = {
3533

3634
export default [
3735
{
38-
...getSharedConfig('es', 'dist/index.es.js'),
36+
...getSharedConfig('es', '.es.js'),
3937
plugins: [
4038
typescript({
4139
module: 'esnext',
@@ -52,7 +50,7 @@ export default [
5250
],
5351
},
5452
{
55-
...getSharedConfig('cjs', 'dist/index.cjs.js'),
53+
...getSharedConfig('cjs', '.cjs.js'),
5654
plugins: [typescript(), common(), resolve(), terser(terserOpts), json()],
5755
},
5856
];

packages/sdk/browser/setup-jest.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { TextEncoder, TextDecoder } from 'node:util';
2+
import * as crypto from 'crypto';
3+
4+
global.TextEncoder = TextEncoder;
5+
6+
Object.assign(window, { TextDecoder, TextEncoder });
7+
8+
Object.defineProperty(global.self, "crypto", {
9+
value: {
10+
getRandomValues: (arr) => crypto.randomBytes(arr.length),
11+
subtle: {
12+
digest: (algorithm, data) => {
13+
return new Promise((resolve, reject) =>
14+
resolve(
15+
crypto.createHash(algorithm.toLowerCase().replace("-", ""))
16+
.update(data)
17+
.digest()
18+
)
19+
);
20+
},
21+
},
22+
},
23+
});
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import { LDContext, LDFlagSet } from '@launchdarkly/js-client-sdk-common';
2+
import { LDClient as LDCLientBrowser } from '../BrowserClient';
3+
4+
/**
5+
* Compatibility interface. This interface extends the base LDCLient interface with functions
6+
* that improve backwards compatibility.
7+
*
8+
* If starting a new project please import the root package instead of `/compat`.
9+
*
10+
* In the `[email protected]` package a number of functions had the return typings
11+
* incorrect. Any function which optionally returned a promise based on a callback had incorrect
12+
* typings. Those have been corrected in this implementation.
13+
*/
14+
export interface LDClient extends Omit<LDCLientBrowser, 'close' | 'flush' | 'identify'> {
15+
/**
16+
* Identifies a context to LaunchDarkly.
17+
*
18+
* Unlike the server-side SDKs, the client-side JavaScript SDKs maintain a current context state,
19+
* which is set at initialization time. You only need to call `identify()` if the context has changed
20+
* since then.
21+
*
22+
* Changing the current context also causes all feature flag values to be reloaded. Until that has
23+
* finished, calls to {@link variation} will still return flag values for the previous context. You can
24+
* use a callback or a Promise to determine when the new flag values are available.
25+
*
26+
* @param context
27+
* The context properties. Must contain at least the `key` property.
28+
* @param hash
29+
* The signed context key if you are using [Secure Mode](https://docs.launchdarkly.com/sdk/features/secure-mode#configuring-secure-mode-in-the-javascript-client-side-sdk).
30+
* @param onDone
31+
* A function which will be called as soon as the flag values for the new context are available,
32+
* with two parameters: an error value (if any), and an {@link LDFlagSet} containing the new values
33+
* (which can also be obtained by calling {@link variation}). If the callback is omitted, you will
34+
* receive a Promise instead.
35+
* @returns
36+
* If you provided a callback, then nothing. Otherwise, a Promise which resolve once the flag
37+
* values for the new context are available, providing an {@link LDFlagSet} containing the new values
38+
* (which can also be obtained by calling {@link variation}).
39+
*/
40+
identify(
41+
context: LDContext,
42+
hash?: string,
43+
onDone?: (err: Error | null, flags: LDFlagSet | null) => void
44+
): Promise<LDFlagSet> | undefined;
45+
46+
/**
47+
* Returns a Promise that tracks the client's initialization state.
48+
*
49+
* The Promise will be resolved if the client successfully initializes, or rejected if client
50+
* initialization has irrevocably failed (for instance, if it detects that the SDK key is invalid).
51+
*
52+
* ```
53+
* // using Promise then() and catch() handlers
54+
* client.waitForInitialization(5).then(() => {
55+
* doSomethingWithSuccessfullyInitializedClient();
56+
* }).catch(err => {
57+
* doSomethingForFailedStartup(err);
58+
* });
59+
*
60+
* // using async/await
61+
* try {
62+
* await client.waitForInitialization(5);
63+
* doSomethingWithSuccessfullyInitializedClient();
64+
* } catch (err) {
65+
* doSomethingForFailedStartup(err);
66+
* }
67+
* ```
68+
*
69+
* It is important that you handle the rejection case; otherwise it will become an unhandled Promise
70+
* rejection, which is a serious error on some platforms. The Promise is not created unless you
71+
* request it, so if you never call `waitForInitialization()` then you do not have to worry about
72+
* unhandled rejections.
73+
*
74+
* Note that you can also use event listeners ({@link on}) for the same purpose: the event `"initialized"`
75+
* indicates success, and `"failed"` indicates failure.
76+
*
77+
* @param timeout
78+
* The amount of time, in seconds, to wait for initialization before rejecting the promise.
79+
* Using a large timeout is not recommended. If you use a large timeout and await it, then
80+
* any network delays will cause your application to wait a long time before
81+
* continuing execution.
82+
*
83+
* If no timeout is specified, then the returned promise will only be resolved when the client
84+
* successfully initializes or initialization fails.
85+
*
86+
* @returns
87+
* A Promise that will be resolved if the client initializes successfully, or rejected if it
88+
* fails or the specified timeout elapses.
89+
*/
90+
waitForInitialization(timeout?: number): Promise<void>;
91+
92+
/**
93+
* Shuts down the client and releases its resources, after delivering any pending analytics
94+
* events.
95+
*
96+
* @param onDone
97+
* A function which will be called when the operation completes. If omitted, you
98+
* will receive a Promise instead.
99+
*
100+
* @returns
101+
* If you provided a callback, then nothing. Otherwise, a Promise which resolves once
102+
* closing is finished. It will never be rejected.
103+
*/
104+
close(onDone?: () => void): Promise<void> | undefined;
105+
106+
/**
107+
* Flushes all pending analytics events.
108+
*
109+
* Normally, batches of events are delivered in the background at intervals determined by the
110+
* `flushInterval` property of {@link LDOptions}. Calling `flush()` triggers an immediate delivery.
111+
*
112+
* @param onDone
113+
* A function which will be called when the flush completes. If omitted, you
114+
* will receive a Promise instead.
115+
*
116+
* @returns
117+
* If you provided a callback, then nothing. Otherwise, a Promise which resolves once
118+
* flushing is finished. Note that the Promise will be rejected if the HTTP request
119+
* fails, so be sure to attach a rejection handler to it.
120+
*/
121+
flush(onDone?: () => void): Promise<void> | undefined;
122+
}

0 commit comments

Comments
 (0)