@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.
Files changed (72) hide show
  1. package/CHANGES.txt +13 -3
  2. package/LICENSE +1 -1
  3. package/README.md +1 -1
  4. package/cjs/logger/messages/info.js +3 -3
  5. package/cjs/sdkClient/client.js +2 -1
  6. package/cjs/sdkClient/clientAttributesDecoration.js +108 -0
  7. package/cjs/sdkClient/clientCS.js +10 -7
  8. package/cjs/sdkClient/sdkClientMethodCS.js +2 -2
  9. package/cjs/sdkClient/sdkClientMethodCSWithTT.js +2 -2
  10. package/cjs/sdkFactory/index.js +2 -2
  11. package/cjs/services/splitHttpClient.js +1 -1
  12. package/cjs/storages/inMemory/AttributesCacheInMemory.js +70 -0
  13. package/cjs/sync/polling/updaters/splitChangesUpdater.js +6 -3
  14. package/cjs/sync/streaming/AuthClient/index.js +1 -2
  15. package/cjs/sync/streaming/pushManager.js +26 -22
  16. package/cjs/sync/syncManagerOnline.js +12 -7
  17. package/cjs/utils/inputValidation/attribute.js +20 -0
  18. package/cjs/utils/inputValidation/attributes.js +13 -1
  19. package/cjs/utils/murmur3/legacy.js +44 -0
  20. package/cjs/utils/promise/timeout.js +1 -1
  21. package/cjs/utils/settingsValidation/index.js +1 -1
  22. package/esm/logger/messages/info.js +3 -3
  23. package/esm/sdkClient/client.js +2 -1
  24. package/esm/sdkClient/clientAttributesDecoration.js +104 -0
  25. package/esm/sdkClient/clientCS.js +10 -7
  26. package/esm/sdkClient/sdkClientMethodCS.js +2 -2
  27. package/esm/sdkClient/sdkClientMethodCSWithTT.js +2 -2
  28. package/esm/sdkFactory/index.js +2 -2
  29. package/esm/services/splitHttpClient.js +1 -1
  30. package/esm/storages/inMemory/AttributesCacheInMemory.js +67 -0
  31. package/esm/sync/polling/updaters/splitChangesUpdater.js +6 -3
  32. package/esm/sync/streaming/AuthClient/index.js +1 -2
  33. package/esm/sync/streaming/pushManager.js +26 -22
  34. package/esm/sync/syncManagerOnline.js +12 -7
  35. package/esm/utils/inputValidation/attribute.js +16 -0
  36. package/esm/utils/inputValidation/attributes.js +11 -0
  37. package/esm/utils/murmur3/legacy.js +39 -0
  38. package/esm/utils/promise/timeout.js +1 -1
  39. package/esm/utils/settingsValidation/index.js +1 -1
  40. package/package.json +2 -2
  41. package/src/logger/messages/info.ts +3 -3
  42. package/src/sdkClient/client.ts +2 -1
  43. package/src/sdkClient/clientAttributesDecoration.ts +122 -0
  44. package/src/sdkClient/clientCS.ts +14 -7
  45. package/src/sdkClient/sdkClientMethodCS.ts +2 -0
  46. package/src/sdkClient/sdkClientMethodCSWithTT.ts +2 -0
  47. package/src/sdkFactory/index.ts +2 -2
  48. package/src/services/splitHttpClient.ts +1 -1
  49. package/src/storages/inMemory/AttributesCacheInMemory.ts +73 -0
  50. package/src/sync/polling/updaters/splitChangesUpdater.ts +5 -2
  51. package/src/sync/streaming/AuthClient/index.ts +1 -2
  52. package/src/sync/streaming/pushManager.ts +26 -26
  53. package/src/sync/syncManagerOnline.ts +10 -6
  54. package/src/types.ts +43 -0
  55. package/src/utils/inputValidation/attribute.ts +21 -0
  56. package/src/utils/inputValidation/attributes.ts +14 -0
  57. package/src/utils/murmur3/legacy.ts +48 -0
  58. package/src/utils/promise/timeout.ts +1 -1
  59. package/src/utils/settingsValidation/index.ts +1 -1
  60. package/types/sdkClient/clientAttributesDecoration.d.ts +51 -0
  61. package/types/sdkClient/clientCS.d.ts +2 -1
  62. package/types/storages/inMemory/AttributesCacheInMemory.d.ts +43 -0
  63. package/types/storages/inMemory/TelemetryCacheInMemory.d.ts +51 -0
  64. package/types/storages/inRedis/TelemetryCacheInRedis.d.ts +0 -0
  65. package/types/storages/pluggable/TelemetryCachePluggable.d.ts +2 -0
  66. package/types/sync/submitters/telemetrySyncTask.d.ts +17 -0
  67. package/types/trackers/telemetryRecorder.d.ts +0 -0
  68. package/types/types.d.ts +40 -0
  69. package/types/utils/EventEmitter.d.ts +4 -0
  70. package/types/utils/inputValidation/attribute.d.ts +2 -0
  71. package/types/utils/inputValidation/attributes.d.ts +1 -0
  72. 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
- return objectAssign(client, {
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: client.getTreatment.bind(client, key),
16
- getTreatmentWithConfig: client.getTreatmentWithConfig.bind(client, key),
17
- getTreatments: client.getTreatments.bind(client, key),
18
- getTreatmentsWithConfig: client.getTreatmentsWithConfig.bind(client, key),
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 ? client.track.bind(client, key, trafficType) : client.track.bind(client, key)
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,
@@ -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 instantiates submitters, and therefore it is not able to emit readiness events.
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);
@@ -49,7 +49,7 @@ export function splitHttpClientFactory(settings: Pick<ISettings, 'log' | 'versio
49
49
  return response;
50
50
  })
51
51
  .catch(error => {
52
- const resp = error.response;
52
+ const resp = error && error.response;
53
53
  let msg = '';
54
54
 
55
55
  if (resp) { // An HTTP error
@@ -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(cacheReady => { if (cacheReady) splitsEventEmitter.emit(SDK_SPLITS_CACHE_LOADED); });
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
- let authPromise = fetchAuth(userKeys); // errors handled by fetchAuth service
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
- const segmentsUpdateWorker = userKey ? new MySegmentsUpdateWorker(pollingManager.segmentsSyncTask) : new SegmentsUpdateWorker(storage.segments, pollingManager.segmentsSyncTask);
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
- log.info(STREAMING_CONNECTING, [disconnected === undefined ? '' : 'Re-']);
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
- // Expose functionality for starting and stoping push mode:
309
- stop: disconnectPush, // `handleNonRetryableError` cannot be used as `stop`, because it emits PUSH_SUBSYSTEM_DOWN event, which starts polling.
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
- // Run in next event-loop cycle for optimization on client-side: if multiple clients are created in the same cycle than the factory, only one authentication is performed.
316
- setTimeout(connectPush);
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
- // Reconnects in case of a new client.
331
- // Run in next event-loop cycle to save authentication calls
332
- // in case the user is creating several clients in the current cycle.
333
- setTimeout(function checkForReconnect() {
334
- if (connectForNewClient) {
335
- connectForNewClient = false;
336
- connectPush();
337
- }
338
- }, 0);
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 (!pollingManager!.isRunning()) {
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 && submitter.start();
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(withDefaults.startup.eventsFirstPushWindow);
125
+ startup.eventsFirstPushWindow = fromSecondsToMillis(startup.eventsFirstPushWindow);
126
126
 
127
127
  // ensure a valid SDK mode
128
128
  // @ts-ignore, modify readonly prop