@splitsoftware/splitio-commons 2.13.0 → 2.14.0-rc.0

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,9 @@
1
+ 2.14.0 (October XX, 2026)
2
+ - Added `storage.disableFlagSetCache` option (default `false`) for storage type `LOCALSTORAGE`. When set to `true`, the SDK skips building the internal flag set index, which speeds up initialization for large numbers of feature flags. The `getTreatments(WithConfig)ByFlagSet(s)` methods then return empty results and log an error.
3
+
4
+ 2.13.1 (October 7, 2026)
5
+ - Updated the 'LOCALSTORAGE' storage to batch the writes of flag sets and traffic type/segment counters when storing feature flags, reducing storage update times and the time to SDK_READY event.
6
+
1
7
  2.13.0 (September 24, 2026)
2
8
  - 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
9
  - 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.
@@ -2,7 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ENGINE_VALUE_INVALID = exports.SUBMITTERS_PUSH_PAGE_HIDDEN = exports.USER_CONSENT_INITIAL = exports.USER_CONSENT_NOT_UPDATED = exports.USER_CONSENT_UPDATED = exports.IMPRESSIONS_TRACKER_SUCCESS = exports.EVENTS_TRACKER_SUCCESS = exports.SYNC_STOP_POLLING = exports.SYNC_CONTINUE_POLLING = exports.SYNC_START_POLLING = exports.SUBMITTERS_PUSH = exports.SUBMITTERS_PUSH_FULL_QUEUE = exports.STREAMING_DISCONNECTING = exports.STREAMING_DISABLED = exports.STREAMING_CONNECTING = exports.STREAMING_RECONNECT = exports.STREAMING_REFRESH_TOKEN = exports.SYNC_SPLITS_FETCH_RETRY = exports.POLLING_STOP = exports.POLLING_START = exports.POLLING_SMART_PAUSING = exports.NEW_FACTORY = exports.NEW_SHARED_CLIENT = exports.IMPRESSION_QUEUEING = exports.CLIENT_READY = exports.CLIENT_READY_FROM_CACHE = exports.ENGINE_DEFAULT = exports.ENGINE_MATCHER_RESULT = exports.SETTINGS_SPLITS_FILTER = exports.SYNC_TASK_STOP = exports.SYNC_TASK_EXECUTE = exports.SYNC_TASK_START = exports.STREAMING_NEW_MESSAGE = exports.SYNC_RBS_UPDATE = exports.SYNC_SPLITS_UPDATE = exports.SYNC_SPLITS_FETCH = exports.SYNC_OFFLINE_DATA = exports.RETRIEVE_MANAGER = exports.RETRIEVE_CLIENT_EXISTING = exports.RETRIEVE_CLIENT_DEFAULT = exports.CLEANUP_DEREGISTERING = exports.CLEANUP_REGISTERING = exports.ENGINE_SANITIZE = exports.ENGINE_VALUE = exports.ENGINE_MATCHER_DEPENDENCY_PRE = exports.ENGINE_MATCHER_DEPENDENCY = exports.ENGINE_BUCKET = exports.ENGINE_COMBINER_IFELSEIF_NO_TREATMENT = exports.ENGINE_COMBINER_IFELSEIF = exports.ENGINE_COMBINER_AND = void 0;
4
4
  exports.ERROR_INVALID_CONFIG_PARAM = exports.ERROR_EMPTY_ARRAY = exports.ERROR_EMPTY = exports.ERROR_INVALID = exports.ERROR_INVALID_KEY_OBJECT = exports.ERROR_TOO_LONG = exports.ERROR_NULL = exports.ERROR_CLIENT_DESTROYED = exports.ERROR_NOT_FINITE = exports.ERROR_SIZE_EXCEEDED = exports.ERROR_NOT_PLAIN_OBJECT = exports.ERROR_EVENT_TYPE_FORMAT = exports.ERROR_EVENTS_TRACKER = exports.ERROR_IMPRESSIONS_LISTENER = exports.ERROR_IMPRESSIONS_TRACKER = exports.ERROR_STREAMING_AUTH = exports.ERROR_STREAMING_SSE = exports.ERROR_SYNC_OFFLINE_LOADING = exports.ERROR_CLIENT_CANNOT_GET_READY = exports.ERROR_CLIENT_LISTENER = exports.ERROR_LOGLEVEL_INVALID = exports.ERROR_ENGINE_COMBINER_IFELSEIF = exports.WARN_FLAGSET_WITHOUT_FLAGS = exports.WARN_FLAGSET_NOT_CONFIGURED = exports.WARN_LOWERCASE_FLAGSET = exports.WARN_INVALID_FLAGSET = exports.STREAMING_PARSING_SPLIT_UPDATE = exports.STREAMING_PARSING_MEMBERSHIPS_UPDATE = exports.WARN_SDK_KEY = exports.WARN_SPLITS_FILTER_EMPTY = exports.WARN_SPLITS_FILTER_INVALID = exports.WARN_SPLITS_FILTER_IGNORED = exports.WARN_INTEGRATION_INVALID = exports.WARN_NOT_EXISTENT_TT = exports.WARN_LOWERCASE_TRAFFIC_TYPE = exports.WARN_NOT_EXISTENT_SPLIT = exports.WARN_TRIMMING = exports.WARN_CONVERTING = exports.WARN_TRIMMING_PROPERTIES = exports.WARN_SETTING_NULL = exports.SUBMITTERS_PUSH_RETRY = exports.SUBMITTERS_PUSH_FAILS = exports.STREAMING_FALLBACK = exports.STREAMING_PARSING_MESSAGE_FAILS = exports.STREAMING_PARSING_ERROR_FAILS = exports.SYNC_SPLITS_FETCH_FAILS = exports.SYNC_MYSEGMENTS_FETCH_RETRY = exports.CLIENT_NOT_READY_FROM_CACHE = exports.CLIENT_NO_LISTENER = exports.ENGINE_VALUE_NO_ATTRIBUTES = void 0;
