@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
@@ -75,11 +75,14 @@ export class DefinitionsCachePluggable extends AbstractDefinitionsCacheAsync {
75
75
  let parsedPreviousDefinition: IDefinition, stringifiedNewDefinition;
76
76
  try {
77
77
  parsedPreviousDefinition = definitionFromStorage ? JSON.parse(definitionFromStorage) : undefined;
78
+ if (parsedPreviousDefinition && parsedPreviousDefinition.changeNumber >= definition.changeNumber) return false;
79
+
78
80
  stringifiedNewDefinition = JSON.stringify(definition);
79
81
  } catch (e) {
80
82
  throw new Error('Error parsing feature flag definition: ' + e);
81
83
  }
82
84
 
85
+
83
86
  return this.wrapper.set(definitionKey, stringifiedNewDefinition).then(() => {
84
87
  // avoid unnecessary increment/decrement operations
85
88
  if (parsedPreviousDefinition && parsedPreviousDefinition.trafficTypeName === definition.trafficTypeName) return;
@@ -88,8 +91,8 @@ export class DefinitionsCachePluggable extends AbstractDefinitionsCacheAsync {
88
91
  return this._incrementCounts(definition).then(() => {
89
92
  if (parsedPreviousDefinition) return this._decrementCounts(parsedPreviousDefinition);
90
93
  });
91
- }).then(() => this._updateSets(name, parsedPreviousDefinition && parsedPreviousDefinition.sets, definition.sets));
92
- }).then(() => true);
94
+ }).then(() => this._updateSets(name, parsedPreviousDefinition && parsedPreviousDefinition.sets, definition.sets)).then(() => true);
95
+ });
93
96
  }
94
97
 
95
98
  /**
@@ -99,11 +102,11 @@ export class DefinitionsCachePluggable extends AbstractDefinitionsCacheAsync {
99
102
  */
100
103
  remove(name: string) {
101
104
  return this.get(name).then((definition) => {
102
- if (definition) {
103
- return this._decrementCounts(definition).then(() => this._updateSets(name, definition.sets));
104
- }
105
- }).then(() => {
106
- return this.wrapper.del(this.keys.buildDefinitionKey(name));
105
+ if (!definition) return false;
106
+
107
+ return this._decrementCounts(definition).then(() => this._updateSets(name, definition.sets)).then(() => {
108
+ return this.wrapper.del(this.keys.buildDefinitionKey(name));
109
+ });
107
110
  });
108
111
  }
109
112
 
