Skip to content

Commit f485d43

Browse files
Merge pull request #315 from splitio/add_getOptions
Add `getOptions` method
2 parents b97a5fe + c28c94f commit f485d43

File tree

10 files changed

+69
-42
lines changed

10 files changed

+69
-42
lines changed

CHANGES.txt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
1.15.1 (May 28, 2024)
1+
1.16.0 (June 13, 2024)
2+
- Added the `getOptions` method to the `IPlatform` interface to allow the SDK to pass request options to the `fetch` function and `EventSource` constructor when fetching data from the Split servers. The method is optional and, if provided, it is called twice: first for the `fetch` options and then for the `EventSource` options.
3+
Useful for advanced use cases like configuring a proxy or validating certificates in NodeJS.
24
- Updated the Redis storage to lazily import the `ioredis` dependency when the storage is created. This prevents errors when the SDK is imported or bundled in a .mjs file, as `ioredis` is a CommonJS module.
35
- Bugfixing - Restored some input validation error logs that were removed in version 1.12.0. The logs inform the user when the `getTreatment(s)` methods are called with an invalid value as feature flag name or flag set name.
46
- Bugfixing - Fixed localhost mode for client-side SDKs to emit SDK_UPDATE when mocked feature flags are updated in the `features` property of the config object (Related to issue https://github.com/splitio/javascript-browser-client/issues/119).

package-lock.json

Lines changed: 16 additions & 16 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@splitsoftware/splitio-commons",
3-
"version": "1.15.1-rc.1",
3+
"version": "1.15.1-rc.3",
44
"description": "Split JavaScript SDK common components",
55
"main": "cjs/index.js",
66
"module": "esm/index.js",

src/sdkFactory/__tests__/index.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ const fullParamsForSyncSDK = {
4949
platform: {
5050
getEventSource: jest.fn(),
5151
getFetch: jest.fn(),
52+
getOptions: jest.fn(),
5253
EventEmitter
5354
},
5455
splitApiFactory: jest.fn(),

src/sdkFactory/types.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,15 @@ export interface IPlatform {
1717
/**
1818
* If provided, it is used to retrieve the Fetch API for HTTP requests. Otherwise, the global fetch is used.
1919
*/
20-
getFetch?: () => (IFetch | undefined)
20+
getFetch?: (settings: ISettings) => (IFetch | undefined)
21+
/**
22+
* If provided, it is used to pass additional options to fetch and eventsource calls.
23+
*/
24+
getOptions?: (settings: ISettings) => object
2125
/**
2226
* If provided, it is used to retrieve the EventSource constructor for streaming support.
2327
*/
24-
getEventSource?: () => (IEventSourceConstructor | undefined)
28+
getEventSource?: (settings: ISettings) => (IEventSourceConstructor | undefined)
2529
/**
2630
* EventEmitter constructor, like NodeJS.EventEmitter or a polyfill.
2731
*/

src/services/splitApi.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,15 @@ function userKeyToQueryParam(userKey: string) {
2222
*/
2323
export function splitApiFactory(
2424
settings: ISettings,
25-
platform: Pick<IPlatform, 'getFetch'>,
25+
platform: IPlatform,
2626
telemetryTracker: ITelemetryTracker
2727
): ISplitApi {
2828

2929
const urls = settings.urls;
3030
const filterQueryString = settings.sync.__splitFiltersValidation && settings.sync.__splitFiltersValidation.queryString;
3131
const SplitSDKImpressionsMode = settings.sync.impressionsMode;
3232
const flagSpecVersion = settings.sync.flagSpecVersion;
33-
const splitHttpClient = splitHttpClientFactory(settings, platform.getFetch);
33+
const splitHttpClient = splitHttpClientFactory(settings, platform);
3434

3535
return {
3636
// @TODO throw errors if health check requests fail, to log them in the Synchronizer

src/services/splitHttpClient.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,13 @@ const messageNoFetch = 'Global fetch API is not available.';
1010
* Factory of Split HTTP clients, which are HTTP clients with predefined headers for Split endpoints.
1111
*
1212
* @param settings SDK settings, used to access authorizationKey, logger instance and metadata (SDK version, ip and hostname) to set additional headers
13-
* @param getFetch retrieves the Fetch API for HTTP requests
13+
* @param platform object containing environment-specific dependencies
1414
*/
15-
export function splitHttpClientFactory(settings: ISettings, getFetch?: IPlatform['getFetch']): ISplitHttpClient {
15+
export function splitHttpClientFactory(settings: ISettings, { getOptions, getFetch }: IPlatform): ISplitHttpClient {
1616

1717
const { log, core: { authorizationKey }, version, runtime: { ip, hostname } } = settings;
18-
const fetch = getFetch && getFetch();
18+
const options = getOptions && getOptions(settings);
19+
const fetch = getFetch && getFetch(settings);
1920

2021
// if fetch is not available, log Error
2122
if (!fetch) log.error(ERROR_CLIENT_CANNOT_GET_READY, [messageNoFetch]);
@@ -32,11 +33,11 @@ export function splitHttpClientFactory(settings: ISettings, getFetch?: IPlatform
3233

3334
return function httpClient(url: string, reqOpts: IRequestOptions = {}, latencyTracker: (error?: NetworkError) => void = () => { }, logErrorsAsInfo: boolean = false): Promise<IResponse> {
3435

35-
const request = {
36+
const request = objectAssign({
3637
headers: reqOpts.headers ? objectAssign({}, headers, reqOpts.headers) : headers,
3738
method: reqOpts.method || 'GET',
3839
body: reqOpts.body
39-
};
40+
}, options);
4041

4142
// using `fetch(url, options)` signature to work with unfetch, a lightweight ponyfill of fetch API.
4243
return fetch ? fetch(url, request)

src/sync/streaming/SSEClient/__tests__/index.spec.ts

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,12 @@ const EXPECTED_HEADERS = {
1818

1919
test('SSClient / instance creation throws error if EventSource is not provided', () => {
2020
expect(() => { new SSEClient(settings); }).toThrow(Error);
21-
expect(() => { new SSEClient(settings, false, () => undefined); }).toThrow(Error);
21+
expect(() => { new SSEClient(settings, false, {}); }).toThrow(Error);
22+
expect(() => { new SSEClient(settings, false, { getEventSource: () => undefined }); }).toThrow(Error);
2223
});
2324

2425
test('SSClient / instance creation success if EventSource is provided', () => {
25-
const instance = new SSEClient(settings, false, () => EventSourceMock);
26+
const instance = new SSEClient(settings, false, { getEventSource: () => EventSourceMock });
2627
expect(instance.eventSource).toBe(EventSourceMock);
2728
});
2829

@@ -35,7 +36,7 @@ test('SSClient / setEventHandler, open and close methods', () => {
3536
};
3637

3738
// instance SSEClient
38-
const instance = new SSEClient(settings, false, () => EventSourceMock);
39+
const instance = new SSEClient(settings, false, { getEventSource: () => EventSourceMock });
3940
instance.setEventHandler(handler);
4041

4142
// open connection
@@ -81,13 +82,13 @@ test('SSClient / setEventHandler, open and close methods', () => {
8182

8283
test('SSClient / open method: URL with metadata query params', () => {
8384

84-
const instance = new SSEClient(settings, false, () => EventSourceMock);
85+
const instance = new SSEClient(settings, false, { getEventSource: () => EventSourceMock });
8586
instance.open(authDataSample);
8687

8788
const EXPECTED_BROWSER_URL = EXPECTED_URL + `&SplitSDKVersion=${settings.version}&SplitSDKClientKey=${EXPECTED_HEADERS.SplitSDKClientKey}`;
8889

8990
expect(instance.connection.url).toBe(EXPECTED_BROWSER_URL); // URL is properly set for streaming connection
90-
expect(instance.connection.__eventSourceInitDict).toBe(undefined); // No headers are passed for streaming connection
91+
expect(instance.connection.__eventSourceInitDict).toEqual({}); // No headers are passed for streaming connection
9192
});
9293

9394
test('SSClient / open method: URL and metadata headers with IP and Hostname', () => {
@@ -99,7 +100,7 @@ test('SSClient / open method: URL and metadata headers with IP and Hostname', ()
99100
hostname: 'some hostname'
100101
}
101102
};
102-
const instance = new SSEClient(settingsWithRuntime, true, () => EventSourceMock);
103+
const instance = new SSEClient(settingsWithRuntime, true, { getEventSource: () => EventSourceMock });
103104
instance.open(authDataSample);
104105

105106
expect(instance.connection.url).toBe(EXPECTED_URL); // URL is properly set for streaming connection
@@ -114,9 +115,23 @@ test('SSClient / open method: URL and metadata headers with IP and Hostname', ()
114115

115116
test('SSClient / open method: URL and metadata headers without IP and Hostname', () => {
116117

117-
const instance = new SSEClient(settings, true, () => EventSourceMock);
118+
const instance = new SSEClient(settings, true, { getEventSource: () => EventSourceMock });
118119
instance.open(authDataSample);
119120

120121
expect(instance.connection.url).toBe(EXPECTED_URL); // URL is properly set for streaming connection
121122
expect(instance.connection.__eventSourceInitDict).toEqual({ headers: EXPECTED_HEADERS }); // Headers are properly set for streaming connection
122123
});
124+
125+
test('SSClient / open method: URL, metadata headers and options', () => {
126+
const platform = { getEventSource: jest.fn(() => EventSourceMock), getOptions: jest.fn(() => ({ withCredentials: true })) };
127+
128+
const instance = new SSEClient(settings, true, platform);
129+
instance.open(authDataSample);
130+
131+
expect(instance.connection.url).toBe(EXPECTED_URL); // URL is properly set for streaming connection
132+
expect(instance.connection.__eventSourceInitDict).toEqual({ headers: EXPECTED_HEADERS, withCredentials: true }); // Headers and options are properly set for streaming connection
133+
134+
// Assert that getEventSource and getOptions were called once with settings
135+
expect(platform.getEventSource.mock.calls).toEqual([[settings]]);
136+
expect(platform.getOptions.mock.calls).toEqual([[settings]]);
137+
});

src/sync/streaming/SSEClient/index.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
import { IPlatform } from '../../../sdkFactory/types';
12
import { IEventSourceConstructor } from '../../../services/types';
23
import { ISettings } from '../../../types';
34
import { isString } from '../../../utils/lang';
5+
import { objectAssign } from '../../../utils/lang/objectAssign';
46
import { IAuthTokenPushEnabled } from '../AuthClient/types';
57
import { ISSEClient, ISseEventHandler } from './types';
68

@@ -39,24 +41,26 @@ export class SSEClient implements ISSEClient {
3941
handler?: ISseEventHandler;
4042
useHeaders?: boolean;
4143
headers: Record<string, string>;
44+
options?: object;
4245

4346
/**
4447
* SSEClient constructor.
4548
*
4649
* @param settings Validated settings.
4750
* @param useHeaders True to send metadata as headers or false to send as query params. If `true`, the provided EventSource must support headers.
48-
* @param getEventSource Function to get the EventSource constructor.
49-
* @throws 'EventSource API is not available. ' if EventSource is not available.
51+
* @param platform object containing environment-specific dependencies
52+
* @throws 'EventSource API is not available.' if EventSource is not available.
5053
*/
51-
constructor(settings: ISettings, useHeaders?: boolean, getEventSource?: () => (IEventSourceConstructor | undefined)) {
52-
this.eventSource = getEventSource && getEventSource();
54+
constructor(settings: ISettings, useHeaders: boolean, { getEventSource, getOptions }: IPlatform) {
55+
this.eventSource = getEventSource && getEventSource(settings);
5356
// if eventSource is not available, throw an exception
54-
if (!this.eventSource) throw new Error('EventSource API is not available. ');
57+
if (!this.eventSource) throw new Error('EventSource API is not available.');
5558

5659
this.streamingUrl = settings.urls.streaming + '/sse';
5760
// @TODO get `useHeaders` flag from `getEventSource`, to use EventSource headers on client-side SDKs when possible.
5861
this.useHeaders = useHeaders;
5962
this.headers = buildSSEHeaders(settings);
63+
this.options = getOptions && getOptions(settings);
6064
}
6165

6266
setEventHandler(handler: ISseEventHandler) {
@@ -84,8 +88,8 @@ export class SSEClient implements ISSEClient {
8488
// For client-side SDKs, SplitSDKClientKey and SplitSDKClientKey metadata is passed as query params,
8589
// because native EventSource implementations for browser doesn't support headers.
8690
this.useHeaders ? url : url + `&SplitSDKVersion=${this.headers.SplitSDKVersion}&SplitSDKClientKey=${this.headers.SplitSDKClientKey}`,
87-
// @ts-ignore. For server-side SDKs, metadata is passed via headers. EventSource must support headers, like 'eventsource' package for Node.
88-
this.useHeaders ? { headers: this.headers } : undefined
91+
// For server-side SDKs, metadata is passed via headers. EventSource must support headers, like 'eventsource' package for Node.
92+
objectAssign(this.useHeaders ? { headers: this.headers } : {}, this.options)
8993
);
9094

9195
if (this.handler) { // no need to check if SSEClient is used only by PushManager

src/sync/streaming/pushManager.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ export function pushManagerFactory(
4242
let sseClient: ISSEClient;
4343
try {
4444
// `useHeaders` false for client-side, even if the platform EventSource supports headers (e.g., React Native).
45-
sseClient = new SSEClient(settings, userKey ? false : true, platform.getEventSource);
45+
sseClient = new SSEClient(settings, userKey ? false : true, platform);
4646
} catch (e) {
4747
log.warn(STREAMING_FALLBACK, [e]);
4848
return;

0 commit comments

Comments
 (0)