@splitsoftware/splitio-commons 3.1.1-rc.1 → 3.1.1-rc.11

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 +12 -1
  2. package/cjs/evaluator/index.js +11 -6
  3. package/cjs/presets/serverSide.js +3 -3
  4. package/cjs/sdkClient/client.js +3 -5
  5. package/cjs/sdkClient/sdkLifecycle.js +2 -0
  6. package/cjs/services/authProvider.js +23 -10
  7. package/cjs/services/secureSplitHttpClient.js +10 -13
  8. package/cjs/services/serviceApi.js +23 -21
  9. package/cjs/services/splitApi.js +10 -9
  10. package/cjs/storages/inRedis/RedisAdapter.js +8 -5
  11. package/cjs/sync/polling/pollingManagerCS.js +84 -79
  12. package/cjs/sync/polling/pollingManagerSS.js +43 -35
  13. package/cjs/sync/polling/updaters/definitionChangesUpdater.js +2 -2
  14. package/cjs/sync/polling/updaters/segmentChangesUpdater.js +1 -1
  15. package/cjs/sync/streaming/AuthClient/index.js +13 -14
  16. package/cjs/sync/streaming/SSEHandler/index.js +1 -0
  17. package/cjs/sync/streaming/constants.js +2 -1
  18. package/cjs/sync/streaming/pushManager.js +5 -10
  19. package/cjs/sync/submitters/impressionsSubmitter.js +1 -1
  20. package/cjs/sync/syncManagerOnline.js +2 -7
  21. package/cjs/utils/Backoff.js +16 -4
  22. package/esm/evaluator/index.js +11 -6
  23. package/esm/presets/serverSide.js +3 -3
  24. package/esm/sdkClient/client.js +3 -5
  25. package/esm/sdkClient/sdkLifecycle.js +2 -0
  26. package/esm/services/authProvider.js +24 -11
  27. package/esm/services/secureSplitHttpClient.js +10 -13
  28. package/esm/services/serviceApi.js +24 -22
  29. package/esm/services/splitApi.js +10 -9
  30. package/esm/storages/inRedis/RedisAdapter.js +8 -5
  31. package/esm/sync/polling/pollingManagerCS.js +84 -79
  32. package/esm/sync/polling/pollingManagerSS.js +43 -35
  33. package/esm/sync/polling/updaters/definitionChangesUpdater.js +3 -3
  34. package/esm/sync/polling/updaters/segmentChangesUpdater.js +1 -1
  35. package/esm/sync/streaming/AuthClient/index.js +11 -11
  36. package/esm/sync/streaming/SSEHandler/index.js +2 -1
  37. package/esm/sync/streaming/constants.js +1 -0
  38. package/esm/sync/streaming/pushManager.js +6 -11
  39. package/esm/sync/submitters/impressionsSubmitter.js +1 -1
  40. package/esm/sync/syncManagerOnline.js +2 -7
  41. package/esm/utils/Backoff.js +16 -4
  42. package/package.json +6 -7
  43. package/src/dtos/types.ts +1 -1
  44. package/src/evaluator/index.ts +11 -5
  45. package/src/evaluator/types.ts +1 -0
  46. package/src/presets/serverSide.ts +3 -3
  47. package/src/sdkClient/client.ts +3 -4
  48. package/src/sdkClient/sdkLifecycle.ts +3 -0
  49. package/src/services/authProvider.ts +41 -19
  50. package/src/services/secureSplitHttpClient.ts +10 -18
  51. package/src/services/serviceApi.ts +27 -24
  52. package/src/services/splitApi.ts +10 -9
  53. package/src/services/types.ts +5 -4
  54. package/src/storages/inRedis/RedisAdapter.ts +8 -5
  55. package/src/sync/polling/pollingManagerCS.ts +80 -78
  56. package/src/sync/polling/pollingManagerSS.ts +49 -45
  57. package/src/sync/polling/updaters/definitionChangesUpdater.ts +3 -3
  58. package/src/sync/polling/updaters/segmentChangesUpdater.ts +1 -1
  59. package/src/sync/streaming/AuthClient/index.ts +14 -15
  60. package/src/sync/streaming/AuthClient/types.ts +9 -13
  61. package/src/sync/streaming/SSEClient/index.ts +2 -2
  62. package/src/sync/streaming/SSEClient/types.ts +2 -2
  63. package/src/sync/streaming/SSEHandler/index.ts +2 -1
  64. package/src/sync/streaming/SSEHandler/types.ts +2 -2
  65. package/src/sync/streaming/constants.ts +1 -0
  66. package/src/sync/streaming/pushManager.ts +32 -42
  67. package/src/sync/streaming/types.ts +3 -2
  68. package/src/sync/submitters/impressionsSubmitter.ts +3 -3
  69. package/src/sync/submitters/types.ts +2 -2
  70. package/src/sync/syncManagerOnline.ts +2 -8
  71. package/src/utils/Backoff.ts +16 -4
  72. package/types/splitio.d.ts +5 -4
