@splitsoftware/splitio-commons 3.1.1-rc.4 → 3.1.1-rc.6

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 (44) hide show
  1. package/CHANGES.txt +2 -1
  2. package/cjs/presets/serverSide.js +2 -2
  3. package/cjs/services/authProvider.js +3 -4
  4. package/cjs/services/secureSplitHttpClient.js +3 -8
  5. package/cjs/services/serviceApi.js +17 -15
  6. package/cjs/services/splitApi.js +10 -9
  7. package/cjs/sync/polling/updaters/definitionChangesUpdater.js +2 -2
  8. package/cjs/sync/streaming/AuthClient/index.js +13 -14
  9. package/cjs/sync/streaming/SSEHandler/index.js +1 -0
  10. package/cjs/sync/streaming/constants.js +2 -1
  11. package/cjs/sync/streaming/pushManager.js +5 -10
  12. package/cjs/sync/submitters/submitterManager.js +53 -30
  13. package/cjs/sync/syncManagerOnline.js +4 -2
  14. package/esm/presets/serverSide.js +2 -2
  15. package/esm/services/authProvider.js +4 -5
  16. package/esm/services/secureSplitHttpClient.js +3 -8
  17. package/esm/services/serviceApi.js +18 -16
  18. package/esm/services/splitApi.js +10 -9
  19. package/esm/sync/polling/updaters/definitionChangesUpdater.js +3 -3
  20. package/esm/sync/streaming/AuthClient/index.js +11 -11
  21. package/esm/sync/streaming/SSEHandler/index.js +2 -1
  22. package/esm/sync/streaming/constants.js +1 -0
  23. package/esm/sync/streaming/pushManager.js +6 -11
  24. package/esm/sync/submitters/submitterManager.js +53 -30
  25. package/esm/sync/syncManagerOnline.js +4 -2
  26. package/package.json +1 -1
  27. package/src/presets/serverSide.ts +2 -2
  28. package/src/services/authProvider.ts +19 -13
  29. package/src/services/secureSplitHttpClient.ts +5 -13
  30. package/src/services/serviceApi.ts +21 -18
  31. package/src/services/splitApi.ts +10 -9
  32. package/src/services/types.ts +2 -2
  33. package/src/sync/polling/updaters/definitionChangesUpdater.ts +3 -3
  34. package/src/sync/streaming/AuthClient/index.ts +14 -15
  35. package/src/sync/streaming/AuthClient/types.ts +9 -13
  36. package/src/sync/streaming/SSEClient/index.ts +2 -2
  37. package/src/sync/streaming/SSEClient/types.ts +2 -2
  38. package/src/sync/streaming/SSEHandler/index.ts +2 -1
  39. package/src/sync/streaming/SSEHandler/types.ts +2 -2
  40. package/src/sync/streaming/constants.ts +1 -0
  41. package/src/sync/streaming/pushManager.ts +32 -42
  42. package/src/sync/streaming/types.ts +3 -2
  43. package/src/sync/submitters/submitterManager.ts +58 -32
  44. package/src/sync/syncManagerOnline.ts +3 -1
@@ -4,7 +4,7 @@ import { SYNC_FETCH, SYNC_UPDATE, SYNC_FETCH_FAILS, SYNC_FETCH_RETRY } from '../
4
4
  import { startsWith } from '../../../utils/lang';
5
5
  import { IN_RULE_BASED_SEGMENT, IN_SEGMENT, RULE_BASED_SEGMENT, STANDARD_SEGMENT } from '../../../utils/constants';
6
6
  import { setToArray } from '../../../utils/lang/sets';
7
- import { SPLIT_UPDATE } from '../../streaming/constants';
7
+ import { RB_SEGMENT_UPDATE } from '../../streaming/constants';
8
8
  /**
9
9
  * Collect segments from a raw FF or RBS definition.
10
10
  * Exported for testing purposes.
@@ -120,14 +120,14 @@ export function definitionChangesUpdaterFactory(log, definitionChangesFetcher, s
120
120
  var since = sinces[0], rbSince = sinces[1];
121
121
  log.debug(SYNC_FETCH, [definitionChangesFetcher.type, since, rbSince]);
122
122
  return Promise.resolve(instantUpdate ?
123
- instantUpdate.type === SPLIT_UPDATE ?
123
+ instantUpdate.type === RB_SEGMENT_UPDATE ?
124
+ { rbs: convertInstantUpdateToDefinitionChanges(instantUpdate) } :
124
125
  // IFFU edge case: a change to definition that adds an IN_RULE_BASED_SEGMENT matcher that is not present yet
125
126
  Promise.resolve(rbSegments.contains(parseSegments(instantUpdate.payload, IN_RULE_BASED_SEGMENT))).then(function (contains) {
126
127
  return contains ?
127
128
  { d: convertInstantUpdateToDefinitionChanges(instantUpdate) } :
128
129
  definitionChangesFetcher(since, noCache, till, rbSince, _promiseDecorator);
129
130
  }) :
130
- { rbs: convertInstantUpdateToDefinitionChanges(instantUpdate) } :
131
131
  definitionChangesFetcher(since, noCache, till, rbSince, _promiseDecorator))
132
132
  .then(function (definitionChanges) {
133
133
  var usedSegments = new Set();
@@ -1,13 +1,11 @@
1
1
  import { objectAssign } from '../../../utils/lang/objectAssign';
2
- import { encodeToBase64 } from '../../../utils/base64';
3
2
  import { decodeJWTtoken } from '../../../utils/jwt';
4
- import { hash } from '../../../utils/murmur3/murmur3';
5
3
  /**
6
4
  * Factory of authentication function.
7
5
  *
8
- * @param fetchAuth - `ServiceApi.fetchAuth` endpoint
6
+ * @param fetchAuth - /auth endpoint
9
7
  */