@@ -202,8 +205,10 @@ export class DefinitionsCachePluggable extends AbstractDefinitionsCacheAsync {
202
205
  * The returned promise is resolved when the operation success,
203
206
  * or rejected if it fails (e.g., wrapper operation fails).
204
207
  */
205
- setChangeNumber(changeNumber: number) {
206
- return this.wrapper.set(this.keys.buildDefinitionsTillKey(), changeNumber + '');
208
+ setChangeNumber(changeNumber?: number) {
209
+ if (changeNumber !== undefined) {
210
+ return this.wrapper.set(this.keys.buildDefinitionsTillKey(), changeNumber + '');
211
+ }
207
212
  }
208
213
 
209
214
  /**
@@ -36,25 +36,36 @@ export class RBSegmentsCachePluggable implements IRBSegmentsCacheAsync {
36
36
  });
37
37
  }
38
38
 
39
- update(toAdd: IRBSegment[], toRemove: string[], changeNumber: number): Promise<boolean> {
39
+ update(toAdd: IRBSegment[], toRemove: string[], changeNumber?: number): Promise<string[]> {
40
40
  return Promise.all([
41
41
  this.setChangeNumber(changeNumber),
42
- Promise.all(toAdd.map(toAdd => {
43
- const key = this.keys.buildRBSegmentKey(toAdd.name);
44
- const stringifiedNewRBSegment = JSON.stringify(toAdd);
45
- return this.wrapper.set(key, stringifiedNewRBSegment).then(() => true);
46
- })),
47
- Promise.all(toRemove.map(toRemove => {
48
- const key = this.keys.buildRBSegmentKey(toRemove);
49
- return this.wrapper.del(key);
50
- }))
42
+ Promise.all(toAdd.map(rbSegment => this.add(rbSegment))),
43
+ Promise.all(toRemove.map(name => this.remove(name)))
51
44
  ]).then(([, added, removed]) => {
52
- return added.some(result => result) || removed.some(result => result);
45
+ return toRemove.filter((_, i) => removed[i])
46
+ .concat(toAdd.filter((_, i) => added[i]).map(rbSegment => rbSegment.name));
53
47
  });
54
48
  }
55
49
 
56
- setChangeNumber(changeNumber: number) {
57
- return this.wrapper.set(this.keys.buildRBSegmentsTillKey(), changeNumber + '');
50
+ private add(rbSegment: IRBSegment): Promise<boolean> {
51
+ const key = this.keys.buildRBSegmentKey(rbSegment.name);
52
+ return this.get(rbSegment.name).then(previous => {
53
+ if (previous && previous.changeNumber >= rbSegment.changeNumber) return false;
54
+
55
+ const stringifiedNewRBSegment = JSON.stringify(rbSegment);
56
+ return this.wrapper.set(key, stringifiedNewRBSegment).then(() => true);
57
+ });
58
+ }
59
+
60
+ private remove(name: string): Promise<boolean> {
61
+ const key = this.keys.buildRBSegmentKey(name);
62
+ return this.wrapper.del(key);
63
+ }
64
+
65
+ setChangeNumber(changeNumber?: number) {
66
+ if (changeNumber !== undefined) {
67
+ return this.wrapper.set(this.keys.buildRBSegmentsTillKey(), changeNumber + '');
68
+ }
58
69
  }
59
70
 
60
71
  getChangeNumber(): Promise<number> {
@@ -195,7 +195,7 @@ export interface IPluggableStorageWrapper {
195
195
  /** Splits cache */
196
196
 
197
197
  export interface IDefinitionsCacheBase {
198
- update(toUpsert: IDefinition[], toRemove: string[], changeNumber: number): MaybeThenable<boolean>,
198
+ update(toUpsert: IDefinition[], toRemove: string[], changeNumber?: number): MaybeThenable<string[]>,
199
199
  get(name: string): MaybeThenable<IDefinition | null>,
200
200
  getMany(names: string[]): MaybeThenable<Record<string, IDefinition | null>>, // `fetchMany` in spec
201
201
  // should never reject or throw an exception. Instead return -1 by default, assuming no splits are present in the storage.
@@ -212,7 +212,7 @@ export interface IDefinitionsCacheBase {
212
212
  }
213
213
 
214
214
  export interface IDefinitionsCacheSync extends IDefinitionsCacheBase {
215
- update(toUpsert: IDefinition[], toRemove: string[], changeNumber: number): boolean,
215
+ update(toUpsert: IDefinition[], toRemove: string[], changeNumber?: number): string[],
216
216
  get(name: string): IDefinition | null,
217
217
  getMany(names: string[]): Record<string, IDefinition | null>,
218
218
  getChangeNumber(): number,
@@ -226,7 +226,7 @@ export interface IDefinitionsCacheSync extends IDefinitionsCacheBase {
226
226
  }
227
227
 
228
228
  export interface IDefinitionsCacheAsync extends IDefinitionsCacheBase {
229
- update(toUpsert: IDefinition[], toRemove: string[], changeNumber: number): Promise<boolean>,
229
+ update(toUpsert: IDefinition[], toRemove: string[], changeNumber?: number): Promise<string[]>,
230
230
  get(name: string): Promise<IDefinition | null>,
231
231
  getMany(names: string[]): Promise<Record<string, IDefinition | null>>,
232
232
  getChangeNumber(): Promise<number>,
@@ -242,7 +242,7 @@ export interface IDefinitionsCacheAsync extends IDefinitionsCacheBase {
242
242
  /** Rule-Based Segments cache */
243
243
 
244
244
  export interface IRBSegmentsCacheBase {
245
- update(toAdd: IRBSegment[], toRemove: string[], changeNumber: number): MaybeThenable<boolean>,
245
+ update(toAdd: IRBSegment[], toRemove: string[], changeNumber?: number): MaybeThenable<string[]>,
246
246
  get(name: string): MaybeThenable<IRBSegment | null>,
247
247
  getChangeNumber(): MaybeThenable<number>,
248
248
  clear(): MaybeThenable<boolean | void>,
@@ -250,7 +250,7 @@ export interface IRBSegmentsCacheBase {
250
250
  }
251
251
 
252
252
  export interface IRBSegmentsCacheSync extends IRBSegmentsCacheBase {
253
- update(toAdd: IRBSegment[], toRemove: string[], changeNumber: number): boolean,
253
+ update(toAdd: IRBSegment[], toRemove: string[], changeNumber?: number): string[],
254
254
  get(name: string): IRBSegment | null,
255
255
  getChangeNumber(): number,
256
256
  getAll(): IRBSegment[],
@@ -261,7 +261,7 @@ export interface IRBSegmentsCacheSync extends IRBSegmentsCacheBase {
261
261
  }
262
262
 
263
263
  export interface IRBSegmentsCacheAsync extends IRBSegmentsCacheBase {
264
- update(toAdd: IRBSegment[], toRemove: string[], changeNumber: number): Promise<boolean>,
264
+ update(toAdd: IRBSegment[], toRemove: string[], changeNumber?: number): Promise<string[]>,
265
265
  get(name: string): Promise<IRBSegment | null>,
266
266
  getChangeNumber(): Promise<number>,
267
267
  clear(): Promise<boolean | void>,
@@ -0,0 +1,49 @@
1
+ import { IDefinition, IDefinitionChangesResponse } from '../../../dtos/types';
2
+ import { CONTROL } from '../../../utils/constants';
3
+ import { IDefinitionPartial } from './types';
4
+
5
+ /**
6
+ * Factory of a stateful function that, given the current mocked definitions, returns the changes since the previous call
7
+ * as an `IDefinitionChangesResponse`, i.e., the full definitions that were added or modified, and the names of the removed ones.
8
+ * Definitions are compared by value. The response `till` is an incrementing value based on the current time.
9
+ * Definitions have no `changeNumber`, so storages never consider them outdated.
10
+ *
11
+ * The response has no `d` property if there are no changes, except for the first call, which reports the (possibly empty) mock.
12
+ * It is meant to be used by `IDefinitionsParser` implementations, such as `splitsParserFromSettings`.
13
+ */
14
+ export function definitionChangesBuilderFactory() {
15
+
16
+ let previous: Record<string, string> | undefined; // serialized definitions of the last reported mock, by name
17
+ let lastTill = 0;
18
+
19
+ return function definitionChangesBuilder(mock: Record<string, IDefinitionPartial>): IDefinitionChangesResponse {
20
+ const current: Record<string, string> = {};
21
+ const updatedNames = Object.keys(mock).reduce((updatedNames: string[], name) => {
22
+ current[name] = JSON.stringify(mock[name]);
23
+ if (!previous || previous[name] !== current[name]) updatedNames.push(name);
24
+ return updatedNames;
25
+ }, []);
26
+
27
+ const removed = previous ? Object.keys(previous).filter(name => current[name] === undefined) : [];
28
+
29
+ // Only the first call reports an empty change
30
+ if (previous && updatedNames.length === 0 && removed.length === 0) return {};
31
+
32
+ previous = current;
33
+
34
+ // Incrementing `till` based on the current time
35
+ const till = lastTill = Math.max(Date.now(), lastTill + 1);
36
+
37
+ const updated = updatedNames.map(name => ({
38
+ name,
39
+ status: 'ACTIVE',
40
+ killed: false,
41
+ trafficAllocation: 100,
42
+ defaultTreatment: CONTROL, // `seed` is undefined in localhost mode
43
+ ...mock[name]
44
+ } as IDefinition));
45
+
46
+ return { d: { updated, removed, till } };
47
+ };
48
+
49
+ }
@@ -1,52 +1,22 @@
1
1
  import { IDefinitionPartial } from './types';
2
+ import { IDefinitionChangesResponse } from '../../../dtos/types';
2
3
  import SplitIO from '../../../../types/splitio';
3
- import { isObject, forOwn, merge } from '../../../utils/lang';
4
+ import { isObject, forOwn } from '../../../utils/lang';
4
5
  import { parseCondition } from './parseCondition';
5
-
6
- function hasTreatmentChanged(prev: string | SplitIO.TreatmentWithConfig, curr: string | SplitIO.TreatmentWithConfig) {
7
- if (typeof prev !== typeof curr) return true;
8
-
9
- if (typeof prev === 'string') { // strings treatments, just compare
10
- return prev !== curr;
11
- } else { // has treatment and config, compare both
12
- return prev.treatment !== (curr as SplitIO.TreatmentWithConfig).treatment || prev.config !== (curr as SplitIO.TreatmentWithConfig).config;
13
- }
14
- }
6
+ import { definitionChangesBuilderFactory } from './definitionChangesBuilder';
15
7
 
16
8
  export function splitsParserFromSettingsFactory() {
17
9
 
18
- let previousMock: SplitIO.MockedFeaturesMap = { 'emptyMock': '1' };
19
-
20
- function mockUpdated(currentData: SplitIO.MockedFeaturesMap) {
21
- const names = Object.keys(currentData);
22
-
23
- // Different amount of items
24
- if (names.length !== Object.keys(previousMock).length) {
25
- previousMock = merge({}, currentData) as SplitIO.MockedFeaturesMap;
26
- return true;
27
- }
28
-
29
- return names.some(name => {
30
- const newSplit = !previousMock[name];
31
- const newTreatment = hasTreatmentChanged(previousMock[name], currentData[name]);
32
- const changed = newSplit || newTreatment;
33
-
34
- if (changed) previousMock = merge({}, currentData) as SplitIO.MockedFeaturesMap;
35
-
36
- return changed;
37
- });
38
- }
10
+ const definitionChangesBuilder = definitionChangesBuilderFactory();
39
11
 
40
12
  /**
41
13
  *
42
14
  * @param settings - validated object with mocked features mapping.
43
15
  */
44
- return function splitsParserFromSettings(settings: Pick<SplitIO.ISettings, 'features'>): false | Record<string, IDefinitionPartial> {
16
+ return function splitsParserFromSettings(settings: Pick<SplitIO.ISettings, 'features'>): IDefinitionChangesResponse {
45
17
  const features = settings.features as SplitIO.MockedFeaturesMap || {};
46
18
 
47
- if (!mockUpdated(features)) return false;
48
-
49
- const splitObjects: Record<string, IDefinitionPartial> = {};
19
+ const mock: Record<string, IDefinitionPartial> = {};
50
20
 
51
21
  forOwn(features, (data, splitName) => {
52
22
  let treatment = data;
@@ -59,14 +29,14 @@ export function splitsParserFromSettingsFactory() {
59
29
  const configurations: Record<string, string> = {};
60
30
  if (config !== null) configurations[treatment as string] = config;
61
31
 
62
- splitObjects[splitName] = {
32
+ mock[splitName] = {
63
33
  trafficTypeName: 'localhost',
64
34
  conditions: [parseCondition({ treatment: treatment as string })],
65
35
  configurations
66
36
  };
67
37
  });
68
38
 
69
- return splitObjects;
39
+ return definitionChangesBuilder(mock);
70
40
  };
71
41
 
72
42
  }
@@ -1,8 +1,12 @@
1
- import { IDefinition } from '../../../dtos/types';
1
+ import { IDefinition, IDefinitionChangesResponse } from '../../../dtos/types';
2
2
  import { ISettings } from '../../../types';
3
3
 
4
4
  // Split definition used in offline mode
5
5
  export type IDefinitionPartial = Pick<IDefinition, 'conditions' | 'configurations' | 'trafficTypeName'>
6
6
 
7
- // Analog to `IDefinitionChangesFetcher` used by `definitionChangesUpdaterFactory`
8
- export type IDefinitionsParser = (settings: ISettings) => false | Record<string, IDefinitionPartial>
7
+ /**
8
+ * Analog to `IDefinitionChangesFetcher` used by `definitionChangesUpdaterFactory`.
9
+ * It returns the definitions changes since the last call, with an incrementing `till`.
10
+ * The response has no `d` property if there are no changes, e.g., if the mocked definitions were not modified since the last call.
11
+ */
12
+ export type IDefinitionsParser = (settings: ISettings) => IDefinitionChangesResponse
@@ -1,12 +1,10 @@
1
- import { forOwn } from '../../../utils/lang';
2
1
  import { IReadinessManager } from '../../../readiness/types';
3
2
  import { IStorageSync } from '../../../storages/types';
4
- import { IDefinitionsParser, IDefinitionPartial } from '../splitsParser/types';
5
- import { IDefinition } from '../../../dtos/types';
3
+ import { IDefinitionsParser } from '../splitsParser/types';
4
+ import { IDefinitionChangesResponse } from '../../../dtos/types';
6
5
  import { syncTaskFactory } from '../../syncTask';
7
6
  import { ISyncTask } from '../../types';
8
7
  import { ISettings } from '../../../types';
9
- import { CONTROL } from '../../../utils/constants';
10
8
  import { SDK_DEFINITIONS_ARRIVED, SDK_SEGMENTS_ARRIVED, SDK_DEFINITIONS_CACHE_LOADED, FLAGS_UPDATE, SEGMENTS_UPDATE } from '../../../readiness/constants';
11
9
  import { SYNC_OFFLINE_DATA, ERROR_SYNC_OFFLINE_LOADING } from '../../../logger/constants';
12
10
 
@@ -24,55 +22,37 @@ export function fromObjectUpdaterFactory(
24
22
  let startingUp = true;
25
23
 
26
24
  return function objectUpdater() {
27
- const splits: IDefinition[] = [];
28
- let loadError = null;
29
- let splitsMock: false | Record<string, IDefinitionPartial> = {};
25
+ let changes: IDefinitionChangesResponse | undefined;
30
26
  try {
31
- splitsMock = splitsParser(settings);
27
+ changes = splitsParser(settings);
32
28
  } catch (err) {
33
- loadError = err;
34
29
  log.error(ERROR_SYNC_OFFLINE_LOADING, [err]);
35
30
  }
36
31
 
37
- if (!loadError && splitsMock) {
38
- log.debug(SYNC_OFFLINE_DATA, [JSON.stringify(splitsMock)]);
32
+ if (!changes || !changes.d) return Promise.resolve(true);
39
33
 
40
- forOwn(splitsMock, (val, name) => {
41
- // @ts-ignore Split changeNumber and seed is undefined in localhost mode
42
- splits.push({
43
- name,
44
- status: 'ACTIVE',
45
- killed: false,
46
- trafficAllocation: 100,
47
- defaultTreatment: CONTROL,
48
- conditions: val.conditions || [],
49
- configurations: val.configurations,
50
- trafficTypeName: val.trafficTypeName
51
- });
52
- });
34
+ const { updated, removed, till } = changes.d;
35
+ log.debug(SYNC_OFFLINE_DATA, [JSON.stringify(changes.d)]);
53
36
 
54
- return Promise.all([
55
- definitions.clear(), // required to sync removed splits from mock
56
- definitions.update(splits, [], Date.now())
57
- ]).then(() => {
58
- readiness.definitions.emit(SDK_DEFINITIONS_ARRIVED, { type: FLAGS_UPDATE, names: [] });
37
+ // On start, clear definitions that might be in the storage from a previous session (InLocalStorage), since the parser only tracks its own changes.
38
+ return Promise.resolve(startingUp && definitions.clear()).then(() => {
39
+ return definitions.update(updated, removed, till);
40
+ }).then((names) => {
41
+ readiness.definitions.emit(SDK_DEFINITIONS_ARRIVED, { type: FLAGS_UPDATE, names });
59
42
 
60
- if (startingUp) {
61
- startingUp = false;
62
- Promise.resolve(storage.validateCache ? storage.validateCache() : { initialCacheLoad: true /* Fallback: assume initial load when validateCache doesn't exist */ }).then((cacheMetadata) => {
63
- // Emits SDK_READY_FROM_CACHE
64
- if (!cacheMetadata.initialCacheLoad) {
65
- readiness.definitions.emit(SDK_DEFINITIONS_CACHE_LOADED, cacheMetadata);
66
- }
67
- // Emits SDK_READY
68
- readiness.segments.emit(SDK_SEGMENTS_ARRIVED, { type: SEGMENTS_UPDATE, names: [] });
69
- });
70
- }
71
- return true;
72
- });
73
- } else {
74
- return Promise.resolve(true);
75
- }
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((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
+ });
76
56
  };
77
57
  }
78
58
 
@@ -21,12 +21,12 @@ export type ISplit = IDefinition;
21
21
  export interface ISplitChangesResponse {
22
22
  ff?: {
23
23
  t: number,
24
- s?: number,
24
+ s?: number | null,
25
25
  d?: ISplit[] | null,
26
26
  },
27
27
  rbs?: {
28
28
  t: number,
29
- s?: number,
29
+ s?: number | null,
30
30
  d?: IRBSegment[] | null,
31
31
  }
32
32
  }
@@ -34,7 +34,7 @@ export interface ISplitChangesResponse {
34
34
  /** JSON response of `/splitChanges` for flag spec version 1.2 or below */
35
35
  export interface ISplitChangesLegacyResponse {
36
36
  till: number,
37
- since?: number,
37
+ since?: number | null,
38
38
  splits: ISplit[]
39
39
  }
40
40
 
@@ -42,10 +42,10 @@ function isSplitChangesLegacyResponse(data: ISplitChangesResponse | ISplitChange
42
42
  return (data as ISplitChangesLegacyResponse).splits != null;
43
43
  }
44
44
 
45
- function partitionByStatus<T extends { status?: string; name: string }>(items: T[], till: number, since?: number) {
45
+ function partitionByStatus<T extends { status?: string; name: string }>(items: T[], till: number, since?: number | null) {
46
46
  const updated: T[] = [], removed: string[] = [];
47
47
  items.forEach(item => item.status === 'ARCHIVED' ? removed.push(item.name) : updated.push(item));
48
- return { updated, removed, till, since };
48
+ return { updated, removed, till, since: since ?? undefined };
49
49
  }
50
50
 
51
51
  /**
@@ -4,7 +4,28 @@ import { IStorageSync } from '../../storages/types';
4
4
  import { MEMBERSHIPS_LS_UPDATE, MEMBERSHIPS_MS_UPDATE } from '../streaming/types';
5
5
  import { ITask, ISyncTask } from '../types';
6
6
 
7
- export interface IDefinitionsSyncTask extends ISyncTask<[noCache?: boolean, till?: number, definitionUpdateNotification?: { payload: IDefinition | IRBSegment, changeNumber: number }], boolean> { }
7
+ export type InstantUpdate = {
8
+ payload: IDefinition | IRBSegment,
9
+ /**
10
+ * Environment-scoped change number to track as the storage change number.
11
+ * If not provided, the storage change number is preserved. That's the case of config instant
12
+ * updates, where the notification change number is scoped to the config rather than to the
13
+ * environment.
14
+ */
15
+ changeNumber?: number,
16
+ type: string
17
+ };
18
+
19
+ /**
20
+ * `pcn` of CONFIG_UPDATE and RB_SEGMENT_UPDATE notifications for the Configs SDK, used as lower bounds for the `since` and `rbSince` fetch params.
21
+ * Added for robustness, although in practice `pcn` should never be lower than the current storage change number.
22
+ */
23
+ export type PreviousChangeNumbers = {
24
+ since?: number, // `pcn` of CONFIG_UPDATE notifications
25
+ rbSince?: number // `pcn` of RB_SEGMENT_UPDATE notifications
26
+ };
27
+
28
+ export interface IDefinitionsSyncTask extends ISyncTask<[noCache?: boolean, till?: number, instantUpdate?: InstantUpdate, pcns?: PreviousChangeNumbers], boolean> { }
8
29
 
9
30
  export interface ISegmentsSyncTask extends ISyncTask<[fetchOnlyNew?: boolean, segmentName?: string, noCache?: boolean, till?: number], boolean> { }
10
31
 
@@ -11,11 +11,10 @@ import { IN_RULE_BASED_SEGMENT, IN_SEGMENT, RULE_BASED_SEGMENT, STANDARD_SEGMENT
11
11
  import { setToArray } from '../../../utils/lang/sets';
12
12
  import { RB_SEGMENT_UPDATE } from '../../streaming/constants';
13
13
  import { SdkUpdateMetadata } from '../../../../types/splitio';
14
- import { ISplit } from '../fetchers/splitChangesFetcher';
15
- import { ISegmentsSyncTask } from '../types';
14
+ import { InstantUpdate, ISegmentsSyncTask, PreviousChangeNumbers } from '../types';
15
+ import { thenable } from '../../../utils/promise/thenable';
16
16
 
17
- export type InstantUpdate = { payload: ISplit | IRBSegment, changeNumber: number, type: string };
18
- type DefinitionChangesUpdater = (noCache?: boolean, till?: number, instantUpdate?: InstantUpdate) => Promise<boolean>
17
+ type DefinitionChangesUpdater = (noCache?: boolean, till?: number, instantUpdate?: InstantUpdate, pcns?: PreviousChangeNumbers) => Promise<boolean>
19
18
 
20
19
  /**
21
20
  * Collect segments from a raw FF or RBS definition.
@@ -49,7 +48,6 @@ export function parseSegments(ruleEntity: IDefinition | IRBSegment, matcherType:
49
48
  interface IDefinitionMutations<T extends IDefinition | IRBSegment> {
50
49
  updated: T[],
51
50
  removed: string[],
52
- names: string[]
53
51
  }
54
52
 
55
53
  /**
@@ -89,9 +87,8 @@ export function computeMutation<T extends IDefinition | IRBSegment>(update: { up
89
87
  } else {
90
88
  accum.removed.push(ruleEntity.name);
91
89
  }
92
- accum.names.push(ruleEntity.name);
93
90
  return accum;
94
- }, { updated: [], removed: update.removed, names: update.removed.slice() } as IDefinitionMutations<T>);
91
+ }, { updated: [], removed: update.removed } as IDefinitionMutations<T>);
95
92
  }
96
93
 
97
94
  function convertInstantUpdateToDefinitionChanges(instantUpdate: InstantUpdate) {
@@ -142,58 +139,63 @@ export function definitionChangesUpdaterFactory(
142
139
  * @param noCache - true to revalidate data to fetch
143
140
  * @param till - query param to bypass CDN requests
144
141
  */
145
- return function definitionChangesUpdater(noCache?: boolean, till?: number, instantUpdate?: InstantUpdate) {
142
+ return function definitionChangesUpdater(noCache?: boolean, till?: number, instantUpdate?: InstantUpdate, pcns?: PreviousChangeNumbers) {
146
143
 
147
144
  /**
148
145
  * @param since - current changeNumber at definitionsCache
149
146
  * @param retry - current number of retry attempts
150
147
  */
151
148
  function _definitionChangesUpdater(sinces: [number, number], retry = 0): Promise<boolean> {
152
- const [since, rbSince] = sinces;
153
- log.debug(SYNC_FETCH, [definitionChangesFetcher.type, since, rbSince]);
154
-
155
- return Promise.resolve(
156
- instantUpdate ?
157
- instantUpdate.type === RB_SEGMENT_UPDATE ?
158
- { rbs: convertInstantUpdateToDefinitionChanges(instantUpdate) as IDefinitionChangesResponse['rbs'] } :
159
- // IFFU edge case: a change to definition that adds an IN_RULE_BASED_SEGMENT matcher that is not present yet
160
- Promise.resolve(rbSegments.contains(parseSegments(instantUpdate.payload, IN_RULE_BASED_SEGMENT))).then((contains) => {
161
- return contains ?
162
- { d: convertInstantUpdateToDefinitionChanges(instantUpdate) as IDefinitionChangesResponse['d'] } :
163
- definitionChangesFetcher(since, noCache, till, rbSince, _promiseDecorator);
164
- }) :
165
- definitionChangesFetcher(since, noCache, till, rbSince, _promiseDecorator)
166
- )
149
+ let [since, rbSince] = sinces;
150
+
151
+ // 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.
152
+ if (pcns) {
153
+ if (pcns.since !== undefined && pcns.since < since) since = pcns.since;
154
+ if (pcns.rbSince !== undefined && pcns.rbSince < rbSince) rbSince = pcns.rbSince;
155
+ }
156
+
157
+ const definitionChangesRequest = instantUpdate ?
158
+ instantUpdate.type === RB_SEGMENT_UPDATE ?
159
+ { rbs: convertInstantUpdateToDefinitionChanges(instantUpdate) as IDefinitionChangesResponse['rbs'] } :
160
+ // IFFU edge case: a change to definition that adds an IN_RULE_BASED_SEGMENT matcher that is not present yet
161
+ Promise.resolve(rbSegments.contains(parseSegments(instantUpdate.payload, IN_RULE_BASED_SEGMENT))).then((contains) => {
162
+ return contains ?
163
+ { d: convertInstantUpdateToDefinitionChanges(instantUpdate) as IDefinitionChangesResponse['d'] } :
164
+ definitionChangesFetcher(since, noCache, till, rbSince, _promiseDecorator);
165
+ }) :
166
+ definitionChangesFetcher(since, noCache, till, rbSince, _promiseDecorator);
167
+
168
+ if (thenable(definitionChangesRequest)) log.debug(SYNC_FETCH, [definitionChangesFetcher.type, since, rbSince]);
169
+
170
+ return Promise.resolve(definitionChangesRequest)
167
171
  .then((definitionChanges: IDefinitionChangesResponse) => {
168
172
  const usedSegments = new Set<string>();
169
173
 
170
- let updatedDefinitions: string[] = [];
171
- let ffUpdate: MaybeThenable<boolean> = false;
174
+ let updatedDefinitions: MaybeThenable<string[]> = [];
172
175
  if (definitionChanges.d) {
173
- const { updated, removed, names } = computeMutation(definitionChanges.d, usedSegments, splitFiltersValidation);
174
- updatedDefinitions = names;
176
+ const { updated, removed } = computeMutation(definitionChanges.d, usedSegments, splitFiltersValidation);
175
177
  log.debug(SYNC_UPDATE, [definitionChangesFetcher.type, updated.length, removed.length]);
176
- ffUpdate = definitions.update(updated, removed, definitionChanges.d.till);
178
+ updatedDefinitions = definitions.update(updated, removed, definitionChanges.d.till);
177
179
  }
178
180
 
179
- let rbsUpdate: MaybeThenable<boolean> = false;
181
+ let updatedRBSNames: MaybeThenable<string[]> = [];
180
182
  if (definitionChanges.rbs) {
181
183
  const { updated, removed } = computeMutation(definitionChanges.rbs, usedSegments);
182
184
  log.debug(SYNC_UPDATE, ['rule-based segments', updated.length, removed.length]);
183
- rbsUpdate = rbSegments.update(updated, removed, definitionChanges.rbs.till);
185
+ updatedRBSNames = rbSegments.update(updated, removed, definitionChanges.rbs.till);
184
186
  }
185
187
 
186
- return Promise.all([ffUpdate, rbsUpdate,
188
+ return Promise.all([updatedDefinitions, updatedRBSNames,
187
189
  // @TODO if at least 1 segment fetch fails due to 404 and other segments are updated in the storage, SDK_UPDATE is not emitted
188
190
  segments.registerSegments(setToArray(usedSegments))
189
- ]).then(([ffChanged, rbsChanged]) => {
191
+ ]).then(([updatedDefinitions, updatedRBSNames]) => {
190
192
  if (storage.save) storage.save();
191
193
 
192
194
  startingUp = false;
193
195
 
194
196
  if (definitionsEventEmitter) {
195
197
  // To emit SDK_DEFINITIONS_ARRIVED for server-side SDK, we must wait for all registered segments to be fetched
196
- return Promise.resolve(!definitionsEventEmitter.definitionsArrived || ((ffChanged || rbsChanged) && (!segmentsSyncTask || segmentsSyncTask.execute(true))))
198
+ return Promise.resolve(!definitionsEventEmitter.definitionsArrived || ((updatedDefinitions.length > 0 || updatedRBSNames.length > 0) && (!segmentsSyncTask || segmentsSyncTask.execute(true))))
197
199
  .catch(() => false /** noop. just to handle a possible `checkAllSegmentsExist` rejection, before emitting SDK event */)
198
200
  .then(emitSplitsArrivedEvent => {
199
201
  // emit SDK events
@@ -5,7 +5,7 @@ import { ISettings } from '../../../types';
5
5
  import { checkIfServerSide } from '../../../utils/key';
6
6
  import { isString } from '../../../utils/lang';
7
7
  import { objectAssign } from '../../../utils/lang/objectAssign';
8
- import { IJwtCredential } from '../AuthClient/types';
8
+ import { IJwtCredential } from '../../../services/types';
9
9
  import { ISSEClient, ISseEventHandler } from './types';
10
10
 
11
11
  const ABLY_API_VERSION = '1.1';
@@ -1,4 +1,4 @@
1
- import { IJwtCredential } from '../AuthClient/types';
1
+ import { IJwtCredential } from '../../../services/types';
2
2
 
3
3
  export interface ISseEventHandler {
4
4
  handleError: (ev: Event) => any;