5
- exports.LOG_PREFIX_CLEANUP = exports.LOG_PREFIX_UNIQUE_KEYS_TRACKER = exports.LOG_PREFIX_EVENTS_TRACKER = exports.LOG_PREFIX_IMPRESSIONS_TRACKER = exports.LOG_PREFIX_SYNC_SUBMITTERS = exports.LOG_PREFIX_SYNC_POLLING = exports.LOG_PREFIX_SYNC_MYSEGMENTS = exports.LOG_PREFIX_SYNC_SEGMENTS = exports.LOG_PREFIX_SYNC_SPLITS = exports.LOG_PREFIX_SYNC_STREAMING = exports.LOG_PREFIX_SYNC_OFFLINE = exports.LOG_PREFIX_SYNC_MANAGER = exports.LOG_PREFIX_SYNC = exports.LOG_PREFIX_ENGINE_VALUE = exports.LOG_PREFIX_ENGINE_MATCHER = exports.LOG_PREFIX_ENGINE_COMBINER = exports.LOG_PREFIX_ENGINE = exports.LOG_PREFIX_CLIENT_INSTANTIATION = exports.LOG_PREFIX_INSTANTIATION = exports.LOG_PREFIX_SETTINGS = exports.ENGINE_MATCHER_ERROR = exports.ERROR_SETS_FILTER_EXCLUSIVE = exports.ERROR_TOO_MANY_SETS = exports.ERROR_MIN_CONFIG_PARAM = exports.ERROR_NOT_BOOLEAN = exports.ERROR_STORAGE_INVALID = exports.ERROR_HTTP = void 0;
5
+ exports.LOG_PREFIX_CLEANUP = exports.LOG_PREFIX_UNIQUE_KEYS_TRACKER = exports.LOG_PREFIX_EVENTS_TRACKER = exports.LOG_PREFIX_IMPRESSIONS_TRACKER = exports.LOG_PREFIX_SYNC_SUBMITTERS = exports.LOG_PREFIX_SYNC_POLLING = exports.LOG_PREFIX_SYNC_MYSEGMENTS = exports.LOG_PREFIX_SYNC_SEGMENTS = exports.LOG_PREFIX_SYNC_SPLITS = exports.LOG_PREFIX_SYNC_STREAMING = exports.LOG_PREFIX_SYNC_OFFLINE = exports.LOG_PREFIX_SYNC_MANAGER = exports.LOG_PREFIX_SYNC = exports.LOG_PREFIX_ENGINE_VALUE = exports.LOG_PREFIX_ENGINE_MATCHER = exports.LOG_PREFIX_ENGINE_COMBINER = exports.LOG_PREFIX_ENGINE = exports.LOG_PREFIX_CLIENT_INSTANTIATION = exports.LOG_PREFIX_INSTANTIATION = exports.LOG_PREFIX_SETTINGS = exports.ERROR_FLAGSET_CACHE_DISABLED = exports.ENGINE_MATCHER_ERROR = exports.ERROR_SETS_FILTER_EXCLUSIVE = exports.ERROR_TOO_MANY_SETS = exports.ERROR_MIN_CONFIG_PARAM = exports.ERROR_NOT_BOOLEAN = exports.ERROR_STORAGE_INVALID = exports.ERROR_HTTP = void 0;
6
6
  /**
7
7
  * Message codes used to trim string log messages from commons and client-side API modules,
8
8
  * in order to reduce the minimal SDK size for Browser and eventually other client-side environments.
@@ -117,6 +117,7 @@ exports.ERROR_MIN_CONFIG_PARAM = 326;
117
117
  exports.ERROR_TOO_MANY_SETS = 327;
118
118
  exports.ERROR_SETS_FILTER_EXCLUSIVE = 328;
119
119
  exports.ENGINE_MATCHER_ERROR = 329;
120
+ exports.ERROR_FLAGSET_CACHE_DISABLED = 330;
120
121
  // Log prefixes (a.k.a. tags or categories)
121
122
  exports.LOG_PREFIX_SETTINGS = 'settings';
122
123
  exports.LOG_PREFIX_INSTANTIATION = 'Factory instantiation';
@@ -33,6 +33,7 @@ exports.codesError = [
33
33
  [c.ERROR_EMPTY, '%s: you passed an empty %s. It must be a non-empty string.'],
34
34
  [c.ERROR_EMPTY_ARRAY, '%s: %s must be a non-empty array.'],
35
35
  [c.ERROR_NOT_BOOLEAN, '%s: provided param must be a boolean value.'],
36
+ [c.ERROR_FLAGSET_CACHE_DISABLED, 'The flag set cache is disabled via the `disableFlagSetCache` setting, so `getTreatments(WithConfig)ByFlagSet(s)` methods always return empty objects. Remove the setting to use them.'],
36
37
  // initialization / settings validation
37
38
  [c.ERROR_INVALID_CONFIG_PARAM, c.LOG_PREFIX_SETTINGS + ': you passed an invalid "%s" config param. It should be one of the following values: %s. Defaulting to "%s".'],
38
39
  [c.ERROR_STORAGE_INVALID, c.LOG_PREFIX_SETTINGS + ': the provided storage is invalid.%s Falling back into default MEMORY storage'],
@@ -5,49 +5,29 @@ var tslib_1 = require("tslib");
5
5
  var AbstractSplitsCacheSync_1 = require("../AbstractSplitsCacheSync");
6
6
  var lang_1 = require("../../utils/lang");
7
7
  var constants_1 = require("./constants");
8
+ var constants_2 = require("../../logger/constants");
8
9
  var sets_1 = require("../../utils/lang/sets");
10
+ function newBatch() {
11
+ return { flagSets: new Map(), counts: new Map() };
12
+ }
9
13
  var SplitsCacheInLocal = /** @class */ (function (_super) {
10
14
  tslib_1.__extends(SplitsCacheInLocal, _super);
11
- function SplitsCacheInLocal(settings, keys, storage) {
15
+ function SplitsCacheInLocal(settings, keys, storage, disableFlagSetCache) {
12
16
  var _this = _super.call(this) || this;
13
17
  _this.keys = keys;
14
18
  _this.log = settings.log;
15
19
  _this.flagSetsFilter = settings.sync.__splitFiltersValidation.groupedFilters.bySet;
16
20
  _this.storage = storage;
21
+ _this.disableFlagSetCache = disableFlagSetCache;
17
22
  return _this;
18
23
  }
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
- }
24
+ SplitsCacheInLocal.prototype.updateCount = function (batch, key, diff) {
25
+ batch.counts.set(key, (batch.counts.get(key) || 0) + diff);
38
26
  };
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
- }
27
+ SplitsCacheInLocal.prototype.updateCounts = function (split, batch, diff) {
28
+ this.updateCount(batch, this.keys.buildTrafficTypeKey(split.trafficTypeName), diff);
29
+ if ((0, AbstractSplitsCacheSync_1.usesSegments)(split))
30
+ this.updateCount(batch, this.keys.buildSplitsWithSegmentCountKey(), diff);
51
31
  };
52
32
  /**
53
33
  * Removes all splits cache related data from localStorage (splits, counters, changeNumber and lastUpdated).
@@ -69,27 +49,64 @@ var SplitsCacheInLocal = /** @class */ (function (_super) {
69
49
  });
70
50
  this.hasSync = false;
71
51
  };
