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

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 (77) hide show
  1. package/CHANGES.txt +3 -0
  2. package/cjs/services/authProvider.js +2 -2
  3. package/cjs/{sync/streaming/AuthClient/index.js → services/fetchAuth.js} +2 -2
  4. package/cjs/services/splitApi.js +2 -2
  5. package/cjs/storages/AbstractDefinitionsCacheAsync.js +2 -1
  6. package/cjs/storages/AbstractDefinitionsCacheSync.js +2 -2
  7. package/cjs/storages/inLocalStorage/DefinitionsCacheInLocal.js +4 -1
  8. package/cjs/storages/inLocalStorage/RBSegmentsCacheInLocal.js +6 -3
  9. package/cjs/storages/inMemory/DefinitionsCacheInMemory.js +4 -1
  10. package/cjs/storages/inMemory/RBSegmentsCacheInMemory.js +6 -3
  11. package/cjs/storages/inRedis/DefinitionsCacheInRedis.js +12 -8
  12. package/cjs/storages/inRedis/RBSegmentsCacheInRedis.js +21 -11
  13. package/cjs/storages/pluggable/DefinitionsCachePluggable.js +12 -8
  14. package/cjs/storages/pluggable/RBSegmentsCachePluggable.js +21 -11
  15. package/cjs/sync/offline/splitsParser/definitionChangesBuilder.js +37 -0
  16. package/cjs/sync/offline/splitsParser/splitsParserFromSettings.js +5 -32
  17. package/cjs/sync/offline/syncTasks/fromObjectSyncTask.js +26 -46
  18. package/cjs/sync/polling/fetchers/splitChangesFetcher.js +1 -1
  19. package/cjs/sync/polling/updaters/definitionChangesUpdater.js +22 -15
  20. package/cjs/sync/streaming/UpdateWorkers/DefinitionsUpdateWorker.js +6 -4
  21. package/cjs/sync/streaming/parseUtils.js +4 -4
  22. package/cjs/sync/streaming/pushManager.js +4 -3
  23. package/esm/services/authProvider.js +1 -1
  24. package/esm/{sync/streaming/AuthClient/index.js → services/fetchAuth.js} +2 -2
  25. package/esm/services/splitApi.js +1 -1
  26. package/esm/storages/AbstractDefinitionsCacheAsync.js +2 -1
  27. package/esm/storages/AbstractDefinitionsCacheSync.js +2 -2
  28. package/esm/storages/inLocalStorage/DefinitionsCacheInLocal.js +4 -1
  29. package/esm/storages/inLocalStorage/RBSegmentsCacheInLocal.js +6 -3
  30. package/esm/storages/inMemory/DefinitionsCacheInMemory.js +4 -1
  31. package/esm/storages/inMemory/RBSegmentsCacheInMemory.js +6 -3
  32. package/esm/storages/inRedis/DefinitionsCacheInRedis.js +12 -8
  33. package/esm/storages/inRedis/RBSegmentsCacheInRedis.js +21 -11
  34. package/esm/storages/pluggable/DefinitionsCachePluggable.js +12 -8
  35. package/esm/storages/pluggable/RBSegmentsCachePluggable.js +21 -11
  36. package/esm/sync/offline/splitsParser/definitionChangesBuilder.js +33 -0
  37. package/esm/sync/offline/splitsParser/splitsParserFromSettings.js +6 -33
  38. package/esm/sync/offline/syncTasks/fromObjectSyncTask.js +23 -43
  39. package/esm/sync/polling/fetchers/splitChangesFetcher.js +1 -1
  40. package/esm/sync/polling/updaters/definitionChangesUpdater.js +22 -15
  41. package/esm/sync/streaming/UpdateWorkers/DefinitionsUpdateWorker.js +8 -6
  42. package/esm/sync/streaming/parseUtils.js +2 -2
  43. package/esm/sync/streaming/pushManager.js +4 -3
  44. package/package.json +1 -1
  45. package/src/dtos/types.ts +4 -4
  46. package/src/services/authProvider.ts +2 -2
  47. package/src/{sync/streaming/AuthClient/index.ts → services/fetchAuth.ts} +3 -4
  48. package/src/services/splitApi.ts +1 -1
  49. package/src/services/types.ts +21 -4
  50. package/src/storages/AbstractDefinitionsCacheAsync.ts +5 -4
  51. package/src/storages/AbstractDefinitionsCacheSync.ts +4 -4
  52. package/src/storages/inLocalStorage/DefinitionsCacheInLocal.ts +5 -3
  53. package/src/storages/inLocalStorage/RBSegmentsCacheInLocal.ts +7 -5
  54. package/src/storages/inMemory/DefinitionsCacheInMemory.ts +3 -2
  55. package/src/storages/inMemory/RBSegmentsCacheInMemory.ts +5 -4
  56. package/src/storages/inRedis/DefinitionsCacheInRedis.ts +15 -11
  57. package/src/storages/inRedis/RBSegmentsCacheInRedis.ts +26 -15
  58. package/src/storages/pluggable/DefinitionsCachePluggable.ts +14 -9
  59. package/src/storages/pluggable/RBSegmentsCachePluggable.ts +24 -13
  60. package/src/storages/types.ts +6 -6
  61. package/src/sync/offline/splitsParser/definitionChangesBuilder.ts +49 -0
  62. package/src/sync/offline/splitsParser/splitsParserFromSettings.ts +8 -38
  63. package/src/sync/offline/splitsParser/types.ts +7 -3
  64. package/src/sync/offline/syncTasks/fromObjectSyncTask.ts +25 -45
  65. package/src/sync/polling/fetchers/splitChangesFetcher.ts +5 -5
  66. package/src/sync/polling/types.ts +22 -1
  67. package/src/sync/polling/updaters/definitionChangesUpdater.ts +35 -33
  68. package/src/sync/streaming/SSEClient/index.ts +1 -1
  69. package/src/sync/streaming/SSEClient/types.ts +1 -1
  70. package/src/sync/streaming/UpdateWorkers/DefinitionsUpdateWorker.ts +17 -13
  71. package/src/sync/streaming/UpdateWorkers/types.ts +6 -0
  72. package/src/sync/streaming/parseUtils.ts +3 -4
  73. package/src/sync/streaming/pushManager.ts +4 -3
  74. package/src/sync/syncManagerOnline.ts +0 -1
  75. package/cjs/sync/streaming/AuthClient/types.js +0 -2
  76. package/esm/sync/streaming/AuthClient/types.js +0 -1
  77. package/src/sync/streaming/AuthClient/types.ts +0 -19
