Skip to content

Commit dc3a859

Browse files
Merge pull request #411 from splitio/development
Release v2.3.0
2 parents 6584f28 + 287b242 commit dc3a859

File tree

12 files changed

+86
-56
lines changed

12 files changed

+86
-56
lines changed

CHANGES.txt

Lines changed: 30 additions & 24 deletions
Large diffs are not rendered by default.

package-lock.json

Lines changed: 2 additions & 2 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": "2.2.0",
3+
"version": "2.3.0",
44
"description": "Split JavaScript SDK common components",
55
"main": "cjs/index.js",
66
"module": "esm/index.js",

src/consent/__tests__/sdkUserConsent.spec.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ test('createUserConsentAPI', () => {
77
const syncManager = { submitterManager: syncTaskFactory() };
88
const storage = {
99
events: { clear: jest.fn() },
10-
impressions: { clear: jest.fn() }
10+
impressions: { clear: jest.fn() },
11+
impressionCounts: { clear: jest.fn() },
12+
uniqueKeys: { clear: jest.fn() }
1113
};
1214

1315
// @ts-ignore

src/consent/sdkUserConsent.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ const ConsentStatus = {
1515
* The public user consent API exposed via SplitFactory, used to control if the SDK tracks and sends impressions and events or not.
1616
*/
1717
export function createUserConsentAPI(params: ISdkFactoryContext) {
18-
const { settings, settings: { log }, syncManager, storage: { events, impressions, impressionCounts } } = params;
18+
const { settings, settings: { log }, syncManager, storage: { events, impressions, impressionCounts, uniqueKeys } } = params;
1919

2020
if (!isConsentGranted(settings)) log.info(USER_CONSENT_INITIAL, [settings.userConsent]);
2121

@@ -41,7 +41,8 @@ export function createUserConsentAPI(params: ISdkFactoryContext) {
4141
// @ts-ignore, clear method is present in storage for standalone and partial consumer mode
4242
if (events.clear) events.clear(); // @ts-ignore
4343
if (impressions.clear) impressions.clear(); // @ts-ignore
44-
if (impressionCounts && impressionCounts.clear) impressionCounts.clear();
44+
if (impressionCounts.clear) impressionCounts.clear(); // @ts-ignore
45+
if (uniqueKeys.clear) uniqueKeys.clear();
4546
}
4647
} else {
4748
log.info(USER_CONSENT_NOT_UPDATED, [newConsentStatus]);

src/listeners/__tests__/browser.spec.ts

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { BrowserSignalListener } from '../browser';
2-
import { IEventsCacheSync, IImpressionCountsCacheSync, IImpressionsCacheSync, IStorageSync, ITelemetryCacheSync, IUniqueKeysCacheBase } from '../../storages/types';
32
import { ISplitApi } from '../../services/types';
43
import { fullSettings } from '../../utils/settingsValidation/__tests__/settings.mocks';
54

@@ -30,42 +29,48 @@ const fakeUniqueKeys = {
3029
};
3130

3231
// Storage with impressionsCount and telemetry cache
33-
const fakeStorageOptimized = { // @ts-expect-error
32+
const fakeStorageOptimized = {
3433
impressions: {
3534
isEmpty: jest.fn(),
3635
pop() {
3736
return [fakeImpression];
3837
}
39-
} as IImpressionsCacheSync, // @ts-expect-error
38+
},
4039
events: {
4140
isEmpty: jest.fn(),
4241
pop() {
4342
return [fakeEvent];
4443
}
45-
} as IEventsCacheSync, // @ts-expect-error
44+
},
4645
impressionCounts: {
4746
isEmpty: jest.fn(),
4847
pop() {
4948
return fakeImpressionCounts;
5049
}
51-
} as IImpressionCountsCacheSync, // @ts-expect-error
50+
},
5251
uniqueKeys: {
5352
isEmpty: jest.fn(),
5453
pop() {
5554
return fakeUniqueKeys;
5655
}
57-
} as IUniqueKeysCacheBase, // @ts-expect-error
56+
},
5857
telemetry: {
5958
isEmpty: jest.fn(),
6059
pop() {
6160
return 'fake telemetry';
6261
}
63-
} as ITelemetryCacheSync
62+
}
6463
};
6564

6665
const fakeStorageDebug = {
6766
impressions: fakeStorageOptimized.impressions,
68-
events: fakeStorageOptimized.events
67+
events: fakeStorageOptimized.events,
68+
impressionCounts: {
69+
isEmpty: jest.fn(() => true)
70+
},
71+
uniqueKeys: {
72+
isEmpty: jest.fn(() => true)
73+
}
6974
};
7075

