@splitsoftware/splitio-commons 2.4.2-rc.3 → 2.5.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 +2 -10
- package/cjs/storages/dataLoader.js +102 -43
- package/cjs/storages/inLocalStorage/MySegmentsCacheInLocal.js +16 -16
- package/cjs/storages/inLocalStorage/RBSegmentsCacheInLocal.js +21 -17
- package/cjs/storages/inLocalStorage/SplitsCacheInLocal.js +37 -33
- package/cjs/storages/inLocalStorage/index.js +13 -31
- package/cjs/storages/inLocalStorage/validateCache.js +23 -28
- package/cjs/storages/inMemory/InMemoryStorageCS.js +32 -14
- package/cjs/storages/inMemory/RBSegmentsCacheInMemory.js +4 -0
- package/cjs/sync/offline/syncTasks/fromObjectSyncTask.js +3 -2
- package/cjs/sync/polling/updaters/mySegmentsUpdater.js +0 -2
- package/cjs/sync/polling/updaters/splitChangesUpdater.js +0 -2
- package/cjs/sync/syncManagerOnline.js +24 -28
- package/cjs/utils/env/isLocalStorageAvailable.js +5 -28
- package/cjs/utils/settingsValidation/storage/storageCS.js +1 -1
- package/esm/storages/dataLoader.js +99 -41
- package/esm/storages/inLocalStorage/MySegmentsCacheInLocal.js +16 -16
- package/esm/storages/inLocalStorage/RBSegmentsCacheInLocal.js +21 -17
- package/esm/storages/inLocalStorage/SplitsCacheInLocal.js +37 -33
- package/esm/storages/inLocalStorage/index.js +14 -32
- package/esm/storages/inLocalStorage/validateCache.js +23 -28
- package/esm/storages/inMemory/InMemoryStorageCS.js +32 -14
- package/esm/storages/inMemory/RBSegmentsCacheInMemory.js +4 -0
- package/esm/sync/offline/syncTasks/fromObjectSyncTask.js +3 -2
- package/esm/sync/polling/updaters/mySegmentsUpdater.js +0 -2
- package/esm/sync/polling/updaters/splitChangesUpdater.js +0 -2
- package/esm/sync/syncManagerOnline.js +24 -28
- package/esm/utils/env/isLocalStorageAvailable.js +3 -24
- package/esm/utils/settingsValidation/storage/storageCS.js +1 -1
- package/package.json +1 -1
- package/src/sdkFactory/index.ts +3 -2
- package/src/storages/dataLoader.ts +107 -49
- package/src/storages/inLocalStorage/MySegmentsCacheInLocal.ts +17 -18
- package/src/storages/inLocalStorage/RBSegmentsCacheInLocal.ts +22 -19
- package/src/storages/inLocalStorage/SplitsCacheInLocal.ts +37 -34
- package/src/storages/inLocalStorage/index.ts +16 -37
- package/src/storages/inLocalStorage/validateCache.ts +23 -29
- package/src/storages/inMemory/InMemoryStorageCS.ts +37 -14
- package/src/storages/inMemory/RBSegmentsCacheInMemory.ts +4 -0
- package/src/storages/types.ts +6 -22
- package/src/sync/offline/syncTasks/fromObjectSyncTask.ts +2 -1
- package/src/sync/polling/updaters/mySegmentsUpdater.ts +0 -2
- package/src/sync/polling/updaters/splitChangesUpdater.ts +1 -3
- package/src/sync/syncManagerOnline.ts +22 -27
- package/src/types.ts +0 -35
- package/src/utils/env/isLocalStorageAvailable.ts +3 -24
- package/src/utils/settingsValidation/storage/storageCS.ts +1 -1
- package/types/splitio.d.ts +46 -42
- package/cjs/storages/inLocalStorage/storageAdapter.js +0 -54
- package/esm/storages/inLocalStorage/storageAdapter.js +0 -50
- package/src/storages/inLocalStorage/storageAdapter.ts +0 -62
|
@@ -6,28 +6,29 @@ import { ILogger } from '../../logger/types';
|
|
|
6
6
|
import { LOG_PREFIX } from './constants';
|
|
7
7
|
import { ISettings } from '../../types';
|
|
8
8
|
import { setToArray } from '../../utils/lang/sets';
|
|
9
|
-
import { StorageAdapter } from '../types';
|
|
10
9
|
|
|
10
|
+
/**
|
|
11
|
+
* ISplitsCacheSync implementation that stores split definitions in browser LocalStorage.
|
|
12
|
+
*/
|
|
11
13
|
export class SplitsCacheInLocal extends AbstractSplitsCacheSync {
|
|
12
14
|
|
|
13
15
|
private readonly keys: KeyBuilderCS;
|
|
14
16
|
private readonly log: ILogger;
|
|
15
17
|
private readonly flagSetsFilter: string[];
|
|
16
18
|
private hasSync?: boolean;
|
|
17
|
-
private readonly storage: StorageAdapter;
|
|
18
19
|
|
|
19
|
-
constructor(settings: ISettings, keys: KeyBuilderCS
|
|
20
|
+
constructor(settings: ISettings, keys: KeyBuilderCS) {
|
|
20
21
|
super();
|
|
21
22
|
this.keys = keys;
|
|
22
23
|
this.log = settings.log;
|
|
23
24
|
this.flagSetsFilter = settings.sync.__splitFiltersValidation.groupedFilters.bySet;
|
|
24
|
-
this.storage = storage;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
private _decrementCount(key: string) {
|
|
28
|
-
const count = toNumber(
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
const count = toNumber(localStorage.getItem(key)) - 1;
|
|
29
|
+
// @ts-expect-error
|
|
30
|
+
if (count > 0) localStorage.setItem(key, count);
|
|
31
|
+
else localStorage.removeItem(key);
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
private _decrementCounts(split: ISplit) {
|
|
@@ -47,11 +48,13 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync {
|
|
|
47
48
|
private _incrementCounts(split: ISplit) {
|
|
48
49
|
try {
|
|
49
50
|
const ttKey = this.keys.buildTrafficTypeKey(split.trafficTypeName);
|
|
50
|
-
|
|
51
|
+
// @ts-expect-error
|
|
52
|
+
localStorage.setItem(ttKey, toNumber(localStorage.getItem(ttKey)) + 1);
|
|
51
53
|
|
|
52
54
|
if (usesSegments(split)) {
|
|
53
55
|
const segmentsCountKey = this.keys.buildSplitsWithSegmentCountKey();
|
|
54
|
-
|
|
56
|
+
// @ts-expect-error
|
|
57
|
+
localStorage.setItem(segmentsCountKey, toNumber(localStorage.getItem(segmentsCountKey)) + 1);
|
|
55
58
|
}
|
|
56
59
|
} catch (e) {
|
|
57
60
|
this.log.error(LOG_PREFIX + e);
|
|
@@ -65,15 +68,15 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync {
|
|
|
65
68
|
*/
|
|
66
69
|
clear() {
|
|
67
70
|
// collect item keys
|
|
68
|
-
const len =
|
|
71
|
+
const len = localStorage.length;
|
|
69
72
|
const accum = [];
|
|
70
73
|
for (let cur = 0; cur < len; cur++) {
|
|
71
|
-
const key =
|
|
74
|
+
const key = localStorage.key(cur);
|
|
72
75
|
if (key != null && this.keys.isSplitsCacheKey(key)) accum.push(key);
|
|
73
76
|
}
|
|
74
77
|
// remove items
|
|
75
78
|
accum.forEach(key => {
|
|
76
|
-
|
|
79
|
+
localStorage.removeItem(key);
|
|
77
80
|
});
|
|
78
81
|
|
|
79
82
|
this.hasSync = false;
|
|
@@ -83,15 +86,15 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync {
|
|
|
83
86
|
try {
|
|
84
87
|
const name = split.name;
|
|
85
88
|
const splitKey = this.keys.buildSplitKey(name);
|
|
86
|
-
const
|
|
87
|
-
const previousSplit =
|
|
89
|
+
const splitFromLocalStorage = localStorage.getItem(splitKey);
|
|
90
|
+
const previousSplit = splitFromLocalStorage ? JSON.parse(splitFromLocalStorage) : null;
|
|
88
91
|
|
|
89
92
|
if (previousSplit) {
|
|
90
93
|
this._decrementCounts(previousSplit);
|
|
91
94
|
this.removeFromFlagSets(previousSplit.name, previousSplit.sets);
|
|
92
95
|
}
|
|
93
96
|
|
|
94
|
-
|
|
97
|
+
localStorage.setItem(splitKey, JSON.stringify(split));
|
|
95
98
|
|
|
96
99
|
this._incrementCounts(split);
|
|
97
100
|
this.addToFlagSets(split);
|
|
@@ -108,7 +111,7 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync {
|
|
|
108
111
|
const split = this.getSplit(name);
|
|
109
112
|
if (!split) return false;
|
|
110
113
|
|
|
111
|
-
|
|
114
|
+
localStorage.removeItem(this.keys.buildSplitKey(name));
|
|
112
115
|
|
|
113
116
|
this._decrementCounts(split);
|
|
114
117
|
this.removeFromFlagSets(split.name, split.sets);
|
|
@@ -121,15 +124,15 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync {
|
|
|
121
124
|
}
|
|
122
125
|
|
|
123
126
|
getSplit(name: string): ISplit | null {
|
|
124
|
-
const item =
|
|
127
|
+
const item = localStorage.getItem(this.keys.buildSplitKey(name));
|
|
125
128
|
return item && JSON.parse(item);
|
|
126
129
|
}
|
|
127
130
|
|
|
128
131
|
setChangeNumber(changeNumber: number): boolean {
|
|
129
132
|
try {
|
|
130
|
-
|
|
133
|
+
localStorage.setItem(this.keys.buildSplitsTillKey(), changeNumber + '');
|
|
131
134
|
// update "last updated" timestamp with current time
|
|
132
|
-
|
|
135
|
+
localStorage.setItem(this.keys.buildLastUpdatedKey(), Date.now() + '');
|
|
133
136
|
this.hasSync = true;
|
|
134
137
|
return true;
|
|
135
138
|
} catch (e) {
|
|
@@ -140,7 +143,7 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync {
|
|
|
140
143
|
|
|
141
144
|
getChangeNumber(): number {
|
|
142
145
|
const n = -1;
|
|
143
|
-
let value: string | number | null =
|
|
146
|
+
let value: string | number | null = localStorage.getItem(this.keys.buildSplitsTillKey());
|
|
144
147
|
|
|
145
148
|
if (value !== null) {
|
|
146
149
|
value = parseInt(value, 10);
|
|
@@ -152,13 +155,13 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync {
|
|
|
152
155
|
}
|
|
153
156
|
|
|
154
157
|
getSplitNames(): string[] {
|
|
155
|
-
const len =
|
|
158
|
+
const len = localStorage.length;
|
|
156
159
|
const accum = [];
|
|
157
160
|
|
|
158
161
|
let cur = 0;
|
|
159
162
|
|
|
160
163
|
while (cur < len) {
|
|
161
|
-
const key =
|
|
164
|
+
const key = localStorage.key(cur);
|
|
162
165
|
|
|
163
166
|
if (key != null && this.keys.isSplitKey(key)) accum.push(this.keys.extractKey(key));
|
|
164
167
|
|
|
@@ -169,7 +172,7 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync {
|
|
|
169
172
|
}
|
|
170
173
|
|
|
171
174
|
trafficTypeExists(trafficType: string): boolean {
|
|
172
|
-
const ttCount = toNumber(
|
|
175
|
+
const ttCount = toNumber(localStorage.getItem(this.keys.buildTrafficTypeKey(trafficType)));
|
|
173
176
|
return isFiniteNumber(ttCount) && ttCount > 0;
|
|
174
177
|
}
|
|
175
178
|
|
|
@@ -177,7 +180,7 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync {
|
|
|
177
180
|
// If cache hasn't been synchronized with the cloud, assume we need them.
|
|
178
181
|
if (!this.hasSync) return true;
|
|
179
182
|
|
|
180
|
-
const storedCount =
|
|
183
|
+
const storedCount = localStorage.getItem(this.keys.buildSplitsWithSegmentCountKey());
|
|
181
184
|
const splitsWithSegmentsCount = storedCount === null ? 0 : toNumber(storedCount);
|
|
182
185
|
|
|
183
186
|
return isFiniteNumber(splitsWithSegmentsCount) ?
|
|
@@ -188,9 +191,9 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync {
|
|
|
188
191
|
getNamesByFlagSets(flagSets: string[]): Set<string>[] {
|
|
189
192
|
return flagSets.map(flagSet => {
|
|
190
193
|
const flagSetKey = this.keys.buildFlagSetKey(flagSet);
|
|
191
|
-
const
|
|
194
|
+
const flagSetFromLocalStorage = localStorage.getItem(flagSetKey);
|
|
192
195
|
|
|
193
|
-
return new Set(
|
|
196
|
+
return new Set(flagSetFromLocalStorage ? JSON.parse(flagSetFromLocalStorage) : []);
|
|
194
197
|
});
|
|
195
198
|
}
|
|
196
199
|
|
|
@@ -203,12 +206,12 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync {
|
|
|
203
206
|
|
|
204
207
|
const flagSetKey = this.keys.buildFlagSetKey(featureFlagSet);
|
|
205
208
|
|
|
206
|
-
const
|
|
209
|
+
const flagSetFromLocalStorage = localStorage.getItem(flagSetKey);
|
|
207
210
|
|
|
208
|
-
const flagSetCache = new Set(
|
|
211
|
+
const flagSetCache = new Set(flagSetFromLocalStorage ? JSON.parse(flagSetFromLocalStorage) : []);
|
|
209
212
|
flagSetCache.add(featureFlag.name);
|
|
210
213
|
|
|
211
|
-
|
|
214
|
+
localStorage.setItem(flagSetKey, JSON.stringify(setToArray(flagSetCache)));
|
|
212
215
|
});
|
|
213
216
|
}
|
|
214
217
|
|
|
@@ -223,19 +226,19 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync {
|
|
|
223
226
|
private removeNames(flagSetName: string, featureFlagName: string) {
|
|
224
227
|
const flagSetKey = this.keys.buildFlagSetKey(flagSetName);
|
|
225
228
|
|
|
226
|
-
const
|
|
229
|
+
const flagSetFromLocalStorage = localStorage.getItem(flagSetKey);
|
|
227
230
|
|
|
228
|
-
if (!
|
|
231
|
+
if (!flagSetFromLocalStorage) return;
|
|
229
232
|
|
|
230
|
-
const flagSetCache = new Set(JSON.parse(
|
|
233
|
+
const flagSetCache = new Set(JSON.parse(flagSetFromLocalStorage));
|
|
231
234
|
flagSetCache.delete(featureFlagName);
|
|
232
235
|
|
|
233
236
|
if (flagSetCache.size === 0) {
|
|
234
|
-
|
|
237
|
+
localStorage.removeItem(flagSetKey);
|
|
235
238
|
return;
|
|
236
239
|
}
|
|
237
240
|
|
|
238
|
-
|
|
241
|
+
localStorage.setItem(flagSetKey, JSON.stringify(setToArray(flagSetCache)));
|
|
239
242
|
}
|
|
240
243
|
|
|
241
244
|
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { ImpressionsCacheInMemory } from '../inMemory/ImpressionsCacheInMemory';
|
|
2
2
|
import { ImpressionCountsCacheInMemory } from '../inMemory/ImpressionCountsCacheInMemory';
|
|
3
3
|
import { EventsCacheInMemory } from '../inMemory/EventsCacheInMemory';
|
|
4
|
-
import { IStorageFactoryParams, IStorageSync, IStorageSyncFactory
|
|
4
|
+
import { IStorageFactoryParams, IStorageSync, IStorageSyncFactory } from '../types';
|
|
5
5
|
import { validatePrefix } from '../KeyBuilder';
|
|
6
6
|
import { KeyBuilderCS, myLargeSegmentsKeyBuilder } from '../KeyBuilderCS';
|
|
7
|
-
import { isLocalStorageAvailable
|
|
7
|
+
import { isLocalStorageAvailable } from '../../utils/env/isLocalStorageAvailable';
|
|
8
8
|
import { SplitsCacheInLocal } from './SplitsCacheInLocal';
|
|
9
9
|
import { RBSegmentsCacheInLocal } from './RBSegmentsCacheInLocal';
|
|
10
10
|
import { MySegmentsCacheInLocal } from './MySegmentsCacheInLocal';
|
|
@@ -15,24 +15,7 @@ import { shouldRecordTelemetry, TelemetryCacheInMemory } from '../inMemory/Telem
|
|
|
15
15
|
import { UniqueKeysCacheInMemoryCS } from '../inMemory/UniqueKeysCacheInMemoryCS';
|
|
16
16
|
import { getMatching } from '../../utils/key';
|
|
17
17
|
import { validateCache } from './validateCache';
|
|
18
|
-
import { ILogger } from '../../logger/types';
|
|
19
18
|
import SplitIO from '../../../types/splitio';
|
|
20
|
-
import { storageAdapter } from './storageAdapter';
|
|
21
|
-
|
|
22
|
-
function validateStorage(log: ILogger, prefix: string, wrapper?: SplitIO.SyncStorageWrapper | SplitIO.AsyncStorageWrapper): StorageAdapter | undefined {
|
|
23
|
-
if (wrapper) {
|
|
24
|
-
if (isValidStorageWrapper(wrapper)) {
|
|
25
|
-
return isWebStorage(wrapper) ?
|
|
26
|
-
wrapper as StorageAdapter: // localStorage and sessionStorage don't need adapter
|
|
27
|
-
storageAdapter(log, prefix, wrapper);
|
|
28
|
-
}
|
|
29
|
-
log.warn(LOG_PREFIX + 'Invalid storage provided. Falling back to LocalStorage API');
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
if (isLocalStorageAvailable()) return localStorage;
|
|
33
|
-
|
|
34
|
-
log.warn(LOG_PREFIX + 'LocalStorage API is unavailable. Falling back to default MEMORY storage');
|
|
35
|
-
}
|
|
36
19
|
|
|
37
20
|
/**
|
|
38
21
|
* InLocal storage factory for standalone client-side SplitFactory
|
|
@@ -42,19 +25,21 @@ export function InLocalStorage(options: SplitIO.InLocalStorageOptions = {}): ISt
|
|
|
42
25
|
const prefix = validatePrefix(options.prefix);
|
|
43
26
|
|
|
44
27
|
function InLocalStorageCSFactory(params: IStorageFactoryParams): IStorageSync {
|
|
45
|
-
const { settings, settings: { log, scheduler: { impressionsQueueSize, eventsQueueSize } } } = params;
|
|
46
28
|
|
|
47
|
-
|
|
48
|
-
if (!
|
|
29
|
+
// Fallback to InMemoryStorage if LocalStorage API is not available
|
|
30
|
+
if (!isLocalStorageAvailable()) {
|
|
31
|
+
params.settings.log.warn(LOG_PREFIX + 'LocalStorage API is unavailable. Falling back to default MEMORY storage');
|
|
32
|
+
return InMemoryStorageCSFactory(params);
|
|
33
|
+
}
|
|
49
34
|
|
|
35
|
+
const { settings, settings: { log, scheduler: { impressionsQueueSize, eventsQueueSize } } } = params;
|
|
50
36
|
const matchingKey = getMatching(settings.core.key);
|
|
51
37
|
const keys = new KeyBuilderCS(prefix, matchingKey);
|
|
52
38
|
|
|
53
|
-
const splits = new SplitsCacheInLocal(settings, keys
|
|
54
|
-
const rbSegments = new RBSegmentsCacheInLocal(settings, keys
|
|
55
|
-
const segments = new MySegmentsCacheInLocal(log, keys
|
|
56
|
-
const largeSegments = new MySegmentsCacheInLocal(log, myLargeSegmentsKeyBuilder(prefix, matchingKey)
|
|
57
|
-
let validateCachePromise: Promise<boolean> | undefined;
|
|
39
|
+
const splits = new SplitsCacheInLocal(settings, keys);
|
|
40
|
+
const rbSegments = new RBSegmentsCacheInLocal(settings, keys);
|
|
41
|
+
const segments = new MySegmentsCacheInLocal(log, keys);
|
|
42
|
+
const largeSegments = new MySegmentsCacheInLocal(log, myLargeSegmentsKeyBuilder(prefix, matchingKey));
|
|
58
43
|
|
|
59
44
|
return {
|
|
60
45
|
splits,
|
|
@@ -68,16 +53,10 @@ export function InLocalStorage(options: SplitIO.InLocalStorageOptions = {}): ISt
|
|
|
68
53
|
uniqueKeys: new UniqueKeysCacheInMemoryCS(),
|
|
69
54
|
|
|
70
55
|
validateCache() {
|
|
71
|
-
return
|
|
56
|
+
return validateCache(options, settings, keys, splits, rbSegments, segments, largeSegments);
|
|
72
57
|
},
|
|
73
58
|
|
|
74
|
-
|
|
75
|
-
return storage.save && storage.save();
|
|
76
|
-
},
|
|
77
|
-
|
|
78
|
-
destroy() {
|
|
79
|
-
return storage.whenSaved && storage.whenSaved();
|
|
80
|
-
},
|
|
59
|
+
destroy() { },
|
|
81
60
|
|
|
82
61
|
// When using shared instantiation with MEMORY we reuse everything but segments (they are customer per key).
|
|
83
62
|
shared(matchingKey: string) {
|
|
@@ -85,8 +64,8 @@ export function InLocalStorage(options: SplitIO.InLocalStorageOptions = {}): ISt
|
|
|
85
64
|
return {
|
|
86
65
|
splits: this.splits,
|
|
87
66
|
rbSegments: this.rbSegments,
|
|
88
|
-
segments: new MySegmentsCacheInLocal(log, new KeyBuilderCS(prefix, matchingKey)
|
|
89
|
-
largeSegments: new MySegmentsCacheInLocal(log, myLargeSegmentsKeyBuilder(prefix, matchingKey)
|
|
67
|
+
segments: new MySegmentsCacheInLocal(log, new KeyBuilderCS(prefix, matchingKey)),
|
|
68
|
+
largeSegments: new MySegmentsCacheInLocal(log, myLargeSegmentsKeyBuilder(prefix, matchingKey)),
|
|
90
69
|
impressions: this.impressions,
|
|
91
70
|
impressionCounts: this.impressionCounts,
|
|
92
71
|
events: this.events,
|
|
@@ -7,7 +7,6 @@ import type { RBSegmentsCacheInLocal } from './RBSegmentsCacheInLocal';
|
|
|
7
7
|
import type { MySegmentsCacheInLocal } from './MySegmentsCacheInLocal';
|
|
8
8
|
import { KeyBuilderCS } from '../KeyBuilderCS';
|
|
9
9
|
import SplitIO from '../../../types/splitio';
|
|
10
|
-
import { StorageAdapter } from '../types';
|
|
11
10
|
|
|
12
11
|
const DEFAULT_CACHE_EXPIRATION_IN_DAYS = 10;
|
|
13
12
|
const MILLIS_IN_A_DAY = 86400000;
|
|
@@ -17,11 +16,11 @@ const MILLIS_IN_A_DAY = 86400000;
|
|
|
17
16
|
*
|
|
18
17
|
* @returns `true` if cache should be cleared, `false` otherwise
|
|
19
18
|
*/
|
|
20
|
-
function validateExpiration(options: SplitIO.InLocalStorageOptions,
|
|
19
|
+
function validateExpiration(options: SplitIO.InLocalStorageOptions, settings: ISettings, keys: KeyBuilderCS, currentTimestamp: number, isThereCache: boolean) {
|
|
21
20
|
const { log } = settings;
|
|
22
21
|
|
|
23
22
|
// Check expiration
|
|
24
|
-
const lastUpdatedTimestamp = parseInt(
|
|
23
|
+
const lastUpdatedTimestamp = parseInt(localStorage.getItem(keys.buildLastUpdatedKey()) as string, 10);
|
|
25
24
|
if (!isNaNNumber(lastUpdatedTimestamp)) {
|
|
26
25
|
const cacheExpirationInDays = isFiniteNumber(options.expirationDays) && options.expirationDays >= 1 ? options.expirationDays : DEFAULT_CACHE_EXPIRATION_IN_DAYS;
|
|
27
26
|
const expirationTimestamp = currentTimestamp - MILLIS_IN_A_DAY * cacheExpirationInDays;
|
|
@@ -33,12 +32,12 @@ function validateExpiration(options: SplitIO.InLocalStorageOptions, storage: Sto
|
|
|
33
32
|
|
|
34
33
|
// Check hash
|
|
35
34
|
const storageHashKey = keys.buildHashKey();
|
|
36
|
-
const storageHash =
|
|
35
|
+
const storageHash = localStorage.getItem(storageHashKey);
|
|
37
36
|
const currentStorageHash = getStorageHash(settings);
|
|
38
37
|
|
|
39
38
|
if (storageHash !== currentStorageHash) {
|
|
40
39
|
try {
|
|
41
|
-
|
|
40
|
+
localStorage.setItem(storageHashKey, currentStorageHash);
|
|
42
41
|
} catch (e) {
|
|
43
42
|
log.error(LOG_PREFIX + e);
|
|
44
43
|
}
|
|
@@ -51,7 +50,7 @@ function validateExpiration(options: SplitIO.InLocalStorageOptions, storage: Sto
|
|
|
51
50
|
|
|
52
51
|
// Clear on init
|
|
53
52
|
if (options.clearOnInit) {
|
|
54
|
-
const lastClearTimestamp = parseInt(
|
|
53
|
+
const lastClearTimestamp = parseInt(localStorage.getItem(keys.buildLastClear()) as string, 10);
|
|
55
54
|
|
|
56
55
|
if (isNaNNumber(lastClearTimestamp) || lastClearTimestamp < currentTimestamp - MILLIS_IN_A_DAY) {
|
|
57
56
|
log.info(LOG_PREFIX + 'clearOnInit was set and cache was not cleared in the last 24 hours. Cleaning up cache');
|
|
@@ -68,32 +67,27 @@ function validateExpiration(options: SplitIO.InLocalStorageOptions, storage: Sto
|
|
|
68
67
|
*
|
|
69
68
|
* @returns `true` if cache is ready to be used, `false` otherwise (cache was cleared or there is no cache)
|
|
70
69
|
*/
|
|
71
|
-
export function validateCache(options: SplitIO.InLocalStorageOptions,
|
|
70
|
+
export function validateCache(options: SplitIO.InLocalStorageOptions, settings: ISettings, keys: KeyBuilderCS, splits: SplitsCacheInLocal, rbSegments: RBSegmentsCacheInLocal, segments: MySegmentsCacheInLocal, largeSegments: MySegmentsCacheInLocal): boolean {
|
|
72
71
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
const isThereCache = splits.getChangeNumber() > -1;
|
|
72
|
+
const currentTimestamp = Date.now();
|
|
73
|
+
const isThereCache = splits.getChangeNumber() > -1;
|
|
76
74
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
75
|
+
if (validateExpiration(options, settings, keys, currentTimestamp, isThereCache)) {
|
|
76
|
+
splits.clear();
|
|
77
|
+
rbSegments.clear();
|
|
78
|
+
segments.clear();
|
|
79
|
+
largeSegments.clear();
|
|
82
80
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// Persist clear
|
|
91
|
-
if (storage.save) storage.save();
|
|
92
|
-
|
|
93
|
-
return false;
|
|
81
|
+
// Update last clear timestamp
|
|
82
|
+
try {
|
|
83
|
+
localStorage.setItem(keys.buildLastClear(), currentTimestamp + '');
|
|
84
|
+
} catch (e) {
|
|
85
|
+
settings.log.error(LOG_PREFIX + e);
|
|
94
86
|
}
|
|
95
87
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Check if ready from cache
|
|
92
|
+
return isThereCache;
|
|
99
93
|
}
|
|
@@ -7,6 +7,8 @@ import { ImpressionCountsCacheInMemory } from './ImpressionCountsCacheInMemory';
|
|
|
7
7
|
import { LOCALHOST_MODE, STORAGE_MEMORY } from '../../utils/constants';
|
|
8
8
|
import { shouldRecordTelemetry, TelemetryCacheInMemory } from './TelemetryCacheInMemory';
|
|
9
9
|
import { UniqueKeysCacheInMemoryCS } from './UniqueKeysCacheInMemoryCS';
|
|
10
|
+
import { getMatching } from '../../utils/key';
|
|
11
|
+
import { setCache } from '../dataLoader';
|
|
10
12
|
import { RBSegmentsCacheInMemory } from './RBSegmentsCacheInMemory';
|
|
11
13
|
|
|
12
14
|
/**
|
|
@@ -15,7 +17,9 @@ import { RBSegmentsCacheInMemory } from './RBSegmentsCacheInMemory';
|
|
|
15
17
|
* @param params - parameters required by EventsCacheSync
|
|
16
18
|
*/
|
|
17
19
|
export function InMemoryStorageCSFactory(params: IStorageFactoryParams): IStorageSync {
|
|
18
|
-
const { settings: { scheduler: { impressionsQueueSize, eventsQueueSize }, sync: { __splitFiltersValidation } } } = params;
|
|
20
|
+
const { settings: { log, scheduler: { impressionsQueueSize, eventsQueueSize }, sync: { __splitFiltersValidation }, preloadedData }, onReadyFromCacheCb } = params;
|
|
21
|
+
|
|
22
|
+
const storages: Record<string, IStorageSync> = {};
|
|
19
23
|
|
|
20
24
|
const splits = new SplitsCacheInMemory(__splitFiltersValidation);
|
|
21
25
|
const rbSegments = new RBSegmentsCacheInMemory();
|
|
@@ -36,20 +40,31 @@ export function InMemoryStorageCSFactory(params: IStorageFactoryParams): IStorag
|
|
|
36
40
|
destroy() { },
|
|
37
41
|
|
|
38
42
|
// When using shared instantiation with MEMORY we reuse everything but segments (they are unique per key)
|
|
39
|
-
shared() {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
43
|
+
shared(matchingKey: string) {
|
|
44
|
+
if (!storages[matchingKey]) {
|
|
45
|
+
const segments = new MySegmentsCacheInMemory();
|
|
46
|
+
const largeSegments = new MySegmentsCacheInMemory();
|
|
47
|
+
|
|
48
|
+
if (preloadedData) {
|
|
49
|
+
setCache(log, preloadedData, { segments, largeSegments }, matchingKey);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
storages[matchingKey] = {
|
|
53
|
+
splits: this.splits,
|
|
54
|
+
rbSegments: this.rbSegments,
|
|
55
|
+
segments,
|
|
56
|
+
largeSegments,
|
|
57
|
+
impressions: this.impressions,
|
|
58
|
+
impressionCounts: this.impressionCounts,
|
|
59
|
+
events: this.events,
|
|
60
|
+
telemetry: this.telemetry,
|
|
61
|
+
uniqueKeys: this.uniqueKeys,
|
|
62
|
+
|
|
63
|
+
destroy() { }
|
|
64
|
+
};
|
|
65
|
+
}
|
|
50
66
|
|
|
51
|
-
|
|
52
|
-
};
|
|
67
|
+
return storages[matchingKey];
|
|
53
68
|
},
|
|
54
69
|
};
|
|
55
70
|
|
|
@@ -63,6 +78,14 @@ export function InMemoryStorageCSFactory(params: IStorageFactoryParams): IStorag
|
|
|
63
78
|
storage.uniqueKeys.track = noopTrack;
|
|
64
79
|
}
|
|
65
80
|
|
|
81
|
+
const matchingKey = getMatching(params.settings.core.key);
|
|
82
|
+
storages[matchingKey] = storage;
|
|
83
|
+
|
|
84
|
+
if (preloadedData) {
|
|
85
|
+
setCache(log, preloadedData, storage, matchingKey);
|
|
86
|
+
if (splits.getChangeNumber() > -1) onReadyFromCacheCb();
|
|
87
|
+
}
|
|
88
|
+
|
|
66
89
|
return storage;
|
|
67
90
|
}
|
|
68
91
|
|
|
@@ -51,6 +51,10 @@ export class RBSegmentsCacheInMemory implements IRBSegmentsCacheSync {
|
|
|
51
51
|
return this.cache[name] || null;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
getAll(): IRBSegment[] {
|
|
55
|
+
return this.getNames().map(key => this.get(key)!);
|
|
56
|
+
}
|
|
57
|
+
|
|
54
58
|
contains(names: Set<string>): boolean {
|
|
55
59
|
const namesArray = setToArray(names);
|
|
56
60
|
const namesInStorage = this.getNames();
|
package/src/storages/types.ts
CHANGED
|
@@ -4,23 +4,6 @@ import { MySegmentsData } from '../sync/polling/types';
|
|
|
4
4
|
import { EventDataType, HttpErrors, HttpLatencies, ImpressionDataType, LastSync, Method, MethodExceptions, MethodLatencies, MultiMethodExceptions, MultiMethodLatencies, MultiConfigs, OperationType, StoredEventWithMetadata, StoredImpressionWithMetadata, StreamingEvent, UniqueKeysPayloadCs, UniqueKeysPayloadSs, TelemetryUsageStatsPayload, UpdatesFromSSEEnum } from '../sync/submitters/types';
|
|
5
5
|
import { ISettings } from '../types';
|
|
6
6
|
|
|
7
|
-
/**
|
|
8
|
-
* Internal interface based on a subset of the Web Storage API interface
|
|
9
|
-
* (https://developer.mozilla.org/en-US/docs/Web/API/Storage) used by the SDK
|
|
10
|
-
*/
|
|
11
|
-
export interface StorageAdapter {
|
|
12
|
-
// Methods to support async storages
|
|
13
|
-
load?: () => Promise<void>;
|
|
14
|
-
save?: () => Promise<void>;
|
|
15
|
-
whenSaved?: () => Promise<void>;
|
|
16
|
-
// Methods based on https://developer.mozilla.org/en-US/docs/Web/API/Storage
|
|
17
|
-
readonly length: number;
|
|
18
|
-
key(index: number): string | null;
|
|
19
|
-
getItem(key: string): string | null;
|
|
20
|
-
removeItem(key: string): void;
|
|
21
|
-
setItem(key: string, value: string): void;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
7
|
/**
|
|
25
8
|
* Interface of a pluggable storage wrapper.
|
|
26
9
|
*/
|
|
@@ -252,6 +235,7 @@ export interface IRBSegmentsCacheSync extends IRBSegmentsCacheBase {
|
|
|
252
235
|
update(toAdd: IRBSegment[], toRemove: IRBSegment[], changeNumber: number): boolean,
|
|
253
236
|
get(name: string): IRBSegment | null,
|
|
254
237
|
getChangeNumber(): number,
|
|
238
|
+
getAll(): IRBSegment[],
|
|
255
239
|
clear(): void,
|
|
256
240
|
contains(names: Set<string>): boolean,
|
|
257
241
|
// Used only for smart pausing in client-side standalone. Returns true if the storage contains a RBSegment using segments or large segments matchers
|
|
@@ -482,8 +466,7 @@ export interface IStorageBase<
|
|
|
482
466
|
telemetry?: TTelemetryCache,
|
|
483
467
|
uniqueKeys: TUniqueKeysCache,
|
|
484
468
|
destroy(): void | Promise<void>,
|
|
485
|
-
shared?: (matchingKey: string, onReadyCb
|
|
486
|
-
save?: () => void | Promise<void>,
|
|
469
|
+
shared?: (matchingKey: string, onReadyCb?: (error?: any) => void) => this
|
|
487
470
|
}
|
|
488
471
|
|
|
489
472
|
export interface IStorageSync extends IStorageBase<
|
|
@@ -497,7 +480,7 @@ export interface IStorageSync extends IStorageBase<
|
|
|
497
480
|
IUniqueKeysCacheSync
|
|
498
481
|
> {
|
|
499
482
|
// Defined in client-side
|
|
500
|
-
validateCache?: () =>
|
|
483
|
+
validateCache?: () => boolean, // @TODO support async
|
|
501
484
|
largeSegments?: ISegmentsCacheSync,
|
|
502
485
|
}
|
|
503
486
|
|
|
@@ -514,8 +497,6 @@ export interface IStorageAsync extends IStorageBase<
|
|
|
514
497
|
|
|
515
498
|
/** StorageFactory */
|
|
516
499
|
|
|
517
|
-
export type DataLoader = (storage: IStorageSync, matchingKey: string) => void
|
|
518
|
-
|
|
519
500
|
export interface IStorageFactoryParams {
|
|
520
501
|
settings: ISettings,
|
|
521
502
|
/**
|
|
@@ -523,6 +504,9 @@ export interface IStorageFactoryParams {
|
|
|
523
504
|
* It is meant for emitting SDK_READY event in consumer mode, and waiting before using the storage in the synchronizer.
|
|
524
505
|
*/
|
|
525
506
|
onReadyCb: (error?: any) => void,
|
|
507
|
+
/**
|
|
508
|
+
* For emitting SDK_READY_FROM_CACHE event in consumer mode with Redis and standalone mode with preloaded data
|
|
509
|
+
*/
|
|
526
510
|
onReadyFromCacheCb: () => void,
|
|
527
511
|
}
|
|
528
512
|
|
|
@@ -59,7 +59,8 @@ export function fromObjectUpdaterFactory(
|
|
|
59
59
|
|
|
60
60
|
if (startingUp) {
|
|
61
61
|
startingUp = false;
|
|
62
|
-
|
|
62
|
+
const isCacheLoaded = storage.validateCache ? storage.validateCache() : false;
|
|
63
|
+
Promise.resolve().then(() => {
|
|
63
64
|
// Emits SDK_READY_FROM_CACHE
|
|
64
65
|
if (isCacheLoaded) readiness.splits.emit(SDK_SPLITS_CACHE_LOADED);
|
|
65
66
|
// Emits SDK_READY
|
|
@@ -51,8 +51,6 @@ export function mySegmentsUpdaterFactory(
|
|
|
51
51
|
shouldNotifyUpdate = largeSegments!.resetSegments((segmentsData as IMembershipsResponse).ls || {}) || shouldNotifyUpdate;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
if (storage.save) storage.save();
|
|
55
|
-
|
|
56
54
|
// Notify update if required
|
|
57
55
|
if (usesSegmentsSync(storage) && (shouldNotifyUpdate || readyOnAlreadyExistentState)) {
|
|
58
56
|
readyOnAlreadyExistentState = false;
|
|
@@ -117,7 +117,7 @@ export function computeMutation<T extends ISplit | IRBSegment>(rules: Array<T>,
|
|
|
117
117
|
export function splitChangesUpdaterFactory(
|
|
118
118
|
log: ILogger,
|
|
119
119
|
splitChangesFetcher: ISplitChangesFetcher,
|
|
120
|
-
storage: Pick<IStorageBase, 'splits' | 'rbSegments' | 'segments'
|
|
120
|
+
storage: Pick<IStorageBase, 'splits' | 'rbSegments' | 'segments'>,
|
|
121
121
|
splitFiltersValidation: ISplitFiltersValidation,
|
|
122
122
|
splitsEventEmitter?: ISplitsEventEmitter,
|
|
123
123
|
requestTimeoutBeforeReady: number = 0,
|
|
@@ -185,8 +185,6 @@ export function splitChangesUpdaterFactory(
|
|
|
185
185
|
// @TODO if at least 1 segment fetch fails due to 404 and other segments are updated in the storage, SDK_UPDATE is not emitted
|
|
186
186
|
segments.registerSegments(setToArray(usedSegments))
|
|
187
187
|
]).then(([ffChanged, rbsChanged]) => {
|
|
188
|
-
if (storage.save) storage.save();
|
|
189
|
-
|
|
190
188
|
if (splitsEventEmitter) {
|
|
191
189
|
// To emit SDK_SPLITS_ARRIVED for server-side SDK, we must check that all registered segments have been fetched
|
|
192
190
|
return Promise.resolve(!splitsEventEmitter.splitsArrived || ((ffChanged || rbsChanged) && (isClientSide || checkAllSegmentsExist(segments))))
|