@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.
- package/CHANGES.txt +3 -0
- package/cjs/services/authProvider.js +2 -2
- package/cjs/{sync/streaming/AuthClient/index.js → services/fetchAuth.js} +2 -2
- package/cjs/services/splitApi.js +2 -2
- package/cjs/storages/AbstractDefinitionsCacheAsync.js +2 -1
- package/cjs/storages/AbstractDefinitionsCacheSync.js +2 -2
- package/cjs/storages/inLocalStorage/DefinitionsCacheInLocal.js +4 -1
- package/cjs/storages/inLocalStorage/RBSegmentsCacheInLocal.js +6 -3
- package/cjs/storages/inMemory/DefinitionsCacheInMemory.js +4 -1
- package/cjs/storages/inMemory/RBSegmentsCacheInMemory.js +6 -3
- package/cjs/storages/inRedis/DefinitionsCacheInRedis.js +12 -8
- package/cjs/storages/inRedis/RBSegmentsCacheInRedis.js +21 -11
- package/cjs/storages/pluggable/DefinitionsCachePluggable.js +12 -8
- package/cjs/storages/pluggable/RBSegmentsCachePluggable.js +21 -11
- package/cjs/sync/offline/splitsParser/definitionChangesBuilder.js +37 -0
- package/cjs/sync/offline/splitsParser/splitsParserFromSettings.js +5 -32
- package/cjs/sync/offline/syncTasks/fromObjectSyncTask.js +26 -46
- package/cjs/sync/polling/fetchers/splitChangesFetcher.js +1 -1
- package/cjs/sync/polling/updaters/definitionChangesUpdater.js +22 -15
- package/cjs/sync/streaming/UpdateWorkers/DefinitionsUpdateWorker.js +6 -4
- package/cjs/sync/streaming/parseUtils.js +4 -4
- package/cjs/sync/streaming/pushManager.js +4 -3
- package/esm/services/authProvider.js +1 -1
- package/esm/{sync/streaming/AuthClient/index.js → services/fetchAuth.js} +2 -2
- package/esm/services/splitApi.js +1 -1
- package/esm/storages/AbstractDefinitionsCacheAsync.js +2 -1
- package/esm/storages/AbstractDefinitionsCacheSync.js +2 -2
- package/esm/storages/inLocalStorage/DefinitionsCacheInLocal.js +4 -1
- package/esm/storages/inLocalStorage/RBSegmentsCacheInLocal.js +6 -3
- package/esm/storages/inMemory/DefinitionsCacheInMemory.js +4 -1
- package/esm/storages/inMemory/RBSegmentsCacheInMemory.js +6 -3
- package/esm/storages/inRedis/DefinitionsCacheInRedis.js +12 -8
- package/esm/storages/inRedis/RBSegmentsCacheInRedis.js +21 -11
- package/esm/storages/pluggable/DefinitionsCachePluggable.js +12 -8
- package/esm/storages/pluggable/RBSegmentsCachePluggable.js +21 -11
- package/esm/sync/offline/splitsParser/definitionChangesBuilder.js +33 -0
- package/esm/sync/offline/splitsParser/splitsParserFromSettings.js +6 -33
- package/esm/sync/offline/syncTasks/fromObjectSyncTask.js +23 -43
- package/esm/sync/polling/fetchers/splitChangesFetcher.js +1 -1
- package/esm/sync/polling/updaters/definitionChangesUpdater.js +22 -15
- package/esm/sync/streaming/UpdateWorkers/DefinitionsUpdateWorker.js +8 -6
- package/esm/sync/streaming/parseUtils.js +2 -2
- package/esm/sync/streaming/pushManager.js +4 -3
- package/package.json +1 -1
- package/src/dtos/types.ts +4 -4
- package/src/services/authProvider.ts +2 -2
- package/src/{sync/streaming/AuthClient/index.ts → services/fetchAuth.ts} +3 -4
- package/src/services/splitApi.ts +1 -1
- package/src/services/types.ts +21 -4
- package/src/storages/AbstractDefinitionsCacheAsync.ts +5 -4
- package/src/storages/AbstractDefinitionsCacheSync.ts +4 -4
- package/src/storages/inLocalStorage/DefinitionsCacheInLocal.ts +5 -3
- package/src/storages/inLocalStorage/RBSegmentsCacheInLocal.ts +7 -5
- package/src/storages/inMemory/DefinitionsCacheInMemory.ts +3 -2
- package/src/storages/inMemory/RBSegmentsCacheInMemory.ts +5 -4
- package/src/storages/inRedis/DefinitionsCacheInRedis.ts +15 -11
- package/src/storages/inRedis/RBSegmentsCacheInRedis.ts +26 -15
- package/src/storages/pluggable/DefinitionsCachePluggable.ts +14 -9
- package/src/storages/pluggable/RBSegmentsCachePluggable.ts +24 -13
- package/src/storages/types.ts +6 -6
- package/src/sync/offline/splitsParser/definitionChangesBuilder.ts +49 -0
- package/src/sync/offline/splitsParser/splitsParserFromSettings.ts +8 -38
- package/src/sync/offline/splitsParser/types.ts +7 -3
- package/src/sync/offline/syncTasks/fromObjectSyncTask.ts +25 -45
- package/src/sync/polling/fetchers/splitChangesFetcher.ts +5 -5
- package/src/sync/polling/types.ts +22 -1
- package/src/sync/polling/updaters/definitionChangesUpdater.ts +35 -33
- package/src/sync/streaming/SSEClient/index.ts +1 -1
- package/src/sync/streaming/SSEClient/types.ts +1 -1
- package/src/sync/streaming/UpdateWorkers/DefinitionsUpdateWorker.ts +17 -13
- package/src/sync/streaming/UpdateWorkers/types.ts +6 -0
- package/src/sync/streaming/parseUtils.ts +3 -4
- package/src/sync/streaming/pushManager.ts +4 -3
- package/src/sync/syncManagerOnline.ts +0 -1
- package/cjs/sync/streaming/AuthClient/types.js +0 -2
- package/esm/sync/streaming/AuthClient/types.js +0 -1
- 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
|
-
})
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
|
206
|
-
|
|
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
|
|
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(
|
|
43
|
-
|
|
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
|
|
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
|
-
|
|
57
|
-
|
|
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> {
|
package/src/storages/types.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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'>):
|
|
16
|
+
return function splitsParserFromSettings(settings: Pick<SplitIO.ISettings, 'features'>): IDefinitionChangesResponse {
|
|
45
17
|
const features = settings.features as SplitIO.MockedFeaturesMap || {};
|
|
46
18
|
|
|
47
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
8
|
-
|
|
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
|
|
5
|
-
import {
|
|
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
|
-
|
|
28
|
-
let loadError = null;
|
|
29
|
-
let splitsMock: false | Record<string, IDefinitionPartial> = {};
|
|
25
|
+
let changes: IDefinitionChangesResponse | undefined;
|
|
30
26
|
try {
|
|
31
|
-
|
|
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 (!
|
|
38
|
-
log.debug(SYNC_OFFLINE_DATA, [JSON.stringify(splitsMock)]);
|
|
32
|
+
if (!changes || !changes.d) return Promise.resolve(true);
|
|
39
33
|
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
|
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 {
|
|
15
|
-
import {
|
|
14
|
+
import { InstantUpdate, ISegmentsSyncTask, PreviousChangeNumbers } from '../types';
|
|
15
|
+
import { thenable } from '../../../utils/promise/thenable';
|
|
16
16
|
|
|
17
|
-
|
|
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
|
|
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
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
|
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
|
-
|
|
178
|
+
updatedDefinitions = definitions.update(updated, removed, definitionChanges.d.till);
|
|
177
179
|
}
|
|
178
180
|
|
|
179
|
-
let
|
|
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
|
-
|
|
185
|
+
updatedRBSNames = rbSegments.update(updated, removed, definitionChanges.rbs.till);
|
|
184
186
|
}
|
|
185
187
|
|
|
186
|
-
return Promise.all([
|
|
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(([
|
|
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 || ((
|
|
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 '
|
|
8
|
+
import { IJwtCredential } from '../../../services/types';
|
|
9
9
|
import { ISSEClient, ISseEventHandler } from './types';
|
|
10
10
|
|
|
11
11
|
const ABLY_API_VERSION = '1.1';
|