@splitsoftware/splitio-commons 2.4.2-rc.1 → 2.4.2-rc.3
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/logger/messages/error.js +1 -1
- package/cjs/sdkClient/sdkClient.js +0 -1
- package/cjs/sdkFactory/index.js +3 -10
- package/cjs/services/splitHttpClient.js +1 -1
- package/cjs/storages/AbstractSplitsCacheSync.js +5 -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/pollingManagerCS.js +5 -4
- package/cjs/sync/polling/updaters/mySegmentsUpdater.js +5 -2
- package/cjs/sync/polling/updaters/splitChangesUpdater.js +3 -1
- package/cjs/sync/syncManagerOnline.js +31 -26
- package/cjs/trackers/impressionsTracker.js +4 -4
- package/cjs/utils/env/isLocalStorageAvailable.js +28 -5
- package/cjs/utils/settingsValidation/splitFilters.js +0 -6
- package/cjs/utils/settingsValidation/storage/storageCS.js +1 -1
- package/esm/logger/messages/error.js +1 -1
- package/esm/sdkClient/sdkClient.js +0 -1
- package/esm/sdkFactory/index.js +3 -10
- package/esm/services/splitHttpClient.js +1 -1
- package/esm/storages/AbstractSplitsCacheSync.js +3 -0
- 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/pollingManagerCS.js +5 -4
- package/esm/sync/polling/updaters/mySegmentsUpdater.js +5 -2
- package/esm/sync/polling/updaters/splitChangesUpdater.js +3 -1
- package/esm/sync/syncManagerOnline.js +31 -26
- package/esm/trackers/impressionsTracker.js +4 -4
- package/esm/utils/env/isLocalStorageAvailable.js +24 -3
- package/esm/utils/settingsValidation/splitFilters.js +0 -6
- package/esm/utils/settingsValidation/storage/storageCS.js +1 -1
- package/package.json +1 -1
- package/src/logger/messages/error.ts +1 -1
- package/src/sdkClient/sdkClient.ts +0 -1
- package/src/sdkFactory/index.ts +3 -13
- package/src/services/splitHttpClient.ts +1 -1
- package/src/storages/AbstractSplitsCacheSync.ts +5 -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/pollingManagerCS.ts +5 -4
- package/src/sync/polling/updaters/mySegmentsUpdater.ts +5 -2
- package/src/sync/polling/updaters/splitChangesUpdater.ts +4 -2
- package/src/sync/syncManagerOnline.ts +30 -24
- package/src/trackers/impressionsTracker.ts +3 -3
- package/src/utils/env/isLocalStorageAvailable.ts +24 -3
- package/src/utils/settingsValidation/splitFilters.ts +0 -6
- package/src/utils/settingsValidation/storage/storageCS.ts +1 -1
- package/types/splitio.d.ts +72 -16
|
@@ -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.SyncStorageWrapper | SplitIO.AsyncStorageWrapper): 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 } = 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
|
*/
|
|
@@ -466,6 +483,7 @@ export interface IStorageBase<
|
|
|
466
483
|
uniqueKeys: TUniqueKeysCache,
|
|
467
484
|
destroy(): void | Promise<void>,
|
|
468
485
|
shared?: (matchingKey: string, onReadyCb: (error?: any) => void) => this
|
|
486
|
+
save?: () => void | Promise<void>,
|
|
469
487
|
}
|
|
470
488
|
|
|
471
489
|
export interface IStorageSync extends IStorageBase<
|
|
@@ -479,7 +497,7 @@ export interface IStorageSync extends IStorageBase<
|
|
|
479
497
|
IUniqueKeysCacheSync
|
|
480
498
|
> {
|
|
481
499
|
// Defined in client-side
|
|
482
|
-
validateCache?: () => boolean
|
|
500
|
+
validateCache?: () => Promise<boolean>,
|
|
483
501
|
largeSegments?: ISegmentsCacheSync,
|
|
484
502
|
}
|
|
485
503
|
|
|
@@ -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
|
|
@@ -8,6 +8,7 @@ import { getMatching } from '../../utils/key';
|
|
|
8
8
|
import { SDK_SPLITS_ARRIVED, SDK_SEGMENTS_ARRIVED } from '../../readiness/constants';
|
|
9
9
|
import { POLLING_SMART_PAUSING, POLLING_START, POLLING_STOP } from '../../logger/constants';
|
|
10
10
|
import { ISdkFactoryContextSync } from '../../sdkFactory/types';
|
|
11
|
+
import { usesSegmentsSync } from '../../storages/AbstractSplitsCacheSync';
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* Expose start / stop mechanism for polling data from services.
|
|
@@ -43,7 +44,7 @@ export function pollingManagerCSFactory(
|
|
|
43
44
|
// smart pausing
|
|
44
45
|
readiness.splits.on(SDK_SPLITS_ARRIVED, () => {
|
|
45
46
|
if (!splitsSyncTask.isRunning()) return; // noop if not doing polling
|
|
46
|
-
const usingSegments =
|
|
47
|
+
const usingSegments = usesSegmentsSync(storage);
|
|
47
48
|
if (usingSegments !== mySegmentsSyncTask.isRunning()) {
|
|
48
49
|
log.info(POLLING_SMART_PAUSING, [usingSegments ? 'ON' : 'OFF']);
|
|
49
50
|
if (usingSegments) {
|
|
@@ -59,9 +60,9 @@ export function pollingManagerCSFactory(
|
|
|
59
60
|
|
|
60
61
|
// smart ready
|
|
61
62
|
function smartReady() {
|
|
62
|
-
if (!readiness.isReady() && !
|
|
63
|
+
if (!readiness.isReady() && !usesSegmentsSync(storage)) readiness.segments.emit(SDK_SEGMENTS_ARRIVED);
|
|
63
64
|
}
|
|
64
|
-
if (!
|
|
65
|
+
if (!usesSegmentsSync(storage)) setTimeout(smartReady, 0);
|
|
65
66
|
else readiness.splits.once(SDK_SPLITS_ARRIVED, smartReady);
|
|
66
67
|
|
|
67
68
|
mySegmentsSyncTasks[matchingKey] = mySegmentsSyncTask;
|
|
@@ -77,7 +78,7 @@ export function pollingManagerCSFactory(
|
|
|
77
78
|
log.info(POLLING_START);
|
|
78
79
|
|
|
79
80
|
splitsSyncTask.start();
|
|
80
|
-
if (
|
|
81
|
+
if (usesSegmentsSync(storage)) startMySegmentsSyncTasks();
|
|
81
82
|
},
|
|
82
83
|
|
|
83
84
|
// Stop periodic fetching (polling)
|
|
@@ -8,6 +8,7 @@ import { SYNC_MYSEGMENTS_FETCH_RETRY } from '../../../logger/constants';
|
|
|
8
8
|
import { MySegmentsData } from '../types';
|
|
9
9
|
import { IMembershipsResponse } from '../../../dtos/types';
|
|
10
10
|
import { MEMBERSHIPS_LS_UPDATE } from '../../streaming/constants';
|
|
11
|
+
import { usesSegmentsSync } from '../../../storages/AbstractSplitsCacheSync';
|
|
11
12
|
|
|
12
13
|
type IMySegmentsUpdater = (segmentsData?: MySegmentsData, noCache?: boolean, till?: number) => Promise<boolean>
|
|
13
14
|
|
|
@@ -27,7 +28,7 @@ export function mySegmentsUpdaterFactory(
|
|
|
27
28
|
matchingKey: string
|
|
28
29
|
): IMySegmentsUpdater {
|
|
29
30
|
|
|
30
|
-
const {
|
|
31
|
+
const { segments, largeSegments } = storage;
|
|
31
32
|
let readyOnAlreadyExistentState = true;
|
|
32
33
|
let startingUp = true;
|
|
33
34
|
|
|
@@ -50,8 +51,10 @@ export function mySegmentsUpdaterFactory(
|
|
|
50
51
|
shouldNotifyUpdate = largeSegments!.resetSegments((segmentsData as IMembershipsResponse).ls || {}) || shouldNotifyUpdate;
|
|
51
52
|
}
|
|
52
53
|
|
|
54
|
+
if (storage.save) storage.save();
|
|
55
|
+
|
|
53
56
|
// Notify update if required
|
|
54
|
-
if ((
|
|
57
|
+
if (usesSegmentsSync(storage) && (shouldNotifyUpdate || readyOnAlreadyExistentState)) {
|
|
55
58
|
readyOnAlreadyExistentState = false;
|
|
56
59
|
segmentsEventEmitter.emit(SDK_SEGMENTS_ARRIVED);
|
|
57
60
|
}
|
|
@@ -59,7 +59,7 @@ interface ISplitMutations<T extends ISplit | IRBSegment> {
|
|
|
59
59
|
|
|
60
60
|
/**
|
|
61
61
|
* If there are defined filters and one feature flag doesn't match with them, its status is changed to 'ARCHIVE' to avoid storing it
|
|
62
|
-
* If there
|
|
62
|
+
* If there is `bySet` filter, `byName` and `byPrefix` filters are ignored
|
|
63
63
|
*
|
|
64
64
|
* @param featureFlag - feature flag to be evaluated
|
|
65
65
|
* @param filters - splitFiltersValidation bySet | byName
|
|
@@ -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))))
|
|
@@ -10,6 +10,7 @@ import { isConsentGranted } from '../consent';
|
|
|
10
10
|
import { POLLING, STREAMING, SYNC_MODE_UPDATE } from '../utils/constants';
|
|
11
11
|
import { ISdkFactoryContextSync } from '../sdkFactory/types';
|
|
12
12
|
import { SDK_SPLITS_CACHE_LOADED } from '../readiness/constants';
|
|
13
|
+
import { usesSegmentsSync } from '../storages/AbstractSplitsCacheSync';
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* Online SyncManager factory.
|
|
@@ -88,36 +89,41 @@ export function syncManagerOnlineFactory(
|
|
|
88
89
|
start() {
|
|
89
90
|
running = true;
|
|
90
91
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
if (isCacheLoaded) Promise.resolve().then(() => { readiness.splits.emit(SDK_SPLITS_CACHE_LOADED); });
|
|
94
|
-
}
|
|
92
|
+
// @TODO once event, impression and telemetry storages support persistence, call when `validateCache` promise is resolved
|
|
93
|
+
submitterManager.start(!isConsentGranted(settings));
|
|
95
94
|
|
|
96
|
-
|
|
97
|
-
|
|
95
|
+
return Promise.resolve(storage.validateCache ? storage.validateCache() : false).then((isCacheLoaded) => {
|
|
96
|
+
if (!running) return;
|
|
98
97
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
if (
|
|
102
|
-
|
|
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 {
|
|
103
119
|
if (startFirstTime) {
|
|
104
120
|
pollingManager.syncAll();
|
|
105
121
|
}
|
|
106
|
-
pushManager.start();
|
|
107
|
-
} else {
|
|
108
|
-
pollingManager.start();
|
|
109
|
-
}
|
|
110
|
-
} else {
|
|
111
|
-
if (startFirstTime) {
|
|
112
|
-
pollingManager.syncAll();
|
|
113
122
|
}
|
|
114
123
|
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// start periodic data recording (events, impressions, telemetry).
|
|
118
|
-
submitterManager.start(!isConsentGranted(settings));
|
|
119
124
|
|
|
120
|
-
|
|
125
|
+
startFirstTime = false;
|
|
126
|
+
});
|
|
121
127
|
},
|
|
122
128
|
|
|
123
129
|
/**
|
|
@@ -155,14 +161,14 @@ export function syncManagerOnlineFactory(
|
|
|
155
161
|
if (pushManager) {
|
|
156
162
|
if (pollingManager.isRunning()) {
|
|
157
163
|
// if doing polling, we must start the periodic fetch of data
|
|
158
|
-
if (
|
|
164
|
+
if (usesSegmentsSync(storage)) mySegmentsSyncTask.start();
|
|
159
165
|
} else {
|
|
160
166
|
// if not polling, we must execute the sync task for the initial fetch
|
|
161
167
|
// of segments since `syncAll` was already executed when starting the main client
|
|
162
168
|
mySegmentsSyncTask.execute();
|
|
163
169
|
}
|
|
164
170
|
} else {
|
|
165
|
-
if (
|
|
171
|
+
if (usesSegmentsSync(storage)) mySegmentsSyncTask.start();
|
|
166
172
|
}
|
|
167
173
|
} else {
|
|
168
174
|
if (!readinessManager.isReady()) mySegmentsSyncTask.execute();
|
|
@@ -20,7 +20,7 @@ export function impressionsTrackerFactory(
|
|
|
20
20
|
telemetryCache?: ITelemetryCacheSync | ITelemetryCacheAsync,
|
|
21
21
|
): IImpressionsTracker {
|
|
22
22
|
|
|
23
|
-
const { log, runtime: { ip, hostname }, version } = settings;
|
|
23
|
+
const { log, impressionListener, runtime: { ip, hostname }, version } = settings;
|
|
24
24
|
|
|
25
25
|
return {
|
|
26
26
|
track(impressions: ImpressionDecorated[], attributes?: SplitIO.Attributes) {
|
|
@@ -56,7 +56,7 @@ export function impressionsTrackerFactory(
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
// @TODO next block might be handled by the integration manager. In that case, the metadata object doesn't need to be passed in the constructor
|
|
59
|
-
if (
|
|
59
|
+
if (impressionListener || integrationsManager) {
|
|
60
60
|
for (let i = 0; i < impressionsLength; i++) {
|
|
61
61
|
const impressionData: SplitIO.ImpressionData = {
|
|
62
62
|
// copy of impression, to avoid unexpected behavior if modified by integrations or impressionListener
|
|
@@ -74,7 +74,7 @@ export function impressionsTrackerFactory(
|
|
|
74
74
|
if (integrationsManager) integrationsManager.handleImpression(impressionData);
|
|
75
75
|
|
|
76
76
|
try { // @ts-ignore. An exception on the listeners should not break the SDK.
|
|
77
|
-
if (
|
|
77
|
+
if (impressionListener) impressionListener.logImpression(impressionData);
|
|
78
78
|
} catch (err) {
|
|
79
79
|
log.error(ERROR_IMPRESSIONS_LISTENER, [err]);
|
|
80
80
|
}
|
|
@@ -1,11 +1,32 @@
|
|
|
1
|
-
/* eslint-disable no-undef */
|
|
2
1
|
export function isLocalStorageAvailable(): boolean {
|
|
2
|
+
try {
|
|
3
|
+
// eslint-disable-next-line no-undef
|
|
4
|
+
return isValidStorageWrapper(localStorage);
|
|
5
|
+
} catch (e) {
|
|
6
|
+
return false;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function isValidStorageWrapper(wrapper: any): boolean {
|
|
3
11
|
var mod = '__SPLITSOFTWARE__';
|
|
4
12
|
try {
|
|
5
|
-
|
|
6
|
-
|
|
13
|
+
wrapper.setItem(mod, mod);
|
|
14
|
+
wrapper.getItem(mod);
|
|
15
|
+
wrapper.removeItem(mod);
|
|
7
16
|
return true;
|
|
8
17
|
} catch (e) {
|
|
9
18
|
return false;
|
|
10
19
|
}
|
|
11
20
|
}
|
|
21
|
+
|
|
22
|
+
export function isWebStorage(wrapper: any): boolean {
|
|
23
|
+
if (typeof wrapper.length === 'number') {
|
|
24
|
+
try {
|
|
25
|
+
wrapper.key(0);
|
|
26
|
+
return true;
|
|
27
|
+
} catch (e) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
@@ -69,12 +69,6 @@ function validateSplitFilter(log: ILogger, type: SplitIO.SplitFilterType, values
|
|
|
69
69
|
/**
|
|
70
70
|
* Returns a string representing the URL encoded query component of /splitChanges URL.
|
|
71
71
|
*
|
|
72
|
-
* The possible formats of the query string are:
|
|
73
|
-
* - null: if all filters are empty
|
|
74
|
-
* - '&names=<comma-separated-values>': if only `byPrefix` filter is undefined
|
|
75
|
-
* - '&prefixes=<comma-separated-values>': if only `byName` filter is undefined
|
|
76
|
-
* - '&names=<comma-separated-values>&prefixes=<comma-separated-values>': if no one is undefined
|
|
77
|
-
*
|
|
78
72
|
* @param groupedFilters - object of filters. Each filter must be a list of valid, unique and ordered string values.
|
|
79
73
|
* @returns null or string with the `split filter query` component of the URL.
|
|
80
74
|
*/
|
|
@@ -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;
|
package/types/splitio.d.ts
CHANGED
|
@@ -24,7 +24,11 @@ interface ISharedSettings {
|
|
|
24
24
|
sync?: {
|
|
25
25
|
/**
|
|
26
26
|
* List of feature flag filters. These filters are used to fetch a subset of the feature flag definitions in your environment, in order to reduce the delay of the SDK to be ready.
|
|
27
|
-
*
|
|
27
|
+
*
|
|
28
|
+
* NOTES:
|
|
29
|
+
* - This configuration is only meaningful when the SDK is working in `"standalone"` mode.
|
|
30
|
+
* - If `bySet` filter is provided, `byName` and `byPrefix` filters are ignored.
|
|
31
|
+
* - If both `byName` and `byPrefix` filters are provided, the intersection of the two groups of feature flags is fetched.
|
|
28
32
|
*
|
|
29
33
|
* Example:
|
|
30
34
|
* ```
|
|
@@ -66,12 +70,17 @@ interface ISharedSettings {
|
|
|
66
70
|
*
|
|
67
71
|
* @example
|
|
68
72
|
* ```
|
|
69
|
-
* const
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
73
|
+
* const factory = SplitFactory({
|
|
74
|
+
* ...
|
|
75
|
+
* sync: {
|
|
76
|
+
* getHeaderOverrides: (context) => {
|
|
77
|
+
* return {
|
|
78
|
+
* 'Authorization': context.headers['Authorization'] + ', other-value',
|
|
79
|
+
* 'custom-header': 'custom-value'
|
|
80
|
+
* };
|
|
81
|
+
* }
|
|
82
|
+
* }
|
|
83
|
+
* });
|
|
75
84
|
* ```
|
|
76
85
|
*/
|
|
77
86
|
getHeaderOverrides?: (context: { headers: Record<string, string> }) => Record<string, string>;
|
|
@@ -449,6 +458,36 @@ interface IClientSideSyncSharedSettings extends IClientSideSharedSettings, ISync
|
|
|
449
458
|
*/
|
|
450
459
|
declare namespace SplitIO {
|
|
451
460
|
|
|
461
|
+
interface SyncStorageWrapper {
|
|
462
|
+
/**
|
|
463
|
+
* Returns the value associated with the given key, or null if the key does not exist.
|
|
464
|
+
*/
|
|
465
|
+
getItem(key: string): string | null;
|
|
466
|
+
/**
|
|
467
|
+
* Sets the value for the given key, creating a new key/value pair if key does not exist.
|
|
468
|
+
*/
|
|
469
|
+
setItem(key: string, value: string): void;
|
|
470
|
+
/**
|
|
471
|
+
* Removes the key/value pair for the given key, if the key exists.
|
|
472
|
+
*/
|
|
473
|
+
removeItem(key: string): void;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
interface AsyncStorageWrapper {
|
|
477
|
+
/**
|
|
478
|
+
* Returns a promise that resolves to the value associated with the given key, or null if the key does not exist.
|
|
479
|
+
*/
|
|
480
|
+
getItem(key: string): Promise<string | null>;
|
|
481
|
+
/**
|
|
482
|
+
* Returns a promise that resolves when the value of the pair identified by key is set to value, creating a new key/value pair if key does not exist.
|
|
483
|
+
*/
|
|
484
|
+
setItem(key: string, value: string): Promise<void>;
|
|
485
|
+
/**
|
|
486
|
+
* Returns a promise that resolves when the key/value pair for the given key is removed, if the key exists.
|
|
487
|
+
*/
|
|
488
|
+
removeItem(key: string): Promise<void>;
|
|
489
|
+
}
|
|
490
|
+
|
|
452
491
|
/**
|
|
453
492
|
* EventEmitter interface based on a subset of the Node.js EventEmitter methods.
|
|
454
493
|
*/
|
|
@@ -952,7 +991,7 @@ declare namespace SplitIO {
|
|
|
952
991
|
*/
|
|
953
992
|
prefix?: string;
|
|
954
993
|
/**
|
|
955
|
-
* Number of days before cached data expires if it was not
|
|
994
|
+
* Number of days before cached data expires if it was not successfully synchronized (i.e., last SDK_READY or SDK_UPDATE event emitted). If cache expires, it is cleared on initialization.
|
|
956
995
|
*
|
|
957
996
|
* @defaultValue `10`
|
|
958
997
|
*/
|
|
@@ -963,6 +1002,12 @@ declare namespace SplitIO {
|
|
|
963
1002
|
* @defaultValue `false`
|
|
964
1003
|
*/
|
|
965
1004
|
clearOnInit?: boolean;
|
|
1005
|
+
/**
|
|
1006
|
+
* Optional storage wrapper to persist rollout plan related data. If not provided, the SDK will use the default localStorage Web API.
|
|
1007
|
+
*
|
|
1008
|
+
* @defaultValue `window.localStorage`
|
|
1009
|
+
*/
|
|
1010
|
+
wrapper?: SyncStorageWrapper | AsyncStorageWrapper;
|
|
966
1011
|
}
|
|
967
1012
|
/**
|
|
968
1013
|
* Storage for asynchronous (consumer) SDK.
|
|
@@ -1130,7 +1175,7 @@ declare namespace SplitIO {
|
|
|
1130
1175
|
*/
|
|
1131
1176
|
type: SplitFilterType;
|
|
1132
1177
|
/**
|
|
1133
|
-
* List of values: feature flag names for 'byName' filter type, and feature flag name prefixes for 'byPrefix' type.
|
|
1178
|
+
* List of values: flag set names for 'bySet' filter type, feature flag names for 'byName' filter type, and feature flag name prefixes for 'byPrefix' type.
|
|
1134
1179
|
*/
|
|
1135
1180
|
values: string[];
|
|
1136
1181
|
}
|
|
@@ -1292,7 +1337,7 @@ declare namespace SplitIO {
|
|
|
1292
1337
|
*/
|
|
1293
1338
|
prefix?: string;
|
|
1294
1339
|
/**
|
|
1295
|
-
* Optional settings for the 'LOCALSTORAGE' storage type. It specifies the number of days before cached data expires if it was not
|
|
1340
|
+
* Optional settings for the 'LOCALSTORAGE' storage type. It specifies the number of days before cached data expires if it was not successfully synchronized (i.e., last SDK_READY or SDK_UPDATE event emitted). If cache expires, it is cleared on initialization.
|
|
1296
1341
|
*
|
|
1297
1342
|
* @defaultValue `10`
|
|
1298
1343
|
*/
|
|
@@ -1303,6 +1348,12 @@ declare namespace SplitIO {
|
|
|
1303
1348
|
* @defaultValue `false`
|
|
1304
1349
|
*/
|
|
1305
1350
|
clearOnInit?: boolean;
|
|
1351
|
+
/**
|
|
1352
|
+
* Optional storage wrapper to persist rollout plan related data. If not provided, the SDK will use the default localStorage Web API.
|
|
1353
|
+
*
|
|
1354
|
+
* @defaultValue `window.localStorage`
|
|
1355
|
+
*/
|
|
1356
|
+
wrapper?: SyncStorageWrapper | AsyncStorageWrapper;
|
|
1306
1357
|
};
|
|
1307
1358
|
}
|
|
1308
1359
|
/**
|
|
@@ -1350,12 +1401,17 @@ declare namespace SplitIO {
|
|
|
1350
1401
|
*
|
|
1351
1402
|
* @example
|
|
1352
1403
|
* ```
|
|
1353
|
-
* const
|
|
1354
|
-
*
|
|
1355
|
-
*
|
|
1356
|
-
*
|
|
1357
|
-
*
|
|
1358
|
-
*
|
|
1404
|
+
* const factory = SplitFactory({
|
|
1405
|
+
* ...
|
|
1406
|
+
* sync: {
|
|
1407
|
+
* getHeaderOverrides: (context) => {
|
|
1408
|
+
* return {
|
|
1409
|
+
* 'Authorization': context.headers['Authorization'] + ', other-value',
|
|
1410
|
+
* 'custom-header': 'custom-value'
|
|
1411
|
+
* };
|
|
1412
|
+
* }
|
|
1413
|
+
* }
|
|
1414
|
+
* });
|
|
1359
1415
|
* ```
|
|
1360
1416
|
*/
|
|
1361
1417
|
getHeaderOverrides?: (context: { headers: Record<string, string> }) => Record<string, string>;
|