7176
// @ts-expect-error
@@ -155,7 +160,8 @@ function assertStop(listener: BrowserSignalListener) {
155160

156161
test('Browser JS listener / consumer mode', () => {
157162
// No SyncManager ==> consumer mode
158-
const listener = new BrowserSignalListener(undefined, fullSettings, fakeStorageOptimized as IStorageSync, fakeSplitApi);
163+
// @ts-expect-error
164+
const listener = new BrowserSignalListener(undefined, fullSettings, fakeStorageOptimized, fakeSplitApi);
159165

160166
listener.start();
161167
assertStart(listener);
@@ -180,7 +186,7 @@ test('Browser JS listener / standalone mode / Impressions optimized mode with te
180186
const syncManagerMock = {};
181187

182188
// @ts-expect-error
183-
const listener = new BrowserSignalListener(syncManagerMock, fullSettings, fakeStorageOptimized as IStorageSync, fakeSplitApi);
189+
const listener = new BrowserSignalListener(syncManagerMock, fullSettings, fakeStorageOptimized, fakeSplitApi);
184190

185191
listener.start();
186192
assertStart(listener);
@@ -205,7 +211,7 @@ test('Browser JS listener / standalone mode / Impressions debug mode', () => {
205211
const syncManagerMock = {};
206212

207213
// @ts-expect-error
208-
const listener = new BrowserSignalListener(syncManagerMock, fullSettings, fakeStorageDebug as IStorageSync, fakeSplitApi);
214+
const listener = new BrowserSignalListener(syncManagerMock, fullSettings, fakeStorageDebug, fakeSplitApi);
209215

210216
listener.start();
211217
assertStart(listener);
@@ -234,7 +240,7 @@ test('Browser JS listener / standalone mode / Impressions debug mode', () => {
234240
test('Browser JS listener / standalone mode / Fallback to regular Fetch transport', () => {
235241

236242
function runBrowserListener() { // @ts-expect-error
237-
const listener = new BrowserSignalListener({}, fullSettings, fakeStorageDebug as IStorageSync, fakeSplitApi);
243+
const listener = new BrowserSignalListener({}, fullSettings, fakeStorageDebug, fakeSplitApi);
238244
listener.start();
239245
// Trigger data flush
240246
triggerEvent(VISIBILITYCHANGE_EVENT, 'hidden');
@@ -270,7 +276,7 @@ test('Browser JS listener / standalone mode / user consent status', () => {
270276
const settings = { ...fullSettings };
271277

272278
// @ts-expect-error
273-
const listener = new BrowserSignalListener(syncManagerMock, settings, fakeStorageOptimized as IStorageSync, fakeSplitApi);
279+
const listener = new BrowserSignalListener(syncManagerMock, settings, fakeStorageOptimized, fakeSplitApi);
274280

275281
listener.start();
276282

src/listeners/browser.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,9 +84,8 @@ export class BrowserSignalListener implements ISignalListener {
8484

8585
this._flushData(events + '/testImpressions/beacon', this.storage.impressions, this.serviceApi.postTestImpressionsBulk, this.fromImpressionsCollector, extraMetadata);
8686
this._flushData(events + '/events/beacon', this.storage.events, this.serviceApi.postEventsBulk);
87-
if (this.storage.impressionCounts) this._flushData(events + '/testImpressions/count/beacon', this.storage.impressionCounts, this.serviceApi.postTestImpressionsCount, fromImpressionCountsCollector);
88-
// @ts-ignore
89-
if (this.storage.uniqueKeys) this._flushData(telemetry + '/v1/keys/cs/beacon', this.storage.uniqueKeys, this.serviceApi.postUniqueKeysBulkCs);
87+
this._flushData(events + '/testImpressions/count/beacon', this.storage.impressionCounts, this.serviceApi.postTestImpressionsCount, fromImpressionCountsCollector);
88+
this._flushData(telemetry + '/v1/keys/cs/beacon', this.storage.uniqueKeys, this.serviceApi.postUniqueKeysBulkCs);
9089
}
9190

9291
// Flush telemetry data

src/sdkFactory/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import SplitIO from '../../types/splitio';
77
import { validateAndTrackApiKey } from '../utils/inputValidation/apiKey';
88
import { createLoggerAPI } from '../logger/sdkLogger';
99
import { NEW_FACTORY, RETRIEVE_MANAGER } from '../logger/constants';
10-
import { SDK_SPLITS_ARRIVED, SDK_SEGMENTS_ARRIVED } from '../readiness/constants';
10+
import { SDK_SPLITS_ARRIVED, SDK_SEGMENTS_ARRIVED, SDK_SPLITS_CACHE_LOADED } from '../readiness/constants';
1111
import { objectAssign } from '../utils/lang/objectAssign';
1212
import { strategyDebugFactory } from '../trackers/strategy/strategyDebug';
1313
import { strategyOptimizedFactory } from '../trackers/strategy/strategyOptimized';
@@ -52,6 +52,9 @@ export function sdkFactory(params: ISdkFactoryParams): SplitIO.ISDK | SplitIO.IA
5252
readiness.splits.emit(SDK_SPLITS_ARRIVED);
5353
readiness.segments.emit(SDK_SEGMENTS_ARRIVED);
5454
},
55+
onReadyFromCacheCb: () => {
56+
readiness.splits.emit(SDK_SPLITS_CACHE_LOADED);
57+
}
5558
});
5659
// @TODO add support for dataloader: `if (params.dataLoader) params.dataLoader(storage);`
5760
const clients: Record<string, SplitIO.IBasicClient> = {};

src/sdkFactory/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export interface IPlatform {
2222
/**
2323
* If provided, it is used to pass additional options to fetch and eventsource calls.
2424
*/
25-
getOptions?: (settings: ISettings) => object
25+
getOptions?: (settings: ISettings) => (object | undefined)
2626
/**
2727
* If provided, it is used to retrieve the EventSource constructor for streaming support.
2828
*/

src/services/splitHttpClient.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ import { ERROR_HTTP, ERROR_CLIENT_CANNOT_GET_READY } from '../logger/constants';
44
import { ISettings } from '../types';
55
import { IPlatform } from '../sdkFactory/types';
66
import { decorateHeaders, removeNonISO88591 } from './decorateHeaders';
7+
import { timeout } from '../utils/promise/timeout';
78

9+
const PENDING_FETCH_ERROR_TIMEOUT = 100;
810
const messageNoFetch = 'Global fetch API is not available.';
911

1012
/**
@@ -45,7 +47,8 @@ export function splitHttpClientFactory(settings: ISettings, { getOptions, getFet
4547
// https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch#Checking_that_the_fetch_was_successful
4648
.then(response => {
4749
if (!response.ok) {
48-
return response.text().then(message => Promise.reject({ response, message }));
50+
// `text()` promise might not settle in some fetch implementations and cases (e.g. no content)
51+
return timeout(PENDING_FETCH_ERROR_TIMEOUT, response.text()).then(message => Promise.reject({ response, message }), () => Promise.reject({ response }));
4952
}
5053
latencyTracker();
5154
return response;

src/storages/inRedis/index.ts

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,28 +17,37 @@ export interface InRedisStorageOptions {
1717
options?: Record<string, any>
1818
}
1919

20+
let RD: typeof RedisAdapter | undefined;
21+
22+
try {
23+
// Using `require` to prevent error when bundling or importing the SDK in a .mjs file, since ioredis is a CommonJS module.
24+
// Redis storage is not supported with .mjs files.
25+
RD = require('./RedisAdapter').RedisAdapter;
26+
} catch (error) { /* empty */ }
27+
2028
/**
2129
* InRedis storage factory for consumer server-side SplitFactory, that uses `Ioredis` Redis client for Node.js
2230
* @see {@link https://www.npmjs.com/package/ioredis}
2331
*/
2432
export function InRedisStorage(options: InRedisStorageOptions = {}): IStorageAsyncFactory {
2533

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-
3034
const prefix = validatePrefix(options.prefix);
3135

3236
function InRedisStorageFactory(params: IStorageFactoryParams): IStorageAsync {
33-
const { onReadyCb, settings, settings: { log } } = params;
37+
if (!RD) throw new Error('The SDK Redis storage is unavailable. Make sure your runtime environment supports CommonJS (`require`) so the `ioredis` dependency can be imported.');
38+
39+
const { onReadyFromCacheCb, onReadyCb, settings, settings: { log } } = params;
3440
const metadata = metadataBuilder(settings);
3541
const keys = new KeyBuilderSS(prefix, metadata);
36-
const redisClient: RedisAdapter = new RD(log, options.options || {});
42+
const redisClient = new RD(log, options.options || {});
3743
const telemetry = new TelemetryCacheInRedis(log, keys, redisClient);
3844
const impressionCountsCache = new ImpressionCountsCacheInRedis(log, keys.buildImpressionsCountKey(), redisClient);
3945
const uniqueKeysCache = new UniqueKeysCacheInRedis(log, keys.buildUniqueKeysKey(), redisClient);
4046

41-
// subscription to Redis connect event in order to emit SDK_READY event on consumer mode
47+
// RedisAdapter lets queue operations before connected
48+
onReadyFromCacheCb();
49+
50+
// Subscription to Redis connect event in order to emit SDK_READY event on consumer mode
4251
redisClient.on('connect', () => {
4352
onReadyCb();
4453
impressionCountsCache.start();

src/storages/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -472,6 +472,7 @@ export interface IStorageFactoryParams {
472472
* It is meant for emitting SDK_READY event in consumer mode, and waiting before using the storage in the synchronizer.
473473
*/
474474
onReadyCb: (error?: any) => void,
475+
onReadyFromCacheCb: () => void,
475476
}
476477

477478

0 commit comments

Comments
 (0)