Skip to content

Commit 526f718

Browse files
Merge pull request #314 from splitio/update_imports_to_support_mjs
Update internal import of `ioredis` to support bundling the SDK into .mjs files
2 parents e5eca00 + 5b3cbeb commit 526f718

File tree

14 files changed

+132
-48
lines changed

14 files changed

+132
-48
lines changed

CHANGES.txt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
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. Useful for advanced use cases like configuring a proxy or validating HTTPS certificates in NodeJS.
3+
- 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.
24
- 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.
5+
- Bugfixing - Fixed localhost mode to emit SDK_UPDATE when mocked feature flags are updated in the `features` object map of the config object (Related to issue https://github.com/splitio/javascript-browser-client/issues/119).
36

47
1.15.0 (May 13, 2024)
58
- Added an optional settings validation parameter to let overwrite the default flag spec version, used by the JS Synchronizer.

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.0",
3+
"version": "1.16.0",
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/storages/inRedis/index.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { RedisAdapter } from './RedisAdapter';
1+
import type { RedisAdapter } from './RedisAdapter';
22
import { IStorageAsync, IStorageAsyncFactory, IStorageFactoryParams } from '../types';
33
import { validatePrefix } from '../KeyBuilder';
44
import { KeyBuilderSS } from '../KeyBuilderSS';
@@ -23,13 +23,17 @@ export interface InRedisStorageOptions {
2323
*/
2424
export function InRedisStorage(options: InRedisStorageOptions = {}): IStorageAsyncFactory {
2525

26+
// Lazy loading to prevent error when bundling or importing the SDK in a .mjs file, since ioredis is a CommonJS module.
27+
// Redis storage is not supported with .mjs files.
28+
const RD = require('./RedisAdapter').RedisAdapter;
29+
2630
const prefix = validatePrefix(options.prefix);
2731

2832
function InRedisStorageFactory(params: IStorageFactoryParams): IStorageAsync {
2933
const { onReadyCb, settings, settings: { log, sync: { impressionsMode } } } = params;
3034
const metadata = metadataBuilder(settings);
3135
const keys = new KeyBuilderSS(prefix, metadata);
32-
const redisClient = new RedisAdapter(log, options.options || {});
36+
const redisClient: RedisAdapter = new RD(log, options.options || {});
3337
const telemetry = new TelemetryCacheInRedis(log, keys, redisClient);
3438
const impressionCountsCache = impressionsMode !== DEBUG ? new ImpressionCountsCacheInRedis(log, keys.buildImpressionsCountKey(), redisClient) : undefined;
3539
const uniqueKeysCache = impressionsMode === NONE ? new UniqueKeysCacheInRedis(log, keys.buildUniqueKeysKey(), redisClient) : undefined;
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { SplitIO } from '../../../../types';
2+
import { splitsParserFromSettingsFactory } from '../splitsParserFromSettings';
3+
4+
const FEATURE_ON = { conditions: [{ conditionType: 'ROLLOUT', label: 'default rule', matcherGroup: { combiner: 'AND', matchers: [{ keySelector: null, matcherType: 'ALL_KEYS', negate: false }] }, partitions: [{ size: 100, treatment: 'on' }] }], configurations: {}, trafficTypeName: 'localhost' };
5+
const FEATURE_OFF = { conditions: [{ conditionType: 'ROLLOUT', label: 'default rule', matcherGroup: { combiner: 'AND', matchers: [{ keySelector: null, matcherType: 'ALL_KEYS', negate: false }] }, partitions: [{ size: 100, treatment: 'off' }] }], configurations: {}, trafficTypeName: 'localhost' };
6+
7+
test('splitsParserFromSettingsFactory', () => {
8+
9+
const instance = splitsParserFromSettingsFactory();
10+
11+
const settings = { features: {} as SplitIO.MockedFeaturesMap };
12+
expect(instance(settings)).toEqual({});
13+
14+
// Pass the same settings
15+
expect(instance(settings)).toEqual(false);
16+
17+
// New features object with new content
18+
settings.features = { feature1: 'on' };
19+
expect(instance(settings)).toEqual({ feature1: FEATURE_ON });
20+
21+
// New features object but same content
22+
settings.features = { feature1: 'on' };
23+
expect(instance(settings)).toEqual(false);
24+
25+
// Update property
26+
settings.features['feature1'] = 'off';
27+
expect(instance(settings)).toEqual({ feature1: FEATURE_OFF });
28+
29+
// New settings object but same content
30+
expect(instance({ features: { feature1: 'off' } })).toEqual(false);
31+
32+
// Update property. Same content but in a different format
33+
settings.features['feature1'] = { treatment: 'off', config: null };
34+
expect(instance(settings)).toEqual({ feature1: FEATURE_OFF });
35+
36+
// Add new feature flag property
37+
settings.features['feature2'] = { treatment: 'on', config: null };
38+
expect(instance(settings)).toEqual({ feature1: FEATURE_OFF, feature2: FEATURE_ON });
39+
40+
// New settings object but same content
41+
expect(instance({ features: { feature1: { treatment: 'off', config: null }, feature2: { treatment: 'on', config: null } } })).toEqual(false);
42+
43+
// Update property
44+
settings.features['feature2'].config = 'some_config';
45+
expect(instance(settings)).toEqual({ feature1: FEATURE_OFF, feature2: { ...FEATURE_ON, configurations: { on: 'some_config' } } });
46+
47+
// @ts-expect-error No object implies no features
48+
settings.features = undefined;
49+
expect(instance(settings)).toEqual({});
50+
});

src/sync/offline/splitsParser/splitsParserFromSettings.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { ISplitPartial } from '../../../dtos/types';
22
import { ISettings, SplitIO } from '../../../types';
3-
import { isObject, forOwn } from '../../../utils/lang';
3+
import { isObject, forOwn, merge } from '../../../utils/lang';
44
import { parseCondition } from './parseCondition';
55

66
function hasTreatmentChanged(prev: string | SplitIO.TreatmentWithConfig, curr: string | SplitIO.TreatmentWithConfig) {
@@ -22,7 +22,7 @@ export function splitsParserFromSettingsFactory() {
2222

2323
// Different amount of items
2424
if (names.length !== Object.keys(previousMock).length) {
25-
previousMock = currentData;
25+
previousMock = merge({}, currentData) as SplitIO.MockedFeaturesMap;
2626
return true;
2727
}
2828

@@ -31,7 +31,7 @@ export function splitsParserFromSettingsFactory() {
3131
const newTreatment = hasTreatmentChanged(previousMock[name], currentData[name]);
3232
const changed = newSplit || newTreatment;
3333

34-
if (changed) previousMock = currentData;
34+
if (changed) previousMock = merge({}, currentData) as SplitIO.MockedFeaturesMap;
3535

3636
return changed;
3737
});
@@ -41,7 +41,7 @@ export function splitsParserFromSettingsFactory() {
4141
*
4242
* @param settings validated object with mocked features mapping.
4343
*/
44-
return function splitsParserFromSettings(settings: ISettings): false | Record<string, ISplitPartial> {
44+
return function splitsParserFromSettings(settings: Pick<ISettings, 'features'>): false | Record<string, ISplitPartial> {
4545
const features = settings.features as SplitIO.MockedFeaturesMap || {};
4646

4747
if (!mockUpdated(features)) return false;

0 commit comments

Comments
 (0)