@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._decrementCount = function (key) {
20
- var count = (0, lang_1.toNumber)(this.storage.getItem(key)) - 1;
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._incrementCounts = function (split) {
40
- try {
41
- var ttKey = this.keys.buildTrafficTypeKey(split.trafficTypeName);
42
- this.storage.setItem(ttKey, ((0, lang_1.toNumber)(this.storage.getItem(ttKey)) + 1) + '');
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._decrementCounts(previousSplit);
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._incrementCounts(split);
83
- this.addToFlagSets(split);
97
+ this.updateCounts(split, batch, 1);
98
+ this.addToFlagSets(split, batch);
84
99
  return true;
85
100
  };
86
- SplitsCacheInLocal.prototype.removeSplit = function (name) {
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._decrementCounts(split);
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
- var flagSetFromStorage = _this.storage.getItem(flagSetKey);
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
- var flagSetFromStorage = this.storage.getItem(flagSetKey);
182
- if (!flagSetFromStorage)
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
- this.storage.setItem(flagSetKey, JSON.stringify((0, sets_1.setToArray)(flagSetCache)));
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._decrementCount = function (key) {
17
- var count = toNumber(this.storage.getItem(key)) - 1;
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._incrementCounts = function (split) {
37
- try {
38
- var ttKey = this.keys.buildTrafficTypeKey(split.trafficTypeName);
39
- this.storage.setItem(ttKey, (toNumber(this.storage.getItem(ttKey)) + 1) + '');
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._decrementCounts(previousSplit);
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._incrementCounts(split);
80
- this.addToFlagSets(split);
94
+ this.updateCounts(split, batch, 1);
95
+ this.addToFlagSets(split, batch);
81
96
  return true;
82
97
  };
83
- SplitsCacheInLocal.prototype.removeSplit = function (name) {
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._decrementCounts(split);
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
- var flagSetFromStorage = _this.storage.getItem(flagSetKey);
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
- var flagSetFromStorage = this.storage.getItem(flagSetKey);
179
- if (!flagSetFromStorage)
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
- this.storage.setItem(flagSetKey, JSON.stringify(setToArray(flagSetCache)));
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@splitsoftware/splitio-commons",
3
- "version": "2.13.0",
3
+ "version": "2.13.1",
4
4
  "description": "Split JavaScript SDK common components",
5
5
  "main": "cjs/index.js",
6
6
  "module": "esm/index.js",
@@ -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 _decrementCount(key: string) {
28
- const count = toNumber(this.storage.getItem(key)) - 1;
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 _decrementCounts(split: ISplit) {
34
- try {
35
- const ttKey = this.keys.buildTrafficTypeKey(split.trafficTypeName);
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._decrementCounts(previousSplit);
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._incrementCounts(split);
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
- removeSplit(name: string): boolean {
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._decrementCounts(split);
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
- const flagSetFromStorage = this.storage.getItem(flagSetKey);
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?: string[] | null) {
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
- const flagSetFromStorage = this.storage.getItem(flagSetKey);
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
- const flagSetCache = new Set(JSON.parse(flagSetFromStorage));
224
- flagSetCache.delete(featureFlagName);
236
+ this.getFlagSetFromBatch(flagSetKey, batch).delete(featureFlagName);
237
+ }
225
238
 
226
- if (flagSetCache.size === 0) {
227
- this.storage.removeItem(flagSetKey);
228
- return;
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
- this.storage.setItem(flagSetKey, JSON.stringify(setToArray(flagSetCache)));
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
  }