@splitsoftware/splitio-commons 2.1.0-rc.1 → 2.1.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 +4 -5
- package/LICENSE +1 -1
- package/cjs/evaluator/index.js +2 -0
- package/cjs/listeners/browser.js +4 -6
- package/cjs/readiness/readinessManager.js +0 -6
- package/cjs/sdkClient/client.js +14 -11
- package/cjs/sdkClient/sdkClient.js +1 -1
- package/cjs/sdkFactory/index.js +9 -14
- package/cjs/sdkManager/index.js +2 -1
- package/cjs/storages/AbstractSplitsCacheAsync.js +7 -0
- package/cjs/storages/AbstractSplitsCacheSync.js +7 -0
- package/cjs/storages/KeyBuilderCS.js +0 -3
- package/cjs/storages/dataLoader.js +2 -3
- package/cjs/storages/inLocalStorage/SplitsCacheInLocal.js +57 -1
- package/cjs/storages/inLocalStorage/index.js +7 -8
- package/cjs/storages/inMemory/InMemoryStorage.js +5 -7
- package/cjs/storages/inMemory/InMemoryStorageCS.js +6 -7
- package/cjs/storages/inRedis/constants.js +1 -1
- package/cjs/storages/inRedis/index.js +9 -13
- package/cjs/storages/pluggable/index.js +16 -21
- package/cjs/sync/offline/syncTasks/fromObjectSyncTask.js +2 -3
- package/cjs/sync/polling/updaters/splitChangesUpdater.js +10 -1
- package/cjs/sync/submitters/impressionCountsSubmitter.js +2 -4
- package/cjs/sync/submitters/submitterManager.js +3 -6
- package/cjs/sync/syncManagerOnline.js +3 -8
- package/cjs/trackers/impressionsTracker.js +17 -18
- package/cjs/trackers/strategy/strategyDebug.js +4 -11
- package/cjs/trackers/strategy/strategyNone.js +11 -16
- package/cjs/trackers/strategy/strategyOptimized.js +11 -21
- package/cjs/trackers/uniqueKeysTracker.js +1 -1
- package/cjs/utils/constants/browser.js +5 -0
- package/cjs/utils/settingsValidation/storage/storageCS.js +1 -1
- package/esm/evaluator/index.js +2 -0
- package/esm/listeners/browser.js +1 -3
- package/esm/readiness/readinessManager.js +0 -6
- package/esm/sdkClient/client.js +14 -11
- package/esm/sdkClient/sdkClient.js +1 -1
- package/esm/sdkFactory/index.js +10 -15
- package/esm/sdkManager/index.js +2 -1
- package/esm/storages/AbstractSplitsCacheAsync.js +7 -0
- package/esm/storages/AbstractSplitsCacheSync.js +7 -0
- package/esm/storages/KeyBuilderCS.js +0 -3
- package/esm/storages/dataLoader.js +1 -2
- package/esm/storages/inLocalStorage/SplitsCacheInLocal.js +57 -1
- package/esm/storages/inLocalStorage/index.js +8 -9
- package/esm/storages/inMemory/InMemoryStorage.js +6 -8
- package/esm/storages/inMemory/InMemoryStorageCS.js +7 -8
- package/esm/storages/inRedis/constants.js +1 -1
- package/esm/storages/inRedis/index.js +10 -14
- package/esm/storages/pluggable/index.js +17 -22
- package/esm/sync/offline/syncTasks/fromObjectSyncTask.js +2 -3
- package/esm/sync/polling/updaters/splitChangesUpdater.js +11 -2
- package/esm/sync/submitters/impressionCountsSubmitter.js +2 -4
- package/esm/sync/submitters/submitterManager.js +3 -6
- package/esm/sync/syncManagerOnline.js +3 -8
- package/esm/trackers/impressionsTracker.js +17 -18
- package/esm/trackers/strategy/strategyDebug.js +4 -11
- package/esm/trackers/strategy/strategyNone.js +11 -16
- package/esm/trackers/strategy/strategyOptimized.js +11 -21
- package/esm/trackers/uniqueKeysTracker.js +1 -1
- package/esm/utils/constants/browser.js +2 -0
- package/esm/utils/settingsValidation/storage/storageCS.js +1 -1
- package/package.json +1 -1
- package/src/dtos/types.ts +2 -1
- package/src/evaluator/index.ts +2 -0
- package/src/evaluator/types.ts +1 -1
- package/src/listeners/browser.ts +1 -3
- package/src/readiness/readinessManager.ts +0 -5
- package/src/sdkClient/client.ts +19 -15
- package/src/sdkClient/sdkClient.ts +1 -1
- package/src/sdkFactory/index.ts +11 -16
- package/src/sdkFactory/types.ts +1 -1
- package/src/sdkManager/index.ts +2 -1
- package/src/storages/AbstractSplitsCacheAsync.ts +8 -0
- package/src/storages/AbstractSplitsCacheSync.ts +8 -0
- package/src/storages/KeyBuilderCS.ts +0 -4
- package/src/storages/dataLoader.ts +1 -3
- package/src/storages/inLocalStorage/SplitsCacheInLocal.ts +66 -1
- package/src/storages/inLocalStorage/index.ts +13 -12
- package/src/storages/inMemory/InMemoryStorage.ts +6 -6
- package/src/storages/inMemory/InMemoryStorageCS.ts +7 -6
- package/src/storages/inRedis/constants.ts +1 -1
- package/src/storages/inRedis/index.ts +10 -10
- package/src/storages/pluggable/index.ts +17 -22
- package/src/storages/types.ts +6 -3
- package/src/sync/offline/syncTasks/fromObjectSyncTask.ts +5 -6
- package/src/sync/polling/updaters/splitChangesUpdater.ts +11 -2
- package/src/sync/submitters/impressionCountsSubmitter.ts +2 -4
- package/src/sync/submitters/submitterManager.ts +3 -4
- package/src/sync/submitters/uniqueKeysSubmitter.ts +2 -3
- package/src/sync/syncManagerOnline.ts +3 -9
- package/src/trackers/impressionsTracker.ts +18 -19
- package/src/trackers/strategy/strategyDebug.ts +4 -11
- package/src/trackers/strategy/strategyNone.ts +11 -17
- package/src/trackers/strategy/strategyOptimized.ts +10 -20
- package/src/trackers/types.ts +13 -8
- package/src/trackers/uniqueKeysTracker.ts +1 -1
- package/src/utils/constants/browser.ts +2 -0
- package/src/utils/lang/index.ts +1 -1
- package/src/utils/settingsValidation/storage/storageCS.ts +1 -1
- package/types/splitio.d.ts +5 -25
- package/cjs/storages/inLocalStorage/validateCache.js +0 -79
- package/esm/storages/inLocalStorage/validateCache.js +0 -75
- package/src/storages/inLocalStorage/validateCache.ts +0 -91
|
@@ -2,17 +2,21 @@ 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 { getStorageHash } from '../KeyBuilder';
|
|
5
6
|
import { setToArray } from '../../utils/lang/sets';
|
|
6
7
|
/**
|
|
7
8
|
* ISplitsCacheSync implementation that stores split definitions in browser LocalStorage.
|
|
8
9
|
*/
|
|
9
10
|
var SplitsCacheInLocal = /** @class */ (function (_super) {
|
|
10
11
|
__extends(SplitsCacheInLocal, _super);
|
|
11
|
-
function SplitsCacheInLocal(settings, keys) {
|
|
12
|
+
function SplitsCacheInLocal(settings, keys, expirationTimestamp) {
|
|
12
13
|
var _this = _super.call(this) || this;
|
|
13
14
|
_this.keys = keys;
|
|
14
15
|
_this.log = settings.log;
|
|
16
|
+
_this.storageHash = getStorageHash(settings);
|
|
15
17
|
_this.flagSetsFilter = settings.sync.__splitFiltersValidation.groupedFilters.bySet;
|
|
18
|
+
_this._checkExpiration(expirationTimestamp);
|
|
19
|
+
_this._checkFilterQuery();
|
|
16
20
|
return _this;
|
|
17
21
|
}
|
|
18
22
|
SplitsCacheInLocal.prototype._decrementCount = function (key) {
|
|
@@ -60,6 +64,7 @@ var SplitsCacheInLocal = /** @class */ (function (_super) {
|
|
|
60
64
|
* We cannot simply call `localStorage.clear()` since that implies removing user items from the storage.
|
|
61
65
|
*/
|
|
62
66
|
SplitsCacheInLocal.prototype.clear = function () {
|
|
67
|
+
this.log.info(LOG_PREFIX + 'Flushing Splits data from localStorage');
|
|
63
68
|
// collect item keys
|
|
64
69
|
var len = localStorage.length;
|
|
65
70
|
var accum = [];
|
|
@@ -111,6 +116,18 @@ var SplitsCacheInLocal = /** @class */ (function (_super) {
|
|
|
111
116
|
return item && JSON.parse(item);
|
|
112
117
|
};
|
|
113
118
|
SplitsCacheInLocal.prototype.setChangeNumber = function (changeNumber) {
|
|
119
|
+
// when using a new split query, we must update it at the store
|
|
120
|
+
if (this.updateNewFilter) {
|
|
121
|
+
this.log.info(LOG_PREFIX + 'SDK key, flags filter criteria or flags spec version was modified. Updating cache');
|
|
122
|
+
var storageHashKey = this.keys.buildHashKey();
|
|
123
|
+
try {
|
|
124
|
+
localStorage.setItem(storageHashKey, this.storageHash);
|
|
125
|
+
}
|
|
126
|
+
catch (e) {
|
|
127
|
+
this.log.error(LOG_PREFIX + e);
|
|
128
|
+
}
|
|
129
|
+
this.updateNewFilter = false;
|
|
130
|
+
}
|
|
114
131
|
try {
|
|
115
132
|
localStorage.setItem(this.keys.buildSplitsTillKey(), changeNumber + '');
|
|
116
133
|
// update "last updated" timestamp with current time
|
|
@@ -161,6 +178,45 @@ var SplitsCacheInLocal = /** @class */ (function (_super) {
|
|
|
161
178
|
return true;
|
|
162
179
|
}
|
|
163
180
|
};
|
|
181
|
+
/**
|
|
182
|
+
* Check if the splits information is already stored in browser LocalStorage.
|
|
183
|
+
* In this function we could add more code to check if the data is valid.
|
|
184
|
+
* @override
|
|
185
|
+
*/
|
|
186
|
+
SplitsCacheInLocal.prototype.checkCache = function () {
|
|
187
|
+
return this.getChangeNumber() > -1;
|
|
188
|
+
};
|
|
189
|
+
/**
|
|
190
|
+
* Clean Splits cache if its `lastUpdated` timestamp is older than the given `expirationTimestamp`,
|
|
191
|
+
*
|
|
192
|
+
* @param expirationTimestamp - if the value is not a number, data will not be cleaned
|
|
193
|
+
*/
|
|
194
|
+
SplitsCacheInLocal.prototype._checkExpiration = function (expirationTimestamp) {
|
|
195
|
+
var value = localStorage.getItem(this.keys.buildLastUpdatedKey());
|
|
196
|
+
if (value !== null) {
|
|
197
|
+
value = parseInt(value, 10);
|
|
198
|
+
if (!isNaNNumber(value) && expirationTimestamp && value < expirationTimestamp)
|
|
199
|
+
this.clear();
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
// @TODO eventually remove `_checkFilterQuery`. Cache should be cleared at the storage level, reusing same logic than PluggableStorage
|
|
203
|
+
SplitsCacheInLocal.prototype._checkFilterQuery = function () {
|
|
204
|
+
var storageHashKey = this.keys.buildHashKey();
|
|
205
|
+
var storageHash = localStorage.getItem(storageHashKey);
|
|
206
|
+
if (storageHash !== this.storageHash) {
|
|
207
|
+
try {
|
|
208
|
+
// mark cache to update the new query filter on first successful splits fetch
|
|
209
|
+
this.updateNewFilter = true;
|
|
210
|
+
// if there is cache, clear it
|
|
211
|
+
if (this.checkCache())
|
|
212
|
+
this.clear();
|
|
213
|
+
}
|
|
214
|
+
catch (e) {
|
|
215
|
+
this.log.error(LOG_PREFIX + e);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
// if the filter didn't change, nothing is done
|
|
219
|
+
};
|
|
164
220
|
SplitsCacheInLocal.prototype.getNamesByFlagSets = function (flagSets) {
|
|
165
221
|
var _this = this;
|
|
166
222
|
return flagSets.map(function (flagSet) {
|
|
@@ -6,13 +6,13 @@ import { KeyBuilderCS, myLargeSegmentsKeyBuilder } from '../KeyBuilderCS';
|
|
|
6
6
|
import { isLocalStorageAvailable } from '../../utils/env/isLocalStorageAvailable';
|
|
7
7
|
import { SplitsCacheInLocal } from './SplitsCacheInLocal';
|
|
8
8
|
import { MySegmentsCacheInLocal } from './MySegmentsCacheInLocal';
|
|
9
|
+
import { DEFAULT_CACHE_EXPIRATION_IN_MILLIS } from '../../utils/constants/browser';
|
|
9
10
|
import { InMemoryStorageCSFactory } from '../inMemory/InMemoryStorageCS';
|
|
10
11
|
import { LOG_PREFIX } from './constants';
|
|
11
|
-
import {
|
|
12
|
+
import { STORAGE_LOCALSTORAGE } from '../../utils/constants';
|
|
12
13
|
import { shouldRecordTelemetry, TelemetryCacheInMemory } from '../inMemory/TelemetryCacheInMemory';
|
|
13
14
|
import { UniqueKeysCacheInMemoryCS } from '../inMemory/UniqueKeysCacheInMemoryCS';
|
|
14
15
|
import { getMatching } from '../../utils/key';
|
|
15
|
-
import { validateCache } from './validateCache';
|
|
16
16
|
/**
|
|
17
17
|
* InLocal storage factory for standalone client-side SplitFactory
|
|
18
18
|
*/
|
|
@@ -25,10 +25,11 @@ export function InLocalStorage(options) {
|
|
|
25
25
|
params.settings.log.warn(LOG_PREFIX + 'LocalStorage API is unavailable. Falling back to default MEMORY storage');
|
|
26
26
|
return InMemoryStorageCSFactory(params);
|
|
27
27
|
}
|
|
28
|
-
var settings = params.settings, _a = params.settings, log = _a.log, _b = _a.scheduler, impressionsQueueSize = _b.impressionsQueueSize, eventsQueueSize = _b.eventsQueueSize
|
|
28
|
+
var settings = params.settings, _a = params.settings, log = _a.log, _b = _a.scheduler, impressionsQueueSize = _b.impressionsQueueSize, eventsQueueSize = _b.eventsQueueSize;
|
|
29
29
|
var matchingKey = getMatching(settings.core.key);
|
|
30
30
|
var keys = new KeyBuilderCS(prefix, matchingKey);
|
|
31
|
-
var
|
|
31
|
+
var expirationTimestamp = Date.now() - DEFAULT_CACHE_EXPIRATION_IN_MILLIS;
|
|
32
|
+
var splits = new SplitsCacheInLocal(settings, keys, expirationTimestamp);
|
|
32
33
|
var segments = new MySegmentsCacheInLocal(log, keys);
|
|
33
34
|
var largeSegments = new MySegmentsCacheInLocal(log, myLargeSegmentsKeyBuilder(prefix, matchingKey));
|
|
34
35
|
return {
|
|
@@ -36,13 +37,10 @@ export function InLocalStorage(options) {
|
|
|
36
37
|
segments: segments,
|
|
37
38
|
largeSegments: largeSegments,
|
|
38
39
|
impressions: new ImpressionsCacheInMemory(impressionsQueueSize),
|
|
39
|
-
impressionCounts:
|
|
40
|
+
impressionCounts: new ImpressionCountsCacheInMemory(),
|
|
40
41
|
events: new EventsCacheInMemory(eventsQueueSize),
|
|
41
42
|
telemetry: shouldRecordTelemetry(params) ? new TelemetryCacheInMemory(splits, segments) : undefined,
|
|
42
|
-
uniqueKeys:
|
|
43
|
-
validateCache: function () {
|
|
44
|
-
return validateCache(options, settings, keys, splits, segments, largeSegments);
|
|
45
|
-
},
|
|
43
|
+
uniqueKeys: new UniqueKeysCacheInMemoryCS(),
|
|
46
44
|
destroy: function () { },
|
|
47
45
|
// When using shared instantiation with MEMORY we reuse everything but segments (they are customer per key).
|
|
48
46
|
shared: function (matchingKey) {
|
|
@@ -54,6 +52,7 @@ export function InLocalStorage(options) {
|
|
|
54
52
|
impressionCounts: this.impressionCounts,
|
|
55
53
|
events: this.events,
|
|
56
54
|
telemetry: this.telemetry,
|
|
55
|
+
uniqueKeys: this.uniqueKeys,
|
|
57
56
|
destroy: function () { }
|
|
58
57
|
};
|
|
59
58
|
},
|
|
@@ -3,7 +3,7 @@ import { SegmentsCacheInMemory } from './SegmentsCacheInMemory';
|
|
|
3
3
|
import { ImpressionsCacheInMemory } from './ImpressionsCacheInMemory';
|
|
4
4
|
import { EventsCacheInMemory } from './EventsCacheInMemory';
|
|
5
5
|
import { ImpressionCountsCacheInMemory } from './ImpressionCountsCacheInMemory';
|
|
6
|
-
import {
|
|
6
|
+
import { LOCALHOST_MODE, STORAGE_MEMORY } from '../../utils/constants';
|
|
7
7
|
import { shouldRecordTelemetry, TelemetryCacheInMemory } from './TelemetryCacheInMemory';
|
|
8
8
|
import { UniqueKeysCacheInMemory } from './UniqueKeysCacheInMemory';
|
|
9
9
|
/**
|
|
@@ -12,17 +12,17 @@ import { UniqueKeysCacheInMemory } from './UniqueKeysCacheInMemory';
|
|
|
12
12
|
* @param params - parameters required by EventsCacheSync
|
|
13
13
|
*/
|
|
14
14
|
export function InMemoryStorageFactory(params) {
|
|
15
|
-
var _a = params.settings, _b = _a.scheduler, impressionsQueueSize = _b.impressionsQueueSize, eventsQueueSize = _b.eventsQueueSize,
|
|
15
|
+
var _a = params.settings, _b = _a.scheduler, impressionsQueueSize = _b.impressionsQueueSize, eventsQueueSize = _b.eventsQueueSize, __splitFiltersValidation = _a.sync.__splitFiltersValidation;
|
|
16
16
|
var splits = new SplitsCacheInMemory(__splitFiltersValidation);
|
|
17
17
|
var segments = new SegmentsCacheInMemory();
|
|
18
18
|
var storage = {
|
|
19
19
|
splits: splits,
|
|
20
20
|
segments: segments,
|
|
21
21
|
impressions: new ImpressionsCacheInMemory(impressionsQueueSize),
|
|
22
|
-
impressionCounts:
|
|
22
|
+
impressionCounts: new ImpressionCountsCacheInMemory(),
|
|
23
23
|
events: new EventsCacheInMemory(eventsQueueSize),
|
|
24
24
|
telemetry: shouldRecordTelemetry(params) ? new TelemetryCacheInMemory(splits, segments) : undefined,
|
|
25
|
-
uniqueKeys:
|
|
25
|
+
uniqueKeys: new UniqueKeysCacheInMemory(),
|
|
26
26
|
destroy: function () { }
|
|
27
27
|
};
|
|
28
28
|
// @TODO revisit storage logic in localhost mode
|
|
@@ -31,10 +31,8 @@ export function InMemoryStorageFactory(params) {
|
|
|
31
31
|
var noopTrack = function () { return true; };
|
|
32
32
|
storage.impressions.track = noopTrack;
|
|
33
33
|
storage.events.track = noopTrack;
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
if (storage.uniqueKeys)
|
|
37
|
-
storage.uniqueKeys.track = noopTrack;
|
|
34
|
+
storage.impressionCounts.track = noopTrack;
|
|
35
|
+
storage.uniqueKeys.track = noopTrack;
|
|
38
36
|
}
|
|
39
37
|
return storage;
|
|
40
38
|
}
|
|
@@ -3,7 +3,7 @@ import { MySegmentsCacheInMemory } from './MySegmentsCacheInMemory';
|
|
|
3
3
|
import { ImpressionsCacheInMemory } from './ImpressionsCacheInMemory';
|
|
4
4
|
import { EventsCacheInMemory } from './EventsCacheInMemory';
|
|
5
5
|
import { ImpressionCountsCacheInMemory } from './ImpressionCountsCacheInMemory';
|
|
6
|
-
import {
|
|
6
|
+
import { LOCALHOST_MODE, STORAGE_MEMORY } from '../../utils/constants';
|
|
7
7
|
import { shouldRecordTelemetry, TelemetryCacheInMemory } from './TelemetryCacheInMemory';
|
|
8
8
|
import { UniqueKeysCacheInMemoryCS } from './UniqueKeysCacheInMemoryCS';
|
|
9
9
|
/**
|
|
@@ -12,7 +12,7 @@ import { UniqueKeysCacheInMemoryCS } from './UniqueKeysCacheInMemoryCS';
|
|
|
12
12
|
* @param params - parameters required by EventsCacheSync
|
|
13
13
|
*/
|
|
14
14
|
export function InMemoryStorageCSFactory(params) {
|
|
15
|
-
var _a = params.settings, _b = _a.scheduler, impressionsQueueSize = _b.impressionsQueueSize, eventsQueueSize = _b.eventsQueueSize,
|
|
15
|
+
var _a = params.settings, _b = _a.scheduler, impressionsQueueSize = _b.impressionsQueueSize, eventsQueueSize = _b.eventsQueueSize, __splitFiltersValidation = _a.sync.__splitFiltersValidation;
|
|
16
16
|
var splits = new SplitsCacheInMemory(__splitFiltersValidation);
|
|
17
17
|
var segments = new MySegmentsCacheInMemory();
|
|
18
18
|
var largeSegments = new MySegmentsCacheInMemory();
|
|
@@ -21,10 +21,10 @@ export function InMemoryStorageCSFactory(params) {
|
|
|
21
21
|
segments: segments,
|
|
22
22
|
largeSegments: largeSegments,
|
|
23
23
|
impressions: new ImpressionsCacheInMemory(impressionsQueueSize),
|
|
24
|
-
impressionCounts:
|
|
24
|
+
impressionCounts: new ImpressionCountsCacheInMemory(),
|
|
25
25
|
events: new EventsCacheInMemory(eventsQueueSize),
|
|
26
26
|
telemetry: shouldRecordTelemetry(params) ? new TelemetryCacheInMemory(splits, segments) : undefined,
|
|
27
|
-
uniqueKeys:
|
|
27
|
+
uniqueKeys: new UniqueKeysCacheInMemoryCS(),
|
|
28
28
|
destroy: function () { },
|
|
29
29
|
// When using shared instantiation with MEMORY we reuse everything but segments (they are unique per key)
|
|
30
30
|
shared: function () {
|
|
@@ -36,6 +36,7 @@ export function InMemoryStorageCSFactory(params) {
|
|
|
36
36
|
impressionCounts: this.impressionCounts,
|
|
37
37
|
events: this.events,
|
|
38
38
|
telemetry: this.telemetry,
|
|
39
|
+
uniqueKeys: this.uniqueKeys,
|
|
39
40
|
destroy: function () { }
|
|
40
41
|
};
|
|
41
42
|
},
|
|
@@ -46,10 +47,8 @@ export function InMemoryStorageCSFactory(params) {
|
|
|
46
47
|
var noopTrack = function () { return true; };
|
|
47
48
|
storage.impressions.track = noopTrack;
|
|
48
49
|
storage.events.track = noopTrack;
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
if (storage.uniqueKeys)
|
|
52
|
-
storage.uniqueKeys.track = noopTrack;
|
|
50
|
+
storage.impressionCounts.track = noopTrack;
|
|
51
|
+
storage.uniqueKeys.track = noopTrack;
|
|
53
52
|
}
|
|
54
53
|
return storage;
|
|
55
54
|
}
|
|
@@ -4,7 +4,7 @@ import { SplitsCacheInRedis } from './SplitsCacheInRedis';
|
|
|
4
4
|
import { SegmentsCacheInRedis } from './SegmentsCacheInRedis';
|
|
5
5
|
import { ImpressionsCacheInRedis } from './ImpressionsCacheInRedis';
|
|
6
6
|
import { EventsCacheInRedis } from './EventsCacheInRedis';
|
|
7
|
-
import {
|
|
7
|
+
import { STORAGE_REDIS } from '../../utils/constants';
|
|
8
8
|
import { TelemetryCacheInRedis } from './TelemetryCacheInRedis';
|
|
9
9
|
import { UniqueKeysCacheInRedis } from './UniqueKeysCacheInRedis';
|
|
10
10
|
import { ImpressionCountsCacheInRedis } from './ImpressionCountsCacheInRedis';
|
|
@@ -20,20 +20,18 @@ export function InRedisStorage(options) {
|
|
|
20
20
|
var RD = require('./RedisAdapter').RedisAdapter;
|
|
21
21
|
var prefix = validatePrefix(options.prefix);
|
|
22
22
|
function InRedisStorageFactory(params) {
|
|
23
|
-
var onReadyCb = params.onReadyCb, settings = params.settings,
|
|
23
|
+
var onReadyCb = params.onReadyCb, settings = params.settings, log = params.settings.log;
|
|
24
24
|
var metadata = metadataBuilder(settings);
|
|
25
25
|
var keys = new KeyBuilderSS(prefix, metadata);
|
|
26
26
|
var redisClient = new RD(log, options.options || {});
|
|
27
27
|
var telemetry = new TelemetryCacheInRedis(log, keys, redisClient);
|
|
28
|
-
var impressionCountsCache =
|
|
29
|
-
var uniqueKeysCache =
|
|
28
|
+
var impressionCountsCache = new ImpressionCountsCacheInRedis(log, keys.buildImpressionsCountKey(), redisClient);
|
|
29
|
+
var uniqueKeysCache = new UniqueKeysCacheInRedis(log, keys.buildUniqueKeysKey(), redisClient);
|
|
30
30
|
// subscription to Redis connect event in order to emit SDK_READY event on consumer mode
|
|
31
31
|
redisClient.on('connect', function () {
|
|
32
32
|
onReadyCb();
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
if (uniqueKeysCache)
|
|
36
|
-
uniqueKeysCache.start();
|
|
33
|
+
impressionCountsCache.start();
|
|
34
|
+
uniqueKeysCache.start();
|
|
37
35
|
// Synchronize config
|
|
38
36
|
telemetry.recordConfig();
|
|
39
37
|
});
|
|
@@ -48,12 +46,10 @@ export function InRedisStorage(options) {
|
|
|
48
46
|
// When using REDIS we should:
|
|
49
47
|
// 1- Disconnect from the storage
|
|
50
48
|
destroy: function () {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
promises.push(uniqueKeysCache.stop());
|
|
56
|
-
return Promise.all(promises).then(function () { redisClient.disconnect(); });
|
|
49
|
+
return Promise.all([
|
|
50
|
+
impressionCountsCache.stop(),
|
|
51
|
+
uniqueKeysCache.stop()
|
|
52
|
+
]).then(function () { redisClient.disconnect(); });
|
|
57
53
|
// @TODO check that caches works as expected when redisClient is disconnected
|
|
58
54
|
}
|
|
59
55
|
};
|
|
@@ -7,7 +7,7 @@ import { EventsCachePluggable } from './EventsCachePluggable';
|
|
|
7
7
|
import { wrapperAdapter, METHODS_TO_PROMISE_WRAP } from './wrapperAdapter';
|
|
8
8
|
import { isObject } from '../../utils/lang';
|
|
9
9
|
import { getStorageHash, validatePrefix } from '../KeyBuilder';
|
|
10
|
-
import { CONSUMER_PARTIAL_MODE,
|
|
10
|
+
import { CONSUMER_PARTIAL_MODE, STORAGE_PLUGGABLE } from '../../utils/constants';
|
|
11
11
|
import { ImpressionsCacheInMemory } from '../inMemory/ImpressionsCacheInMemory';
|
|
12
12
|
import { EventsCacheInMemory } from '../inMemory/EventsCacheInMemory';
|
|
13
13
|
import { ImpressionCountsCacheInMemory } from '../inMemory/ImpressionCountsCacheInMemory';
|
|
@@ -51,32 +51,27 @@ export function PluggableStorage(options) {
|
|
|
51
51
|
validatePluggableStorageOptions(options);
|
|
52
52
|
var prefix = validatePrefix(options.prefix);
|
|
53
53
|
function PluggableStorageFactory(params) {
|
|
54
|
-
var onReadyCb = params.onReadyCb, settings = params.settings, _a = params.settings, log = _a.log, mode = _a.mode,
|
|
54
|
+
var onReadyCb = params.onReadyCb, settings = params.settings, _a = params.settings, log = _a.log, mode = _a.mode, _b = _a.scheduler, impressionsQueueSize = _b.impressionsQueueSize, eventsQueueSize = _b.eventsQueueSize;
|
|
55
55
|
var metadata = metadataBuilder(settings);
|
|
56
56
|
var keys = new KeyBuilderSS(prefix, metadata);
|
|
57
57
|
var wrapper = wrapperAdapter(log, options.wrapper);
|
|
58
|
-
var
|
|
58
|
+
var isSynchronizer = mode === undefined; // If mode is not defined, the synchronizer is running
|
|
59
59
|
var isPartialConsumer = mode === CONSUMER_PARTIAL_MODE;
|
|
60
|
-
var telemetry = shouldRecordTelemetry(params) ||
|
|
60
|
+
var telemetry = shouldRecordTelemetry(params) || isSynchronizer ?
|
|
61
61
|
isPartialConsumer ?
|
|
62
62
|
new TelemetryCacheInMemory() :
|
|
63
63
|
new TelemetryCachePluggable(log, keys, wrapper) :
|
|
64
64
|
undefined;
|
|
65
|
-
var impressionCountsCache =
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
undefined
|
|
70
|
-
|
|
71
|
-
isPartialConsumer ?
|
|
72
|
-
settings.core.key === undefined ? new UniqueKeysCacheInMemory() : new UniqueKeysCacheInMemoryCS() :
|
|
73
|
-
new UniqueKeysCachePluggable(log, keys.buildUniqueKeysKey(), wrapper) :
|
|
74
|
-
undefined;
|
|
65
|
+
var impressionCountsCache = isPartialConsumer ?
|
|
66
|
+
new ImpressionCountsCacheInMemory() :
|
|
67
|
+
new ImpressionCountsCachePluggable(log, keys.buildImpressionsCountKey(), wrapper);
|
|
68
|
+
var uniqueKeysCache = isPartialConsumer ?
|
|
69
|
+
settings.core.key === undefined ? new UniqueKeysCacheInMemory() : new UniqueKeysCacheInMemoryCS() :
|
|
70
|
+
new UniqueKeysCachePluggable(log, keys.buildUniqueKeysKey(), wrapper);
|
|
75
71
|
// Connects to wrapper and emits SDK_READY event on main client
|
|
76
72
|
var connectPromise = wrapper.connect().then(function () {
|
|
77
|
-
if (
|
|
78
|
-
//
|
|
79
|
-
// In standalone or producer mode, clear storage if SDK key, flags filter criteria or flags spec version was modified
|
|
73
|
+
if (isSynchronizer) {
|
|
74
|
+
// In standalone or producer mode, clear storage if SDK key or feature flag filter has changed
|
|
80
75
|
return wrapper.get(keys.buildHashKey()).then(function (hash) {
|
|
81
76
|
var currentHash = getStorageHash(settings);
|
|
82
77
|
if (hash !== currentHash) {
|
|
@@ -91,9 +86,9 @@ export function PluggableStorage(options) {
|
|
|
91
86
|
}
|
|
92
87
|
else {
|
|
93
88
|
// Start periodic flush of async storages if not running synchronizer (producer mode)
|
|
94
|
-
if (impressionCountsCache
|
|
89
|
+
if (impressionCountsCache.start)
|
|
95
90
|
impressionCountsCache.start();
|
|
96
|
-
if (uniqueKeysCache
|
|
91
|
+
if (uniqueKeysCache.start)
|
|
97
92
|
uniqueKeysCache.start();
|
|
98
93
|
if (telemetry && telemetry.recordConfig)
|
|
99
94
|
telemetry.recordConfig();
|
|
@@ -114,9 +109,9 @@ export function PluggableStorage(options) {
|
|
|
114
109
|
uniqueKeys: uniqueKeysCache,
|
|
115
110
|
// Stop periodic flush and disconnect the underlying storage
|
|
116
111
|
destroy: function () {
|
|
117
|
-
return Promise.all(
|
|
118
|
-
impressionCountsCache
|
|
119
|
-
uniqueKeysCache
|
|
112
|
+
return Promise.all(isSynchronizer ? [] : [
|
|
113
|
+
impressionCountsCache.stop && impressionCountsCache.stop(),
|
|
114
|
+
uniqueKeysCache.stop && uniqueKeysCache.stop(),
|
|
120
115
|
]).then(function () { return wrapper.disconnect(); });
|
|
121
116
|
},
|
|
122
117
|
// emits SDK_READY event on shared clients and returns a reference to the storage
|
|
@@ -43,10 +43,9 @@ export function fromObjectUpdaterFactory(splitsParser, storage, readiness, setti
|
|
|
43
43
|
readiness.splits.emit(SDK_SPLITS_ARRIVED);
|
|
44
44
|
if (startingUp) {
|
|
45
45
|
startingUp = false;
|
|
46
|
-
|
|
47
|
-
Promise.resolve().then(function () {
|
|
46
|
+
Promise.resolve(splitsCache.checkCache()).then(function (cacheReady) {
|
|
48
47
|
// Emits SDK_READY_FROM_CACHE
|
|
49
|
-
if (
|
|
48
|
+
if (cacheReady)
|
|
50
49
|
readiness.splits.emit(SDK_SPLITS_CACHE_LOADED);
|
|
51
50
|
// Emits SDK_READY
|
|
52
51
|
readiness.segments.emit(SDK_SEGMENTS_ARRIVED);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { timeout } from '../../../utils/promise/timeout';
|
|
2
|
-
import { SDK_SPLITS_ARRIVED } from '../../../readiness/constants';
|
|
2
|
+
import { SDK_SPLITS_ARRIVED, SDK_SPLITS_CACHE_LOADED } from '../../../readiness/constants';
|
|
3
3
|
import { SYNC_SPLITS_FETCH, SYNC_SPLITS_NEW, SYNC_SPLITS_REMOVED, SYNC_SPLITS_SEGMENTS, SYNC_SPLITS_FETCH_FAILS, SYNC_SPLITS_FETCH_RETRY } from '../../../logger/constants';
|
|
4
4
|
import { startsWith } from '../../../utils/lang';
|
|
5
5
|
import { IN_SEGMENT } from '../../../utils/constants';
|
|
@@ -121,7 +121,7 @@ export function splitChangesUpdaterFactory(log, splitChangesFetcher, splits, seg
|
|
|
121
121
|
function _splitChangesUpdater(since, retry) {
|
|
122
122
|
if (retry === void 0) { retry = 0; }
|
|
123
123
|
log.debug(SYNC_SPLITS_FETCH, [since]);
|
|
124
|
-
|
|
124
|
+
var fetcherPromise = Promise.resolve(splitUpdateNotification ?
|
|
125
125
|
{ splits: [splitUpdateNotification.payload], till: splitUpdateNotification.changeNumber } :
|
|
126
126
|
splitChangesFetcher(since, noCache, till, _promiseDecorator))
|
|
127
127
|
.then(function (splitChanges) {
|
|
@@ -165,6 +165,15 @@ export function splitChangesUpdaterFactory(log, splitChangesFetcher, splits, seg
|
|
|
165
165
|
}
|
|
166
166
|
return false;
|
|
167
167
|
});
|
|
168
|
+
// After triggering the requests, if we have cached splits information let's notify that to emit SDK_READY_FROM_CACHE.
|
|
169
|
+
// Wrapping in a promise since checkCache can be async.
|
|
170
|
+
if (splitsEventEmitter && startingUp) {
|
|
171
|
+
Promise.resolve(splits.checkCache()).then(function (isCacheReady) {
|
|
172
|
+
if (isCacheReady)
|
|
173
|
+
splitsEventEmitter.emit(SDK_SPLITS_CACHE_LOADED);
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
return fetcherPromise;
|
|
168
177
|
}
|
|
169
178
|
var sincePromise = Promise.resolve(splits.getChangeNumber()); // `getChangeNumber` never rejects or throws error
|
|
170
179
|
return sincePromise.then(_splitChangesUpdater);
|
|
@@ -26,8 +26,6 @@ var IMPRESSIONS_COUNT_RATE = 1800000; // 30 minutes
|
|
|
26
26
|
*/
|
|
27
27
|
export function impressionCountsSubmitterFactory(params) {
|
|
28
28
|
var log = params.settings.log, postTestImpressionsCount = params.splitApi.postTestImpressionsCount, impressionCounts = params.storage.impressionCounts;
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
return submitterFactory(log, postTestImpressionsCount, impressionCounts, IMPRESSIONS_COUNT_RATE, 'impression counts', fromImpressionCountsCollector, 1);
|
|
32
|
-
}
|
|
29
|
+
// retry impressions counts only once.
|
|
30
|
+
return submitterFactory(log, postTestImpressionsCount, impressionCounts, IMPRESSIONS_COUNT_RATE, 'impression counts', fromImpressionCountsCollector, 1);
|
|
33
31
|
}
|
|
@@ -6,14 +6,11 @@ import { uniqueKeysSubmitterFactory } from './uniqueKeysSubmitter';
|
|
|
6
6
|
export function submitterManagerFactory(params) {
|
|
7
7
|
var submitters = [
|
|
8
8
|
impressionsSubmitterFactory(params),
|
|
9
|
-
eventsSubmitterFactory(params)
|
|
9
|
+
eventsSubmitterFactory(params),
|
|
10
|
+
impressionCountsSubmitterFactory(params),
|
|
11
|
+
uniqueKeysSubmitterFactory(params)
|
|
10
12
|
];
|
|
11
|
-
var impressionCountsSubmitter = impressionCountsSubmitterFactory(params);
|
|
12
|
-
if (impressionCountsSubmitter)
|
|
13
|
-
submitters.push(impressionCountsSubmitter);
|
|
14
13
|
var telemetrySubmitter = telemetrySubmitterFactory(params);
|
|
15
|
-
if (params.storage.uniqueKeys)
|
|
16
|
-
submitters.push(uniqueKeysSubmitterFactory(params));
|
|
17
14
|
return {
|
|
18
15
|
// `onlyTelemetry` true if SDK is created with userConsent not GRANTED
|
|
19
16
|
start: function (onlyTelemetry) {
|
|
@@ -3,7 +3,6 @@ import { PUSH_SUBSYSTEM_UP, PUSH_SUBSYSTEM_DOWN } from './streaming/constants';
|
|
|
3
3
|
import { SYNC_START_POLLING, SYNC_CONTINUE_POLLING, SYNC_STOP_POLLING } from '../logger/constants';
|
|
4
4
|
import { isConsentGranted } from '../consent';
|
|
5
5
|
import { POLLING, STREAMING, SYNC_MODE_UPDATE } from '../utils/constants';
|
|
6
|
-
import { SDK_SPLITS_CACHE_LOADED } from '../readiness/constants';
|
|
7
6
|
/**
|
|
8
7
|
* Online SyncManager factory.
|
|
9
8
|
* Can be used for server-side API, and client-side API with or without multiple clients.
|
|
@@ -17,7 +16,7 @@ export function syncManagerOnlineFactory(pollingManagerFactory, pushManagerFacto
|
|
|
17
16
|
* SyncManager factory for modular SDK
|
|
18
17
|
*/
|
|
19
18
|
return function (params) {
|
|
20
|
-
var settings = params.settings, _a = params.settings, log = _a.log, streamingEnabled = _a.streamingEnabled, syncEnabled = _a.sync.enabled, telemetryTracker = params.telemetryTracker
|
|
19
|
+
var settings = params.settings, _a = params.settings, log = _a.log, streamingEnabled = _a.streamingEnabled, syncEnabled = _a.sync.enabled, telemetryTracker = params.telemetryTracker;
|
|
21
20
|
/** Polling Manager */
|
|
22
21
|
var pollingManager = pollingManagerFactory && pollingManagerFactory(params);
|
|
23
22
|
/** Push Manager */
|
|
@@ -65,11 +64,6 @@ export function syncManagerOnlineFactory(pollingManagerFactory, pushManagerFacto
|
|
|
65
64
|
*/
|
|
66
65
|
start: function () {
|
|
67
66
|
running = true;
|
|
68
|
-
if (startFirstTime) {
|
|
69
|
-
var isCacheLoaded = storage.validateCache ? storage.validateCache() : false;
|
|
70
|
-
if (isCacheLoaded)
|
|
71
|
-
Promise.resolve().then(function () { readiness.splits.emit(SDK_SPLITS_CACHE_LOADED); });
|
|
72
|
-
}
|
|
73
67
|
// start syncing splits and segments
|
|
74
68
|
if (pollingManager) {
|
|
75
69
|
// If synchronization is disabled pushManager and pollingManager should not start
|
|
@@ -78,6 +72,7 @@ export function syncManagerOnlineFactory(pollingManagerFactory, pushManagerFacto
|
|
|
78
72
|
// Doesn't call `syncAll` when the syncManager is resuming
|
|
79
73
|
if (startFirstTime) {
|
|
80
74
|
pollingManager.syncAll();
|
|
75
|
+
startFirstTime = false;
|
|
81
76
|
}
|
|
82
77
|
pushManager.start();
|
|
83
78
|
}
|
|
@@ -88,12 +83,12 @@ export function syncManagerOnlineFactory(pollingManagerFactory, pushManagerFacto
|
|
|
88
83
|
else {
|
|
89
84
|
if (startFirstTime) {
|
|
90
85
|
pollingManager.syncAll();
|
|
86
|
+
startFirstTime = false;
|
|
91
87
|
}
|
|
92
88
|
}
|
|
93
89
|
}
|
|
94
90
|
// start periodic data recording (events, impressions, telemetry).
|
|
95
91
|
submitterManager.start(!isConsentGranted(settings));
|
|
96
|
-
startFirstTime = false;
|
|
97
92
|
},
|
|
98
93
|
/**
|
|
99
94
|
* Method used to stop/pause the syncManager.
|
|
@@ -4,38 +4,37 @@ import { IMPRESSIONS_TRACKER_SUCCESS, ERROR_IMPRESSIONS_TRACKER, ERROR_IMPRESSIO
|
|
|
4
4
|
import { CONSENT_DECLINED, DEDUPED, QUEUED } from '../utils/constants';
|
|
5
5
|
/**
|
|
6
6
|
* Impressions tracker stores impressions in cache and pass them to the listener and integrations manager if provided.
|
|
7
|
-
*
|
|
8
|
-
* @param impressionsCache - cache to save impressions
|
|
9
|
-
* @param metadata - runtime metadata (ip, hostname and version)
|
|
10
|
-
* @param impressionListener - optional impression listener
|
|
11
|
-
* @param integrationsManager - optional integrations manager
|
|
12
|
-
* @param strategy - strategy for impressions tracking.
|
|
13
7
|
*/
|
|
14
|
-
export function impressionsTrackerFactory(settings, impressionsCache, strategy, whenInit, integrationsManager, telemetryCache) {
|
|
8
|
+
export function impressionsTrackerFactory(settings, impressionsCache, noneStrategy, strategy, whenInit, integrationsManager, telemetryCache) {
|
|
15
9
|
var log = settings.log, impressionListener = settings.impressionListener, _a = settings.runtime, ip = _a.ip, hostname = _a.hostname, version = settings.version;
|
|
16
10
|
return {
|
|
17
11
|
track: function (impressions, attributes) {
|
|
18
12
|
if (settings.userConsent === CONSENT_DECLINED)
|
|
19
13
|
return;
|
|
20
|
-
var
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
14
|
+
var impressionsToStore = impressions.filter(function (_a) {
|
|
15
|
+
var imp = _a.imp, disabled = _a.disabled;
|
|
16
|
+
return disabled ?
|
|
17
|
+
noneStrategy.process(imp) :
|
|
18
|
+
strategy.process(imp);
|
|
19
|
+
});
|
|
20
|
+
var impressionsLength = impressions.length;
|
|
21
|
+
var impressionsToStoreLength = impressionsToStore.length;
|
|
22
|
+
if (impressionsToStoreLength) {
|
|
23
|
+
var res = impressionsCache.track(impressionsToStore.map(function (item) { return item.imp; }));
|
|
25
24
|
// If we're on an async storage, handle error and log it.
|
|
26
25
|
if (thenable(res)) {
|
|
27
26
|
res.then(function () {
|
|
28
|
-
log.info(IMPRESSIONS_TRACKER_SUCCESS, [
|
|
27
|
+
log.info(IMPRESSIONS_TRACKER_SUCCESS, [impressionsLength]);
|
|
29
28
|
}).catch(function (err) {
|
|
30
|
-
log.error(ERROR_IMPRESSIONS_TRACKER, [
|
|
29
|
+
log.error(ERROR_IMPRESSIONS_TRACKER, [impressionsLength, err]);
|
|
31
30
|
});
|
|
32
31
|
}
|
|
33
32
|
else {
|
|
34
33
|
// Record when impressionsCache is sync only (standalone mode)
|
|
35
34
|
// @TODO we are not dropping impressions on full queue yet, so DROPPED stats are not recorded
|
|
36
35
|
if (telemetryCache) {
|
|
37
|
-
telemetryCache.recordImpressionStats(QUEUED,
|
|
38
|
-
telemetryCache.recordImpressionStats(DEDUPED,
|
|
36
|
+
telemetryCache.recordImpressionStats(QUEUED, impressionsToStoreLength);
|
|
37
|
+
telemetryCache.recordImpressionStats(DEDUPED, impressionsLength - impressionsToStoreLength);
|
|
39
38
|
}
|
|
40
39
|
}
|
|
41
40
|
}
|
|
@@ -44,7 +43,7 @@ export function impressionsTrackerFactory(settings, impressionsCache, strategy,
|
|
|
44
43
|
var _loop_1 = function (i) {
|
|
45
44
|
var impressionData = {
|
|
46
45
|
// copy of impression, to avoid unexpected behavior if modified by integrations or impressionListener
|
|
47
|
-
impression: objectAssign({},
|
|
46
|
+
impression: objectAssign({}, impressions[i].imp),
|
|
48
47
|
attributes: attributes,
|
|
49
48
|
ip: ip,
|
|
50
49
|
hostname: hostname,
|
|
@@ -66,7 +65,7 @@ export function impressionsTrackerFactory(settings, impressionsCache, strategy,
|
|
|
66
65
|
});
|
|
67
66
|
});
|
|
68
67
|
};
|
|
69
|
-
for (var i = 0; i <
|
|
68
|
+
for (var i = 0; i < impressionsLength; i++) {
|
|
70
69
|
_loop_1(i);
|
|
71
70
|
}
|
|
72
71
|
}
|
|
@@ -2,20 +2,13 @@
|
|
|
2
2
|
* Debug strategy for impressions tracker. Wraps impressions to store and adds previousTime if it corresponds
|
|
3
3
|
*
|
|
4
4
|
* @param impressionsObserver - impression observer. Previous time (pt property) is included in impression instances
|
|
5
|
-
* @returns
|
|
5
|
+
* @returns Debug strategy
|
|
6
6
|
*/
|
|
7
7
|
export function strategyDebugFactory(impressionsObserver) {
|
|
8
8
|
return {
|
|
9
|
-
process: function (
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
impression.pt = impressionsObserver.testAndSet(impression);
|
|
13
|
-
});
|
|
14
|
-
return {
|
|
15
|
-
impressionsToStore: impressions,
|
|
16
|
-
impressionsToListener: impressions,
|
|
17
|
-
deduped: 0
|
|
18
|
-
};
|
|
9
|
+
process: function (impression) {
|
|
10
|
+
impression.pt = impressionsObserver.testAndSet(impression);
|
|
11
|
+
return true;
|
|
19
12
|
}
|
|
20
13
|
};
|
|
21
14
|
}
|