@@ -20,7 +20,7 @@ export function fromImpressionsCollector(sendLabels, data) {
20
20
  b: entry.bucketingKey,
21
21
  pt: entry.pt,
22
22
  properties: entry.properties,
23
- // @ts-expect-error - entityType is not yet public. @TODO: add to SplitIO.ImpressionDTO type
23
+ // @TODO: add entityType to public SplitIO.ImpressionDTO type
24
24
  et: entry.entityType, // Definition type
25
25
  };
26
26
  })
@@ -5,8 +5,6 @@ import { isConsentGranted } from '../consent';
5
5
  import { POLLING, STREAMING, SYNC_MODE_UPDATE } from '../utils/constants';
6
6
  import { SDK_DEFINITIONS_CACHE_LOADED } from '../readiness/constants';
7
7
  import { usesSegmentsSync } from '../storages/AbstractDefinitionsCacheSync';
8
- import { splitChangesFetcherFactory } from './polling/fetchers/splitChangesFetcher';
9
- import { segmentChangesFetcherFactory } from './polling/fetchers/segmentChangesFetcher';
10
8
  /**
11
9
  * Online SyncManager factory.
12
10
  * Can be used for server-side API, and client-side API with or without multiple clients.
@@ -14,18 +12,15 @@ import { segmentChangesFetcherFactory } from './polling/fetchers/segmentChangesF
14
12
  * @param pollingManagerFactory - allows to specialize the SyncManager for server-side or client-side API by passing
15
13
  * `pollingManagerSSFactory` or `pollingManagerCSFactory` respectively.
16
14
  * @param pushManagerFactory - optional to build a SyncManager with or without streaming support
17
- * @param definitionChangesFetcherFactory - optional to replace the default split changes fetcher
18
15
  */
