@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 +6 -0
- package/cjs/logger/constants.js +2 -1
- package/cjs/logger/messages/error.js +1 -0
- package/cjs/storages/inLocalStorage/SplitsCacheInLocal.js +111 -59
- package/cjs/storages/inLocalStorage/index.js +6 -1
- package/esm/logger/constants.js +1 -0
- package/esm/logger/messages/error.js +1 -0
- package/esm/storages/inLocalStorage/SplitsCacheInLocal.js +111 -59
- package/esm/storages/inLocalStorage/index.js +6 -1
- package/package.json +1 -1
- package/src/logger/constants.ts +1 -0
- package/src/logger/messages/error.ts +1 -0
- package/src/storages/inLocalStorage/SplitsCacheInLocal.ts +113 -63
- package/src/storages/inLocalStorage/index.ts +7 -1
- package/types/splitio.d.ts +18 -0
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.
|
package/cjs/logger/constants.js
CHANGED
|
@@ -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.
|
|
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
|
-
}
|
|
24
|
+
SplitsCacheInLocal.prototype.updateCount = function (batch, key, diff) {
|
|
25
|
+
batch.counts.set(key, (batch.counts.get(key) || 0) + diff);
|
|
38
26
|
};
|
|
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
|
-
}
|
|
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.
|
|
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.
|
|
83
|
-
this.addToFlagSets(split);
|
|
99
|
+
this.updateCounts(split, batch, 1);
|
|
100
|
+
this.addToFlagSets(split, batch);
|
|
84
101
|
return true;
|
|
85
102
|
};
|
|
86
|
-
SplitsCacheInLocal.prototype.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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);
|
|
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
|
-
|
|
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
|
-
|
|
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);
|
package/esm/logger/constants.js
CHANGED
|
@@ -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.
|
|
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
|
-
}
|
|
21
|
+
SplitsCacheInLocal.prototype.updateCount = function (batch, key, diff) {
|
|
22
|
+
batch.counts.set(key, (batch.counts.get(key) || 0) + diff);
|
|
35
23
|
};
|
|
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
|
-
}
|
|
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.
|
|
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.
|
|
80
|
-
this.addToFlagSets(split);
|
|
96
|
+
this.updateCounts(split, batch, 1);
|
|
97
|
+
this.addToFlagSets(split, batch);
|
|
81
98
|
return true;
|
|
82
99
|
};
|
|
83
|
-
SplitsCacheInLocal.prototype.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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);
|
|
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
|
-
|
|
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
|
-
|
|
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
package/src/logger/constants.ts
CHANGED
|
@@ -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
|
|
28
|
-
|
|
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
|
|
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
|
-
}
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
222
|
-
|
|
223
|
-
const flagSetCache = new Set(JSON.parse(flagSetFromStorage));
|
|
224
|
-
flagSetCache.delete(featureFlagName);
|
|
244
|
+
this.getFlagSetFromBatch(flagSetKey, batch).delete(featureFlagName);
|
|
245
|
+
}
|
|
225
246
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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);
|
package/types/splitio.d.ts
CHANGED
|
@@ -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
|
/**
|