@splitsoftware/splitio-commons 1.0.1-rc.6 → 1.2.1-rc.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGES.txt +13 -3
- package/LICENSE +1 -1
- package/README.md +1 -1
- package/cjs/logger/messages/info.js +3 -3
- package/cjs/sdkClient/client.js +2 -1
- package/cjs/sdkClient/clientAttributesDecoration.js +108 -0
- package/cjs/sdkClient/clientCS.js +10 -7
- package/cjs/sdkClient/sdkClientMethodCS.js +2 -2
- package/cjs/sdkClient/sdkClientMethodCSWithTT.js +2 -2
- package/cjs/sdkFactory/index.js +2 -2
- package/cjs/services/splitHttpClient.js +1 -1
- package/cjs/storages/inMemory/AttributesCacheInMemory.js +70 -0
- package/cjs/sync/polling/updaters/splitChangesUpdater.js +6 -3
- package/cjs/sync/streaming/AuthClient/index.js +1 -2
- package/cjs/sync/streaming/pushManager.js +26 -22
- package/cjs/sync/syncManagerOnline.js +12 -7
- package/cjs/utils/inputValidation/attribute.js +20 -0
- package/cjs/utils/inputValidation/attributes.js +13 -1
- package/cjs/utils/murmur3/legacy.js +44 -0
- package/cjs/utils/promise/timeout.js +1 -1
- package/cjs/utils/settingsValidation/index.js +1 -1
- package/esm/logger/messages/info.js +3 -3
- package/esm/sdkClient/client.js +2 -1
- package/esm/sdkClient/clientAttributesDecoration.js +104 -0
- package/esm/sdkClient/clientCS.js +10 -7
- package/esm/sdkClient/sdkClientMethodCS.js +2 -2
- package/esm/sdkClient/sdkClientMethodCSWithTT.js +2 -2
- package/esm/sdkFactory/index.js +2 -2
- package/esm/services/splitHttpClient.js +1 -1
- package/esm/storages/inMemory/AttributesCacheInMemory.js +67 -0
- package/esm/sync/polling/updaters/splitChangesUpdater.js +6 -3
- package/esm/sync/streaming/AuthClient/index.js +1 -2
- package/esm/sync/streaming/pushManager.js +26 -22
- package/esm/sync/syncManagerOnline.js +12 -7
- package/esm/utils/inputValidation/attribute.js +16 -0
- package/esm/utils/inputValidation/attributes.js +11 -0
- package/esm/utils/murmur3/legacy.js +39 -0
- package/esm/utils/promise/timeout.js +1 -1
- package/esm/utils/settingsValidation/index.js +1 -1
- package/package.json +2 -2
- package/src/logger/messages/info.ts +3 -3
- package/src/sdkClient/client.ts +2 -1
- package/src/sdkClient/clientAttributesDecoration.ts +122 -0
- package/src/sdkClient/clientCS.ts +14 -7
- package/src/sdkClient/sdkClientMethodCS.ts +2 -0
- package/src/sdkClient/sdkClientMethodCSWithTT.ts +2 -0
- package/src/sdkFactory/index.ts +2 -2
- package/src/services/splitHttpClient.ts +1 -1
- package/src/storages/inMemory/AttributesCacheInMemory.ts +73 -0
- package/src/sync/polling/updaters/splitChangesUpdater.ts +5 -2
- package/src/sync/streaming/AuthClient/index.ts +1 -2
- package/src/sync/streaming/pushManager.ts +26 -26
- package/src/sync/syncManagerOnline.ts +10 -6
- package/src/types.ts +43 -0
- package/src/utils/inputValidation/attribute.ts +21 -0
- package/src/utils/inputValidation/attributes.ts +14 -0
- package/src/utils/murmur3/legacy.ts +48 -0
- package/src/utils/promise/timeout.ts +1 -1
- package/src/utils/settingsValidation/index.ts +1 -1
- package/types/sdkClient/clientAttributesDecoration.d.ts +51 -0
- package/types/sdkClient/clientCS.d.ts +2 -1
- package/types/storages/inMemory/AttributesCacheInMemory.d.ts +43 -0
- package/types/storages/inMemory/TelemetryCacheInMemory.d.ts +51 -0
- package/types/storages/inRedis/TelemetryCacheInRedis.d.ts +0 -0
- package/types/storages/pluggable/TelemetryCachePluggable.d.ts +2 -0
- package/types/sync/submitters/telemetrySyncTask.d.ts +17 -0
- package/types/trackers/telemetryRecorder.d.ts +0 -0
- package/types/types.d.ts +40 -0
- package/types/utils/EventEmitter.d.ts +4 -0
- package/types/utils/inputValidation/attribute.d.ts +2 -0
- package/types/utils/inputValidation/attributes.d.ts +1 -0
- package/types/utils/murmur3/legacy.d.ts +2 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { AttributesCacheInMemory } from '../storages/inMemory/AttributesCacheInMemory';
|
|
2
|
+
import { validateAttributesDeep } from '../utils/inputValidation/attributes';
|
|
3
|
+
import { SplitIO } from '../types';
|
|
4
|
+
import { ILogger } from '../logger/types';
|
|
5
|
+
import { objectAssign } from '../utils/lang/objectAssign';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Add in memory attributes storage methods and combine them with any attribute received from the getTreatment/s call
|
|
9
|
+
*/
|
|
10
|
+
export function clientAttributesDecoration<TClient extends SplitIO.IClient | SplitIO.IAsyncClient>(log: ILogger, client: TClient) {
|
|
11
|
+
|
|
12
|
+
const attributeStorage = new AttributesCacheInMemory();
|
|
13
|
+
|
|
14
|
+
// Keep a reference to the original methods
|
|
15
|
+
const clientGetTreatment = client.getTreatment;
|
|
16
|
+
const clientGetTreatmentWithConfig = client.getTreatmentWithConfig;
|
|
17
|
+
const clientGetTreatments = client.getTreatments;
|
|
18
|
+
const clientGetTreatmentsWithConfig = client.getTreatmentsWithConfig;
|
|
19
|
+
const clientTrack = client.track;
|
|
20
|
+
|
|
21
|
+
function getTreatment(maybeKey: SplitIO.SplitKey, maybeSplit: string, maybeAttributes?: SplitIO.Attributes) {
|
|
22
|
+
return clientGetTreatment(maybeKey, maybeSplit, combineAttributes(maybeAttributes));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function getTreatmentWithConfig(maybeKey: SplitIO.SplitKey, maybeSplit: string, maybeAttributes?: SplitIO.Attributes) {
|
|
26
|
+
return clientGetTreatmentWithConfig(maybeKey, maybeSplit, combineAttributes(maybeAttributes));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function getTreatments(maybeKey: SplitIO.SplitKey, maybeSplits: string[], maybeAttributes?: SplitIO.Attributes) {
|
|
30
|
+
return clientGetTreatments(maybeKey, maybeSplits, combineAttributes(maybeAttributes));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function getTreatmentsWithConfig(maybeKey: SplitIO.SplitKey, maybeSplits: string[], maybeAttributes?: SplitIO.Attributes) {
|
|
34
|
+
return clientGetTreatmentsWithConfig(maybeKey, maybeSplits, combineAttributes(maybeAttributes));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function track(maybeKey: SplitIO.SplitKey, maybeTT: string, maybeEvent: string, maybeEventValue?: number, maybeProperties?: SplitIO.Properties) {
|
|
38
|
+
return clientTrack(maybeKey, maybeTT, maybeEvent, maybeEventValue, maybeProperties);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function combineAttributes(maybeAttributes: SplitIO.Attributes | undefined): SplitIO.Attributes | undefined{
|
|
42
|
+
const storedAttributes = attributeStorage.getAll();
|
|
43
|
+
if (Object.keys(storedAttributes).length > 0) {
|
|
44
|
+
return objectAssign({}, storedAttributes, maybeAttributes);
|
|
45
|
+
}
|
|
46
|
+
return maybeAttributes;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return objectAssign(client, {
|
|
50
|
+
getTreatment: getTreatment,
|
|
51
|
+
getTreatmentWithConfig: getTreatmentWithConfig,
|
|
52
|
+
getTreatments: getTreatments,
|
|
53
|
+
getTreatmentsWithConfig: getTreatmentsWithConfig,
|
|
54
|
+
track: track,
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Add an attribute to client's in memory attributes storage
|
|
58
|
+
*
|
|
59
|
+
* @param {string} attributeName Attrinute name
|
|
60
|
+
* @param {string, number, boolean, list} attributeValue Attribute value
|
|
61
|
+
* @returns {boolean} true if the attribute was stored and false otherways
|
|
62
|
+
*/
|
|
63
|
+
setAttribute(attributeName: string, attributeValue: Object) {
|
|
64
|
+
const attribute: Record<string, Object> = {};
|
|
65
|
+
attribute[attributeName] = attributeValue;
|
|
66
|
+
if (!validateAttributesDeep(log, attribute, 'setAttribute')) return false;
|
|
67
|
+
log.debug(`stored ${attributeValue} for attribute ${attributeName}`);
|
|
68
|
+
return attributeStorage.setAttribute(attributeName, attributeValue);
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Returns the attribute with the given key
|
|
73
|
+
*
|
|
74
|
+
* @param {string} attributeName Attribute name
|
|
75
|
+
* @returns {Object} Attribute with the given key
|
|
76
|
+
*/
|
|
77
|
+
getAttribute(attributeName: string) {
|
|
78
|
+
log.debug(`retrieved attribute ${attributeName}`);
|
|
79
|
+
return attributeStorage.getAttribute(attributeName + '');
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Add to client's in memory attributes storage the attributes in 'attributes'
|
|
84
|
+
*
|
|
85
|
+
* @param {Object} attributes Object with attributes to store
|
|
86
|
+
* @returns true if attributes were stored an false otherways
|
|
87
|
+
*/
|
|
88
|
+
setAttributes(attributes: Record<string, Object>) {
|
|
89
|
+
if (!validateAttributesDeep(log, attributes, 'setAttributes')) return false;
|
|
90
|
+
return attributeStorage.setAttributes(attributes);
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Return all the attributes stored in client's in memory attributes storage
|
|
95
|
+
*
|
|
96
|
+
* @returns {Object} returns all the stored attributes
|
|
97
|
+
*/
|
|
98
|
+
getAttributes(): Record<string, Object> {
|
|
99
|
+
return attributeStorage.getAll();
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Removes from client's in memory attributes storage the attribute with the given key
|
|
104
|
+
*
|
|
105
|
+
* @param {string} attributeName
|
|
106
|
+
* @returns {boolean} true if attribute was removed and false otherways
|
|
107
|
+
*/
|
|
108
|
+
removeAttribute(attributeName: string) {
|
|
109
|
+
log.debug(`removed attribute ${attributeName}`);
|
|
110
|
+
return attributeStorage.removeAttribute(attributeName + '');
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Remove all the stored attributes in the client's in memory attribute storage
|
|
115
|
+
*/
|
|
116
|
+
clearAttributes() {
|
|
117
|
+
return attributeStorage.clear();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { objectAssign } from '../utils/lang/objectAssign';
|
|
2
|
+
import { ILogger } from '../logger/types';
|
|
2
3
|
import { SplitIO } from '../types';
|
|
4
|
+
import { clientAttributesDecoration } from './clientAttributesDecoration';
|
|
3
5
|
|
|
4
6
|
|
|
5
7
|
/**
|
|
@@ -9,15 +11,20 @@ import { SplitIO } from '../types';
|
|
|
9
11
|
* @param key validated split key
|
|
10
12
|
* @param trafficType validated traffic type
|
|
11
13
|
*/
|
|
12
|
-
export function clientCSDecorator(client: SplitIO.IClient, key: SplitIO.SplitKey, trafficType?: string): SplitIO.ICsClient {
|
|
13
|
-
|
|
14
|
+
export function clientCSDecorator(log: ILogger, client: SplitIO.IClient, key: SplitIO.SplitKey, trafficType?: string): SplitIO.ICsClient {
|
|
15
|
+
|
|
16
|
+
let clientCS = clientAttributesDecoration(log, client);
|
|
17
|
+
|
|
18
|
+
return objectAssign(clientCS, {
|
|
14
19
|
// In the client-side API, we bind a key to the client `getTreatment*` methods
|
|
15
|
-
getTreatment:
|
|
16
|
-
getTreatmentWithConfig:
|
|
17
|
-
getTreatments:
|
|
18
|
-
getTreatmentsWithConfig:
|
|
20
|
+
getTreatment: clientCS.getTreatment.bind(clientCS, key),
|
|
21
|
+
getTreatmentWithConfig: clientCS.getTreatmentWithConfig.bind(clientCS, key),
|
|
22
|
+
getTreatments: clientCS.getTreatments.bind(clientCS, key),
|
|
23
|
+
getTreatmentsWithConfig: clientCS.getTreatmentsWithConfig.bind(clientCS, key),
|
|
19
24
|
|
|
20
25
|
// Key is bound to the `track` method. Same thing happens with trafficType but only if provided
|
|
21
|
-
track: trafficType ?
|
|
26
|
+
track: trafficType ? clientCS.track.bind(clientCS, key, trafficType) : clientCS.track.bind(clientCS, key),
|
|
27
|
+
|
|
28
|
+
isBrowserClient: true
|
|
22
29
|
}) as SplitIO.ICsClient;
|
|
23
30
|
}
|
|
@@ -29,6 +29,7 @@ export function sdkClientMethodCSFactory(params: ISdkClientFactoryParams): (key?
|
|
|
29
29
|
const validKey = validateKey(log, key, method);
|
|
30
30
|
|
|
31
31
|
const mainClientInstance = clientCSDecorator(
|
|
32
|
+
log,
|
|
32
33
|
sdkClientFactory(params) as SplitIO.IClient, // @ts-ignore
|
|
33
34
|
validKey
|
|
34
35
|
);
|
|
@@ -74,6 +75,7 @@ export function sdkClientMethodCSFactory(params: ISdkClientFactoryParams): (key?
|
|
|
74
75
|
// As shared clients reuse all the storage information, we don't need to check here if we
|
|
75
76
|
// will use offline or online mode. We should stick with the original decision.
|
|
76
77
|
clientInstances[instanceId] = clientCSDecorator(
|
|
78
|
+
log,
|
|
77
79
|
sdkClientFactory(objectAssign({}, params, {
|
|
78
80
|
sdkReadinessManager: sharedSdkReadiness,
|
|
79
81
|
storage: sharedStorage || storage,
|
|
@@ -35,6 +35,7 @@ export function sdkClientMethodCSFactory(params: ISdkClientFactoryParams): (key?
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
const mainClientInstance = clientCSDecorator(
|
|
38
|
+
log,
|
|
38
39
|
sdkClientFactory(params) as SplitIO.IClient, // @ts-ignore
|
|
39
40
|
validKey,
|
|
40
41
|
validTrafficType
|
|
@@ -88,6 +89,7 @@ export function sdkClientMethodCSFactory(params: ISdkClientFactoryParams): (key?
|
|
|
88
89
|
// As shared clients reuse all the storage information, we don't need to check here if we
|
|
89
90
|
// will use offline or online mode. We should stick with the original decision.
|
|
90
91
|
clientInstances[instanceId] = clientCSDecorator(
|
|
92
|
+
log,
|
|
91
93
|
sdkClientFactory(objectAssign({}, params, {
|
|
92
94
|
sdkReadinessManager: sharedSdkReadiness,
|
|
93
95
|
storage: sharedStorage || storage,
|
package/src/sdkFactory/index.ts
CHANGED
|
@@ -43,8 +43,8 @@ export function sdkFactory(params: ISdkFactoryParams): SplitIO.ICsSDK | SplitIO.
|
|
|
43
43
|
// ATM, only used by PluggableStorage
|
|
44
44
|
mode: settings.mode,
|
|
45
45
|
|
|
46
|
-
// Callback used to emit SDK_READY in consumer mode, where `syncManagerFactory` is undefined
|
|
47
|
-
// or only
|
|
46
|
+
// Callback used to emit SDK_READY in consumer mode, where `syncManagerFactory` is undefined,
|
|
47
|
+
// or partial consumer mode, where it only has submitters, and therefore it doesn't emit readiness events.
|
|
48
48
|
onReadyCb: (error) => {
|
|
49
49
|
if (error) return; // Don't emit SDK_READY if storage failed to connect. Error message is logged by wrapperAdapter
|
|
50
50
|
readinessManager.splits.emit(SDK_SPLITS_ARRIVED);
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { objectAssign } from '../../utils/lang/objectAssign';
|
|
2
|
+
|
|
3
|
+
export class AttributesCacheInMemory {
|
|
4
|
+
|
|
5
|
+
private attributesCache: Record<string, Object> = {};
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Create or update the value for the given attribute
|
|
10
|
+
*
|
|
11
|
+
* @param {string} attributeName attribute name
|
|
12
|
+
* @param {Object} attributeValue attribute value
|
|
13
|
+
* @returns {boolean} the attribute was stored
|
|
14
|
+
*/
|
|
15
|
+
setAttribute(attributeName: string, attributeValue: Object): boolean {
|
|
16
|
+
this.attributesCache[attributeName] = attributeValue;
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Retrieves the value of a given attribute
|
|
22
|
+
*
|
|
23
|
+
* @param {string} attributeName attribute name
|
|
24
|
+
* @returns {Object?} stored attribute value
|
|
25
|
+
*/
|
|
26
|
+
getAttribute(attributeName: string): Object {
|
|
27
|
+
return this.attributesCache[attributeName];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Create or update all the given attributes
|
|
32
|
+
*
|
|
33
|
+
* @param {[string, Object]} attributes attributes to create or update
|
|
34
|
+
* @returns {boolean} attributes were stored
|
|
35
|
+
*/
|
|
36
|
+
setAttributes(attributes: Record<string, Object>): boolean {
|
|
37
|
+
this.attributesCache = objectAssign(this.attributesCache, attributes);
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Retrieve the full attributes map
|
|
43
|
+
*
|
|
44
|
+
* @returns {Map<string, Object>} stored attributes
|
|
45
|
+
*/
|
|
46
|
+
getAll(): Record<string, Object> {
|
|
47
|
+
return this.attributesCache;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Removes a given attribute from the map
|
|
52
|
+
*
|
|
53
|
+
* @param {string} attributeName attribute to remove
|
|
54
|
+
* @returns {boolean} attribute removed
|
|
55
|
+
*/
|
|
56
|
+
removeAttribute(attributeName: string): boolean {
|
|
57
|
+
if (Object.keys(this.attributesCache).indexOf(attributeName) >= 0) {
|
|
58
|
+
delete this.attributesCache[attributeName];
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Clears all attributes stored in the SDK
|
|
66
|
+
*
|
|
67
|
+
*/
|
|
68
|
+
clear() {
|
|
69
|
+
this.attributesCache = {};
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
}
|
|
@@ -168,9 +168,12 @@ export function splitChangesUpdaterFactory(
|
|
|
168
168
|
return false;
|
|
169
169
|
});
|
|
170
170
|
|
|
171
|
-
// After triggering the requests, if we have cached splits information let's notify that.
|
|
171
|
+
// After triggering the requests, if we have cached splits information let's notify that to emit SDK_READY_FROM_CACHE.
|
|
172
|
+
// Wrapping in a promise since checkCache can be async.
|
|
172
173
|
if (splitsEventEmitter && startingUp) {
|
|
173
|
-
Promise.resolve(splits.checkCache()).then(
|
|
174
|
+
Promise.resolve(splits.checkCache()).then(isCacheReady => {
|
|
175
|
+
if (isCacheReady) splitsEventEmitter.emit(SDK_SPLITS_CACHE_LOADED);
|
|
176
|
+
});
|
|
174
177
|
}
|
|
175
178
|
return fetcherPromise;
|
|
176
179
|
}
|
|
@@ -17,8 +17,7 @@ export function authenticateFactory(fetchAuth: IFetchAuth): IAuthenticate {
|
|
|
17
17
|
* @param {string[] | undefined} userKeys set of user Keys to track MY_SEGMENTS_CHANGES. It is undefined for server-side API.
|
|
18
18
|
*/
|
|
19
19
|
return function authenticate(userKeys?: string[]): Promise<IAuthToken> {
|
|
20
|
-
|
|
21
|
-
return authPromise
|
|
20
|
+
return fetchAuth(userKeys)
|
|
22
21
|
.then(resp => resp.json())
|
|
23
22
|
.then(json => {
|
|
24
23
|
if (json.token) { // empty token when `"pushEnabled": false`
|
|
@@ -40,7 +40,7 @@ export function pushManagerFactory(
|
|
|
40
40
|
|
|
41
41
|
// `userKey` is the matching key of main client in client-side SDK.
|
|
42
42
|
// It can be used to check if running on client-side or server-side SDK.
|
|
43
|
-
const userKey = settings.core.key ? getMatching(settings.core.key) : undefined;
|
|
43
|
+
const userKey = settings.core.key ? getMatching(settings.core.key) : undefined;
|
|
44
44
|
const log = settings.log;
|
|
45
45
|
|
|
46
46
|
let sseClient: ISSEClient;
|
|
@@ -59,7 +59,8 @@ export function pushManagerFactory(
|
|
|
59
59
|
sseClient.setEventHandler(sseHandler);
|
|
60
60
|
|
|
61
61
|
// init workers
|
|
62
|
-
|
|
62
|
+
// MySegmentsUpdateWorker (client-side) are initiated in `add` method
|
|
63
|
+
const segmentsUpdateWorker = userKey ? undefined : new SegmentsUpdateWorker(storage.segments, pollingManager.segmentsSyncTask);
|
|
63
64
|
// For server-side we pass the segmentsSyncTask, used by SplitsUpdateWorker to fetch new segments
|
|
64
65
|
const splitsUpdateWorker = new SplitsUpdateWorker(storage.splits, pollingManager.splitsSyncTask, readiness.splits, userKey ? undefined : pollingManager.segmentsSyncTask);
|
|
65
66
|
|
|
@@ -68,11 +69,6 @@ export function pushManagerFactory(
|
|
|
68
69
|
// [Only for client-side] map of user keys to their corresponding hash64 and MySegmentsUpdateWorkers.
|
|
69
70
|
// Hash64 is used to process MY_SEGMENTS_UPDATE_V2 events and dispatch actions to the corresponding MySegmentsUpdateWorker.
|
|
70
71
|
const clients: Record<string, { hash64: Hash64, worker: MySegmentsUpdateWorker }> = {};
|
|
71
|
-
if (userKey) {
|
|
72
|
-
const hash = hashUserKey(userKey);
|
|
73
|
-
userKeyHashes[hash] = userKey;
|
|
74
|
-
clients[userKey] = { hash64: hash64(userKey), worker: segmentsUpdateWorker as MySegmentsUpdateWorker };
|
|
75
|
-
}
|
|
76
72
|
|
|
77
73
|
// [Only for client-side] variable to flag that a new client was added. It is needed to reconnect streaming.
|
|
78
74
|
let connectForNewClient = false;
|
|
@@ -115,7 +111,8 @@ export function pushManagerFactory(
|
|
|
115
111
|
function connectPush() {
|
|
116
112
|
// Guard condition in case `stop/disconnectPush` has been called (e.g., calling SDK destroy, or app signal close/background)
|
|
117
113
|
if (disconnected) return;
|
|
118
|
-
|
|
114
|
+
// @TODO distinguish log for 'Connecting' (1st time) and 'Re-connecting'
|
|
115
|
+
log.info(STREAMING_CONNECTING);
|
|
119
116
|
disconnected = false;
|
|
120
117
|
|
|
121
118
|
const userKeys = userKey ? Object.keys(clients) : undefined;
|
|
@@ -175,7 +172,7 @@ export function pushManagerFactory(
|
|
|
175
172
|
function stopWorkers() {
|
|
176
173
|
splitsUpdateWorker.backoff.reset();
|
|
177
174
|
if (userKey) forOwn(clients, ({ worker }) => worker.backoff.reset());
|
|
178
|
-
else segmentsUpdateWorker.backoff.reset();
|
|
175
|
+
else (segmentsUpdateWorker as SegmentsUpdateWorker).backoff.reset();
|
|
179
176
|
}
|
|
180
177
|
|
|
181
178
|
pushEmitter.on(PUSH_SUBSYSTEM_DOWN, stopWorkers);
|
|
@@ -305,37 +302,40 @@ export function pushManagerFactory(
|
|
|
305
302
|
// Expose Event Emitter functionality and Event constants
|
|
306
303
|
Object.create(pushEmitter),
|
|
307
304
|
{
|
|
308
|
-
//
|
|
309
|
-
stop
|
|
310
|
-
|
|
305
|
+
// Stop/pause push mode
|
|
306
|
+
stop() {
|
|
307
|
+
disconnectPush(); // `handleNonRetryableError` cannot be used as `stop`, because it emits PUSH_SUBSYSTEM_DOWN event, which starts polling.
|
|
308
|
+
if (userKey) this.remove(userKey); // Necessary to properly resume streaming in client-side (e.g., RN SDK transition to foreground).
|
|
309
|
+
},
|
|
310
|
+
// Start/resume push mode
|
|
311
311
|
start() {
|
|
312
312
|
// Guard condition to avoid calling `connectPush` again if the `start` method is called multiple times or if push has been disabled.
|
|
313
313
|
if (disabled || disconnected === false) return;
|
|
314
314
|
disconnected = false;
|
|
315
|
-
|
|
316
|
-
|
|
315
|
+
|
|
316
|
+
if (userKey) this.add(userKey, pollingManager.segmentsSyncTask); // client-side
|
|
317
|
+
else setTimeout(connectPush); // server-side runs in next cycle as in client-side, for consistency with client-side
|
|
317
318
|
},
|
|
318
319
|
|
|
319
320
|
// [Only for client-side]
|
|
320
321
|
add(userKey: string, mySegmentsSyncTask: ISegmentsSyncTask) {
|
|
321
|
-
clients[userKey] = { hash64: hash64(userKey), worker: new MySegmentsUpdateWorker(mySegmentsSyncTask) };
|
|
322
|
-
|
|
323
322
|
const hash = hashUserKey(userKey);
|
|
324
323
|
|
|
325
324
|
if (!userKeyHashes[hash]) {
|
|
326
325
|
userKeyHashes[hash] = userKey;
|
|
326
|
+
clients[userKey] = { hash64: hash64(userKey), worker: new MySegmentsUpdateWorker(mySegmentsSyncTask) };
|
|
327
327
|
connectForNewClient = true; // we must reconnect on start, to listen the channel for the new user key
|
|
328
|
-
}
|
|
329
328
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
329
|
+
// Reconnects in case of a new client.
|
|
330
|
+
// Run in next event-loop cycle to save authentication calls
|
|
331
|
+
// in case multiple clients are created in the current cycle.
|
|
332
|
+
setTimeout(function checkForReconnect() {
|
|
333
|
+
if (connectForNewClient) {
|
|
334
|
+
connectForNewClient = false;
|
|
335
|
+
connectPush();
|
|
336
|
+
}
|
|
337
|
+
}, 0);
|
|
338
|
+
}
|
|
339
339
|
},
|
|
340
340
|
// [Only for client-side]
|
|
341
341
|
remove(userKey: string) {
|
|
@@ -49,11 +49,11 @@ export function syncManagerOnlineFactory(
|
|
|
49
49
|
/** Sync Manager logic */
|
|
50
50
|
|
|
51
51
|
function startPolling() {
|
|
52
|
-
if (
|
|
52
|
+
if (pollingManager!.isRunning()) {
|
|
53
|
+
log.info(SYNC_CONTINUE_POLLING);
|
|
54
|
+
} else {
|
|
53
55
|
log.info(SYNC_START_POLLING);
|
|
54
56
|
pollingManager!.start();
|
|
55
|
-
} else {
|
|
56
|
-
log.info(SYNC_CONTINUE_POLLING);
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
59
|
|
|
@@ -81,6 +81,9 @@ export function syncManagerOnlineFactory(
|
|
|
81
81
|
* Method used to start the syncManager for the first time, or resume it after being stopped.
|
|
82
82
|
*/
|
|
83
83
|
start() {
|
|
84
|
+
if (running) return;
|
|
85
|
+
running = true;
|
|
86
|
+
|
|
84
87
|
// start syncing splits and segments
|
|
85
88
|
if (pollingManager) {
|
|
86
89
|
if (pushManager) {
|
|
@@ -96,21 +99,22 @@ export function syncManagerOnlineFactory(
|
|
|
96
99
|
}
|
|
97
100
|
|
|
98
101
|
// start periodic data recording (events, impressions, telemetry).
|
|
99
|
-
submitter
|
|
100
|
-
running = true;
|
|
102
|
+
if (submitter) submitter.start();
|
|
101
103
|
},
|
|
102
104
|
|
|
103
105
|
/**
|
|
104
106
|
* Method used to stop/pause the syncManager.
|
|
105
107
|
*/
|
|
106
108
|
stop() {
|
|
109
|
+
if (!running) return;
|
|
110
|
+
running = false;
|
|
111
|
+
|
|
107
112
|
// stop syncing
|
|
108
113
|
if (pushManager) pushManager.stop();
|
|
109
114
|
if (pollingManager && pollingManager.isRunning()) pollingManager.stop();
|
|
110
115
|
|
|
111
116
|
// stop periodic data recording (events, impressions, telemetry).
|
|
112
117
|
if (submitter) submitter.stop();
|
|
113
|
-
running = false;
|
|
114
118
|
},
|
|
115
119
|
|
|
116
120
|
isRunning() {
|
package/src/types.ts
CHANGED
|
@@ -381,6 +381,10 @@ interface IBasicClient extends IStatusInterface {
|
|
|
381
381
|
* @returns {Promise<void>}
|
|
382
382
|
*/
|
|
383
383
|
destroy(): Promise<void>
|
|
384
|
+
|
|
385
|
+
// Whether the client implements the client-side API, i.e, with bound key, (true), or the server-side API (false).
|
|
386
|
+
// Exposed for internal purposes only. Not considered part of the public API, and might be renamed eventually.
|
|
387
|
+
isBrowserClient: boolean
|
|
384
388
|
}
|
|
385
389
|
/**
|
|
386
390
|
* Common definitions between SDK instances for different environments interface.
|
|
@@ -1108,6 +1112,45 @@ export namespace SplitIO {
|
|
|
1108
1112
|
* @returns {boolean} Whether the event was added to the queue succesfully or not.
|
|
1109
1113
|
*/
|
|
1110
1114
|
track(...args: [trafficType: string, eventType: string, value?: number, properties?: Properties] | [eventType: string, value?: number, properties?: Properties]): boolean,
|
|
1115
|
+
/**
|
|
1116
|
+
* Add an attribute to client's in memory attributes storage
|
|
1117
|
+
* @function setAttribute
|
|
1118
|
+
* @param {string} attributeName Attrinute name
|
|
1119
|
+
* @param {string, number, boolean, list} attributeValue Attribute value
|
|
1120
|
+
* @returns {boolean} true if the attribute was stored and false otherways
|
|
1121
|
+
*/
|
|
1122
|
+
setAttribute(attributeName: string, attributeValue: Object): boolean,
|
|
1123
|
+
/**
|
|
1124
|
+
* Returns the attribute with the given key
|
|
1125
|
+
* @function getAttribute
|
|
1126
|
+
* @param {string} attributeName Attribute name
|
|
1127
|
+
* @returns {Object} Attribute with the given key
|
|
1128
|
+
*/
|
|
1129
|
+
getAttribute(attributeName: string): Object,
|
|
1130
|
+
/**
|
|
1131
|
+
* Add to client's in memory attributes storage the attributes in 'attributes'
|
|
1132
|
+
* @function setAttributes
|
|
1133
|
+
* @param {Object} attributes Object with attributes to store
|
|
1134
|
+
* @returns true if attributes were stored an false otherways
|
|
1135
|
+
*/
|
|
1136
|
+
setAttributes(attributes: Record<string, Object>): boolean,
|
|
1137
|
+
/**
|
|
1138
|
+
* Return all the attributes stored in client's in memory attributes storage
|
|
1139
|
+
* @function getAttributes
|
|
1140
|
+
* @returns {Object} returns all the stored attributes
|
|
1141
|
+
*/
|
|
1142
|
+
getAttributes(): Record<string, Object>,
|
|
1143
|
+
/**
|
|
1144
|
+
* Removes from client's in memory attributes storage the attribute with the given key
|
|
1145
|
+
* @function removeAttribute
|
|
1146
|
+
* @param {string} attributeName
|
|
1147
|
+
* @returns {boolean} true if attribute was removed and false otherways
|
|
1148
|
+
*/
|
|
1149
|
+
removeAttribute(attributeName: string): boolean,
|
|
1150
|
+
/**
|
|
1151
|
+
* Remove all the stored attributes in the client's in memory attribute storage
|
|
1152
|
+
*/
|
|
1153
|
+
clearAttributes(): boolean
|
|
1111
1154
|
}
|
|
1112
1155
|
/**
|
|
1113
1156
|
* Representation of a manager instance with synchronous storage of the SDK.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { isString, isFiniteNumber, isBoolean } from '../../utils/lang';
|
|
2
|
+
import { ILogger } from '../../logger/types';
|
|
3
|
+
|
|
4
|
+
export function validateAttribute(log: ILogger, attributeKey: string, attributeValue: Object, method: string): boolean {
|
|
5
|
+
if (!isString(attributeKey) || attributeKey.length === 0) {
|
|
6
|
+
log.warn(`${method}: you passed an invalid attribute name, attribute name must be a non-empty string.`);
|
|
7
|
+
return false;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const isStringVal = isString(attributeValue);
|
|
11
|
+
const isFiniteVal = isFiniteNumber(attributeValue);
|
|
12
|
+
const isBoolVal = isBoolean(attributeValue);
|
|
13
|
+
const isArrayVal = Array.isArray(attributeValue);
|
|
14
|
+
|
|
15
|
+
if (!(isStringVal || isFiniteVal || isBoolVal || isArrayVal)) { // If it's not of valid type.
|
|
16
|
+
log.warn(`${method}: you passed an invalid attribute value for ${attributeKey}. Acceptable types are: string, number, boolean and array of strings.`);
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { isObject } from '../lang';
|
|
2
2
|
import { SplitIO } from '../../types';
|
|
3
3
|
import { ILogger } from '../../logger/types';
|
|
4
|
+
import { validateAttribute } from './attribute';
|
|
4
5
|
import { ERROR_NOT_PLAIN_OBJECT } from '../../logger/constants';
|
|
5
6
|
|
|
6
7
|
export function validateAttributes(log: ILogger, maybeAttrs: any, method: string): SplitIO.Attributes | undefined | false {
|
|
@@ -11,3 +12,16 @@ export function validateAttributes(log: ILogger, maybeAttrs: any, method: string
|
|
|
11
12
|
log.error(ERROR_NOT_PLAIN_OBJECT, [method, 'attributes']);
|
|
12
13
|
return false;
|
|
13
14
|
}
|
|
15
|
+
|
|
16
|
+
export function validateAttributesDeep(log: ILogger, maybeAttributes: Record<string, Object>, method: string): boolean {
|
|
17
|
+
if (!validateAttributes(log, maybeAttributes, method)) return false;
|
|
18
|
+
|
|
19
|
+
let result = true;
|
|
20
|
+
Object.keys(maybeAttributes).forEach(attributeKey => {
|
|
21
|
+
if (!validateAttribute(log, attributeKey, maybeAttributes[attributeKey], method))
|
|
22
|
+
result = false;
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
return result;
|
|
26
|
+
|
|
27
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Deprecated hashing function, used for split bucketing. Replaced by murmur3
|
|
2
|
+
|
|
3
|
+
//
|
|
4
|
+
// JAVA reference implementation for the hashing function.
|
|
5
|
+
//
|
|
6
|
+
// int h = 0;
|
|
7
|
+
// for (int i = 0; i < key.length(); i++) {
|
|
8
|
+
// h = 31 * h + key.charAt(i);
|
|
9
|
+
// }
|
|
10
|
+
// return h ^ seed; // XOR the hash and seed
|
|
11
|
+
//
|
|
12
|
+
|
|
13
|
+
function ToInteger(x: number) {
|
|
14
|
+
x = Number(x);
|
|
15
|
+
return x < 0 ? Math.ceil(x) : Math.floor(x);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function modulo(a: number, b: number) {
|
|
19
|
+
return a - Math.floor(a / b) * b;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function ToUint32(x: number) {
|
|
23
|
+
return modulo(ToInteger(x), Math.pow(2, 32));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function ToInt32(x: number) {
|
|
27
|
+
let uint32 = ToUint32(x);
|
|
28
|
+
|
|
29
|
+
if (uint32 >= Math.pow(2, 31)) {
|
|
30
|
+
return uint32 - Math.pow(2, 32);
|
|
31
|
+
} else {
|
|
32
|
+
return uint32;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function hash(str: string, seed: number): number {
|
|
37
|
+
let h = 0;
|
|
38
|
+
|
|
39
|
+
for (let i = 0; i < str.length; i++) {
|
|
40
|
+
h = ToInt32(ToInt32(31 * h) + str.charCodeAt(i));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return ToInt32(h ^ seed);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function bucket(str: string, seed: number): number {
|
|
47
|
+
return Math.abs(hash(str, seed) % 100) + 1;
|
|
48
|
+
}
|
|
@@ -3,7 +3,7 @@ export function timeout<T>(ms: number, promise: Promise<T>): Promise<T> {
|
|
|
3
3
|
|
|
4
4
|
return new Promise((resolve, reject) => {
|
|
5
5
|
const tid = setTimeout(() => {
|
|
6
|
-
reject(new Error(`Operation timed out because it exceeded the configured time limit of ${ms}ms.`));
|
|
6
|
+
reject(new Error(`Operation timed out because it exceeded the configured time limit of ${ms} ms.`));
|
|
7
7
|
}, ms);
|
|
8
8
|
|
|
9
9
|
promise.then((res) => {
|
|
@@ -122,7 +122,7 @@ export function settingsValidation(config: unknown, validationParams: ISettingsV
|
|
|
122
122
|
// Startup periods
|
|
123
123
|
startup.requestTimeoutBeforeReady = fromSecondsToMillis(startup.requestTimeoutBeforeReady);
|
|
124
124
|
startup.readyTimeout = fromSecondsToMillis(startup.readyTimeout);
|
|
125
|
-
startup.eventsFirstPushWindow = fromSecondsToMillis(
|
|
125
|
+
startup.eventsFirstPushWindow = fromSecondsToMillis(startup.eventsFirstPushWindow);
|
|
126
126
|
|
|
127
127
|
// ensure a valid SDK mode
|
|
128
128
|
// @ts-ignore, modify readonly prop
|