10
- export function authenticateFactory(fetchAuth) {
8
+ export function fetchAuthFactory(fetchAuth) {
11
9
  /**
12
10
  * Run authentication requests to Auth Server, and returns a promise that resolves with the decoded JTW token.
13
11
  * @param userKeys - set of user Keys to track membership updates. It is undefined for server-side API.
@@ -21,18 +19,20 @@ export function authenticateFactory(fetchAuth) {
21
19
  if (typeof decodedToken.iat !== 'number' || typeof decodedToken.exp !== 'number')
22
20
  throw new Error('token properties "issuedAt" (iat) or "expiration" (exp) are missing or invalid');
23
21
  var channels = JSON.parse(decodedToken['x-ably-capability']);
24
- return objectAssign({
22
+ var credential = objectAssign({
25
23
  decodedToken: decodedToken,
26
24
  channels: channels
27
25
  }, json);
26
+ // The `/api/v3/auth` endpoint nests the streaming settings under `config.streaming`. Normalize
27
+ // them into the flat `pushEnabled`/`connDelay` properties used by the PushManager.
28
+ var streaming = credential.config && credential.config.streaming;
29
+ if (streaming) {
30
+ credential.pushEnabled = streaming.enabled;
31
+ credential.connDelay = streaming.delay;
32
+ }
33
+ return credential;
28
34
  }
29
35
  return json;
30
36
  });
31
37
  };
32
38
  }
33
- /**
34
- * Returns the hash of a given user key
35
- */
36
- export function hashUserKey(userKey) {
37
- return encodeToBase64(hash(userKey, 0).toString());
38
- }
@@ -1,6 +1,6 @@
1
1
  import { errorParser, messageParser } from './NotificationParser';
2
2
  import { notificationKeeperFactory } from './NotificationKeeper';
3
- import { PUSH_RETRYABLE_ERROR, PUSH_NON_RETRYABLE_ERROR, OCCUPANCY, CONTROL, SEGMENT_UPDATE, SPLIT_KILL, SPLIT_UPDATE, MEMBERSHIPS_MS_UPDATE, MEMBERSHIPS_LS_UPDATE, RB_SEGMENT_UPDATE } from '../constants';
3
+ import { PUSH_RETRYABLE_ERROR, PUSH_NON_RETRYABLE_ERROR, OCCUPANCY, CONTROL, SEGMENT_UPDATE, SPLIT_KILL, SPLIT_UPDATE, CONFIG_UPDATE, MEMBERSHIPS_MS_UPDATE, MEMBERSHIPS_LS_UPDATE, RB_SEGMENT_UPDATE } from '../constants';
4
4
  import { STREAMING_PARSING_ERROR_FAILS, ERROR_STREAMING_SSE, STREAMING_PARSING_MESSAGE_FAILS, STREAMING_NEW_MESSAGE } from '../../../logger/constants';
5
5
  import { ABLY_ERROR, NON_REQUESTED, SSE_CONNECTION_ERROR } from '../../../utils/constants';
6
6
  /**
@@ -71,6 +71,7 @@ export function SSEHandlerFactory(log, pushEmitter, telemetryTracker) {
71
71
  switch (parsedData.type) {
72
72
  /* update events */
73
73
  case SPLIT_UPDATE:
74
+ case CONFIG_UPDATE:
74
75
  case SEGMENT_UPDATE:
75
76
  case MEMBERSHIPS_MS_UPDATE:
76
77
  case MEMBERSHIPS_LS_UPDATE:
@@ -27,6 +27,7 @@ export var MEMBERSHIPS_LS_UPDATE = 'MEMBERSHIPS_LS_UPDATE';
27
27
  export var SEGMENT_UPDATE = 'SEGMENT_UPDATE';
28
28
  export var SPLIT_KILL = 'SPLIT_KILL';
29
29
  export var SPLIT_UPDATE = 'SPLIT_UPDATE';
30
+ export var CONFIG_UPDATE = 'CONFIG_UPDATE';
30
31
  export var RB_SEGMENT_UPDATE = 'RB_SEGMENT_UPDATE';
31
32
  // Control-type push notifications, handled by NotificationKeeper
32
33
  export var CONTROL = 'CONTROL';
@@ -4,11 +4,10 @@ import { SSEHandlerFactory } from './SSEHandler';
4
4
  import { MySegmentsUpdateWorker } from './UpdateWorkers/MySegmentsUpdateWorker';
5
5
  import { SegmentsUpdateWorker } from './UpdateWorkers/SegmentsUpdateWorker';
6
6
  import { DefinitionsUpdateWorker } from './UpdateWorkers/DefinitionsUpdateWorker';
7
- import { authenticateFactory, hashUserKey } from './AuthClient';
8
7
  import { forOwn } from '../../utils/lang';
9
8
  import { SSEClient } from './SSEClient';
10
9
  import { checkIfServerSide, getMatching } from '../../utils/key';
11
- import { MEMBERSHIPS_MS_UPDATE, MEMBERSHIPS_LS_UPDATE, PUSH_NON_RETRYABLE_ERROR, PUSH_SUBSYSTEM_DOWN, SECONDS_BEFORE_EXPIRATION, SEGMENT_UPDATE, SPLIT_KILL, SPLIT_UPDATE, RB_SEGMENT_UPDATE, PUSH_RETRYABLE_ERROR, PUSH_SUBSYSTEM_UP, ControlType } from './constants';
10
+ import { MEMBERSHIPS_MS_UPDATE, MEMBERSHIPS_LS_UPDATE, PUSH_NON_RETRYABLE_ERROR, PUSH_SUBSYSTEM_DOWN, SECONDS_BEFORE_EXPIRATION, SEGMENT_UPDATE, SPLIT_KILL, SPLIT_UPDATE, CONFIG_UPDATE, RB_SEGMENT_UPDATE, PUSH_RETRYABLE_ERROR, PUSH_SUBSYSTEM_UP, ControlType } from './constants';
12
11
  import { STREAMING_FALLBACK, STREAMING_REFRESH_TOKEN, STREAMING_CONNECTING, STREAMING_DISABLED, ERROR_STREAMING_AUTH, STREAMING_DISCONNECTING, STREAMING_RECONNECT, STREAMING_PARSING_MEMBERSHIPS_UPDATE } from '../../logger/constants';
13
12
  import { UpdateStrategy } from './SSEHandler/types';
14
13
  import { getDelay, isInBitmap, parseBitmap, parseCompressedData } from './parseUtils';
@@ -34,7 +33,6 @@ export function pushManagerFactory(params, pollingManager) {
34
33
  log.warn(STREAMING_FALLBACK, [e]);
35
34
  return;
36
35
  }
37
- var authenticate = authenticateFactory(serviceApi.fetchAuth);
38
36
  // init feedback loop
39
37
  var pushEmitter = new platform.EventEmitter();
40
38
  var sseHandler = SSEHandlerFactory(log, pushEmitter, telemetryTracker);
@@ -44,8 +42,6 @@ export function pushManagerFactory(params, pollingManager) {
44
42
  var segmentsUpdateWorker = userKey ? undefined : SegmentsUpdateWorker(log, pollingManager.segmentsSyncTask, storage.segments);
45
43
  // For server-side we pass the segmentsSyncTask, used by DefinitionsUpdateWorker to fetch new segments
46
44
  var definitionsUpdateWorker = DefinitionsUpdateWorker(log, storage, pollingManager.definitionsSyncTask, readiness.definitions, telemetryTracker);
47
- // [Only for client-side] map of hashes to user keys, to dispatch membership update events to the corresponding MySegmentsUpdateWorker
48
- var userKeyHashes = {};
49
45
  // [Only for client-side] map of user keys to their corresponding hash64 and MySegmentsUpdateWorkers.
50
46
  // Hash64 is used to process membership update events and dispatch actions to the corresponding MySegmentsUpdateWorker.
51
47
  var clients = {};
@@ -90,7 +86,7 @@ export function pushManagerFactory(params, pollingManager) {
90
86
  log.info(STREAMING_CONNECTING);
91
87
  disconnected = false;
92
88
  var userKeys = userKey ? Object.keys(clients) : undefined;
93
- authenticate(userKeys).then(function (authData) {
89
+ serviceApi.fetchAuth(userKeys).then(function (authData) {
94
90
  if (disconnected)
95
91
  return;
96
92
  // 'pushEnabled: false' is handled as a PUSH_NON_RETRYABLE_ERROR instead of PUSH_SUBSYSTEM_DOWN, in order to
@@ -178,8 +174,11 @@ export function pushManagerFactory(params, pollingManager) {
178
174
  connectPush();
179
175
  });
180
176
  /** Functions related to synchronization (Queues and Workers in the spec) */
177
+ // Some notification types are specific to the Feature flags or Configs SDKs. Their handlers are
178
+ // registered unconditionally, since the SDK only subscribes to the channels of its own entities.
181
179
  pushEmitter.on(SPLIT_KILL, definitionsUpdateWorker.killDefinition);
182
180
  pushEmitter.on(SPLIT_UPDATE, definitionsUpdateWorker.put);
181
+ pushEmitter.on(CONFIG_UPDATE, definitionsUpdateWorker.put);
183
182
  pushEmitter.on(RB_SEGMENT_UPDATE, definitionsUpdateWorker.put);
184
183
  function handleMySegmentsUpdate(parsedData) {
185
184
  switch (parsedData.u) {
@@ -282,9 +281,7 @@ export function pushManagerFactory(params, pollingManager) {
282
281
  },
283
282
  // [Only for client-side]
284
283
  add: function (userKey, mySegmentsSyncTask) {
285
- var hash = hashUserKey(userKey);
286
- if (!userKeyHashes[hash]) {
287
- userKeyHashes[hash] = userKey;
284
+ if (!clients[userKey]) {
288
285
  clients[userKey] = {
289
286
  hash64: hash64(userKey),
290
287
  worker: MySegmentsUpdateWorker(log, storage, mySegmentsSyncTask, telemetryTracker)
@@ -305,8 +302,6 @@ export function pushManagerFactory(params, pollingManager) {
305
302
  },
306
303
  // [Only for client-side]
307
304
  remove: function (userKey) {
308
- var hash = hashUserKey(userKey);
309
- delete userKeyHashes[hash];
310
305
  delete clients[userKey];
311
306
  }
312
307
  });
@@ -3,40 +3,63 @@ import { impressionsSubmitterFactory } from './impressionsSubmitter';
3
3
  import { impressionCountsSubmitterFactory } from './impressionCountsSubmitter';
4
4
  import { telemetrySubmitterFactory } from './telemetrySubmitter';
5
5
  import { uniqueKeysSubmitterFactory } from './uniqueKeysSubmitter';
6
- export function submitterManagerFactory(params) {
7
- var submitters = [
8
- impressionsSubmitterFactory(params),
9
- eventsSubmitterFactory(params),
10
- impressionCountsSubmitterFactory(params),
11
- uniqueKeysSubmitterFactory(params)
12
- ];
13
- var telemetrySubmitter = telemetrySubmitterFactory(params);
14
- return {
15
- // `onlyTelemetry` true if SDK is created with userConsent not GRANTED
16
- start: function (onlyTelemetry) {
6
+ import { SDK_READY } from '../../readiness/constants';
7
+ /**
8
+ * Creates a submitter manager.
9
+ *
10
+ * @param startWhenReady - If true, the submitters will only start and execute the underlying submitters when the SDK is ready.
11
+ * `true` for Configs SDK, `false` for Flags SDKs.
12
+ */
13
+ export function submitterManagerFactory(startWhenReady) {
14
+ return function (params) {
15
+ var readiness = params.readiness;
16
+ var submitters = [
17
+ impressionsSubmitterFactory(params),
18
+ eventsSubmitterFactory(params),
19
+ impressionCountsSubmitterFactory(params),
20
+ uniqueKeysSubmitterFactory(params)
21
+ ];
22
+ var telemetrySubmitter = telemetrySubmitterFactory(params);
23
+ var isRunning = false;
24
+ function startSubmitters(onlyTelemetry) {
25
+ if (!isRunning)
26
+ return;
17
27
  if (!onlyTelemetry)
18
28
  submitters.forEach(function (submitter) { return submitter.start(); });
19
29
  if (telemetrySubmitter)
20
30
  telemetrySubmitter.start();
21
- },
22
- // `allExceptTelemetry` true if userConsent is changed to DECLINED
23
- stop: function (allExceptTelemetry) {
24
- submitters.forEach(function (submitter) { return submitter.stop(); });
25
- if (!allExceptTelemetry && telemetrySubmitter)
26
- telemetrySubmitter.stop();
27
- },
28
- isRunning: function () {
29
- return submitters.some(function (submitter) { return submitter.isRunning(); });
30
- },
31
- // Flush data. Called with `onlyTelemetry` true if SDK is destroyed with userConsent not GRANTED
32
- execute: function (onlyTelemetry) {
33
- var promises = onlyTelemetry ? [] : submitters.map(function (submitter) { return submitter.execute(); });
34
- if (telemetrySubmitter)
35
- promises.push(telemetrySubmitter.execute());
36
- return Promise.all(promises).then(function () { });
37
- },
38
- isExecuting: function () {
39
- return submitters.some(function (submitter) { return submitter.isExecuting(); });
40
31
  }
32
+ return {
33
+ // `onlyTelemetry` true if SDK is created with userConsent not GRANTED
34
+ start: function (onlyTelemetry) {
35
+ isRunning = true;
36
+ if (!startWhenReady || readiness.isReady())
37
+ startSubmitters(onlyTelemetry);
38
+ else
39
+ readiness.gate.once(SDK_READY, function () { return startSubmitters(onlyTelemetry); });
40
+ },
41
+ // `allExceptTelemetry` true if userConsent is changed to DECLINED
42
+ stop: function (allExceptTelemetry) {
43
+ isRunning = false;
44
+ submitters.forEach(function (submitter) { return submitter.stop(); });
45
+ if (!allExceptTelemetry && telemetrySubmitter)
46
+ telemetrySubmitter.stop();
47
+ },
48
+ isRunning: function () {
49
+ return isRunning;
50
+ },
51
+ // Flush data. Called with `onlyTelemetry` true if SDK is destroyed with userConsent not GRANTED
52
+ execute: function (onlyTelemetry) {
53
+ if (startWhenReady && !readiness.isReady())
54
+ return Promise.resolve();
55
+ var promises = onlyTelemetry ? [] : submitters.map(function (submitter) { return submitter.execute(); });
56
+ if (telemetrySubmitter)
57
+ promises.push(telemetrySubmitter.execute());
58
+ return Promise.all(promises).then(function () { });
59
+ },
60
+ isExecuting: function () {
61
+ return submitters.some(function (submitter) { return submitter.isExecuting(); });
62
+ }
63
+ };
41
64
  };
42
65
  }
@@ -12,8 +12,10 @@ import { usesSegmentsSync } from '../storages/AbstractDefinitionsCacheSync';
12
12
  * @param pollingManagerFactory - allows to specialize the SyncManager for server-side or client-side API by passing
13
13
  * `pollingManagerSSFactory` or `pollingManagerCSFactory` respectively.
14
14
  * @param pushManagerFactory - optional to build a SyncManager with or without streaming support
15
+ * @param submitterManagerFactory - specify the submitter manager factory to use
15
16
  */
16
- export function syncManagerOnlineFactory(pollingManagerFactory, pushManagerFactory) {
17
+ export function syncManagerOnlineFactory(pollingManagerFactory, pushManagerFactory, _submitterManagerFactory) {
18
+ if (_submitterManagerFactory === void 0) { _submitterManagerFactory = submitterManagerFactory(); }
17
19
  /**
18
20
  * SyncManager factory for modular SDK
19
21
  */
@@ -27,7 +29,7 @@ export function syncManagerOnlineFactory(pollingManagerFactory, pushManagerFacto
27
29
  undefined;
28
30
  /** Submitter Manager */
29
31
  // It is not injected as push and polling managers, because at the moment it is required
30
- var submitterManager = submitterManagerFactory(params);
32
+ var submitterManager = _submitterManagerFactory(params);
31
33
  /** Sync Manager logic */
32
34
  function startPolling() {
33
35
  if (pollingManager.isRunning()) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@splitsoftware/splitio-commons",
3
- "version": "3.1.1-rc.4",
3
+ "version": "3.1.1-rc.6",
4
4
  "description": "Split JavaScript SDK common components",
5
5
  "main": "cjs/index.js",
6
6
  "module": "esm/index.js",
@@ -1,4 +1,4 @@
1
- import { serviceApiFactory } from '../services/serviceApi';
1
+ import { splitApiFactory } from '../services/splitApi';
2
2
  import { syncManagerOnlineFactory } from '../sync/syncManagerOnline';
3
3
  import { pushManagerFactory } from '../sync/streaming/pushManager';
4
4
  import { pollingManagerSSFactory } from '../sync/polling/pollingManagerSS';
@@ -11,7 +11,7 @@ const syncManagerOnlineSSFactory = syncManagerOnlineFactory(pollingManagerSSFact
11
11
 
12
12
  export const serverSideModules = {
13
13
  storageFactory: InMemoryStorageFactory,
14
- serviceApiFactory,
14
+ serviceApiFactory: splitApiFactory,
15
15
  syncManagerFactory: syncManagerOnlineSSFactory,
16
16
  sdkManagerFactory,
17
17
  sdkClientMethodFactory,
@@ -1,6 +1,6 @@
1
1
  import { ISplitHttpClient, NetworkError } from './types';
2
- import { IJwtCredentialV3 } from '../sync/streaming/AuthClient/types';
3
- import { authenticateFactory } from '../sync/streaming/AuthClient';
2
+ import { IJwtCredential } from '../sync/streaming/AuthClient/types';
3
+ import { fetchAuthFactory } from '../sync/streaming/AuthClient';
4
4
  import { Backoff } from '../utils/Backoff';
5
5
  import { LOG_PREFIX_SYNC_AUTH } from '../logger/constants';
6
6
  import { ISettings } from '../types';
@@ -9,12 +9,20 @@ import { ITelemetryTracker } from '../trackers/types';
9
9
 
10
10
  const SKEW_SECONDS = 30;
11
11
 
12
- function isExpired(credential: IJwtCredentialV3): boolean {
12
+ function isExpired(credential: IJwtCredential): boolean {
13
13
  return Date.now() / 1000 + SKEW_SECONDS >= credential.decodedToken.exp;
14
14
  }
15
15
 
16
16
  export interface IAuthProvider {
17
- credential(): Promise<IJwtCredentialV3>;
17
+ /**
18
+ * Returns the cached credential, or fetches a new one if there isn't one cached or it's expired,
19
+ * retrying with backoff on recoverable errors. Used by `secureSplitHttpClient` and `serviceApi.fetchAuth`.
20
+ */
21
+ credential(): Promise<IJwtCredential>;
22
+ /**
23
+ * Invalidates/clears the cached credential. Used by `secureSplitHttpClient` in the special case of 401 error,
24
+ * and by `serviceApi.fetchAuth` to force a credential/token refresh.
25
+ */
18
26
  invalidate(): void;
19
27
  stop(): void;
20
28
  }
@@ -27,20 +35,18 @@ export function authProviderFactory(settings: ISettings, splitHttpClient: ISplit
27
35
 
28
36
  const { urls, log } = settings;
29
37
 
30
- function fetchAuth() {
38
+ const fetchAuth = fetchAuthFactory(() => {
31
39
  let url = `${urls.auth}/api/v3/auth?capabilities=config,aiconfig`;
32
40
  return splitHttpClient(url, undefined, telemetryTracker.trackHttp(TOKEN), false, true);
33
- }
34
-
35
- const authenticate = authenticateFactory(fetchAuth);
41
+ });
36
42
  const backoff = new Backoff(fetchCredential);
37
43
 
38
- let cachedCredential: IJwtCredentialV3 | undefined;
39
- let inFlightPromise: Promise<IJwtCredentialV3> | undefined;
44
+ let cachedCredential: IJwtCredential | undefined;
45
+ let inFlightPromise: Promise<IJwtCredential> | undefined;
40
46
  let stopped = false;
41
47
 
42
- function fetchCredential(): Promise<IJwtCredentialV3> {
43
- return authenticate().then((credential: IJwtCredentialV3) => {
48
+ function fetchCredential(): Promise<IJwtCredential> {
49
+ return fetchAuth().then((credential: IJwtCredential) => {
44
50
  log.info(LOG_PREFIX_SYNC_AUTH + 'credential fetched successfully');
45
51
  cachedCredential = credential;
46
52
  inFlightPromise = undefined;
@@ -62,7 +68,7 @@ export function authProviderFactory(settings: ISettings, splitHttpClient: ISplit
62
68
  }
63
69
 
64
70
  return {
65
- credential(): Promise<IJwtCredentialV3> {
71
+ credential(): Promise<IJwtCredential> {
66
72
  if (cachedCredential && !isExpired(cachedCredential)) {
67
73
  return Promise.resolve(cachedCredential);
68
74
  }
@@ -1,22 +1,14 @@
1
- import { IRequestOptions, IResponse, ISecureSplitHttpClient, NetworkError } from './types';
2
- import { ISettings } from '../types';
3
- import { IPlatform } from '../sdkFactory/types';
4
- import { splitHttpClientFactory } from './splitHttpClient';
5
- import { authProviderFactory } from './authProvider';
6
- import { ITelemetryTracker } from '../trackers/types';
1
+ import { IRequestOptions, IResponse, ISecureSplitHttpClient, ISplitHttpClient, NetworkError } from './types';
2
+ import { IAuthProvider } from './authProvider';
7
3
 
8
4
  /**
9
5
  * Factory of Secure HTTP client, which authenticates requests using a JWT token.
10
6
  * On 401 responses, invalidates the cached credential and retries once with a fresh token.
11
7
  *
12
- * @param settings - SDK settings
13
- * @param platform - object containing environment-specific dependencies
14
- * @param fetchAuth - function to fetch auth credentials from the /v2/auth endpoint
8
+ * @param splitHttpClient - `splitHttpClientFactory` to use for making requests
9
+ * @param authProvider - `authProviderFactory` to use for authentication
15
10
  */
16
- export function secureSplitHttpClientFactory(settings: ISettings, platform: Pick<IPlatform, 'getOptions' | 'getFetch'>, telemetryTracker: ITelemetryTracker): ISecureSplitHttpClient {
17
-
18
- const splitHttpClient = splitHttpClientFactory(settings, platform);
19
- const authProvider = authProviderFactory(settings, splitHttpClient, telemetryTracker);
11
+ export function secureSplitHttpClientFactory(splitHttpClient: ISplitHttpClient, authProvider: IAuthProvider): ISecureSplitHttpClient {
20
12
 
21
13
  function makeRequest(url: string, options: IRequestOptions | undefined, latencyTracker: ((error?: NetworkError) => void) | undefined, logErrorsAsInfo: boolean | undefined, token: string): Promise<IResponse> {
22
14
  return splitHttpClient(url, { ...options, headers: { ...options?.headers, Authorization: `Bearer ${token}` } }, latencyTracker, logErrorsAsInfo, true);
@@ -1,40 +1,52 @@
1
1
  import { IPlatform } from '../sdkFactory/types';
2
2
  import { ISettings } from '../types';
3
3
  import { splitHttpClientFactory } from './splitHttpClient';
4
- import { ISecureSplitHttpClient, IServiceApi } from './types';
4
+ import { IServiceApi } from './types';
5
5
  import { objectAssign } from '../utils/lang/objectAssign';
6
6
  import { ITelemetryTracker } from '../trackers/types';
7
- import { SPLITS, IMPRESSIONS, IMPRESSIONS_COUNT, EVENTS, TELEMETRY, TOKEN, SEGMENT, MEMBERSHIPS } from '../utils/constants';
7
+ import { SPLITS, IMPRESSIONS, IMPRESSIONS_COUNT, EVENTS, TELEMETRY, SEGMENT, MEMBERSHIPS } from '../utils/constants';
8
8
  import { ERROR_TOO_MANY_SETS } from '../logger/constants';
9
+ import { authProviderFactory } from './authProvider';
10
+ import { secureSplitHttpClientFactory } from './secureSplitHttpClient';
9
11
 
10
12
  const noCacheHeaderOptions = { headers: { 'Cache-Control': 'no-cache' } };
11
13
 
12
- function userKeyToQueryParam(userKey: string) {
13
- return 'users=' + encodeURIComponent(userKey); // no need to check availability of `encodeURIComponent`, since it is a global highly supported.
14
- }
15
-
16
14
  /**
17
15
  * Factory of ServiceApi objects, which group the collection of HTTP endpoints used by the SDKs
18
16
  *
19
17
  * @param settings - validated settings object
20
18
  * @param platform - object containing environment-specific dependencies
21
19
  * @param telemetryTracker - telemetry tracker
22
- * @param secureSplitHttpClientFactory - factory of SecureSplitHttpClient objects
23
20
  */
24
21
  export function serviceApiFactory(
25
22
  settings: ISettings,
26
23
  platform: Pick<IPlatform, 'getOptions' | 'getFetch'>,
27
24
  telemetryTracker: ITelemetryTracker,
28
- secureSplitHttpClientFactory?: (settings: ISettings, platform: Pick<IPlatform, 'getOptions' | 'getFetch'>, telemetryTracker: ITelemetryTracker) => ISecureSplitHttpClient,
29
25
  ): IServiceApi {
30
26
 
31
27
  const urls = settings.urls;
32
28
  const filterQueryString = settings.sync.__splitFiltersValidation && settings.sync.__splitFiltersValidation.queryString;
33
29
  const SplitSDKImpressionsMode = settings.sync.impressionsMode;
30
+
34
31
  const splitHttpClient = splitHttpClientFactory(settings, platform);
35
- const secureSplitHttpClient = secureSplitHttpClientFactory!(settings, platform, telemetryTracker);
32
+
33
+ // Shared authProvider so that the SSE authentication (via `fetchAuth`,
34
+ // used by `pushManager`) and every authenticated HTTP request to the Configs
35
+ // API reuse the same cached credential instead of each fetching their own.
36
+ const authProvider = authProviderFactory(settings, splitHttpClient, telemetryTracker);
37
+ const secureSplitHttpClient = secureSplitHttpClientFactory(splitHttpClient, authProvider);
38
+
39
+ let initialAuth = true;
36
40
 
37
41
  return {
42
+ fetchAuth() {
43
+ // Guard condition to avoid invalidating the cached credential on the first pushManager authentication
44
+ if (initialAuth) initialAuth = false;
45
+ else authProvider.invalidate();
46
+
47
+ return authProvider.credential();
48
+ },
49
+
38
50
  // @TODO throw errors if health check requests fail, to log them in the Synchronizer
39
51
  getSdkAPIHealthCheck() {
40
52
  const url = `${urls.sdk}/api/version`;
@@ -46,15 +58,6 @@ export function serviceApiFactory(
46
58
  return splitHttpClient(url).then(() => true).catch(() => false);
47
59
  },
48
60
 
49
- fetchAuth(userMatchingKeys?: string[]) {
50
- let url = `${urls.auth}/api/v2/auth?s=${settings.sync.flagSpecVersion}`;
51
- if (userMatchingKeys) { // `userMatchingKeys` is undefined in server-side
52
- const queryParams = userMatchingKeys.map(userKeyToQueryParam).join('&');
53
- if (queryParams) url += '&' + queryParams;
54
- }
55
- return splitHttpClient(url, undefined, telemetryTracker.trackHttp(TOKEN));
56
- },
57
-
58
61
  fetchSplitChanges(since: number, noCache?: boolean, till?: number, rbSince?: number) {
59
62
  const url = `${urls.sdk}/api/splitChanges?s=${settings.sync.flagSpecVersion}&since=${since}${rbSince ? '&rbSince=' + rbSince : ''}${filterQueryString || ''}${till ? '&till=' + till : ''}`;
60
63
  return splitHttpClient(url, noCache ? noCacheHeaderOptions : undefined, telemetryTracker.trackHttp(SPLITS))
@@ -6,6 +6,7 @@ import { objectAssign } from '../utils/lang/objectAssign';
6
6
  import { ITelemetryTracker } from '../trackers/types';
7
7
  import { SPLITS, IMPRESSIONS, IMPRESSIONS_COUNT, EVENTS, TELEMETRY, TOKEN, SEGMENT, MEMBERSHIPS } from '../utils/constants';
8
8
  import { ERROR_TOO_MANY_SETS } from '../logger/constants';
9
+ import { fetchAuthFactory } from '../sync/streaming/AuthClient';
9
10
 
10
11
  const noCacheHeaderOptions = { headers: { 'Cache-Control': 'no-cache' } };
11
12
 
@@ -33,6 +34,15 @@ export function splitApiFactory(
33
34
  const splitHttpClient = splitHttpClientFactory(settings, platform);
34
35
 
35
36
  return {
37
+ fetchAuth: fetchAuthFactory((userMatchingKeys?: string[]) => {
38
+ let url = `${urls.auth}/v2/auth?s=${settings.sync.flagSpecVersion}`;
39
+ if (userMatchingKeys) { // `userMatchingKeys` is undefined in server-side
40
+ const queryParams = userMatchingKeys.map(userKeyToQueryParam).join('&');
41
+ if (queryParams) url += '&' + queryParams;
42
+ }
43
+ return splitHttpClient(url, undefined, telemetryTracker.trackHttp(TOKEN));
44
+ }),
45
+
36
46
  // @TODO throw errors if health check requests fail, to log them in the Synchronizer
37
47
  getSdkAPIHealthCheck() {
38
48
  const url = `${urls.sdk}/version`;
@@ -44,15 +54,6 @@ export function splitApiFactory(
44
54
  return splitHttpClient(url).then(() => true).catch(() => false);
45
55
  },
46
56
 
47
- fetchAuth(userMatchingKeys?: string[]) {
48
- let url = `${urls.auth}/v2/auth?s=${settings.sync.flagSpecVersion}`;
49
- if (userMatchingKeys) { // `userMatchingKeys` is undefined in server-side
50
- const queryParams = userMatchingKeys.map(userKeyToQueryParam).join('&');
51
- if (queryParams) url += '&' + queryParams;
52
- }
53
- return splitHttpClient(url, undefined, telemetryTracker.trackHttp(TOKEN));
54
- },
55
-
56
57
  fetchSplitChanges(since: number, noCache?: boolean, till?: number, rbSince?: number) {
57
58
  const url = `${urls.sdk}/splitChanges?s=${settings.sync.flagSpecVersion}&since=${since}${rbSince ? '&rbSince=' + rbSince : ''}${filterQueryString || ''}${till ? '&till=' + till : ''}`;
58
59
  return splitHttpClient(url, noCache ? noCacheHeaderOptions : undefined, telemetryTracker.trackHttp(SPLITS))
@@ -1,3 +1,5 @@
1
+ import { IFetchAuth } from '../sync/streaming/AuthClient/types';
2
+
1
3
  export type IRequestOptions = {
2
4
  method?: string,
3
5
  headers?: Record<string, string>,
@@ -35,8 +37,6 @@ export type ISplitHttpClient = (url: string, options?: IRequestOptions, latencyT
35
37
 
36
38
  export type ISecureSplitHttpClient = ISplitHttpClient & { stop(): void }
37
39
 
38
- export type IFetchAuth = (userKeys?: string[]) => Promise<IResponse>
39
-
40
40
  export type IFetchDefinitionChanges = (since: number, noCache?: boolean, till?: number, rbSince?: number) => Promise<IResponse>
41
41
 
42
42
  export type IFetchSegmentChanges = (since: number, segmentName: string, noCache?: boolean, till?: number) => Promise<IResponse>
@@ -9,7 +9,7 @@ import { SYNC_FETCH, SYNC_UPDATE, SYNC_FETCH_FAILS, SYNC_FETCH_RETRY } from '../
9
9
  import { startsWith } from '../../../utils/lang';
10
10
  import { IN_RULE_BASED_SEGMENT, IN_SEGMENT, RULE_BASED_SEGMENT, STANDARD_SEGMENT } from '../../../utils/constants';
11
11
  import { setToArray } from '../../../utils/lang/sets';
12
- import { SPLIT_UPDATE } from '../../streaming/constants';
12
+ import { RB_SEGMENT_UPDATE } from '../../streaming/constants';
13
13
  import { SdkUpdateMetadata } from '../../../../types/splitio';
14
14
  import { ISplit } from '../fetchers/splitChangesFetcher';
15
15
  import { ISegmentsSyncTask } from '../types';
@@ -154,14 +154,14 @@ export function definitionChangesUpdaterFactory(
154
154
 
155
155
  return Promise.resolve(
156
156
  instantUpdate ?
157
- instantUpdate.type === SPLIT_UPDATE ?
157
+ instantUpdate.type === RB_SEGMENT_UPDATE ?
158
+ { rbs: convertInstantUpdateToDefinitionChanges(instantUpdate) as IDefinitionChangesResponse['rbs'] } :
158
159
  // IFFU edge case: a change to definition that adds an IN_RULE_BASED_SEGMENT matcher that is not present yet
159
160
  Promise.resolve(rbSegments.contains(parseSegments(instantUpdate.payload, IN_RULE_BASED_SEGMENT))).then((contains) => {
160
161
  return contains ?
161
162
  { d: convertInstantUpdateToDefinitionChanges(instantUpdate) as IDefinitionChangesResponse['d'] } :
162
163
  definitionChangesFetcher(since, noCache, till, rbSince, _promiseDecorator);
163
164
  }) :
164
- { rbs: convertInstantUpdateToDefinitionChanges(instantUpdate) as IDefinitionChangesResponse['rbs'] } :
165
165
  definitionChangesFetcher(since, noCache, till, rbSince, _promiseDecorator)
166
166
  )
167
167
  .then((definitionChanges: IDefinitionChangesResponse) => {
@@ -1,22 +1,20 @@
1
- import { IFetchAuth } from '../../../services/types';
2
- import { IAuthenticate, IJwtCredentialV2 } from './types';
1
+ import { IJwtCredential, IFetchAuth } from './types';
3
2
  import { objectAssign } from '../../../utils/lang/objectAssign';
4
- import { encodeToBase64 } from '../../../utils/base64';
5
3
  import { decodeJWTtoken } from '../../../utils/jwt';
6
- import { hash } from '../../../utils/murmur3/murmur3';
4
+ import { IResponse } from '../../../services/types';
7
5
 
8
6
  /**
9
7
  * Factory of authentication function.
10
8
  *
11
- * @param fetchAuth - `ServiceApi.fetchAuth` endpoint
9
+ * @param fetchAuth - /auth endpoint
12
10
  */
13
- export function authenticateFactory(fetchAuth: IFetchAuth): IAuthenticate {
11
+ export function fetchAuthFactory(fetchAuth: (userKeys?: string[]) => Promise<IResponse>): IFetchAuth {
14
12
 
15
13
  /**
16
14
  * Run authentication requests to Auth Server, and returns a promise that resolves with the decoded JTW token.
17
15
  * @param userKeys - set of user Keys to track membership updates. It is undefined for server-side API.
18
16
  */
19
- return function authenticate(userKeys?: string[]): Promise<IJwtCredentialV2> {
17
+ return function authenticate(userKeys?: string[]): Promise<IJwtCredential> {
20
18
  return fetchAuth(userKeys)
21
19
  .then(resp => resp.json())
22
20
  .then(json => {
@@ -24,19 +22,20 @@ export function authenticateFactory(fetchAuth: IFetchAuth): IAuthenticate {
24
22
  const decodedToken = decodeJWTtoken(json.token);
25
23
  if (typeof decodedToken.iat !== 'number' || typeof decodedToken.exp !== 'number') throw new Error('token properties "issuedAt" (iat) or "expiration" (exp) are missing or invalid');
26
24
  const channels = JSON.parse(decodedToken['x-ably-capability']);
27
- return objectAssign({
25
+ const credential = objectAssign({
28
26
  decodedToken,
29
27
  channels
30
28
  }, json);
29
+ // The `/api/v3/auth` endpoint nests the streaming settings under `config.streaming`. Normalize
30
+ // them into the flat `pushEnabled`/`connDelay` properties used by the PushManager.
31
+ const streaming = credential.config && credential.config.streaming;
32
+ if (streaming) {
33
+ credential.pushEnabled = streaming.enabled;
34
+ credential.connDelay = streaming.delay;
35
+ }
36
+ return credential;
31
37
  }
32
38
  return json;
33
39
  });
34
40
  };
35
41
  }
36
-
37
- /**
38
- * Returns the hash of a given user key
39
- */
40
- export function hashUserKey(userKey: string): string {
41
- return encodeToBase64(hash(userKey, 0).toString());
42
- }