52
+ /**
53
+ * Overrides the default implementation to batch the flag set and counter writes: each flag set and each counter is read from
54
+ * storage once, updated in memory, and written once at the end, instead of once per feature flag.
55
+ * `update` is synchronous, so no other reader can observe the intermediate state.
56
+ */
57
+ SplitsCacheInLocal.prototype.update = function (toAdd, toRemove, changeNumber) {
58
+ var _this = this;
59
+ var batch = newBatch();
60
+ var updated;
61
+ try {
62
+ updated = toAdd.map(function (addedFF) { return _this.addSplitToBatch(addedFF, batch); }).some(function (result) { return result; });
63
+ updated = toRemove.map(function (removedFF) { return _this.removeSplitFromBatch(removedFF.name, batch); }).some(function (result) { return result; }) || updated;
64
+ }
65
+ finally {
66
+ this.flushBatch(batch);
67
+ }
68
+ this.setChangeNumber(changeNumber);
69
+ return updated;
70
+ };
72
71
  SplitsCacheInLocal.prototype.addSplit = function (split) {
72
+ var batch = newBatch();
73
+ try {
74
+ return this.addSplitToBatch(split, batch);
75
+ }
76
+ finally {
77
+ this.flushBatch(batch);
78
+ }
79
+ };
80
+ SplitsCacheInLocal.prototype.removeSplit = function (name) {
81
+ var batch = newBatch();
82
+ try {
83
+ return this.removeSplitFromBatch(name, batch);
84
+ }
85
+ finally {
86
+ this.flushBatch(batch);
87
+ }
88
+ };
89
+ SplitsCacheInLocal.prototype.addSplitToBatch = function (split, batch) {
73
90
  var name = split.name;
74
91
  var splitKey = this.keys.buildSplitKey(name);
75
92
  var splitFromStorage = this.storage.getItem(splitKey);
76
93
  var previousSplit = splitFromStorage ? JSON.parse(splitFromStorage) : null;
77
94
  if (previousSplit) {
78
- this._decrementCounts(previousSplit);
79
- this.removeFromFlagSets(previousSplit.name, previousSplit.sets);
95
+ this.updateCounts(previousSplit, batch, -1);
96
+ this.removeFromFlagSets(previousSplit.name, previousSplit.sets, batch);
80
97
  }
81
98
  this.storage.setItem(splitKey, JSON.stringify(split));
82
- this._incrementCounts(split);
83
- this.addToFlagSets(split);
99
+ this.updateCounts(split, batch, 1);
100
+ this.addToFlagSets(split, batch);
84
101
  return true;
85
102
  };
86
- SplitsCacheInLocal.prototype.removeSplit = function (name) {
103
+ SplitsCacheInLocal.prototype.removeSplitFromBatch = function (name, batch) {
87
104
  var split = this.getSplit(name);
88
105
  if (!split)
89
106
  return false;
90
107
  this.storage.removeItem(this.keys.buildSplitKey(name));
91
- this._decrementCounts(split);
92
- this.removeFromFlagSets(split.name, split.sets);
108
+ this.updateCounts(split, batch, -1);
109
+ this.removeFromFlagSets(split.name, split.sets, batch);
93
110
  return true;
94
111
  };
95
112
  SplitsCacheInLocal.prototype.getSplit = function (name) {
@@ -146,48 +163,83 @@ var SplitsCacheInLocal = /** @class */ (function (_super) {
146
163
  };
147
164
  SplitsCacheInLocal.prototype.getNamesByFlagSets = function (flagSets) {
148
165
  var _this = this;
166
+ if (this.disableFlagSetCache) {
167
+ this.log.error(constants_2.ERROR_FLAGSET_CACHE_DISABLED);
168
+ return flagSets.map(function () { return new Set(); });
169
+ }
149
170
  return flagSets.map(function (flagSet) {
150
171
  var flagSetKey = _this.keys.buildFlagSetKey(flagSet);
151
172
  var flagSetFromStorage = _this.storage.getItem(flagSetKey);
152
173
  return new Set(flagSetFromStorage ? JSON.parse(flagSetFromStorage) : []);
153
174
  });
154
175
  };
155
- SplitsCacheInLocal.prototype.addToFlagSets = function (featureFlag) {
176
+ SplitsCacheInLocal.prototype.addToFlagSets = function (featureFlag, batch) {
156
177
  var _this = this;
157
- if (!featureFlag.sets)
178
+ if (this.disableFlagSetCache || !featureFlag.sets)
158
179
  return;
159
180
  featureFlag.sets.forEach(function (featureFlagSet) {
160
181
  if (_this.flagSetsFilter.length > 0 && !_this.flagSetsFilter.some(function (filterFlagSet) { return filterFlagSet === featureFlagSet; }))
161
182
  return;
162
183
  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)));
184
+ _this.getFlagSetFromBatch(flagSetKey, batch).add(featureFlag.name);
169
185
  });
170
186
  };
171
- SplitsCacheInLocal.prototype.removeFromFlagSets = function (featureFlagName, flagSets) {
187
+ SplitsCacheInLocal.prototype.removeFromFlagSets = function (featureFlagName, flagSets, batch) {
172
188
  var _this = this;
173
- if (!flagSets)
189
+ if (this.disableFlagSetCache || !flagSets)
174
190
  return;
175
191
  flagSets.forEach(function (flagSet) {
176
- _this.removeNames(flagSet, featureFlagName);
192
+ _this.removeNames(flagSet, featureFlagName, batch);
177
193
  });
178
194
  };
179
- SplitsCacheInLocal.prototype.removeNames = function (flagSetName, featureFlagName) {
195
+ SplitsCacheInLocal.prototype.removeNames = function (flagSetName, featureFlagName, batch) {
180
196
  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);
197
+ // nothing to remove from a flag set that is neither in storage nor in the batch
198
+ if (!batch.flagSets.has(flagSetKey) && !this.storage.getItem(flagSetKey))
188
199
  return;
200
+ this.getFlagSetFromBatch(flagSetKey, batch).delete(featureFlagName);
201
+ };
202
+ /**
203
+ * Returns the flag names of the given flag set, reading from storage only the first time
204
+ */
205
+ SplitsCacheInLocal.prototype.getFlagSetFromBatch = function (flagSetKey, batch) {
206
+ var flagSetCache = batch.flagSets.get(flagSetKey);
207
+ if (!flagSetCache) {
208
+ var flagSetFromStorage = this.storage.getItem(flagSetKey);
209
+ flagSetCache = new Set(flagSetFromStorage ? JSON.parse(flagSetFromStorage) : []);
210
+ batch.flagSets.set(flagSetKey, flagSetCache);
189
211
  }
190
- this.storage.setItem(flagSetKey, JSON.stringify((0, sets_1.setToArray)(flagSetCache)));
212
+ return flagSetCache;
213
+ };
214
+ SplitsCacheInLocal.prototype.flushBatch = function (batch) {
215
+ var _this = this;
216
+ // flush counts
217
+ batch.counts.forEach(function (diff, key) {
218
+ try {
219
+ if (diff === 0)
220
+ return;
221
+ var count = (0, lang_1.toNumber)(_this.storage.getItem(key)) + diff;
222
+ if (count > 0)
223
+ _this.storage.setItem(key, count + '');
224
+ else
225
+ _this.storage.removeItem(key);
226
+ }
227
+ catch (e) {
228
+ _this.log.error(constants_1.LOG_PREFIX + e);
229
+ }
230
+ });
231
+ // flush flag sets
232
+ batch.flagSets.forEach(function (flagSetCache, flagSetKey) {
233
+ try {
234
+ if (flagSetCache.size === 0)
235
+ _this.storage.removeItem(flagSetKey);
236
+ else
237
+ _this.storage.setItem(flagSetKey, JSON.stringify((0, sets_1.setToArray)(flagSetCache)));
238
+ }
239
+ catch (e) {
240
+ _this.log.error(constants_1.LOG_PREFIX + e);
241
+ }
242
+ });
191
243
  };
192
244
  return SplitsCacheInLocal;
193
245
  }(AbstractSplitsCacheSync_1.AbstractSplitsCacheSync));