@@ -1,6 +1,4 @@
1
- import { forOwn } from '../../../utils/lang';
2
1
  import { syncTaskFactory } from '../../syncTask';
3
- import { CONTROL } from '../../../utils/constants';
4
2
  import { SDK_DEFINITIONS_ARRIVED, SDK_SEGMENTS_ARRIVED, SDK_DEFINITIONS_CACHE_LOADED, FLAGS_UPDATE, SEGMENTS_UPDATE } from '../../../readiness/constants';
5
3
  import { SYNC_OFFLINE_DATA, ERROR_SYNC_OFFLINE_LOADING } from '../../../logger/constants';
6
4
  /**
@@ -10,53 +8,35 @@ export function fromObjectUpdaterFactory(splitsParser, storage, readiness, setti
10
8
  var log = settings.log, definitions = storage.definitions;
11
9
  var startingUp = true;
12
10
  return function objectUpdater() {
13
- var splits = [];
14
- var loadError = null;
15
- var splitsMock = {};
11
+ var changes;
16
12
  try {
17
- splitsMock = splitsParser(settings);
13
+ changes = splitsParser(settings);
18
14
  }
19
15
  catch (err) {
20
- loadError = err;
21
16
  log.error(ERROR_SYNC_OFFLINE_LOADING, [err]);
22
17
  }
23
- if (!loadError && splitsMock) {
24
- log.debug(SYNC_OFFLINE_DATA, [JSON.stringify(splitsMock)]);
25
- forOwn(splitsMock, function (val, name) {
26
- // @ts-ignore Split changeNumber and seed is undefined in localhost mode
27
- splits.push({
28
- name: name,
29
- status: 'ACTIVE',
30
- killed: false,
31
- trafficAllocation: 100,
32
- defaultTreatment: CONTROL,
33
- conditions: val.conditions || [],
34
- configurations: val.configurations,
35
- trafficTypeName: val.trafficTypeName
36
- });
37
- });
38
- return Promise.all([
39
- definitions.clear(),
40
- definitions.update(splits, [], Date.now())
41
- ]).then(function () {
42
- readiness.definitions.emit(SDK_DEFINITIONS_ARRIVED, { type: FLAGS_UPDATE, names: [] });
43
- if (startingUp) {
44
- startingUp = false;
45
- Promise.resolve(storage.validateCache ? storage.validateCache() : { initialCacheLoad: true /* Fallback: assume initial load when validateCache doesn't exist */ }).then(function (cacheMetadata) {
46
- // Emits SDK_READY_FROM_CACHE
47
- if (!cacheMetadata.initialCacheLoad) {
48
- readiness.definitions.emit(SDK_DEFINITIONS_CACHE_LOADED, cacheMetadata);
49
- }
50
- // Emits SDK_READY
51
- readiness.segments.emit(SDK_SEGMENTS_ARRIVED, { type: SEGMENTS_UPDATE, names: [] });
52
- });
53
- }
54
- return true;
55
- });
56
- }
57
- else {
18
+ if (!changes || !changes.d)
58
19
  return Promise.resolve(true);
59
- }
20
+ var _a = changes.d, updated = _a.updated, removed = _a.removed, till = _a.till;
21
+ log.debug(SYNC_OFFLINE_DATA, [JSON.stringify(changes.d)]);
22
+ // On start, clear definitions that might be in the storage from a previous session (InLocalStorage), since the parser only tracks its own changes.
23
+ return Promise.resolve(startingUp && definitions.clear()).then(function () {
24
+ return definitions.update(updated, removed, till);
25
+ }).then(function (names) {
26
+ readiness.definitions.emit(SDK_DEFINITIONS_ARRIVED, { type: FLAGS_UPDATE, names: names });
27
+ if (startingUp) {
28
+ startingUp = false;
29
+ Promise.resolve(storage.validateCache ? storage.validateCache() : { initialCacheLoad: true /* Fallback: assume initial load when validateCache doesn't exist */ }).then(function (cacheMetadata) {
30
+ // Emits SDK_READY_FROM_CACHE
31
+ if (!cacheMetadata.initialCacheLoad) {
32
+ readiness.definitions.emit(SDK_DEFINITIONS_CACHE_LOADED, cacheMetadata);
33
+ }
34
+ // Emits SDK_READY
35
+ readiness.segments.emit(SDK_SEGMENTS_ARRIVED, { type: SEGMENTS_UPDATE, names: [] });
36
+ });
37
+ }
38
+ return true;
39
+ });
60
40
  };
