@splitsoftware/splitio-commons 2.13.0 → 2.13.1
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
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
2.13.1 (October 7, 2026)
|
|
2
|
+
- Updated the 'LOCALSTORAGE' storage to batch the writes of flag sets and traffic type/segment counters when storing feature flags, reducing the time of cold initializations.
|
|
3
|
+
|
|
1
4
|
2.13.0 (September 24, 2026)
|
|
2
5
|
- Added support for ioredis v6 (Related to issue https://github.com/splitio/javascript-client/issues/951). Note that ioredis v6 requires Node.js 20 or above; stay on ioredis v4 or v5 for older Node.js versions.
|
|
3
6
|
- Removed the `@types/ioredis` dependency, which is no longer required since the public type definitions don't depend on ioredis types. The `@types/node` dependency, previously installed transitively by `@types/ioredis`, is now declared explicitly, since the public type definitions use Node.js types for the `agent` and `tls` options.
|
|
@@ -6,6 +6,9 @@ var AbstractSplitsCacheSync_1 = require("../AbstractSplitsCacheSync");
|
|
|
6
6
|
var lang_1 = require("../../utils/lang");
|
|
7
7
|
var constants_1 = require("./constants");
|
|
8
8
|
var sets_1 = require("../../utils/lang/sets");
|
|
9
|
+
function newBatch() {
|
|
10
|
+
return { flagSets: new Map(), counts: new Map() };
|
|
11
|
+
}
|
|
9
12
|
var SplitsCacheInLocal = /** @class */ (function (_super) {
|
|
10
13
|
tslib_1.__extends(SplitsCacheInLocal, _super);
|
|
11
14
|
function SplitsCacheInLocal(settings, keys, storage) {
|
|
@@ -16,38 +19,13 @@ var SplitsCacheInLocal = /** @class */ (function (_super) {
|
|
|
16
19
|
_this.storage = storage;
|
|
17
20
|
return _this;
|
|
18
21
|
}
|
|
19
|
-
SplitsCacheInLocal.prototype.
|
|
20
|
-
|
|
21
|
-
if (count > 0)
|
|
22
|
-
this.storage.setItem(key, count + '');
|
|
23
|
-
else
|
|
24
|
-
this.storage.removeItem(key);
|
|
25
|
-
};
|
|
26
|
-
SplitsCacheInLocal.prototype._decrementCounts = function (split) {
|
|
27
|
-
try {
|
|
28
|
-
var ttKey = this.keys.buildTrafficTypeKey(split.trafficTypeName);
|
|
29
|
-
this._decrementCount(ttKey);
|
|
30
|
-
if ((0, AbstractSplitsCacheSync_1.usesSegments)(split)) {
|
|
31
|
-
var segmentsCountKey = this.keys.buildSplitsWithSegmentCountKey();
|
|
32
|
-
this._decrementCount(segmentsCountKey);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
catch (e) {
|
|
36
|
-
this.log.error(constants_1.LOG_PREFIX + e);
|
|
37
|
-
}
|
|
22
|
+
SplitsCacheInLocal.prototype.updateCount = function (batch, key, diff) {
|
|
23
|
+
batch.counts.set(key, (batch.counts.get(key) || 0) + diff);
|
|
38
24
|
};
|
|
39
|
-
SplitsCacheInLocal.prototype.
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
this.
|
|
43
|
-
if ((0, AbstractSplitsCacheSync_1.usesSegments)(split)) {
|
|
44
|
-
var segmentsCountKey = this.keys.buildSplitsWithSegmentCountKey();
|
|
45
|
-
this.storage.setItem(segmentsCountKey, ((0, lang_1.toNumber)(this.storage.getItem(segmentsCountKey)) + 1) + '');
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
catch (e) {
|
|
49
|
-
this.log.error(constants_1.LOG_PREFIX + e);
|
|
50
|
-
}
|
|
25
|
+
SplitsCacheInLocal.prototype.updateCounts = function (split, batch, diff) {
|
|
26
|
+
this.updateCount(batch, this.keys.buildTrafficTypeKey(split.trafficTypeName), diff);
|
|
27
|
+
if ((0, AbstractSplitsCacheSync_1.usesSegments)(split))
|
|
28
|
+
this.updateCount(batch, this.keys.buildSplitsWithSegmentCountKey(), diff);
|
|
51
29
|
};
|
|
52
30
|
/**
|
|
53
31
|
* Removes all splits cache related data from localStorage (splits, counters, changeNumber and lastUpdated).
|
|
@@ -69,27 +47,64 @@ var SplitsCacheInLocal = /** @class */ (function (_super) {
|
|
|
69
47
|
});
|
|
70
48
|
this.hasSync = false;
|
|
71
49
|
};
|
|
50
|
+
/**
|
|
51
|
+
* Overrides the default implementation to batch the flag set and counter writes: each flag set and each counter is read from
|
|
52
|
+
* storage once, updated in memory, and written once at the end, instead of once per feature flag.
|
|
53
|
+
* `update` is synchronous, so no other reader can observe the intermediate state.
|
|
54
|
+
*/
|
|
55
|
+
SplitsCacheInLocal.prototype.update = function (toAdd, toRemove, changeNumber) {
|
|
56
|
+
var _this = this;
|
|
57
|
+
var batch = newBatch();
|
|
58
|
+
var updated;
|
|
59
|
+
try {
|
|
60
|
+
updated = toAdd.map(function (addedFF) { return _this.addSplitToBatch(addedFF, batch); }).some(function (result) { return result; });
|
|
61
|
+
updated = toRemove.map(function (removedFF) { return _this.removeSplitFromBatch(removedFF.name, batch); }).some(function (result) { return result; }) || updated;
|
|
62
|
+
}
|
|
63
|
+
finally {
|
|
64
|
+
this.flushBatch(batch);
|
|
65
|
+
}
|
|
66
|
+
this.setChangeNumber(changeNumber);
|
|
67
|
+
return updated;
|
|
68
|
+
};
|
|
72
69
|
SplitsCacheInLocal.prototype.addSplit = function (split) {
|
|
70
|
+
var batch = newBatch();
|
|
71
|
+
try {
|
|
72
|
+
return this.addSplitToBatch(split, batch);
|
|
73
|
+
}
|
|
74
|
+
finally {
|
|
75
|
+
this.flushBatch(batch);
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
SplitsCacheInLocal.prototype.removeSplit = function (name) {
|
|
79
|
+
var batch = newBatch();
|
|
80
|
+
try {
|
|
81
|
+
return this.removeSplitFromBatch(name, batch);
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
this.flushBatch(batch);
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
SplitsCacheInLocal.prototype.addSplitToBatch = function (split, batch) {
|
|
73
88
|
var name = split.name;
|
|
74
89
|
var splitKey = this.keys.buildSplitKey(name);
|
|
75
90
|
var splitFromStorage = this.storage.getItem(splitKey);
|
|
76
91
|
var previousSplit = splitFromStorage ? JSON.parse(splitFromStorage) : null;
|
|
77
92
|
if (previousSplit) {
|
|
78
|
-
this.
|
|
79
|
-
this.removeFromFlagSets(previousSplit.name, previousSplit.sets);
|
|
93
|
+
this.updateCounts(previousSplit, batch, -1);
|
|
94
|
+
this.removeFromFlagSets(previousSplit.name, previousSplit.sets, batch);
|
|
80
95
|
}
|
|
81
96
|
this.storage.setItem(splitKey, JSON.stringify(split));
|
|
82
|
-
this.
|
|
83
|
-
this.addToFlagSets(split);
|
|
97
|
+
this.updateCounts(split, batch, 1);
|
|
98
|
+
this.addToFlagSets(split, batch);
|
|
84
99
|
return true;
|
|
85
100
|
};
|
|
86
|
-
SplitsCacheInLocal.prototype.
|
|
101
|
+
SplitsCacheInLocal.prototype.removeSplitFromBatch = function (name, batch) {
|
|
87
102
|
var split = this.getSplit(name);
|
|
88
103
|
if (!split)
|
|
89
104
|
return false;
|
|
90
105
|
this.storage.removeItem(this.keys.buildSplitKey(name));
|
|
91
|
-
this.
|
|
92
|
-
this.removeFromFlagSets(split.name, split.sets);
|
|
106
|
+
this.updateCounts(split, batch, -1);
|
|
107
|
+
this.removeFromFlagSets(split.name, split.sets, batch);
|
|
93
108
|
return true;
|
|
94
109
|
};
|
|
95
110
|
SplitsCacheInLocal.prototype.getSplit = function (name) {
|
|
@@ -152,7 +167,7 @@ var SplitsCacheInLocal = /** @class */ (function (_super) {
|
|
|
152
167
|
return new Set(flagSetFromStorage ? JSON.parse(flagSetFromStorage) : []);
|
|
153
168
|
});
|
|
154
169
|
};
|
|
155
|
-
SplitsCacheInLocal.prototype.addToFlagSets = function (featureFlag) {
|
|
170
|
+
SplitsCacheInLocal.prototype.addToFlagSets = function (featureFlag, batch) {
|
|
156
171
|
var _this = this;
|
|
157
172
|
if (!featureFlag.sets)
|
|
158
173
|
return;
|
|
@@ -160,34 +175,65 @@ var SplitsCacheInLocal = /** @class */ (function (_super) {
|
|
|
160
175
|
if (_this.flagSetsFilter.length > 0 && !_this.flagSetsFilter.some(function (filterFlagSet) { return filterFlagSet === featureFlagSet; }))
|
|
161
176
|
return;
|
|
162
177
|
var flagSetKey = _this.keys.buildFlagSetKey(featureFlagSet);
|
|
163
|
-
|
|
164
|
-
var flagSetCache = new Set(flagSetFromStorage ? JSON.parse(flagSetFromStorage) : []);
|
|
165
|
-
if (flagSetCache.has(featureFlag.name))
|
|
166
|
-
return;
|
|
167
|
-
flagSetCache.add(featureFlag.name);
|
|
168
|
-
_this.storage.setItem(flagSetKey, JSON.stringify((0, sets_1.setToArray)(flagSetCache)));
|
|
178
|
+
_this.getFlagSetFromBatch(flagSetKey, batch).add(featureFlag.name);
|
|
169
179
|
});
|
|
170
180
|
};
|
|
171
|
-
SplitsCacheInLocal.prototype.removeFromFlagSets = function (featureFlagName, flagSets) {
|
|
181
|
+
SplitsCacheInLocal.prototype.removeFromFlagSets = function (featureFlagName, flagSets, batch) {
|
|
172
182
|
var _this = this;
|
|
173
183
|
if (!flagSets)
|
|
174
184
|
return;
|
|
175
185
|
flagSets.forEach(function (flagSet) {
|
|
176
|
-
_this.removeNames(flagSet, featureFlagName);
|
|
186
|
+
_this.removeNames(flagSet, featureFlagName, batch);
|
|
177
187
|
});
|
|
178
188
|
};
|
|
179
|
-
SplitsCacheInLocal.prototype.removeNames = function (flagSetName, featureFlagName) {
|
|
189
|
+
SplitsCacheInLocal.prototype.removeNames = function (flagSetName, featureFlagName, batch) {
|
|
180
190
|
var flagSetKey = this.keys.buildFlagSetKey(flagSetName);
|
|
181
|
-
|
|
182
|
-
if (!
|
|
183
|
-
return;
|
|
184
|
-
var flagSetCache = new Set(JSON.parse(flagSetFromStorage));
|
|
185
|
-
flagSetCache.delete(featureFlagName);
|
|
186
|
-
if (flagSetCache.size === 0) {
|
|
187
|
-
this.storage.removeItem(flagSetKey);
|
|
191
|
+
// nothing to remove from a flag set that is neither in storage nor in the batch
|
|
192
|
+
if (!batch.flagSets.has(flagSetKey) && !this.storage.getItem(flagSetKey))
|
|
188
193
|
return;
|
|
194
|
+
this.getFlagSetFromBatch(flagSetKey, batch).delete(featureFlagName);
|
|
195
|
+
};
|
|
196
|
+
/**
|
|
197
|
+
* Returns the flag names of the given flag set, reading from storage only the first time
|
|
198
|
+
*/
|
|
199
|
+
SplitsCacheInLocal.prototype.getFlagSetFromBatch = function (flagSetKey, batch) {
|
|
200
|
+
var flagSetCache = batch.flagSets.get(flagSetKey);
|
|
201
|
+
if (!flagSetCache) {
|
|
202
|
+
var flagSetFromStorage = this.storage.getItem(flagSetKey);
|
|
203
|
+
flagSetCache = new Set(flagSetFromStorage ? JSON.parse(flagSetFromStorage) : []);
|
|
204
|
+
batch.flagSets.set(flagSetKey, flagSetCache);
|
|
189
205
|
}
|
|
190
|
-
|
|
206
|
+
return flagSetCache;
|
|
207
|
+
};
|
|
208
|
+
SplitsCacheInLocal.prototype.flushBatch = function (batch) {
|
|
209
|
+
var _this = this;
|
|
210
|
+
// flush counts
|
|
211
|
+
batch.counts.forEach(function (diff, key) {
|
|
212
|
+
try {
|
|
213
|
+
if (diff === 0)
|
|
214
|
+
return;
|
|
215
|
+
var count = (0, lang_1.toNumber)(_this.storage.getItem(key)) + diff;
|
|
216
|
+
if (count > 0)
|
|
217
|
+
_this.storage.setItem(key, count + '');
|
|
218
|
+
else
|
|
219
|
+
_this.storage.removeItem(key);
|
|
220
|
+
}
|
|
221
|
+
catch (e) {
|
|
222
|
+
_this.log.error(constants_1.LOG_PREFIX + e);
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
// flush flag sets
|
|
226
|
+
batch.flagSets.forEach(function (flagSetCache, flagSetKey) {
|
|
227
|
+
try {
|
|
228
|
+
if (flagSetCache.size === 0)
|
|
229
|
+
_this.storage.removeItem(flagSetKey);
|
|
230
|
+
else
|
|
231
|
+
_this.storage.setItem(flagSetKey, JSON.stringify((0, sets_1.setToArray)(flagSetCache)));
|
|
232
|
+
}
|
|
233
|
+
catch (e) {
|
|
234
|
+
_this.log.error(constants_1.LOG_PREFIX + e);
|
|
235
|
+
}
|
|
236
|
+
});
|
|
191
237
|
};
|
|
192
238
|
return SplitsCacheInLocal;
|
|
193
239
|
}(AbstractSplitsCacheSync_1.AbstractSplitsCacheSync));
|
|
@@ -3,6 +3,9 @@ import { AbstractSplitsCacheSync, usesSegments } from '../AbstractSplitsCacheSyn
|
|
|
3
3
|
import { isFiniteNumber, toNumber, isNaNNumber } from '../../utils/lang';
|
|
4
4
|
import { LOG_PREFIX } from './constants';
|
|
5
5
|
import { setToArray } from '../../utils/lang/sets';
|
|
6
|
+
function newBatch() {
|
|
7
|
+
return { flagSets: new Map(), counts: new Map() };
|
|
8
|
+
}
|
|
6
9
|
var SplitsCacheInLocal = /** @class */ (function (_super) {
|
|
7
10
|
__extends(SplitsCacheInLocal, _super);
|
|
8
11
|
function SplitsCacheInLocal(settings, keys, storage) {
|
|
@@ -13,38 +16,13 @@ var SplitsCacheInLocal = /** @class */ (function (_super) {
|
|
|
13
16
|
_this.storage = storage;
|
|
14
17
|
return _this;
|
|
15
18
|
}
|
|
16
|
-
SplitsCacheInLocal.prototype.
|
|
17
|
-
|
|
18
|
-
if (count > 0)
|
|
19
|
-
this.storage.setItem(key, count + '');
|
|
20
|
-
else
|
|
21
|
-
this.storage.removeItem(key);
|
|
22
|
-
};
|
|
23
|
-
SplitsCacheInLocal.prototype._decrementCounts = function (split) {
|
|
24
|
-
try {
|
|
25
|
-
var ttKey = this.keys.buildTrafficTypeKey(split.trafficTypeName);
|
|
26
|
-
this._decrementCount(ttKey);
|
|
27
|
-
if (usesSegments(split)) {
|
|
28
|
-
var segmentsCountKey = this.keys.buildSplitsWithSegmentCountKey();
|
|
29
|
-
this._decrementCount(segmentsCountKey);
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
catch (e) {
|
|
33
|
-
this.log.error(LOG_PREFIX + e);
|
|
34
|
-
}
|
|
19
|
+
SplitsCacheInLocal.prototype.updateCount = function (batch, key, diff) {
|
|
20
|
+
batch.counts.set(key, (batch.counts.get(key) || 0) + diff);
|
|
35
21
|
};
|
|
36
|
-
SplitsCacheInLocal.prototype.
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
this.
|
|
40
|
-
if (usesSegments(split)) {
|
|
41
|
-
var segmentsCountKey = this.keys.buildSplitsWithSegmentCountKey();
|
|
42
|
-
this.storage.setItem(segmentsCountKey, (toNumber(this.storage.getItem(segmentsCountKey)) + 1) + '');
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
catch (e) {
|
|
46
|
-
this.log.error(LOG_PREFIX + e);
|
|
47
|
-
}
|
|
22
|
+
SplitsCacheInLocal.prototype.updateCounts = function (split, batch, diff) {
|
|
23
|
+
this.updateCount(batch, this.keys.buildTrafficTypeKey(split.trafficTypeName), diff);
|
|
24
|
+
if (usesSegments(split))
|
|
25
|
+
this.updateCount(batch, this.keys.buildSplitsWithSegmentCountKey(), diff);
|
|
48
26
|
};
|
|
49
27
|
/**
|
|
50
28
|
* Removes all splits cache related data from localStorage (splits, counters, changeNumber and lastUpdated).
|
|
@@ -66,27 +44,64 @@ var SplitsCacheInLocal = /** @class */ (function (_super) {
|
|
|
66
44
|
});
|
|
67
45
|
this.hasSync = false;
|
|
68
46
|
};
|
|
47
|
+
/**
|
|
48
|
+
* Overrides the default implementation to batch the flag set and counter writes: each flag set and each counter is read from
|
|
49
|
+
* storage once, updated in memory, and written once at the end, instead of once per feature flag.
|
|
50
|
+
* `update` is synchronous, so no other reader can observe the intermediate state.
|
|
51
|
+
*/
|
|
52
|
+
SplitsCacheInLocal.prototype.update = function (toAdd, toRemove, changeNumber) {
|
|
53
|
+
var _this = this;
|
|
54
|
+
var batch = newBatch();
|
|
55
|
+
var updated;
|
|
56
|
+
try {
|
|
57
|
+
updated = toAdd.map(function (addedFF) { return _this.addSplitToBatch(addedFF, batch); }).some(function (result) { return result; });
|
|
58
|
+
updated = toRemove.map(function (removedFF) { return _this.removeSplitFromBatch(removedFF.name, batch); }).some(function (result) { return result; }) || updated;
|
|
59
|
+
}
|
|
60
|
+
finally {
|
|
61
|
+
this.flushBatch(batch);
|
|
62
|
+
}
|
|
63
|
+
this.setChangeNumber(changeNumber);
|
|
64
|
+
return updated;
|
|
65
|
+
};
|
|
69
66
|
SplitsCacheInLocal.prototype.addSplit = function (split) {
|
|
67
|
+
var batch = newBatch();
|
|
68
|
+
try {
|
|
69
|
+
return this.addSplitToBatch(split, batch);
|
|
70
|
+
}
|
|
71
|
+
finally {
|
|
72
|
+
this.flushBatch(batch);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
SplitsCacheInLocal.prototype.removeSplit = function (name) {
|
|
76
|
+
var batch = newBatch();
|
|
77
|
+
try {
|
|
78
|
+
return this.removeSplitFromBatch(name, batch);
|
|
79
|
+
}
|
|
80
|
+
finally {
|
|
81
|
+
this.flushBatch(batch);
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
SplitsCacheInLocal.prototype.addSplitToBatch = function (split, batch) {
|
|
70
85
|
var name = split.name;
|
|
71
86
|
var splitKey = this.keys.buildSplitKey(name);
|
|
72
87
|
var splitFromStorage = this.storage.getItem(splitKey);
|
|
73
88
|
var previousSplit = splitFromStorage ? JSON.parse(splitFromStorage) : null;
|
|
74
89
|
if (previousSplit) {
|
|
75
|
-
this.
|
|
76
|
-
this.removeFromFlagSets(previousSplit.name, previousSplit.sets);
|
|
90
|
+
this.updateCounts(previousSplit, batch, -1);
|
|
91
|
+
this.removeFromFlagSets(previousSplit.name, previousSplit.sets, batch);
|
|
77
92
|
}
|
|
78
93
|
this.storage.setItem(splitKey, JSON.stringify(split));
|
|
79
|
-
this.
|
|
80
|
-
this.addToFlagSets(split);
|
|
94
|
+
this.updateCounts(split, batch, 1);
|
|
95
|
+
this.addToFlagSets(split, batch);
|
|
81
96
|
return true;
|
|
82
97
|
};
|
|
83
|
-
SplitsCacheInLocal.prototype.
|
|
98
|
+
SplitsCacheInLocal.prototype.removeSplitFromBatch = function (name, batch) {
|
|
84
99
|
var split = this.getSplit(name);
|
|
85
100
|
if (!split)
|
|
86
101
|
return false;
|
|
87
102
|
this.storage.removeItem(this.keys.buildSplitKey(name));
|
|
88
|
-
this.
|
|
89
|
-
this.removeFromFlagSets(split.name, split.sets);
|
|
103
|
+
this.updateCounts(split, batch, -1);
|
|
104
|
+
this.removeFromFlagSets(split.name, split.sets, batch);
|
|
90
105
|
return true;
|
|
91
106
|
};
|
|
92
107
|
SplitsCacheInLocal.prototype.getSplit = function (name) {
|
|
@@ -149,7 +164,7 @@ var SplitsCacheInLocal = /** @class */ (function (_super) {
|
|
|
149
164
|
return new Set(flagSetFromStorage ? JSON.parse(flagSetFromStorage) : []);
|
|
150
165
|
});
|
|
151
166
|
};
|
|
152
|
-
SplitsCacheInLocal.prototype.addToFlagSets = function (featureFlag) {
|
|
167
|
+
SplitsCacheInLocal.prototype.addToFlagSets = function (featureFlag, batch) {
|
|
153
168
|
var _this = this;
|
|
154
169
|
if (!featureFlag.sets)
|
|
155
170
|
return;
|
|
@@ -157,34 +172,65 @@ var SplitsCacheInLocal = /** @class */ (function (_super) {
|
|
|
157
172
|
if (_this.flagSetsFilter.length > 0 && !_this.flagSetsFilter.some(function (filterFlagSet) { return filterFlagSet === featureFlagSet; }))
|
|
158
173
|
return;
|
|
159
174
|
var flagSetKey = _this.keys.buildFlagSetKey(featureFlagSet);
|
|
160
|
-
|
|
161
|
-
var flagSetCache = new Set(flagSetFromStorage ? JSON.parse(flagSetFromStorage) : []);
|
|
162
|
-
if (flagSetCache.has(featureFlag.name))
|
|
163
|
-
return;
|
|
164
|
-
flagSetCache.add(featureFlag.name);
|
|
165
|
-
_this.storage.setItem(flagSetKey, JSON.stringify(setToArray(flagSetCache)));
|
|
175
|
+
_this.getFlagSetFromBatch(flagSetKey, batch).add(featureFlag.name);
|
|
166
176
|
});
|
|
167
177
|
};
|
|
168
|
-
SplitsCacheInLocal.prototype.removeFromFlagSets = function (featureFlagName, flagSets) {
|
|
178
|
+
SplitsCacheInLocal.prototype.removeFromFlagSets = function (featureFlagName, flagSets, batch) {
|
|
169
179
|
var _this = this;
|
|
170
180
|
if (!flagSets)
|
|
171
181
|
return;
|
|
172
182
|
flagSets.forEach(function (flagSet) {
|
|
173
|
-
_this.removeNames(flagSet, featureFlagName);
|
|
183
|
+
_this.removeNames(flagSet, featureFlagName, batch);
|
|
174
184
|
});
|
|
175
185
|
};
|
|
176
|
-
SplitsCacheInLocal.prototype.removeNames = function (flagSetName, featureFlagName) {
|
|
186
|
+
SplitsCacheInLocal.prototype.removeNames = function (flagSetName, featureFlagName, batch) {
|
|
177
187
|
var flagSetKey = this.keys.buildFlagSetKey(flagSetName);
|
|
178
|
-
|
|
179
|
-
if (!
|
|
180
|
-
return;
|
|
181
|
-
var flagSetCache = new Set(JSON.parse(flagSetFromStorage));
|
|
182
|
-
flagSetCache.delete(featureFlagName);
|
|
183
|
-
if (flagSetCache.size === 0) {
|
|
184
|
-
this.storage.removeItem(flagSetKey);
|
|
188
|
+
// nothing to remove from a flag set that is neither in storage nor in the batch
|
|
189
|
+
if (!batch.flagSets.has(flagSetKey) && !this.storage.getItem(flagSetKey))
|
|
185
190
|
return;
|
|
191
|
+
this.getFlagSetFromBatch(flagSetKey, batch).delete(featureFlagName);
|
|
192
|
+
};
|
|
193
|
+
/**
|
|
194
|
+
* Returns the flag names of the given flag set, reading from storage only the first time
|
|
195
|
+
*/
|
|
196
|
+
SplitsCacheInLocal.prototype.getFlagSetFromBatch = function (flagSetKey, batch) {
|
|
197
|
+
var flagSetCache = batch.flagSets.get(flagSetKey);
|
|
198
|
+
if (!flagSetCache) {
|
|
199
|
+
var flagSetFromStorage = this.storage.getItem(flagSetKey);
|
|
200
|
+
flagSetCache = new Set(flagSetFromStorage ? JSON.parse(flagSetFromStorage) : []);
|
|
201
|
+
batch.flagSets.set(flagSetKey, flagSetCache);
|
|
186
202
|
}
|
|
187
|
-
|
|
203
|
+
return flagSetCache;
|
|
204
|
+
};
|
|
205
|
+
SplitsCacheInLocal.prototype.flushBatch = function (batch) {
|
|
206
|
+
var _this = this;
|
|
207
|
+
// flush counts
|
|
208
|
+
batch.counts.forEach(function (diff, key) {
|
|
209
|
+
try {
|
|
210
|
+
if (diff === 0)
|
|
211
|
+
return;
|
|
212
|
+
var count = toNumber(_this.storage.getItem(key)) + diff;
|
|
213
|
+
if (count > 0)
|
|
214
|
+
_this.storage.setItem(key, count + '');
|
|
215
|
+
else
|
|
216
|
+
_this.storage.removeItem(key);
|
|
217
|
+
}
|
|
218
|
+
catch (e) {
|
|
219
|
+
_this.log.error(LOG_PREFIX + e);
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
// flush flag sets
|
|
223
|
+
batch.flagSets.forEach(function (flagSetCache, flagSetKey) {
|
|
224
|
+
try {
|
|
225
|
+
if (flagSetCache.size === 0)
|
|
226
|
+
_this.storage.removeItem(flagSetKey);
|
|
227
|
+
else
|
|
228
|
+
_this.storage.setItem(flagSetKey, JSON.stringify(setToArray(flagSetCache)));
|
|
229
|
+
}
|
|
230
|
+
catch (e) {
|
|
231
|
+
_this.log.error(LOG_PREFIX + e);
|
|
232
|
+
}
|
|
233
|
+
});
|
|
188
234
|
};
|
|
189
235
|
return SplitsCacheInLocal;
|
|
190
236
|
}(AbstractSplitsCacheSync));
|
package/package.json
CHANGED
|
@@ -8,6 +8,18 @@ import { ISettings } from '../../types';
|
|
|
8
8
|
import { setToArray } from '../../utils/lang/sets';
|
|
9
9
|
import { StorageAdapter } from '../types';
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Changes accumulated in memory while applying an update, so that each flag set and counter delta is written once at the end of the update.
|
|
13
|
+
*/
|
|
14
|
+
interface UpdateBatch {
|
|
15
|
+
flagSets: Map<string, Set<string>>;
|
|
16
|
+
counts: Map<string, number>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function newBatch(): UpdateBatch {
|
|
20
|
+
return { flagSets: new Map(), counts: new Map() };
|
|
21
|
+
}
|
|
22
|
+
|
|
11
23
|
export class SplitsCacheInLocal extends AbstractSplitsCacheSync {
|
|
12
24
|
|
|
13
25
|
private readonly keys: KeyBuilderCS;
|
|
@@ -24,41 +36,15 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync {
|
|
|
24
36
|
this.storage = storage;
|
|
25
37
|
}
|
|
26
38
|
|
|
27
|
-
private
|
|
28
|
-
|
|
29
|
-
if (count > 0) this.storage.setItem(key, count + '');
|
|
30
|
-
else this.storage.removeItem(key);
|
|
39
|
+
private updateCount(batch: UpdateBatch, key: string, diff: number) {
|
|
40
|
+
batch.counts.set(key, (batch.counts.get(key) || 0) + diff);
|
|
31
41
|
}
|
|
32
42
|
|
|
33
|
-
private
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
this._decrementCount(ttKey);
|
|
37
|
-
|
|
38
|
-
if (usesSegments(split)) {
|
|
39
|
-
const segmentsCountKey = this.keys.buildSplitsWithSegmentCountKey();
|
|
40
|
-
this._decrementCount(segmentsCountKey);
|
|
41
|
-
}
|
|
42
|
-
} catch (e) {
|
|
43
|
-
this.log.error(LOG_PREFIX + e);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
private _incrementCounts(split: ISplit) {
|
|
48
|
-
try {
|
|
49
|
-
const ttKey = this.keys.buildTrafficTypeKey(split.trafficTypeName);
|
|
50
|
-
this.storage.setItem(ttKey, (toNumber(this.storage.getItem(ttKey)) + 1) + '');
|
|
51
|
-
|
|
52
|
-
if (usesSegments(split)) {
|
|
53
|
-
const segmentsCountKey = this.keys.buildSplitsWithSegmentCountKey();
|
|
54
|
-
this.storage.setItem(segmentsCountKey, (toNumber(this.storage.getItem(segmentsCountKey)) + 1) + '');
|
|
55
|
-
}
|
|
56
|
-
} catch (e) {
|
|
57
|
-
this.log.error(LOG_PREFIX + e);
|
|
58
|
-
}
|
|
43
|
+
private updateCounts(split: ISplit, batch: UpdateBatch, diff: number) {
|
|
44
|
+
this.updateCount(batch, this.keys.buildTrafficTypeKey(split.trafficTypeName), diff);
|
|
45
|
+
if (usesSegments(split)) this.updateCount(batch, this.keys.buildSplitsWithSegmentCountKey(), diff);
|
|
59
46
|
}
|
|
60
47
|
|
|
61
|
-
|
|
62
48
|
/**
|
|
63
49
|
* Removes all splits cache related data from localStorage (splits, counters, changeNumber and lastUpdated).
|
|
64
50
|
* We cannot simply call `localStorage.clear()` since that implies removing user items from the storage.
|
|
@@ -79,33 +65,69 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync {
|
|
|
79
65
|
this.hasSync = false;
|
|
80
66
|
}
|
|
81
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Overrides the default implementation to batch the flag set and counter writes: each flag set and each counter is read from
|
|
70
|
+
* storage once, updated in memory, and written once at the end, instead of once per feature flag.
|
|
71
|
+
* `update` is synchronous, so no other reader can observe the intermediate state.
|
|
72
|
+
*/
|
|
73
|
+
update(toAdd: ISplit[], toRemove: ISplit[], changeNumber: number): boolean {
|
|
74
|
+
const batch = newBatch();
|
|
75
|
+
let updated;
|
|
76
|
+
try {
|
|
77
|
+
updated = toAdd.map(addedFF => this.addSplitToBatch(addedFF, batch)).some(result => result);
|
|
78
|
+
updated = toRemove.map(removedFF => this.removeSplitFromBatch(removedFF.name, batch)).some(result => result) || updated;
|
|
79
|
+
} finally {
|
|
80
|
+
this.flushBatch(batch);
|
|
81
|
+
}
|
|
82
|
+
this.setChangeNumber(changeNumber);
|
|
83
|
+
return updated;
|
|
84
|
+
}
|
|
85
|
+
|
|
82
86
|
addSplit(split: ISplit) {
|
|
87
|
+
const batch = newBatch();
|
|
88
|
+
try {
|
|
89
|
+
return this.addSplitToBatch(split, batch);
|
|
90
|
+
} finally {
|
|
91
|
+
this.flushBatch(batch);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
removeSplit(name: string): boolean {
|
|
96
|
+
const batch = newBatch();
|
|
97
|
+
try {
|
|
98
|
+
return this.removeSplitFromBatch(name, batch);
|
|
99
|
+
} finally {
|
|
100
|
+
this.flushBatch(batch);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private addSplitToBatch(split: ISplit, batch: UpdateBatch) {
|
|
83
105
|
const name = split.name;
|
|
84
106
|
const splitKey = this.keys.buildSplitKey(name);
|
|
85
107
|
const splitFromStorage = this.storage.getItem(splitKey);
|
|
86
108
|
const previousSplit = splitFromStorage ? JSON.parse(splitFromStorage) : null;
|
|
87
109
|
|
|
88
110
|
if (previousSplit) {
|
|
89
|
-
this.
|
|
90
|
-
this.removeFromFlagSets(previousSplit.name, previousSplit.sets);
|
|
111
|
+
this.updateCounts(previousSplit, batch, -1);
|
|
112
|
+
this.removeFromFlagSets(previousSplit.name, previousSplit.sets, batch);
|
|
91
113
|
}
|
|
92
114
|
|
|
93
115
|
this.storage.setItem(splitKey, JSON.stringify(split));
|
|
94
116
|
|
|
95
|
-
this.
|
|
96
|
-
this.addToFlagSets(split);
|
|
117
|
+
this.updateCounts(split, batch, 1);
|
|
118
|
+
this.addToFlagSets(split, batch);
|
|
97
119
|
|
|
98
120
|
return true;
|
|
99
121
|
}
|
|
100
122
|
|
|
101
|
-
|
|
123
|
+
private removeSplitFromBatch(name: string, batch: UpdateBatch): boolean {
|
|
102
124
|
const split = this.getSplit(name);
|
|
103
125
|
if (!split) return false;
|
|
104
126
|
|
|
105
127
|
this.storage.removeItem(this.keys.buildSplitKey(name));
|
|
106
128
|
|
|
107
|
-
this.
|
|
108
|
-
this.removeFromFlagSets(split.name, split.sets);
|
|
129
|
+
this.updateCounts(split, batch, -1);
|
|
130
|
+
this.removeFromFlagSets(split.name, split.sets, batch);
|
|
109
131
|
|
|
110
132
|
return true;
|
|
111
133
|
}
|
|
@@ -184,7 +206,7 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync {
|
|
|
184
206
|
});
|
|
185
207
|
}
|
|
186
208
|
|
|
187
|
-
private addToFlagSets(featureFlag: ISplit) {
|
|
209
|
+
private addToFlagSets(featureFlag: ISplit, batch: UpdateBatch) {
|
|
188
210
|
if (!featureFlag.sets) return;
|
|
189
211
|
|
|
190
212
|
featureFlag.sets.forEach(featureFlagSet => {
|
|
@@ -193,42 +215,62 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync {
|
|
|
193
215
|
|
|
194
216
|
const flagSetKey = this.keys.buildFlagSetKey(featureFlagSet);
|
|
195
217
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
const flagSetCache = new Set(flagSetFromStorage ? JSON.parse(flagSetFromStorage) : []);
|
|
199
|
-
|
|
200
|
-
if (flagSetCache.has(featureFlag.name)) return;
|
|
201
|
-
|
|
202
|
-
flagSetCache.add(featureFlag.name);
|
|
203
|
-
|
|
204
|
-
this.storage.setItem(flagSetKey, JSON.stringify(setToArray(flagSetCache)));
|
|
218
|
+
this.getFlagSetFromBatch(flagSetKey, batch).add(featureFlag.name);
|
|
205
219
|
});
|
|
206
220
|
}
|
|
207
221
|
|
|
208
|
-
private removeFromFlagSets(featureFlagName: string, flagSets
|
|
222
|
+
private removeFromFlagSets(featureFlagName: string, flagSets: string[] | null | undefined, batch: UpdateBatch) {
|
|
209
223
|
if (!flagSets) return;
|
|
210
224
|
|
|
211
225
|
flagSets.forEach(flagSet => {
|
|
212
|
-
this.removeNames(flagSet, featureFlagName);
|
|
226
|
+
this.removeNames(flagSet, featureFlagName, batch);
|
|
213
227
|
});
|
|
214
228
|
}
|
|
215
229
|
|
|
216
|
-
private removeNames(flagSetName: string, featureFlagName: string) {
|
|
230
|
+
private removeNames(flagSetName: string, featureFlagName: string, batch: UpdateBatch) {
|
|
217
231
|
const flagSetKey = this.keys.buildFlagSetKey(flagSetName);
|
|
218
232
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
if (!flagSetFromStorage) return;
|
|
233
|
+
// nothing to remove from a flag set that is neither in storage nor in the batch
|
|
234
|
+
if (!batch.flagSets.has(flagSetKey) && !this.storage.getItem(flagSetKey)) return;
|
|
222
235
|
|
|
223
|
-
|
|
224
|
-
|
|
236
|
+
this.getFlagSetFromBatch(flagSetKey, batch).delete(featureFlagName);
|
|
237
|
+
}
|
|
225
238
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
239
|
+
/**
|
|
240
|
+
* Returns the flag names of the given flag set, reading from storage only the first time
|
|
241
|
+
*/
|
|
242
|
+
private getFlagSetFromBatch(flagSetKey: string, batch: UpdateBatch): Set<string> {
|
|
243
|
+
let flagSetCache = batch.flagSets.get(flagSetKey);
|
|
244
|
+
if (!flagSetCache) {
|
|
245
|
+
const flagSetFromStorage = this.storage.getItem(flagSetKey);
|
|
246
|
+
flagSetCache = new Set(flagSetFromStorage ? JSON.parse(flagSetFromStorage) : []);
|
|
247
|
+
batch.flagSets.set(flagSetKey, flagSetCache);
|
|
229
248
|
}
|
|
249
|
+
return flagSetCache;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
private flushBatch(batch: UpdateBatch) {
|
|
253
|
+
// flush counts
|
|
254
|
+
batch.counts.forEach((diff, key) => {
|
|
255
|
+
try {
|
|
256
|
+
if (diff === 0) return;
|
|
257
|
+
const count = toNumber(this.storage.getItem(key)) + diff;
|
|
258
|
+
if (count > 0) this.storage.setItem(key, count + '');
|
|
259
|
+
else this.storage.removeItem(key);
|
|
260
|
+
} catch (e) {
|
|
261
|
+
this.log.error(LOG_PREFIX + e);
|
|
262
|
+
}
|
|
263
|
+
});
|
|
230
264
|
|
|
231
|
-
|
|
265
|
+
// flush flag sets
|
|
266
|
+
batch.flagSets.forEach((flagSetCache, flagSetKey) => {
|
|
267
|
+
try {
|
|
268
|
+
if (flagSetCache.size === 0) this.storage.removeItem(flagSetKey);
|
|
269
|
+
else this.storage.setItem(flagSetKey, JSON.stringify(setToArray(flagSetCache)));
|
|
270
|
+
} catch (e) {
|
|
271
|
+
this.log.error(LOG_PREFIX + e);
|
|
272
|
+
}
|
|
273
|
+
});
|
|
232
274
|
}
|
|
233
275
|
|
|
234
276
|
}
|