@@ -18,6 +18,8 @@ var UniqueKeysCacheInMemoryCS_1 = require("../inMemory/UniqueKeysCacheInMemoryCS
18
18
  var key_1 = require("../../utils/key");
19
19
  var validateCache_1 = require("./validateCache");
20
20
  var storageAdapter_1 = require("./storageAdapter");
21
+ var lang_1 = require("../../utils/lang");
22
+ var constants_3 = require("../../logger/constants");
21
23
  function validateStorage(log, prefix, wrapper) {
22
24
  if (wrapper) {
23
25
  if ((0, isLocalStorageAvailable_1.isValidStorageWrapper)(wrapper)) {
@@ -44,7 +46,10 @@ function InLocalStorage(options) {
44
46
  return (0, InMemoryStorageCS_1.InMemoryStorageCSFactory)(params);
45
47
  var matchingKey = (0, key_1.getMatching)(settings.core.key);
46
48
  var keys = new KeyBuilderCS_1.KeyBuilderCS(prefix, matchingKey);
47
- var splits = new SplitsCacheInLocal_1.SplitsCacheInLocal(settings, keys, storage);
49
+ if (options.disableFlagSetCache !== undefined && !(0, lang_1.isBoolean)(options.disableFlagSetCache)) {
50
+ log.error(constants_3.ERROR_INVALID_CONFIG_PARAM, ['disableFlagSetCache', 'true, false', 'false']);
51
+ }
52
+ var splits = new SplitsCacheInLocal_1.SplitsCacheInLocal(settings, keys, storage, options.disableFlagSetCache === true);
48
53
  var rbSegments = new RBSegmentsCacheInLocal_1.RBSegmentsCacheInLocal(settings, keys, storage);
49
54
  var segments = new MySegmentsCacheInLocal_1.MySegmentsCacheInLocal(log, keys, storage);
50
55
  var largeSegments = new MySegmentsCacheInLocal_1.MySegmentsCacheInLocal(log, (0, KeyBuilderCS_1.myLargeSegmentsKeyBuilder)(prefix, matchingKey), storage);
@@ -112,6 +112,7 @@ export var ERROR_MIN_CONFIG_PARAM = 326;
112
112
  export var ERROR_TOO_MANY_SETS = 327;
113
113
  export var ERROR_SETS_FILTER_EXCLUSIVE = 328;
114
114
  export var ENGINE_MATCHER_ERROR = 329;
115
+ export var ERROR_FLAGSET_CACHE_DISABLED = 330;
115
116
  // Log prefixes (a.k.a. tags or categories)
116
117
  export var LOG_PREFIX_SETTINGS = 'settings';
117
118
  export var LOG_PREFIX_INSTANTIATION = 'Factory instantiation';
@@ -29,6 +29,7 @@ export var codesError = [
29
29
  [c.ERROR_EMPTY, '%s: you passed an empty %s. It must be a non-empty string.'],
30
30
  [c.ERROR_EMPTY_ARRAY, '%s: %s must be a non-empty array.'],
31
31
  [c.ERROR_NOT_BOOLEAN, '%s: provided param must be a boolean value.'],
32
+ [c.ERROR_FLAGSET_CACHE_DISABLED, 'The flag set cache is disabled via the `disableFlagSetCache` setting, so `getTreatments(WithConfig)ByFlagSet(s)` methods always return empty objects. Remove the setting to use them.'],
32
33
  // initialization / settings validation
33
34
  [c.ERROR_INVALID_CONFIG_PARAM, c.LOG_PREFIX_SETTINGS + ': you passed an invalid "%s" config param. It should be one of the following values: %s. Defaulting to "%s".'],
34
35
  [c.ERROR_STORAGE_INVALID, c.LOG_PREFIX_SETTINGS + ': the provided storage is invalid.%s Falling back into default MEMORY storage'],
@@ -2,49 +2,29 @@ import { __extends } from "tslib";
2
2
  import { AbstractSplitsCacheSync, usesSegments } from '../AbstractSplitsCacheSync';
3
3
  import { isFiniteNumber, toNumber, isNaNNumber } from '../../utils/lang';
4
4
  import { LOG_PREFIX } from './constants';
5
+ import { ERROR_FLAGSET_CACHE_DISABLED } from '../../logger/constants';
5
6
  import { setToArray } from '../../utils/lang/sets';
7
+ function newBatch() {
8
+ return { flagSets: new Map(), counts: new Map() };
9
+ }
6
10
  var SplitsCacheInLocal = /** @class */ (function (_super) {
7
11
  __extends(SplitsCacheInLocal, _super);
8
- function SplitsCacheInLocal(settings, keys, storage) {
12
+ function SplitsCacheInLocal(settings, keys, storage, disableFlagSetCache) {
9
13
  var _this = _super.call(this) || this;
10
14
  _this.keys = keys;
11
15
  _this.log = settings.log;
12
16
  _this.flagSetsFilter = settings.sync.__splitFiltersValidation.groupedFilters.bySet;
13
17
  _this.storage = storage;
18
+ _this.disableFlagSetCache = disableFlagSetCache;
14
19
  return _this;
15
20
  }
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
- }
21
+ SplitsCacheInLocal.prototype.updateCount = function (batch, key, diff) {
22
+ batch.counts.set(key, (batch.counts.get(key) || 0) + diff);
35
23
  };
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
- }
24
+ SplitsCacheInLocal.prototype.updateCounts = function (split, batch, diff) {
25
+ this.updateCount(batch, this.keys.buildTrafficTypeKey(split.trafficTypeName), diff);
26
+ if (usesSegments(split))
27
+ this.updateCount(batch, this.keys.buildSplitsWithSegmentCountKey(), diff);
48
28
  };
49
29
  /**
50
30
  * Removes all splits cache related data from localStorage (splits, counters, changeNumber and lastUpdated).
@@ -66,27 +46,64 @@ var SplitsCacheInLocal = /** @class */ (function (_super) {
66
46
  });
67
47
  this.hasSync = false;
68
48
  };
49
+ /**
50
+ * Overrides the default implementation to batch the flag set and counter writes: each flag set and each counter is read from
51
+ * storage once, updated in memory, and written once at the end, instead of once per feature flag.
52
+ * `update` is synchronous, so no other reader can observe the intermediate state.
53
+ */
54
+ SplitsCacheInLocal.prototype.update = function (toAdd, toRemove, changeNumber) {
55
+ var _this = this;
56
+ var batch = newBatch();
57
+ var updated;
58
+ try {
59
+ updated = toAdd.map(function (addedFF) { return _this.addSplitToBatch(addedFF, batch); }).some(function (result) { return result; });
60
+ updated = toRemove.map(function (removedFF) { return _this.removeSplitFromBatch(removedFF.name, batch); }).some(function (result) { return result; }) || updated;
61
+ }
62
+ finally {
63
+ this.flushBatch(batch);
64
+ }
65
+ this.setChangeNumber(changeNumber);
66
+ return updated;
67
+ };
69
68
  SplitsCacheInLocal.prototype.addSplit = function (split) {
69
+ var batch = newBatch();
70
+ try {
71
+ return this.addSplitToBatch(split, batch);
72
+ }
73
+ finally {
74
+ this.flushBatch(batch);
75
+ }
76
+ };
77
+ SplitsCacheInLocal.prototype.removeSplit = function (name) {
78
+ var batch = newBatch();
79
+ try {
80
+ return this.removeSplitFromBatch(name, batch);
81
+ }
82
+ finally {
83
+ this.flushBatch(batch);
84
+ }
85
+ };
86
+ SplitsCacheInLocal.prototype.addSplitToBatch = function (split, batch) {
70
87
  var name = split.name;
71
88
  var splitKey = this.keys.buildSplitKey(name);
72
89
  var splitFromStorage = this.storage.getItem(splitKey);
73
90
  var previousSplit = splitFromStorage ? JSON.parse(splitFromStorage) : null;
74
91
  if (previousSplit) {
75
- this._decrementCounts(previousSplit);
76
- this.removeFromFlagSets(previousSplit.name, previousSplit.sets);
92
+ this.updateCounts(previousSplit, batch, -1);
93
+ this.removeFromFlagSets(previousSplit.name, previousSplit.sets, batch);
77
94
  }
78
95
  this.storage.setItem(splitKey, JSON.stringify(split));
79
- this._incrementCounts(split);
80
- this.addToFlagSets(split);
96
+ this.updateCounts(split, batch, 1);
97
+ this.addToFlagSets(split, batch);
81
98
  return true;
82
99
  };
83
- SplitsCacheInLocal.prototype.removeSplit = function (name) {
100
+ SplitsCacheInLocal.prototype.removeSplitFromBatch = function (name, batch) {
84
101
  var split = this.getSplit(name);
85
102
  if (!split)
86
103
  return false;
87
104
  this.storage.removeItem(this.keys.buildSplitKey(name));
88
- this._decrementCounts(split);
89
- this.removeFromFlagSets(split.name, split.sets);
105
+ this.updateCounts(split, batch, -1);
106
+ this.removeFromFlagSets(split.name, split.sets, batch);
90
107
  return true;
91
108
  };
92
109
  SplitsCacheInLocal.prototype.getSplit = function (name) {
@@ -143,48 +160,83 @@ var SplitsCacheInLocal = /** @class */ (function (_super) {
143
160
  };
144
161
  SplitsCacheInLocal.prototype.getNamesByFlagSets = function (flagSets) {
145
162
  var _this = this;
163
+ if (this.disableFlagSetCache) {
164
+ this.log.error(ERROR_FLAGSET_CACHE_DISABLED);
165
+ return flagSets.map(function () { return new Set(); });
166
+ }
146
167
  return flagSets.map(function (flagSet) {
147
168
  var flagSetKey = _this.keys.buildFlagSetKey(flagSet);
148
169
  var flagSetFromStorage = _this.storage.getItem(flagSetKey);
149
170
  return new Set(flagSetFromStorage ? JSON.parse(flagSetFromStorage) : []);
150
171
  });
151
172
  };
152
- SplitsCacheInLocal.prototype.addToFlagSets = function (featureFlag) {
173
+ SplitsCacheInLocal.prototype.addToFlagSets = function (featureFlag, batch) {
153
174
  var _this = this;
154
- if (!featureFlag.sets)
175
+ if (this.disableFlagSetCache || !featureFlag.sets)
155
176
  return;
156
177
  featureFlag.sets.forEach(function (featureFlagSet) {
157
178
  if (_this.flagSetsFilter.length > 0 && !_this.flagSetsFilter.some(function (filterFlagSet) { return filterFlagSet === featureFlagSet; }))
158
179
  return;
159
180
  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)));
181
+ _this.getFlagSetFromBatch(flagSetKey, batch).add(featureFlag.name);
166
182
  });
167
183
  };
168
- SplitsCacheInLocal.prototype.removeFromFlagSets = function (featureFlagName, flagSets) {
184
+ SplitsCacheInLocal.prototype.removeFromFlagSets = function (featureFlagName, flagSets, batch) {
169
185
  var _this = this;
170
- if (!flagSets)
186
+ if (this.disableFlagSetCache || !flagSets)
171
187
  return;
172
188
  flagSets.forEach(function (flagSet) {
173
- _this.removeNames(flagSet, featureFlagName);
189
+ _this.removeNames(flagSet, featureFlagName, batch);
174
190
  });
175
191
  };
176
- SplitsCacheInLocal.prototype.removeNames = function (flagSetName, featureFlagName) {
192
+ SplitsCacheInLocal.prototype.removeNames = function (flagSetName, featureFlagName, batch) {
177
193
  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);
194
+ // nothing to remove from a flag set that is neither in storage nor in the batch
195
+ if (!batch.flagSets.has(flagSetKey) && !this.storage.getItem(flagSetKey))
185
196
  return;
197
+ this.getFlagSetFromBatch(flagSetKey, batch).delete(featureFlagName);
198
+ };
199
+ /**
200
+ * Returns the flag names of the given flag set, reading from storage only the first time
201
+ */
202
+ SplitsCacheInLocal.prototype.getFlagSetFromBatch = function (flagSetKey, batch) {
203
+ var flagSetCache = batch.flagSets.get(flagSetKey);
204
+ if (!flagSetCache) {
205
+ var flagSetFromStorage = this.storage.getItem(flagSetKey);
206
+ flagSetCache = new Set(flagSetFromStorage ? JSON.parse(flagSetFromStorage) : []);
207
+ batch.flagSets.set(flagSetKey, flagSetCache);
186
208
  }
187
- this.storage.setItem(flagSetKey, JSON.stringify(setToArray(flagSetCache)));
209
+ return flagSetCache;
210
+ };
211
+ SplitsCacheInLocal.prototype.flushBatch = function (batch) {
212
+ var _this = this;
213
+ // flush counts
214
+ batch.counts.forEach(function (diff, key) {
215
+ try {
216
+ if (diff === 0)
217
+ return;
218
+ var count = toNumber(_this.storage.getItem(key)) + diff;
219
+ if (count > 0)
220
+ _this.storage.setItem(key, count + '');
221
+ else
222
+ _this.storage.removeItem(key);
223
+ }
224
+ catch (e) {
225
+ _this.log.error(LOG_PREFIX + e);
226
+ }
227
+ });
228
+ // flush flag sets
229
+ batch.flagSets.forEach(function (flagSetCache, flagSetKey) {
230
+ try {
231
+ if (flagSetCache.size === 0)
232
+ _this.storage.removeItem(flagSetKey);
233
+ else
234
+ _this.storage.setItem(flagSetKey, JSON.stringify(setToArray(flagSetCache)));
235
+ }
236
+ catch (e) {
237
+ _this.log.error(LOG_PREFIX + e);
238
+ }
239
+ });
188
240
  };
189
241
  return SplitsCacheInLocal;
190
242
  }(AbstractSplitsCacheSync));
@@ -15,6 +15,8 @@ import { UniqueKeysCacheInMemoryCS } from '../inMemory/UniqueKeysCacheInMemoryCS
15
15
  import { getMatching } from '../../utils/key';
16
16
  import { validateCache } from './validateCache';
17
17
  import { storageAdapter } from './storageAdapter';
18
+ import { isBoolean } from '../../utils/lang';
19
+ import { ERROR_INVALID_CONFIG_PARAM } from '../../logger/constants';
18
20
  function validateStorage(log, prefix, wrapper) {
19
21
  if (wrapper) {
20
22
  if (isValidStorageWrapper(wrapper)) {
@@ -41,7 +43,10 @@ export function InLocalStorage(options) {
41
43
  return InMemoryStorageCSFactory(params);
42
44
  var matchingKey = getMatching(settings.core.key);
43
45
  var keys = new KeyBuilderCS(prefix, matchingKey);
44
- var splits = new SplitsCacheInLocal(settings, keys, storage);
46
+ if (options.disableFlagSetCache !== undefined && !isBoolean(options.disableFlagSetCache)) {
47
+ log.error(ERROR_INVALID_CONFIG_PARAM, ['disableFlagSetCache', 'true, false', 'false']);
48
+ }
49
+ var splits = new SplitsCacheInLocal(settings, keys, storage, options.disableFlagSetCache === true);
45
50
  var rbSegments = new RBSegmentsCacheInLocal(settings, keys, storage);
46
51
  var segments = new MySegmentsCacheInLocal(log, keys, storage);
47
52
  var largeSegments = new MySegmentsCacheInLocal(log, myLargeSegmentsKeyBuilder(prefix, matchingKey), storage);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@splitsoftware/splitio-commons",
3
- "version": "2.13.0",
3
+ "version": "2.14.0-rc.0",
4
4
  "description": "Split JavaScript SDK common components",
5
5
  "main": "cjs/index.js",
6
6
  "module": "esm/index.js",
@@ -115,6 +115,7 @@ export const ERROR_MIN_CONFIG_PARAM = 326;
115
115
  export const ERROR_TOO_MANY_SETS = 327;
116
116
  export const ERROR_SETS_FILTER_EXCLUSIVE = 328;
117
117
  export const ENGINE_MATCHER_ERROR = 329;
118
+ export const ERROR_FLAGSET_CACHE_DISABLED = 330;
118
119
 
119
120
  // Log prefixes (a.k.a. tags or categories)
120
121
  export const LOG_PREFIX_SETTINGS = 'settings';
@@ -30,6 +30,7 @@ export const codesError: [number, string][] = [
30
30
  [c.ERROR_EMPTY, '%s: you passed an empty %s. It must be a non-empty string.'],
31
31
  [c.ERROR_EMPTY_ARRAY, '%s: %s must be a non-empty array.'],
32
32
  [c.ERROR_NOT_BOOLEAN, '%s: provided param must be a boolean value.'],
33
+ [c.ERROR_FLAGSET_CACHE_DISABLED, 'The flag set cache is disabled via the `disableFlagSetCache` setting, so `getTreatments(WithConfig)ByFlagSet(s)` methods always return empty objects. Remove the setting to use them.'],
33
34
  // initialization / settings validation
34
35
  [c.ERROR_INVALID_CONFIG_PARAM, c.LOG_PREFIX_SETTINGS + ': you passed an invalid "%s" config param. It should be one of the following values: %s. Defaulting to "%s".'],
35
36
  [c.ERROR_STORAGE_INVALID, c.LOG_PREFIX_SETTINGS+': the provided storage is invalid.%s Falling back into default MEMORY storage'],
@@ -4,61 +4,50 @@ import { isFiniteNumber, toNumber, isNaNNumber } from '../../utils/lang';
4
4
  import { KeyBuilderCS } from '../KeyBuilderCS';
5
5
  import { ILogger } from '../../logger/types';
6
6
  import { LOG_PREFIX } from './constants';
7
+ import { ERROR_FLAGSET_CACHE_DISABLED } from '../../logger/constants';
7
8
  import { ISettings } from '../../types';
8
9
  import { setToArray } from '../../utils/lang/sets';
9
10
  import { StorageAdapter } from '../types';
10
11
 
12
+ /**
13
+ * 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.
14
+ */
15
+ interface UpdateBatch {
16
+ flagSets: Map<string, Set<string>>;
17
+ counts: Map<string, number>;
18
+ }
19
+
20
+ function newBatch(): UpdateBatch {
21
+ return { flagSets: new Map(), counts: new Map() };
22
+ }
23
+
11
24
  export class SplitsCacheInLocal extends AbstractSplitsCacheSync {
12
25
 
13
26
  private readonly keys: KeyBuilderCS;
14
27
  private readonly log: ILogger;
15
28
  private readonly flagSetsFilter: string[];
29
+ private readonly disableFlagSetCache?: boolean;
16
30
  private hasSync?: boolean;
17
31
  private readonly storage: StorageAdapter;
18
32
 
19
- constructor(settings: ISettings, keys: KeyBuilderCS, storage: StorageAdapter) {
33
+ constructor(settings: ISettings, keys: KeyBuilderCS, storage: StorageAdapter, disableFlagSetCache?: boolean) {
20
34
  super();
21
35
  this.keys = keys;
22
36
  this.log = settings.log;
23
37
  this.flagSetsFilter = settings.sync.__splitFiltersValidation.groupedFilters.bySet;
24
38
  this.storage = storage;
39
+ this.disableFlagSetCache = disableFlagSetCache;
25
40
  }
26
41
 
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);
42
+ private updateCount(batch: UpdateBatch, key: string, diff: number) {
43
+ batch.counts.set(key, (batch.counts.get(key) || 0) + diff);
31
44
  }
32
45
 
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
- }
46
+ private updateCounts(split: ISplit, batch: UpdateBatch, diff: number) {
47
+ this.updateCount(batch, this.keys.buildTrafficTypeKey(split.trafficTypeName), diff);
48
+ if (usesSegments(split)) this.updateCount(batch, this.keys.buildSplitsWithSegmentCountKey(), diff);
45
49
  }
46
50
 
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
- }
59
- }
60
-
61
-
62
51
  /**
63
52
  * Removes all splits cache related data from localStorage (splits, counters, changeNumber and lastUpdated).
64
53
  * We cannot simply call `localStorage.clear()` since that implies removing user items from the storage.
@@ -79,33 +68,69 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync {
79
68
  this.hasSync = false;
80
69
  }
81
70
 
71
+ /**
72
+ * Overrides the default implementation to batch the flag set and counter writes: each flag set and each counter is read from
73
+ * storage once, updated in memory, and written once at the end, instead of once per feature flag.
74
+ * `update` is synchronous, so no other reader can observe the intermediate state.
75
+ */
76
+ update(toAdd: ISplit[], toRemove: ISplit[], changeNumber: number): boolean {
77
+ const batch = newBatch();
78
+ let updated;
79
+ try {
80
+ updated = toAdd.map(addedFF => this.addSplitToBatch(addedFF, batch)).some(result => result);
81
+ updated = toRemove.map(removedFF => this.removeSplitFromBatch(removedFF.name, batch)).some(result => result) || updated;
82
+ } finally {
83
+ this.flushBatch(batch);
84
+ }
85
+ this.setChangeNumber(changeNumber);
86
+ return updated;
87
+ }
88
+
82
89
  addSplit(split: ISplit) {
90
+ const batch = newBatch();
91
+ try {
92
+ return this.addSplitToBatch(split, batch);
93
+ } finally {
94
+ this.flushBatch(batch);
95
+ }
96
+ }
97
+
98
+ removeSplit(name: string): boolean {
99
+ const batch = newBatch();
100
+ try {
101
+ return this.removeSplitFromBatch(name, batch);
102
+ } finally {
103
+ this.flushBatch(batch);
104
+ }
105
+ }
106
+
107
+ private addSplitToBatch(split: ISplit, batch: UpdateBatch) {
83
108
  const name = split.name;
84
109
  const splitKey = this.keys.buildSplitKey(name);
85
110
  const splitFromStorage = this.storage.getItem(splitKey);
86
111
  const previousSplit = splitFromStorage ? JSON.parse(splitFromStorage) : null;
87
112
 
88
113
  if (previousSplit) {
89
- this._decrementCounts(previousSplit);
90
- this.removeFromFlagSets(previousSplit.name, previousSplit.sets);
114
+ this.updateCounts(previousSplit, batch, -1);
115
+ this.removeFromFlagSets(previousSplit.name, previousSplit.sets, batch);
91
116
  }
92
117
 
93
118
  this.storage.setItem(splitKey, JSON.stringify(split));
94
119
 
95
- this._incrementCounts(split);
96
- this.addToFlagSets(split);
120
+ this.updateCounts(split, batch, 1);
121
+ this.addToFlagSets(split, batch);
97
122
 
98
123
  return true;
99
124
  }
100
125
 
101
- removeSplit(name: string): boolean {
126
+ private removeSplitFromBatch(name: string, batch: UpdateBatch): boolean {
102
127
  const split = this.getSplit(name);
103
128
  if (!split) return false;
104
129
 
105
130
  this.storage.removeItem(this.keys.buildSplitKey(name));
106
131
 
107
- this._decrementCounts(split);
108
- this.removeFromFlagSets(split.name, split.sets);
132
+ this.updateCounts(split, batch, -1);
133
+ this.removeFromFlagSets(split.name, split.sets, batch);
109
134
 
110
135
  return true;
111
136
  }
@@ -176,6 +201,11 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync {
176
201
  }
177
202
 
178
203
  getNamesByFlagSets(flagSets: string[]): Set<string>[] {
204
+ if (this.disableFlagSetCache) {
205
+ this.log.error(ERROR_FLAGSET_CACHE_DISABLED);
206
+ return flagSets.map(() => new Set<string>());
207
+ }
208
+
179
209
  return flagSets.map(flagSet => {
180
210
  const flagSetKey = this.keys.buildFlagSetKey(flagSet);
181
211
  const flagSetFromStorage = this.storage.getItem(flagSetKey);
@@ -184,8 +214,8 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync {
184
214
  });
185
215
  }
186
216
 
187
- private addToFlagSets(featureFlag: ISplit) {
188
- if (!featureFlag.sets) return;
217
+ private addToFlagSets(featureFlag: ISplit, batch: UpdateBatch) {
218
+ if (this.disableFlagSetCache || !featureFlag.sets) return;
189
219
 
190
220
  featureFlag.sets.forEach(featureFlagSet => {
191
221
 
@@ -193,42 +223,62 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync {
193
223
 
194
224
  const flagSetKey = this.keys.buildFlagSetKey(featureFlagSet);
195
225
 
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)));
226
+ this.getFlagSetFromBatch(flagSetKey, batch).add(featureFlag.name);
205
227
  });
206
228
  }
207
229
 
208
- private removeFromFlagSets(featureFlagName: string, flagSets?: string[] | null) {
209
- if (!flagSets) return;
230
+ private removeFromFlagSets(featureFlagName: string, flagSets: string[] | null | undefined, batch: UpdateBatch) {
231
+ if (this.disableFlagSetCache || !flagSets) return;
210
232
 
211
233
  flagSets.forEach(flagSet => {
212
- this.removeNames(flagSet, featureFlagName);
234
+ this.removeNames(flagSet, featureFlagName, batch);
213
235
  });
214
236
  }
215
237
 
216
- private removeNames(flagSetName: string, featureFlagName: string) {
238
+ private removeNames(flagSetName: string, featureFlagName: string, batch: UpdateBatch) {
217
239
  const flagSetKey = this.keys.buildFlagSetKey(flagSetName);
218
240
 
219
- const flagSetFromStorage = this.storage.getItem(flagSetKey);
241
+ // nothing to remove from a flag set that is neither in storage nor in the batch
242
+ if (!batch.flagSets.has(flagSetKey) && !this.storage.getItem(flagSetKey)) return;
220
243
 
221
- if (!flagSetFromStorage) return;
222
-
223
- const flagSetCache = new Set(JSON.parse(flagSetFromStorage));
224
- flagSetCache.delete(featureFlagName);
244
+ this.getFlagSetFromBatch(flagSetKey, batch).delete(featureFlagName);
245
+ }
225
246
 
226
- if (flagSetCache.size === 0) {
227
- this.storage.removeItem(flagSetKey);
228
- return;
247
+ /**
248
+ * Returns the flag names of the given flag set, reading from storage only the first time
249
+ */
250
+ private getFlagSetFromBatch(flagSetKey: string, batch: UpdateBatch): Set<string> {
251
+ let flagSetCache = batch.flagSets.get(flagSetKey);
252
+ if (!flagSetCache) {
253
+ const flagSetFromStorage = this.storage.getItem(flagSetKey);
254
+ flagSetCache = new Set(flagSetFromStorage ? JSON.parse(flagSetFromStorage) : []);
255
+ batch.flagSets.set(flagSetKey, flagSetCache);
229
256
  }
257
+ return flagSetCache;
258
+ }
259
+
260
+ private flushBatch(batch: UpdateBatch) {
261
+ // flush counts
262
+ batch.counts.forEach((diff, key) => {
263
+ try {
264
+ if (diff === 0) return;
265
+ const count = toNumber(this.storage.getItem(key)) + diff;
266
+ if (count > 0) this.storage.setItem(key, count + '');
267
+ else this.storage.removeItem(key);
268
+ } catch (e) {
269
+ this.log.error(LOG_PREFIX + e);
270
+ }
271
+ });
230
272
 
231
- this.storage.setItem(flagSetKey, JSON.stringify(setToArray(flagSetCache)));
273
+ // flush flag sets
274
+ batch.flagSets.forEach((flagSetCache, flagSetKey) => {
275
+ try {
276
+ if (flagSetCache.size === 0) this.storage.removeItem(flagSetKey);
277
+ else this.storage.setItem(flagSetKey, JSON.stringify(setToArray(flagSetCache)));
278
+ } catch (e) {
279
+ this.log.error(LOG_PREFIX + e);
280
+ }
281
+ });
232
282
  }
233
283
 
234
284
  }
@@ -18,6 +18,8 @@ import { validateCache } from './validateCache';
18
18
  import { ILogger } from '../../logger/types';
19
19
  import SplitIO from '../../../types/splitio';
20
20
  import { storageAdapter } from './storageAdapter';
21
+ import { isBoolean } from '../../utils/lang';
22
+ import { ERROR_INVALID_CONFIG_PARAM } from '../../logger/constants';
21
23
 
22
24
  function validateStorage(log: ILogger, prefix: string, wrapper?: SplitIO.StorageWrapper): StorageAdapter | undefined {
23
25
  if (wrapper) {
@@ -50,7 +52,11 @@ export function InLocalStorage(options: SplitIO.InLocalStorageOptions = {}): ISt
50
52
  const matchingKey = getMatching(settings.core.key);
51
53
  const keys = new KeyBuilderCS(prefix, matchingKey);
52
54
 
53
- const splits = new SplitsCacheInLocal(settings, keys, storage);
55
+ if (options.disableFlagSetCache !== undefined && !isBoolean(options.disableFlagSetCache)) {
56
+ log.error(ERROR_INVALID_CONFIG_PARAM, ['disableFlagSetCache', 'true, false', 'false']);
57
+ }
58
+
59
+ const splits = new SplitsCacheInLocal(settings, keys, storage, options.disableFlagSetCache === true);
54
60
  const rbSegments = new RBSegmentsCacheInLocal(settings, keys, storage);
55
61
  const segments = new MySegmentsCacheInLocal(log, keys, storage);
56
62
  const largeSegments = new MySegmentsCacheInLocal(log, myLargeSegmentsKeyBuilder(prefix, matchingKey), storage);
@@ -1162,6 +1162,15 @@ declare namespace SplitIO {
1162
1162
  * @defaultValue `window.localStorage`
1163
1163
  */
1164
1164
  wrapper?: StorageWrapper;
1165
+ /**
1166
+ * If set to `true`, the SDK does not build the flag set index when it stores feature flag definitions, which speeds up the SDK initialization (SDK_READY event) and updates (SDK_UPDATE event).
1167
+ *
1168
+ * NOTE: when enabled, the `getTreatments(WithConfig)ByFlagSet(s)` methods return empty results.
1169
+ * If you disable it afterwards, clear the cached data (e.g., with `storage.clearOnInit`) so the index is rebuilt.
1170
+ *
1171
+ * @defaultValue `false`
1172
+ */
1173
+ disableFlagSetCache?: boolean;
1165
1174
  }
1166
1175
  /**
1167
1176
  * Storage for asynchronous (consumer) SDK.
@@ -1542,6 +1551,15 @@ declare namespace SplitIO {
1542
1551
  * @defaultValue `window.localStorage`
1543
1552
  */
1544
1553
  wrapper?: StorageWrapper;
1554
+ /**
1555
+ * Optional settings for the 'LOCALSTORAGE' storage type. If set to `true`, the SDK does not build the flag set index when it stores feature flag definitions, which speeds up the SDK initialization (SDK_READY event) and updates (SDK_UPDATE event).
1556
+ *
1557
+ * NOTE: when enabled, the `getTreatments(WithConfig)ByFlagSet(s)` methods return empty results.
1558
+ * If you disable it afterwards, clear the cached data (e.g., with `storage.clearOnInit`) so the index is rebuilt.
1559
+ *
1560
+ * @defaultValue `false`
1561
+ */
1562
+ disableFlagSetCache?: boolean;
1545
1563
  };
1546
1564
  }
1547
1565
  /**