19
- export function syncManagerOnlineFactory(pollingManagerFactory, pushManagerFactory, definitionFetcherFactory, segmentFetcherFactory) {
20
- if (definitionFetcherFactory === void 0) { definitionFetcherFactory = splitChangesFetcherFactory; }
21
- if (segmentFetcherFactory === void 0) { segmentFetcherFactory = function (params) { return segmentChangesFetcherFactory(params.serviceApi.fetchSegmentChanges); }; }
16
+ export function syncManagerOnlineFactory(pollingManagerFactory, pushManagerFactory) {
22
17
  /**
23
18
  * SyncManager factory for modular SDK
24
19
  */
25
20
  return function (params) {
26
21
  var settings = params.settings, _a = params.settings, log = _a.log, streamingEnabled = _a.streamingEnabled, syncEnabled = _a.sync.enabled, telemetryTracker = params.telemetryTracker, storage = params.storage, readiness = params.readiness;
27
22
  /** Polling Manager */
28
- var pollingManager = pollingManagerFactory && pollingManagerFactory(params, definitionFetcherFactory(params), segmentFetcherFactory(params));
23
+ var pollingManager = pollingManagerFactory && pollingManagerFactory(params);
29
24
  /** Push Manager */
30
25
  var pushManager = syncEnabled && streamingEnabled && pollingManager && pushManagerFactory ?
31
26
  pushManagerFactory(params, pollingManager) :
@@ -1,6 +1,7 @@
1
1
  var Backoff = /** @class */ (function () {
2
2
  /**
3
- * Schedule function calls with exponential backoff
3
+ * Schedule function calls with exponential backoff.
4
+ * @param cb - The function to call. Must return a promise if used with `scheduleCallAsync`.
4
5
  */
5
6
  function Backoff(cb, baseMillis, maxMillis) {
6
7
  this.baseMillis = Backoff.__TEST__BASE_MILLIS || baseMillis || Backoff.DEFAULT_BASE_MILLIS;
@@ -17,11 +18,11 @@ var Backoff = /** @class */ (function () {
17
18
  var delayInMillis = Math.min(this.baseMillis * Math.pow(2, this.attempts), this.maxMillis);
18
19
  if (this.timeoutID)
19
20
  clearTimeout(this.timeoutID);
21
+ this.attempts++;
20
22
  this.timeoutID = setTimeout(function () {
21
23
  _this.timeoutID = undefined;
22
24
  _this.cb();
23
25
  }, delayInMillis);
24
- this.attempts++;
25
26
  return delayInMillis;
26
27
  };
27
28
  /**
@@ -35,21 +36,32 @@ var Backoff = /** @class */ (function () {
35
36
  clearTimeout(this.timeoutID);
36
37
  this.attempts++;
37
38
  return new Promise(function (resolve, reject) {
39
+ _this.pending = { resolve: resolve, reject: reject };
38
40
  _this.timeoutID = setTimeout(function () {
39
41
  _this.timeoutID = undefined;
42
+ _this.pending = undefined;
40
43
  _this.cb().then(resolve, reject);
41
44
  }, delayInMillis);
42
45
  });
43
46
  };
44
47
  /**
45
- * Reset the backoff attempts
48
+ * Reset the backoff attempts, canceling any scheduled `cb` call. If a `scheduleCallAsync` call is pending
49
+ * and the caller wants to settle it immediately instead of waiting for `cb` to be invoked, pass `settle`:
50
+ * `{ value }` to resolve, or `{ error }` to reject.
46
51
  */
47
- Backoff.prototype.reset = function () {
52
+ Backoff.prototype.reset = function (settle) {
48
53
  this.attempts = 0;
49
54
  if (this.timeoutID) {
50
55
  clearTimeout(this.timeoutID);
51
56
  this.timeoutID = undefined;
52
57
  }
58
+ if (settle && this.pending) {
59
+ if ('value' in settle)
60
+ this.pending.resolve(settle.value);
61
+ else
62
+ this.pending.reject(settle.error);
63
+ this.pending = undefined;
64
+ }
53
65
  };
54
66
  Backoff.DEFAULT_BASE_MILLIS = 1000; // 1 second
55
67
  Backoff.DEFAULT_MAX_MILLIS = 1800000; // 30 minutes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@splitsoftware/splitio-commons",
3
- "version": "3.1.1-rc.1",
3
+ "version": "3.1.1-rc.11",
4
4
  "description": "Split JavaScript SDK common components",
5
5
  "main": "cjs/index.js",
6
6
  "module": "esm/index.js",
@@ -20,8 +20,8 @@
20
20
  "check:lint": "eslint src types --ext .js,.ts",
21
21
  "check:types": "tsc --noEmit",
22
22
  "build": "npm run build:cjs && npm run build:esm",
23
- "build:esm": "rimraf esm && tsc -m es2015 --outDir esm",
24
- "build:cjs": "rimraf cjs && tsc -m CommonJS --outDir cjs",
23
+ "build:esm": "rimraf esm && tsc -p tsconfig.build.json -m es2015 --outDir esm",
24
+ "build:cjs": "rimraf cjs && tsc -p tsconfig.build.json -m CommonJS --outDir cjs",
25
25
  "test": "jest",
26
26
  "test:coverage": "jest --coverage",
27
27
  "all": "npm run check && npm run build && npm run test",
@@ -45,12 +45,12 @@
45
45
  "bugs": "https://github.com/splitio/javascript-commons/issues",
46
46
  "homepage": "https://github.com/splitio/javascript-commons#readme",
47
47
  "dependencies": {
48
- "@types/ioredis": "^4.28.0",
48
+ "@types/node": ">=14.0.0",
49
49
  "tslib": "^2.3.1"
50
50
  },
51
51
  "peerDependencies": {
52
52
  "bloom-filters": "^3.0.0",
53
- "ioredis": "^4.28.0 || ^5.0.0",
53
+ "ioredis": "^4.28.0 || ^5.0.0 || ^6.0.0",
54
54
  "node-fetch": "^2.7.0"
55
55
  },
56
56
  "peerDependenciesMeta": {
@@ -65,7 +65,6 @@
65
65
  }
66
66
  },
67
67
  "devDependencies": {
68
- "@types/ioredis": "^4.28.0",
69
68
  "@types/jest": "^27.0.0",
70
69
  "@types/lodash": "^4.14.162",
71
70
  "@typescript-eslint/eslint-plugin": "^7.18.0",
@@ -77,7 +76,7 @@
77
76
  "eslint-plugin-import": "^2.25.3",
78
77
  "eslint-plugin-tsdoc": "^0.3.0",
79
78
  "fetch-mock": "^9.11.0",
80
- "ioredis": "^5.0.0",
79
+ "ioredis": "^6.0.0",
81
80
  "jest": "^27.2.3",
82
81
  "jest-localstorage-mock": "^2.4.3",
83
82
  "lodash": "^4.17.21",
package/src/dtos/types.ts CHANGED
@@ -236,7 +236,7 @@ export interface IDefinition extends TargetingEntity {
236
236
  } | null;
237
237
  /** Definition classification. Absent means a feature flag. */
238
238
  type?: ConfigType;
239
- /** Only meaningful when `type === 'AI'`. */
239
+ /** Only meaningful when `type === 'ai'`. */
240
240
  subtype?: ConfigSubtype;
241
241
  }
242
242
 
@@ -31,6 +31,13 @@ function treatmentsException(definitionNames: string[]) {
31
31
  return evaluations;
32
32
  }
33
33
 
34
+ function setEvaluationDataFromDefinition(evaluation: IEvaluation, definition: IDefinition): IEvaluation {
35
+ return objectAssign(evaluation, {
36
+ definition,
37
+ config: definition.configurations && definition.configurations[evaluation.treatment] || null
38
+ });
39
+ }
40
+
34
41
  export function evaluateFeature(
35
42
  log: ILogger,
36
43
  key: SplitIO.SplitKey,
@@ -137,8 +144,8 @@ function getEvaluation(
137
144
  const evaluation = split.getTreatment(key, attributes, evaluateFeature);
138
145
 
139
146
  return thenable(evaluation) ?
140
- evaluation.then(result => objectAssign(result, { definition })) :
141
- objectAssign(evaluation, { definition });
147
+ evaluation.then(result => setEvaluationDataFromDefinition(result, definition)) :
148
+ setEvaluationDataFromDefinition(evaluation, definition);
142
149
  }
143
150
 
144
151
  return EVALUATION_DEFINITION_NOT_FOUND;
@@ -187,11 +194,10 @@ function getDefaultTreatment(
187
194
  definition: IDefinition | null,
188
195
  ): MaybeThenable<IEvaluation> {
189
196
  if (definition) {
190
- return {
197
+ return setEvaluationDataFromDefinition({
191
198
  treatment: definition.defaultTreatment,
192
199
  label: NO_CONDITION_MATCH, // "default rule"
193
- definition,
194
- };
200
+ }, definition);
195
201
  }
196
202
 
197
203
  return EVALUATION_DEFINITION_NOT_FOUND;
@@ -22,6 +22,7 @@ export interface IEvaluation {
22
22
  treatment: string,
23
23
  label: string,
24
24
  definition?: IDefinition
25
+ config?: string | null | SplitIO.JsonObject
25
26
  }
26
27
 
27
28
  export type IDefinitionEvaluator = (log: ILogger, key: SplitIO.SplitKey, definitionName: string, attributes: SplitIO.Attributes | undefined, storage: IStorageSync | IStorageAsync) => MaybeThenable<IEvaluation>
@@ -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';
@@ -7,11 +7,11 @@ import { sdkManagerFactory } from '../sdkManager';
7
7
  import { sdkClientMethodFactory } from '../sdkClient/sdkClientMethod';
8
8
  import { impressionObserverSSFactory } from '../trackers/impressionObserver/impressionObserverSS';
9
9
 
10
- const syncManagerOnlineSSFactory = syncManagerOnlineFactory(pollingManagerSSFactory, pushManagerFactory);
10
+ const syncManagerOnlineSSFactory = syncManagerOnlineFactory(pollingManagerSSFactory(), pushManagerFactory);
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,
@@ -142,8 +142,7 @@ export function clientFactory(params: ISdkFactoryContext): SplitIO.IClient | Spl
142
142
  const bucketingKey = getBucketing(key);
143
143
 
144
144
  const { definition } = evaluation;
145
- let { treatment, label } = evaluation;
146
- let config = definition?.configurations?.[treatment] || null;
145
+ let { treatment, label, config = null } = evaluation;
147
146
 
148
147
  if (treatment === CONTROL) {
149
148
  const fallbackTreatment = fallbackCalculator(featureFlagName, label);
@@ -162,10 +161,10 @@ export function clientFactory(params: ISdkFactoryContext): SplitIO.IClient | Spl
162
161
  time: Date.now(),
163
162
  bucketingKey,
164
163
  label,
165
- changeNumber: definition!.changeNumber,
164
+ changeNumber: definition?.changeNumber,
166
165
  properties: stringify(options.properties)
167
166
  }, // @ts-expect-error impressionsDisabled is not exposed in the public typings yet.
168
- disabled: options.impressionsDisabled || definition!.impressionsDisabled
167
+ disabled: options.impressionsDisabled || definition?.impressionsDisabled
169
168
  });
170
169
  }
171
170
 
@@ -69,6 +69,9 @@ export function sdkLifecycleFactory(params: ISdkFactoryContext, isSharedClient?:
69
69
 
70
70
  // Stop background jobs
71
71
  syncManager && syncManager.stop();
72
+
73
+ // Stop the service API (the auth provider backoff retries specifically) before flushing,
74
+ // so that a pending/never-resolved auth retry doesn't block the flush forever
72
75
  serviceApi && serviceApi.stop();
73
76
 
74
77
  return __flush().then(() => {
@@ -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,24 @@ 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
+ function noCachedCredentialError() {
17
+ return new Error(LOG_PREFIX_SYNC_AUTH + 'stopped without a cached credential');
18
+ }
19
+
16
20
  export interface IAuthProvider {
17
- credential(): Promise<IJwtCredentialV3>;
21
+ /**
22
+ * Returns the cached credential, or fetches a new one if there isn't one cached or it's expired,
23
+ * retrying with backoff on recoverable errors. Used by `secureSplitHttpClient` and `serviceApi.fetchAuth`.
24
+ */
25
+ credential(): Promise<IJwtCredential>;
26
+ /**
27
+ * Invalidates/clears the cached credential. Used by `secureSplitHttpClient` in the special case of 401 error,
28
+ * and by `serviceApi.fetchAuth` to force a credential/token refresh.
29
+ */
18
30
  invalidate(): void;
19
31
  stop(): void;
20
32
  }
@@ -27,28 +39,27 @@ export function authProviderFactory(settings: ISettings, splitHttpClient: ISplit
27
39
 
28
40
  const { urls, log } = settings;
29
41
 
30
- function fetchAuth() {
42
+ const fetchAuth = fetchAuthFactory(() => {
31
43
  let url = `${urls.auth}/api/v3/auth?capabilities=config,aiconfig`;
32
44
  return splitHttpClient(url, undefined, telemetryTracker.trackHttp(TOKEN), false, true);
33
- }
34
-
35
- const authenticate = authenticateFactory(fetchAuth);
45
+ });
36
46
  const backoff = new Backoff(fetchCredential);
37
47
 
38
- let cachedCredential: IJwtCredentialV3 | undefined;
39
- let inFlightPromise: Promise<IJwtCredentialV3> | undefined;
48
+ let cachedCredential: IJwtCredential | undefined;
49
+ let inFlightPromise: Promise<IJwtCredential> | undefined;
40
50
  let stopped = false;
41
51
 
42
- function fetchCredential(): Promise<IJwtCredentialV3> {
43
- return authenticate().then((credential: IJwtCredentialV3) => {
52
+ function fetchCredential(): Promise<IJwtCredential> {
53
+ return fetchAuth().then((credential: IJwtCredential) => {
44
54
  log.info(LOG_PREFIX_SYNC_AUTH + 'credential fetched successfully');
45
55
  cachedCredential = credential;
46
56
  inFlightPromise = undefined;
47
57
  backoff.reset();
48
58
  return credential;
49
59
  }).catch((error: NetworkError) => {
50
- // Avoid rejected promises and unnecessary retries after stop()
51
- if (stopped) return cachedCredential!;
60
+ // Avoid unnecessary retries after stop(): fall back to any cached credential, or reject
61
+ // (never resolve with `undefined`, which callers would otherwise send as an unauthenticated request).
62
+ if (stopped) return cachedCredential || Promise.reject(noCachedCredentialError());
52
63
 
53
64
  if (error.statusCode && error.statusCode >= 400 && error.statusCode < 500) {
54
65
  log.error(LOG_PREFIX_SYNC_AUTH + 'non-retryable error fetching credential (status ' + error.statusCode + '): ' + error.message);
@@ -62,14 +73,24 @@ export function authProviderFactory(settings: ISettings, splitHttpClient: ISplit
62
73
  }
63
74
 
64
75
  return {
65
- credential(): Promise<IJwtCredentialV3> {
76
+ credential(): Promise<IJwtCredential> {
66
77
  if (cachedCredential && !isExpired(cachedCredential)) {
67
78
  return Promise.resolve(cachedCredential);
68
79
  }
69
80
 
81
+ // Reuse any fetch already in flight (e.g. one started just before stop()), instead of firing a redundant one.
82
+ if (inFlightPromise) return inFlightPromise;
83
+
84
+ // No cached credential to fall back to and stopped: reject so callers don't send an unauthenticated request.
85
+ if (stopped && !cachedCredential) {
86
+ return Promise.reject(noCachedCredentialError());
87
+ }
88
+
70
89
  if (cachedCredential) log.debug(LOG_PREFIX_SYNC_AUTH + 'cached credential expired');
71
90
 
72
- return inFlightPromise || (inFlightPromise = fetchCredential());
91
+ // If stopped, this is a last, bounded refetch: fetchCredential()'s catch short-circuits (no backoff retry)
92
+ // once stopped, so it's a single attempt that falls back to the stale cached credential on failure.
93
+ return inFlightPromise = fetchCredential();
73
94
  },
74
95
 
75
96
  invalidate() {
@@ -78,9 +99,10 @@ export function authProviderFactory(settings: ISettings, splitHttpClient: ISplit
78
99
 
79
100
  stop() {
80
101
  stopped = true;
81
- cachedCredential = undefined;
82
- inFlightPromise = undefined;
83
- backoff.reset();
102
+ // Keep any in-flight fetch and already-cached credential usable (e.g. by a final flush on destroy).
103
+ // If a retry is only pending (not actively in flight), settle it immediately instead of waiting out the
104
+ // backoff delay: with the cached credential if there is one, or rejected otherwise.
105
+ backoff.reset(cachedCredential ? { value: cachedCredential } : { error: noCachedCredentialError() });
84
106
  }
85
107
  };
86
108
  }
@@ -1,36 +1,28 @@
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 {
11
+ export function secureSplitHttpClientFactory(splitHttpClient: ISplitHttpClient, authProvider: IAuthProvider): ISecureSplitHttpClient {
17
12
 
18
- const splitHttpClient = splitHttpClientFactory(settings, platform);
19
- const authProvider = authProviderFactory(settings, splitHttpClient, telemetryTracker);
20
-
21
- function makeRequest(url: string, options: IRequestOptions | undefined, latencyTracker: ((error?: NetworkError) => void) | undefined, logErrorsAsInfo: boolean | undefined, token: string): Promise<IResponse> {
22
- return splitHttpClient(url, { ...options, headers: { ...options?.headers, Authorization: `Bearer ${token}` } }, latencyTracker, logErrorsAsInfo, true);
13
+ function makeRequest(url: string, options: IRequestOptions | undefined, latencyTracker?: (error?: NetworkError) => void, logErrorsAsInfo?: boolean, newVersionHeader?: boolean, token?: string): Promise<IResponse> {
14
+ return splitHttpClient(url, token ? { ...options, headers: { ...options?.headers, Authorization: `Bearer ${token}` } } : options, latencyTracker, logErrorsAsInfo, newVersionHeader);
23
15
  }
24
16
 
25
- const httpClient = function (url: string, options?: IRequestOptions, latencyTracker?: (error?: NetworkError) => void, logErrorsAsInfo?: boolean): Promise<IResponse> {
17
+ const httpClient = function (url: string, options?: IRequestOptions, latencyTracker?: (error?: NetworkError) => void, logErrorsAsInfo?: boolean, newVersionHeader = true, useJwt = true): Promise<IResponse> {
26
18
  return authProvider.credential().then(credential => {
27
- return makeRequest(url, options, latencyTracker, logErrorsAsInfo, credential.token)
19
+ return makeRequest(url, options, latencyTracker, logErrorsAsInfo, newVersionHeader, useJwt ? credential.token : undefined)
28
20
  .catch((error: NetworkError) => {
29
21
  if (error.statusCode === 401) {
30
22
  // retry once for 401, in case the token has just expired
31
23
  authProvider.invalidate();
32
24
  return authProvider.credential().then(newCredential => {
33
- return makeRequest(url, options, latencyTracker, logErrorsAsInfo, newCredential.token);
25
+ return makeRequest(url, options, latencyTracker, logErrorsAsInfo, newVersionHeader, useJwt ? newCredential.token : undefined);
34
26
  });
35
27
  }
36
28
  throw error;
@@ -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))
@@ -99,7 +102,7 @@ export function serviceApiFactory(
99
102
  */
100
103
  postEventsBulk(body: string, headers?: Record<string, string>) {
101
104
  const url = `${urls.events}/api/events/bulk`;
102
- return splitHttpClient(url, { method: 'POST', body, headers }, telemetryTracker.trackHttp(EVENTS));
105
+ return secureSplitHttpClient(url, { method: 'POST', body, headers }, telemetryTracker.trackHttp(EVENTS), false, false, false);
103
106
  },
104
107
 
105
108
  /**
@@ -110,10 +113,10 @@ export function serviceApiFactory(
110
113
  */
111
114
  postTestImpressionsBulk(body: string, headers?: Record<string, string>) {
112
115
  const url = `${urls.events}/api/testImpressions/bulk`;
113
- return splitHttpClient(url, {
116
+ return secureSplitHttpClient(url, {
114
117
  // Adding extra headers to send impressions in OPTIMIZED or DEBUG modes.
115
118
  method: 'POST', body, headers: objectAssign({ SplitSDKImpressionsMode }, headers)
116
- }, telemetryTracker.trackHttp(IMPRESSIONS));
119
+ }, telemetryTracker.trackHttp(IMPRESSIONS), false, false, false);
117
120
  },
118
121
 
119
122
  /**
@@ -124,7 +127,7 @@ export function serviceApiFactory(
124
127
  */
125
128
  postTestImpressionsCount(body: string, headers?: Record<string, string>) {
126
129
  const url = `${urls.events}/api/testImpressions/count`;
127
- return splitHttpClient(url, { method: 'POST', body, headers }, telemetryTracker.trackHttp(IMPRESSIONS_COUNT));
130
+ return secureSplitHttpClient(url, { method: 'POST', body, headers }, telemetryTracker.trackHttp(IMPRESSIONS_COUNT), false, false, false);
128
131
  },
129
132
 
130
133
  /**
@@ -135,7 +138,7 @@ export function serviceApiFactory(
135
138
  */
136
139
  postUniqueKeysBulkCs(body: string, headers?: Record<string, string>) {
137
140
  const url = `${urls.telemetry}/api/v1/keys/cs`;
138
- return splitHttpClient(url, { method: 'POST', body, headers }, telemetryTracker.trackHttp(TELEMETRY));
141
+ return secureSplitHttpClient(url, { method: 'POST', body, headers }, telemetryTracker.trackHttp(TELEMETRY), false, false, false);
139
142
  },
140
143
 
141
144
  /**
@@ -146,7 +149,7 @@ export function serviceApiFactory(
146
149
  */
147
150
  postUniqueKeysBulkSs(body: string, headers?: Record<string, string>) {
148
151
  const url = `${urls.telemetry}/api/v1/keys/ss`;
149
- return splitHttpClient(url, { method: 'POST', body, headers }, telemetryTracker.trackHttp(TELEMETRY));
152
+ return secureSplitHttpClient(url, { method: 'POST', body, headers }, telemetryTracker.trackHttp(TELEMETRY), false, false, false);
150
153
  },
151
154
 
152
155
  postMetricsConfig(body: string, headers?: Record<string, string>) {
@@ -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>,
@@ -33,9 +35,8 @@ export type IHealthCheckAPI = () => Promise<boolean>
33
35
 
34
36
  export type ISplitHttpClient = (url: string, options?: IRequestOptions, latencyTracker?: (error?: NetworkError) => void, logErrorsAsInfo?: boolean, newVersionHeader?: boolean) => Promise<IResponse>
35
37
 
36
- export type ISecureSplitHttpClient = ISplitHttpClient & { stop(): void }
37
-
38
- export type IFetchAuth = (userKeys?: string[]) => Promise<IResponse>
38
+ // @TODO: remove `newVersionHeader` and `useJwt` arguments when all service endpoints are migrated to the new auth mechanism
39
+ export type ISecureSplitHttpClient = ((url: string, options?: IRequestOptions, latencyTracker?: (error?: NetworkError) => void, logErrorsAsInfo?: boolean, newVersionHeader?: boolean, useJwt?: boolean) => Promise<IResponse>) & { stop(): void }
39
40
 
40
41
  export type IFetchDefinitionChanges = (since: number, noCache?: boolean, till?: number, rbSince?: number) => Promise<IResponse>
41
42
 
@@ -73,7 +74,7 @@ export interface IServiceApi {
73
74
  postTestImpressionsCount: IPostTestImpressionsCount
74
75
  postMetricsConfig: IPostMetricsConfig
75
76
  postMetricsUsage: IPostMetricsUsage
76
- // lifecycle
77
+ // lifecycle: stops authProvider backoff retries
77
78
  stop(): void
78
79
  }
79
80