61
41
  }
62
42
  /**
@@ -13,7 +13,7 @@ function isSplitChangesLegacyResponse(data) {
13
13
  function partitionByStatus(items, till, since) {
14
14
  var updated = [], removed = [];
15
15
  items.forEach(function (item) { return item.status === 'ARCHIVED' ? removed.push(item.name) : updated.push(item); });
16
- return { updated: updated, removed: removed, till: till, since: since };
16
+ return { updated: updated, removed: removed, till: till, since: since !== null && since !== void 0 ? since : undefined };
17
17
  }
18
18
  /**
19
19
  * Factory of SplitChanges fetcher.
@@ -5,6 +5,7 @@ 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
7
  import { RB_SEGMENT_UPDATE } from '../../streaming/constants';
8
+ import { thenable } from '../../../utils/promise/thenable';
8
9
  /**
9
10
  * Collect segments from a raw FF or RBS definition.
10
11
  * Exported for testing purposes.
@@ -67,9 +68,8 @@ export function computeMutation(update, segments, filters) {
67
68
  else {
68
69
  accum.removed.push(ruleEntity.name);
69
70
  }
70
- accum.names.push(ruleEntity.name);
71
71
  return accum;
72
- }, { updated: [], removed: update.removed, names: update.removed.slice() });
72
+ }, { updated: [], removed: update.removed });
73
73
  }
74
74
  function convertInstantUpdateToDefinitionChanges(instantUpdate) {
75
75
  var payload = instantUpdate.payload, changeNumber = instantUpdate.changeNumber;
@@ -110,7 +110,7 @@ export function definitionChangesUpdaterFactory(log, definitionChangesFetcher, s
110
110
  * @param noCache - true to revalidate data to fetch
111
111
  * @param till - query param to bypass CDN requests
112
112
  */
