@splitsoftware/splitio-commons 2.5.0-rc.1 → 2.5.1-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 +13 -1
- package/cjs/storages/getRolloutPlan.js +1 -1
- package/cjs/storages/inLocalStorage/MySegmentsCacheInLocal.js +16 -16
- package/cjs/storages/inLocalStorage/RBSegmentsCacheInLocal.js +17 -17
- package/cjs/storages/inLocalStorage/SplitsCacheInLocal.js +33 -37
- package/cjs/storages/inLocalStorage/index.js +31 -13
- package/cjs/storages/inLocalStorage/storageAdapter.js +54 -0
- package/cjs/storages/inLocalStorage/validateCache.js +28 -23
- package/cjs/sync/offline/syncTasks/fromObjectSyncTask.js +2 -3
- package/cjs/sync/polling/updaters/mySegmentsUpdater.js +2 -0
- package/cjs/sync/polling/updaters/splitChangesUpdater.js +2 -0
- package/cjs/sync/syncManagerOnline.js +28 -24
- package/cjs/utils/env/isLocalStorageAvailable.js +22 -1
- package/cjs/utils/settingsValidation/storage/storageCS.js +1 -1
- package/esm/storages/getRolloutPlan.js +1 -1
- package/esm/storages/inLocalStorage/MySegmentsCacheInLocal.js +16 -16
- package/esm/storages/inLocalStorage/RBSegmentsCacheInLocal.js +17 -17
- package/esm/storages/inLocalStorage/SplitsCacheInLocal.js +33 -37
- package/esm/storages/inLocalStorage/index.js +32 -14
- package/esm/storages/inLocalStorage/storageAdapter.js +50 -0
- package/esm/storages/inLocalStorage/validateCache.js +28 -23
- package/esm/sync/offline/syncTasks/fromObjectSyncTask.js +2 -3
- package/esm/sync/polling/updaters/mySegmentsUpdater.js +2 -0
- package/esm/sync/polling/updaters/splitChangesUpdater.js +2 -0
- package/esm/sync/syncManagerOnline.js +28 -24
- package/esm/utils/env/isLocalStorageAvailable.js +19 -0
- package/esm/utils/settingsValidation/storage/storageCS.js +1 -1
- package/package.json +1 -1
- package/src/storages/getRolloutPlan.ts +1 -1
- package/src/storages/inLocalStorage/MySegmentsCacheInLocal.ts +18 -17
- package/src/storages/inLocalStorage/RBSegmentsCacheInLocal.ts +19 -18
- package/src/storages/inLocalStorage/SplitsCacheInLocal.ts +34 -37
- package/src/storages/inLocalStorage/index.ts +37 -16
- package/src/storages/inLocalStorage/storageAdapter.ts +62 -0
- package/src/storages/inLocalStorage/validateCache.ts +29 -23
- package/src/storages/types.ts +19 -1
- package/src/sync/offline/syncTasks/fromObjectSyncTask.ts +1 -2
- package/src/sync/polling/updaters/mySegmentsUpdater.ts +2 -0
- package/src/sync/polling/updaters/splitChangesUpdater.ts +3 -1
- package/src/sync/syncManagerOnline.ts +27 -22
- package/src/utils/env/isLocalStorageAvailable.ts +20 -0
- package/src/utils/settingsValidation/storage/storageCS.ts +1 -1
- package/types/splitio.d.ts +38 -1
|
@@ -6,29 +6,28 @@ 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';
|
|
9
10
|
|
|
10
|
-
/**
|
|
11
|
-
* ISplitsCacheSync implementation that stores split definitions in browser LocalStorage.
|
|
12
|
-
*/
|
|
13
11
|
export class SplitsCacheInLocal extends AbstractSplitsCacheSync {
|
|
14
12
|
|
|
15
13
|
private readonly keys: KeyBuilderCS;
|
|
16
14
|
private readonly log: ILogger;
|
|
17
15
|
private readonly flagSetsFilter: string[];
|
|
18
16
|
private hasSync?: boolean;
|
|
17
|
+
private readonly storage: StorageAdapter;
|
|
19
18
|
|
|
20
|
-
constructor(settings: ISettings, keys: KeyBuilderCS) {
|
|
19
|
+
constructor(settings: ISettings, keys: KeyBuilderCS, storage: StorageAdapter) {
|
|
21
20
|
super();
|
|
22
21
|
this.keys = keys;
|
|
23
22
|
this.log = settings.log;
|
|
24
23
|
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
|
-
|
|
31
|
-
else localStorage.removeItem(key);
|
|
28
|
+
const count = toNumber(this.storage.getItem(key)) - 1;
|
|
29
|
+
if (count > 0) this.storage.setItem(key, count + '');
|
|
30
|
+
else this.storage.removeItem(key);
|
|
32
31
|
}
|
|
33
32
|
|
|
34
33
|
private _decrementCounts(split: ISplit) {
|
|
@@ -48,13 +47,11 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync {
|
|
|
48
47
|
private _incrementCounts(split: ISplit) {
|
|
49
48
|
try {
|
|
50
49
|
const ttKey = this.keys.buildTrafficTypeKey(split.trafficTypeName);
|
|
51
|
-
|
|
52
|
-
localStorage.setItem(ttKey, toNumber(localStorage.getItem(ttKey)) + 1);
|
|
50
|
+
this.storage.setItem(ttKey, (toNumber(this.storage.getItem(ttKey)) + 1) + '');
|
|
53
51
|
|
|
54
52
|
if (usesSegments(split)) {
|
|
55
53
|
const segmentsCountKey = this.keys.buildSplitsWithSegmentCountKey();
|
|
56
|
-
|
|
57
|
-
localStorage.setItem(segmentsCountKey, toNumber(localStorage.getItem(segmentsCountKey)) + 1);
|
|
54
|
+
this.storage.setItem(segmentsCountKey, (toNumber(this.storage.getItem(segmentsCountKey)) + 1) + '');
|
|
58
55
|
}
|
|
59
56
|
} catch (e) {
|
|
60
57
|
this.log.error(LOG_PREFIX + e);
|
|
@@ -68,15 +65,15 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync {
|
|
|
68
65
|
*/
|
|
69
66
|
clear() {
|
|
70
67
|
// collect item keys
|
|
71
|
-
const len =
|
|
68
|
+
const len = this.storage.length;
|
|
72
69
|
const accum = [];
|
|
73
70
|
for (let cur = 0; cur < len; cur++) {
|
|
74
|
-
const key =
|
|
71
|
+
const key = this.storage.key(cur);
|
|
75
72
|
if (key != null && this.keys.isSplitsCacheKey(key)) accum.push(key);
|
|
76
73
|
}
|
|
77
74
|
// remove items
|
|
78
75
|
accum.forEach(key => {
|
|
79
|
-
|
|
76
|
+
this.storage.removeItem(key);
|
|
80
77
|
});
|
|
81
78
|
|
|
82
79
|
this.hasSync = false;
|
|
@@ -86,15 +83,15 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync {
|
|
|
86
83
|
try {
|
|
87
84
|
const name = split.name;
|
|
88
85
|
const splitKey = this.keys.buildSplitKey(name);
|
|
89
|
-
const
|
|
90
|
-
const previousSplit =
|
|
86
|
+
const splitFromStorage = this.storage.getItem(splitKey);
|
|
87
|
+
const previousSplit = splitFromStorage ? JSON.parse(splitFromStorage) : null;
|
|
91
88
|
|
|
92
89
|
if (previousSplit) {
|
|
93
90
|
this._decrementCounts(previousSplit);
|
|
94
91
|
this.removeFromFlagSets(previousSplit.name, previousSplit.sets);
|
|
95
92
|
}
|
|
96
93
|
|
|
97
|
-
|
|
94
|
+
this.storage.setItem(splitKey, JSON.stringify(split));
|
|
98
95
|
|
|
99
96
|
this._incrementCounts(split);
|
|
100
97
|
this.addToFlagSets(split);
|
|
@@ -111,7 +108,7 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync {
|
|
|
111
108
|
const split = this.getSplit(name);
|
|
112
109
|
if (!split) return false;
|
|
113
110
|
|
|
114
|
-
|
|
111
|
+
this.storage.removeItem(this.keys.buildSplitKey(name));
|
|
115
112
|
|
|
116
113
|
this._decrementCounts(split);
|
|
117
114
|
this.removeFromFlagSets(split.name, split.sets);
|
|
@@ -124,15 +121,15 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync {
|
|
|
124
121
|
}
|
|
125
122
|
|
|
126
123
|
getSplit(name: string): ISplit | null {
|
|
127
|
-
const item =
|
|
124
|
+
const item = this.storage.getItem(this.keys.buildSplitKey(name));
|
|
128
125
|
return item && JSON.parse(item);
|
|
129
126
|
}
|
|
130
127
|
|
|
131
128
|
setChangeNumber(changeNumber: number): boolean {
|
|
132
129
|
try {
|
|
133
|
-
|
|
130
|
+
this.storage.setItem(this.keys.buildSplitsTillKey(), changeNumber + '');
|
|
134
131
|
// update "last updated" timestamp with current time
|
|
135
|
-
|
|
132
|
+
this.storage.setItem(this.keys.buildLastUpdatedKey(), Date.now() + '');
|
|
136
133
|
this.hasSync = true;
|
|
137
134
|
return true;
|
|
138
135
|
} catch (e) {
|
|
@@ -143,7 +140,7 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync {
|
|
|
143
140
|
|
|
144
141
|
getChangeNumber(): number {
|
|
145
142
|
const n = -1;
|
|
146
|
-
let value: string | number | null =
|
|
143
|
+
let value: string | number | null = this.storage.getItem(this.keys.buildSplitsTillKey());
|
|
147
144
|
|
|
148
145
|
if (value !== null) {
|
|
149
146
|
value = parseInt(value, 10);
|
|
@@ -155,13 +152,13 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync {
|
|
|
155
152
|
}
|
|
156
153
|
|
|
157
154
|
getSplitNames(): string[] {
|
|
158
|
-
const len =
|
|
155
|
+
const len = this.storage.length;
|
|
159
156
|
const accum = [];
|
|
160
157
|
|
|
161
158
|
let cur = 0;
|
|
162
159
|
|
|
163
160
|
while (cur < len) {
|
|
164
|
-
const key =
|
|
161
|
+
const key = this.storage.key(cur);
|
|
165
162
|
|
|
166
163
|
if (key != null && this.keys.isSplitKey(key)) accum.push(this.keys.extractKey(key));
|
|
167
164
|
|
|
@@ -172,7 +169,7 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync {
|
|
|
172
169
|
}
|
|
173
170
|
|
|
174
171
|
trafficTypeExists(trafficType: string): boolean {
|
|
175
|
-
const ttCount = toNumber(
|
|
172
|
+
const ttCount = toNumber(this.storage.getItem(this.keys.buildTrafficTypeKey(trafficType)));
|
|
176
173
|
return isFiniteNumber(ttCount) && ttCount > 0;
|
|
177
174
|
}
|
|
178
175
|
|
|
@@ -180,7 +177,7 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync {
|
|
|
180
177
|
// If cache hasn't been synchronized with the cloud, assume we need them.
|
|
181
178
|
if (!this.hasSync) return true;
|
|
182
179
|
|
|
183
|
-
const storedCount =
|
|
180
|
+
const storedCount = this.storage.getItem(this.keys.buildSplitsWithSegmentCountKey());
|
|
184
181
|
const splitsWithSegmentsCount = storedCount === null ? 0 : toNumber(storedCount);
|
|
185
182
|
|
|
186
183
|
return isFiniteNumber(splitsWithSegmentsCount) ?
|
|
@@ -191,9 +188,9 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync {
|
|
|
191
188
|
getNamesByFlagSets(flagSets: string[]): Set<string>[] {
|
|
192
189
|
return flagSets.map(flagSet => {
|
|
193
190
|
const flagSetKey = this.keys.buildFlagSetKey(flagSet);
|
|
194
|
-
const
|
|
191
|
+
const flagSetFromStorage = this.storage.getItem(flagSetKey);
|
|
195
192
|
|
|
196
|
-
return new Set(
|
|
193
|
+
return new Set(flagSetFromStorage ? JSON.parse(flagSetFromStorage) : []);
|
|
197
194
|
});
|
|
198
195
|
}
|
|
199
196
|
|
|
@@ -206,12 +203,12 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync {
|
|
|
206
203
|
|
|
207
204
|
const flagSetKey = this.keys.buildFlagSetKey(featureFlagSet);
|
|
208
205
|
|
|
209
|
-
const
|
|
206
|
+
const flagSetFromStorage = this.storage.getItem(flagSetKey);
|
|
210
207
|
|
|
211
|
-
const flagSetCache = new Set(
|
|
208
|
+
const flagSetCache = new Set(flagSetFromStorage ? JSON.parse(flagSetFromStorage) : []);
|
|
212
209
|
flagSetCache.add(featureFlag.name);
|
|
213
210
|
|
|
214
|
-
|
|
211
|
+
this.storage.setItem(flagSetKey, JSON.stringify(setToArray(flagSetCache)));
|
|
215
212
|
});
|
|
216
213
|
}
|
|
217
214
|
|
|
@@ -226,19 +223,19 @@ export class SplitsCacheInLocal extends AbstractSplitsCacheSync {
|
|
|
226
223
|
private removeNames(flagSetName: string, featureFlagName: string) {
|
|
227
224
|
const flagSetKey = this.keys.buildFlagSetKey(flagSetName);
|
|
228
225
|
|
|
229
|
-
const
|
|
226
|
+
const flagSetFromStorage = this.storage.getItem(flagSetKey);
|
|
230
227
|
|
|
231
|
-
if (!
|
|
228
|
+
if (!flagSetFromStorage) return;
|
|
232
229
|
|
|
233
|
-
const flagSetCache = new Set(JSON.parse(
|
|
230
|
+
const flagSetCache = new Set(JSON.parse(flagSetFromStorage));
|
|
234
231
|
flagSetCache.delete(featureFlagName);
|
|
235
232
|
|
|
236
233
|
if (flagSetCache.size === 0) {
|
|
237
|
-
|
|
234
|
+
this.storage.removeItem(flagSetKey);
|
|
238
235
|
return;
|
|
239
236
|
}
|
|
240
237
|
|
|
241
|
-
|
|
238
|
+
this.storage.setItem(flagSetKey, JSON.stringify(setToArray(flagSetCache)));
|
|
242
239
|
}
|
|
243
240
|
|
|
244
241
|
}
|
|
@@ -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 } from '../types';
|
|
4
|
+
import { IStorageFactoryParams, IStorageSync, IStorageSyncFactory, StorageAdapter } from '../types';
|
|
5
5
|
import { validatePrefix } from '../KeyBuilder';
|
|
6
6
|
import { KeyBuilderCS, myLargeSegmentsKeyBuilder } from '../KeyBuilderCS';
|
|
7
|
-
import { isLocalStorageAvailable } from '../../utils/env/isLocalStorageAvailable';
|
|
7
|
+
import { isLocalStorageAvailable, isValidStorageWrapper, isWebStorage } from '../../utils/env/isLocalStorageAvailable';
|
|
8
8
|
import { SplitsCacheInLocal } from './SplitsCacheInLocal';
|
|
9
9
|
import { RBSegmentsCacheInLocal } from './RBSegmentsCacheInLocal';
|
|
10
10
|
import { MySegmentsCacheInLocal } from './MySegmentsCacheInLocal';
|
|
@@ -15,7 +15,24 @@ 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';
|
|
18
19
|
import SplitIO from '../../../types/splitio';
|
|
20
|
+
import { storageAdapter } from './storageAdapter';
|
|
21
|
+
|
|
22
|
+
function validateStorage(log: ILogger, prefix: string, wrapper?: SplitIO.StorageWrapper): 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
|
+
}
|
|
19
36
|
|
|
20
37
|
/**
|
|
21
38
|
* InLocal storage factory for standalone client-side SplitFactory
|
|
@@ -25,21 +42,19 @@ export function InLocalStorage(options: SplitIO.InLocalStorageOptions = {}): ISt
|
|
|
25
42
|
const prefix = validatePrefix(options.prefix);
|
|
26
43
|
|
|
27
44
|
function InLocalStorageCSFactory(params: IStorageFactoryParams): IStorageSync {
|
|
45
|
+
const { settings, settings: { log, scheduler: { impressionsQueueSize, eventsQueueSize } } } = params;
|
|
28
46
|
|
|
29
|
-
|
|
30
|
-
if (!
|
|
31
|
-
params.settings.log.warn(LOG_PREFIX + 'LocalStorage API is unavailable. Falling back to default MEMORY storage');
|
|
32
|
-
return InMemoryStorageCSFactory(params);
|
|
33
|
-
}
|
|
47
|
+
const storage = validateStorage(log, prefix, options.wrapper);
|
|
48
|
+
if (!storage) return InMemoryStorageCSFactory(params);
|
|
34
49
|
|
|
35
|
-
const { settings, settings: { log, scheduler: { impressionsQueueSize, eventsQueueSize } } } = params;
|
|
36
50
|
const matchingKey = getMatching(settings.core.key);
|
|
37
51
|
const keys = new KeyBuilderCS(prefix, matchingKey);
|
|
38
52
|
|
|
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));
|
|
53
|
+
const splits = new SplitsCacheInLocal(settings, keys, storage);
|
|
54
|
+
const rbSegments = new RBSegmentsCacheInLocal(settings, keys, storage);
|
|
55
|
+
const segments = new MySegmentsCacheInLocal(log, keys, storage);
|
|
56
|
+
const largeSegments = new MySegmentsCacheInLocal(log, myLargeSegmentsKeyBuilder(prefix, matchingKey), storage);
|
|
57
|
+
let validateCachePromise: Promise<boolean> | undefined;
|
|
43
58
|
|
|
44
59
|
return {
|
|
45
60
|
splits,
|
|
@@ -53,10 +68,16 @@ export function InLocalStorage(options: SplitIO.InLocalStorageOptions = {}): ISt
|
|
|
53
68
|
uniqueKeys: new UniqueKeysCacheInMemoryCS(),
|
|
54
69
|
|
|
55
70
|
validateCache() {
|
|
56
|
-
return validateCache(options, settings, keys, splits, rbSegments, segments, largeSegments);
|
|
71
|
+
return validateCachePromise || (validateCachePromise = validateCache(options, storage, settings, keys, splits, rbSegments, segments, largeSegments));
|
|
57
72
|
},
|
|
58
73
|
|
|
59
|
-
|
|
74
|
+
save() {
|
|
75
|
+
return storage.save && storage.save();
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
destroy() {
|
|
79
|
+
return storage.whenSaved && storage.whenSaved();
|
|
80
|
+
},
|
|
60
81
|
|
|
61
82
|
// When using shared instantiation with MEMORY we reuse everything but segments (they are customer per key).
|
|
62
83
|
shared(matchingKey: string) {
|
|
@@ -64,8 +85,8 @@ export function InLocalStorage(options: SplitIO.InLocalStorageOptions = {}): ISt
|
|
|
64
85
|
return {
|
|
65
86
|
splits: this.splits,
|
|
66
87
|
rbSegments: this.rbSegments,
|
|
67
|
-
segments: new MySegmentsCacheInLocal(log, new KeyBuilderCS(prefix, matchingKey)),
|
|
68
|
-
largeSegments: new MySegmentsCacheInLocal(log, myLargeSegmentsKeyBuilder(prefix, matchingKey)),
|
|
88
|
+
segments: new MySegmentsCacheInLocal(log, new KeyBuilderCS(prefix, matchingKey), storage),
|
|
89
|
+
largeSegments: new MySegmentsCacheInLocal(log, myLargeSegmentsKeyBuilder(prefix, matchingKey), storage),
|
|
69
90
|
impressions: this.impressions,
|
|
70
91
|
impressionCounts: this.impressionCounts,
|
|
71
92
|
events: this.events,
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { ILogger } from '../../logger/types';
|
|
2
|
+
import SplitIO from '../../../types/splitio';
|
|
3
|
+
import { LOG_PREFIX } from './constants';
|
|
4
|
+
import { StorageAdapter } from '../types';
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
export function storageAdapter(log: ILogger, prefix: string, wrapper: SplitIO.StorageWrapper): Required<StorageAdapter> {
|
|
8
|
+
let keys: string[] = [];
|
|
9
|
+
let cache: Record<string, string> = {};
|
|
10
|
+
|
|
11
|
+
let loadPromise: Promise<void> | undefined;
|
|
12
|
+
let savePromise = Promise.resolve();
|
|
13
|
+
|
|
14
|
+
return {
|
|
15
|
+
load() {
|
|
16
|
+
return loadPromise || (loadPromise = Promise.resolve().then(() => {
|
|
17
|
+
return wrapper.getItem(prefix);
|
|
18
|
+
}).then((storedCache) => {
|
|
19
|
+
cache = JSON.parse(storedCache || '{}');
|
|
20
|
+
keys = Object.keys(cache);
|
|
21
|
+
}).catch((e) => {
|
|
22
|
+
log.error(LOG_PREFIX + 'Rejected promise calling wrapper `getItem` method, with error: ' + e);
|
|
23
|
+
}));
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
save() {
|
|
27
|
+
return savePromise = savePromise.then(() => {
|
|
28
|
+
return Promise.resolve(wrapper.setItem(prefix, JSON.stringify(cache)));
|
|
29
|
+
}).catch((e) => {
|
|
30
|
+
log.error(LOG_PREFIX + 'Rejected promise calling wrapper `setItem` method, with error: ' + e);
|
|
31
|
+
});
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
whenSaved() {
|
|
35
|
+
return savePromise;
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
get length() {
|
|
39
|
+
return keys.length;
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
getItem(key: string) {
|
|
43
|
+
return cache[key] || null;
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
key(index: number) {
|
|
47
|
+
return keys[index] || null;
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
removeItem(key: string) {
|
|
51
|
+
const index = keys.indexOf(key);
|
|
52
|
+
if (index === -1) return;
|
|
53
|
+
keys.splice(index, 1);
|
|
54
|
+
delete cache[key];
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
setItem(key: string, value: string) {
|
|
58
|
+
if (keys.indexOf(key) === -1) keys.push(key);
|
|
59
|
+
cache[key] = value;
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
}
|
|
@@ -7,6 +7,7 @@ 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';
|
|
10
11
|
|
|
11
12
|
const DEFAULT_CACHE_EXPIRATION_IN_DAYS = 10;
|
|
12
13
|
const MILLIS_IN_A_DAY = 86400000;
|
|
@@ -16,11 +17,11 @@ const MILLIS_IN_A_DAY = 86400000;
|
|
|
16
17
|
*
|
|
17
18
|
* @returns `true` if cache should be cleared, `false` otherwise
|
|
18
19
|
*/
|
|
19
|
-
function validateExpiration(options: SplitIO.InLocalStorageOptions, settings: ISettings, keys: KeyBuilderCS, currentTimestamp: number, isThereCache: boolean) {
|
|
20
|
+
function validateExpiration(options: SplitIO.InLocalStorageOptions, storage: StorageAdapter, settings: ISettings, keys: KeyBuilderCS, currentTimestamp: number, isThereCache: boolean) {
|
|
20
21
|
const { log, initialRolloutPlan } = settings;
|
|
21
22
|
|
|
22
23
|
// Check expiration
|
|
23
|
-
const lastUpdatedTimestamp = parseInt(
|
|
24
|
+
const lastUpdatedTimestamp = parseInt(storage.getItem(keys.buildLastUpdatedKey()) as string, 10);
|
|
24
25
|
if (!isNaNNumber(lastUpdatedTimestamp)) {
|
|
25
26
|
const cacheExpirationInDays = isFiniteNumber(options.expirationDays) && options.expirationDays >= 1 ? options.expirationDays : DEFAULT_CACHE_EXPIRATION_IN_DAYS;
|
|
26
27
|
const expirationTimestamp = currentTimestamp - MILLIS_IN_A_DAY * cacheExpirationInDays;
|
|
@@ -32,12 +33,12 @@ function validateExpiration(options: SplitIO.InLocalStorageOptions, settings: IS
|
|
|
32
33
|
|
|
33
34
|
// Check hash
|
|
34
35
|
const storageHashKey = keys.buildHashKey();
|
|
35
|
-
const storageHash =
|
|
36
|
+
const storageHash = storage.getItem(storageHashKey);
|
|
36
37
|
const currentStorageHash = getStorageHash(settings);
|
|
37
38
|
|
|
38
39
|
if (storageHash !== currentStorageHash) {
|
|
39
40
|
try {
|
|
40
|
-
|
|
41
|
+
storage.setItem(storageHashKey, currentStorageHash);
|
|
41
42
|
} catch (e) {
|
|
42
43
|
log.error(LOG_PREFIX + e);
|
|
43
44
|
}
|
|
@@ -50,7 +51,7 @@ function validateExpiration(options: SplitIO.InLocalStorageOptions, settings: IS
|
|
|
50
51
|
|
|
51
52
|
// Clear on init
|
|
52
53
|
if (options.clearOnInit) {
|
|
53
|
-
const lastClearTimestamp = parseInt(
|
|
54
|
+
const lastClearTimestamp = parseInt(storage.getItem(keys.buildLastClear()) as string, 10);
|
|
54
55
|
|
|
55
56
|
if (isNaNNumber(lastClearTimestamp) || lastClearTimestamp < currentTimestamp - MILLIS_IN_A_DAY) {
|
|
56
57
|
log.info(LOG_PREFIX + 'clearOnInit was set and cache was not cleared in the last 24 hours. Cleaning up cache');
|
|
@@ -67,27 +68,32 @@ function validateExpiration(options: SplitIO.InLocalStorageOptions, settings: IS
|
|
|
67
68
|
*
|
|
68
69
|
* @returns `true` if cache is ready to be used, `false` otherwise (cache was cleared or there is no cache)
|
|
69
70
|
*/
|
|
70
|
-
export function validateCache(options: SplitIO.InLocalStorageOptions, settings: ISettings, keys: KeyBuilderCS, splits: SplitsCacheInLocal, rbSegments: RBSegmentsCacheInLocal, segments: MySegmentsCacheInLocal, largeSegments: MySegmentsCacheInLocal): boolean {
|
|
71
|
+
export function validateCache(options: SplitIO.InLocalStorageOptions, storage: StorageAdapter, settings: ISettings, keys: KeyBuilderCS, splits: SplitsCacheInLocal, rbSegments: RBSegmentsCacheInLocal, segments: MySegmentsCacheInLocal, largeSegments: MySegmentsCacheInLocal): Promise<boolean> {
|
|
71
72
|
|
|
72
|
-
|
|
73
|
-
|
|
73
|
+
return Promise.resolve(storage.load && storage.load()).then(() => {
|
|
74
|
+
const currentTimestamp = Date.now();
|
|
75
|
+
const isThereCache = splits.getChangeNumber() > -1;
|
|
74
76
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
77
|
+
if (validateExpiration(options, storage, settings, keys, currentTimestamp, isThereCache)) {
|
|
78
|
+
splits.clear();
|
|
79
|
+
rbSegments.clear();
|
|
80
|
+
segments.clear();
|
|
81
|
+
largeSegments.clear();
|
|
80
82
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
83
|
+
// Update last clear timestamp
|
|
84
|
+
try {
|
|
85
|
+
storage.setItem(keys.buildLastClear(), currentTimestamp + '');
|
|
86
|
+
} catch (e) {
|
|
87
|
+
settings.log.error(LOG_PREFIX + e);
|
|
88
|
+
}
|
|
87
89
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
+
// Persist clear
|
|
91
|
+
if (storage.save) storage.save();
|
|
92
|
+
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
90
95
|
|
|
91
|
-
|
|
92
|
-
|
|
96
|
+
// Check if ready from cache
|
|
97
|
+
return isThereCache;
|
|
98
|
+
});
|
|
93
99
|
}
|
package/src/storages/types.ts
CHANGED
|
@@ -4,6 +4,23 @@ 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
|
+
|
|
7
24
|
/**
|
|
8
25
|
* Interface of a pluggable storage wrapper.
|
|
9
26
|
*/
|
|
@@ -467,6 +484,7 @@ export interface IStorageBase<
|
|
|
467
484
|
uniqueKeys: TUniqueKeysCache,
|
|
468
485
|
destroy(): void | Promise<void>,
|
|
469
486
|
shared?: (matchingKey: string, onReadyCb?: (error?: any) => void) => this
|
|
487
|
+
save?: () => void | Promise<void>,
|
|
470
488
|
}
|
|
471
489
|
|
|
472
490
|
export interface IStorageSync extends IStorageBase<
|
|
@@ -480,7 +498,7 @@ export interface IStorageSync extends IStorageBase<
|
|
|
480
498
|
IUniqueKeysCacheSync
|
|
481
499
|
> {
|
|
482
500
|
// Defined in client-side
|
|
483
|
-
validateCache?: () => boolean
|
|
501
|
+
validateCache?: () => Promise<boolean>,
|
|
484
502
|
largeSegments?: ISegmentsCacheSync,
|
|
485
503
|
}
|
|
486
504
|
|
|
@@ -59,8 +59,7 @@ export function fromObjectUpdaterFactory(
|
|
|
59
59
|
|
|
60
60
|
if (startingUp) {
|
|
61
61
|
startingUp = false;
|
|
62
|
-
|
|
63
|
-
Promise.resolve().then(() => {
|
|
62
|
+
Promise.resolve(storage.validateCache ? storage.validateCache() : false).then((isCacheLoaded) => {
|
|
64
63
|
// Emits SDK_READY_FROM_CACHE
|
|
65
64
|
if (isCacheLoaded) readiness.splits.emit(SDK_SPLITS_CACHE_LOADED);
|
|
66
65
|
// Emits SDK_READY
|
|
@@ -51,6 +51,8 @@ export function mySegmentsUpdaterFactory(
|
|
|
51
51
|
shouldNotifyUpdate = largeSegments!.resetSegments((segmentsData as IMembershipsResponse).ls || {}) || shouldNotifyUpdate;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
if (storage.save) storage.save();
|
|
55
|
+
|
|
54
56
|
// Notify update if required
|
|
55
57
|
if (usesSegmentsSync(storage) && (shouldNotifyUpdate || readyOnAlreadyExistentState)) {
|
|
56
58
|
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' | 'save'>,
|
|
121
121
|
splitFiltersValidation: ISplitFiltersValidation,
|
|
122
122
|
splitsEventEmitter?: ISplitsEventEmitter,
|
|
123
123
|
requestTimeoutBeforeReady: number = 0,
|
|
@@ -185,6 +185,8 @@ 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
|
+
|
|
188
190
|
if (splitsEventEmitter) {
|
|
189
191
|
// To emit SDK_SPLITS_ARRIVED for server-side SDK, we must check that all registered segments have been fetched
|
|
190
192
|
return Promise.resolve(!splitsEventEmitter.splitsArrived || ((ffChanged || rbsChanged) && (isClientSide || checkAllSegmentsExist(segments))))
|
|
@@ -89,36 +89,41 @@ export function syncManagerOnlineFactory(
|
|
|
89
89
|
start() {
|
|
90
90
|
running = true;
|
|
91
91
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
if (isCacheLoaded) Promise.resolve().then(() => { readiness.splits.emit(SDK_SPLITS_CACHE_LOADED); });
|
|
95
|
-
}
|
|
92
|
+
// @TODO once event, impression and telemetry storages support persistence, call when `validateCache` promise is resolved
|
|
93
|
+
submitterManager.start(!isConsentGranted(settings));
|
|
96
94
|
|
|
97
|
-
|
|
98
|
-
|
|
95
|
+
return Promise.resolve(storage.validateCache ? storage.validateCache() : false).then((isCacheLoaded) => {
|
|
96
|
+
if (!running) return;
|
|
99
97
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
if (
|
|
103
|
-
|
|
98
|
+
if (startFirstTime) {
|
|
99
|
+
// Emits SDK_READY_FROM_CACHE
|
|
100
|
+
if (isCacheLoaded) readiness.splits.emit(SDK_SPLITS_CACHE_LOADED);
|
|
101
|
+
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// start syncing splits and segments
|
|
105
|
+
if (pollingManager) {
|
|
106
|
+
|
|
107
|
+
// If synchronization is disabled pushManager and pollingManager should not start
|
|
108
|
+
if (syncEnabled) {
|
|
109
|
+
if (pushManager) {
|
|
110
|
+
// Doesn't call `syncAll` when the syncManager is resuming
|
|
111
|
+
if (startFirstTime) {
|
|
112
|
+
pollingManager.syncAll();
|
|
113
|
+
}
|
|
114
|
+
pushManager.start();
|
|
115
|
+
} else {
|
|
116
|
+
pollingManager.start();
|
|
117
|
+
}
|
|
118
|
+
} else {
|
|
104
119
|
if (startFirstTime) {
|
|
105
120
|
pollingManager.syncAll();
|
|
106
121
|
}
|
|
107
|
-
pushManager.start();
|
|
108
|
-
} else {
|
|
109
|
-
pollingManager.start();
|
|
110
|
-
}
|
|
111
|
-
} else {
|
|
112
|
-
if (startFirstTime) {
|
|
113
|
-
pollingManager.syncAll();
|
|
114
122
|
}
|
|
115
123
|
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// start periodic data recording (events, impressions, telemetry).
|
|
119
|
-
submitterManager.start(!isConsentGranted(settings));
|
|
120
124
|
|
|
121
|
-
|
|
125
|
+
startFirstTime = false;
|
|
126
|
+
});
|
|
122
127
|
},
|
|
123
128
|
|
|
124
129
|
/**
|
|
@@ -9,3 +9,23 @@ export function isLocalStorageAvailable(): boolean {
|
|
|
9
9
|
return false;
|
|
10
10
|
}
|
|
11
11
|
}
|
|
12
|
+
|
|
13
|
+
export function isValidStorageWrapper(wrapper: any): boolean {
|
|
14
|
+
return wrapper !== null &&
|
|
15
|
+
typeof wrapper === 'object' &&
|
|
16
|
+
typeof wrapper.setItem === 'function' &&
|
|
17
|
+
typeof wrapper.getItem === 'function' &&
|
|
18
|
+
typeof wrapper.removeItem === 'function';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function isWebStorage(wrapper: any): boolean {
|
|
22
|
+
if (typeof wrapper.length === 'number') {
|
|
23
|
+
try {
|
|
24
|
+
wrapper.key(0);
|
|
25
|
+
return true;
|
|
26
|
+
} catch (e) {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
@@ -8,7 +8,7 @@ import { IStorageFactoryParams, IStorageSync } from '../../../storages/types';
|
|
|
8
8
|
|
|
9
9
|
export function __InLocalStorageMockFactory(params: IStorageFactoryParams): IStorageSync {
|
|
10
10
|
const result = InMemoryStorageCSFactory(params);
|
|
11
|
-
result.validateCache = () => true; // to emit SDK_READY_FROM_CACHE
|
|
11
|
+
result.validateCache = () => Promise.resolve(true); // to emit SDK_READY_FROM_CACHE
|
|
12
12
|
return result;
|
|
13
13
|
}
|
|
14
14
|
__InLocalStorageMockFactory.type = STORAGE_MEMORY;
|