113
- return function definitionChangesUpdater(noCache, till, instantUpdate) {
113
+ return function definitionChangesUpdater(noCache, till, instantUpdate, pcns) {
114
114
  /**
115
115
  * @param since - current changeNumber at definitionsCache
116
116
  * @param retry - current number of retry attempts
@@ -118,8 +118,14 @@ export function definitionChangesUpdaterFactory(log, definitionChangesFetcher, s
118
118
  function _definitionChangesUpdater(sinces, retry) {
119
119
  if (retry === void 0) { retry = 0; }
120
120
  var since = sinces[0], rbSince = sinces[1];
121
- log.debug(SYNC_FETCH, [definitionChangesFetcher.type, since, rbSince]);
122
- return Promise.resolve(instantUpdate ?
121
+ // If provided, `since` and `rbSince` must not be higher than it, to avoid missing an update that the scoped `pcn` indicates hasn't been fetched yet.
122
+ if (pcns) {
123
+ if (pcns.since !== undefined && pcns.since < since)
124
+ since = pcns.since;
125
+ if (pcns.rbSince !== undefined && pcns.rbSince < rbSince)
126
+ rbSince = pcns.rbSince;
127
+ }
128
+ var definitionChangesRequest = instantUpdate ?
123
129
  instantUpdate.type === RB_SEGMENT_UPDATE ?
124
130
  { rbs: convertInstantUpdateToDefinitionChanges(instantUpdate) } :
125
131
  // IFFU edge case: a change to definition that adds an IN_RULE_BASED_SEGMENT matcher that is not present yet
@@ -128,34 +134,35 @@ export function definitionChangesUpdaterFactory(log, definitionChangesFetcher, s
128
134
  { d: convertInstantUpdateToDefinitionChanges(instantUpdate) } :
129
135
  definitionChangesFetcher(since, noCache, till, rbSince, _promiseDecorator);
130
136
  }) :
131
- definitionChangesFetcher(since, noCache, till, rbSince, _promiseDecorator))
137
+ definitionChangesFetcher(since, noCache, till, rbSince, _promiseDecorator);
138
+ if (thenable(definitionChangesRequest))
139
+ log.debug(SYNC_FETCH, [definitionChangesFetcher.type, since, rbSince]);
140
+ return Promise.resolve(definitionChangesRequest)
132
141
  .then(function (definitionChanges) {
133
142
  var usedSegments = new Set();
134
143
  var updatedDefinitions = [];
135
- var ffUpdate = false;
136
144
  if (definitionChanges.d) {
137
- var _a = computeMutation(definitionChanges.d, usedSegments, splitFiltersValidation), updated = _a.updated, removed = _a.removed, names = _a.names;
138
- updatedDefinitions = names;
145
+ var _a = computeMutation(definitionChanges.d, usedSegments, splitFiltersValidation), updated = _a.updated, removed = _a.removed;
139
146
  log.debug(SYNC_UPDATE, [definitionChangesFetcher.type, updated.length, removed.length]);
140
- ffUpdate = definitions.update(updated, removed, definitionChanges.d.till);
147
+ updatedDefinitions = definitions.update(updated, removed, definitionChanges.d.till);
141
148
  }
142
- var rbsUpdate = false;
149
+ var updatedRBSNames = [];
143
150
  if (definitionChanges.rbs) {
144
151
  var _b = computeMutation(definitionChanges.rbs, usedSegments), updated = _b.updated, removed = _b.removed;
145
152
  log.debug(SYNC_UPDATE, ['rule-based segments', updated.length, removed.length]);
146
- rbsUpdate = rbSegments.update(updated, removed, definitionChanges.rbs.till);
153
+ updatedRBSNames = rbSegments.update(updated, removed, definitionChanges.rbs.till);
147
154
  }
148
- return Promise.all([ffUpdate, rbsUpdate,
155
+ return Promise.all([updatedDefinitions, updatedRBSNames,
149
156
  // @TODO if at least 1 segment fetch fails due to 404 and other segments are updated in the storage, SDK_UPDATE is not emitted
150
157
  segments.registerSegments(setToArray(usedSegments))
151
158
  ]).then(function (_a) {
152
- var ffChanged = _a[0], rbsChanged = _a[1];
159
+ var updatedDefinitions = _a[0], updatedRBSNames = _a[1];
153
160
  if (storage.save)
154
161
  storage.save();
155
162
  startingUp = false;
156
163
  if (definitionsEventEmitter) {
157
164
  // To emit SDK_DEFINITIONS_ARRIVED for server-side SDK, we must wait for all registered segments to be fetched
158
- return Promise.resolve(!definitionsEventEmitter.definitionsArrived || ((ffChanged || rbsChanged) && (!segmentsSyncTask || segmentsSyncTask.execute(true))))
165
+ return Promise.resolve(!definitionsEventEmitter.definitionsArrived || ((updatedDefinitions.length > 0 || updatedRBSNames.length > 0) && (!segmentsSyncTask || segmentsSyncTask.execute(true))))
159
166
  .catch(function () { return false; } /** noop. just to handle a possible `checkAllSegmentsExist` rejection, before emitting SDK event */)
160
167
  .then(function (emitSplitsArrivedEvent) {
161
168
  // emit SDK events
@@ -1,14 +1,16 @@
1
1
  import { STREAMING_PARSING_SPLIT_UPDATE } from '../../../logger/constants';
2
- import { SDK_DEFINITIONS_ARRIVED } from '../../../readiness/constants';
2
+ import { SDK_DEFINITIONS_ARRIVED, FLAGS_UPDATE } from '../../../readiness/constants';
3
3
  import { Backoff } from '../../../utils/Backoff';
4
4
  import { SPLITS } from '../../../utils/constants';
5
5
  import { RB_SEGMENT_UPDATE } from '../constants';
6
- import { parseFFUpdatePayload } from '../parseUtils';
6
+ import { parseUpdatePayload } from '../parseUtils';
7
7
  import { FETCH_BACKOFF_BASE, FETCH_BACKOFF_MAX_WAIT, FETCH_BACKOFF_MAX_RETRIES } from './constants';
8
8
  /**
9
- * DefinitionsUpdateWorker factory
9
+ * DefinitionsUpdateWorker factory for Flags SDKs, where notifications pcn and changeNumber are environment-scoped
10
10
  */
11
- export function DefinitionsUpdateWorker(log, storage, definitionsSyncTask, definitionsEventEmitter, telemetryTracker) {
11
+ export function DefinitionsUpdateWorker(params, pollingManager) {
12
+ var log = params.settings.log, storage = params.storage, telemetryTracker = params.telemetryTracker, definitionsEventEmitter = params.readiness.definitions;
13
+ var definitionsSyncTask = pollingManager.definitionsSyncTask;
12
14
  var ff = DefinitionsUpdateWorker(storage.definitions);
13
15
  var rbs = DefinitionsUpdateWorker(storage.rbSegments);
14
16
  function DefinitionsUpdateWorker(cache) {
@@ -93,7 +95,7 @@ export function DefinitionsUpdateWorker(log, storage, definitionsSyncTask, defin
93
95
  put: function (parsedData) {
94
96
  if (parsedData.d && parsedData.c !== undefined) {
95
97
  try {
96
- var payload = parseFFUpdatePayload(parsedData.c, parsedData.d);
98
+ var payload = parseUpdatePayload(parsedData.c, parsedData.d);
97
99
  if (payload) {
98
100
  (parsedData.type === RB_SEGMENT_UPDATE ? rbs : ff).put(parsedData, payload);
99
101
  return;
@@ -116,7 +118,7 @@ export function DefinitionsUpdateWorker(log, storage, definitionsSyncTask, defin
116
118
  var changeNumber = _a.changeNumber, splitName = _a.splitName, defaultTreatment = _a.defaultTreatment;
117
119
  if (storage.definitions.killLocally(splitName, defaultTreatment, changeNumber)) {
118
120
  // trigger an SDK_UPDATE if Split was killed locally
119
- definitionsEventEmitter.emit(SDK_DEFINITIONS_ARRIVED, true);
121
+ definitionsEventEmitter.emit(SDK_DEFINITIONS_ARRIVED, { type: FLAGS_UPDATE, names: [splitName] }, true /* isSplitKill */);
120
122
  }
121
123
  // queues the SplitChanges fetch (only if changeNumber is newer)
122
124
  ff.put({ changeNumber: changeNumber });
@@ -73,9 +73,9 @@ export function isInBitmap(bitmap, hash64hex) {
73
73
  return (bitmap[internal] & 1 << offset) > 0;
74
74
  }
75
75
  /**
76
- * Parse feature flags notifications for instant feature flag updates
76
+ * Parse notifications for instant updates
77
77
  */
78
- export function parseFFUpdatePayload(compression, data) {
78
+ export function parseUpdatePayload(compression, data) {
79
79
  return compression > 0 ?
80
80
  parseCompressedData(data, compression, false) :
81
81
  JSON.parse(decodeFromBase64(data));
@@ -18,8 +18,9 @@ import { TOKEN_REFRESH, AUTH_REJECTION } from '../../utils/constants';
18
18
  * - for server-side if key is not provided in settings.
19
19
  * - for client-side, with support for multiple clients, if key is provided in settings
20
20
  */
21
- export function pushManagerFactory(params, pollingManager) {
22
- var settings = params.settings, storage = params.storage, serviceApi = params.serviceApi, readiness = params.readiness, platform = params.platform, telemetryTracker = params.telemetryTracker;
21
+ export function pushManagerFactory(params, pollingManager, definitionsUpdateWorkerFactory) {
22
+ if (definitionsUpdateWorkerFactory === void 0) { definitionsUpdateWorkerFactory = DefinitionsUpdateWorker; }
23
+ var settings = params.settings, storage = params.storage, serviceApi = params.serviceApi, platform = params.platform, telemetryTracker = params.telemetryTracker;
23
24
  // `userKey` is the matching key of main client in client-side SDK.
24
25
  // It can be used to check if running on client-side or server-side SDK.
25
26
  var userKey = checkIfServerSide(settings) ? undefined : getMatching(settings.core.key);
@@ -41,7 +42,7 @@ export function pushManagerFactory(params, pollingManager) {
41
42
  // MySegmentsUpdateWorker (client-side) are initiated in `add` method
42
43
  var segmentsUpdateWorker = userKey ? undefined : SegmentsUpdateWorker(log, pollingManager.segmentsSyncTask, storage.segments);
43
44
  // For server-side we pass the segmentsSyncTask, used by DefinitionsUpdateWorker to fetch new segments
44
- var definitionsUpdateWorker = DefinitionsUpdateWorker(log, storage, pollingManager.definitionsSyncTask, readiness.definitions, telemetryTracker);
45
+ var definitionsUpdateWorker = definitionsUpdateWorkerFactory(params, pollingManager);
45
46
  // [Only for client-side] map of user keys to their corresponding hash64 and MySegmentsUpdateWorkers.
46
47
  // Hash64 is used to process membership update events and dispatch actions to the corresponding MySegmentsUpdateWorker.
47
48
  var clients = {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@splitsoftware/splitio-commons",
3
- "version": "3.1.1-rc.11",
3
+ "version": "3.1.1-rc.13",
4
4
  "description": "Split JavaScript SDK common components",
5
5
  "main": "cjs/index.js",
6
6
  "module": "esm/index.js",
package/src/dtos/types.ts CHANGED
@@ -243,14 +243,14 @@ export interface IDefinition extends TargetingEntity {
243
243
  /** Interface of the parsed JSON response of `/splitChanges` */
244
244
  export interface IDefinitionChangesResponse {
245
245
  d?: {
246
- till: number,
247
- since?: number | null,
246
+ till?: number,
247
+ since?: number,
248
248
  updated: IDefinition[],
249
249
  removed: string[],
250
250
  },
251
251
  rbs?: {
252
- till: number,
253
- since?: number | null,
252
+ till?: number,
253
+ since?: number,
254
254
  updated: IRBSegment[],
255
255
  removed: string[],
256
256
  }
@@ -1,6 +1,6 @@
1
1
  import { ISplitHttpClient, NetworkError } from './types';
2
- import { IJwtCredential } from '../sync/streaming/AuthClient/types';
3
- import { fetchAuthFactory } from '../sync/streaming/AuthClient';
2
+ import { IJwtCredential } from './types';
3
+ import { fetchAuthFactory } from './fetchAuth';
4
4
  import { Backoff } from '../utils/Backoff';
5
5
  import { LOG_PREFIX_SYNC_AUTH } from '../logger/constants';
6
6
  import { ISettings } from '../types';
@@ -1,7 +1,6 @@
1
- import { IJwtCredential, IFetchAuth } from './types';
2
- import { objectAssign } from '../../../utils/lang/objectAssign';
3
- import { decodeJWTtoken } from '../../../utils/jwt';
4
- import { IResponse } from '../../../services/types';
1
+ import { IJwtCredential, IFetchAuth, IResponse } from './types';
2
+ import { objectAssign } from '../utils/lang/objectAssign';
3
+ import { decodeJWTtoken } from '../utils/jwt';
5
4
 
6
5
  /**
7
6
  * Factory of authentication function.
@@ -6,7 +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
+ import { fetchAuthFactory } from './fetchAuth';
10
10
 
11
11
  const noCacheHeaderOptions = { headers: { 'Cache-Control': 'no-cache' } };
12
12
 
@@ -1,4 +1,4 @@
1
- import { IFetchAuth } from '../sync/streaming/AuthClient/types';
1
+ import { IDecodedJWTToken } from '../utils/jwt/types';
2
2
 
3
3
  export type IRequestOptions = {
4
4
  method?: string,
@@ -30,14 +30,31 @@ export type NetworkError = Error & { statusCode?: number }
30
30
  // Reduced version of Fetch API
31
31
  export type IFetch = (url: string, options?: IRequestOptions) => Promise<IResponse>
32
32
 
33
- // IFetch specialization
34
- export type IHealthCheckAPI = () => Promise<boolean>
35
-
36
33
  export type ISplitHttpClient = (url: string, options?: IRequestOptions, latencyTracker?: (error?: NetworkError) => void, logErrorsAsInfo?: boolean, newVersionHeader?: boolean) => Promise<IResponse>
37
34
 
38
35
  // @TODO: remove `newVersionHeader` and `useJwt` arguments when all service endpoints are migrated to the new auth mechanism
39
36
  export type ISecureSplitHttpClient = ((url: string, options?: IRequestOptions, latencyTracker?: (error?: NetworkError) => void, logErrorsAsInfo?: boolean, newVersionHeader?: boolean, useJwt?: boolean) => Promise<IResponse>) & { stop(): void }
40
37
 
38
+ export type IJwtCredential = {
39
+ token: string; // empty string ("") when `"pushEnabled": false`
40
+ decodedToken: IDecodedJWTToken
41
+ channels: { [channel: string]: string[] }
42
+ // /api/v2/auth fields
43
+ pushEnabled?: boolean | null;
44
+ connDelay?: number | null;
45
+ // /api/v3/auth fields
46
+ config?: {
47
+ streaming?: {
48
+ delay?: number | null;
49
+ enabled?: boolean | null;
50
+ } | null;
51
+ } | null;
52
+ }
53
+
54
+ export type IFetchAuth = (userKeys?: string[]) => Promise<IJwtCredential>
55
+
56
+ export type IHealthCheckAPI = () => Promise<boolean>
57
+
41
58
  export type IFetchDefinitionChanges = (since: number, noCache?: boolean, till?: number, rbSince?: number) => Promise<IResponse>
42
59
 
43
60
  export type IFetchSegmentChanges = (since: number, segmentName: string, noCache?: boolean, till?: number) => Promise<IResponse>
@@ -1,5 +1,5 @@
1
1
  import { IDefinitionsCacheAsync } from './types';
2
- import { IDefinition } from '../dtos/types';
2
+ import { IDefinition, MaybeThenable } from '../dtos/types';
3
3
  import { objectAssign } from '../utils/lang/objectAssign';
4
4
 
5
5
  /**
@@ -10,15 +10,16 @@ export abstract class AbstractDefinitionsCacheAsync implements IDefinitionsCache
10
10
 
11
11
  protected abstract add(definition: IDefinition): Promise<boolean>
12
12
  protected abstract remove(name: string): Promise<boolean>
13
- protected abstract setChangeNumber(changeNumber: number): Promise<boolean | void>
13
+ protected abstract setChangeNumber(changeNumber?: number): MaybeThenable<boolean | void>
14
14
 
15
- update(toAdd: IDefinition[], toRemove: string[], changeNumber: number): Promise<boolean> {
15
+ update(toAdd: IDefinition[], toRemove: string[], changeNumber?: number): Promise<string[]> {
16
16
  return Promise.all([
17
17
  this.setChangeNumber(changeNumber),
18
18
  Promise.all(toAdd.map(addedFF => this.add(addedFF))),
19
19
  Promise.all(toRemove.map(removedFF => this.remove(removedFF)))
20
20
  ]).then(([, added, removed]) => {
21
- return added.some(result => result) || removed.some(result => result);
21
+ return toRemove.filter((_, i) => removed[i])
22
+ .concat(toAdd.filter((_, i) => added[i]).map(addedFF => addedFF.name));
22
23
  });
23
24
  }
24
25
 
@@ -11,11 +11,11 @@ export abstract class AbstractDefinitionsCacheSync implements IDefinitionsCacheS
11
11
 
12
12
  protected abstract add(definition: IDefinition): boolean
13
13
  protected abstract remove(name: string): boolean
14
- protected abstract setChangeNumber(changeNumber: number): boolean | void
14
+ protected abstract setChangeNumber(changeNumber?: number): boolean | void
15
15
 
16
- update(toAdd: IDefinition[], toRemove: string[], changeNumber: number): boolean {
17
- let updated = toAdd.map(addedFF => this.add(addedFF)).some(result => result);
18
- updated = toRemove.map(removedFF => this.remove(removedFF)).some(result => result) || updated;
16
+ update(toAdd: IDefinition[], toRemove: string[], changeNumber?: number): string[] {
17
+ const updated = toRemove.filter(removedFF => this.remove(removedFF))
18
+ .concat(toAdd.filter(addedFF => this.add(addedFF)).map(addedFF => addedFF.name));
19
19
  this.setChangeNumber(changeNumber);
20
20
  return updated;
21
21
  }
@@ -79,13 +79,15 @@ export class DefinitionsCacheInLocal extends AbstractDefinitionsCacheSync {
79
79
  this.hasSync = false;
80
80
  }
81
81
 
82
- add(definition: IDefinition) {
82
+ add(definition: IDefinition): boolean {
83
83
  const name = definition.name;
84
84
  const definitionKey = this.keys.buildDefinitionKey(name);
85
85
  const definitionFromStorage = this.storage.getItem(definitionKey);
86
86
  const previousDefinition = definitionFromStorage ? JSON.parse(definitionFromStorage) : null;
87
87
 
88
88
  if (previousDefinition) {
89
+ if (previousDefinition.changeNumber >= definition.changeNumber) return false;
90
+
89
91
  this._decrementCounts(previousDefinition);
90
92
  this.removeFromSets(previousDefinition.name, previousDefinition.sets);
91
93
  }
@@ -115,9 +117,9 @@ export class DefinitionsCacheInLocal extends AbstractDefinitionsCacheSync {
115
117
  return item && JSON.parse(item);
116
118
  }
117
119
 
118
- setChangeNumber(changeNumber: number): boolean {
120
+ setChangeNumber(changeNumber?: number): boolean {
119
121
  try {
120
- this.storage.setItem(this.keys.buildDefinitionsTillKey(), changeNumber + '');
122
+ if (changeNumber !== undefined) this.storage.setItem(this.keys.buildDefinitionsTillKey(), changeNumber + '');
121
123
  // update "last updated" timestamp with current time
122
124
  this.storage.setItem(this.keys.buildLastUpdatedKey(), Date.now() + '');
123
125
  this.hasSync = true;
@@ -25,16 +25,16 @@ export class RBSegmentsCacheInLocal implements IRBSegmentsCacheSync {
25
25
  this.storage.removeItem(this.keys.buildRBSegmentsTillKey());
26
26
  }
27
27
 
28
- update(toAdd: IRBSegment[], toRemove: string[], changeNumber: number): boolean {
29
- let updated = toAdd.map(toAdd => this.add(toAdd)).some(result => result);
30
- updated = toRemove.map(toRemove => this.remove(toRemove)).some(result => result) || updated;
28
+ update(toAdd: IRBSegment[], toRemove: string[], changeNumber?: number): string[] {
29
+ const updated = toRemove.filter(name => this.remove(name))
30
+ .concat(toAdd.filter(rbSegment => this.add(rbSegment)).map(rbSegment => rbSegment.name));
31
31
  this.setChangeNumber(changeNumber);
32
32
  return updated;
33
33
  }
34
34
 
35
- private setChangeNumber(changeNumber: number) {
35
+ private setChangeNumber(changeNumber?: number) {
36
36
  try {
37
- this.storage.setItem(this.keys.buildRBSegmentsTillKey(), changeNumber + '');
37
+ if (changeNumber !== undefined) this.storage.setItem(this.keys.buildRBSegmentsTillKey(), changeNumber + '');
38
38
  this.storage.setItem(this.keys.buildLastUpdatedKey(), Date.now() + '');
39
39
  } catch (e) {
40
40
  this.log.error(LOG_PREFIX + e);
@@ -54,6 +54,8 @@ export class RBSegmentsCacheInLocal implements IRBSegmentsCacheSync {
54
54
  const rbSegmentFromStorage = this.storage.getItem(rbSegmentKey);
55
55
  const previous = rbSegmentFromStorage ? JSON.parse(rbSegmentFromStorage) : null;
56
56
 
57
+ if (previous && previous.changeNumber >= rbSegment.changeNumber) return false;
58
+
57
59
  this.storage.setItem(rbSegmentKey, JSON.stringify(rbSegment));
58
60
 
59
61
  let usesSegmentsDiff = 0;
@@ -31,6 +31,7 @@ export class DefinitionsCacheInMemory extends AbstractDefinitionsCacheSync {
31
31
  const name = definition.name;
32
32
  const previousDefinition = this.get(name);
33
33
  if (previousDefinition) { // We had this Split already
34
+ if (previousDefinition.changeNumber >= definition.changeNumber) return false;
34
35
 
35
36
  const previousTtName = previousDefinition.trafficTypeName;
36
37
  this.ttCache[previousTtName]--;
@@ -77,8 +78,8 @@ export class DefinitionsCacheInMemory extends AbstractDefinitionsCacheSync {
77
78
  return this.definitionsCache[name] || null;
78
79
  }
79
80
 
80
- setChangeNumber(changeNumber: number): boolean {
81
- this.changeNumber = changeNumber;
81
+ setChangeNumber(changeNumber?: number): boolean {
82
+ if (changeNumber !== undefined) this.changeNumber = changeNumber;
82
83
  return true;
83
84
  }
84
85
 
@@ -15,16 +15,17 @@ export class RBSegmentsCacheInMemory implements IRBSegmentsCacheSync {
15
15
  this.segmentsCount = 0;
16
16
  }
17
17
 
18
- update(toAdd: IRBSegment[], toRemove: string[], changeNumber: number): boolean {
19
- let updated = toAdd.map(toAdd => this.add(toAdd)).some(result => result);
20
- updated = toRemove.map(toRemove => this.remove(toRemove)).some(result => result) || updated;
21
- this.changeNumber = changeNumber;
18
+ update(toAdd: IRBSegment[], toRemove: string[], changeNumber?: number): string[] {
19
+ const updated = toRemove.filter(name => this.remove(name))
20
+ .concat(toAdd.filter(rbSegment => this.add(rbSegment)).map(rbSegment => rbSegment.name));
21
+ if (changeNumber !== undefined) this.changeNumber = changeNumber;
22
22
  return updated;
23
23
  }
24
24
 
25
25
  private add(rbSegment: IRBSegment): boolean {
26
26
  const name = rbSegment.name;
27
27
  const previous = this.get(name);
28
+ if (previous && previous.changeNumber >= rbSegment.changeNumber) return false;
28
29
  if (previous && usesSegments(previous)) this.segmentsCount--;
29
30
 
30
31
  this.cache[name] = rbSegment;
@@ -91,6 +91,8 @@ export class DefinitionsCacheInRedis extends AbstractDefinitionsCacheAsync {
91
91
  let parsedPreviousDefinition: IDefinition, stringifiedNewDefinition;
92
92
  try {
93
93
  parsedPreviousDefinition = definitionFromStorage ? JSON.parse(definitionFromStorage) : undefined;
94
+ if (parsedPreviousDefinition && parsedPreviousDefinition.changeNumber >= definition.changeNumber) return false;
95
+
94
96
  stringifiedNewDefinition = JSON.stringify(definition);
95
97
  } catch (e) {
96
98
  throw new Error('Error parsing feature flag definition: ' + e);
@@ -104,8 +106,8 @@ export class DefinitionsCacheInRedis extends AbstractDefinitionsCacheAsync {
104
106
  return this._incrementCounts(definition).then(() => {
105
107
  if (parsedPreviousDefinition) return this._decrementCounts(parsedPreviousDefinition);
106
108
  });
107
- }).then(() => this._updateSets(name, parsedPreviousDefinition && parsedPreviousDefinition.sets, definition.sets));
108
- }).then(() => true);
109
+ }).then(() => this._updateSets(name, parsedPreviousDefinition && parsedPreviousDefinition.sets, definition.sets)).then(() => true);
110
+ });
109
111
  }
110
112
 
111
113
  /**
@@ -115,11 +117,11 @@ export class DefinitionsCacheInRedis extends AbstractDefinitionsCacheAsync {
115
117
  */
116
118
  remove(name: string) {
117
119
  return this.get(name).then((definition) => {
118
- if (definition) {
119
- return this._decrementCounts(definition).then(() => this._updateSets(name, definition.sets));
120
- }
121
- }).then(() => {
122
- return this.redis.del(this.keys.buildDefinitionKey(name)).then((status: number) => status === 1);
120
+ if (!definition) return false;
121
+
122
+ return this._decrementCounts(definition).then(() => this._updateSets(name, definition.sets)).then(() => {
123
+ return this.redis.del(this.keys.buildDefinitionKey(name)).then((status: number) => status === 1);
124
+ });
123
125
  });
124
126
  }
125
127
 
@@ -143,10 +145,12 @@ export class DefinitionsCacheInRedis extends AbstractDefinitionsCacheAsync {
143
145
  * The returned promise is resolved when the operation success,
144
146
  * or rejected if it fails.
145
147
  */
146
- setChangeNumber(changeNumber: number): Promise<boolean> {
147
- return this.redis.set(this.keys.buildDefinitionsTillKey(), changeNumber + '').then(
148
- (status: string | null) => status === 'OK'
149
- );
148
+ setChangeNumber(changeNumber?: number) {
149
+ if (changeNumber !== undefined) {
150
+ return this.redis.set(this.keys.buildDefinitionsTillKey(), changeNumber + '').then(
151
+ (status: string | null) => status === 'OK'
152
+ );
153
+ }
150
154
  }
151
155
 
152
156
  /**
@@ -37,27 +37,38 @@ export class RBSegmentsCacheInRedis implements IRBSegmentsCacheAsync {
37
37
  });
38
38
  }
39
39
 
40
- update(toAdd: IRBSegment[], toRemove: string[], changeNumber: number): Promise<boolean> {
40
+ update(toAdd: IRBSegment[], toRemove: string[], changeNumber?: number): Promise<string[]> {
41
41
  return Promise.all([
42
42
  this.setChangeNumber(changeNumber),
43
- Promise.all(toAdd.map(toAdd => {
44
- const key = this.keys.buildRBSegmentKey(toAdd.name);
45
- const stringifiedNewRBSegment = JSON.stringify(toAdd);
46
- return this.redis.set(key, stringifiedNewRBSegment).then(() => true);
47
- })),
48
- Promise.all(toRemove.map(toRemove => {
49
- const key = this.keys.buildRBSegmentKey(toRemove);
50
- return this.redis.del(key).then((status: number) => status === 1);
51
- }))
43
+ Promise.all(toAdd.map(rbSegment => this.add(rbSegment))),
44
+ Promise.all(toRemove.map(name => this.remove(name)))
52
45
  ]).then(([, added, removed]) => {
53
- return added.some(result => result) || removed.some(result => result);
46
+ return toRemove.filter((_, i) => removed[i])
47
+ .concat(toAdd.filter((_, i) => added[i]).map(rbSegment => rbSegment.name));
54
48
  });
55
49
  }
56
50
 
57
- setChangeNumber(changeNumber: number) {
58
- return this.redis.set(this.keys.buildRBSegmentsTillKey(), changeNumber + '').then(
59
- (status: string | null) => status === 'OK'
60
- );
51
+ private add(rbSegment: IRBSegment): Promise<boolean> {
52
+ const key = this.keys.buildRBSegmentKey(rbSegment.name);
53
+ return this.get(rbSegment.name).then(previous => {
54
+ if (previous && previous.changeNumber >= rbSegment.changeNumber) return false;
55
+
56
+ const stringifiedNewRBSegment = JSON.stringify(rbSegment);
57
+ return this.redis.set(key, stringifiedNewRBSegment).then(() => true);
58
+ });
59
+ }
60
+
61
+ private remove(name: string): Promise<boolean> {
62
+ const key = this.keys.buildRBSegmentKey(name);
63
+ return this.redis.del(key).then((status: number) => status === 1);
64
+ }
65
+
66
+ setChangeNumber(changeNumber?: number) {
67
+ if (changeNumber !== undefined) {
68
+ return this.redis.set(this.keys.buildRBSegmentsTillKey(), changeNumber + '').then(
69
+ (status: string | null) => status === 'OK'
70
+ );
71
+ }
61
72
  }
62
73
 
63
74
  getChangeNumber(): Promise<number> {