From df9ad6a4e8e2625902d330185ad192b114ba2ebc Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 13 Apr 2021 11:48:16 -0300 Subject: [PATCH 001/144] fixed some log messages --- src/utils/inputValidation/event.ts | 7 ++++--- src/utils/inputValidation/trafficType.ts | 7 ++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/utils/inputValidation/event.ts b/src/utils/inputValidation/event.ts index 03e79c9e..98311224 100644 --- a/src/utils/inputValidation/event.ts +++ b/src/utils/inputValidation/event.ts @@ -3,15 +3,16 @@ import { ILogger } from '../../logger/types'; import { isString } from '../lang'; const EVENT_TYPE_REGEX = /^[a-zA-Z0-9][-_.:a-zA-Z0-9]{0,79}$/; +const item = 'event_type'; export function validateEvent(log: ILogger, maybeEvent: any, method: string): string | false { if (maybeEvent == undefined) { // eslint-disable-line eqeqeq - log.error(ERROR_NULL, [method]); + log.error(ERROR_NULL, [method, item]); } else if (!isString(maybeEvent)) { - log.error(ERROR_INVALID, [method]); + log.error(ERROR_INVALID, [method, item]); } else { // It is a string. if (maybeEvent.length === 0) { - log.error(ERROR_EMPTY, [method]); + log.error(ERROR_EMPTY, [method, item]); } else if (!EVENT_TYPE_REGEX.test(maybeEvent)) { log.error(ERROR_EVENT_TYPE_FORMAT, [method, maybeEvent]); } else { diff --git a/src/utils/inputValidation/trafficType.ts b/src/utils/inputValidation/trafficType.ts index 79f1eb02..01c64dbd 100644 --- a/src/utils/inputValidation/trafficType.ts +++ b/src/utils/inputValidation/trafficType.ts @@ -3,15 +3,16 @@ import { ILogger } from '../../logger/types'; import { isString } from '../lang'; const CAPITAL_LETTERS_REGEX = /[A-Z]/; +const item = 'traffic_type'; export function validateTrafficType(log: ILogger, maybeTT: any, method: string): string | false { if (maybeTT == undefined) { // eslint-disable-line eqeqeq - log.error(ERROR_NULL, [method]); + log.error(ERROR_NULL, [method, item]); } else if (!isString(maybeTT)) { - log.error(ERROR_INVALID, [method]); + log.error(ERROR_INVALID, [method, item]); } else { if (maybeTT.length === 0) { - log.error(ERROR_EMPTY, [method]); + log.error(ERROR_EMPTY, [method, item]); } else { if (CAPITAL_LETTERS_REGEX.test(maybeTT)) { log.warn(WARN_LOWERCASE_TRAFFIC_TYPE, [method]); From 676838c29d2f47e904437bc121f950dfd4061307 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 15 Apr 2021 11:24:23 -0300 Subject: [PATCH 002/144] fixed related UTs --- src/utils/inputValidation/__tests__/event.spec.ts | 2 +- src/utils/inputValidation/__tests__/trafficType.spec.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils/inputValidation/__tests__/event.spec.ts b/src/utils/inputValidation/__tests__/event.spec.ts index 2e54136b..ce7e2cff 100644 --- a/src/utils/inputValidation/__tests__/event.spec.ts +++ b/src/utils/inputValidation/__tests__/event.spec.ts @@ -47,7 +47,7 @@ describe('INPUT VALIDATION for Event types', () => { const expectedLog = invalidEvents[i]['msg']; expect(validateEvent(loggerMock, invalidValue, 'test_method')).toBe(false); // Invalid event types should always return false. - expect(loggerMock.error).toBeCalledWith(expectedLog, expectedLog === ERROR_EVENT_TYPE_FORMAT ? ['test_method', invalidValue] : ['test_method']); // Should log the error for the invalid event type. + expect(loggerMock.error).toBeCalledWith(expectedLog, expectedLog === ERROR_EVENT_TYPE_FORMAT ? ['test_method', invalidValue] : ['test_method', 'event_type']); // Should log the error for the invalid event type. loggerMock.error.mockClear(); } diff --git a/src/utils/inputValidation/__tests__/trafficType.spec.ts b/src/utils/inputValidation/__tests__/trafficType.spec.ts index bbea3d23..7a3b6bb3 100644 --- a/src/utils/inputValidation/__tests__/trafficType.spec.ts +++ b/src/utils/inputValidation/__tests__/trafficType.spec.ts @@ -59,7 +59,7 @@ describe('INPUT VALIDATION for Traffic Types', () => { const expectedLog = invalidTrafficTypes[i]['msg']; expect(validateTrafficType(loggerMock, invalidValue, 'test_method')).toBe(false); // Invalid traffic types should always return false. - expect(loggerMock.error.mock.calls[i]).toEqual([expectedLog, ['test_method']]); // Should log the error for the invalid traffic type. + expect(loggerMock.error.mock.calls[i]).toEqual([expectedLog, ['test_method', 'traffic_type']]); // Should log the error for the invalid traffic type. } expect(loggerMock.warn).not.toBeCalled(); // It should have not logged any warnings. From 3b4553a079e158aa8596c4998b2a3915bd1c49ba Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 15 Apr 2021 16:07:11 -0300 Subject: [PATCH 003/144] simplify a few warning logs --- src/logger/messages/warn.ts | 2 +- src/utils/inputValidation/apiKey.ts | 2 +- src/utils/settingsValidation/integrations/common.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/logger/messages/warn.ts b/src/logger/messages/warn.ts index e10da2f5..78c0c46c 100644 --- a/src/logger/messages/warn.ts +++ b/src/logger/messages/warn.ts @@ -25,7 +25,7 @@ export const codesWarn: [number, string][] = codesError.concat([ [c.WARN_LOWERCASE_TRAFFIC_TYPE, '%s: traffic_type_name should be all lowercase - converting string to lowercase.'], [c.WARN_NOT_EXISTENT_TT, '%s: traffic type "%s" does not have any corresponding split in this environment, make sure you\'re tracking your events to a valid traffic type defined in the web console.'], // initialization / settings validation - [c.WARN_INTEGRATION_INVALID, c.logPrefixSettings+': %s integration %s at settings %s invalid. %s'], + [c.WARN_INTEGRATION_INVALID, c.logPrefixSettings+': %s integration item(s) at settings is invalid. %s'], [c.WARN_SPLITS_FILTER_IGNORED, c.logPrefixSettings+': split filters have been configured but will have no effect if mode is not "%s", since synchronization is being deferred to an external tool.'], [c.WARN_SPLITS_FILTER_INVALID, c.logPrefixSettings+': split filter at position %s is invalid. It must be an object with a valid filter type ("byName" or "byPrefix") and a list of "values".'], [c.WARN_SPLITS_FILTER_EMPTY, c.logPrefixSettings+': splitFilters configuration must be a non-empty array of filter objects.'], diff --git a/src/utils/inputValidation/apiKey.ts b/src/utils/inputValidation/apiKey.ts index 0b891abf..f97860cf 100644 --- a/src/utils/inputValidation/apiKey.ts +++ b/src/utils/inputValidation/apiKey.ts @@ -36,7 +36,7 @@ export function validateAndTrackApiKey(log: ILogger, maybeApiKey: any): string | log.warn(WARN_API_KEY, ['an instance of the Split factory']); } } else { - log.warn(WARN_API_KEY, [`${usedKeysMap[apiKey]} ${usedKeysMap[apiKey] === 1 ? 'factory' : 'factories'} with this API Key`]); + log.warn(WARN_API_KEY, [`${usedKeysMap[apiKey]} factory/ies with this API Key`]); usedKeysMap[apiKey]++; } } diff --git a/src/utils/settingsValidation/integrations/common.ts b/src/utils/settingsValidation/integrations/common.ts index 95fa1cd4..5ead4509 100644 --- a/src/utils/settingsValidation/integrations/common.ts +++ b/src/utils/settingsValidation/integrations/common.ts @@ -21,7 +21,7 @@ export function validateIntegrations(settings: { log: ILogger, integrations?: an // Log a warning if at least one item is invalid const invalids = integrations.length - validIntegrations.length; - if (invalids) log.warn(WARN_INTEGRATION_INVALID, [invalids, invalids === 1 ? 'item' : 'items', invalids === 1 ? 'is' : 'are', extraWarning || '']); + if (invalids) log.warn(WARN_INTEGRATION_INVALID, [invalids, extraWarning || '']); return validIntegrations; } From 11a193b9fd0342c3748aabbe96b4bcd1fc02b64c Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 15 Apr 2021 16:31:56 -0300 Subject: [PATCH 004/144] fixed related UT --- src/utils/inputValidation/__tests__/apiKey.spec.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/utils/inputValidation/__tests__/apiKey.spec.ts b/src/utils/inputValidation/__tests__/apiKey.spec.ts index c1cbb25d..76d18f1f 100644 --- a/src/utils/inputValidation/__tests__/apiKey.spec.ts +++ b/src/utils/inputValidation/__tests__/apiKey.spec.ts @@ -86,9 +86,9 @@ describe('validateAndTrackApiKey', () => { expect(validateAndTrackApiKey(loggerMock, validApiKey)).toBe(validApiKey); expect(loggerMock.warn.mock.calls).toEqual([ - [WARN_API_KEY, ['1 factory with this API Key']], - [WARN_API_KEY, ['2 factories with this API Key']], - [WARN_API_KEY, ['3 factories with this API Key']] + [WARN_API_KEY, ['1 factory/ies with this API Key']], + [WARN_API_KEY, ['2 factory/ies with this API Key']], + [WARN_API_KEY, ['3 factory/ies with this API Key']] ]); // We get a warning each time we register the same api key, with the number of instances we have. // We will release the used api key leaving only 1 "use" on the cache. @@ -100,7 +100,7 @@ describe('validateAndTrackApiKey', () => { // So we get the warning again. expect(validateAndTrackApiKey(loggerMock, validApiKey)).toBe(validApiKey); - expect(loggerMock.warn).toBeCalledWith(WARN_API_KEY, ['1 factory with this API Key']); + expect(loggerMock.warn).toBeCalledWith(WARN_API_KEY, ['1 factory/ies with this API Key']); // Leave it with 0 releaseApiKey(validApiKey); From ea734477db731fbdc43b0a0112897a70f136a6f8 Mon Sep 17 00:00:00 2001 From: Michael Trinkala Date: Tue, 20 Apr 2021 14:55:51 -0700 Subject: [PATCH 005/144] Fix the queued typo --- src/logger/messages/info.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/logger/messages/info.ts b/src/logger/messages/info.ts index 2877a9d8..5cc660de 100644 --- a/src/logger/messages/info.ts +++ b/src/logger/messages/info.ts @@ -12,7 +12,7 @@ export const codesInfo: [number, string][] = codesWarn.concat([ [c.IMPRESSION_QUEUEING, c.logPrefixImpressionsTracker +'Queueing corresponding impression.'], [c.NEW_SHARED_CLIENT, ' New shared client instance created.'], [c.NEW_FACTORY, ' New Split SDK instance created.'], - [c.EVENTS_TRACKER_SUCCESS, c.logPrefixEventsTracker + 'Successfully qeued %s'], + [c.EVENTS_TRACKER_SUCCESS, c.logPrefixEventsTracker + 'Successfully queued %s'], [c.IMPRESSIONS_TRACKER_SUCCESS, c.logPrefixImpressionsTracker + 'Successfully stored %s impression(s).'], // synchronizer From 58b36a2ac5e1f795b01f5d894c9a595a33cafcfd Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Fri, 23 Apr 2021 20:33:31 -0300 Subject: [PATCH 006/144] use setTimeout to emit SDK_READY_FROM_CACHE --- src/readiness/readinessManager.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/readiness/readinessManager.ts b/src/readiness/readinessManager.ts index 91ab79ff..3f8db413 100644 --- a/src/readiness/readinessManager.ts +++ b/src/readiness/readinessManager.ts @@ -63,8 +63,11 @@ export function readinessManagerFactory( function checkIsReadyFromCache() { if (!isReadyFromCache && splits.splitsCacheLoaded) { - isReadyFromCache = true; - gate.emit(SDK_READY_FROM_CACHE); + // Make it async, in case user callbacks throw an exception + setTimeout(() => { + isReadyFromCache = true; + gate.emit(SDK_READY_FROM_CACHE); + }, 0); } } From c59423cd859d595756b856b1f05aa26cbd9a5d3c Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Fri, 23 Apr 2021 21:34:02 -0300 Subject: [PATCH 007/144] removed SplitErrors, and handling user callback errors in another way --- src/readiness/readinessManager.ts | 14 ++++++-- src/services/__tests__/splitApi.spec.ts | 2 -- src/services/splitHttpClient.ts | 9 +++--- .../polling/fetchers/mySegmentsFetcher.ts | 4 +-- .../polling/fetchers/segmentChangesFetcher.ts | 1 - .../polling/fetchers/splitChangesFetcher.ts | 5 +-- .../polling/syncTasks/mySegmentsSyncTask.ts | 5 +-- .../polling/syncTasks/segmentsSyncTask.ts | 3 -- src/sync/polling/syncTasks/splitsSyncTask.ts | 8 +---- src/sync/streaming/AuthClient/index.ts | 1 - src/utils/lang/__tests__/errors.spec.ts | 27 ---------------- src/utils/lang/errors.ts | 32 ------------------- src/utils/promise/timeout.ts | 4 +-- 13 files changed, 22 insertions(+), 93 deletions(-) delete mode 100644 src/utils/lang/__tests__/errors.spec.ts delete mode 100644 src/utils/lang/errors.ts diff --git a/src/readiness/readinessManager.ts b/src/readiness/readinessManager.ts index 3f8db413..246bfb00 100644 --- a/src/readiness/readinessManager.ts +++ b/src/readiness/readinessManager.ts @@ -73,12 +73,22 @@ export function readinessManagerFactory( function checkIsReadyOrUpdate(diff: any) { if (isReady) { - gate.emit(SDK_UPDATE, diff); + try { + gate.emit(SDK_UPDATE, diff); + } catch (e) { + // throws user callback exceptions in next tick + setTimeout(() => { throw e; }, 0); + } } else { if (splits.splitsArrived && segments.segmentsArrived) { clearTimeout(readyTimeoutId); isReady = true; - gate.emit(SDK_READY); + try { + gate.emit(SDK_READY); + } catch (e) { + // throws user callback exceptions in next tick + setTimeout(() => { throw e; }, 0); + } } } } diff --git a/src/services/__tests__/splitApi.spec.ts b/src/services/__tests__/splitApi.spec.ts index 265f52e3..c177fe07 100644 --- a/src/services/__tests__/splitApi.spec.ts +++ b/src/services/__tests__/splitApi.spec.ts @@ -2,7 +2,6 @@ import { splitApiFactory } from '../splitApi'; import { ISettings } from '../../types'; import { settingsSplitApi } from '../../utils/settingsValidation/__tests__/settings.mocks'; -import { SplitError } from '../../utils/lang/errors'; const settingsWithRuntime = { ...settingsSplitApi, runtime: { ip: 'ip', hostname: 'hostname' } } as ISettings; @@ -61,7 +60,6 @@ describe('splitApi', () => { // Invoking any Service method, returns a rejected promise with Split error splitApi.fetchAuth().catch(error => { - expect(error).toBeInstanceOf(SplitError); expect(error.message).toBe('Global fetch API is not available.'); done(); }); diff --git a/src/services/splitHttpClient.ts b/src/services/splitHttpClient.ts index d19beb9c..65691299 100644 --- a/src/services/splitHttpClient.ts +++ b/src/services/splitHttpClient.ts @@ -1,5 +1,4 @@ import { IFetch, IRequestOptions, ISplitHttpClient } from './types'; -import { SplitError, SplitNetworkError } from '../utils/lang/errors'; import objectAssign from 'object-assign'; import { IMetadata } from '../dtos/types'; import { ILogger } from '../logger/types'; @@ -69,8 +68,10 @@ export function splitHttpClientFactory(log: ILogger, apikey: string, metadata: I log[logErrorsAsInfo ? 'info' : 'error'](ERROR_HTTP, [resp ? resp.status : 'NO_STATUS', url, msg]); } - // passes `undefined` as statusCode if not an HTTP error (resp === undefined) - throw new SplitNetworkError(msg, resp && resp.status); - }) : Promise.reject(new SplitError(messageNoFetch)); + const networkError = new Error(msg); + // @ts-ignore. Passes `undefined` as statusCode if not an HTTP error (resp === undefined) + networkError.statusCode = resp && resp.status; + throw networkError; + }) : Promise.reject(new Error(messageNoFetch)); }; } diff --git a/src/sync/polling/fetchers/mySegmentsFetcher.ts b/src/sync/polling/fetchers/mySegmentsFetcher.ts index 2ba6b7b5..a378ab23 100644 --- a/src/sync/polling/fetchers/mySegmentsFetcher.ts +++ b/src/sync/polling/fetchers/mySegmentsFetcher.ts @@ -1,4 +1,3 @@ -import { SplitError } from '../../../utils/lang/errors'; import { IFetchMySegments } from '../../../services/types'; import { IMySegmentsResponseItem } from '../../../dtos/types'; import { IMySegmentsFetcher } from './types'; @@ -20,8 +19,7 @@ export default function mySegmentsFetcherFactory(fetchMySegments: IFetchMySegmen // Extract segment names return mySegmentsPromise - // JSON parsing errors are handled as SplitErrors, to distinguish from user callback errors - .then(resp => resp.json().catch(error => { throw new SplitError(error.message); })) + .then(resp => resp.json()) .then(json => json.mySegments.map((segment: IMySegmentsResponseItem) => segment.name)); }; diff --git a/src/sync/polling/fetchers/segmentChangesFetcher.ts b/src/sync/polling/fetchers/segmentChangesFetcher.ts index db7d6c38..e95b7c37 100644 --- a/src/sync/polling/fetchers/segmentChangesFetcher.ts +++ b/src/sync/polling/fetchers/segmentChangesFetcher.ts @@ -4,7 +4,6 @@ import { ISegmentChangesFetcher } from './types'; function greedyFetch(fetchSegmentChanges: IFetchSegmentChanges, since: number, segmentName: string, noCache?: boolean): Promise { return fetchSegmentChanges(since, segmentName, noCache) - // no need to handle json parsing errors as SplitError, since errors are handled differently for segments .then(resp => resp.json()) .then((json: ISegmentChangesResponse) => { let { since, till } = json; diff --git a/src/sync/polling/fetchers/splitChangesFetcher.ts b/src/sync/polling/fetchers/splitChangesFetcher.ts index d8679c31..a68de96b 100644 --- a/src/sync/polling/fetchers/splitChangesFetcher.ts +++ b/src/sync/polling/fetchers/splitChangesFetcher.ts @@ -1,4 +1,3 @@ -import { SplitError } from '../../../utils/lang/errors'; import { IFetchSplitChanges } from '../../../services/types'; import { ISplitChangesFetcher } from './types'; @@ -18,9 +17,7 @@ export default function splitChangesFetcherFactory(fetchSplitChanges: IFetchSpli let splitsPromise = fetchSplitChanges(since, noCache); if (decorator) splitsPromise = decorator(splitsPromise); - return splitsPromise - // JSON parsing errors are handled as SplitErrors, to distinguish from user callback errors - .then(resp => resp.json().catch(error => { throw new SplitError(error.message); })); + return splitsPromise.then(resp => resp.json()); }; } diff --git a/src/sync/polling/syncTasks/mySegmentsSyncTask.ts b/src/sync/polling/syncTasks/mySegmentsSyncTask.ts index 6b19166d..0c96f626 100644 --- a/src/sync/polling/syncTasks/mySegmentsSyncTask.ts +++ b/src/sync/polling/syncTasks/mySegmentsSyncTask.ts @@ -1,7 +1,6 @@ import { IMySegmentsFetcher } from '../fetchers/types'; import { ISegmentsCacheSync, ISplitsCacheSync, IStorageSync } from '../../../storages/types'; import { IReadinessManager, ISegmentsEventEmitter } from '../../../readiness/types'; -import { SplitError } from '../../../utils/lang/errors'; import timeout from '../../../utils/promise/timeout'; import syncTaskFactory from '../../syncTask'; import { ISegmentsSyncTask } from '../types'; @@ -43,7 +42,7 @@ function mySegmentsUpdaterFactory( // mySegmentsPromise = tracker.start(tracker.TaskNames.MY_SEGMENTS_FETCH, startingUp ? metricCollectors : false, mySegmentsPromise); } - // @TODO if allowing custom storages, handle async execution and wrap errors as SplitErrors to distinguish from user callback errors + // @TODO if allowing custom storages, handle async execution function updateSegments(segments: string[]) { // Update the list of segment names available const shouldNotifyUpdate = mySegmentsCache.resetSegments(segments); @@ -69,8 +68,6 @@ function mySegmentsUpdaterFactory( }); return updaterPromise.catch(error => { - // handle user callback errors - if (!(error instanceof SplitError)) setTimeout(() => { throw error; }, 0); if (startingUp && retriesOnFailureBeforeReady > retry) { retry += 1; diff --git a/src/sync/polling/syncTasks/segmentsSyncTask.ts b/src/sync/polling/syncTasks/segmentsSyncTask.ts index 4d0d400b..1c8f2c0c 100644 --- a/src/sync/polling/syncTasks/segmentsSyncTask.ts +++ b/src/sync/polling/syncTasks/segmentsSyncTask.ts @@ -3,7 +3,6 @@ import { ISegmentsCacheSync, IStorageSync } from '../../../storages/types'; import { IReadinessManager } from '../../../readiness/types'; import { ISegmentChangesResponse } from '../../../dtos/types'; import { findIndex } from '../../../utils/lang'; -import { SplitError } from '../../../utils/lang/errors'; import syncTaskFactory from '../../syncTask'; import { ISegmentsSyncTask } from '../types'; import segmentChangesFetcherFactory from '../fetchers/segmentChangesFetcher'; @@ -91,8 +90,6 @@ function segmentChangesUpdaterFactory( if (shouldUpdateFlags.indexOf(-1) !== -1) return false; return true; }).catch(error => { - // handle user callback errors - if (!(error instanceof SplitError)) setTimeout(() => { throw error; }, 0); if (error.statusCode === 403) { // @TODO although factory status is destroyed, synchronization is not stopped diff --git a/src/sync/polling/syncTasks/splitsSyncTask.ts b/src/sync/polling/syncTasks/splitsSyncTask.ts index a69e579a..aeb23767 100644 --- a/src/sync/polling/syncTasks/splitsSyncTask.ts +++ b/src/sync/polling/syncTasks/splitsSyncTask.ts @@ -1,4 +1,3 @@ -import { SplitError } from '../../../utils/lang/errors'; import { _Set, setToArray, ISet } from '../../../utils/lang/sets'; import { ISegmentsCacheSync, ISplitsCacheSync, IStorageSync } from '../../../storages/types'; import { ISplitChangesFetcher } from '../fetchers/types'; @@ -129,7 +128,7 @@ export function splitChangesUpdaterFactory( log.debug(SYNC_SPLITS_SEGMENTS, [mutation.segments.length]); // Write into storage - // @TODO if allowing custom storages, wrap errors as SplitErrors to distinguish from user callback errors + // @TODO if allowing custom storages, handle consistency/atomicity of operations return Promise.all([ // calling first `setChangenumber` method, to perform cache flush if split filter queryString changed splitsCache.setChangeNumber(splitChanges.till), @@ -146,11 +145,6 @@ export function splitChangesUpdaterFactory( }); }) .catch(error => { - // handle user callback errors - if (!(error instanceof SplitError)) { - setTimeout(() => { throw error; }, 0); - startingUp = false; // Stop retrying. - } log.warn(SYNC_SPLITS_FETCH_FAILS, [error]); diff --git a/src/sync/streaming/AuthClient/index.ts b/src/sync/streaming/AuthClient/index.ts index 1f0192a9..8f19c34e 100644 --- a/src/sync/streaming/AuthClient/index.ts +++ b/src/sync/streaming/AuthClient/index.ts @@ -19,7 +19,6 @@ export function authenticateFactory(fetchAuth: IFetchAuth): IAuthenticate { return function authenticate(userKeys?: string[]): Promise { let authPromise = fetchAuth(userKeys); // errors handled by fetchAuth service return authPromise - // no need to handle json parsing errors as SplitError, since no user callbacks are executed after this promise is resolved .then(resp => resp.json()) .then(json => { if (json.token) { // empty token when `"pushEnabled": false` diff --git a/src/utils/lang/__tests__/errors.spec.ts b/src/utils/lang/__tests__/errors.spec.ts deleted file mode 100644 index a4a383e1..00000000 --- a/src/utils/lang/__tests__/errors.spec.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { SplitError, SplitNetworkError, SplitTimeoutError } from '../errors'; - -test('LANG UTILS / errors', () => { - const splitErrorInst = new SplitError(); // This one should not be used directly, so only testing default message - let splitNetErrorInst = new SplitNetworkError(); - let splitTimeErrorInst = new SplitTimeoutError(); - - expect(splitErrorInst.message).toBe('Split Error'); // The errors should have a default message. - expect(splitNetErrorInst.message).toBe('Split Network Error'); // The errors should have a default message. - expect(splitTimeErrorInst.message).toBe('Split Timeout Error'); // The errors should have a default message. - - splitNetErrorInst = new SplitNetworkError('NETWORK_ERR', 341); - - expect(splitNetErrorInst.message).toBe('NETWORK_ERR'); // Split Network Error should store params (message) correctly - expect(splitNetErrorInst.statusCode).toBe(341); // Split Network Error should store params (status code) correctly - expect(splitNetErrorInst instanceof SplitNetworkError).toBeTruthy(); - expect(splitNetErrorInst instanceof SplitError).toBeTruthy(); // All custom errors should extend from the root one - expect(splitNetErrorInst instanceof Error).toBeTruthy(); // All custom errors should extend from the native one. - - splitTimeErrorInst = new SplitTimeoutError('TIMEOUT_ERR'); - - expect(splitTimeErrorInst.message).toBe('TIMEOUT_ERR'); // Split Network Error should store params (message) correctly - expect(splitTimeErrorInst instanceof SplitTimeoutError).toBeTruthy(); - expect(splitTimeErrorInst instanceof SplitError).toBeTruthy(); // All custom errors should extend from the root one. - expect(splitTimeErrorInst instanceof Error).toBeTruthy(); // All custom errors should extend from the native one. - -}); diff --git a/src/utils/lang/errors.ts b/src/utils/lang/errors.ts deleted file mode 100644 index 75cf7ad8..00000000 --- a/src/utils/lang/errors.ts +++ /dev/null @@ -1,32 +0,0 @@ -// Simplified version of https://github.com/wesleytodd/setprototypeof/blob/master/index.js -const setPrototypeOf = Object.setPrototypeOf || function (obj, proto) { obj.__proto__ = proto; return obj; }; - -export class SplitError extends Error { - constructor(msg = 'Split Error') { - super(msg); - this.message = msg; - - // Required when extending a native constructor and transpiling to ES5 - // See https://github.com/Microsoft/TypeScript/wiki/Breaking-Changes#extending-built-ins-like-error-array-and-map-may-no-longer-work - setPrototypeOf(this, SplitError.prototype); - } -} - -export class SplitTimeoutError extends SplitError { - constructor(msg?: string) { - super(msg || 'Split Timeout Error'); - setPrototypeOf(this, SplitTimeoutError.prototype); - } -} - -export class SplitNetworkError extends SplitError { - - // HTTP error status code, or undefined if it is not an HTTP error - statusCode?: number; - - constructor(msg?: string, code?: number) { - super(msg || 'Split Network Error'); - this.statusCode = code; - setPrototypeOf(this, SplitNetworkError.prototype); - } -} diff --git a/src/utils/promise/timeout.ts b/src/utils/promise/timeout.ts index 9a049bec..3ca70b8c 100644 --- a/src/utils/promise/timeout.ts +++ b/src/utils/promise/timeout.ts @@ -1,11 +1,9 @@ -import { SplitTimeoutError } from '../lang/errors'; - export default function timeout(ms: number, promise: Promise): Promise { if (ms < 1) return promise; return new Promise((resolve, reject) => { const tid = setTimeout(() => { - reject(new SplitTimeoutError(`Operation timed out because it exceeded the configured time limit of ${ms}ms.`)); + reject(new Error(`Operation timed out because it exceeded the configured time limit of ${ms}ms.`)); }, ms); promise.then((res) => { From 2d6e83afdaf5f1bed846a411cee05d226edabdc9 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 26 Apr 2021 17:41:58 -0300 Subject: [PATCH 008/144] wrapper adapter impl and tests --- .eslintrc | 2 +- src/evaluator/value/sanitize.ts | 4 +- .../pluggable/__tests__/wrapper.mock.ts | 98 +++++++++++++ .../__tests__/wrapperAdapter.spec.ts | 136 ++++++++++++++++++ src/storages/pluggable/wrapperAdapter.ts | 87 +++++++++++ src/storages/types.ts | 60 ++++++++ 6 files changed, 384 insertions(+), 3 deletions(-) create mode 100644 src/storages/pluggable/__tests__/wrapper.mock.ts create mode 100644 src/storages/pluggable/__tests__/wrapperAdapter.spec.ts create mode 100644 src/storages/pluggable/wrapperAdapter.ts diff --git a/.eslintrc b/.eslintrc index a44d175d..9231c4df 100644 --- a/.eslintrc +++ b/.eslintrc @@ -66,7 +66,7 @@ "plugin:compat/recommended" ], "rules": { - "no-restricted-syntax": ["error", "ForOfStatement", "ForInStatement", "ArrayPattern"], + "no-restricted-syntax": ["error", "ForOfStatement", "ForInStatement"], "compat/compat": ["error", "defaults, not ie < 10, not node < 6"], "no-throw-literal": "error" }, diff --git a/src/evaluator/value/sanitize.ts b/src/evaluator/value/sanitize.ts index 46cae859..b42ff769 100644 --- a/src/evaluator/value/sanitize.ts +++ b/src/evaluator/value/sanitize.ts @@ -23,12 +23,12 @@ function sanitizeString(val: any): string | undefined { return str ? str : undefined; } -function sanitizeArray(val: any): number[] | string[] | undefined { +function sanitizeArray(val: any): string[] | undefined { const arr = Array.isArray(val) ? uniq(val.map(e => e + '')) : []; return arr.length ? arr : undefined; } -function sanitizeBoolean(val: any): boolean | undefined { +export function sanitizeBoolean(val: any): boolean | undefined { if (val === true || val === false) return val; if (typeof val === 'string') { diff --git a/src/storages/pluggable/__tests__/wrapper.mock.ts b/src/storages/pluggable/__tests__/wrapper.mock.ts new file mode 100644 index 00000000..c84de383 --- /dev/null +++ b/src/storages/pluggable/__tests__/wrapper.mock.ts @@ -0,0 +1,98 @@ +import { startsWith, toNumber } from '../../../utils/lang'; + +let _cache: Record = {}; + +// An in memory ICustomStorageWrapper implementation with Jest mocks +export const wrapperMock = { + + _cache, + + get: jest.fn((key => { + return Promise.resolve(key in _cache ? _cache[key] : null); + })), + set: jest.fn((key: string, value: string) => { + const result = key in _cache; + _cache[key] = value; + return Promise.resolve(result); + }), + del: jest.fn((key: string) => { + const result = key in _cache; + delete _cache[key]; + return Promise.resolve(result); + }), + getKeysByPrefix: jest.fn((prefix: string) => { + return Promise.resolve(Object.keys(_cache).filter(key => startsWith(key, prefix))); + }), + incr: jest.fn((key: string) => { + if (key in _cache) { + const count = toNumber(_cache[key]) + 1; + if (isNaN(count)) return Promise.resolve(false); + _cache[key] = count + ''; + } else { + _cache[key] = '1'; + } + return Promise.resolve(true); + }), + decr: jest.fn((key: string) => { + if (key in _cache) { + const count = toNumber(_cache[key]) - 1; + if (isNaN(count)) return Promise.resolve(false); + _cache[key] = count + ''; + } else { + _cache[key] = '-1'; + } + return Promise.resolve(true); + }), + getMany: jest.fn((keys: string[]) => { + return Promise.resolve(keys.map(key => _cache[key] ? _cache[key] : null)); + }), + // always connects and close + connect: jest.fn(() => Promise.resolve(true)), + close: jest.fn(() => Promise.resolve()), + + mockClear() { + _cache = {}; + this.get.mockClear(); + this.set.mockClear(); + this.del.mockClear(); + this.getKeysByPrefix.mockClear(); + this.incr.mockClear(); + this.decr.mockClear(); + this.getMany.mockClear(); + this.connect.mockClear(); + this.close.mockClear(); + } +}; + +function throwsException() { + throw new Error('some error'); +} + +function rejectedPromise() { + return Promise.reject('some error'); +} + +// @TODO remove if not used +export const wrapperWithExceptions = { + get: throwsException, + set: throwsException, + delete: throwsException, + getKeysByPrefix: throwsException, + incr: throwsException, + decr: throwsException, + getMany: throwsException, + connect: throwsException, + close: throwsException +}; + +export const wrapperWithRejectedPromiseResults = { + get: rejectedPromise, + set: rejectedPromise, + delete: rejectedPromise, + getKeysByPrefix: rejectedPromise, + incr: rejectedPromise, + decr: rejectedPromise, + getMany: rejectedPromise, + connect: rejectedPromise, + close: rejectedPromise +}; diff --git a/src/storages/pluggable/__tests__/wrapperAdapter.spec.ts b/src/storages/pluggable/__tests__/wrapperAdapter.spec.ts new file mode 100644 index 00000000..c6fe4bfe --- /dev/null +++ b/src/storages/pluggable/__tests__/wrapperAdapter.spec.ts @@ -0,0 +1,136 @@ +import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; +import thenable from '../../../utils/promise/thenable'; +import { logPrefix } from '../constants'; +import { SplitError } from '../../../utils/lang/errors'; + +/** Mocks */ + +import { wrapperMock } from './wrapper.mock'; // Well implemented wrapper + +function throwsException() { + throw new Error('some error'); +} + +function rejectedPromise() { + return Promise.reject('some error'); +} + +function invalidThenable() { + return { then() { } }; +} + +export const wrapperWithIssues = { + get: undefined, + set: 'invalid value', + del: throwsException, + getKeysByPrefix: rejectedPromise, + incr: invalidThenable, + decr: 'invalid value', + getMany: throwsException, + connect: rejectedPromise, + close: invalidThenable +}; + +export const wrapperWithValuesToSanitize = { + get: () => Promise.resolve(10), + set: () => Promise.resolve('tRue'), + del: () => Promise.resolve(), // no result + getKeysByPrefix: () => Promise.resolve(['1', null, false, true, '2', null]), + incr: () => Promise.resolve('1'), + decr: () => Promise.resolve('0'), + getMany: () => Promise.resolve(['1', null, false, true, '2', null]), + connect: () => Promise.resolve(1), + close: () => Promise.resolve(0), +}; + +const SANITIZED_RESULTS = { + get: '10', + set: true, + del: false, + getKeysByPrefix: ['1', '2'], + incr: false, + decr: false, + getMany: ['1', null, '2', null], + connect: false, + close: false +}; + +const VALID_METHOD_CALLS = { + 'get': ['some_key'], + 'set': ['some_key', 'some_value'], + 'del': ['some_key'], + 'getKeysByPrefix': ['some_prefix'], + 'incr': ['some_key'], + 'decr': ['some_key'], + 'getMany': [['some_key_1', 'some_key_2']], + 'connect': [], + 'close': [] +}; + +// Test target +import { wrapperAdapter } from '../wrapperAdapter'; + +describe('Wrapper Adapter', () => { + + afterEach(() => { + loggerMock.mockClear(); + wrapperMock.mockClear(); + }); + + test('calls original wrapper methods', async () => { + const instance = wrapperAdapter(loggerMock, wrapperMock); + const methods = Object.keys(VALID_METHOD_CALLS); + + for (let i = 0; i < methods.length; i++) { + const method = methods[i]; + const result = instance[method](...VALID_METHOD_CALLS[method]); + expect(wrapperMock[method]).toBeCalledTimes(1); + expect(wrapperMock[method]).toBeCalledWith(...VALID_METHOD_CALLS[method]); + expect(wrapperMock[method]).toHaveReturnedWith(result); + await result; + } + + // no logs if there isn't issues + expect(loggerMock.error).not.toBeCalled(); + expect(loggerMock.warn).not.toBeCalled(); + }); + + test('handle wrapper call exceptions', async () => { // @ts-ignore + const instance = wrapperAdapter(loggerMock, wrapperWithIssues); + const methods = Object.keys(VALID_METHOD_CALLS); + + for (let i = 0; i < methods.length; i++) { + const method = methods[i]; + const result = instance[method](...VALID_METHOD_CALLS[method]); + expect(thenable(result)).toBe(true); + try { + await result; + expect(true).toBe(false); // promise shouldn't be resolved + } catch (e) { + expect(e).toBeInstanceOf(SplitError); + expect(loggerMock.error).toHaveBeenCalledWith(`${logPrefix} wrapper '${method}' operation threw an error. Message: ${e.message}`); + } + } + + expect(loggerMock.error).toBeCalledTimes(methods.length); + }); + + test('sanitize wrapper call results', async () => { // @ts-ignore + const instance = wrapperAdapter(loggerMock, wrapperWithValuesToSanitize); + const methods = Object.keys(VALID_METHOD_CALLS); + + for (let i = 0; i < methods.length; i++) { + try { + const method = methods[i]; + const result = await instance[method](...VALID_METHOD_CALLS[method]); + + expect(result).toEqual(SANITIZED_RESULTS[method]); // result should be sanitized + } catch (e) { + expect(true).toBe(false); // promise shouldn't be rejected + } + } + + expect(loggerMock.warn).toBeCalledTimes(methods.length); // one warning for each wrapper operation call which result had to be sanitized + }); + +}); diff --git a/src/storages/pluggable/wrapperAdapter.ts b/src/storages/pluggable/wrapperAdapter.ts new file mode 100644 index 00000000..f68cf1e4 --- /dev/null +++ b/src/storages/pluggable/wrapperAdapter.ts @@ -0,0 +1,87 @@ +import { toString, isString } from '../../utils/lang'; +import { sanitizeBoolean as sBoolean } from '../../evaluator/value/sanitize'; +import { ILogger } from '../../logger/types'; +import { SplitError } from '../../utils/lang/errors'; +import { ICustomStorageWrapper } from '../types'; +import { logPrefix } from './constants'; + +// Sanitizers return the given value if it is of the expected type, or a new sanitized one if invalid. + +function sanitizeBoolean(val: any): boolean { + return sBoolean(val) || false; +} +sanitizeBoolean.type = 'boolean'; + +function sanitizeArray(val: any): string[] { + if (!Array.isArray(val)) return []; // if not an array, returns a new empty one + if (val.every(isString)) return val; // if all items are valid, return the given array + return val.filter(isString); // otherwise, return a new array filtering the invalid items +} +sanitizeArray.type = 'Array'; + +function sanitizeNullableString(val: any): string | null { + if (val === null) return val; + return toString(val); +} +sanitizeNullableString.type = 'string | null'; + +function sanitizeArrayOfNullableString(val: any): (string | null)[] { + const isStringOrNull = (v: any) => v === null || isString(v); + if (!Array.isArray(val)) return []; + if (val.every(isStringOrNull)) return val; + return val.filter(isStringOrNull); +} +sanitizeArrayOfNullableString.type = 'Array'; + +const METHODS_TO_PROMISE_WRAP: [string, { (val: any): any, type: string }][] = [ + ['get', sanitizeNullableString], + ['set', sanitizeBoolean], + ['del', sanitizeBoolean], + ['getKeysByPrefix', sanitizeArray], + ['incr', sanitizeBoolean], + ['decr', sanitizeBoolean], + ['getMany', sanitizeArrayOfNullableString], + ['connect', sanitizeBoolean], + ['close', sanitizeBoolean] +]; + +/** + * Adapter of the Custom Storage Wrapper. + * Used to properly handle exception and rejected promise results: logs error and wrap them into SplitErrors. + * + * @param log logger instance + * @param wrapper custom storage wrapper to adapt + * @returns an adapted version of the given storage wrapper + */ +export function wrapperAdapter(log: ILogger, wrapper: ICustomStorageWrapper): ICustomStorageWrapper { + + const wrapperAdapter: Record = {}; + + METHODS_TO_PROMISE_WRAP.forEach(([method, sanitizer]) => { + + // Logs error and wraps it into an SplitError object + function handleError(e: any) { + log.error(`${logPrefix} wrapper '${method}' operation threw an error. Message: ${e}`); + return Promise.reject(new SplitError(e)); + } + + wrapperAdapter[method] = function () { + try { + // @ts-ignore + return wrapper[method].apply(wrapper, arguments).then((value => { + const sanitizedValue = sanitizer(value); + // if value had to be sanitized, log a warning + if (sanitizedValue !== value) log.warn(`${logPrefix} Attempted to sanitize return value [${value}] of wrapper '${method}' operation which should be of type [${sanitizer.type}]. Sanitized and processed value => [${sanitizedValue}]`); + + return sanitizedValue; + })).catch(handleError); + } catch (e) { + return handleError(e); + } + }; + + }); + + // @ts-ignore + return wrapperAdapter; +} diff --git a/src/storages/types.ts b/src/storages/types.ts index c4b94ad2..5fe8c1e1 100644 --- a/src/storages/types.ts +++ b/src/storages/types.ts @@ -3,6 +3,66 @@ import { ILogger } from '../logger/types'; import { IReadinessManager } from '../readiness/types'; import { SplitIO, ImpressionDTO } from '../types'; +/** + * Interface to define a custom wrapper storage. + */ +export interface ICustomStorageWrapper { + /** + * Returns a promise that resolves with the element associated with the specified `key`, or null if the key can't be found in the storage. + */ + get: (key: string) => Promise + /** + * Adds or updates an element with a specified `key` and a `value`. + * Returns a promise that resolves with a boolean value: + * - true if the element existed and was updated, + * - or false if the element didn't exist and was added. + */ + set: (key: string, value: string) => Promise + /** + * Removes the specified element by `key`. + * Return a promise that resolves with a boolean value: + * - true if the element existed and has been removed, + * - or false if the element does not exist. + */ + del: (key: string) => Promise + /** + * Returns a promise that resolves with the list of element keys that match with the given `prefix`. + */ + getKeysByPrefix: (prefix: string) => Promise + /** + * Increment in 1 the given `key` value or set it in 1 if the value doesn't exist. + * Return a resolved promise with a boolean value: + * - true if the value was incremented, + * - or false if the value already existed and couldn't be parsed into a finite number. + * @param key + */ + incr: (key: string) => Promise + /** + * Decrement in 1 the given `key` value or set it in -1 if the value doesn't exist. + * Return a promise that resolves with a boolean value: + * - true if the value was decremented, + * - or false if the value already existed and couldn't be parsed into a finite number. + * @param key + */ + decr: (key: string) => Promise + /** + * Returns a promise that resolves with the list of elements associated with the specified list of `keys`. + */ + getMany: (keys: string[]) => Promise<(string | null)[]> + + /** + * For storages that requires to be connected, like database servers. + * Return a promise that resolves with a boolean value: + * - true if the operation succeeded, + * - or false if the operation failed. + */ + connect: () => Promise + /** + * For storages that requires to be closed, for example, to release resources. + */ + close: () => Promise +} + /** Splits cache */ export interface ISplitsCacheBase { From 7c63777bdb96b2f819aa1a24abbac9d1c46cd4bf Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 26 Apr 2021 18:04:13 -0300 Subject: [PATCH 009/144] update --- src/storages/pluggable/__tests__/wrapperAdapter.spec.ts | 5 +++-- src/storages/pluggable/constants.ts | 1 + src/storages/pluggable/wrapperAdapter.ts | 8 +++++--- 3 files changed, 9 insertions(+), 5 deletions(-) create mode 100644 src/storages/pluggable/constants.ts diff --git a/src/storages/pluggable/__tests__/wrapperAdapter.spec.ts b/src/storages/pluggable/__tests__/wrapperAdapter.spec.ts index c6fe4bfe..26c7c17b 100644 --- a/src/storages/pluggable/__tests__/wrapperAdapter.spec.ts +++ b/src/storages/pluggable/__tests__/wrapperAdapter.spec.ts @@ -1,3 +1,4 @@ +// @ts-nocheck import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; import thenable from '../../../utils/promise/thenable'; import { logPrefix } from '../constants'; @@ -95,7 +96,7 @@ describe('Wrapper Adapter', () => { expect(loggerMock.warn).not.toBeCalled(); }); - test('handle wrapper call exceptions', async () => { // @ts-ignore + test('handle wrapper call exceptions', async () => { const instance = wrapperAdapter(loggerMock, wrapperWithIssues); const methods = Object.keys(VALID_METHOD_CALLS); @@ -115,7 +116,7 @@ describe('Wrapper Adapter', () => { expect(loggerMock.error).toBeCalledTimes(methods.length); }); - test('sanitize wrapper call results', async () => { // @ts-ignore + test('sanitize wrapper call results', async () => { const instance = wrapperAdapter(loggerMock, wrapperWithValuesToSanitize); const methods = Object.keys(VALID_METHOD_CALLS); diff --git a/src/storages/pluggable/constants.ts b/src/storages/pluggable/constants.ts new file mode 100644 index 00000000..dbac131e --- /dev/null +++ b/src/storages/pluggable/constants.ts @@ -0,0 +1 @@ +export const logPrefix = 'storage:pluggable:'; diff --git a/src/storages/pluggable/wrapperAdapter.ts b/src/storages/pluggable/wrapperAdapter.ts index f68cf1e4..a442b254 100644 --- a/src/storages/pluggable/wrapperAdapter.ts +++ b/src/storages/pluggable/wrapperAdapter.ts @@ -33,7 +33,7 @@ function sanitizeArrayOfNullableString(val: any): (string | null)[] { } sanitizeArrayOfNullableString.type = 'Array'; -const METHODS_TO_PROMISE_WRAP: [string, { (val: any): any, type: string }][] = [ +const METHODS_TO_PROMISE_WRAP: [string, undefined | { (val: any): any, type: string }][] = [ ['get', sanitizeNullableString], ['set', sanitizeBoolean], ['del', sanitizeBoolean], @@ -42,7 +42,7 @@ const METHODS_TO_PROMISE_WRAP: [string, { (val: any): any, type: string }][] = [ ['decr', sanitizeBoolean], ['getMany', sanitizeArrayOfNullableString], ['connect', sanitizeBoolean], - ['close', sanitizeBoolean] + ['close', undefined] ]; /** @@ -61,7 +61,7 @@ export function wrapperAdapter(log: ILogger, wrapper: ICustomStorageWrapper): IC // Logs error and wraps it into an SplitError object function handleError(e: any) { - log.error(`${logPrefix} wrapper '${method}' operation threw an error. Message: ${e}`); + log.error(`${logPrefix} Wrapper '${method}' operation threw an error. Message: ${e}`); return Promise.reject(new SplitError(e)); } @@ -69,7 +69,9 @@ export function wrapperAdapter(log: ILogger, wrapper: ICustomStorageWrapper): IC try { // @ts-ignore return wrapper[method].apply(wrapper, arguments).then((value => { + if (!sanitizer) return value; const sanitizedValue = sanitizer(value); + // if value had to be sanitized, log a warning if (sanitizedValue !== value) log.warn(`${logPrefix} Attempted to sanitize return value [${value}] of wrapper '${method}' operation which should be of type [${sanitizer.type}]. Sanitized and processed value => [${sanitizedValue}]`); From 111d53cbb85b8e188a32dd7ac8dad8f78a0b5d87 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 26 Apr 2021 18:19:17 -0300 Subject: [PATCH 010/144] fixed test --- .../pluggable/__tests__/wrapperAdapter.spec.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/storages/pluggable/__tests__/wrapperAdapter.spec.ts b/src/storages/pluggable/__tests__/wrapperAdapter.spec.ts index 26c7c17b..0764c182 100644 --- a/src/storages/pluggable/__tests__/wrapperAdapter.spec.ts +++ b/src/storages/pluggable/__tests__/wrapperAdapter.spec.ts @@ -40,8 +40,7 @@ export const wrapperWithValuesToSanitize = { incr: () => Promise.resolve('1'), decr: () => Promise.resolve('0'), getMany: () => Promise.resolve(['1', null, false, true, '2', null]), - connect: () => Promise.resolve(1), - close: () => Promise.resolve(0), + connect: () => Promise.resolve(1) }; const SANITIZED_RESULTS = { @@ -52,8 +51,7 @@ const SANITIZED_RESULTS = { incr: false, decr: false, getMany: ['1', null, '2', null], - connect: false, - close: false + connect: false }; const VALID_METHOD_CALLS = { @@ -109,7 +107,7 @@ describe('Wrapper Adapter', () => { expect(true).toBe(false); // promise shouldn't be resolved } catch (e) { expect(e).toBeInstanceOf(SplitError); - expect(loggerMock.error).toHaveBeenCalledWith(`${logPrefix} wrapper '${method}' operation threw an error. Message: ${e.message}`); + expect(loggerMock.error).toHaveBeenCalledWith(`${logPrefix} Wrapper '${method}' operation threw an error. Message: ${e.message}`); } } @@ -118,7 +116,7 @@ describe('Wrapper Adapter', () => { test('sanitize wrapper call results', async () => { const instance = wrapperAdapter(loggerMock, wrapperWithValuesToSanitize); - const methods = Object.keys(VALID_METHOD_CALLS); + const methods = Object.keys(SANITIZED_RESULTS); for (let i = 0; i < methods.length; i++) { try { From e5cd83799211bddbee04b219970691032d256c1e Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 26 Apr 2021 19:18:52 -0300 Subject: [PATCH 011/144] splits pluggable storage implementation and tests --- src/evaluator/index.ts | 37 ++- src/storages/AbstractSplitsCacheSync.ts | 13 +- src/storages/KeyBuilder.ts | 8 + src/storages/KeyBuilderCS.ts | 4 - src/storages/KeyBuilderSS.ts | 2 +- src/storages/dataLoader.ts | 4 +- .../inLocalStorage/SplitsCacheInLocal.ts | 6 +- src/storages/inMemory/SplitsCacheInMemory.ts | 6 +- src/storages/inRedis/SegmentsCacheInRedis.ts | 1 + src/storages/inRedis/SplitsCacheInRedis.ts | 55 ++-- .../__tests__/EventsCacheInRedis.spec.ts | 2 +- src/storages/inRedis/__tests__/index.spec.ts | 2 +- src/storages/inRedis/index.ts | 12 +- src/storages/metadataBuilder.ts | 10 + .../pluggable/SplitsCachePluggable.ts | 274 ++++++++++++++++++ .../__tests__/SplitsCachePluggable.spec.ts | 180 ++++++++++++ src/storages/types.ts | 16 +- src/sync/polling/syncTasks/splitsSyncTask.ts | 6 +- 18 files changed, 557 insertions(+), 81 deletions(-) create mode 100644 src/storages/metadataBuilder.ts create mode 100644 src/storages/pluggable/SplitsCachePluggable.ts create mode 100644 src/storages/pluggable/__tests__/SplitsCachePluggable.spec.ts diff --git a/src/evaluator/index.ts b/src/evaluator/index.ts index 8dd5aa14..3c12c9b3 100644 --- a/src/evaluator/index.ts +++ b/src/evaluator/index.ts @@ -15,6 +15,14 @@ const treatmentException = { config: null }; +function treatmentsException(splitNames: string[]) { + const evaluations: Record = {}; + splitNames.forEach(splitName => { + evaluations[splitName] = treatmentException; + }); + return evaluations; +} + export function evaluateFeature( log: ILogger, key: SplitIO.SplitKey, @@ -27,10 +35,8 @@ export function evaluateFeature( try { stringifiedSplit = storage.splits.getSplit(splitName); } catch (e) { - // the only scenario where getSplit can throw an error is when the storage - // is redis and there is a connection issue and we can't retrieve the split - // to be evaluated - return Promise.resolve(treatmentException); + // Exception on sync `getSplit` storage. Not possible ATM with InMemory and InLocal storages. + return treatmentException; } if (thenable(stringifiedSplit)) { @@ -40,7 +46,11 @@ export function evaluateFeature( key, attributes, storage, - )); + )).catch( + // Exception on async `getSplit` storage. For example, when the storage is redis or + // pluggable and there is a connection issue and we can't retrieve the split to be evaluated + () => treatmentException + ); } return getEvaluation( @@ -60,22 +70,21 @@ export function evaluateFeatures( storage: IStorageSync | IStorageAsync, ): MaybeThenable> { let stringifiedSplits; - const evaluations: Record = {}; try { stringifiedSplits = storage.splits.getSplits(splitNames); } catch (e) { - // the only scenario where `getSplits` can throw an error is when the storage - // is redis and there is a connection issue and we can't retrieve the split - // to be evaluated - splitNames.forEach(splitName => { - evaluations[splitName] = treatmentException; - }); - return Promise.resolve(evaluations); + // Exception on sync `getSplits` storage. Not possible ATM with InMemory and InLocal storages. + return treatmentsException(splitNames); } return (thenable(stringifiedSplits)) ? - stringifiedSplits.then(splits => getEvaluations(log, splitNames, splits, key, attributes, storage)) : + stringifiedSplits.then(splits => getEvaluations(log, splitNames, splits, key, attributes, storage)) + .catch(() => { + // Exception on async `getSplits` storage. For example, when the storage is redis or + // pluggable and there is a connection issue and we can't retrieve the split to be evaluated + return treatmentsException(splitNames); + }) : getEvaluations(log, splitNames, stringifiedSplits, key, attributes, storage); } diff --git a/src/storages/AbstractSplitsCacheSync.ts b/src/storages/AbstractSplitsCacheSync.ts index 36eef20d..3494f56d 100644 --- a/src/storages/AbstractSplitsCacheSync.ts +++ b/src/storages/AbstractSplitsCacheSync.ts @@ -19,17 +19,10 @@ export default abstract class AbstractSplitsCacheSync implements ISplitsCacheSyn return results; } - abstract removeSplit(name: string): number + abstract removeSplit(name: string): boolean - removeSplits(names: string[]): number { - let len = names.length; - let counter = 0; - - for (let i = 0; i < len; i++) { - counter += this.removeSplit(names[i]); - } - - return counter; + removeSplits(names: string[]): boolean[] { + return names.map(name => this.removeSplit(name)); } abstract getSplit(name: string): string | null diff --git a/src/storages/KeyBuilder.ts b/src/storages/KeyBuilder.ts index 36e4badc..801bab3b 100644 --- a/src/storages/KeyBuilder.ts +++ b/src/storages/KeyBuilder.ts @@ -33,6 +33,14 @@ export default class KeyBuilder { return startsWith(key, `${this.prefix}.split.`); } + buildSplitKeyPrefix() { + return `${this.prefix}.split.`; + } + + buildSplitsWithSegmentCountKey() { + return `${this.prefix}.splits.usingSegments`; + } + buildSegmentNameKey(segmentName: string) { return `${this.prefix}.segment.${segmentName}`; } diff --git a/src/storages/KeyBuilderCS.ts b/src/storages/KeyBuilderCS.ts index 7d2c1312..6bfe7adf 100644 --- a/src/storages/KeyBuilderCS.ts +++ b/src/storages/KeyBuilderCS.ts @@ -26,10 +26,6 @@ export default class KeyBuilderCS extends KeyBuilder { return builtSegmentKeyName.substr(prefix.length); } - buildSplitsWithSegmentCountKey() { - return `${this.prefix}.splits.usingSegments`; - } - buildLastUpdatedKey() { return `${this.prefix}.splits.lastUpdated`; } diff --git a/src/storages/KeyBuilderSS.ts b/src/storages/KeyBuilderSS.ts index 903a0c73..1270c6f3 100644 --- a/src/storages/KeyBuilderSS.ts +++ b/src/storages/KeyBuilderSS.ts @@ -53,7 +53,7 @@ export default class KeyBuilderSS extends KeyBuilder { // } searchPatternForSplitKeys() { - return `${this.prefix}.split.*`; + return `${this.buildSplitKeyPrefix()}*`; } // NOT USED diff --git a/src/storages/dataLoader.ts b/src/storages/dataLoader.ts index 97ae6b3b..26e0090d 100644 --- a/src/storages/dataLoader.ts +++ b/src/storages/dataLoader.ts @@ -39,9 +39,7 @@ export function dataLoaderFactory(preloadedData: SplitIO.PreloadedData): DataLoa storage.splits.setChangeNumber(since); // splitsData in an object where the property is the split name and the pertaining value is a stringified json of its data - Object.keys(splitsData).forEach(splitName => { - storage.splits.addSplit(splitName, splitsData[splitName]); - }); + storage.splits.addSplits(Object.keys(splitsData).map(splitName => [splitName, splitsData[splitName]])); // add mySegments data let mySegmentsData = preloadedData.mySegmentsData && preloadedData.mySegmentsData[userId]; diff --git a/src/storages/inLocalStorage/SplitsCacheInLocal.ts b/src/storages/inLocalStorage/SplitsCacheInLocal.ts index 58d5ba09..56135e57 100644 --- a/src/storages/inLocalStorage/SplitsCacheInLocal.ts +++ b/src/storages/inLocalStorage/SplitsCacheInLocal.ts @@ -119,7 +119,7 @@ export default class SplitsCacheInLocal extends AbstractSplitsCacheSync { } } - removeSplit(name: string): number { + removeSplit(name: string): boolean { try { const split = this.getSplit(name); localStorage.removeItem(this.keys.buildSplitKey(name)); @@ -127,10 +127,10 @@ export default class SplitsCacheInLocal extends AbstractSplitsCacheSync { const parsedSplit = JSON.parse(split as string); this._decrementCounts(parsedSplit); - return 1; + return true; } catch (e) { this.log.error(logPrefix + e); - return 0; + return false; } } diff --git a/src/storages/inMemory/SplitsCacheInMemory.ts b/src/storages/inMemory/SplitsCacheInMemory.ts index 5388d2a4..39eefdcf 100644 --- a/src/storages/inMemory/SplitsCacheInMemory.ts +++ b/src/storages/inMemory/SplitsCacheInMemory.ts @@ -57,7 +57,7 @@ export default class SplitsCacheInMemory extends AbstractSplitsCacheSync { } } - removeSplit(name: string): number { + removeSplit(name: string): boolean { const split = this.getSplit(name); if (split) { // Delete the Split @@ -74,9 +74,9 @@ export default class SplitsCacheInMemory extends AbstractSplitsCacheSync { // Update the segments count. if (usesSegments(parsedSplit)) this.splitsWithSegmentsCount--; - return 1; + return true; } else { - return 0; + return false; } } diff --git a/src/storages/inRedis/SegmentsCacheInRedis.ts b/src/storages/inRedis/SegmentsCacheInRedis.ts index e4b23ccd..73955761 100644 --- a/src/storages/inRedis/SegmentsCacheInRedis.ts +++ b/src/storages/inRedis/SegmentsCacheInRedis.ts @@ -53,6 +53,7 @@ export default class SegmentsCacheInRedis implements ISegmentsCacheAsync { }); } + // @TODO remove: not used and not part of interface registerSegment(segment: string) { return this.registerSegments([segment]); } diff --git a/src/storages/inRedis/SplitsCacheInRedis.ts b/src/storages/inRedis/SplitsCacheInRedis.ts index c601b549..a5ec21c7 100644 --- a/src/storages/inRedis/SplitsCacheInRedis.ts +++ b/src/storages/inRedis/SplitsCacheInRedis.ts @@ -3,6 +3,7 @@ import KeyBuilderSS from '../KeyBuilderSS'; import { ISplitsCacheAsync } from '../types'; import { Redis } from 'ioredis'; import { ILogger } from '../../logger/types'; +import { SplitError } from '../../utils/lang/errors'; const logPrefix = 'storage:redis: '; @@ -17,28 +18,29 @@ function processPipelineAnswer(results: Array<[Error | null, string]>): string[] } /** - * Default ISplitsCacheSync implementation that stores split definitions in memory. - * Supported by all JS runtimes. + * ISplitsCacheAsync implementation that stores split definitions in Redis. + * Supported by Node. */ export default class SplitsCacheInRedis implements ISplitsCacheAsync { private readonly redis: Redis; private readonly keys: KeyBuilderSS; - private redisError?: string; + // private redisError?: string; constructor(private readonly log: ILogger, keys: KeyBuilderSS, redis: Redis) { this.redis = redis; this.keys = keys; - this.redis.on('error', (e) => { - this.redisError = e; - }); + // this.redis.on('error', (e) => { + // this.redisError = e; + // }); - this.redis.on('connect', () => { - this.redisError = undefined; - }); + // this.redis.on('connect', () => { + // this.redisError = undefined; + // }); } + // @TODO fix: incr/decr TT and segments for producer mode. Follow pluggable storage signature addSplit(name: string, split: string): Promise { return this.redis.set( this.keys.buildSplitKey(name), split @@ -47,6 +49,7 @@ export default class SplitsCacheInRedis implements ISplitsCacheAsync { ); } + // @TODO fix: incr/decr TT and segments for producer mode. Follow pluggable storage signature addSplits(entries: [string, string][]): Promise { if (entries.length) { const cmds = entries.map(keyValuePair => ['set', this.keys.buildSplitKey(keyValuePair[0]), keyValuePair[1]]); @@ -60,17 +63,22 @@ export default class SplitsCacheInRedis implements ISplitsCacheAsync { } } + // @TODO implement for producer mode. Follow pluggable storage signature + killLocally(): Promise { + throw new Error('Method not implemented.'); + } + /** * Remove a given split from Redis. Returns the number of deleted keys. */ - removeSplit(name: string): Promise { + removeSplit(name: string): Promise { return this.redis.del(this.keys.buildSplitKey(name)); } /** * Bulk delete of splits from Redis. Returns the number of deleted keys. */ - removeSplits(names: string[]): Promise { + removeSplits(names: string[]): Promise { if (names.length) { return this.redis.del(names.map(n => this.keys.buildSplitKey(n))); } else { @@ -80,13 +88,15 @@ export default class SplitsCacheInRedis implements ISplitsCacheAsync { /** * Get split definition or null if it's not defined. + * Returned promise is Rejected with an SplitError if redis operation fails. */ getSplit(name: string): Promise { - if (this.redisError) { - this.log.error(logPrefix + this.redisError); + // @TODO remove next block if not required + // if (this.redisError) { + // this.log.error(logPrefix + this.redisError); - throw this.redisError; - } + // throw this.redisError; + // } return this.redis.get(this.keys.buildSplitKey(name)); } @@ -135,6 +145,8 @@ export default class SplitsCacheInRedis implements ISplitsCacheAsync { // If there is a number there should be > 0, otherwise the TT is considered as not existent. return this.redis.get(this.keys.buildTrafficTypeKey(trafficType)) .then((ttCount: string | null | number) => { + if (ttCount === null) return false; // if entry doesn't exist, means that TT doesn't exist + ttCount = parseInt(ttCount as string, 10); if (!isFiniteNumber(ttCount) || ttCount < 0) { this.log.info(logPrefix + `Could not validate traffic type existance of ${trafficType} due to data corruption of some sorts.`); @@ -166,13 +178,16 @@ export default class SplitsCacheInRedis implements ISplitsCacheAsync { /** * Fetches multiple splits definitions. + * Returned promise is Rejected with an SplitError if redis operation fails. */ getSplits(names: string[]): Promise> { - if (this.redisError) { - this.log.error(logPrefix + this.redisError); + // @TODO remove next block if not required + // if (this.redisError) { + // this.log.error(logPrefix + this.redisError); + + // throw this.redisError; + // } - throw this.redisError; - } const splits: Record = {}; const keys = names.map(name => this.keys.buildSplitKey(name)); return this.redis.mget(...keys) @@ -184,7 +199,7 @@ export default class SplitsCacheInRedis implements ISplitsCacheAsync { }) .catch(e => { this.log.error(logPrefix + `Could not grab splits due to an error: ${e}.`); - return Promise.reject(e); + return Promise.reject(new SplitError(e)); }); } diff --git a/src/storages/inRedis/__tests__/EventsCacheInRedis.spec.ts b/src/storages/inRedis/__tests__/EventsCacheInRedis.spec.ts index c4384726..f5831481 100644 --- a/src/storages/inRedis/__tests__/EventsCacheInRedis.spec.ts +++ b/src/storages/inRedis/__tests__/EventsCacheInRedis.spec.ts @@ -4,7 +4,7 @@ import isEqual from 'lodash/isEqual'; import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; import KeyBuilderSS from '../../KeyBuilderSS'; import EventsCacheInRedis from '../EventsCacheInRedis'; -import { metadataBuilder } from '../index'; +import { metadataBuilder } from '../../metadataBuilder'; const prefix = 'events_cache_ut'; const metadata = { version: 'js_someversion', ip: 'some_ip', hostname: 'some_hostname' }; diff --git a/src/storages/inRedis/__tests__/index.spec.ts b/src/storages/inRedis/__tests__/index.spec.ts index fa62acf3..2ac012f1 100644 --- a/src/storages/inRedis/__tests__/index.spec.ts +++ b/src/storages/inRedis/__tests__/index.spec.ts @@ -1,4 +1,4 @@ -import { metadataBuilder } from '../index'; +import { metadataBuilder } from '../../metadataBuilder'; import { UNKNOWN } from '../../../utils/constants'; import { IMetadata } from '../../../dtos/types'; diff --git a/src/storages/inRedis/index.ts b/src/storages/inRedis/index.ts index 52093236..935cdf67 100644 --- a/src/storages/inRedis/index.ts +++ b/src/storages/inRedis/index.ts @@ -1,6 +1,5 @@ import RedisAdapter from './RedisAdapter'; import { IStorageAsync, IStorageFactoryParams } from '../types'; -import { IMetadata, IRedisMetadata } from '../../dtos/types'; import KeyBuilderSS from '../KeyBuilderSS'; import SplitsCacheInRedis from './SplitsCacheInRedis'; import SegmentsCacheInRedis from './SegmentsCacheInRedis'; @@ -8,23 +7,14 @@ import ImpressionsCacheInRedis from './ImpressionsCacheInRedis'; import EventsCacheInRedis from './EventsCacheInRedis'; import LatenciesCacheInRedis from './LatenciesCacheInRedis'; import CountsCacheInRedis from './CountsCacheInRedis'; -import { UNKNOWN } from '../../utils/constants'; import { SDK_SPLITS_ARRIVED, SDK_SEGMENTS_ARRIVED } from '../../readiness/constants'; +import { metadataBuilder } from '../metadataBuilder'; export interface InRedisStorageOptions { prefix?: string options?: Record } -// exported for testing purposes -export function metadataBuilder(metadata: IMetadata): IRedisMetadata { - return { - s: metadata.version, - i: metadata.ip || UNKNOWN, - n: metadata.hostname || UNKNOWN, - }; -} - /** * InRedis storage factory for consumer server-side SplitFactory, that uses `Ioredis` Redis client for Node. * @see {@link https://www.npmjs.com/package/ioredis} diff --git a/src/storages/metadataBuilder.ts b/src/storages/metadataBuilder.ts new file mode 100644 index 00000000..ca75572d --- /dev/null +++ b/src/storages/metadataBuilder.ts @@ -0,0 +1,10 @@ +import { IMetadata, IRedisMetadata } from '../dtos/types'; +import { UNKNOWN } from '../utils/constants'; + +export function metadataBuilder(metadata: IMetadata): IRedisMetadata { + return { + s: metadata.version, + i: metadata.ip || UNKNOWN, + n: metadata.hostname || UNKNOWN, + }; +} diff --git a/src/storages/pluggable/SplitsCachePluggable.ts b/src/storages/pluggable/SplitsCachePluggable.ts new file mode 100644 index 00000000..a1866150 --- /dev/null +++ b/src/storages/pluggable/SplitsCachePluggable.ts @@ -0,0 +1,274 @@ +import { isFiniteNumber, toNumber, isNaNNumber } from '../../utils/lang'; +import KeyBuilder from '../KeyBuilder'; +import { ICustomStorageWrapper, ISplitsCacheAsync } from '../types'; +import { ILogger } from '../../logger/types'; +import { usesSegments } from '../AbstractSplitsCacheSync'; +import { ISplit } from '../../dtos/types'; +import { logPrefix } from './constants'; +import { SplitError } from '../../utils/lang/errors'; + +/** + * ISplitsCacheAsync implementation for pluggable storages. + */ +export class SplitsCachePluggable implements ISplitsCacheAsync { + + private readonly log: ILogger; + private readonly keys: KeyBuilder; + private readonly wrapper: ICustomStorageWrapper; + + /** + * + * @param log logger instance + * @param keys key builder + * @param wrapper adapted wrapper storage + */ + constructor(log: ILogger, keys: KeyBuilder, wrapper: ICustomStorageWrapper) { + this.log = log; + this.keys = keys; + this.wrapper = wrapper; + } + + private _decrementCounts(split: ISplit) { + const promises = []; + if (split.trafficTypeName) { + const ttKey = this.keys.buildTrafficTypeKey(split.trafficTypeName); + promises.push(this.wrapper.decr(ttKey)); + } + + if (usesSegments(split)) { + const segmentsCountKey = this.keys.buildSplitsWithSegmentCountKey(); + promises.push(this.wrapper.decr(segmentsCountKey)); + } + + return Promise.all(promises); + } + + private _incrementCounts(split: ISplit) { + const promises = []; + if (split.trafficTypeName) { + const ttKey = this.keys.buildTrafficTypeKey(split.trafficTypeName); + promises.push(this.wrapper.incr(ttKey)); + } + + if (usesSegments(split)) { + const segmentsCountKey = this.keys.buildSplitsWithSegmentCountKey(); + promises.push(this.wrapper.incr(segmentsCountKey)); + } + + return Promise.all(promises); + } + + /** + * Add a given split. + * The returned promise is resolved when the operation success + * or rejected with an SplitError if it fails (e.g., wrapper operation fails) + */ + addSplit(name: string, split: string): Promise { + const splitKey = this.keys.buildSplitKey(name); + return this.wrapper.get(splitKey).then(splitFromStorage => { + + // handling parsing error as SplitErrors + let parsedPreviousSplit, parsedSplit; + try { + parsedPreviousSplit = splitFromStorage ? JSON.parse(splitFromStorage) : undefined; + parsedSplit = JSON.parse(split); + } catch (e) { + throw new SplitError('Error parsing split definition: ' + e); + } + + return Promise.all([ + // If it's an update, we decrement the traffic type and segment count of the existing split, + parsedPreviousSplit && this._decrementCounts(parsedPreviousSplit), + this.wrapper.set(splitKey, split), + this._incrementCounts(parsedSplit) + ]); + }).then(() => true); + } + + /** + * Add a list of splits. + * The returned promise is resolved when the operation success + * or rejected with an SplitError if it fails (e.g., wrapper operation fails) + */ + addSplits(entries: [string, string][]): Promise { + return Promise.all(entries.map(keyValuePair => this.addSplit(keyValuePair[0], keyValuePair[1]))); + } + + /** + * Remove a given split. + * The returned promise is resolved when the operation success, with a boolean indicating if the split existed or not. + * or rejected with an SplitError if it fails (e.g., wrapper operation fails). + */ + removeSplit(name: string): Promise { + return this.getSplit(name).then((split) => { + if (split) { + const parsedSplit = JSON.parse(split); + this._decrementCounts(parsedSplit); + } + return this.wrapper.del(this.keys.buildSplitKey(name)); + }); + } + + /** + * Remove a list of splits. + * The returned promise is resolved when the operation success, with a boolean array indicating if the splits existed or not. + * or rejected with an SplitError if it fails (e.g., wrapper operation fails). + */ + removeSplits(names: string[]): Promise { + return Promise.all(names.map(name => this.removeSplit(name))); + } + + /** + * Get split. + * The returned promise is resolved with the split definition or null if it's not defined, + * or rejected with an SplitError if wrapper operation fails. + */ + getSplit(name: string): Promise { + return this.wrapper.get(this.keys.buildSplitKey(name)); + } + + /** + * Get list of splits. + * The returned promise is resolved with a map of split names to their split definition or null if it's not defined, + * or rejected with an SplitError if wrapper operation fails. + */ + getSplits(names: string[]): Promise> { + const keys = names.map(name => this.keys.buildSplitKey(name)); + + return this.wrapper.getMany(keys).then(splitDefinitions => { + const splits: Record = {}; + names.forEach((name, idx) => { + splits[name] = splitDefinitions[idx]; + }); + return Promise.resolve(splits); + }); + } + + /** + * Get list of all split definitions. + * The returned promise is resolved with the list of split definitions, + * or rejected with an SplitError if wrapper operation fails. + */ + getAll(): Promise { + return this.wrapper.getKeysByPrefix(this.keys.buildSplitKeyPrefix()).then( + (listOfKeys) => Promise.all(listOfKeys.map(this.wrapper.get) as Promise[]) + ); + } + + /** + * Get list of split names. + * The returned promise is resolved with the list of split names, + * or rejected with an SplitError if wrapper operation fails. + */ + getSplitNames(): Promise { + return this.wrapper.getKeysByPrefix(this.keys.buildSplitKeyPrefix()).then( + (listOfKeys) => listOfKeys.map(this.keys.extractKey) + ); + } + + /** + * Check traffic type existance. + * The returned promise is resolved with a boolean indicating whether the TT exist or not. + * In case of wrapper operation failures, the promise resolves with a true value, assuming that the TT might exist. + * It will never be rejected. + */ + trafficTypeExists(trafficType: string): Promise { + // If there is a number there should be > 0, otherwise the TT is considered as not existent. + return this.wrapper.get(this.keys.buildTrafficTypeKey(trafficType)) + .then((ttCount: string | null | number) => { + if (ttCount === null) return false; // if entry doesn't exist, means that TT doesn't exist + + ttCount = parseInt(ttCount as string, 10); + if (!isFiniteNumber(ttCount) || ttCount < 0) { + this.log.info(logPrefix + `Could not validate traffic type existance of ${trafficType} due to data corruption of some sorts.`); + return false; + } + + return ttCount > 0; + }).catch(e => { + this.log.error(logPrefix + `Could not validate traffic type existance of ${trafficType} due to an error: ${e}.`); + // If there is an error, bypass the validation so the event can get tracked. + return true; + }); + } + + /** + * Set till number. + * The returned promise is resolved when the operation success, + * or rejected with an SplitError if it fails (e.g., wrapper operation fails). + */ + setChangeNumber(changeNumber: number): Promise { + return this.wrapper.set(this.keys.buildSplitsTillKey(), changeNumber + ''); + } + + /** + * Get till number or null if it's not defined. + * The returned promise is resolved with the changeNumber or -1 if it doesn't exist or a wrapper operation fails. + * The promise will never be rejected. + */ + getChangeNumber(): Promise { + return this.wrapper.get(this.keys.buildSplitsTillKey()).then((value) => { + const i = parseInt(value as string, 10); + + return isNaNNumber(i) ? -1 : i; + }).catch((e) => { + this.log.error(logPrefix + 'Could not retrieve changeNumber from storage. Error: ' + e); + return -1; + }); + } + + // @TODO revisit segment-related methods ('usesSegments', 'getRegisteredSegments', 'registerSegments') + usesSegments(): Promise { + return this.wrapper.get(this.keys.buildSplitsWithSegmentCountKey()) + .then(storedCount => { + const splitsWithSegmentsCount = storedCount ? toNumber(storedCount) : 0; + if (isFiniteNumber(splitsWithSegmentsCount)) { + return splitsWithSegmentsCount > 0; + } else { + return true; // If stored valus is invalid, assume we need them. + } + }).catch(() => true); // If wrapper operation fails, assume we need them. + } + + // @TODO remove `clear` from SplitsStorage + clear(): Promise { + return Promise.resolve(true); + } + + /** + * Check if the splits information is already stored in cache. + * Noop, just keeping the interface. This is used by client-side implementations only. + */ + checkCache(): Promise { + return Promise.resolve(true); + } + + /** + * Kill `name` split and set `defaultTreatment` and `changeNumber`. + * Used for SPLIT_KILL push notifications. + * + * @param {string} name + * @param {string} defaultTreatment + * @param {number} changeNumber + * @returns {Promise} a promise that is resolved once the split kill operation is performed. The fulfillment value is a boolean: `true` if the kill success updating the split or `false` if no split is updated, + * for instance, if the `changeNumber` is old, or if the split is not found (e.g., `/splitchanges` hasn't been fetched yet), or if the storage fails to apply the update. + * The promise will never be rejected. + */ + killLocally(name: string, defaultTreatment: string, changeNumber: number): Promise { + return this.getSplit(name).then(split => { + + if (split) { + const parsedSplit: ISplit = JSON.parse(split); + if (!parsedSplit.changeNumber || parsedSplit.changeNumber < changeNumber) { + parsedSplit.killed = true; + parsedSplit.defaultTreatment = defaultTreatment; + parsedSplit.changeNumber = changeNumber; + const newSplit = JSON.stringify(parsedSplit); + + return this.addSplit(name, newSplit); + } + } + return false; + }).catch(() => false); + } +} diff --git a/src/storages/pluggable/__tests__/SplitsCachePluggable.spec.ts b/src/storages/pluggable/__tests__/SplitsCachePluggable.spec.ts new file mode 100644 index 00000000..6e3c1c3e --- /dev/null +++ b/src/storages/pluggable/__tests__/SplitsCachePluggable.spec.ts @@ -0,0 +1,180 @@ +import { SplitsCachePluggable } from '../SplitsCachePluggable'; +import KeyBuilder from '../../KeyBuilder'; +import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; +import { wrapperMock } from './wrapper.mock'; + +const splitWithUserTT = '{ "trafficTypeName": "user_tt" }'; +const splitWithAccountTT = '{ "trafficTypeName": "account_tt" }'; +const splitWithAccountTTAndUsesSegments = '{ "trafficTypeName": "account_tt", "conditions": [{ "matcherGroup": { "matchers": [{ "matcherType": "IN_SEGMENT" }]}}] }'; +const keysBuilder = new KeyBuilder(); + +describe('SPLITS CACHE PLUGGABLE', () => { + + afterEach(() => { + loggerMock.mockClear(); + wrapperMock.mockClear(); + }); + + test('add/remove/get splits', async () => { + const cache = new SplitsCachePluggable(loggerMock, keysBuilder, wrapperMock); + + // Assert addSplit and addSplits + await cache.addSplits([ + ['lol1', splitWithUserTT], + ['lol2', splitWithAccountTT] + ]); + await cache.addSplit('lol3', splitWithAccountTT); + + // adding malformed or existing splits with the same definitions will not have effects + await expect(cache.addSplits([ + ['lol1', splitWithUserTT], + ['lol2', '{ /'] + ])).rejects.toBeTruthy(); + await expect(cache.addSplit('lol3', '{ /')).rejects.toBeTruthy(); + + // Assert getAll + let values = await cache.getAll(); + + expect(values.length).toBe(3); + expect(values.indexOf(splitWithUserTT) !== -1).toBe(true); + expect(values.indexOf(splitWithAccountTT) !== -1).toBe(true); + + // Assert getSplits + let valuesObj = await cache.getSplits(['lol2', 'lol3']); + + expect(Object.keys(valuesObj).length).toBe(2); + expect(valuesObj.lol2).toBe(splitWithAccountTT); + expect(valuesObj.lol3).toBe(splitWithAccountTT); + + // Assert getSplitNames + let splitNames = await cache.getSplitNames(); + + expect(splitNames.length).toBe(3); + expect(splitNames.indexOf('lol1') !== -1).toBe(true); + expect(splitNames.indexOf('lol2') !== -1).toBe(true); + expect(splitNames.indexOf('lol3') !== -1).toBe(true); + + // Assert removeSplit + await cache.removeSplit('lol1'); + + values = await cache.getAll(); + expect(values.length).toBe(2); + expect(await cache.getSplit('lol1')).toBe(null); + expect(await cache.getSplit('lol2')).toBe(splitWithAccountTT); + + // Assert removeSplits + await cache.addSplit('lol1', splitWithUserTT); + await cache.removeSplits(['lol1', 'lol3']); + + values = await cache.getAll(); + expect(values.length).toBe(1); + splitNames = await cache.getSplitNames(); + expect(splitNames.length).toBe(1); + expect(await cache.getSplit('lol1')).toBe(null); + expect(await cache.getSplit('lol2')).toBe(splitWithAccountTT); + + }); + + test('set/get change number', async () => { + const cache = new SplitsCachePluggable(loggerMock, keysBuilder, wrapperMock); + + expect(await cache.getChangeNumber()).toBe(-1); // if not set yet, changeNumber is -1 + await cache.setChangeNumber(123); + expect(await cache.getChangeNumber()).toBe(123); + + }); + + test('trafficTypeExists', async () => { + const cache = new SplitsCachePluggable(loggerMock, keysBuilder, wrapperMock); + + await cache.addSplits([ + ['split1', splitWithUserTT], + ['split2', splitWithAccountTT], + ['split3', splitWithUserTT], + ['malformed', '{}'] + ]); + await cache.addSplit('split4', splitWithUserTT); + await cache.addSplit('split4', splitWithUserTT); // trying to add the same definition for an already added split will not have effect + + expect(await cache.trafficTypeExists('user_tt')).toBe(true); + expect(await cache.trafficTypeExists('account_tt')).toBe(true); + expect(await cache.trafficTypeExists('not_existent_tt')).toBe(false); + + await cache.removeSplit('split4'); + + expect(await cache.trafficTypeExists('user_tt')).toBe(true); + expect(await cache.trafficTypeExists('account_tt')).toBe(true); + + await cache.removeSplits(['split3', 'split2']); // it'll invoke a loop of removeSplit + + expect(await cache.trafficTypeExists('user_tt')).toBe(true); + expect(await cache.trafficTypeExists('account_tt')).toBe(false); + + await cache.removeSplit('split1'); + + expect(await cache.trafficTypeExists('user_tt')).toBe(false); + expect(await cache.trafficTypeExists('account_tt')).toBe(false); + + await cache.addSplit('split1', splitWithUserTT); + expect(await cache.trafficTypeExists('user_tt')).toBe(true); + + await cache.addSplit('split1', splitWithAccountTT); + expect(await cache.trafficTypeExists('account_tt')).toBe(true); + expect(await cache.trafficTypeExists('user_tt')).toBe(false); + + }); + + test('usesSegments', async () => { + const cache = new SplitsCachePluggable(loggerMock, keysBuilder, wrapperMock); + + await cache.addSplits([['split1', splitWithUserTT], ['split2', splitWithAccountTT],]); + expect(await cache.usesSegments()).toBe(false); // 0 splits using segments + + await cache.addSplit('split3', splitWithAccountTTAndUsesSegments); + expect(await cache.usesSegments()).toBe(true); // 1 split using segments + + await cache.addSplit('split4', splitWithAccountTTAndUsesSegments); + expect(await cache.usesSegments()).toBe(true); // 2 splits using segments + + await cache.removeSplit('split3'); + expect(await cache.usesSegments()).toBe(true); // 1 split using segments + + await cache.removeSplit('split4'); + expect(await cache.usesSegments()).toBe(false); // 0 splits using segments + }); + + test('killLocally', async () => { + const cache = new SplitsCachePluggable(loggerMock, keysBuilder, wrapperMock); + + await cache.addSplit('lol1', splitWithUserTT); + await cache.addSplit('lol2', splitWithAccountTT); + const initialChangeNumber = await cache.getChangeNumber(); + + // kill an non-existent split + let updated = await cache.killLocally('nonexistent_split', 'other_treatment', 101); + const nonexistentSplit = await cache.getSplit('nonexistent_split'); + + expect(updated).toBe(false); // killLocally resolves without update if split doesn't exist + expect(nonexistentSplit).toBe(null); // non-existent split keeps being non-existent + + // kill an existent split + updated = await cache.killLocally('lol1', 'some_treatment', 100); + let lol1Split = JSON.parse(await cache.getSplit('lol1') as string); + + expect(updated).toBe(true); // killLocally resolves with update if split is changed + expect(lol1Split.killed).toBe(true); // existing split must be killed + expect(lol1Split.defaultTreatment).toBe('some_treatment'); // existing split must have new default treatment + expect(lol1Split.changeNumber).toBe(100); // existing split must have the given change number + expect(await cache.getChangeNumber()).toBe(initialChangeNumber); // cache changeNumber is not changed + + // not update if changeNumber is old + updated = await cache.killLocally('lol1', 'some_treatment_2', 90); + lol1Split = JSON.parse(await cache.getSplit('lol1') as string); + + expect(updated).toBe(false); // killLocally resolves without update if changeNumber is old + expect(lol1Split.defaultTreatment).not.toBe('some_treatment_2'); // existing split is not updated if given changeNumber is older + + }); + +}); + diff --git a/src/storages/types.ts b/src/storages/types.ts index 5fe8c1e1..ecdf9f11 100644 --- a/src/storages/types.ts +++ b/src/storages/types.ts @@ -66,10 +66,10 @@ export interface ICustomStorageWrapper { /** Splits cache */ export interface ISplitsCacheBase { - addSplit(name: string, split: string): MaybeThenable, + addSplit(name: string, split: string): MaybeThenable, // @TODO remove as in spec addSplits(entries: [string, string][]): MaybeThenable, - removeSplit(name: string): MaybeThenable, - removeSplits(names: string[]): MaybeThenable, + removeSplit(name: string): MaybeThenable, // @TODO remove as in spec + removeSplits(names: string[]): MaybeThenable, getSplit(name: string): MaybeThenable, getSplits(names: string[]): MaybeThenable>, // `fetchMany` in spec setChangeNumber(changeNumber: number): MaybeThenable, @@ -80,13 +80,14 @@ export interface ISplitsCacheBase { usesSegments(): MaybeThenable, clear(): MaybeThenable, checkCache(): MaybeThenable, + killLocally(name: string, defaultTreatment: string, changeNumber: number): MaybeThenable } export interface ISplitsCacheSync extends ISplitsCacheBase { addSplit(name: string, split: string): boolean, addSplits(entries: [string, string][]): boolean[] - removeSplit(name: string): number - removeSplits(names: string[]): number + removeSplit(name: string): boolean + removeSplits(names: string[]): boolean[] getSplit(name: string): string | null getSplits(names: string[]): Record setChangeNumber(changeNumber: number): boolean @@ -103,8 +104,8 @@ export interface ISplitsCacheSync extends ISplitsCacheBase { export interface ISplitsCacheAsync extends ISplitsCacheBase { addSplit(name: string, split: string): Promise, addSplits(entries: [string, string][]): Promise, - removeSplit(name: string): Promise, - removeSplits(names: string[]): Promise, + removeSplit(name: string): Promise, + removeSplits(names: string[]): Promise, getSplit(name: string): Promise, getSplits(names: string[]): Promise>, setChangeNumber(changeNumber: number): Promise, @@ -115,6 +116,7 @@ export interface ISplitsCacheAsync extends ISplitsCacheBase { usesSegments(): Promise, clear(): Promise, checkCache(): Promise, + killLocally(name: string, defaultTreatment: string, changeNumber: number): Promise } /** Segments cache */ diff --git a/src/sync/polling/syncTasks/splitsSyncTask.ts b/src/sync/polling/syncTasks/splitsSyncTask.ts index a69e579a..aec19fe5 100644 --- a/src/sync/polling/syncTasks/splitsSyncTask.ts +++ b/src/sync/polling/syncTasks/splitsSyncTask.ts @@ -1,6 +1,6 @@ import { SplitError } from '../../../utils/lang/errors'; import { _Set, setToArray, ISet } from '../../../utils/lang/sets'; -import { ISegmentsCacheSync, ISplitsCacheSync, IStorageSync } from '../../../storages/types'; +import { ISegmentsCacheSync, ISplitsCacheBase, IStorageSync } from '../../../storages/types'; import { ISplitChangesFetcher } from '../fetchers/types'; import { ISplit, ISplitChangesResponse } from '../../../dtos/types'; import { IReadinessManager, ISplitsEventEmitter } from '../../../readiness/types'; @@ -84,7 +84,7 @@ export function computeSplitsMutation(entries: ISplit[]): ISplitMutations { export function splitChangesUpdaterFactory( log: ILogger, splitChangesFetcher: ISplitChangesFetcher, - splitsCache: ISplitsCacheSync, + splitsCache: ISplitsCacheBase, segmentsCache: ISegmentsCacheSync, splitsEventEmitter: ISplitsEventEmitter, requestTimeoutBeforeReady: number, @@ -129,7 +129,7 @@ export function splitChangesUpdaterFactory( log.debug(SYNC_SPLITS_SEGMENTS, [mutation.segments.length]); // Write into storage - // @TODO if allowing custom storages, wrap errors as SplitErrors to distinguish from user callback errors + // @TODO in producer mode, keep consistency of wrapped storate by calling `setChangeNumber` if the other wrapper operations have succeeded return Promise.all([ // calling first `setChangenumber` method, to perform cache flush if split filter queryString changed splitsCache.setChangeNumber(splitChanges.till), From 2c7f89a26bb571548d33e9cf712a44207a5244a3 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 27 Apr 2021 21:21:35 -0300 Subject: [PATCH 012/144] polishing --- src/storages/AbstractSplitsCacheSync.ts | 8 +------- src/storages/inRedis/SplitsCacheInRedis.ts | 2 +- src/storages/pluggable/SplitsCachePluggable.ts | 4 ++-- 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/src/storages/AbstractSplitsCacheSync.ts b/src/storages/AbstractSplitsCacheSync.ts index 3494f56d..3c0c251e 100644 --- a/src/storages/AbstractSplitsCacheSync.ts +++ b/src/storages/AbstractSplitsCacheSync.ts @@ -10,13 +10,7 @@ export default abstract class AbstractSplitsCacheSync implements ISplitsCacheSyn abstract addSplit(name: string, split: string): boolean; addSplits(entries: [string, string][]): boolean[] { - const results: boolean[] = []; - - entries.forEach(keyValuePair => { - results.push(this.addSplit(keyValuePair[0], keyValuePair[1])); - }); - - return results; + return entries.map(keyValuePair => this.addSplit(keyValuePair[0], keyValuePair[1])); } abstract removeSplit(name: string): boolean diff --git a/src/storages/inRedis/SplitsCacheInRedis.ts b/src/storages/inRedis/SplitsCacheInRedis.ts index a5ec21c7..c9694862 100644 --- a/src/storages/inRedis/SplitsCacheInRedis.ts +++ b/src/storages/inRedis/SplitsCacheInRedis.ts @@ -113,7 +113,7 @@ export default class SplitsCacheInRedis implements ISplitsCacheAsync { } /** - * Get till number or null if it's not defined. + * Get till number or -1 if it's not defined. * * @TODO pending error handling */ diff --git a/src/storages/pluggable/SplitsCachePluggable.ts b/src/storages/pluggable/SplitsCachePluggable.ts index a1866150..68e9486b 100644 --- a/src/storages/pluggable/SplitsCachePluggable.ts +++ b/src/storages/pluggable/SplitsCachePluggable.ts @@ -202,7 +202,7 @@ export class SplitsCachePluggable implements ISplitsCacheAsync { } /** - * Get till number or null if it's not defined. + * Get till number or -1 if it's not defined. * The returned promise is resolved with the changeNumber or -1 if it doesn't exist or a wrapper operation fails. * The promise will never be rejected. */ @@ -230,7 +230,7 @@ export class SplitsCachePluggable implements ISplitsCacheAsync { }).catch(() => true); // If wrapper operation fails, assume we need them. } - // @TODO remove `clear` from SplitsStorage + // @TODO implement for DataLoader/Producer mode clear(): Promise { return Promise.resolve(true); } From 7447709045077bc7e944aa16d6f33aca52de3d2e Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 27 Apr 2021 23:26:36 -0300 Subject: [PATCH 013/144] added remaining wrapper methods --- .../pluggable/__tests__/wrapper.mock.ts | 58 ++++++++----------- .../__tests__/wrapperAdapter.spec.ts | 34 +++++++++-- src/storages/pluggable/wrapperAdapter.ts | 15 ++++- src/storages/types.ts | 38 ++++++++++-- 4 files changed, 100 insertions(+), 45 deletions(-) diff --git a/src/storages/pluggable/__tests__/wrapper.mock.ts b/src/storages/pluggable/__tests__/wrapper.mock.ts index c84de383..1f8af875 100644 --- a/src/storages/pluggable/__tests__/wrapper.mock.ts +++ b/src/storages/pluggable/__tests__/wrapper.mock.ts @@ -1,11 +1,13 @@ import { startsWith, toNumber } from '../../../utils/lang'; let _cache: Record = {}; +let _queues: Record = {}; // An in memory ICustomStorageWrapper implementation with Jest mocks export const wrapperMock = { _cache, + _queues, get: jest.fn((key => { return Promise.resolve(key in _cache ? _cache[key] : null); @@ -15,6 +17,11 @@ export const wrapperMock = { _cache[key] = value; return Promise.resolve(result); }), + getAndSet: jest.fn((key: string, value: string) => { + const result = key in _cache ? _cache[key] : null; + _cache[key] = value; + return Promise.resolve(result); + }), del: jest.fn((key: string) => { const result = key in _cache; delete _cache[key]; @@ -23,6 +30,9 @@ export const wrapperMock = { getKeysByPrefix: jest.fn((prefix: string) => { return Promise.resolve(Object.keys(_cache).filter(key => startsWith(key, prefix))); }), + getByPrefix: jest.fn((prefix: string) => { + return Promise.resolve(Object.keys(_cache).filter(key => startsWith(key, prefix)).map(key => _cache[key])); + }), incr: jest.fn((key: string) => { if (key in _cache) { const count = toNumber(_cache[key]) + 1; @@ -46,6 +56,21 @@ export const wrapperMock = { getMany: jest.fn((keys: string[]) => { return Promise.resolve(keys.map(key => _cache[key] ? _cache[key] : null)); }), + pushItems: jest.fn((key: string, items: string[]) => { + if (!(key in _queues)) _queues[key] = []; + _queues[key].push(...items); + return Promise.resolve(true); + }), + popItems: jest.fn((key: string, count: number) => { + return Promise.resolve(key in _queues ? _queues[key].splice(0, count) : []); + }), + getItemsCount: jest.fn((key: string) => { + return Promise.resolve(key in _queues ? _queues[key].length : 0); + }), + itemContains: jest.fn((key: string, item: string) => { + return Promise.resolve(key in _queues && _queues[key].indexOf(item) > -1 ? true : false); + }), + // always connects and close connect: jest.fn(() => Promise.resolve(true)), close: jest.fn(() => Promise.resolve()), @@ -63,36 +88,3 @@ export const wrapperMock = { this.close.mockClear(); } }; - -function throwsException() { - throw new Error('some error'); -} - -function rejectedPromise() { - return Promise.reject('some error'); -} - -// @TODO remove if not used -export const wrapperWithExceptions = { - get: throwsException, - set: throwsException, - delete: throwsException, - getKeysByPrefix: throwsException, - incr: throwsException, - decr: throwsException, - getMany: throwsException, - connect: throwsException, - close: throwsException -}; - -export const wrapperWithRejectedPromiseResults = { - get: rejectedPromise, - set: rejectedPromise, - delete: rejectedPromise, - getKeysByPrefix: rejectedPromise, - incr: rejectedPromise, - decr: rejectedPromise, - getMany: rejectedPromise, - connect: rejectedPromise, - close: rejectedPromise -}; diff --git a/src/storages/pluggable/__tests__/wrapperAdapter.spec.ts b/src/storages/pluggable/__tests__/wrapperAdapter.spec.ts index 0764c182..ea0f2c22 100644 --- a/src/storages/pluggable/__tests__/wrapperAdapter.spec.ts +++ b/src/storages/pluggable/__tests__/wrapperAdapter.spec.ts @@ -29,7 +29,13 @@ export const wrapperWithIssues = { decr: 'invalid value', getMany: throwsException, connect: rejectedPromise, - close: invalidThenable + close: invalidThenable, + getAndSet: 'invalid value', + getByPrefix: throwsException, + pushItems: rejectedPromise, + popItems: invalidThenable, + getItemsCount: 'invalid value', + itemContains: throwsException, }; export const wrapperWithValuesToSanitize = { @@ -40,7 +46,13 @@ export const wrapperWithValuesToSanitize = { incr: () => Promise.resolve('1'), decr: () => Promise.resolve('0'), getMany: () => Promise.resolve(['1', null, false, true, '2', null]), - connect: () => Promise.resolve(1) + connect: () => Promise.resolve(1), + getAndSet: () => Promise.resolve(true), + getByPrefix: () => Promise.resolve(['1', null, false, true, '2', null]), + pushItems: () => Promise.resolve('false'), + popItems: () => Promise.resolve('invalid array'), + getItemsCount: () => Promise.resolve('10'), + itemContains: () => Promise.resolve('true'), }; const SANITIZED_RESULTS = { @@ -51,7 +63,13 @@ const SANITIZED_RESULTS = { incr: false, decr: false, getMany: ['1', null, '2', null], - connect: false + connect: false, + getAndSet: 'true', + getByPrefix: ['1', '2'], + pushItems: false, + popItems: [], + getItemsCount: 10, + itemContains: true, }; const VALID_METHOD_CALLS = { @@ -63,7 +81,13 @@ const VALID_METHOD_CALLS = { 'decr': ['some_key'], 'getMany': [['some_key_1', 'some_key_2']], 'connect': [], - 'close': [] + 'close': [], + 'getAndSet': ['some_key', 'some_value'], + 'getByPrefix': ['some_prefix'], + 'pushItems': ['some_key', ['item1', 'item2']], + 'popItems': ['some_key'], + 'getItemsCount': ['some_key'], + 'itemContains': ['some_key', 'some_value'], }; // Test target @@ -125,7 +149,7 @@ describe('Wrapper Adapter', () => { expect(result).toEqual(SANITIZED_RESULTS[method]); // result should be sanitized } catch (e) { - expect(true).toBe(false); // promise shouldn't be rejected + expect(true).toBe(methods[i]); // promise shouldn't be rejected } } diff --git a/src/storages/pluggable/wrapperAdapter.ts b/src/storages/pluggable/wrapperAdapter.ts index a442b254..37fa666d 100644 --- a/src/storages/pluggable/wrapperAdapter.ts +++ b/src/storages/pluggable/wrapperAdapter.ts @@ -1,4 +1,4 @@ -import { toString, isString } from '../../utils/lang'; +import { toString, isString, toNumber } from '../../utils/lang'; import { sanitizeBoolean as sBoolean } from '../../evaluator/value/sanitize'; import { ILogger } from '../../logger/types'; import { SplitError } from '../../utils/lang/errors'; @@ -12,6 +12,11 @@ function sanitizeBoolean(val: any): boolean { } sanitizeBoolean.type = 'boolean'; +function sanitizeNumber(val: any): number { + return toNumber(val); +} +sanitizeNumber.type = 'number'; + function sanitizeArray(val: any): string[] { if (!Array.isArray(val)) return []; // if not an array, returns a new empty one if (val.every(isString)) return val; // if all items are valid, return the given array @@ -42,7 +47,13 @@ const METHODS_TO_PROMISE_WRAP: [string, undefined | { (val: any): any, type: str ['decr', sanitizeBoolean], ['getMany', sanitizeArrayOfNullableString], ['connect', sanitizeBoolean], - ['close', undefined] + ['close', undefined], + ['getAndSet', sanitizeNullableString], + ['getByPrefix', sanitizeArray], + ['pushItems', sanitizeBoolean], + ['popItems', sanitizeArray], + ['getItemsCount', sanitizeNumber], + ['itemContains', sanitizeBoolean], ]; /** diff --git a/src/storages/types.ts b/src/storages/types.ts index 5fe8c1e1..f7f23eda 100644 --- a/src/storages/types.ts +++ b/src/storages/types.ts @@ -8,27 +8,38 @@ import { SplitIO, ImpressionDTO } from '../types'; */ export interface ICustomStorageWrapper { /** - * Returns a promise that resolves with the element associated with the specified `key`, or null if the key can't be found in the storage. + * Return a promise that resolves with the element value associated with the specified `key`, or null if the key can't be found in the storage. */ get: (key: string) => Promise /** - * Adds or updates an element with a specified `key` and a `value`. + * Add or update an element with a specified `key` and `value`. * Returns a promise that resolves with a boolean value: * - true if the element existed and was updated, * - or false if the element didn't exist and was added. */ set: (key: string, value: string) => Promise /** - * Removes the specified element by `key`. + * Add or update an element with a specified `key` and `value`. + * Returns a promise that resolves with the previous value associated to the given `key`, or null if not set. + * - true if the element existed and was updated, + * - or false if the element didn't exist and was added. + */ + getAndSet: (key: string, value: string) => Promise + /** + * Remove the specified element by `key`. * Return a promise that resolves with a boolean value: * - true if the element existed and has been removed, * - or false if the element does not exist. */ del: (key: string) => Promise /** - * Returns a promise that resolves with the list of element keys that match with the given `prefix`. + * Return a promise that resolves with the list of element keys that match with the given `prefix`. */ getKeysByPrefix: (prefix: string) => Promise + /** + * Return a promise that resolves with the list of element values that match with the given `prefix`. + */ + getByPrefix: (prefix: string) => Promise /** * Increment in 1 the given `key` value or set it in 1 if the value doesn't exist. * Return a resolved promise with a boolean value: @@ -46,9 +57,26 @@ export interface ICustomStorageWrapper { */ decr: (key: string) => Promise /** - * Returns a promise that resolves with the list of elements associated with the specified list of `keys`. + * Return a promise that resolves with the list of elements associated with the specified list of `keys`. */ getMany: (keys: string[]) => Promise<(string | null)[]> + /** + * Push given `items` to `key` queue. + */ + pushItems: (key: string, items: string[]) => Promise + /** + * Pop `count` number of items from `key` queue. + * Return a promise that resolves with the list of removed items the removed members, or an empty array when key does not exist. + */ + popItems: (key: string, count: number) => Promise + /** + * Return a promise that resolves with the number of items at the `key` queue, or 0 when key does not exist. + */ + getItemsCount: (key: string) => Promise + /** + * Return a promise that resolves with true boolean value if `item` is a member of the set stored at `key`, or false otherwise. + */ + itemContains: (key: string, item: string) => Promise /** * For storages that requires to be connected, like database servers. From 395d3124db6aaad688af55cb07dcb10d19c84a84 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 28 Apr 2021 09:32:23 -0300 Subject: [PATCH 014/144] implementation and unit tests --- .../pluggable/SegmentsCachePluggable.ts | 93 +++++++++++++++++++ .../__tests__/SegmentsCachePluggable.spec.ts | 36 +++++++ .../pluggable/__tests__/wrapper.mock.ts | 7 ++ 3 files changed, 136 insertions(+) create mode 100644 src/storages/pluggable/SegmentsCachePluggable.ts create mode 100644 src/storages/pluggable/__tests__/SegmentsCachePluggable.spec.ts diff --git a/src/storages/pluggable/SegmentsCachePluggable.ts b/src/storages/pluggable/SegmentsCachePluggable.ts new file mode 100644 index 00000000..67fc39be --- /dev/null +++ b/src/storages/pluggable/SegmentsCachePluggable.ts @@ -0,0 +1,93 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/* eslint-disable no-unused-vars */ +import { isNaNNumber } from '../../utils/lang'; +import KeyBuilder from '../KeyBuilder'; +import { ICustomStorageWrapper, ISegmentsCacheAsync } from '../types'; +import { ILogger } from '../../logger/types'; +import { logPrefix } from './constants'; + +/** + * ISegmentsCacheAsync implementation for pluggable storages. + */ +export class SegmentsCachePluggable implements ISegmentsCacheAsync { + + private readonly log: ILogger; + private readonly keys: KeyBuilder; + private readonly wrapper: ICustomStorageWrapper; + + constructor(log: ILogger, keys: KeyBuilder, wrapper: ICustomStorageWrapper) { + this.log = log; + this.keys = keys; + this.wrapper = wrapper; + } + + /** + * Returns if `key` is part of `name` segment. + */ + isInSegment(name: string, key: string) { + return this.wrapper.itemContains(this.keys.buildSegmentNameKey(name), key); + } + + /** + * Set till number for the given segment `name`. + * The returned promise is resolved when the operation success, + * or rejected with an SplitError if it fails (e.g., wrapper operation fails). + */ + setChangeNumber(name: string, changeNumber: number) { + return this.wrapper.set( + this.keys.buildSegmentTillKey(name), changeNumber + '' + ); + } + + /** + * Get till number or -1 if it's not defined. + * The returned promise is resolved with the changeNumber or -1 if it doesn't exist or a wrapper operation fails. + * The promise will never be rejected. + */ + getChangeNumber(name: string) { + return this.wrapper.get(this.keys.buildSegmentTillKey(name)).then((value: string | null) => { + const i = parseInt(value as string, 10); + return isNaNNumber(i) ? -1 : i; + }).catch((e) => { + this.log.error(logPrefix + 'Could not retrieve changeNumber from segments storage. Error: ' + e); + return -1; + }); + } + + /** + * Add a list of `segmentKeys` to the given segment `name`. + * The returned promise is resolved when the operation success + * or rejected with an SplitError if it fails (e.g., wrapper operation fails, JSON parsing error) + * + * @TODO implement for DataLoader/Producer mode + */ + addToSegment(name: string, segmentKeys: string[]) { + return Promise.resolve(true); + } + + /** + * Remove a list of `segmentKeys` from the given segment `name`. + * The returned promise is resolved when the operation success + * or rejected with an SplitError if it fails (e.g., wrapper operation fails, JSON parsing error) + * + * @TODO implement for DataLoader/Producer mode + */ + removeFromSegment(name: string, segmentKeys: string[]) { + return Promise.resolve(true); + } + + /** @TODO implement for DataLoader/Producer mode */ + registerSegments(names: string[]) { + return Promise.resolve(true); + } + + /** @TODO implement for DataLoader/Producer mode */ + getRegisteredSegments() { + return Promise.resolve([]); + } + + /** @TODO implement for DataLoader/Producer mode */ + clear(): Promise { + return Promise.resolve(true); + } +} diff --git a/src/storages/pluggable/__tests__/SegmentsCachePluggable.spec.ts b/src/storages/pluggable/__tests__/SegmentsCachePluggable.spec.ts new file mode 100644 index 00000000..4610b00a --- /dev/null +++ b/src/storages/pluggable/__tests__/SegmentsCachePluggable.spec.ts @@ -0,0 +1,36 @@ +import { SegmentsCachePluggable } from '../SegmentsCachePluggable'; +import KeyBuilder from '../../KeyBuilder'; +import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; +import { wrapperMock } from './wrapper.mock'; + +const keyBuilder = new KeyBuilder(); + +describe('SEGMENTS CACHE PLUGGABLE', () => { + + afterEach(() => { + loggerMock.mockClear(); + wrapperMock.mockClear(); + }); + + test('isInSegment, set/getChangeNumber', async () => { + const cache = new SegmentsCachePluggable(loggerMock, keyBuilder, wrapperMock); + + // assert setChangeNumber and getChangeNumber + expect(await cache.setChangeNumber('mocked-segment', 100)).toBe(false); + expect(await cache.getChangeNumber('mocked-segment')).toBe(100); + expect(await cache.setChangeNumber('mocked-segment', 200)).toBe(true); + expect(await cache.getChangeNumber('mocked-segment')).toBe(200); + expect(await cache.getChangeNumber('inexistent-segment')).toBe(-1); // -1 if the segment doesn't exist + + // mock segment keys + wrapperMock._queues[keyBuilder.buildSegmentNameKey('mocked-segment')] = ['b', 'd']; + + expect(await cache.isInSegment('mocked-segment', 'a')).toBe(false); + expect(await cache.isInSegment('mocked-segment', 'b')).toBe(true); + expect(await cache.isInSegment('mocked-segment', 'c')).toBe(false); + expect(await cache.isInSegment('mocked-segment', 'd')).toBe(true); + + expect(await cache.isInSegment('inexistent-segment', 'a')).toBe(false); + }); + +}); diff --git a/src/storages/pluggable/__tests__/wrapper.mock.ts b/src/storages/pluggable/__tests__/wrapper.mock.ts index 1f8af875..7be7f9fe 100644 --- a/src/storages/pluggable/__tests__/wrapper.mock.ts +++ b/src/storages/pluggable/__tests__/wrapper.mock.ts @@ -77,6 +77,7 @@ export const wrapperMock = { mockClear() { _cache = {}; + _queues = {}; this.get.mockClear(); this.set.mockClear(); this.del.mockClear(); @@ -86,5 +87,11 @@ export const wrapperMock = { this.getMany.mockClear(); this.connect.mockClear(); this.close.mockClear(); + this.getAndSet.mockClear(); + this.getByPrefix.mockClear(); + this.pushItems.mockClear(); + this.popItems.mockClear(); + this.getItemsCount.mockClear(); + this.itemContains.mockClear(); } }; From 5cee3ba2c39261b4178901c23b57f21e8cede9c0 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 28 Apr 2021 14:33:22 -0300 Subject: [PATCH 015/144] applied Emi's feedback --- src/storages/inRedis/SplitsCacheInRedis.ts | 40 +++++++++---------- .../pluggable/SplitsCachePluggable.ts | 14 +++---- .../__tests__/splitExistance.spec.ts | 2 +- .../__tests__/trafficTypeExistance.spec.ts | 8 ++-- 4 files changed, 32 insertions(+), 32 deletions(-) diff --git a/src/storages/inRedis/SplitsCacheInRedis.ts b/src/storages/inRedis/SplitsCacheInRedis.ts index c9694862..726ec0a6 100644 --- a/src/storages/inRedis/SplitsCacheInRedis.ts +++ b/src/storages/inRedis/SplitsCacheInRedis.ts @@ -23,21 +23,23 @@ function processPipelineAnswer(results: Array<[Error | null, string]>): string[] */ export default class SplitsCacheInRedis implements ISplitsCacheAsync { + private readonly log: ILogger; private readonly redis: Redis; private readonly keys: KeyBuilderSS; - // private redisError?: string; + private redisError?: string; - constructor(private readonly log: ILogger, keys: KeyBuilderSS, redis: Redis) { + constructor(log: ILogger, keys: KeyBuilderSS, redis: Redis) { + this.log = log; this.redis = redis; this.keys = keys; - // this.redis.on('error', (e) => { - // this.redisError = e; - // }); + this.redis.on('error', (e) => { + this.redisError = e; + }); - // this.redis.on('connect', () => { - // this.redisError = undefined; - // }); + this.redis.on('connect', () => { + this.redisError = undefined; + }); } // @TODO fix: incr/decr TT and segments for producer mode. Follow pluggable storage signature @@ -91,12 +93,11 @@ export default class SplitsCacheInRedis implements ISplitsCacheAsync { * Returned promise is Rejected with an SplitError if redis operation fails. */ getSplit(name: string): Promise { - // @TODO remove next block if not required - // if (this.redisError) { - // this.log.error(logPrefix + this.redisError); + if (this.redisError) { + this.log.error(logPrefix + this.redisError); - // throw this.redisError; - // } + return Promise.reject(this.redisError); // no need to wrap as an SplitError + } return this.redis.get(this.keys.buildSplitKey(name)); } @@ -149,14 +150,14 @@ export default class SplitsCacheInRedis implements ISplitsCacheAsync { ttCount = parseInt(ttCount as string, 10); if (!isFiniteNumber(ttCount) || ttCount < 0) { - this.log.info(logPrefix + `Could not validate traffic type existance of ${trafficType} due to data corruption of some sorts.`); + this.log.info(logPrefix + `Could not validate traffic type existence of ${trafficType} due to data corruption of some sorts.`); return false; } return ttCount > 0; }) .catch(e => { - this.log.error(logPrefix + `Could not validate traffic type existance of ${trafficType} due to an error: ${e}.`); + this.log.error(logPrefix + `Could not validate traffic type existence of ${trafficType} due to an error: ${e}.`); // If there is an error, bypass the validation so the event can get tracked. return true; }); @@ -181,12 +182,11 @@ export default class SplitsCacheInRedis implements ISplitsCacheAsync { * Returned promise is Rejected with an SplitError if redis operation fails. */ getSplits(names: string[]): Promise> { - // @TODO remove next block if not required - // if (this.redisError) { - // this.log.error(logPrefix + this.redisError); + if (this.redisError) { + this.log.error(logPrefix + this.redisError); - // throw this.redisError; - // } + return Promise.reject(this.redisError); // no need to wrap as an SplitError + } const splits: Record = {}; const keys = names.map(name => this.keys.buildSplitKey(name)); diff --git a/src/storages/pluggable/SplitsCachePluggable.ts b/src/storages/pluggable/SplitsCachePluggable.ts index 68e9486b..cc035cea 100644 --- a/src/storages/pluggable/SplitsCachePluggable.ts +++ b/src/storages/pluggable/SplitsCachePluggable.ts @@ -17,10 +17,10 @@ export class SplitsCachePluggable implements ISplitsCacheAsync { private readonly wrapper: ICustomStorageWrapper; /** - * - * @param log logger instance - * @param keys key builder - * @param wrapper adapted wrapper storage + * Create a SplitsCache that uses a custom storage wrapper. + * @param log Logger instance. + * @param keys Key builder. + * @param wrapper Adapted wrapper storage. */ constructor(log: ILogger, keys: KeyBuilder, wrapper: ICustomStorageWrapper) { this.log = log; @@ -167,7 +167,7 @@ export class SplitsCachePluggable implements ISplitsCacheAsync { } /** - * Check traffic type existance. + * Check traffic type existence. * The returned promise is resolved with a boolean indicating whether the TT exist or not. * In case of wrapper operation failures, the promise resolves with a true value, assuming that the TT might exist. * It will never be rejected. @@ -180,13 +180,13 @@ export class SplitsCachePluggable implements ISplitsCacheAsync { ttCount = parseInt(ttCount as string, 10); if (!isFiniteNumber(ttCount) || ttCount < 0) { - this.log.info(logPrefix + `Could not validate traffic type existance of ${trafficType} due to data corruption of some sorts.`); + this.log.info(logPrefix + `Could not validate traffic type existence of ${trafficType} due to data corruption of some sorts.`); return false; } return ttCount > 0; }).catch(e => { - this.log.error(logPrefix + `Could not validate traffic type existance of ${trafficType} due to an error: ${e}.`); + this.log.error(logPrefix + `Could not validate traffic type existence of ${trafficType} due to an error: ${e}.`); // If there is an error, bypass the validation so the event can get tracked. return true; }); diff --git a/src/utils/inputValidation/__tests__/splitExistance.spec.ts b/src/utils/inputValidation/__tests__/splitExistance.spec.ts index 4bc2b8e8..7d0d7b39 100644 --- a/src/utils/inputValidation/__tests__/splitExistance.spec.ts +++ b/src/utils/inputValidation/__tests__/splitExistance.spec.ts @@ -7,7 +7,7 @@ import { validateSplitExistance } from '../splitExistance'; import { IReadinessManager } from '../../../readiness/types'; import { WARN_NOT_EXISTENT_SPLIT } from '../../../logger/constants'; -describe('Split existance (special case)', () => { +describe('Split existence (special case)', () => { afterEach(() => { loggerMock.mockClear(); }); diff --git a/src/utils/inputValidation/__tests__/trafficTypeExistance.spec.ts b/src/utils/inputValidation/__tests__/trafficTypeExistance.spec.ts index ba2f1666..293ccb84 100644 --- a/src/utils/inputValidation/__tests__/trafficTypeExistance.spec.ts +++ b/src/utils/inputValidation/__tests__/trafficTypeExistance.spec.ts @@ -57,7 +57,7 @@ describe('validateTrafficTypeExistance', () => { readinessManagerMock.isReady.mockImplementation(() => true); expect(validateTrafficTypeExistance(loggerMock, readinessManagerMock, splitsCacheMock, STANDALONE_MODE, TEST_EXISTENT_TT, 'test_method')).toBe(true); // If the SDK is in condition to validate but the TT exists, it will return true. - expect(splitsCacheMock.trafficTypeExists.mock.calls).toEqual([[TEST_EXISTENT_TT]]); // If the SDK is in condition to validate, it checks that TT existance with the storage. + expect(splitsCacheMock.trafficTypeExists.mock.calls).toEqual([[TEST_EXISTENT_TT]]); // If the SDK is in condition to validate, it checks that TT existence with the storage. expect(loggerMock.warn).not.toBeCalled(); // If the SDK is in condition to validate but the TT exists, it will not log any warnings. expect(loggerMock.error).not.toBeCalled(); // If the SDK is in condition to validate but the TT exists, it will not log any errors. }); @@ -65,7 +65,7 @@ describe('validateTrafficTypeExistance', () => { test('Should return false and log warning if SDK Ready, not localhost mode and the traffic type does NOT exist in the storage', () => { // Ready, standalone, and the TT not exists in the storage. expect(validateTrafficTypeExistance(loggerMock, readinessManagerMock, splitsCacheMock, STANDALONE_MODE, TEST_NOT_EXISTENT_TT, 'test_method_y')).toBe(false); // If the SDK is in condition to validate but the TT does not exist in the storage, it will return false. - expect(splitsCacheMock.trafficTypeExists.mock.calls).toEqual([[TEST_NOT_EXISTENT_TT]]); // If the SDK is in condition to validate, it checks that TT existance with the storage. + expect(splitsCacheMock.trafficTypeExists.mock.calls).toEqual([[TEST_NOT_EXISTENT_TT]]); // If the SDK is in condition to validate, it checks that TT existence with the storage. expect(loggerMock.warn).toBeCalledWith(WARN_NOT_EXISTENT_TT, ['test_method_y', TEST_NOT_EXISTENT_TT]); // If the SDK is in condition to validate but the TT does not exist in the storage, it will log the expected warning. expect(loggerMock.error).not.toBeCalled(); // It logged a warning so no errors should be logged. }); @@ -75,7 +75,7 @@ describe('validateTrafficTypeExistance', () => { const validationPromise = validateTrafficTypeExistance(loggerMock, readinessManagerMock, splitsCacheMock, STANDALONE_MODE, TEST_EXISTENT_ASYNC_TT, 'test_method_z'); expect(thenable(validationPromise)).toBe(true); // If the storage is async, it should also return a promise. - expect(splitsCacheMock.trafficTypeExists.mock.calls).toEqual([[TEST_EXISTENT_ASYNC_TT]]); // If the SDK is in condition to validate, it checks that TT existance with the async storage. + expect(splitsCacheMock.trafficTypeExists.mock.calls).toEqual([[TEST_EXISTENT_ASYNC_TT]]); // If the SDK is in condition to validate, it checks that TT existence with the async storage. expect(loggerMock.warn).not.toBeCalled(); // We are still fetching the data from the storage, no logs yet. expect(loggerMock.error).not.toBeCalled(); // We are still fetching the data from the storage, no logs yet. @@ -90,7 +90,7 @@ describe('validateTrafficTypeExistance', () => { const validationPromise2 = validateTrafficTypeExistance(loggerMock, readinessManagerMock, splitsCacheMock, STANDALONE_MODE, TEST_NOT_EXISTENT_ASYNC_TT, 'test_method_z'); expect(thenable(validationPromise2)).toBe(true); // If the storage is async, it should also return a promise. - expect(splitsCacheMock.trafficTypeExists.mock.calls).toEqual([[TEST_NOT_EXISTENT_ASYNC_TT]]); // If the SDK is in condition to validate, it checks that TT existance with the async storage. + expect(splitsCacheMock.trafficTypeExists.mock.calls).toEqual([[TEST_NOT_EXISTENT_ASYNC_TT]]); // If the SDK is in condition to validate, it checks that TT existence with the async storage. expect(loggerMock.warn).not.toBeCalled(); // We are still fetching the data from the storage, no logs yet. expect(loggerMock.error).not.toBeCalled(); // We are still fetching the data from the storage, no logs yet. From e487f86c6eee31fded054098867ba6a8419846b1 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 28 Apr 2021 18:47:13 -0300 Subject: [PATCH 016/144] implementation and unit tests --- .../pluggable/EventsCachePluggable.ts | 45 +++++ .../pluggable/ImpressionsCachePluggable.ts | 61 ++++++ .../__tests__/EventsCachePluggable.spec.ts | 77 ++++++++ .../ImpressionsCachePluggable.spec.ts | 84 ++++++++ .../pluggable/__tests__/wrapper.mock.ts | 184 +++++++++--------- .../__tests__/wrapperAdapter.spec.ts | 2 +- 6 files changed, 362 insertions(+), 91 deletions(-) create mode 100644 src/storages/pluggable/EventsCachePluggable.ts create mode 100644 src/storages/pluggable/ImpressionsCachePluggable.ts create mode 100644 src/storages/pluggable/__tests__/EventsCachePluggable.spec.ts create mode 100644 src/storages/pluggable/__tests__/ImpressionsCachePluggable.spec.ts diff --git a/src/storages/pluggable/EventsCachePluggable.ts b/src/storages/pluggable/EventsCachePluggable.ts new file mode 100644 index 00000000..eef10dd1 --- /dev/null +++ b/src/storages/pluggable/EventsCachePluggable.ts @@ -0,0 +1,45 @@ +import { ICustomStorageWrapper, IEventsCacheAsync } from '../types'; +import { IRedisMetadata } from '../../dtos/types'; +import KeyBuilderSS from '../KeyBuilderSS'; +import { SplitIO } from '../../types'; +import { ILogger } from '../../logger/types'; +import { logPrefix } from './constants'; + +export class EventsCachePluggable implements IEventsCacheAsync { + + private readonly log: ILogger; + private readonly wrapper: ICustomStorageWrapper; + private readonly keys: KeyBuilderSS; + private readonly metadata: IRedisMetadata; + + constructor(log: ILogger, keys: KeyBuilderSS, wrapper: ICustomStorageWrapper, metadata: IRedisMetadata) { + this.log = log; + this.keys = keys; + this.wrapper = wrapper; + this.metadata = metadata; + } + + /** + * Push given event to the storage. + * @param eventData Event item to push. + * @returns A promise that is resolved with a boolean value indicating if the push operation succeeded or failed. + * The promise will never be rejected. + */ + track(eventData: SplitIO.EventData): Promise { + return this.wrapper.pushItems( + this.keys.buildEventsKey(), + [this._toJSON(eventData)] + ).catch(e => { + this.log.error(logPrefix + ` Error adding event to queue: ${e}.`); + return false; + }); + } + + private _toJSON(eventData: SplitIO.EventData): string { + return JSON.stringify({ + m: this.metadata, + e: eventData + }); + } + +} diff --git a/src/storages/pluggable/ImpressionsCachePluggable.ts b/src/storages/pluggable/ImpressionsCachePluggable.ts new file mode 100644 index 00000000..643b64de --- /dev/null +++ b/src/storages/pluggable/ImpressionsCachePluggable.ts @@ -0,0 +1,61 @@ +import { ICustomStorageWrapper, IImpressionsCacheAsync } from '../types'; +import { IRedisMetadata } from '../../dtos/types'; +import { ImpressionDTO } from '../../types'; +import KeyBuilderSS from '../KeyBuilderSS'; +import { ILogger } from '../../logger/types'; +import { logPrefix } from './constants'; + +export class ImpressionsCachePluggable implements IImpressionsCacheAsync { + + private readonly log: ILogger; + private readonly keys: KeyBuilderSS; + private readonly wrapper: ICustomStorageWrapper; + private readonly metadata: IRedisMetadata; + + constructor(log: ILogger, keys: KeyBuilderSS, wrapper: ICustomStorageWrapper, metadata: IRedisMetadata) { + this.log = log; + this.keys = keys; + this.wrapper = wrapper; + this.metadata = metadata; + } + + /** + * Push given impressions to the storage. + * @param impressions List of impresions to push. + * @returns A promise that is resolved with a boolean value indicating if the push operation succeeded or failed. + * The promise will never be rejected. + */ + track(impressions: ImpressionDTO[]): Promise { + return this.wrapper.pushItems( + this.keys.buildImpressionsKey(), + this._toJSON(impressions) + ).catch((e) => { + this.log.error(logPrefix + ` Error adding event to queue: ${e}.`); + return false; + }); + } + + private _toJSON(impressions: ImpressionDTO[]): string[] { + return impressions.map(impression => { + const { + keyName, bucketingKey, feature, treatment, label, time, changeNumber + } = impression; + + return JSON.stringify({ + m: this.metadata, + i: { + k: keyName, + b: bucketingKey, + f: feature, + t: treatment, + r: label, + c: changeNumber, + m: time + } + }); + }); + } + + // @TODO implement producer methods + +} diff --git a/src/storages/pluggable/__tests__/EventsCachePluggable.spec.ts b/src/storages/pluggable/__tests__/EventsCachePluggable.spec.ts new file mode 100644 index 00000000..e7f168fb --- /dev/null +++ b/src/storages/pluggable/__tests__/EventsCachePluggable.spec.ts @@ -0,0 +1,77 @@ +import find from 'lodash/find'; +import isEqual from 'lodash/isEqual'; +import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; +import KeyBuilderSS from '../../KeyBuilderSS'; +import { EventsCachePluggable } from '../EventsCachePluggable'; +import { metadataBuilder } from '../../inRedis'; +import { wrapperMockFactory } from './wrapper.mock'; +import { wrapperAdapter } from '../wrapperAdapter'; + +const prefix = 'events_cache_ut'; +const metadata = { version: 'js_someversion', ip: 'some_ip', hostname: 'some_hostname' }; +const keys = new KeyBuilderSS(prefix, metadata); +const key = keys.buildEventsKey(); + +const fakeRedisMetadata = metadataBuilder(metadata); +const fakeEvent1 = { event: 1 }; +const fakeEvent2 = { event: '2' }; +const fakeEvent3 = { event: null }; + +describe('PLUGGABLE EVENTS CACHE', () => { + + test('`track` method should push values into the pluggable storage', async () => { + const wrapperMock = wrapperMockFactory(); + const cache = new EventsCachePluggable(loggerMock, keys, wrapperMock, fakeRedisMetadata); + + // @ts-expect-error + expect(await cache.track(fakeEvent1)).toBe(true); // If the queueing operation was successful, it should resolve the returned promise with "true" + // @ts-expect-error + expect(await cache.track(fakeEvent2)).toBe(true); // If the queueing operation was successful, it should resolve the returned promise with "true" + // @ts-expect-error + expect(await cache.track(fakeEvent3)).toBe(true); // If the queueing operation was successful, it should resolve the returned promise with "true" + + const redisValues = wrapperMock._queues[key]; + + expect(redisValues.length).toBe(3); // After pushing we should have on Redis as many events as we have stored. + expect(typeof redisValues[0]).toBe('string'); // All elements should be strings since those are stringified JSONs. + expect(typeof redisValues[1]).toBe('string'); // All elements should be strings since those are stringified JSONs. + expect(typeof redisValues[2]).toBe('string'); // All elements should be strings since those are stringified JSONs. + + const findMatchingElem = (event: any) => { + return find(redisValues, elem => { + const parsedElem = JSON.parse(elem); + return isEqual(parsedElem.e, event) && isEqual(parsedElem.m, fakeRedisMetadata); + }); + }; + + /* If the elements are found, then the values are correct. */ + const foundEv1 = findMatchingElem(fakeEvent1); + const foundEv2 = findMatchingElem(fakeEvent2); + const foundEv3 = findMatchingElem(fakeEvent3); + expect(foundEv1).not.toBe(undefined); // Events stored on redis matched the values we are expecting. + expect(foundEv2).not.toBe(undefined); // Events stored on redis matched the values we are expecting. + expect(foundEv3).not.toBe(undefined); // Events stored on redis matched the values we are expecting. + + wrapperMock.mockClear(); + }); + + test('`track` method result should not reject if wrapper operation fails', async () => { + const wrapperMock = wrapperMockFactory(); + // @ts-ignore. I'll use a "bad" queue to force an exception with the pushItems wrapper operation. + wrapperMock._queues['non-list-key'] = 10; + // @ts-expect-error. wrapperMock is adapted this time to properly handle unexpected exceptions + const faultyCache = new EventsCachePluggable(loggerMock, { + buildEventsKey: () => 'non-list-key' + }, wrapperAdapter(loggerMock, wrapperMock), fakeRedisMetadata); + + // @ts-expect-error + expect(await faultyCache.track(fakeEvent1)).toBe(false); // If the queueing operation was NOT successful, it should resolve the returned promise with "false" instead of rejecting it. + // @ts-expect-error + expect(await faultyCache.track(fakeEvent2)).toBe(false); // If the queueing operation was NOT successful, it should resolve the returned promise with "false" instead of rejecting it. + // @ts-expect-error + expect(await faultyCache.track(fakeEvent3)).toBe(false); // If the queueing operation was NOT successful, it should resolve the returned promise with "false" instead of rejecting it. + + wrapperMock.mockClear(); + }); + +}); diff --git a/src/storages/pluggable/__tests__/ImpressionsCachePluggable.spec.ts b/src/storages/pluggable/__tests__/ImpressionsCachePluggable.spec.ts new file mode 100644 index 00000000..42dbeedb --- /dev/null +++ b/src/storages/pluggable/__tests__/ImpressionsCachePluggable.spec.ts @@ -0,0 +1,84 @@ + +import KeyBuilderSS from '../../KeyBuilderSS'; +import { ImpressionsCachePluggable } from '../ImpressionsCachePluggable'; +import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; +import { wrapperMock } from './wrapper.mock'; +import { IRedisMetadata } from '../../../dtos/types'; + +const prefix = 'impr_cache_ut'; +const impressionsKey = `${prefix}.impressions`; +const testMeta: IRedisMetadata = { i: 'some_ip', n: 'some_host', s: 'some_sdk_version' }; // @ts-ignore +const keys = new KeyBuilderSS(prefix, testMeta); + +const o1 = { + feature: 'test1', + keyName: 'facundo@split.io', + treatment: 'on', + time: Date.now(), + label: 'default rule', + changeNumber: 1 +}; + +const o2 = { + feature: 'test2', + keyName: 'pepep@split.io', + treatment: 'A', + time: Date.now(), + bucketingKey: '1234-5678', + label: 'is in segment', + changeNumber: 1 +}; + +const o3 = { + feature: 'test3', + keyName: 'pipiip@split.io', + treatment: 'B', + time: Date.now(), + label: 'default rule', + changeNumber: 1 +}; + +describe('PLUGGABLE IMPRESSIONS CACHE', () => { + + afterEach(() => { + loggerMock.mockClear(); + wrapperMock.mockClear(); + }); + + test('`track` method should push values into the pluggable storage', async () => { + const cache = new ImpressionsCachePluggable(loggerMock, keys, wrapperMock, testMeta); + + expect(await cache.track([o1])).toBe(true); // result should resolve to true + + let state = wrapperMock._queues[impressionsKey]; + expect(state.length).toBe(1); // impression should be stored + + expect(await cache.track([o2, o3])).toBe(true); + + state = wrapperMock._queues[impressionsKey]; + expect(state.length).toBe(3); + // This is testing both the track and the toJSON method. + expect(state[0]).toBe(JSON.stringify({ + m: testMeta, + i: { k: o1.keyName, f: o1.feature, t: o1.treatment, r: o1.label, c: o1.changeNumber, m: o1.time } + })); + expect(state[1]).toBe(JSON.stringify({ + m: testMeta, + i: { k: o2.keyName, b: o2.bucketingKey, f: o2.feature, t: o2.treatment, r: o2.label, c: o2.changeNumber, m: o2.time } + })); + expect(state[2]).toBe(JSON.stringify({ + m: testMeta, + i: { k: o3.keyName, f: o3.feature, t: o3.treatment, r: o3.label, c: o3.changeNumber, m: o3.time } + })); + + }); + + test('`track` method result should not reject if wrapper operation fails', async () => { + // make wrapper operation fail + wrapperMock.pushItems.mockImplementation(()=>Promise.reject()); + const cache = new ImpressionsCachePluggable(loggerMock, keys, wrapperMock, testMeta); + + expect(await cache.track([o1])).toBe(false); // result should resolve to false + }); + +}); diff --git a/src/storages/pluggable/__tests__/wrapper.mock.ts b/src/storages/pluggable/__tests__/wrapper.mock.ts index 7be7f9fe..352c67cb 100644 --- a/src/storages/pluggable/__tests__/wrapper.mock.ts +++ b/src/storages/pluggable/__tests__/wrapper.mock.ts @@ -1,97 +1,101 @@ import { startsWith, toNumber } from '../../../utils/lang'; -let _cache: Record = {}; -let _queues: Record = {}; +// Creates an in memory ICustomStorageWrapper implementation with Jest mocks +export function wrapperMockFactory() { -// An in memory ICustomStorageWrapper implementation with Jest mocks -export const wrapperMock = { + let _cache: Record = {}; + let _queues: Record = {}; - _cache, - _queues, + return { + _cache, + _queues, - get: jest.fn((key => { - return Promise.resolve(key in _cache ? _cache[key] : null); - })), - set: jest.fn((key: string, value: string) => { - const result = key in _cache; - _cache[key] = value; - return Promise.resolve(result); - }), - getAndSet: jest.fn((key: string, value: string) => { - const result = key in _cache ? _cache[key] : null; - _cache[key] = value; - return Promise.resolve(result); - }), - del: jest.fn((key: string) => { - const result = key in _cache; - delete _cache[key]; - return Promise.resolve(result); - }), - getKeysByPrefix: jest.fn((prefix: string) => { - return Promise.resolve(Object.keys(_cache).filter(key => startsWith(key, prefix))); - }), - getByPrefix: jest.fn((prefix: string) => { - return Promise.resolve(Object.keys(_cache).filter(key => startsWith(key, prefix)).map(key => _cache[key])); - }), - incr: jest.fn((key: string) => { - if (key in _cache) { - const count = toNumber(_cache[key]) + 1; - if (isNaN(count)) return Promise.resolve(false); - _cache[key] = count + ''; - } else { - _cache[key] = '1'; - } - return Promise.resolve(true); - }), - decr: jest.fn((key: string) => { - if (key in _cache) { - const count = toNumber(_cache[key]) - 1; - if (isNaN(count)) return Promise.resolve(false); - _cache[key] = count + ''; - } else { - _cache[key] = '-1'; - } - return Promise.resolve(true); - }), - getMany: jest.fn((keys: string[]) => { - return Promise.resolve(keys.map(key => _cache[key] ? _cache[key] : null)); - }), - pushItems: jest.fn((key: string, items: string[]) => { - if (!(key in _queues)) _queues[key] = []; - _queues[key].push(...items); - return Promise.resolve(true); - }), - popItems: jest.fn((key: string, count: number) => { - return Promise.resolve(key in _queues ? _queues[key].splice(0, count) : []); - }), - getItemsCount: jest.fn((key: string) => { - return Promise.resolve(key in _queues ? _queues[key].length : 0); - }), - itemContains: jest.fn((key: string, item: string) => { - return Promise.resolve(key in _queues && _queues[key].indexOf(item) > -1 ? true : false); - }), + get: jest.fn((key => { + return Promise.resolve(key in _cache ? _cache[key] : null); + })), + set: jest.fn((key: string, value: string) => { + const result = key in _cache; + _cache[key] = value; + return Promise.resolve(result); + }), + getAndSet: jest.fn((key: string, value: string) => { + const result = key in _cache ? _cache[key] : null; + _cache[key] = value; + return Promise.resolve(result); + }), + del: jest.fn((key: string) => { + const result = key in _cache; + delete _cache[key]; + return Promise.resolve(result); + }), + getKeysByPrefix: jest.fn((prefix: string) => { + return Promise.resolve(Object.keys(_cache).filter(key => startsWith(key, prefix))); + }), + getByPrefix: jest.fn((prefix: string) => { + return Promise.resolve(Object.keys(_cache).filter(key => startsWith(key, prefix)).map(key => _cache[key])); + }), + incr: jest.fn((key: string) => { + if (key in _cache) { + const count = toNumber(_cache[key]) + 1; + if (isNaN(count)) return Promise.resolve(false); + _cache[key] = count + ''; + } else { + _cache[key] = '1'; + } + return Promise.resolve(true); + }), + decr: jest.fn((key: string) => { + if (key in _cache) { + const count = toNumber(_cache[key]) - 1; + if (isNaN(count)) return Promise.resolve(false); + _cache[key] = count + ''; + } else { + _cache[key] = '-1'; + } + return Promise.resolve(true); + }), + getMany: jest.fn((keys: string[]) => { + return Promise.resolve(keys.map(key => _cache[key] ? _cache[key] : null)); + }), + pushItems: jest.fn((key: string, items: string[]) => { + if (!(key in _queues)) _queues[key] = []; + _queues[key].push(...items); + return Promise.resolve(true); + }), + popItems: jest.fn((key: string, count: number) => { + return Promise.resolve(key in _queues ? _queues[key].splice(0, count) : []); + }), + getItemsCount: jest.fn((key: string) => { + return Promise.resolve(key in _queues ? _queues[key].length : 0); + }), + itemContains: jest.fn((key: string, item: string) => { + return Promise.resolve(key in _queues && _queues[key].indexOf(item) > -1 ? true : false); + }), - // always connects and close - connect: jest.fn(() => Promise.resolve(true)), - close: jest.fn(() => Promise.resolve()), + // always connects and close + connect: jest.fn(() => Promise.resolve(true)), + close: jest.fn(() => Promise.resolve()), + + mockClear() { + this._cache = {}; + this._queues = {}; + this.get.mockClear(); + this.set.mockClear(); + this.del.mockClear(); + this.getKeysByPrefix.mockClear(); + this.incr.mockClear(); + this.decr.mockClear(); + this.getMany.mockClear(); + this.connect.mockClear(); + this.close.mockClear(); + this.getAndSet.mockClear(); + this.getByPrefix.mockClear(); + this.pushItems.mockClear(); + this.popItems.mockClear(); + this.getItemsCount.mockClear(); + this.itemContains.mockClear(); + } + }; +} - mockClear() { - _cache = {}; - _queues = {}; - this.get.mockClear(); - this.set.mockClear(); - this.del.mockClear(); - this.getKeysByPrefix.mockClear(); - this.incr.mockClear(); - this.decr.mockClear(); - this.getMany.mockClear(); - this.connect.mockClear(); - this.close.mockClear(); - this.getAndSet.mockClear(); - this.getByPrefix.mockClear(); - this.pushItems.mockClear(); - this.popItems.mockClear(); - this.getItemsCount.mockClear(); - this.itemContains.mockClear(); - } -}; +export const wrapperMock = wrapperMockFactory(); diff --git a/src/storages/pluggable/__tests__/wrapperAdapter.spec.ts b/src/storages/pluggable/__tests__/wrapperAdapter.spec.ts index ea0f2c22..c6292f87 100644 --- a/src/storages/pluggable/__tests__/wrapperAdapter.spec.ts +++ b/src/storages/pluggable/__tests__/wrapperAdapter.spec.ts @@ -6,7 +6,7 @@ import { SplitError } from '../../../utils/lang/errors'; /** Mocks */ -import { wrapperMock } from './wrapper.mock'; // Well implemented wrapper +import { wrapperMock } from './wrapper.mock'; function throwsException() { throw new Error('some error'); From 160944e23e7c3deb7f91a451ef04747aff08c291 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 29 Apr 2021 19:59:26 -0300 Subject: [PATCH 017/144] applied feedback --- .../inLocalStorage/MySegmentsCacheInLocal.ts | 8 +++--- .../inLocalStorage/SplitsCacheInLocal.ts | 20 +++++++-------- src/storages/inLocalStorage/constants.ts | 2 +- src/storages/inLocalStorage/index.ts | 4 +-- src/storages/inRedis/EventsCacheInRedis.ts | 5 ++-- src/storages/inRedis/RedisAdapter.ts | 20 +++++++-------- src/storages/inRedis/SplitsCacheInRedis.ts | 13 +++++----- .../inRedis/__tests__/RedisAdapter.spec.ts | 24 +++++++++--------- src/storages/inRedis/constants.ts | 1 + .../pluggable/EventsCachePluggable.ts | 13 ++++++---- .../pluggable/ImpressionsCachePluggable.ts | 13 ++++++---- .../pluggable/SegmentsCachePluggable.ts | 4 +-- .../__tests__/EventsCachePluggable.spec.ts | 20 +++++++-------- .../ImpressionsCachePluggable.spec.ts | 6 ++--- .../pluggable/__tests__/wrapper.mock.ts | 25 +++++++++++-------- .../__tests__/wrapperAdapter.spec.ts | 4 +-- src/storages/pluggable/constants.ts | 2 +- src/storages/pluggable/wrapperAdapter.ts | 10 ++++---- src/storages/types.ts | 14 +++++++---- 19 files changed, 110 insertions(+), 98 deletions(-) create mode 100644 src/storages/inRedis/constants.ts diff --git a/src/storages/inLocalStorage/MySegmentsCacheInLocal.ts b/src/storages/inLocalStorage/MySegmentsCacheInLocal.ts index 020f823a..12289470 100644 --- a/src/storages/inLocalStorage/MySegmentsCacheInLocal.ts +++ b/src/storages/inLocalStorage/MySegmentsCacheInLocal.ts @@ -1,7 +1,7 @@ import { ILogger } from '../../logger/types'; import AbstractSegmentsCacheSync from '../AbstractSegmentsCacheSync'; import KeyBuilderCS from '../KeyBuilderCS'; -import { logPrefix, DEFINED } from './constants'; +import { LOG_PREFIX, DEFINED } from './constants'; export default class MySegmentsCacheInLocal extends AbstractSegmentsCacheSync { @@ -20,7 +20,7 @@ export default class MySegmentsCacheInLocal extends AbstractSegmentsCacheSync { * @NOTE this method is not being used at the moment. */ clear() { - this.log.info(logPrefix + 'Flushing MySegments data from localStorage'); + this.log.info(LOG_PREFIX + 'Flushing MySegments data from localStorage'); // We cannot simply call `localStorage.clear()` since that implies removing user items from the storage // We could optimize next sentence, since it implies iterating over all localStorage items @@ -34,7 +34,7 @@ export default class MySegmentsCacheInLocal extends AbstractSegmentsCacheSync { localStorage.setItem(segmentKey, DEFINED); return true; } catch (e) { - this.log.error(logPrefix + e); + this.log.error(LOG_PREFIX + e); return false; } } @@ -46,7 +46,7 @@ export default class MySegmentsCacheInLocal extends AbstractSegmentsCacheSync { localStorage.removeItem(segmentKey); return true; } catch (e) { - this.log.error(logPrefix + e); + this.log.error(LOG_PREFIX + e); return false; } } diff --git a/src/storages/inLocalStorage/SplitsCacheInLocal.ts b/src/storages/inLocalStorage/SplitsCacheInLocal.ts index 58d5ba09..58f255f7 100644 --- a/src/storages/inLocalStorage/SplitsCacheInLocal.ts +++ b/src/storages/inLocalStorage/SplitsCacheInLocal.ts @@ -3,7 +3,7 @@ import AbstractSplitsCacheSync, { usesSegments } from '../AbstractSplitsCacheSyn import { isFiniteNumber, toNumber, isNaNNumber } from '../../utils/lang'; import KeyBuilderCS from '../KeyBuilderCS'; import { ILogger } from '../../logger/types'; -import { logPrefix } from './constants'; +import { LOG_PREFIX } from './constants'; /** * ISplitsCacheSync implementation that stores split definitions in browser LocalStorage. @@ -52,7 +52,7 @@ export default class SplitsCacheInLocal extends AbstractSplitsCacheSync { } } } catch (e) { - this.log.error(logPrefix + e); + this.log.error(LOG_PREFIX + e); } } @@ -72,7 +72,7 @@ export default class SplitsCacheInLocal extends AbstractSplitsCacheSync { } } } catch (e) { - this.log.error(logPrefix + e); + this.log.error(LOG_PREFIX + e); } } @@ -82,7 +82,7 @@ export default class SplitsCacheInLocal extends AbstractSplitsCacheSync { * We cannot simply call `localStorage.clear()` since that implies removing user items from the storage. */ clear() { - this.log.info(logPrefix + 'Flushing Splits data from localStorage'); + this.log.info(LOG_PREFIX + 'Flushing Splits data from localStorage'); // collect item keys const len = localStorage.length; @@ -114,7 +114,7 @@ export default class SplitsCacheInLocal extends AbstractSplitsCacheSync { return true; } catch (e) { - this.log.error(logPrefix + e); + this.log.error(LOG_PREFIX + e); return false; } } @@ -129,7 +129,7 @@ export default class SplitsCacheInLocal extends AbstractSplitsCacheSync { return 1; } catch (e) { - this.log.error(logPrefix + e); + this.log.error(LOG_PREFIX + e); return 0; } } @@ -147,14 +147,14 @@ export default class SplitsCacheInLocal extends AbstractSplitsCacheSync { // when using a new split query, we must update it at the store if (this.updateNewFilter) { - this.log.info(logPrefix + 'Split filter query was modified. Updating cache.'); + this.log.info(LOG_PREFIX + 'Split filter query was modified. Updating cache.'); const queryKey = this.keys.buildSplitsFilterQueryKey(); const queryString = this.splitFiltersValidation.queryString; try { if (queryString) localStorage.setItem(queryKey, queryString); else localStorage.removeItem(queryKey); } catch (e) { - this.log.error(logPrefix + e); + this.log.error(LOG_PREFIX + e); } this.updateNewFilter = false; } @@ -166,7 +166,7 @@ export default class SplitsCacheInLocal extends AbstractSplitsCacheSync { this.hasSync = true; return true; } catch (e) { - this.log.error(logPrefix + e); + this.log.error(LOG_PREFIX + e); return false; } } @@ -273,7 +273,7 @@ export default class SplitsCacheInLocal extends AbstractSplitsCacheSync { }); } } catch (e) { - this.log.error(logPrefix + e); + this.log.error(LOG_PREFIX + e); } } // if the filter didn't change, nothing is done diff --git a/src/storages/inLocalStorage/constants.ts b/src/storages/inLocalStorage/constants.ts index 74624310..8dcb6c3e 100644 --- a/src/storages/inLocalStorage/constants.ts +++ b/src/storages/inLocalStorage/constants.ts @@ -1,2 +1,2 @@ -export const logPrefix = 'storage:localstorage: '; +export const LOG_PREFIX = 'storage:localstorage: '; export const DEFINED = '1'; diff --git a/src/storages/inLocalStorage/index.ts b/src/storages/inLocalStorage/index.ts index 226d771c..a26587d1 100644 --- a/src/storages/inLocalStorage/index.ts +++ b/src/storages/inLocalStorage/index.ts @@ -10,7 +10,7 @@ import MySegmentsCacheInMemory from '../inMemory/MySegmentsCacheInMemory'; import SplitsCacheInMemory from '../inMemory/SplitsCacheInMemory'; import { DEFAULT_CACHE_EXPIRATION_IN_MILLIS } from '../../utils/constants/browser'; import { InMemoryStorageCSFactory } from '../inMemory/InMemoryStorageCS'; -import { logPrefix } from './constants'; +import { LOG_PREFIX } from './constants'; export interface InLocalStorageOptions { prefix?: string @@ -27,7 +27,7 @@ export function InLocalStorage(options: InLocalStorageOptions = {}) { // Fallback to InMemoryStorage if LocalStorage API is not available if (!isLocalStorageAvailable()) { - params.log.warn(logPrefix + 'LocalStorage API is unavailable. Fallbacking into default MEMORY storage'); + params.log.warn(LOG_PREFIX + 'LocalStorage API is unavailable. Fallbacking into default MEMORY storage'); return InMemoryStorageCSFactory(params); } diff --git a/src/storages/inRedis/EventsCacheInRedis.ts b/src/storages/inRedis/EventsCacheInRedis.ts index 5a8caab5..97f7d13f 100644 --- a/src/storages/inRedis/EventsCacheInRedis.ts +++ b/src/storages/inRedis/EventsCacheInRedis.ts @@ -4,8 +4,7 @@ import KeyBuilderSS from '../KeyBuilderSS'; import { Redis } from 'ioredis'; import { SplitIO } from '../../types'; import { ILogger } from '../../logger/types'; - -const logPrefix = 'storage:redis: '; +import { LOG_PREFIX } from './constants'; export default class EventsCacheInRedis implements IEventsCacheAsync { @@ -33,7 +32,7 @@ export default class EventsCacheInRedis implements IEventsCacheAsync { // We use boolean values to signal successful queueing .then(() => true) .catch(err => { - this.log.error(logPrefix + `Error adding event to queue: ${err}.`); + this.log.error(LOG_PREFIX + `Error adding event to queue: ${err}.`); return false; }); } diff --git a/src/storages/inRedis/RedisAdapter.ts b/src/storages/inRedis/RedisAdapter.ts index 7d0636bc..de0abee3 100644 --- a/src/storages/inRedis/RedisAdapter.ts +++ b/src/storages/inRedis/RedisAdapter.ts @@ -5,7 +5,7 @@ import { _Set, setToArray, ISet } from '../../utils/lang/sets'; import thenable from '../../utils/promise/thenable'; import timeout from '../../utils/promise/timeout'; -const logPrefix = 'storage:redis-adapter: '; +const LOG_PREFIX = 'storage:redis-adapter: '; // If we ever decide to fully wrap every method, there's a Commander.getBuiltinCommands from ioredis. const METHODS_TO_PROMISE_WRAP = ['set', 'exec', 'del', 'get', 'keys', 'sadd', 'srem', 'sismember', 'smembers', 'incr', 'rpush', 'pipeline', 'expire', 'mget']; @@ -55,16 +55,16 @@ export default class RedisAdapter extends ioredis { _listenToEvents() { this.once('ready', () => { const commandsCount = this._notReadyCommandsQueue ? this._notReadyCommandsQueue.length : 0; - this.log.info(logPrefix + `Redis connection established. Queued commands: ${commandsCount}.`); + this.log.info(LOG_PREFIX + `Redis connection established. Queued commands: ${commandsCount}.`); this._notReadyCommandsQueue && this._notReadyCommandsQueue.forEach(queued => { - this.log.info(logPrefix + `Executing queued ${queued.name} command.`); + this.log.info(LOG_PREFIX + `Executing queued ${queued.name} command.`); queued.command().then(queued.resolve).catch(queued.reject); }); // After the SDK is ready for the first time we'll stop queueing commands. This is just so we can keep handling BUR for them. this._notReadyCommandsQueue = undefined; }); this.once('close', () => { - this.log.info(logPrefix + 'Redis connection closed.'); + this.log.info(LOG_PREFIX + 'Redis connection closed.'); }); } @@ -78,7 +78,7 @@ export default class RedisAdapter extends ioredis { const params = arguments; function commandWrapper() { - instance.log.debug(logPrefix + `Executing ${method}.`); + instance.log.debug(LOG_PREFIX + `Executing ${method}.`); // Return original method const result = originalMethod.apply(instance, params); @@ -93,7 +93,7 @@ export default class RedisAdapter extends ioredis { result.then(cleanUpRunningCommandsCb, cleanUpRunningCommandsCb); return timeout(instance._options.operationTimeout, result).catch(err => { - instance.log.error(logPrefix + `${method} operation threw an error or exceeded configured timeout of ${instance._options.operationTimeout}ms. Message: ${err}`); + instance.log.error(LOG_PREFIX + `${method} operation threw an error or exceeded configured timeout of ${instance._options.operationTimeout}ms. Message: ${err}`); // Handling is not the adapter responsibility. throw err; }); @@ -126,19 +126,19 @@ export default class RedisAdapter extends ioredis { setTimeout(function deferedDisconnect() { if (instance._runningCommands.size > 0) { - instance.log.info(logPrefix + `Attempting to disconnect but there are ${instance._runningCommands.size} commands still waiting for resolution. Defering disconnection until those finish.`); + instance.log.info(LOG_PREFIX + `Attempting to disconnect but there are ${instance._runningCommands.size} commands still waiting for resolution. Defering disconnection until those finish.`); Promise.all(setToArray(instance._runningCommands)) .then(() => { - instance.log.debug(logPrefix + 'Pending commands finished successfully, disconnecting.'); + instance.log.debug(LOG_PREFIX + 'Pending commands finished successfully, disconnecting.'); originalMethod.apply(instance, params); }) .catch(e => { - instance.log.warn(logPrefix + `Pending commands finished with error: ${e}. Proceeding with disconnection.`); + instance.log.warn(LOG_PREFIX + `Pending commands finished with error: ${e}. Proceeding with disconnection.`); originalMethod.apply(instance, params); }); } else { - instance.log.debug(logPrefix + 'No commands pending execution, disconnect.'); + instance.log.debug(LOG_PREFIX + 'No commands pending execution, disconnect.'); // Nothing pending, just proceed. originalMethod.apply(instance, params); } diff --git a/src/storages/inRedis/SplitsCacheInRedis.ts b/src/storages/inRedis/SplitsCacheInRedis.ts index c601b549..7d6503e2 100644 --- a/src/storages/inRedis/SplitsCacheInRedis.ts +++ b/src/storages/inRedis/SplitsCacheInRedis.ts @@ -3,8 +3,7 @@ import KeyBuilderSS from '../KeyBuilderSS'; import { ISplitsCacheAsync } from '../types'; import { Redis } from 'ioredis'; import { ILogger } from '../../logger/types'; - -const logPrefix = 'storage:redis: '; +import { LOG_PREFIX } from './constants'; /** * Discard errors for an answer of multiple operations. @@ -83,7 +82,7 @@ export default class SplitsCacheInRedis implements ISplitsCacheAsync { */ getSplit(name: string): Promise { if (this.redisError) { - this.log.error(logPrefix + this.redisError); + this.log.error(LOG_PREFIX + this.redisError); throw this.redisError; } @@ -137,14 +136,14 @@ export default class SplitsCacheInRedis implements ISplitsCacheAsync { .then((ttCount: string | null | number) => { ttCount = parseInt(ttCount as string, 10); if (!isFiniteNumber(ttCount) || ttCount < 0) { - this.log.info(logPrefix + `Could not validate traffic type existance of ${trafficType} due to data corruption of some sorts.`); + this.log.info(LOG_PREFIX + `Could not validate traffic type existance of ${trafficType} due to data corruption of some sorts.`); return false; } return ttCount > 0; }) .catch(e => { - this.log.error(logPrefix + `Could not validate traffic type existance of ${trafficType} due to an error: ${e}.`); + this.log.error(LOG_PREFIX + `Could not validate traffic type existance of ${trafficType} due to an error: ${e}.`); // If there is an error, bypass the validation so the event can get tracked. return true; }); @@ -169,7 +168,7 @@ export default class SplitsCacheInRedis implements ISplitsCacheAsync { */ getSplits(names: string[]): Promise> { if (this.redisError) { - this.log.error(logPrefix + this.redisError); + this.log.error(LOG_PREFIX + this.redisError); throw this.redisError; } @@ -183,7 +182,7 @@ export default class SplitsCacheInRedis implements ISplitsCacheAsync { return Promise.resolve(splits); }) .catch(e => { - this.log.error(logPrefix + `Could not grab splits due to an error: ${e}.`); + this.log.error(LOG_PREFIX + `Could not grab splits due to an error: ${e}.`); return Promise.reject(e); }); } diff --git a/src/storages/inRedis/__tests__/RedisAdapter.spec.ts b/src/storages/inRedis/__tests__/RedisAdapter.spec.ts index ba369d8c..9557162d 100644 --- a/src/storages/inRedis/__tests__/RedisAdapter.spec.ts +++ b/src/storages/inRedis/__tests__/RedisAdapter.spec.ts @@ -6,7 +6,7 @@ import { _Set, setToArray } from '../../../utils/lang/sets'; // Mocking sdkLogger import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; -const logPrefix = 'storage:redis-adapter: '; +const LOG_PREFIX = 'storage:redis-adapter: '; // Mocking ioredis @@ -188,7 +188,7 @@ describe('STORAGE Redis Adapter', () => { secondCallArgs[1](); // Execute the callback for "close" expect(loggerMock.info).toBeCalledTimes(1); // The callback for the "close" event will only log info to the user about what is going on. - expect(loggerMock.info.mock.calls[0]).toEqual([logPrefix + 'Redis connection closed.']); // The callback for the "close" event will only log info to the user about what is going on. + expect(loggerMock.info.mock.calls[0]).toEqual([LOG_PREFIX + 'Redis connection closed.']); // The callback for the "close" event will only log info to the user about what is going on. loggerMock.info.mockClear(); expect(loggerMock.info).not.toBeCalled(); // Control assertion @@ -198,7 +198,7 @@ describe('STORAGE Redis Adapter', () => { firstCallArgs[1](); expect(loggerMock.info).toBeCalledTimes(1); // The callback for the "ready" event will inform the user about the trigger. - expect(loggerMock.info).toBeCalledWith(logPrefix + 'Redis connection established. Queued commands: 0.'); // The callback for the "ready" event will inform the user about the trigger. + expect(loggerMock.info).toBeCalledWith(LOG_PREFIX + 'Redis connection established. Queued commands: 0.'); // The callback for the "ready" event will inform the user about the trigger. expect(instance._notReadyCommandsQueue).toBe(undefined); // After the DB is ready, it will clean up the offline commands queue so we do not queue commands anymore. // Don't do this at home @@ -222,9 +222,9 @@ describe('STORAGE Redis Adapter', () => { expect(loggerMock.info).toBeCalledTimes(3); // If we had queued commands, it will log the event (1 call) as well as each executed command (n calls). expect(loggerMock.info.mock.calls).toEqual([ - [logPrefix + 'Redis connection established. Queued commands: 2.'], // The callback for the "ready" event will inform the user about the trigger and the amount of queued commands. - [logPrefix + 'Executing queued GET command.'], // If we had queued, it will log the event as well as each executed command. - [logPrefix + 'Executing queued SET command.'] // If we had queued commands, it will log the event as well as each executed command. + [LOG_PREFIX + 'Redis connection established. Queued commands: 2.'], // The callback for the "ready" event will inform the user about the trigger and the amount of queued commands. + [LOG_PREFIX + 'Executing queued GET command.'], // If we had queued, it will log the event as well as each executed command. + [LOG_PREFIX + 'Executing queued SET command.'] // If we had queued commands, it will log the event as well as each executed command. ]); expect(queuedGetCommand.command).toBeCalledTimes(1); // It will execute each queued command. @@ -287,7 +287,7 @@ describe('STORAGE Redis Adapter', () => { commandTimeoutResolver.rej('test'); setTimeout(() => { // Allow the promises to tick. expect(instance._runningCommands.has(commandTimeoutResolver.originalPromise)).toBe(false); // After a command finishes with error, it's promise is removed from the instance._runningCommands queue. - expect(loggerMock.error.mock.calls[index]).toEqual([`${logPrefix}${methodName} operation threw an error or exceeded configured timeout of 5000ms. Message: test`]); // The log error method should be called with the corresponding messages, depending on the method, error and operationTimeout. + expect(loggerMock.error.mock.calls[index]).toEqual([`${LOG_PREFIX}${methodName} operation threw an error or exceeded configured timeout of 5000ms. Message: test`]); // The log error method should be called with the corresponding messages, depending on the method, error and operationTimeout. }, 0); }); @@ -334,7 +334,7 @@ describe('STORAGE Redis Adapter', () => { expect(ioredisMock.disconnect).not.toBeCalled(); // Original method should not be called right away. setTimeout(() => { // o queued commands timeout wrapper. - expect(loggerMock.debug.mock.calls).toEqual([[logPrefix + 'No commands pending execution, disconnect.']]); + expect(loggerMock.debug.mock.calls).toEqual([[LOG_PREFIX + 'No commands pending execution, disconnect.']]); expect(ioredisMock.disconnect).toBeCalledTimes(1); // Original method should have been called once, asynchronously loggerMock.debug.mockClear(); ioredisMock.disconnect.mockClear(); @@ -347,11 +347,11 @@ describe('STORAGE Redis Adapter', () => { rejectedPromise.catch(() => { }); // Swallow the unhandled to avoid unhandledRejection warns setTimeout(() => { // queued with rejection timeout wrapper - expect(loggerMock.info.mock.calls).toEqual([[logPrefix + 'Attempting to disconnect but there are 2 commands still waiting for resolution. Defering disconnection until those finish.']]); + expect(loggerMock.info.mock.calls).toEqual([[LOG_PREFIX + 'Attempting to disconnect but there are 2 commands still waiting for resolution. Defering disconnection until those finish.']]); Promise.all(setToArray(instance._runningCommands)).catch(e => { setImmediate(() => { // Allow the callback to execute before checking. - expect(loggerMock.warn.mock.calls[0]).toEqual([`${logPrefix}Pending commands finished with error: ${e}. Proceeding with disconnection.`]); // Should warn about the error but tell user that will disconnect anyways. + expect(loggerMock.warn.mock.calls[0]).toEqual([`${LOG_PREFIX}Pending commands finished with error: ${e}. Proceeding with disconnection.`]); // Should warn about the error but tell user that will disconnect anyways. expect(ioredisMock.disconnect).toBeCalledTimes(1); // Original method should have been called once, asynchronously loggerMock.info.mockClear(); @@ -367,11 +367,11 @@ describe('STORAGE Redis Adapter', () => { instance._runningCommands.add(Promise.resolve()); setTimeout(() => { - expect(loggerMock.info.mock.calls).toEqual([[logPrefix + 'Attempting to disconnect but there are 4 commands still waiting for resolution. Defering disconnection until those finish.']]); + expect(loggerMock.info.mock.calls).toEqual([[LOG_PREFIX + 'Attempting to disconnect but there are 4 commands still waiting for resolution. Defering disconnection until those finish.']]); Promise.all(setToArray(instance._runningCommands)).then(() => { // This one will go through success path setImmediate(() => { - expect(loggerMock.debug.mock.calls).toEqual([[logPrefix + 'Pending commands finished successfully, disconnecting.']]); + expect(loggerMock.debug.mock.calls).toEqual([[LOG_PREFIX + 'Pending commands finished successfully, disconnecting.']]); expect(ioredisMock.disconnect).toBeCalledTimes(1); // Original method should have been called once, asynchronously }); }); diff --git a/src/storages/inRedis/constants.ts b/src/storages/inRedis/constants.ts new file mode 100644 index 00000000..a3856844 --- /dev/null +++ b/src/storages/inRedis/constants.ts @@ -0,0 +1 @@ +export const LOG_PREFIX = 'storage:redis: '; diff --git a/src/storages/pluggable/EventsCachePluggable.ts b/src/storages/pluggable/EventsCachePluggable.ts index eef10dd1..2af117d7 100644 --- a/src/storages/pluggable/EventsCachePluggable.ts +++ b/src/storages/pluggable/EventsCachePluggable.ts @@ -3,7 +3,7 @@ import { IRedisMetadata } from '../../dtos/types'; import KeyBuilderSS from '../KeyBuilderSS'; import { SplitIO } from '../../types'; import { ILogger } from '../../logger/types'; -import { logPrefix } from './constants'; +import { LOG_PREFIX } from './constants'; export class EventsCachePluggable implements IEventsCacheAsync { @@ -29,10 +29,13 @@ export class EventsCachePluggable implements IEventsCacheAsync { return this.wrapper.pushItems( this.keys.buildEventsKey(), [this._toJSON(eventData)] - ).catch(e => { - this.log.error(logPrefix + ` Error adding event to queue: ${e}.`); - return false; - }); + ) + // We use boolean values to signal successful queueing + .then(() => true) + .catch(e => { + this.log.error(LOG_PREFIX + ` Error adding event to queue: ${e}.`); + return false; + }); } private _toJSON(eventData: SplitIO.EventData): string { diff --git a/src/storages/pluggable/ImpressionsCachePluggable.ts b/src/storages/pluggable/ImpressionsCachePluggable.ts index 643b64de..31436c43 100644 --- a/src/storages/pluggable/ImpressionsCachePluggable.ts +++ b/src/storages/pluggable/ImpressionsCachePluggable.ts @@ -3,7 +3,7 @@ import { IRedisMetadata } from '../../dtos/types'; import { ImpressionDTO } from '../../types'; import KeyBuilderSS from '../KeyBuilderSS'; import { ILogger } from '../../logger/types'; -import { logPrefix } from './constants'; +import { LOG_PREFIX } from './constants'; export class ImpressionsCachePluggable implements IImpressionsCacheAsync { @@ -29,10 +29,13 @@ export class ImpressionsCachePluggable implements IImpressionsCacheAsync { return this.wrapper.pushItems( this.keys.buildImpressionsKey(), this._toJSON(impressions) - ).catch((e) => { - this.log.error(logPrefix + ` Error adding event to queue: ${e}.`); - return false; - }); + ) + // We use boolean values to signal successful queueing + .then(() => true) + .catch((e) => { + this.log.error(LOG_PREFIX + ` Error adding event to queue: ${e}.`); + return false; + }); } private _toJSON(impressions: ImpressionDTO[]): string[] { diff --git a/src/storages/pluggable/SegmentsCachePluggable.ts b/src/storages/pluggable/SegmentsCachePluggable.ts index 67fc39be..612b5ff0 100644 --- a/src/storages/pluggable/SegmentsCachePluggable.ts +++ b/src/storages/pluggable/SegmentsCachePluggable.ts @@ -4,7 +4,7 @@ import { isNaNNumber } from '../../utils/lang'; import KeyBuilder from '../KeyBuilder'; import { ICustomStorageWrapper, ISegmentsCacheAsync } from '../types'; import { ILogger } from '../../logger/types'; -import { logPrefix } from './constants'; +import { LOG_PREFIX } from './constants'; /** * ISegmentsCacheAsync implementation for pluggable storages. @@ -49,7 +49,7 @@ export class SegmentsCachePluggable implements ISegmentsCacheAsync { const i = parseInt(value as string, 10); return isNaNNumber(i) ? -1 : i; }).catch((e) => { - this.log.error(logPrefix + 'Could not retrieve changeNumber from segments storage. Error: ' + e); + this.log.error(LOG_PREFIX + 'Could not retrieve changeNumber from segments storage. Error: ' + e); return -1; }); } diff --git a/src/storages/pluggable/__tests__/EventsCachePluggable.spec.ts b/src/storages/pluggable/__tests__/EventsCachePluggable.spec.ts index e7f168fb..9bb8cef0 100644 --- a/src/storages/pluggable/__tests__/EventsCachePluggable.spec.ts +++ b/src/storages/pluggable/__tests__/EventsCachePluggable.spec.ts @@ -30,15 +30,15 @@ describe('PLUGGABLE EVENTS CACHE', () => { // @ts-expect-error expect(await cache.track(fakeEvent3)).toBe(true); // If the queueing operation was successful, it should resolve the returned promise with "true" - const redisValues = wrapperMock._queues[key]; + const values = wrapperMock._cache[key]; - expect(redisValues.length).toBe(3); // After pushing we should have on Redis as many events as we have stored. - expect(typeof redisValues[0]).toBe('string'); // All elements should be strings since those are stringified JSONs. - expect(typeof redisValues[1]).toBe('string'); // All elements should be strings since those are stringified JSONs. - expect(typeof redisValues[2]).toBe('string'); // All elements should be strings since those are stringified JSONs. + expect(values.length).toBe(3); // After pushing we should have as many events as we have stored. + expect(typeof values[0]).toBe('string'); // All elements should be strings since those are stringified JSONs. + expect(typeof values[1]).toBe('string'); // All elements should be strings since those are stringified JSONs. + expect(typeof values[2]).toBe('string'); // All elements should be strings since those are stringified JSONs. const findMatchingElem = (event: any) => { - return find(redisValues, elem => { + return find(values, elem => { const parsedElem = JSON.parse(elem); return isEqual(parsedElem.e, event) && isEqual(parsedElem.m, fakeRedisMetadata); }); @@ -48,9 +48,9 @@ describe('PLUGGABLE EVENTS CACHE', () => { const foundEv1 = findMatchingElem(fakeEvent1); const foundEv2 = findMatchingElem(fakeEvent2); const foundEv3 = findMatchingElem(fakeEvent3); - expect(foundEv1).not.toBe(undefined); // Events stored on redis matched the values we are expecting. - expect(foundEv2).not.toBe(undefined); // Events stored on redis matched the values we are expecting. - expect(foundEv3).not.toBe(undefined); // Events stored on redis matched the values we are expecting. + expect(foundEv1).not.toBe(undefined); // stored events matched the values we are expecting. + expect(foundEv2).not.toBe(undefined); // stored events matched the values we are expecting. + expect(foundEv3).not.toBe(undefined); // stored events matched the values we are expecting. wrapperMock.mockClear(); }); @@ -58,7 +58,7 @@ describe('PLUGGABLE EVENTS CACHE', () => { test('`track` method result should not reject if wrapper operation fails', async () => { const wrapperMock = wrapperMockFactory(); // @ts-ignore. I'll use a "bad" queue to force an exception with the pushItems wrapper operation. - wrapperMock._queues['non-list-key'] = 10; + wrapperMock._cache['non-list-key'] = 10; // @ts-expect-error. wrapperMock is adapted this time to properly handle unexpected exceptions const faultyCache = new EventsCachePluggable(loggerMock, { buildEventsKey: () => 'non-list-key' diff --git a/src/storages/pluggable/__tests__/ImpressionsCachePluggable.spec.ts b/src/storages/pluggable/__tests__/ImpressionsCachePluggable.spec.ts index 42dbeedb..00a88d95 100644 --- a/src/storages/pluggable/__tests__/ImpressionsCachePluggable.spec.ts +++ b/src/storages/pluggable/__tests__/ImpressionsCachePluggable.spec.ts @@ -50,12 +50,12 @@ describe('PLUGGABLE IMPRESSIONS CACHE', () => { expect(await cache.track([o1])).toBe(true); // result should resolve to true - let state = wrapperMock._queues[impressionsKey]; + let state = wrapperMock._cache[impressionsKey]; expect(state.length).toBe(1); // impression should be stored expect(await cache.track([o2, o3])).toBe(true); - state = wrapperMock._queues[impressionsKey]; + state = wrapperMock._cache[impressionsKey]; expect(state.length).toBe(3); // This is testing both the track and the toJSON method. expect(state[0]).toBe(JSON.stringify({ @@ -75,7 +75,7 @@ describe('PLUGGABLE IMPRESSIONS CACHE', () => { test('`track` method result should not reject if wrapper operation fails', async () => { // make wrapper operation fail - wrapperMock.pushItems.mockImplementation(()=>Promise.reject()); + wrapperMock.pushItems.mockImplementation(() => Promise.reject()); const cache = new ImpressionsCachePluggable(loggerMock, keys, wrapperMock, testMeta); expect(await cache.track([o1])).toBe(false); // result should resolve to false diff --git a/src/storages/pluggable/__tests__/wrapper.mock.ts b/src/storages/pluggable/__tests__/wrapper.mock.ts index 352c67cb..8aaf3a40 100644 --- a/src/storages/pluggable/__tests__/wrapper.mock.ts +++ b/src/storages/pluggable/__tests__/wrapper.mock.ts @@ -3,12 +3,11 @@ import { startsWith, toNumber } from '../../../utils/lang'; // Creates an in memory ICustomStorageWrapper implementation with Jest mocks export function wrapperMockFactory() { + /** Holds items and list of items */ let _cache: Record = {}; - let _queues: Record = {}; return { _cache, - _queues, get: jest.fn((key => { return Promise.resolve(key in _cache ? _cache[key] : null); @@ -57,19 +56,26 @@ export function wrapperMockFactory() { getMany: jest.fn((keys: string[]) => { return Promise.resolve(keys.map(key => _cache[key] ? _cache[key] : null)); }), - pushItems: jest.fn((key: string, items: string[]) => { - if (!(key in _queues)) _queues[key] = []; - _queues[key].push(...items); - return Promise.resolve(true); + pushItems: jest.fn((key: string, items: string[]) => { // @ts-ignore + if (!(key in _cache)) _cache[key] = []; + const list = _cache[key]; + if (Array.isArray(list)) { + list.push(...items); + return Promise.resolve(); + } + return Promise.reject('key is not a list'); }), popItems: jest.fn((key: string, count: number) => { - return Promise.resolve(key in _queues ? _queues[key].splice(0, count) : []); + const list = _cache[key]; + return Promise.resolve(Array.isArray(list) ? list.splice(0, count) : []); }), getItemsCount: jest.fn((key: string) => { - return Promise.resolve(key in _queues ? _queues[key].length : 0); + const list = _cache[key]; + return Promise.resolve(Array.isArray(list) ? list.length : 0); }), itemContains: jest.fn((key: string, item: string) => { - return Promise.resolve(key in _queues && _queues[key].indexOf(item) > -1 ? true : false); + const list = _cache[key]; + return Promise.resolve(Array.isArray(list) && list.indexOf(item) > -1 ? true : false); }), // always connects and close @@ -78,7 +84,6 @@ export function wrapperMockFactory() { mockClear() { this._cache = {}; - this._queues = {}; this.get.mockClear(); this.set.mockClear(); this.del.mockClear(); diff --git a/src/storages/pluggable/__tests__/wrapperAdapter.spec.ts b/src/storages/pluggable/__tests__/wrapperAdapter.spec.ts index c6292f87..430c3663 100644 --- a/src/storages/pluggable/__tests__/wrapperAdapter.spec.ts +++ b/src/storages/pluggable/__tests__/wrapperAdapter.spec.ts @@ -49,7 +49,6 @@ export const wrapperWithValuesToSanitize = { connect: () => Promise.resolve(1), getAndSet: () => Promise.resolve(true), getByPrefix: () => Promise.resolve(['1', null, false, true, '2', null]), - pushItems: () => Promise.resolve('false'), popItems: () => Promise.resolve('invalid array'), getItemsCount: () => Promise.resolve('10'), itemContains: () => Promise.resolve('true'), @@ -66,7 +65,6 @@ const SANITIZED_RESULTS = { connect: false, getAndSet: 'true', getByPrefix: ['1', '2'], - pushItems: false, popItems: [], getItemsCount: 10, itemContains: true, @@ -84,7 +82,7 @@ const VALID_METHOD_CALLS = { 'close': [], 'getAndSet': ['some_key', 'some_value'], 'getByPrefix': ['some_prefix'], - 'pushItems': ['some_key', ['item1', 'item2']], + 'pushItems': ['some_key_list', ['item1', 'item2']], 'popItems': ['some_key'], 'getItemsCount': ['some_key'], 'itemContains': ['some_key', 'some_value'], diff --git a/src/storages/pluggable/constants.ts b/src/storages/pluggable/constants.ts index dbac131e..1f845f45 100644 --- a/src/storages/pluggable/constants.ts +++ b/src/storages/pluggable/constants.ts @@ -1 +1 @@ -export const logPrefix = 'storage:pluggable:'; +export const LOG_PREFIX = 'storage:pluggable:'; diff --git a/src/storages/pluggable/wrapperAdapter.ts b/src/storages/pluggable/wrapperAdapter.ts index 37fa666d..63eb5a4b 100644 --- a/src/storages/pluggable/wrapperAdapter.ts +++ b/src/storages/pluggable/wrapperAdapter.ts @@ -3,7 +3,7 @@ import { sanitizeBoolean as sBoolean } from '../../evaluator/value/sanitize'; import { ILogger } from '../../logger/types'; import { SplitError } from '../../utils/lang/errors'; import { ICustomStorageWrapper } from '../types'; -import { logPrefix } from './constants'; +import { LOG_PREFIX } from './constants'; // Sanitizers return the given value if it is of the expected type, or a new sanitized one if invalid. @@ -50,7 +50,7 @@ const METHODS_TO_PROMISE_WRAP: [string, undefined | { (val: any): any, type: str ['close', undefined], ['getAndSet', sanitizeNullableString], ['getByPrefix', sanitizeArray], - ['pushItems', sanitizeBoolean], + ['pushItems', undefined], ['popItems', sanitizeArray], ['getItemsCount', sanitizeNumber], ['itemContains', sanitizeBoolean], @@ -70,9 +70,9 @@ export function wrapperAdapter(log: ILogger, wrapper: ICustomStorageWrapper): IC METHODS_TO_PROMISE_WRAP.forEach(([method, sanitizer]) => { - // Logs error and wraps it into an SplitError object + // Logs error and wraps it into an SplitError object (required to handle user callback errors in SDK readiness events) function handleError(e: any) { - log.error(`${logPrefix} Wrapper '${method}' operation threw an error. Message: ${e}`); + log.error(`${LOG_PREFIX} Wrapper '${method}' operation threw an error. Message: ${e}`); return Promise.reject(new SplitError(e)); } @@ -84,7 +84,7 @@ export function wrapperAdapter(log: ILogger, wrapper: ICustomStorageWrapper): IC const sanitizedValue = sanitizer(value); // if value had to be sanitized, log a warning - if (sanitizedValue !== value) log.warn(`${logPrefix} Attempted to sanitize return value [${value}] of wrapper '${method}' operation which should be of type [${sanitizer.type}]. Sanitized and processed value => [${sanitizedValue}]`); + if (sanitizedValue !== value) log.warn(`${LOG_PREFIX} Attempted to sanitize return value [${value}] of wrapper '${method}' operation which should be of type [${sanitizer.type}]. Sanitized and processed value => [${sanitizedValue}]`); return sanitizedValue; })).catch(handleError); diff --git a/src/storages/types.ts b/src/storages/types.ts index f7f23eda..96ea2705 100644 --- a/src/storages/types.ts +++ b/src/storages/types.ts @@ -61,20 +61,24 @@ export interface ICustomStorageWrapper { */ getMany: (keys: string[]) => Promise<(string | null)[]> /** - * Push given `items` to `key` queue. + * Push given `items` to `key` list. If key does not exist, an empty list is created for the key before pushing the items. + * Return a promise that resolves if the operation success, + * or rejects if the operation fails, for example, if there is a connectin error or the key holds a value that is not a list. */ - pushItems: (key: string, items: string[]) => Promise + pushItems: (key: string, items: string[]) => Promise /** * Pop `count` number of items from `key` queue. - * Return a promise that resolves with the list of removed items the removed members, or an empty array when key does not exist. + * Return a promise that resolves with the list of removed items the removed members, + * or an empty array when key does not exist or is not a list. */ popItems: (key: string, count: number) => Promise /** - * Return a promise that resolves with the number of items at the `key` queue, or 0 when key does not exist. + * Return a promise that resolves with the number of items at the `key` queue, or 0 when key does not exist or is not a list. */ getItemsCount: (key: string) => Promise /** - * Return a promise that resolves with true boolean value if `item` is a member of the set stored at `key`, or false otherwise. + * Return a promise that resolves with true boolean value if `item` is a member of the list stored at `key`, + * or false if it is not a member or key does not exist or is not a list. */ itemContains: (key: string, item: string) => Promise From 05ccd7df959dd45ce6288cc7099a2f24d33d2fe6 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 29 Apr 2021 21:08:32 -0300 Subject: [PATCH 018/144] some fixes --- .../pluggable/SplitsCachePluggable.ts | 8 ++--- .../pluggable/__tests__/wrapper.mock.ts | 10 +++---- src/storages/pluggable/wrapperAdapter.ts | 30 ++++++++++--------- 3 files changed, 25 insertions(+), 23 deletions(-) diff --git a/src/storages/pluggable/SplitsCachePluggable.ts b/src/storages/pluggable/SplitsCachePluggable.ts index cc035cea..9362aaab 100644 --- a/src/storages/pluggable/SplitsCachePluggable.ts +++ b/src/storages/pluggable/SplitsCachePluggable.ts @@ -4,7 +4,7 @@ import { ICustomStorageWrapper, ISplitsCacheAsync } from '../types'; import { ILogger } from '../../logger/types'; import { usesSegments } from '../AbstractSplitsCacheSync'; import { ISplit } from '../../dtos/types'; -import { logPrefix } from './constants'; +import { LOG_PREFIX } from './constants'; import { SplitError } from '../../utils/lang/errors'; /** @@ -180,13 +180,13 @@ export class SplitsCachePluggable implements ISplitsCacheAsync { ttCount = parseInt(ttCount as string, 10); if (!isFiniteNumber(ttCount) || ttCount < 0) { - this.log.info(logPrefix + `Could not validate traffic type existence of ${trafficType} due to data corruption of some sorts.`); + this.log.info(LOG_PREFIX + `Could not validate traffic type existence of ${trafficType} due to data corruption of some sorts.`); return false; } return ttCount > 0; }).catch(e => { - this.log.error(logPrefix + `Could not validate traffic type existence of ${trafficType} due to an error: ${e}.`); + this.log.error(LOG_PREFIX + `Could not validate traffic type existence of ${trafficType} due to an error: ${e}.`); // If there is an error, bypass the validation so the event can get tracked. return true; }); @@ -212,7 +212,7 @@ export class SplitsCachePluggable implements ISplitsCacheAsync { return isNaNNumber(i) ? -1 : i; }).catch((e) => { - this.log.error(logPrefix + 'Could not retrieve changeNumber from storage. Error: ' + e); + this.log.error(LOG_PREFIX + 'Could not retrieve changeNumber from storage. Error: ' + e); return -1; }); } diff --git a/src/storages/pluggable/__tests__/wrapper.mock.ts b/src/storages/pluggable/__tests__/wrapper.mock.ts index 8aaf3a40..60e8b772 100644 --- a/src/storages/pluggable/__tests__/wrapper.mock.ts +++ b/src/storages/pluggable/__tests__/wrapper.mock.ts @@ -4,13 +4,13 @@ import { startsWith, toNumber } from '../../../utils/lang'; export function wrapperMockFactory() { /** Holds items and list of items */ - let _cache: Record = {}; + let _cache: Record = {}; return { _cache, get: jest.fn((key => { - return Promise.resolve(key in _cache ? _cache[key] : null); + return Promise.resolve(key in _cache ? _cache[key] as string : null); })), set: jest.fn((key: string, value: string) => { const result = key in _cache; @@ -18,7 +18,7 @@ export function wrapperMockFactory() { return Promise.resolve(result); }), getAndSet: jest.fn((key: string, value: string) => { - const result = key in _cache ? _cache[key] : null; + const result = key in _cache ? _cache[key] as string : null; _cache[key] = value; return Promise.resolve(result); }), @@ -31,7 +31,7 @@ export function wrapperMockFactory() { return Promise.resolve(Object.keys(_cache).filter(key => startsWith(key, prefix))); }), getByPrefix: jest.fn((prefix: string) => { - return Promise.resolve(Object.keys(_cache).filter(key => startsWith(key, prefix)).map(key => _cache[key])); + return Promise.resolve(Object.keys(_cache).filter(key => startsWith(key, prefix)).map(key => _cache[key] as string)); }), incr: jest.fn((key: string) => { if (key in _cache) { @@ -54,7 +54,7 @@ export function wrapperMockFactory() { return Promise.resolve(true); }), getMany: jest.fn((keys: string[]) => { - return Promise.resolve(keys.map(key => _cache[key] ? _cache[key] : null)); + return Promise.resolve(keys.map(key => _cache[key] ? _cache[key] as string : null)); }), pushItems: jest.fn((key: string, items: string[]) => { // @ts-ignore if (!(key in _cache)) _cache[key] = []; diff --git a/src/storages/pluggable/wrapperAdapter.ts b/src/storages/pluggable/wrapperAdapter.ts index 63eb5a4b..a7049d9e 100644 --- a/src/storages/pluggable/wrapperAdapter.ts +++ b/src/storages/pluggable/wrapperAdapter.ts @@ -6,6 +6,7 @@ import { ICustomStorageWrapper } from '../types'; import { LOG_PREFIX } from './constants'; // Sanitizers return the given value if it is of the expected type, or a new sanitized one if invalid. +// @TODO review or remove sanitizers. might be expensive for producer methods function sanitizeBoolean(val: any): boolean { return sBoolean(val) || false; @@ -17,41 +18,42 @@ function sanitizeNumber(val: any): number { } sanitizeNumber.type = 'number'; -function sanitizeArray(val: any): string[] { +function sanitizeArrayOfStrings(val: any): string[] { if (!Array.isArray(val)) return []; // if not an array, returns a new empty one if (val.every(isString)) return val; // if all items are valid, return the given array - return val.filter(isString); // otherwise, return a new array filtering the invalid items + return val.map(toString); // otherwise, return a new array with items sanitized } -sanitizeArray.type = 'Array'; +sanitizeArrayOfStrings.type = 'Array'; function sanitizeNullableString(val: any): string | null { if (val === null) return val; - return toString(val); + return toString(val); // if not null, sanitize value as an string } sanitizeNullableString.type = 'string | null'; -function sanitizeArrayOfNullableString(val: any): (string | null)[] { - const isStringOrNull = (v: any) => v === null || isString(v); +function sanitizeArrayOfNullableStrings(val: any): (string | null)[] { if (!Array.isArray(val)) return []; - if (val.every(isStringOrNull)) return val; - return val.filter(isStringOrNull); + if (val.every(v => v === null || isString(v))) return val; // if all items are string or null, return the given array + return val.map(v => v !== null ? toString(v) : null); // otherwise, return a new array with items sanitized } -sanitizeArrayOfNullableString.type = 'Array'; +sanitizeArrayOfNullableStrings.type = 'Array'; const METHODS_TO_PROMISE_WRAP: [string, undefined | { (val: any): any, type: string }][] = [ ['get', sanitizeNullableString], ['set', sanitizeBoolean], + ['getAndSet', sanitizeNullableString], ['del', sanitizeBoolean], - ['getKeysByPrefix', sanitizeArray], + ['getKeysByPrefix', sanitizeArrayOfStrings], + ['getByPrefix', sanitizeArrayOfStrings], ['incr', sanitizeBoolean], ['decr', sanitizeBoolean], - ['getMany', sanitizeArrayOfNullableString], + ['getMany', sanitizeArrayOfNullableStrings], ['connect', sanitizeBoolean], ['close', undefined], - ['getAndSet', sanitizeNullableString], - ['getByPrefix', sanitizeArray], + + ['pushItems', undefined], - ['popItems', sanitizeArray], + ['popItems', sanitizeArrayOfStrings], ['getItemsCount', sanitizeNumber], ['itemContains', sanitizeBoolean], ]; From 226155369f4554eba9b3022621a8a2d4652a3318 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 29 Apr 2021 21:56:41 -0300 Subject: [PATCH 019/144] test fixes --- .../__tests__/EventsCachePluggable.spec.ts | 2 +- .../__tests__/SplitsCachePluggable.spec.ts | 17 ++++++----------- .../pluggable/__tests__/wrapperAdapter.spec.ts | 6 +++--- src/storages/pluggable/wrapperAdapter.ts | 6 +++--- src/sync/polling/syncTasks/splitsSyncTask.ts | 2 +- 5 files changed, 14 insertions(+), 19 deletions(-) diff --git a/src/storages/pluggable/__tests__/EventsCachePluggable.spec.ts b/src/storages/pluggable/__tests__/EventsCachePluggable.spec.ts index 9bb8cef0..dfac96f6 100644 --- a/src/storages/pluggable/__tests__/EventsCachePluggable.spec.ts +++ b/src/storages/pluggable/__tests__/EventsCachePluggable.spec.ts @@ -3,7 +3,7 @@ import isEqual from 'lodash/isEqual'; import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; import KeyBuilderSS from '../../KeyBuilderSS'; import { EventsCachePluggable } from '../EventsCachePluggable'; -import { metadataBuilder } from '../../inRedis'; +import { metadataBuilder } from '../../metadataBuilder'; import { wrapperMockFactory } from './wrapper.mock'; import { wrapperAdapter } from '../wrapperAdapter'; diff --git a/src/storages/pluggable/__tests__/SplitsCachePluggable.spec.ts b/src/storages/pluggable/__tests__/SplitsCachePluggable.spec.ts index 6e3c1c3e..d7b5ff00 100644 --- a/src/storages/pluggable/__tests__/SplitsCachePluggable.spec.ts +++ b/src/storages/pluggable/__tests__/SplitsCachePluggable.spec.ts @@ -1,7 +1,7 @@ import { SplitsCachePluggable } from '../SplitsCachePluggable'; import KeyBuilder from '../../KeyBuilder'; import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; -import { wrapperMock } from './wrapper.mock'; +import { wrapperMockFactory } from './wrapper.mock'; const splitWithUserTT = '{ "trafficTypeName": "user_tt" }'; const splitWithAccountTT = '{ "trafficTypeName": "account_tt" }'; @@ -10,13 +10,8 @@ const keysBuilder = new KeyBuilder(); describe('SPLITS CACHE PLUGGABLE', () => { - afterEach(() => { - loggerMock.mockClear(); - wrapperMock.mockClear(); - }); - test('add/remove/get splits', async () => { - const cache = new SplitsCachePluggable(loggerMock, keysBuilder, wrapperMock); + const cache = new SplitsCachePluggable(loggerMock, keysBuilder, wrapperMockFactory()); // Assert addSplit and addSplits await cache.addSplits([ @@ -76,7 +71,7 @@ describe('SPLITS CACHE PLUGGABLE', () => { }); test('set/get change number', async () => { - const cache = new SplitsCachePluggable(loggerMock, keysBuilder, wrapperMock); + const cache = new SplitsCachePluggable(loggerMock, keysBuilder, wrapperMockFactory()); expect(await cache.getChangeNumber()).toBe(-1); // if not set yet, changeNumber is -1 await cache.setChangeNumber(123); @@ -85,7 +80,7 @@ describe('SPLITS CACHE PLUGGABLE', () => { }); test('trafficTypeExists', async () => { - const cache = new SplitsCachePluggable(loggerMock, keysBuilder, wrapperMock); + const cache = new SplitsCachePluggable(loggerMock, keysBuilder, wrapperMockFactory()); await cache.addSplits([ ['split1', splitWithUserTT], @@ -125,7 +120,7 @@ describe('SPLITS CACHE PLUGGABLE', () => { }); test('usesSegments', async () => { - const cache = new SplitsCachePluggable(loggerMock, keysBuilder, wrapperMock); + const cache = new SplitsCachePluggable(loggerMock, keysBuilder, wrapperMockFactory()); await cache.addSplits([['split1', splitWithUserTT], ['split2', splitWithAccountTT],]); expect(await cache.usesSegments()).toBe(false); // 0 splits using segments @@ -144,7 +139,7 @@ describe('SPLITS CACHE PLUGGABLE', () => { }); test('killLocally', async () => { - const cache = new SplitsCachePluggable(loggerMock, keysBuilder, wrapperMock); + const cache = new SplitsCachePluggable(loggerMock, keysBuilder, wrapperMockFactory()); await cache.addSplit('lol1', splitWithUserTT); await cache.addSplit('lol2', splitWithAccountTT); diff --git a/src/storages/pluggable/__tests__/wrapperAdapter.spec.ts b/src/storages/pluggable/__tests__/wrapperAdapter.spec.ts index 430c3663..ca8d1dd7 100644 --- a/src/storages/pluggable/__tests__/wrapperAdapter.spec.ts +++ b/src/storages/pluggable/__tests__/wrapperAdapter.spec.ts @@ -1,7 +1,7 @@ // @ts-nocheck import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; import thenable from '../../../utils/promise/thenable'; -import { logPrefix } from '../constants'; +import { LOG_PREFIX } from '../constants'; import { SplitError } from '../../../utils/lang/errors'; /** Mocks */ @@ -61,7 +61,7 @@ const SANITIZED_RESULTS = { getKeysByPrefix: ['1', '2'], incr: false, decr: false, - getMany: ['1', null, '2', null], + getMany: ['1', null, 'false', 'true', '2', null], connect: false, getAndSet: 'true', getByPrefix: ['1', '2'], @@ -129,7 +129,7 @@ describe('Wrapper Adapter', () => { expect(true).toBe(false); // promise shouldn't be resolved } catch (e) { expect(e).toBeInstanceOf(SplitError); - expect(loggerMock.error).toHaveBeenCalledWith(`${logPrefix} Wrapper '${method}' operation threw an error. Message: ${e.message}`); + expect(loggerMock.error).toHaveBeenCalledWith(`${LOG_PREFIX} Wrapper '${method}' operation threw an error. Message: ${e.message}`); } } diff --git a/src/storages/pluggable/wrapperAdapter.ts b/src/storages/pluggable/wrapperAdapter.ts index a7049d9e..ad26d17f 100644 --- a/src/storages/pluggable/wrapperAdapter.ts +++ b/src/storages/pluggable/wrapperAdapter.ts @@ -6,7 +6,7 @@ import { ICustomStorageWrapper } from '../types'; import { LOG_PREFIX } from './constants'; // Sanitizers return the given value if it is of the expected type, or a new sanitized one if invalid. -// @TODO review or remove sanitizers. might be expensive for producer methods +// @TODO review or remove sanitizers. Could be expensive for producer methods function sanitizeBoolean(val: any): boolean { return sBoolean(val) || false; @@ -21,7 +21,7 @@ sanitizeNumber.type = 'number'; function sanitizeArrayOfStrings(val: any): string[] { if (!Array.isArray(val)) return []; // if not an array, returns a new empty one if (val.every(isString)) return val; // if all items are valid, return the given array - return val.map(toString); // otherwise, return a new array with items sanitized + return val.filter(isString); // otherwise, return a new array filtering the invalid items } sanitizeArrayOfStrings.type = 'Array'; @@ -33,7 +33,7 @@ sanitizeNullableString.type = 'string | null'; function sanitizeArrayOfNullableStrings(val: any): (string | null)[] { if (!Array.isArray(val)) return []; - if (val.every(v => v === null || isString(v))) return val; // if all items are string or null, return the given array + if (val.every(v => v === null || isString(v))) return val; return val.map(v => v !== null ? toString(v) : null); // otherwise, return a new array with items sanitized } sanitizeArrayOfNullableStrings.type = 'Array'; diff --git a/src/sync/polling/syncTasks/splitsSyncTask.ts b/src/sync/polling/syncTasks/splitsSyncTask.ts index aec19fe5..557e8c0f 100644 --- a/src/sync/polling/syncTasks/splitsSyncTask.ts +++ b/src/sync/polling/syncTasks/splitsSyncTask.ts @@ -129,7 +129,7 @@ export function splitChangesUpdaterFactory( log.debug(SYNC_SPLITS_SEGMENTS, [mutation.segments.length]); // Write into storage - // @TODO in producer mode, keep consistency of wrapped storate by calling `setChangeNumber` if the other wrapper operations have succeeded + // @TODO call `setChangeNumber` only if the other storage operations have succeeded, in order to keep storage consistency return Promise.all([ // calling first `setChangenumber` method, to perform cache flush if split filter queryString changed splitsCache.setChangeNumber(splitChanges.till), From 5273d10a0356556ac596125fbadab3ff2e619c5c Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 29 Apr 2021 22:06:57 -0300 Subject: [PATCH 020/144] test fixes --- .../pluggable/__tests__/wrapperAdapter.spec.ts | 2 +- src/storages/pluggable/wrapperAdapter.ts | 13 ++++++------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/storages/pluggable/__tests__/wrapperAdapter.spec.ts b/src/storages/pluggable/__tests__/wrapperAdapter.spec.ts index ca8d1dd7..49ad448d 100644 --- a/src/storages/pluggable/__tests__/wrapperAdapter.spec.ts +++ b/src/storages/pluggable/__tests__/wrapperAdapter.spec.ts @@ -61,7 +61,7 @@ const SANITIZED_RESULTS = { getKeysByPrefix: ['1', '2'], incr: false, decr: false, - getMany: ['1', null, 'false', 'true', '2', null], + getMany: ['1', null, '2', null], connect: false, getAndSet: 'true', getByPrefix: ['1', '2'], diff --git a/src/storages/pluggable/wrapperAdapter.ts b/src/storages/pluggable/wrapperAdapter.ts index ad26d17f..e668c335 100644 --- a/src/storages/pluggable/wrapperAdapter.ts +++ b/src/storages/pluggable/wrapperAdapter.ts @@ -6,7 +6,7 @@ import { ICustomStorageWrapper } from '../types'; import { LOG_PREFIX } from './constants'; // Sanitizers return the given value if it is of the expected type, or a new sanitized one if invalid. -// @TODO review or remove sanitizers. Could be expensive for producer methods +// @TODO review or remove sanitizers. Could be expensive and error-prone for producer methods function sanitizeBoolean(val: any): boolean { return sBoolean(val) || false; @@ -32,9 +32,10 @@ function sanitizeNullableString(val: any): string | null { sanitizeNullableString.type = 'string | null'; function sanitizeArrayOfNullableStrings(val: any): (string | null)[] { + const isStringOrNull = (v: any) => v === null || isString(v); if (!Array.isArray(val)) return []; - if (val.every(v => v === null || isString(v))) return val; - return val.map(v => v !== null ? toString(v) : null); // otherwise, return a new array with items sanitized + if (val.every(isStringOrNull)) return val; + return val.filter(isStringOrNull); // otherwise, return a new array with items sanitized } sanitizeArrayOfNullableStrings.type = 'Array'; @@ -48,14 +49,12 @@ const METHODS_TO_PROMISE_WRAP: [string, undefined | { (val: any): any, type: str ['incr', sanitizeBoolean], ['decr', sanitizeBoolean], ['getMany', sanitizeArrayOfNullableStrings], - ['connect', sanitizeBoolean], - ['close', undefined], - - ['pushItems', undefined], ['popItems', sanitizeArrayOfStrings], ['getItemsCount', sanitizeNumber], ['itemContains', sanitizeBoolean], + ['connect', sanitizeBoolean], + ['close', undefined], ]; /** From 927d50535310884c741393dc7f602919ab17e718 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Fri, 30 Apr 2021 10:45:23 -0300 Subject: [PATCH 021/144] removed sanitizer logic for a separete PR --- src/evaluator/value/sanitize.ts | 2 +- .../__tests__/wrapperAdapter.spec.ts | 50 ------------ src/storages/pluggable/wrapperAdapter.ts | 80 +++++-------------- 3 files changed, 19 insertions(+), 113 deletions(-) diff --git a/src/evaluator/value/sanitize.ts b/src/evaluator/value/sanitize.ts index b42ff769..991ed8e1 100644 --- a/src/evaluator/value/sanitize.ts +++ b/src/evaluator/value/sanitize.ts @@ -28,7 +28,7 @@ function sanitizeArray(val: any): string[] | undefined { return arr.length ? arr : undefined; } -export function sanitizeBoolean(val: any): boolean | undefined { +function sanitizeBoolean(val: any): boolean | undefined { if (val === true || val === false) return val; if (typeof val === 'string') { diff --git a/src/storages/pluggable/__tests__/wrapperAdapter.spec.ts b/src/storages/pluggable/__tests__/wrapperAdapter.spec.ts index 49ad448d..d8f62ecc 100644 --- a/src/storages/pluggable/__tests__/wrapperAdapter.spec.ts +++ b/src/storages/pluggable/__tests__/wrapperAdapter.spec.ts @@ -38,38 +38,6 @@ export const wrapperWithIssues = { itemContains: throwsException, }; -export const wrapperWithValuesToSanitize = { - get: () => Promise.resolve(10), - set: () => Promise.resolve('tRue'), - del: () => Promise.resolve(), // no result - getKeysByPrefix: () => Promise.resolve(['1', null, false, true, '2', null]), - incr: () => Promise.resolve('1'), - decr: () => Promise.resolve('0'), - getMany: () => Promise.resolve(['1', null, false, true, '2', null]), - connect: () => Promise.resolve(1), - getAndSet: () => Promise.resolve(true), - getByPrefix: () => Promise.resolve(['1', null, false, true, '2', null]), - popItems: () => Promise.resolve('invalid array'), - getItemsCount: () => Promise.resolve('10'), - itemContains: () => Promise.resolve('true'), -}; - -const SANITIZED_RESULTS = { - get: '10', - set: true, - del: false, - getKeysByPrefix: ['1', '2'], - incr: false, - decr: false, - getMany: ['1', null, '2', null], - connect: false, - getAndSet: 'true', - getByPrefix: ['1', '2'], - popItems: [], - getItemsCount: 10, - itemContains: true, -}; - const VALID_METHOD_CALLS = { 'get': ['some_key'], 'set': ['some_key', 'some_value'], @@ -136,22 +104,4 @@ describe('Wrapper Adapter', () => { expect(loggerMock.error).toBeCalledTimes(methods.length); }); - test('sanitize wrapper call results', async () => { - const instance = wrapperAdapter(loggerMock, wrapperWithValuesToSanitize); - const methods = Object.keys(SANITIZED_RESULTS); - - for (let i = 0; i < methods.length; i++) { - try { - const method = methods[i]; - const result = await instance[method](...VALID_METHOD_CALLS[method]); - - expect(result).toEqual(SANITIZED_RESULTS[method]); // result should be sanitized - } catch (e) { - expect(true).toBe(methods[i]); // promise shouldn't be rejected - } - } - - expect(loggerMock.warn).toBeCalledTimes(methods.length); // one warning for each wrapper operation call which result had to be sanitized - }); - }); diff --git a/src/storages/pluggable/wrapperAdapter.ts b/src/storages/pluggable/wrapperAdapter.ts index e668c335..06bcab0f 100644 --- a/src/storages/pluggable/wrapperAdapter.ts +++ b/src/storages/pluggable/wrapperAdapter.ts @@ -1,60 +1,24 @@ -import { toString, isString, toNumber } from '../../utils/lang'; -import { sanitizeBoolean as sBoolean } from '../../evaluator/value/sanitize'; import { ILogger } from '../../logger/types'; import { SplitError } from '../../utils/lang/errors'; import { ICustomStorageWrapper } from '../types'; import { LOG_PREFIX } from './constants'; -// Sanitizers return the given value if it is of the expected type, or a new sanitized one if invalid. -// @TODO review or remove sanitizers. Could be expensive and error-prone for producer methods - -function sanitizeBoolean(val: any): boolean { - return sBoolean(val) || false; -} -sanitizeBoolean.type = 'boolean'; - -function sanitizeNumber(val: any): number { - return toNumber(val); -} -sanitizeNumber.type = 'number'; - -function sanitizeArrayOfStrings(val: any): string[] { - if (!Array.isArray(val)) return []; // if not an array, returns a new empty one - if (val.every(isString)) return val; // if all items are valid, return the given array - return val.filter(isString); // otherwise, return a new array filtering the invalid items -} -sanitizeArrayOfStrings.type = 'Array'; - -function sanitizeNullableString(val: any): string | null { - if (val === null) return val; - return toString(val); // if not null, sanitize value as an string -} -sanitizeNullableString.type = 'string | null'; - -function sanitizeArrayOfNullableStrings(val: any): (string | null)[] { - const isStringOrNull = (v: any) => v === null || isString(v); - if (!Array.isArray(val)) return []; - if (val.every(isStringOrNull)) return val; - return val.filter(isStringOrNull); // otherwise, return a new array with items sanitized -} -sanitizeArrayOfNullableStrings.type = 'Array'; - -const METHODS_TO_PROMISE_WRAP: [string, undefined | { (val: any): any, type: string }][] = [ - ['get', sanitizeNullableString], - ['set', sanitizeBoolean], - ['getAndSet', sanitizeNullableString], - ['del', sanitizeBoolean], - ['getKeysByPrefix', sanitizeArrayOfStrings], - ['getByPrefix', sanitizeArrayOfStrings], - ['incr', sanitizeBoolean], - ['decr', sanitizeBoolean], - ['getMany', sanitizeArrayOfNullableStrings], - ['pushItems', undefined], - ['popItems', sanitizeArrayOfStrings], - ['getItemsCount', sanitizeNumber], - ['itemContains', sanitizeBoolean], - ['connect', sanitizeBoolean], - ['close', undefined], +const METHODS_TO_PROMISE_WRAP: string[] = [ + 'get', + 'set', + 'getAndSet', + 'del', + 'getKeysByPrefix', + 'getByPrefix', + 'incr', + 'decr', + 'getMany', + 'pushItems', + 'popItems', + 'getItemsCount', + 'itemContains', + 'connect', + 'close' ]; /** @@ -69,7 +33,7 @@ export function wrapperAdapter(log: ILogger, wrapper: ICustomStorageWrapper): IC const wrapperAdapter: Record = {}; - METHODS_TO_PROMISE_WRAP.forEach(([method, sanitizer]) => { + METHODS_TO_PROMISE_WRAP.forEach((method) => { // Logs error and wraps it into an SplitError object (required to handle user callback errors in SDK readiness events) function handleError(e: any) { @@ -80,15 +44,7 @@ export function wrapperAdapter(log: ILogger, wrapper: ICustomStorageWrapper): IC wrapperAdapter[method] = function () { try { // @ts-ignore - return wrapper[method].apply(wrapper, arguments).then((value => { - if (!sanitizer) return value; - const sanitizedValue = sanitizer(value); - - // if value had to be sanitized, log a warning - if (sanitizedValue !== value) log.warn(`${LOG_PREFIX} Attempted to sanitize return value [${value}] of wrapper '${method}' operation which should be of type [${sanitizer.type}]. Sanitized and processed value => [${sanitizedValue}]`); - - return sanitizedValue; - })).catch(handleError); + return wrapper[method].apply(wrapper, arguments).then(value => value).catch(handleError); } catch (e) { return handleError(e); } From 8ea1568d7aeea54a49c1e655cfee55275d0781b6 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Fri, 30 Apr 2021 13:31:48 -0300 Subject: [PATCH 022/144] updated merge util types --- src/utils/lang/index.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/utils/lang/index.ts b/src/utils/lang/index.ts index 6074a1c1..d57a0a1e 100644 --- a/src/utils/lang/index.ts +++ b/src/utils/lang/index.ts @@ -168,9 +168,11 @@ export function isString(val: any): val is string { * Deep copy version of Object.assign using recursion. * There are some assumptions here. It's for internal use and we don't need verbose errors * or to ensure the data types or whatever. Parameters should always be correct (at least have a target and a source, of type object). + * @param target The target object to copy to. + * @param source The source object from which to copy properties. */ -export function merge(target: { [key: string]: any }, source: { [key: string]: any }, ...rest: any[]): object { - let res = target; +export function merge(target: T, source: U, ...rest: any[]): T & U { + let res: { [key: string]: any } = target; isObject(source) && Object.keys(source).forEach(key => { let val = source[key]; @@ -191,7 +193,7 @@ export function merge(target: { [key: string]: any }, source: { [key: string]: a res = merge(res, nextSource, ...rest); } - return res; + return res as T & U; } /** From 2ecca5fa2ab00ec32b9d12d44ea26cef6523a6ff Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Fri, 30 Apr 2021 15:00:55 -0300 Subject: [PATCH 023/144] Revert "updated merge util types" This reverts commit 8ea1568d7aeea54a49c1e655cfee55275d0781b6. --- src/utils/lang/index.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/utils/lang/index.ts b/src/utils/lang/index.ts index d57a0a1e..6074a1c1 100644 --- a/src/utils/lang/index.ts +++ b/src/utils/lang/index.ts @@ -168,11 +168,9 @@ export function isString(val: any): val is string { * Deep copy version of Object.assign using recursion. * There are some assumptions here. It's for internal use and we don't need verbose errors * or to ensure the data types or whatever. Parameters should always be correct (at least have a target and a source, of type object). - * @param target The target object to copy to. - * @param source The source object from which to copy properties. */ -export function merge(target: T, source: U, ...rest: any[]): T & U { - let res: { [key: string]: any } = target; +export function merge(target: { [key: string]: any }, source: { [key: string]: any }, ...rest: any[]): object { + let res = target; isObject(source) && Object.keys(source).forEach(key => { let val = source[key]; @@ -193,7 +191,7 @@ export function merge Date: Fri, 30 Apr 2021 16:34:32 -0300 Subject: [PATCH 024/144] storage factory with options validation --- src/sdkFactory/index.ts | 4 +- src/storages/pluggable/index.ts | 67 ++++++++++++++++++++++++ src/storages/pluggable/wrapperAdapter.ts | 2 +- 3 files changed, 70 insertions(+), 3 deletions(-) create mode 100644 src/storages/pluggable/index.ts diff --git a/src/sdkFactory/index.ts b/src/sdkFactory/index.ts index 2fd3e67c..8463abe9 100644 --- a/src/sdkFactory/index.ts +++ b/src/sdkFactory/index.ts @@ -68,8 +68,8 @@ export function sdkFactory(params: ISdkFactoryParams): SplitIO.ICsSDK | SplitIO. const impressionsTracker = impressionsTrackerFactory(log, storage.impressions, metadata, impressionListener, integrationsManager, observer, storage.impressionCounts); const eventTracker = eventTrackerFactory(log, storage.events, integrationsManager); - // signal listener - const signalListener = SignalListener && new SignalListener(syncManager && syncManager.flush, settings, storage, splitApi); + // signal listener. Instantiates only if `syncManager.flush` (used by Node listener) and `splitApi` (used by Browser listener) are available + const signalListener = SignalListener && syncManager && splitApi && new SignalListener(syncManager.flush, settings, storage, splitApi); // Sdk client and manager const clientMethod = sdkClientMethodFactory({ eventTracker, impressionsTracker, sdkReadinessManager, settings, storage, syncManager, signalListener }); diff --git a/src/storages/pluggable/index.ts b/src/storages/pluggable/index.ts new file mode 100644 index 00000000..4be7bf00 --- /dev/null +++ b/src/storages/pluggable/index.ts @@ -0,0 +1,67 @@ +import { ICustomStorageWrapper, IStorageAsync, IStorageFactoryParams } from '../types'; + +import KeyBuilderSS from '../KeyBuilderSS'; +import { SplitsCachePluggable } from './SplitsCachePluggable'; +import { SegmentsCachePluggable } from './SegmentsCachePluggable'; +import { ImpressionsCachePluggable } from './ImpressionsCachePluggable'; +import { EventsCachePluggable } from './EventsCachePluggable'; + +import { SDK_SPLITS_ARRIVED, SDK_SEGMENTS_ARRIVED } from '../../readiness/constants'; +import { metadataBuilder } from '../metadataBuilder'; +import { wrapperAdapter, METHODS_TO_PROMISE_WRAP } from './wrapperAdapter'; +import { isObject } from '../../utils/lang'; + +export interface PluggableStorageOptions { + prefix?: string + wrapper: ICustomStorageWrapper +} + +/** + * Validate the options given to the pluggable storage factory. + * @param options user options + * @throws Will throw an error if the options are invalid. Example: wrapper is not provided or doesn't have some methods. + */ +function validatePluggableStorageOptions(options: any) { + if (!isObject(options) || !isObject(options.wrapper)) throw new Error('No wrapper provided'); + + const wrapper = options.wrapper; + const missingMethods = METHODS_TO_PROMISE_WRAP.filter(method => typeof wrapper[method] !== 'function'); + if (missingMethods.length) throw new Error(`Wrapper instance must implement the following methods: ${missingMethods}`); +} + +/** + * Pluggable storage factory for consumer server-side & client-side SplitFactory. + */ +export function PluggableStorage(options: PluggableStorageOptions) { + + validatePluggableStorageOptions(options); + + const prefix = options.prefix ? options.prefix + '.SPLITIO' : 'SPLITIO'; + + return function PluggableStorageFactory(params: IStorageFactoryParams): IStorageAsync { + + const log = params.log; + const metadata = metadataBuilder(params.metadata); + const keys = new KeyBuilderSS(prefix, { version: metadata.s, ip: metadata.i, hostname: metadata.n }); + const wrapper = wrapperAdapter(log, options.wrapper); + + // subscription to Wrapper connect event in order to emit SDK_READY event + wrapper.connect().then(() => { + params.readinessManager.splits.emit(SDK_SPLITS_ARRIVED); + params.readinessManager.segments.emit(SDK_SEGMENTS_ARRIVED); + }); + + return { + splits: new SplitsCachePluggable(log, keys, wrapper), + segments: new SegmentsCachePluggable(log, keys, wrapper), + impressions: new ImpressionsCachePluggable(log, keys, wrapper, metadata), + events: new EventsCachePluggable(log, keys, wrapper, metadata), + // @TODO add telemetry cache when required + + // Disconnect the underlying storage, to release its resources (such as open files, database connections, etc). + destroy() { + return wrapper.close(); + } + }; + }; +} diff --git a/src/storages/pluggable/wrapperAdapter.ts b/src/storages/pluggable/wrapperAdapter.ts index 06bcab0f..78e56eae 100644 --- a/src/storages/pluggable/wrapperAdapter.ts +++ b/src/storages/pluggable/wrapperAdapter.ts @@ -3,7 +3,7 @@ import { SplitError } from '../../utils/lang/errors'; import { ICustomStorageWrapper } from '../types'; import { LOG_PREFIX } from './constants'; -const METHODS_TO_PROMISE_WRAP: string[] = [ +export const METHODS_TO_PROMISE_WRAP: string[] = [ 'get', 'set', 'getAndSet', From b342cc4342bc47d2f54cddc0b7e48a0193062518 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 3 May 2021 20:27:37 -0300 Subject: [PATCH 025/144] refactors --- src/dtos/types.ts | 17 ++++----- src/sdkFactory/index.ts | 15 +++----- src/services/splitApi.ts | 4 +- src/services/splitHttpClient.ts | 17 ++++----- src/storages/KeyBuilderSS.ts | 6 +-- src/storages/__tests__/KeyBuilder.spec.ts | 6 +-- src/storages/inRedis/EventsCacheInRedis.ts | 6 +-- .../inRedis/ImpressionsCacheInRedis.ts | 6 +-- .../__tests__/CountsCacheInRedis.spec.ts | 2 +- .../__tests__/EventsCacheInRedis.spec.ts | 12 +++--- .../__tests__/LatenciesCacheInRedis.spec.ts | 2 +- src/storages/inRedis/__tests__/index.spec.ts | 27 +++++++------- src/storages/inRedis/index.ts | 11 ++---- src/storages/metadataBuilder.ts | 11 +++--- .../pluggable/EventsCachePluggable.ts | 6 +-- .../pluggable/ImpressionsCachePluggable.ts | 6 +-- .../__tests__/EventsCachePluggable.spec.ts | 12 +++--- .../ImpressionsCachePluggable.spec.ts | 4 +- src/storages/types.ts | 5 +-- .../__tests__/impressionsTracker.spec.ts | 37 ++++++++++--------- src/trackers/impressionsTracker.ts | 5 +-- .../__tests__/settings.mocks.ts | 4 ++ src/utils/settingsValidation/buildMetadata.ts | 11 ------ src/utils/settingsValidation/index.ts | 7 +++- src/utils/settingsValidation/types.ts | 4 +- 25 files changed, 114 insertions(+), 129 deletions(-) delete mode 100644 src/utils/settingsValidation/buildMetadata.ts diff --git a/src/dtos/types.ts b/src/dtos/types.ts index 1351dc8f..aa2c5d44 100644 --- a/src/dtos/types.ts +++ b/src/dtos/types.ts @@ -194,18 +194,15 @@ export interface IMySegmentsResponse { mySegments: IMySegmentsResponseItem[] } -/** Metadata internal types */ +/** Metadata for events, impressions and telemetry storages in consumer mode */ export interface IMetadata { - version: string // SDK version - ip: string | false // host IP - hostname: string | false // host name -} - -export interface IRedisMetadata { - s: string // SDK version - i: string // host IP - n: string // host name + /** SDK version */ + s: string + /** host IP */ + i: string + /** host name */ + n: string } export type ISplitFiltersValidation = { diff --git a/src/sdkFactory/index.ts b/src/sdkFactory/index.ts index 2fd3e67c..468aa77a 100644 --- a/src/sdkFactory/index.ts +++ b/src/sdkFactory/index.ts @@ -1,6 +1,5 @@ import { ISdkFactoryParams } from './types'; import sdkReadinessManagerFactory from '../readiness/sdkReadinessManager'; -import buildMetadata from '../utils/settingsValidation/buildMetadata'; import impressionsTrackerFactory from '../trackers/impressionsTracker'; import eventTrackerFactory from '../trackers/eventTracker'; import { IStorageSync } from '../storages/types'; @@ -11,6 +10,7 @@ import { shouldBeOptimized } from '../trackers/impressionObserver/utils'; import { validateAndTrackApiKey } from '../utils/inputValidation/apiKey'; import { createLoggerAPI } from '../logger/sdkLogger'; import { NEW_FACTORY, RETRIEVE_MANAGER } from '../logger/constants'; +import { metadataBuilder } from '../storages/metadataBuilder'; /** * Modular SDK factory @@ -26,29 +26,26 @@ export function sdkFactory(params: ISdkFactoryParams): SplitIO.ICsSDK | SplitIO. // We will just log and allow for the SDK to end up throwing an SDK_TIMEOUT event for devs to handle. validateAndTrackApiKey(log, settings.core.authorizationKey); - const metadata = buildMetadata(settings); - // @TODO handle non-recoverable error, such as, `fetch` api not available, invalid API Key, etc. const sdkReadinessManager = sdkReadinessManagerFactory(log, platform.EventEmitter, settings.startup.readyTimeout); + // @TODO consider passing the settings object, so that each storage access only what it needs const storageFactoryParams = { eventsQueueSize: settings.scheduler.eventsQueueSize, - // @TODO consider removing next prop and creating impressionsCounts cache somewhere else to simplify custom storages optimize: shouldBeOptimized(settings), - // @TODO add support for dataloader. consider calling outside the storageFactory to simplify custom storages - dataLoader: undefined, // ATM, only used by InLocalStorage matchingKey: getMatching(settings.core.key), splitFiltersValidation: settings.sync.__splitFiltersValidation, - // ATM, only used by InRedisStorage. @TODO pass a callback to simplify custom storages. + // Used by InRedis and Pluggable Storage readinessManager: sdkReadinessManager.readinessManager, - metadata, + metadata: metadataBuilder(settings), log }; const storage = storageFactory(storageFactoryParams); + // @TODO add support for dataloader: `if (params.dataLoader) params.dataLoader(storage);` // splitApi is used by SyncManager and Browser signal listener const splitApi = splitApiFactory && splitApiFactory(settings, platform); @@ -65,7 +62,7 @@ export function sdkFactory(params: ISdkFactoryParams): SplitIO.ICsSDK | SplitIO. // trackers const observer = impressionsObserverFactory && impressionsObserverFactory(); - const impressionsTracker = impressionsTrackerFactory(log, storage.impressions, metadata, impressionListener, integrationsManager, observer, storage.impressionCounts); + const impressionsTracker = impressionsTrackerFactory(log, storage.impressions, settings, impressionListener, integrationsManager, observer, storage.impressionCounts); const eventTracker = eventTrackerFactory(log, storage.events, integrationsManager); // signal listener diff --git a/src/services/splitApi.ts b/src/services/splitApi.ts index 9501f297..a8f8220b 100644 --- a/src/services/splitApi.ts +++ b/src/services/splitApi.ts @@ -1,5 +1,4 @@ import { IPlatform } from '../sdkFactory/types'; -import buildMetadata from '../utils/settingsValidation/buildMetadata'; import { ISettings } from '../types'; import { splitHttpClientFactory } from './splitHttpClient'; import { ISplitApi } from './types'; @@ -21,9 +20,8 @@ export function splitApiFactory(settings: ISettings, platform: IPlatform): ISpli const urls = settings.urls; const filterQueryString = (settings as ISettingsInternal).sync.__splitFiltersValidation && (settings as ISettingsInternal).sync.__splitFiltersValidation.queryString; - const metadata = buildMetadata(settings); const SplitSDKImpressionsMode = settings.sync.impressionsMode; - const splitHttpClient = splitHttpClientFactory(settings.log, settings.core.authorizationKey, metadata, platform.getFetch, platform.getOptions); + const splitHttpClient = splitHttpClientFactory(settings, platform.getFetch, platform.getOptions); return { fetchAuth(userMatchingKeys?: string[]) { diff --git a/src/services/splitHttpClient.ts b/src/services/splitHttpClient.ts index d19beb9c..b6e8952a 100644 --- a/src/services/splitHttpClient.ts +++ b/src/services/splitHttpClient.ts @@ -1,22 +1,21 @@ import { IFetch, IRequestOptions, ISplitHttpClient } from './types'; import { SplitError, SplitNetworkError } from '../utils/lang/errors'; import objectAssign from 'object-assign'; -import { IMetadata } from '../dtos/types'; -import { ILogger } from '../logger/types'; import { ERROR_HTTP, ERROR_CLIENT_CANNOT_GET_READY } from '../logger/constants'; +import { ISettings } from '../types'; const messageNoFetch = 'Global fetch API is not available.'; /** * Factory of Split HTTP clients, which are HTTP clients with predefined headers for Split endpoints. * - * @param apikey api key to set Authorization header - * @param metadata data to set additional headers + * @param settings SDK settings, used to access authorizationKey, logger instance and metadata (SDK version, ip and hostname) to set additional headers * @param options global request options * @param fetch optional http client to use instead of the global Fetch (for environments where Fetch API is not available such as Node) */ -export function splitHttpClientFactory(log: ILogger, apikey: string, metadata: IMetadata, getFetch?: () => (IFetch | undefined), getOptions?: () => object): ISplitHttpClient { +export function splitHttpClientFactory(settings: Pick, getFetch?: () => (IFetch | undefined), getOptions?: () => object): ISplitHttpClient { + const { log, core: { authorizationKey }, version, runtime: { ip, hostname } } = settings; const options = getOptions && getOptions(); const fetch = getFetch && getFetch(); @@ -26,12 +25,12 @@ export function splitHttpClientFactory(log: ILogger, apikey: string, metadata: I const headers: Record = { 'Accept': 'application/json', 'Content-Type': 'application/json', - 'Authorization': `Bearer ${apikey}`, - 'SplitSDKVersion': metadata.version + 'Authorization': `Bearer ${authorizationKey}`, + 'SplitSDKVersion': version }; - if (metadata.ip) headers['SplitSDKMachineIP'] = metadata.ip; - if (metadata.hostname) headers['SplitSDKMachineName'] = metadata.hostname; + if (ip) headers['SplitSDKMachineIP'] = ip; + if (hostname) headers['SplitSDKMachineName'] = hostname; return function httpClient(url: string, reqOpts: IRequestOptions = {}, logErrorsAsInfo: boolean = false): Promise { diff --git a/src/storages/KeyBuilderSS.ts b/src/storages/KeyBuilderSS.ts index 1270c6f3..8944eecb 100644 --- a/src/storages/KeyBuilderSS.ts +++ b/src/storages/KeyBuilderSS.ts @@ -7,9 +7,9 @@ import { IMetadata } from '../dtos/types'; export default class KeyBuilderSS extends KeyBuilder { - protected readonly metadata: Required; + protected readonly metadata: IMetadata; - constructor(prefix: string, metadata: Required) { + constructor(prefix: string, metadata: IMetadata) { super(prefix); this.metadata = metadata; } @@ -19,7 +19,7 @@ export default class KeyBuilderSS extends KeyBuilder { } private buildVersionablePrefix() { - return `${this.prefix}/${this.metadata.version}/${this.metadata.ip}`; + return `${this.prefix}/${this.metadata.s}/${this.metadata.i}`; } buildImpressionsKey() { diff --git a/src/storages/__tests__/KeyBuilder.spec.ts b/src/storages/__tests__/KeyBuilder.spec.ts index b3e77dac..6016d169 100644 --- a/src/storages/__tests__/KeyBuilder.spec.ts +++ b/src/storages/__tests__/KeyBuilder.spec.ts @@ -67,7 +67,7 @@ test('KEYS / traffic type keys', () => { test('KEYS / impressions', () => { const prefix = 'SPLITIO'; - const metadata = { version: 'js-1234', ip: '10-10-10-10', hostname: 'UNKNOWN' }; + const metadata = { s: 'js-1234', i: '10-10-10-10', n: 'UNKNOWN' }; const builder = new KeyBuilderSS(prefix, metadata); const expectedImpressionKey = `${prefix}.impressions`; @@ -92,13 +92,13 @@ test('KEYS / events', () => { test('KEYS / latency keys', () => { const prefix = 'SPLITIO'; - const metadata = { version: 'js-1234', ip: '10-10-10-10', hostname: 'UNKNOWN' }; + const metadata = { s: 'js-1234', i: '10-10-10-10', n: 'UNKNOWN' }; const builder = new KeyBuilderSS(prefix, metadata); const metricName = 'unit testing metric name'; const bucketNumber = '10'; - const expectedLatencyKey = `${prefix}/${metadata.version}/${metadata.ip}/latency.${metricName}.bucket.${bucketNumber}`; + const expectedLatencyKey = `${prefix}/${metadata.s}/${metadata.i}/latency.${metricName}.bucket.${bucketNumber}`; expect(builder.buildLatencyKey(metricName, bucketNumber) === expectedLatencyKey).toBe(true); diff --git a/src/storages/inRedis/EventsCacheInRedis.ts b/src/storages/inRedis/EventsCacheInRedis.ts index 97f7d13f..ff711140 100644 --- a/src/storages/inRedis/EventsCacheInRedis.ts +++ b/src/storages/inRedis/EventsCacheInRedis.ts @@ -1,5 +1,5 @@ import { IEventsCacheAsync } from '../types'; -import { IRedisMetadata } from '../../dtos/types'; +import { IMetadata } from '../../dtos/types'; import KeyBuilderSS from '../KeyBuilderSS'; import { Redis } from 'ioredis'; import { SplitIO } from '../../types'; @@ -10,10 +10,10 @@ export default class EventsCacheInRedis implements IEventsCacheAsync { private readonly redis: Redis; private readonly keys: KeyBuilderSS; - private readonly metadata: IRedisMetadata; + private readonly metadata: IMetadata; private readonly eventsKey: string; - constructor(private readonly log: ILogger, keys: KeyBuilderSS, redis: Redis, metadata: IRedisMetadata) { + constructor(private readonly log: ILogger, keys: KeyBuilderSS, redis: Redis, metadata: IMetadata) { this.keys = keys; this.redis = redis; this.metadata = metadata; diff --git a/src/storages/inRedis/ImpressionsCacheInRedis.ts b/src/storages/inRedis/ImpressionsCacheInRedis.ts index 9fdfdf81..9013478f 100644 --- a/src/storages/inRedis/ImpressionsCacheInRedis.ts +++ b/src/storages/inRedis/ImpressionsCacheInRedis.ts @@ -1,5 +1,5 @@ import { IImpressionsCacheAsync } from '../types'; -import { IRedisMetadata } from '../../dtos/types'; +import { IMetadata } from '../../dtos/types'; import { ImpressionDTO } from '../../types'; import KeyBuilderSS from '../KeyBuilderSS'; import { Redis } from 'ioredis'; @@ -8,9 +8,9 @@ export default class ImpressionsCacheInRedis implements IImpressionsCacheAsync { private readonly redis: Redis; private readonly keys: KeyBuilderSS; - private readonly metadata: IRedisMetadata; + private readonly metadata: IMetadata; - constructor(keys: KeyBuilderSS, redis: Redis, metadata: IRedisMetadata) { + constructor(keys: KeyBuilderSS, redis: Redis, metadata: IMetadata) { this.keys = keys; this.redis = redis; this.metadata = metadata; diff --git a/src/storages/inRedis/__tests__/CountsCacheInRedis.spec.ts b/src/storages/inRedis/__tests__/CountsCacheInRedis.spec.ts index 3d1d4cbd..8220a3ce 100644 --- a/src/storages/inRedis/__tests__/CountsCacheInRedis.spec.ts +++ b/src/storages/inRedis/__tests__/CountsCacheInRedis.spec.ts @@ -3,7 +3,7 @@ import KeyBuilderSS from '../../KeyBuilderSS'; import CountsCacheInRedis from '../CountsCacheInRedis'; const prefix = 'counts_cache_ut'; -const metadata = { version: 'v', ip: 'i', hostname: 'n' }; +const metadata = { s: 'version', i: 'ip', n: 'hostname' }; test('COUNTS CACHE IN REDIS / cover basic behavior', async () => { const connection = new Redis({}); diff --git a/src/storages/inRedis/__tests__/EventsCacheInRedis.spec.ts b/src/storages/inRedis/__tests__/EventsCacheInRedis.spec.ts index f5831481..64ed72a0 100644 --- a/src/storages/inRedis/__tests__/EventsCacheInRedis.spec.ts +++ b/src/storages/inRedis/__tests__/EventsCacheInRedis.spec.ts @@ -4,18 +4,16 @@ import isEqual from 'lodash/isEqual'; import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; import KeyBuilderSS from '../../KeyBuilderSS'; import EventsCacheInRedis from '../EventsCacheInRedis'; -import { metadataBuilder } from '../../metadataBuilder'; const prefix = 'events_cache_ut'; -const metadata = { version: 'js_someversion', ip: 'some_ip', hostname: 'some_hostname' }; +const fakeMetadata = { s: 'js_someversion', i: 'some_ip', n: 'some_hostname' }; test('EVENTS CACHE IN REDIS / should incrementally store values in redis', async () => { const connection = new Redis(); // This piece is being tested elsewhere. - const keys = new KeyBuilderSS(prefix, metadata); + const keys = new KeyBuilderSS(prefix, fakeMetadata); const key = keys.buildEventsKey(); - const fakeRedisMetadata = metadataBuilder(metadata); const fakeEvent1 = { event: 1 }; const fakeEvent2 = { event: '2' }; const fakeEvent3 = { event: null }; @@ -27,13 +25,13 @@ test('EVENTS CACHE IN REDIS / should incrementally store values in redis', async expect(redisValues.length).toBe(0); // control assertion, there are no events previously queued. - const cache = new EventsCacheInRedis(loggerMock, keys, connection, fakeRedisMetadata); + const cache = new EventsCacheInRedis(loggerMock, keys, connection, fakeMetadata); // I'll use a "bad" instance so I can force an issue with the rpush command. I'll store an integer and will make the cache try to use rpush there. await connection.set('non-list-key', 10); // @ts-expect-error const faultyCache = new EventsCacheInRedis(loggerMock, { buildEventsKey: () => 'non-list-key' - }, connection, fakeRedisMetadata); + }, connection, fakeMetadata); // @ts-expect-error expect(await cache.track(fakeEvent1)).toBe(true); // If the queueing operation was successful, it should resolve the returned promise with "true" @@ -58,7 +56,7 @@ test('EVENTS CACHE IN REDIS / should incrementally store values in redis', async const findMatchingElem = (event: any) => { return find(redisValues, elem => { const parsedElem = JSON.parse(elem); - return isEqual(parsedElem.e, event) && isEqual(parsedElem.m, fakeRedisMetadata); + return isEqual(parsedElem.e, event) && isEqual(parsedElem.m, fakeMetadata); }); }; diff --git a/src/storages/inRedis/__tests__/LatenciesCacheInRedis.spec.ts b/src/storages/inRedis/__tests__/LatenciesCacheInRedis.spec.ts index 0e672adb..e592bdd9 100644 --- a/src/storages/inRedis/__tests__/LatenciesCacheInRedis.spec.ts +++ b/src/storages/inRedis/__tests__/LatenciesCacheInRedis.spec.ts @@ -3,7 +3,7 @@ import KeyBuilderSS from '../../KeyBuilderSS'; import LatenciesCacheInRedis from '../LatenciesCacheInRedis'; const prefix = 'latencies_cache_UT'; -const metadata = { version: 'js_someversion', ip: 'some_ip', hostname: 'some_hostname' }; +const metadata = { s: 'js_someversion', i: 'some_ip', n: 'some_hostname' }; test('METRICS (LATENCIES) CACHE IN REDIS / should count based on ranges', async () => { const connection = new Redis(); diff --git a/src/storages/inRedis/__tests__/index.spec.ts b/src/storages/inRedis/__tests__/index.spec.ts index 2ac012f1..2a611419 100644 --- a/src/storages/inRedis/__tests__/index.spec.ts +++ b/src/storages/inRedis/__tests__/index.spec.ts @@ -1,31 +1,32 @@ import { metadataBuilder } from '../../metadataBuilder'; import { UNKNOWN } from '../../../utils/constants'; -import { IMetadata } from '../../../dtos/types'; describe('metadataBuilder', () => { test('metadata object creation', () => { - const metadata = { + const settings = { version: 'test.version', - ip: 'test.ip', - hostname: 'test.hostname' + runtime: { + ip: 'test.ip', + hostname: 'test.hostname' + } }; - let meta = metadataBuilder(metadata); + let meta = metadataBuilder(settings); - expect(meta.s).toBe(metadata.version); // SDK Version should be returned as the "s" property. - expect(meta.i).toBe(metadata.ip); // SDK runtime IP should be returned as the "i" property. - expect(meta.n).toBe(metadata.hostname); // SDK runtime hostname should be returned as the "n" property. + expect(meta.s).toBe(settings.version); // SDK Version should be returned as the "s" property. + expect(meta.i).toBe(settings.runtime.ip); // SDK runtime IP should be returned as the "i" property. + expect(meta.n).toBe(settings.runtime.hostname); // SDK runtime hostname should be returned as the "n" property. - expect(meta !== metadataBuilder(metadata)).toBe(true); // Should return a new object each time we call it. + expect(meta !== metadataBuilder(settings)).toBe(true); // Should return a new object each time we call it. }); test('creation with undefined properties', () => { - const metadata: IMetadata = { - version: 'test.version', ip: false, hostname: false + const fakeSettings = { + version: 'test.version', runtime: { ip: false as false, hostname: false as false } }; - let meta = metadataBuilder(metadata); + let meta = metadataBuilder(fakeSettings); - expect(meta.s).toBe(metadata.version); // SDK Version should be returned as the "s" property. + expect(meta.s).toBe(fakeSettings.version); // SDK Version should be returned as the "s" property. expect(meta.i).toBe(UNKNOWN); // The "i" property should be 'UNKNOWN' if IP value is undefined. expect(meta.n).toBe(UNKNOWN); // The "n" property should be 'UNKNOWN' if hostname value is undefined. }); diff --git a/src/storages/inRedis/index.ts b/src/storages/inRedis/index.ts index 935cdf67..738d1e58 100644 --- a/src/storages/inRedis/index.ts +++ b/src/storages/inRedis/index.ts @@ -8,7 +8,6 @@ import EventsCacheInRedis from './EventsCacheInRedis'; import LatenciesCacheInRedis from './LatenciesCacheInRedis'; import CountsCacheInRedis from './CountsCacheInRedis'; import { SDK_SPLITS_ARRIVED, SDK_SEGMENTS_ARRIVED } from '../../readiness/constants'; -import { metadataBuilder } from '../metadataBuilder'; export interface InRedisStorageOptions { prefix?: string @@ -23,18 +22,16 @@ export function InRedisStorage(options: InRedisStorageOptions = {}) { const prefix = options.prefix ? options.prefix + '.SPLITIO' : 'SPLITIO'; - return function InRedisStorageFactory(params: IStorageFactoryParams): IStorageAsync { + return function InRedisStorageFactory({ log, readinessManager, metadata}: IStorageFactoryParams): IStorageAsync { - const log = params.log; - const metadata = metadataBuilder(params.metadata); - const keys = new KeyBuilderSS(prefix, { version: metadata.s, ip: metadata.i, hostname: metadata.n }); + const keys = new KeyBuilderSS(prefix, metadata); const redisClient = new RedisAdapter(log, options.options || {}); // subscription to Redis connect event in order to emit SDK_READY event // @TODO pass a callback to simplify custom storages redisClient.on('connect', () => { - params.readinessManager.splits.emit(SDK_SPLITS_ARRIVED); - params.readinessManager.segments.emit(SDK_SEGMENTS_ARRIVED); + readinessManager.splits.emit(SDK_SPLITS_ARRIVED); + readinessManager.segments.emit(SDK_SEGMENTS_ARRIVED); }); return { diff --git a/src/storages/metadataBuilder.ts b/src/storages/metadataBuilder.ts index ca75572d..2cfd9d2d 100644 --- a/src/storages/metadataBuilder.ts +++ b/src/storages/metadataBuilder.ts @@ -1,10 +1,11 @@ -import { IMetadata, IRedisMetadata } from '../dtos/types'; +import { IMetadata } from '../dtos/types'; +import { ISettings } from '../types'; import { UNKNOWN } from '../utils/constants'; -export function metadataBuilder(metadata: IMetadata): IRedisMetadata { +export function metadataBuilder(settings: Pick): IMetadata { return { - s: metadata.version, - i: metadata.ip || UNKNOWN, - n: metadata.hostname || UNKNOWN, + s: settings.version, + i: settings.runtime.ip || UNKNOWN, + n: settings.runtime.hostname || UNKNOWN, }; } diff --git a/src/storages/pluggable/EventsCachePluggable.ts b/src/storages/pluggable/EventsCachePluggable.ts index 2af117d7..b706d04e 100644 --- a/src/storages/pluggable/EventsCachePluggable.ts +++ b/src/storages/pluggable/EventsCachePluggable.ts @@ -1,5 +1,5 @@ import { ICustomStorageWrapper, IEventsCacheAsync } from '../types'; -import { IRedisMetadata } from '../../dtos/types'; +import { IMetadata } from '../../dtos/types'; import KeyBuilderSS from '../KeyBuilderSS'; import { SplitIO } from '../../types'; import { ILogger } from '../../logger/types'; @@ -10,9 +10,9 @@ export class EventsCachePluggable implements IEventsCacheAsync { private readonly log: ILogger; private readonly wrapper: ICustomStorageWrapper; private readonly keys: KeyBuilderSS; - private readonly metadata: IRedisMetadata; + private readonly metadata: IMetadata; - constructor(log: ILogger, keys: KeyBuilderSS, wrapper: ICustomStorageWrapper, metadata: IRedisMetadata) { + constructor(log: ILogger, keys: KeyBuilderSS, wrapper: ICustomStorageWrapper, metadata: IMetadata) { this.log = log; this.keys = keys; this.wrapper = wrapper; diff --git a/src/storages/pluggable/ImpressionsCachePluggable.ts b/src/storages/pluggable/ImpressionsCachePluggable.ts index 31436c43..3f039623 100644 --- a/src/storages/pluggable/ImpressionsCachePluggable.ts +++ b/src/storages/pluggable/ImpressionsCachePluggable.ts @@ -1,5 +1,5 @@ import { ICustomStorageWrapper, IImpressionsCacheAsync } from '../types'; -import { IRedisMetadata } from '../../dtos/types'; +import { IMetadata } from '../../dtos/types'; import { ImpressionDTO } from '../../types'; import KeyBuilderSS from '../KeyBuilderSS'; import { ILogger } from '../../logger/types'; @@ -10,9 +10,9 @@ export class ImpressionsCachePluggable implements IImpressionsCacheAsync { private readonly log: ILogger; private readonly keys: KeyBuilderSS; private readonly wrapper: ICustomStorageWrapper; - private readonly metadata: IRedisMetadata; + private readonly metadata: IMetadata; - constructor(log: ILogger, keys: KeyBuilderSS, wrapper: ICustomStorageWrapper, metadata: IRedisMetadata) { + constructor(log: ILogger, keys: KeyBuilderSS, wrapper: ICustomStorageWrapper, metadata: IMetadata) { this.log = log; this.keys = keys; this.wrapper = wrapper; diff --git a/src/storages/pluggable/__tests__/EventsCachePluggable.spec.ts b/src/storages/pluggable/__tests__/EventsCachePluggable.spec.ts index dfac96f6..c64ab417 100644 --- a/src/storages/pluggable/__tests__/EventsCachePluggable.spec.ts +++ b/src/storages/pluggable/__tests__/EventsCachePluggable.spec.ts @@ -3,16 +3,14 @@ import isEqual from 'lodash/isEqual'; import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; import KeyBuilderSS from '../../KeyBuilderSS'; import { EventsCachePluggable } from '../EventsCachePluggable'; -import { metadataBuilder } from '../../metadataBuilder'; import { wrapperMockFactory } from './wrapper.mock'; import { wrapperAdapter } from '../wrapperAdapter'; const prefix = 'events_cache_ut'; -const metadata = { version: 'js_someversion', ip: 'some_ip', hostname: 'some_hostname' }; -const keys = new KeyBuilderSS(prefix, metadata); +const fakeMetadata = { s: 'js_someversion', i: 'some_ip', n: 'some_hostname' }; +const keys = new KeyBuilderSS(prefix, fakeMetadata); const key = keys.buildEventsKey(); -const fakeRedisMetadata = metadataBuilder(metadata); const fakeEvent1 = { event: 1 }; const fakeEvent2 = { event: '2' }; const fakeEvent3 = { event: null }; @@ -21,7 +19,7 @@ describe('PLUGGABLE EVENTS CACHE', () => { test('`track` method should push values into the pluggable storage', async () => { const wrapperMock = wrapperMockFactory(); - const cache = new EventsCachePluggable(loggerMock, keys, wrapperMock, fakeRedisMetadata); + const cache = new EventsCachePluggable(loggerMock, keys, wrapperMock, fakeMetadata); // @ts-expect-error expect(await cache.track(fakeEvent1)).toBe(true); // If the queueing operation was successful, it should resolve the returned promise with "true" @@ -40,7 +38,7 @@ describe('PLUGGABLE EVENTS CACHE', () => { const findMatchingElem = (event: any) => { return find(values, elem => { const parsedElem = JSON.parse(elem); - return isEqual(parsedElem.e, event) && isEqual(parsedElem.m, fakeRedisMetadata); + return isEqual(parsedElem.e, event) && isEqual(parsedElem.m, fakeMetadata); }); }; @@ -62,7 +60,7 @@ describe('PLUGGABLE EVENTS CACHE', () => { // @ts-expect-error. wrapperMock is adapted this time to properly handle unexpected exceptions const faultyCache = new EventsCachePluggable(loggerMock, { buildEventsKey: () => 'non-list-key' - }, wrapperAdapter(loggerMock, wrapperMock), fakeRedisMetadata); + }, wrapperAdapter(loggerMock, wrapperMock), fakeMetadata); // @ts-expect-error expect(await faultyCache.track(fakeEvent1)).toBe(false); // If the queueing operation was NOT successful, it should resolve the returned promise with "false" instead of rejecting it. diff --git a/src/storages/pluggable/__tests__/ImpressionsCachePluggable.spec.ts b/src/storages/pluggable/__tests__/ImpressionsCachePluggable.spec.ts index 00a88d95..f8a89a8c 100644 --- a/src/storages/pluggable/__tests__/ImpressionsCachePluggable.spec.ts +++ b/src/storages/pluggable/__tests__/ImpressionsCachePluggable.spec.ts @@ -3,11 +3,11 @@ import KeyBuilderSS from '../../KeyBuilderSS'; import { ImpressionsCachePluggable } from '../ImpressionsCachePluggable'; import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; import { wrapperMock } from './wrapper.mock'; -import { IRedisMetadata } from '../../../dtos/types'; +import { IMetadata } from '../../../dtos/types'; const prefix = 'impr_cache_ut'; const impressionsKey = `${prefix}.impressions`; -const testMeta: IRedisMetadata = { i: 'some_ip', n: 'some_host', s: 'some_sdk_version' }; // @ts-ignore +const testMeta: IMetadata = { i: 'some_ip', n: 'some_host', s: 'some_sdk_version' }; // @ts-ignore const keys = new KeyBuilderSS(prefix, testMeta); const o1 = { diff --git a/src/storages/types.ts b/src/storages/types.ts index 81024db1..4d52d5fb 100644 --- a/src/storages/types.ts +++ b/src/storages/types.ts @@ -1,4 +1,4 @@ -import { MaybeThenable, IMetadata, ISplitFiltersValidation } from '../dtos/types'; +import { MaybeThenable, ISplitFiltersValidation, IMetadata } from '../dtos/types'; import { ILogger } from '../logger/types'; import { IReadinessManager } from '../readiness/types'; import { SplitIO, ImpressionDTO } from '../types'; @@ -334,13 +334,12 @@ export interface IStorageFactoryParams { log: ILogger, eventsQueueSize: number, optimize: boolean /* whether create the `impressionCounts` cache (OPTIMIZED impression mode) or not (DEBUG impression mode) */, - dataLoader?: DataLoader, // ATM, only used by InLocalStorage matchingKey?: string, /* undefined on server-side SDKs */ splitFiltersValidation: ISplitFiltersValidation, - // ATM, only used by InRedisStorage. @TODO pass a callback to simplify custom storages. + // Used by InRedis and Pluggable Storage readinessManager: IReadinessManager, metadata: IMetadata, } diff --git a/src/trackers/__tests__/impressionsTracker.spec.ts b/src/trackers/__tests__/impressionsTracker.spec.ts index 9a89a61d..b85135f1 100644 --- a/src/trackers/__tests__/impressionsTracker.spec.ts +++ b/src/trackers/__tests__/impressionsTracker.spec.ts @@ -2,7 +2,6 @@ import impressionsTrackerFactory from '../impressionsTracker'; import ImpressionCountsCacheInMemory from '../../storages/inMemory/ImpressionCountsCacheInMemory'; import { impressionObserverSSFactory } from '../impressionObserver/impressionObserverSS'; import { impressionObserverCSFactory } from '../impressionObserver/impressionObserverCS'; -import { IMetadata } from '../../dtos/types'; import { ImpressionDTO } from '../../types'; import { loggerMock } from '../../logger/__tests__/sdkLogger.mock'; @@ -12,9 +11,11 @@ function generateMocks() { const fakeImpressionsCache = { track: jest.fn() }; - const fakeMetadata: IMetadata = { - hostname: 'fake-hostname', - ip: 'fake-ip', + const fakeSettings = { + runtime: { + hostname: 'fake-hostname', + ip: 'fake-ip' + }, version: 'jest-test', }; const fakeListener = { @@ -25,7 +26,7 @@ function generateMocks() { }; return { - fakeImpressionsCache, fakeMetadata, fakeListener, fakeIntegrationsManager + fakeImpressionsCache, fakeSettings, fakeListener, fakeIntegrationsManager }; } @@ -36,14 +37,14 @@ describe('Impressions Tracker', () => { test('Tracker API', () => { expect(typeof impressionsTrackerFactory).toBe('function'); // The module should return a function which acts as a factory. - const { fakeImpressionsCache, fakeMetadata } = generateMocks(); - const instance = impressionsTrackerFactory(loggerMock, fakeImpressionsCache, fakeMetadata); + const { fakeImpressionsCache, fakeSettings } = generateMocks(); + const instance = impressionsTrackerFactory(loggerMock, fakeImpressionsCache, fakeSettings); expect(typeof instance.track).toBe('function'); // The instance should implement the track method which will actually track queued impressions. }); test('Should be able to track impressions (in DEBUG mode without Previous Time).', () => { - const { fakeImpressionsCache, fakeMetadata } = generateMocks(); - const tracker = impressionsTrackerFactory(loggerMock, fakeImpressionsCache, fakeMetadata); + const { fakeImpressionsCache, fakeSettings } = generateMocks(); + const tracker = impressionsTrackerFactory(loggerMock, fakeImpressionsCache, fakeSettings); const imp1 = { feature: '10', @@ -63,8 +64,8 @@ describe('Impressions Tracker', () => { }); test('Tracked impressions should be sent to impression listener and integration manager when we invoke .track()', (done) => { - const { fakeImpressionsCache, fakeMetadata, fakeListener, fakeIntegrationsManager } = generateMocks(); - const tracker = impressionsTrackerFactory(loggerMock, fakeImpressionsCache, fakeMetadata, fakeListener, fakeIntegrationsManager); + const { fakeImpressionsCache, fakeSettings, fakeListener, fakeIntegrationsManager } = generateMocks(); + const tracker = impressionsTrackerFactory(loggerMock, fakeImpressionsCache, fakeSettings, fakeListener, fakeIntegrationsManager); const fakeImpression = { feature: 'impression' @@ -91,8 +92,8 @@ describe('Impressions Tracker', () => { expect(fakeListener.logImpression).toBeCalledTimes(2); // The listener should be executed after the timeout wrapping make it to the queue stack, once per each tracked impression. expect(fakeIntegrationsManager.handleImpression).toBeCalledTimes(2); // The integrations manager handleImpression method should be executed after the timeout wrapping make it to the queue stack, once per each tracked impression. - const impressionData1 = { impression: fakeImpression, attributes: fakeAttributes, sdkLanguageVersion: fakeMetadata.version, ip: fakeMetadata.ip, hostname: fakeMetadata.hostname }; - const impressionData2 = { impression: fakeImpression2, attributes: fakeAttributes, sdkLanguageVersion: fakeMetadata.version, ip: fakeMetadata.ip, hostname: fakeMetadata.hostname }; + const impressionData1 = { impression: fakeImpression, attributes: fakeAttributes, sdkLanguageVersion: fakeSettings.version, ip: fakeSettings.runtime.ip, hostname: fakeSettings.runtime.hostname }; + const impressionData2 = { impression: fakeImpression2, attributes: fakeAttributes, sdkLanguageVersion: fakeSettings.version, ip: fakeSettings.runtime.ip, hostname: fakeSettings.runtime.hostname }; expect(fakeListener.logImpression.mock.calls[0][0]).toEqual(impressionData1); // The listener should be executed with the corresponding map for each of the impressions. expect(fakeListener.logImpression.mock.calls[1][0]).toEqual(impressionData2); // The listener should be executed with the corresponding map for each of the impressions. @@ -134,13 +135,13 @@ describe('Impressions Tracker', () => { } as ImpressionDTO; test('Should track 3 impressions with Previous Time.', () => { - const { fakeImpressionsCache, fakeMetadata } = generateMocks(); + const { fakeImpressionsCache, fakeSettings } = generateMocks(); impression.time = impression2.time = 123456789; impression3.time = 1234567891; const trackers = [ - impressionsTrackerFactory(loggerMock, fakeImpressionsCache, fakeMetadata, undefined, undefined, impressionObserverSSFactory()), - impressionsTrackerFactory(loggerMock, fakeImpressionsCache, fakeMetadata, undefined, undefined, impressionObserverCSFactory()) + impressionsTrackerFactory(loggerMock, fakeImpressionsCache, fakeSettings, undefined, undefined, impressionObserverSSFactory()), + impressionsTrackerFactory(loggerMock, fakeImpressionsCache, fakeSettings, undefined, undefined, impressionObserverCSFactory()) ]; expect(fakeImpressionsCache.track).not.toBeCalled(); // storage method should not be called until impressions are tracked. @@ -162,13 +163,13 @@ describe('Impressions Tracker', () => { }); test('Should track 2 impressions in OPTIMIZED mode (providing ImpressionCountsCache).', () => { - const { fakeImpressionsCache, fakeMetadata } = generateMocks(); + const { fakeImpressionsCache, fakeSettings } = generateMocks(); impression.time = Date.now(); impression2.time = Date.now(); impression3.time = Date.now(); const impressionCountsCache = new ImpressionCountsCacheInMemory(); - const tracker = impressionsTrackerFactory(loggerMock, fakeImpressionsCache, fakeMetadata, undefined, undefined, impressionObserverCSFactory(), impressionCountsCache); + const tracker = impressionsTrackerFactory(loggerMock, fakeImpressionsCache, fakeSettings, undefined, undefined, impressionObserverCSFactory(), impressionCountsCache); expect(fakeImpressionsCache.track).not.toBeCalled(); // cache method should not be called by just creating a tracker diff --git a/src/trackers/impressionsTracker.ts b/src/trackers/impressionsTracker.ts index 5d227892..226e94c4 100644 --- a/src/trackers/impressionsTracker.ts +++ b/src/trackers/impressionsTracker.ts @@ -3,8 +3,7 @@ import thenable from '../utils/promise/thenable'; import { truncateTimeFrame } from '../utils/time'; import { IImpressionCountsCacheBase, IImpressionsCacheBase } from '../storages/types'; import { IImpressionsHandler, IImpressionsTracker } from './types'; -import { IMetadata } from '../dtos/types'; -import { SplitIO, ImpressionDTO } from '../types'; +import { SplitIO, ImpressionDTO, ISettings } from '../types'; import { IImpressionObserver } from './impressionObserver/types'; import { ILogger } from '../logger/types'; import { IMPRESSIONS_TRACKER_SUCCESS, ERROR_IMPRESSIONS_TRACKER, ERROR_IMPRESSIONS_LISTENER } from '../logger/constants'; @@ -24,7 +23,7 @@ export default function impressionsTrackerFactory( impressionsCache: IImpressionsCacheBase, // @TODO consider passing only an optional integrationsManager to handle impressions - { ip, hostname, version }: IMetadata, + { runtime: { ip, hostname }, version }: Pick, impressionListener?: SplitIO.IImpressionListener, integrationsManager?: IImpressionsHandler, diff --git a/src/utils/settingsValidation/__tests__/settings.mocks.ts b/src/utils/settingsValidation/__tests__/settings.mocks.ts index 23b8dfee..98fef431 100644 --- a/src/utils/settingsValidation/__tests__/settings.mocks.ts +++ b/src/utils/settingsValidation/__tests__/settings.mocks.ts @@ -97,5 +97,9 @@ export const settingsSplitApi = { sync: { impressionsMode: 'DEBUG' }, + runtime: { + ip: false, + hostname: false + }, log: loggerMock }; diff --git a/src/utils/settingsValidation/buildMetadata.ts b/src/utils/settingsValidation/buildMetadata.ts deleted file mode 100644 index 52642f63..00000000 --- a/src/utils/settingsValidation/buildMetadata.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { IMetadata } from '../../dtos/types'; -import { ISettings } from '../../types'; - -export default function buildMetadata(settings: ISettings): IMetadata { - const { version, runtime } = settings; - return { - version, - ip: runtime && runtime.ip, - hostname: runtime && runtime.hostname, - }; -} diff --git a/src/utils/settingsValidation/index.ts b/src/utils/settingsValidation/index.ts index 0f07c330..4f77cc8f 100644 --- a/src/utils/settingsValidation/index.ts +++ b/src/utils/settingsValidation/index.ts @@ -76,6 +76,11 @@ const base = { impressionsMode: OPTIMIZED }, + runtime: { + ip: false, + hostname: false + }, + // Logger log: undefined }; @@ -130,7 +135,7 @@ export function settingsValidation(config: unknown, validationParams: ISettingsV // Current ip/hostname information // @ts-ignore, modify readonly prop - withDefaults.runtime = runtime(withDefaults); + if (runtime) withDefaults.runtime = runtime(withDefaults); // ensure a valid list of integrations. // `integrations` returns an array of valid integration items. diff --git a/src/utils/settingsValidation/types.ts b/src/utils/settingsValidation/types.ts index 5a07379d..77759c88 100644 --- a/src/utils/settingsValidation/types.ts +++ b/src/utils/settingsValidation/types.ts @@ -7,10 +7,12 @@ import { ISettings } from '../../types'; */ export interface ISettingsValidationParams { /** + * Object of values to overwrite default settings. * Version and startup properties are mandatory, because these values are not part of the base setting. */ defaults: Partial & { version: string } & { startup: ISettings['startup'] }, - runtime: (settings: ISettings) => ISettings['runtime'], + /** Function to overwrite runtime values (ip and hostname) which are false by default */ + runtime?: (settings: ISettings) => ISettings['runtime'], /** Storage validator */ storage?: (settings: ISettings) => ISettings['storage'], /** Integrations validator */ From 231f754d50721243af7e3bc7b5fe7ea4b877051d Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 3 May 2021 20:59:51 -0300 Subject: [PATCH 026/144] updated comment --- src/dtos/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dtos/types.ts b/src/dtos/types.ts index aa2c5d44..a61e3777 100644 --- a/src/dtos/types.ts +++ b/src/dtos/types.ts @@ -194,7 +194,7 @@ export interface IMySegmentsResponse { mySegments: IMySegmentsResponseItem[] } -/** Metadata for events, impressions and telemetry storages in consumer mode */ +/** Metadata internal type for storages */ export interface IMetadata { /** SDK version */ From 11937d614d11bf76b2f2279c728ddd6efa9e44bb Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 3 May 2021 21:03:24 -0300 Subject: [PATCH 027/144] updated pluggable storage factory --- src/storages/pluggable/index.ts | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/storages/pluggable/index.ts b/src/storages/pluggable/index.ts index 4be7bf00..8dc3f95d 100644 --- a/src/storages/pluggable/index.ts +++ b/src/storages/pluggable/index.ts @@ -7,7 +7,6 @@ import { ImpressionsCachePluggable } from './ImpressionsCachePluggable'; import { EventsCachePluggable } from './EventsCachePluggable'; import { SDK_SPLITS_ARRIVED, SDK_SEGMENTS_ARRIVED } from '../../readiness/constants'; -import { metadataBuilder } from '../metadataBuilder'; import { wrapperAdapter, METHODS_TO_PROMISE_WRAP } from './wrapperAdapter'; import { isObject } from '../../utils/lang'; @@ -38,17 +37,15 @@ export function PluggableStorage(options: PluggableStorageOptions) { const prefix = options.prefix ? options.prefix + '.SPLITIO' : 'SPLITIO'; - return function PluggableStorageFactory(params: IStorageFactoryParams): IStorageAsync { + return function PluggableStorageFactory({ log, metadata, readinessManager }: IStorageFactoryParams): IStorageAsync { - const log = params.log; - const metadata = metadataBuilder(params.metadata); - const keys = new KeyBuilderSS(prefix, { version: metadata.s, ip: metadata.i, hostname: metadata.n }); + const keys = new KeyBuilderSS(prefix, metadata); const wrapper = wrapperAdapter(log, options.wrapper); // subscription to Wrapper connect event in order to emit SDK_READY event wrapper.connect().then(() => { - params.readinessManager.splits.emit(SDK_SPLITS_ARRIVED); - params.readinessManager.segments.emit(SDK_SEGMENTS_ARRIVED); + readinessManager.splits.emit(SDK_SPLITS_ARRIVED); + readinessManager.segments.emit(SDK_SEGMENTS_ARRIVED); }); return { From b71fc335ff90b71a6d48c6104657e9f434c8fb03 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 3 May 2021 21:15:36 -0300 Subject: [PATCH 028/144] fixed sdkFactory signal listener --- src/sdkFactory/index.ts | 4 ++-- src/sdkFactory/types.ts | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/sdkFactory/index.ts b/src/sdkFactory/index.ts index cc49b4a2..468aa77a 100644 --- a/src/sdkFactory/index.ts +++ b/src/sdkFactory/index.ts @@ -65,8 +65,8 @@ export function sdkFactory(params: ISdkFactoryParams): SplitIO.ICsSDK | SplitIO. const impressionsTracker = impressionsTrackerFactory(log, storage.impressions, settings, impressionListener, integrationsManager, observer, storage.impressionCounts); const eventTracker = eventTrackerFactory(log, storage.events, integrationsManager); - // signal listener. Instantiates only if `syncManager.flush` (used by Node listener) and `splitApi` (used by Browser listener) are available - const signalListener = SignalListener && syncManager && splitApi && new SignalListener(syncManager.flush, settings, storage, splitApi); + // signal listener + const signalListener = SignalListener && new SignalListener(syncManager && syncManager.flush, settings, storage, splitApi); // Sdk client and manager const clientMethod = sdkClientMethodFactory({ eventTracker, impressionsTracker, sdkReadinessManager, settings, storage, syncManager, signalListener }); diff --git a/src/sdkFactory/types.ts b/src/sdkFactory/types.ts index dcd6c0d2..32c26954 100644 --- a/src/sdkFactory/types.ts +++ b/src/sdkFactory/types.ts @@ -56,7 +56,8 @@ export interface ISdkFactoryParams { // It Allows to distinguish SDK clients with the client-side API (`ICsSDK`) or server-side API (`ISDK` or `IAsyncSDK`). sdkClientMethodFactory: (params: ISdkClientFactoryParams) => ({ (): SplitIO.ICsClient; (key: SplitIO.SplitKey, trafficType?: string | undefined): SplitIO.ICsClient; } | (() => SplitIO.IClient) | (() => SplitIO.IAsyncClient)) - // Signal listener constructor. Used to handle special app states, like shutdown, app paused or resumed. + // Optional signal listener constructor. Used to handle special app states, like shutdown, app paused or resumed. + // Pass only if `syncManager` (used by Node listener) and `splitApi` (used by Browser listener) are passed. SignalListener?: new ( handler: (() => MaybeThenable) | undefined, // Used by NodeSignalListener settings: ISettings, // Used by BrowserSignalListener From 493bbc12678835272c4641980543a3d64dbf8633 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 4 May 2021 13:20:03 -0300 Subject: [PATCH 029/144] separete updaters from sync tasks --- .../polling/syncTasks/mySegmentsSyncTask.ts | 94 +--------- .../polling/syncTasks/segmentsSyncTask.ts | 102 +--------- src/sync/polling/syncTasks/splitsSyncTask.ts | 175 +----------------- .../__tests__/splitChangesUpdater.spec.ts | 2 +- .../polling/updaters/mySegmentsUpdater.ts | 92 +++++++++ .../polling/updaters/segmentChangesUpdater.ts | 102 ++++++++++ .../polling/updaters/splitChangesUpdater.ts | 173 +++++++++++++++++ 7 files changed, 379 insertions(+), 361 deletions(-) rename src/sync/polling/{syncTasks => updaters}/__tests__/splitChangesUpdater.spec.ts (98%) create mode 100644 src/sync/polling/updaters/mySegmentsUpdater.ts create mode 100644 src/sync/polling/updaters/segmentChangesUpdater.ts create mode 100644 src/sync/polling/updaters/splitChangesUpdater.ts diff --git a/src/sync/polling/syncTasks/mySegmentsSyncTask.ts b/src/sync/polling/syncTasks/mySegmentsSyncTask.ts index 6b19166d..6547c3c6 100644 --- a/src/sync/polling/syncTasks/mySegmentsSyncTask.ts +++ b/src/sync/polling/syncTasks/mySegmentsSyncTask.ts @@ -1,101 +1,15 @@ -import { IMySegmentsFetcher } from '../fetchers/types'; -import { ISegmentsCacheSync, ISplitsCacheSync, IStorageSync } from '../../../storages/types'; -import { IReadinessManager, ISegmentsEventEmitter } from '../../../readiness/types'; -import { SplitError } from '../../../utils/lang/errors'; -import timeout from '../../../utils/promise/timeout'; +import { IStorageSync } from '../../../storages/types'; +import { IReadinessManager } from '../../../readiness/types'; import syncTaskFactory from '../../syncTask'; import { ISegmentsSyncTask } from '../types'; import { IFetchMySegments } from '../../../services/types'; import mySegmentsFetcherFactory from '../fetchers/mySegmentsFetcher'; import { ISettings } from '../../../types'; -import { SDK_SEGMENTS_ARRIVED } from '../../../readiness/constants'; -import { ILogger } from '../../../logger/types'; -import { SYNC_MYSEGMENTS_FETCH_RETRY } from '../../../logger/constants'; - -type IMySegmentsUpdater = (segmentList?: string[], noCache?: boolean) => Promise +import { mySegmentsUpdaterFactory } from '../updaters/mySegmentsUpdater'; /** - * factory of MySegments updater (a.k.a, SegmentsSyncTask), a task that: - * - fetches mySegments using `mySegmentsFetcher` - * - updates `mySegmentsCache` - * - uses `segmentsEventEmitter` to emit events related to segments data updates + * Creates a sync task that periodically executes a `mySegmentsUpdater` task */ -function mySegmentsUpdaterFactory( - log: ILogger, - mySegmentsFetcher: IMySegmentsFetcher, - splitsCache: ISplitsCacheSync, - mySegmentsCache: ISegmentsCacheSync, - segmentsEventEmitter: ISegmentsEventEmitter, - requestTimeoutBeforeReady: number, - retriesOnFailureBeforeReady: number, -): IMySegmentsUpdater { - - let readyOnAlreadyExistentState = true; - let startingUp = true; - - /** timeout and telemetry decorator for `splitChangesFetcher` promise */ - function _promiseDecorator(promise: Promise) { - if (startingUp) promise = timeout(requestTimeoutBeforeReady, promise); - return promise; - - // @TODO telemetry - // NOTE: We only collect metrics on startup. - // mySegmentsPromise = tracker.start(tracker.TaskNames.MY_SEGMENTS_FETCH, startingUp ? metricCollectors : false, mySegmentsPromise); - } - - // @TODO if allowing custom storages, handle async execution and wrap errors as SplitErrors to distinguish from user callback errors - function updateSegments(segments: string[]) { - // Update the list of segment names available - const shouldNotifyUpdate = mySegmentsCache.resetSegments(segments); - - // Notify update if required - if (splitsCache.usesSegments() && (shouldNotifyUpdate || readyOnAlreadyExistentState)) { - readyOnAlreadyExistentState = false; - segmentsEventEmitter.emit(SDK_SEGMENTS_ARRIVED); - } - } - - function _mySegmentsUpdater(retry: number, segmentList?: string[], noCache?: boolean): Promise { - const updaterPromise: Promise = segmentList ? - // If segmentList is provided, there is no need to fetch mySegments - new Promise((res) => { updateSegments(segmentList); res(true); }) : - // If not provided, fetch mySegments - mySegmentsFetcher(noCache, _promiseDecorator).then(segments => { - // Only when we have downloaded segments completely, we should not keep retrying anymore - startingUp = false; - - updateSegments(segments); - return true; - }); - - return updaterPromise.catch(error => { - // handle user callback errors - if (!(error instanceof SplitError)) setTimeout(() => { throw error; }, 0); - - if (startingUp && retriesOnFailureBeforeReady > retry) { - retry += 1; - log.warn(SYNC_MYSEGMENTS_FETCH_RETRY, [retry, error]); - return _mySegmentsUpdater(retry); // no need to forward `segmentList` and `noCache` params - } else { - startingUp = false; - } - - return false; - }); - } - - /** - * MySegments updater returns a promise that resolves with a `false` boolean value if it fails to fetch mySegments or synchronize them with the storage. - * - * @param {string[] | undefined} segmentList list of mySegments names to sync in the storage. If the list is `undefined`, it fetches them before syncing in the storage. - * @param {boolean | undefined} noCache true to revalidate data to fetch - */ - return function mySegmentsUpdater(segmentList?: string[], noCache?: boolean) { - return _mySegmentsUpdater(0, segmentList, noCache); - }; - -} - export default function mySegmentsSyncTaskFactory( fetchMySegments: IFetchMySegments, storage: IStorageSync, diff --git a/src/sync/polling/syncTasks/segmentsSyncTask.ts b/src/sync/polling/syncTasks/segmentsSyncTask.ts index 4d0d400b..69cda5a3 100644 --- a/src/sync/polling/syncTasks/segmentsSyncTask.ts +++ b/src/sync/polling/syncTasks/segmentsSyncTask.ts @@ -1,111 +1,15 @@ -import { ISegmentChangesFetcher } from '../fetchers/types'; -import { ISegmentsCacheSync, IStorageSync } from '../../../storages/types'; +import { IStorageSync } from '../../../storages/types'; import { IReadinessManager } from '../../../readiness/types'; -import { ISegmentChangesResponse } from '../../../dtos/types'; -import { findIndex } from '../../../utils/lang'; -import { SplitError } from '../../../utils/lang/errors'; import syncTaskFactory from '../../syncTask'; import { ISegmentsSyncTask } from '../types'; import segmentChangesFetcherFactory from '../fetchers/segmentChangesFetcher'; import { IFetchSegmentChanges } from '../../../services/types'; import { ISettings } from '../../../types'; -import { SDK_SEGMENTS_ARRIVED } from '../../../readiness/constants'; -import { ILogger } from '../../../logger/types'; -import { logPrefixInstantiation, logPrefixSyncSegments } from '../../../logger/constants'; - -type ISegmentChangesUpdater = (segmentNames?: string[], noCache?: boolean, fetchOnlyNew?: boolean) => Promise +import { segmentChangesUpdaterFactory } from '../updaters/segmentChangesUpdater'; /** - * factory of SegmentChanges updater (a.k.a, SegmentsSyncTask), a task that: - * - fetches segment changes using `segmentChangesFetcher` - * - updates `segmentsCache` - * - uses `segmentsEventEmitter` to emit events related to segments data updates + * Creates a sync task that periodically executes a `segmentChangesUpdater` task */ -function segmentChangesUpdaterFactory( - log: ILogger, - segmentChangesFetcher: ISegmentChangesFetcher, - segmentsCache: ISegmentsCacheSync, - readiness: IReadinessManager, -): ISegmentChangesUpdater { - - let readyOnAlreadyExistentState = true; - - /** telemetry decorator for `segmentChangesFetcher` promise */ - function _promiseDecorator(promise: Promise) { - return promise; - // @TODO handle telemetry? - // const collectMetrics = startingUp || isNode; // If we are on the browser, only collect this metric for first fetch. On node do it always. - // splitsPromise = tracker.start(tracker.TaskNames.SPLITS_FETCH, collectMetrics ? metricCollectors : false, splitsPromise); - } - - /** - * Segments updater returns a promise that resolves with a `false` boolean value if it fails at least to fetch a segment or synchronize it with the storage. - * Thus, a false result doesn't imply that SDK_SEGMENTS_ARRIVED was not emitted. - * - * @param {string[] | undefined} segmentNames list of segment names to fetch. By passing `undefined` it fetches the list of segments registered at the storage - * @param {boolean | undefined} noCache true to revalidate data to fetch on a SEGMENT_UPDATE notifications. - * @param {boolean | undefined} fetchOnlyNew if true, only fetch the segments that not exists, i.e., which `changeNumber` is equal to -1. - * This param is used by SplitUpdateWorker on server-side SDK, to fetch new registered segments on SPLIT_UPDATE notifications. - */ - return function segmentChangesUpdater(segmentNames?: string[], noCache?: boolean, fetchOnlyNew?: boolean) { - log.debug(logPrefixSyncSegments + 'Started segments update'); - - // If not a segment name provided, read list of available segments names to be updated. - let segments = segmentNames ? segmentNames : segmentsCache.getRegisteredSegments(); - if (fetchOnlyNew) segments = segments.filter(segmentName => segmentsCache.getChangeNumber(segmentName) === -1); - - // Async fetchers are collected here. - const updaters: Promise[] = []; - - for (let index = 0; index < segments.length; index++) { - const segmentName = segments[index]; - const since = segmentsCache.getChangeNumber(segmentName); - - log.debug(logPrefixSyncSegments + `Processing segment ${segmentName}`); - - updaters.push(segmentChangesFetcher(since, segmentName, noCache, _promiseDecorator).then(function (changes) { - let changeNumber = -1; - changes.forEach(x => { - if (x.added.length > 0) segmentsCache.addToSegment(segmentName, x.added); - if (x.removed.length > 0) segmentsCache.removeFromSegment(segmentName, x.removed); - if (x.added.length > 0 || x.removed.length > 0) { - segmentsCache.setChangeNumber(segmentName, x.till); - changeNumber = x.till; - } - - log.debug(logPrefixSyncSegments + `Processed ${segmentName} with till = ${x.till}. Added: ${x.added.length}. Removed: ${x.removed.length}`); - }); - - return changeNumber; - })); - - } - - return Promise.all(updaters).then(shouldUpdateFlags => { - // if at least one segment fetch successes, mark segments ready - if (findIndex(shouldUpdateFlags, v => v !== -1) !== -1 || readyOnAlreadyExistentState) { - readyOnAlreadyExistentState = false; - readiness.segments.emit(SDK_SEGMENTS_ARRIVED); - } - // if at least one segment fetch fails, return false to indicate that there was some error (e.g., invalid json, HTTP error, etc) - if (shouldUpdateFlags.indexOf(-1) !== -1) return false; - return true; - }).catch(error => { - // handle user callback errors - if (!(error instanceof SplitError)) setTimeout(() => { throw error; }, 0); - - if (error.statusCode === 403) { - // @TODO although factory status is destroyed, synchronization is not stopped - readiness.destroy(); - log.error(logPrefixInstantiation + ': you passed a client-side type authorizationKey, please grab an Api Key from the Split web console that is of type Server-side.'); - } - - return false; - }); - }; - -} - export default function segmentsSyncTaskFactory( fetchSegmentChanges: IFetchSegmentChanges, storage: IStorageSync, diff --git a/src/sync/polling/syncTasks/splitsSyncTask.ts b/src/sync/polling/syncTasks/splitsSyncTask.ts index 557e8c0f..04544c02 100644 --- a/src/sync/polling/syncTasks/splitsSyncTask.ts +++ b/src/sync/polling/syncTasks/splitsSyncTask.ts @@ -1,182 +1,15 @@ -import { SplitError } from '../../../utils/lang/errors'; -import { _Set, setToArray, ISet } from '../../../utils/lang/sets'; -import { ISegmentsCacheSync, ISplitsCacheBase, IStorageSync } from '../../../storages/types'; -import { ISplitChangesFetcher } from '../fetchers/types'; -import { ISplit, ISplitChangesResponse } from '../../../dtos/types'; -import { IReadinessManager, ISplitsEventEmitter } from '../../../readiness/types'; -import timeout from '../../../utils/promise/timeout'; +import { IStorageSync } from '../../../storages/types'; +import { IReadinessManager } from '../../../readiness/types'; import syncTaskFactory from '../../syncTask'; import { ISplitsSyncTask } from '../types'; import splitChangesFetcherFactory from '../fetchers/splitChangesFetcher'; import { IFetchSplitChanges } from '../../../services/types'; -import thenable from '../../../utils/promise/thenable'; import { ISettings } from '../../../types'; -import { SDK_SPLITS_ARRIVED, SDK_SPLITS_CACHE_LOADED } from '../../../readiness/constants'; -import { ILogger } from '../../../logger/types'; -import { SYNC_SPLITS_FETCH, SYNC_SPLITS_NEW, SYNC_SPLITS_REMOVED, SYNC_SPLITS_SEGMENTS, SYNC_SPLITS_FETCH_FAILS, SYNC_SPLITS_FETCH_RETRY } from '../../../logger/constants'; - -type ISplitChangesUpdater = (noCache?: boolean) => Promise - -// Checks that all registered segments have been fetched (changeNumber !== -1). -// @TODO review together with Segments and MySegments storage APIs -function checkAllSegmentsExist(segmentsStorage: ISegmentsCacheSync) { - return segmentsStorage.getRegisteredSegments().every(segmentName => segmentsStorage.getChangeNumber(segmentName) !== -1); -} +import { splitChangesUpdaterFactory } from '../updaters/splitChangesUpdater'; /** - * Collect segments from a raw split definition. - * Exported for testing purposes. + * Creates a sync task that periodically executes a `splitChangesUpdater` task */ -export function parseSegments({ conditions }: ISplit): ISet { - - let segments = new _Set(); - - for (let i = 0; i < conditions.length; i++) { - const matchers = conditions[i].matcherGroup.matchers; - - matchers.forEach(matcher => { - if (matcher.matcherType === 'IN_SEGMENT') segments.add(matcher.userDefinedSegmentMatcherData.segmentName); - }); - } - - return segments; -} - -interface ISplitMutations { - added: [string, string][], - removed: string[], - segments: string[] -} - -/** - * Given the list of splits from /splitChanges endpoint, it returns the mutations, - * i.e., an object with added splits, removed splits and used segments. - * Exported for testing purposes. - */ -export function computeSplitsMutation(entries: ISplit[]): ISplitMutations { - const segments = new _Set(); - const computed = entries.reduce((accum, split) => { - if (split.status === 'ACTIVE') { - accum.added.push([split.name, JSON.stringify(split)]); - - parseSegments(split).forEach((segmentName: string) => { - segments.add(segmentName); - }); - } else { - accum.removed.push(split.name); - } - - return accum; - }, { added: [], removed: [], segments: [] } as ISplitMutations); - - computed.segments = setToArray(segments); - - return computed; -} - -/** - * factory of SplitChanges updater (a.k.a, SplitsSyncTask), a task that: - * - fetches split changes using `splitChangesFetcher` - * - updates `splitsCache` - * - uses `splitsEventEmitter` to emit events related to split data updates - * Exported for testing purposes. - */ -export function splitChangesUpdaterFactory( - log: ILogger, - splitChangesFetcher: ISplitChangesFetcher, - splitsCache: ISplitsCacheBase, - segmentsCache: ISegmentsCacheSync, - splitsEventEmitter: ISplitsEventEmitter, - requestTimeoutBeforeReady: number, - retriesOnFailureBeforeReady: number, -): ISplitChangesUpdater { - - let startingUp = true; - let readyOnAlreadyExistentState = true; - - /** timeout and telemetry decorator for `splitChangesFetcher` promise */ - function _promiseDecorator(promise: Promise) { - if (startingUp && requestTimeoutBeforeReady) promise = timeout(requestTimeoutBeforeReady, promise); - return promise; - - // @TODO telemetry - // const collectMetrics = startingUp || isNode; // If we are on the browser, only collect this metric for first fetch. On node do it always. - // splitsPromise = tracker.start(tracker.TaskNames.SPLITS_FETCH, collectMetrics ? metricCollectors : false, splitsPromise); - } - - /** - * SplitChanges updater returns a promise that resolves with a `false` boolean value if it fails to fetch splits or synchronize them with the storage. - * - * @param {boolean | undefined} noCache true to revalidate data to fetch - */ - return function splitChangesUpdater(noCache?: boolean) { - - /** - * @param {number} since current changeNumber at splitsCache - * @param {number} retry current number of retry attemps - */ - function _splitChangesUpdater(since: number, retry = 0): Promise { - log.debug(SYNC_SPLITS_FETCH, [since]); - - const fetcherPromise = splitChangesFetcher(since, noCache, _promiseDecorator) - .then((splitChanges: ISplitChangesResponse) => { - startingUp = false; - - const mutation = computeSplitsMutation(splitChanges.splits); - - log.debug(SYNC_SPLITS_NEW, [mutation.added.length]); - log.debug(SYNC_SPLITS_REMOVED, [mutation.removed.length]); - log.debug(SYNC_SPLITS_SEGMENTS, [mutation.segments.length]); - - // Write into storage - // @TODO call `setChangeNumber` only if the other storage operations have succeeded, in order to keep storage consistency - return Promise.all([ - // calling first `setChangenumber` method, to perform cache flush if split filter queryString changed - splitsCache.setChangeNumber(splitChanges.till), - splitsCache.addSplits(mutation.added), - splitsCache.removeSplits(mutation.removed), - segmentsCache.registerSegments(mutation.segments) - ]).then(() => { - // For server-side SDK, we must check that all registered segments have been fetched - if (readyOnAlreadyExistentState || (since !== splitChanges.till && checkAllSegmentsExist(segmentsCache))) { - readyOnAlreadyExistentState = false; - splitsEventEmitter.emit(SDK_SPLITS_ARRIVED); - } - return true; - }); - }) - .catch(error => { - // handle user callback errors - if (!(error instanceof SplitError)) { - setTimeout(() => { throw error; }, 0); - startingUp = false; // Stop retrying. - } - - log.warn(SYNC_SPLITS_FETCH_FAILS, [error]); - - if (startingUp && retriesOnFailureBeforeReady > retry) { - retry += 1; - log.info(SYNC_SPLITS_FETCH_RETRY, [retry, error]); - return _splitChangesUpdater(since, retry); - } else { - startingUp = false; - } - return false; - }); - - // After triggering the requests, if we have cached splits information let's notify that. - if (startingUp && splitsCache.checkCache()) splitsEventEmitter.emit(SDK_SPLITS_CACHE_LOADED); - - return fetcherPromise; - } - - const since = splitsCache.getChangeNumber(); - // Adding an extra promise to keep the fetch call asynchronous - const sincePromise: Promise = thenable(since) ? since : Promise.resolve(since); - return sincePromise.then(_splitChangesUpdater); - }; -} - export default function splitsSyncTaskFactory( fetchSplitChanges: IFetchSplitChanges, storage: IStorageSync, diff --git a/src/sync/polling/syncTasks/__tests__/splitChangesUpdater.spec.ts b/src/sync/polling/updaters/__tests__/splitChangesUpdater.spec.ts similarity index 98% rename from src/sync/polling/syncTasks/__tests__/splitChangesUpdater.spec.ts rename to src/sync/polling/updaters/__tests__/splitChangesUpdater.spec.ts index 9fb8ab89..cbf2e5ac 100644 --- a/src/sync/polling/syncTasks/__tests__/splitChangesUpdater.spec.ts +++ b/src/sync/polling/updaters/__tests__/splitChangesUpdater.spec.ts @@ -4,7 +4,7 @@ import { splitApiFactory } from '../../../../services/splitApi'; import SegmentsCacheInMemory from '../../../../storages/inMemory/SegmentsCacheInMemory'; import SplitsCacheInMemory from '../../../../storages/inMemory/SplitsCacheInMemory'; import splitChangesFetcherFactory from '../../fetchers/splitChangesFetcher'; -import { splitChangesUpdaterFactory, parseSegments, computeSplitsMutation } from '../splitsSyncTask'; +import { splitChangesUpdaterFactory, parseSegments, computeSplitsMutation } from '../splitChangesUpdater'; import splitChangesMock1 from '../../../../__tests__/mocks/splitchanges.since.-1.json'; import fetchMock from '../../../../__tests__/testUtils/fetchMock'; import { settingsSplitApi } from '../../../../utils/settingsValidation/__tests__/settings.mocks'; diff --git a/src/sync/polling/updaters/mySegmentsUpdater.ts b/src/sync/polling/updaters/mySegmentsUpdater.ts new file mode 100644 index 00000000..dae0f660 --- /dev/null +++ b/src/sync/polling/updaters/mySegmentsUpdater.ts @@ -0,0 +1,92 @@ +import { IMySegmentsFetcher } from '../fetchers/types'; +import { ISegmentsCacheSync, ISplitsCacheSync } from '../../../storages/types'; +import { ISegmentsEventEmitter } from '../../../readiness/types'; +import { SplitError } from '../../../utils/lang/errors'; +import timeout from '../../../utils/promise/timeout'; +import { SDK_SEGMENTS_ARRIVED } from '../../../readiness/constants'; +import { ILogger } from '../../../logger/types'; +import { SYNC_MYSEGMENTS_FETCH_RETRY } from '../../../logger/constants'; + +type IMySegmentsUpdater = (segmentList?: string[], noCache?: boolean) => Promise + +/** + * factory of MySegments updater, a task that: + * - fetches mySegments using `mySegmentsFetcher` + * - updates `mySegmentsCache` + * - uses `segmentsEventEmitter` to emit events related to segments data updates + */ +export function mySegmentsUpdaterFactory( + log: ILogger, + mySegmentsFetcher: IMySegmentsFetcher, + splitsCache: ISplitsCacheSync, + mySegmentsCache: ISegmentsCacheSync, + segmentsEventEmitter: ISegmentsEventEmitter, + requestTimeoutBeforeReady: number, + retriesOnFailureBeforeReady: number, +): IMySegmentsUpdater { + + let readyOnAlreadyExistentState = true; + let startingUp = true; + + /** timeout and telemetry decorator for `splitChangesFetcher` promise */ + function _promiseDecorator(promise: Promise) { + if (startingUp) promise = timeout(requestTimeoutBeforeReady, promise); + return promise; + + // @TODO telemetry + // NOTE: We only collect metrics on startup. + // mySegmentsPromise = tracker.start(tracker.TaskNames.MY_SEGMENTS_FETCH, startingUp ? metricCollectors : false, mySegmentsPromise); + } + + // @TODO if allowing custom storages, handle async execution and wrap errors as SplitErrors to distinguish from user callback errors + function updateSegments(segments: string[]) { + // Update the list of segment names available + const shouldNotifyUpdate = mySegmentsCache.resetSegments(segments); + + // Notify update if required + if (splitsCache.usesSegments() && (shouldNotifyUpdate || readyOnAlreadyExistentState)) { + readyOnAlreadyExistentState = false; + segmentsEventEmitter.emit(SDK_SEGMENTS_ARRIVED); + } + } + + function _mySegmentsUpdater(retry: number, segmentList?: string[], noCache?: boolean): Promise { + const updaterPromise: Promise = segmentList ? + // If segmentList is provided, there is no need to fetch mySegments + new Promise((res) => { updateSegments(segmentList); res(true); }) : + // If not provided, fetch mySegments + mySegmentsFetcher(noCache, _promiseDecorator).then(segments => { + // Only when we have downloaded segments completely, we should not keep retrying anymore + startingUp = false; + + updateSegments(segments); + return true; + }); + + return updaterPromise.catch(error => { + // handle user callback errors + if (!(error instanceof SplitError)) setTimeout(() => { throw error; }, 0); + + if (startingUp && retriesOnFailureBeforeReady > retry) { + retry += 1; + log.warn(SYNC_MYSEGMENTS_FETCH_RETRY, [retry, error]); + return _mySegmentsUpdater(retry); // no need to forward `segmentList` and `noCache` params + } else { + startingUp = false; + } + + return false; + }); + } + + /** + * MySegments updater returns a promise that resolves with a `false` boolean value if it fails to fetch mySegments or synchronize them with the storage. + * + * @param {string[] | undefined} segmentList list of mySegments names to sync in the storage. If the list is `undefined`, it fetches them before syncing in the storage. + * @param {boolean | undefined} noCache true to revalidate data to fetch + */ + return function mySegmentsUpdater(segmentList?: string[], noCache?: boolean) { + return _mySegmentsUpdater(0, segmentList, noCache); + }; + +} diff --git a/src/sync/polling/updaters/segmentChangesUpdater.ts b/src/sync/polling/updaters/segmentChangesUpdater.ts new file mode 100644 index 00000000..b33ee7e3 --- /dev/null +++ b/src/sync/polling/updaters/segmentChangesUpdater.ts @@ -0,0 +1,102 @@ +import { ISegmentChangesFetcher } from '../fetchers/types'; +import { ISegmentsCacheSync } from '../../../storages/types'; +import { IReadinessManager } from '../../../readiness/types'; +import { ISegmentChangesResponse } from '../../../dtos/types'; +import { findIndex } from '../../../utils/lang'; +import { SplitError } from '../../../utils/lang/errors'; +import { SDK_SEGMENTS_ARRIVED } from '../../../readiness/constants'; +import { ILogger } from '../../../logger/types'; +import { logPrefixInstantiation, logPrefixSyncSegments } from '../../../logger/constants'; + +type ISegmentChangesUpdater = (segmentNames?: string[], noCache?: boolean, fetchOnlyNew?: boolean) => Promise + +/** + * Factory of SegmentChanges updater, a task that: + * - fetches segment changes using `segmentChangesFetcher` + * - updates `segmentsCache` + * - uses `segmentsEventEmitter` to emit events related to segments data updates + */ +export function segmentChangesUpdaterFactory( + log: ILogger, + segmentChangesFetcher: ISegmentChangesFetcher, + segmentsCache: ISegmentsCacheSync, + readiness: IReadinessManager, +): ISegmentChangesUpdater { + + let readyOnAlreadyExistentState = true; + + /** telemetry decorator for `segmentChangesFetcher` promise */ + function _promiseDecorator(promise: Promise) { + return promise; + // @TODO handle telemetry? + // const collectMetrics = startingUp || isNode; // If we are on the browser, only collect this metric for first fetch. On node do it always. + // splitsPromise = tracker.start(tracker.TaskNames.SPLITS_FETCH, collectMetrics ? metricCollectors : false, splitsPromise); + } + + /** + * Segments updater returns a promise that resolves with a `false` boolean value if it fails at least to fetch a segment or synchronize it with the storage. + * Thus, a false result doesn't imply that SDK_SEGMENTS_ARRIVED was not emitted. + * + * @param {string[] | undefined} segmentNames list of segment names to fetch. By passing `undefined` it fetches the list of segments registered at the storage + * @param {boolean | undefined} noCache true to revalidate data to fetch on a SEGMENT_UPDATE notifications. + * @param {boolean | undefined} fetchOnlyNew if true, only fetch the segments that not exists, i.e., which `changeNumber` is equal to -1. + * This param is used by SplitUpdateWorker on server-side SDK, to fetch new registered segments on SPLIT_UPDATE notifications. + */ + return function segmentChangesUpdater(segmentNames?: string[], noCache?: boolean, fetchOnlyNew?: boolean) { + log.debug(logPrefixSyncSegments + 'Started segments update'); + + // If not a segment name provided, read list of available segments names to be updated. + let segments = segmentNames ? segmentNames : segmentsCache.getRegisteredSegments(); + if (fetchOnlyNew) segments = segments.filter(segmentName => segmentsCache.getChangeNumber(segmentName) === -1); + + // Async fetchers are collected here. + const updaters: Promise[] = []; + + for (let index = 0; index < segments.length; index++) { + const segmentName = segments[index]; + const since = segmentsCache.getChangeNumber(segmentName); + + log.debug(logPrefixSyncSegments + `Processing segment ${segmentName}`); + + updaters.push(segmentChangesFetcher(since, segmentName, noCache, _promiseDecorator).then(function (changes) { + let changeNumber = -1; + changes.forEach(x => { + if (x.added.length > 0) segmentsCache.addToSegment(segmentName, x.added); + if (x.removed.length > 0) segmentsCache.removeFromSegment(segmentName, x.removed); + if (x.added.length > 0 || x.removed.length > 0) { + segmentsCache.setChangeNumber(segmentName, x.till); + changeNumber = x.till; + } + + log.debug(logPrefixSyncSegments + `Processed ${segmentName} with till = ${x.till}. Added: ${x.added.length}. Removed: ${x.removed.length}`); + }); + + return changeNumber; + })); + + } + + return Promise.all(updaters).then(shouldUpdateFlags => { + // if at least one segment fetch successes, mark segments ready + if (findIndex(shouldUpdateFlags, v => v !== -1) !== -1 || readyOnAlreadyExistentState) { + readyOnAlreadyExistentState = false; + readiness.segments.emit(SDK_SEGMENTS_ARRIVED); + } + // if at least one segment fetch fails, return false to indicate that there was some error (e.g., invalid json, HTTP error, etc) + if (shouldUpdateFlags.indexOf(-1) !== -1) return false; + return true; + }).catch(error => { + // handle user callback errors + if (!(error instanceof SplitError)) setTimeout(() => { throw error; }, 0); + + if (error.statusCode === 403) { + // @TODO although factory status is destroyed, synchronization is not stopped + readiness.destroy(); + log.error(logPrefixInstantiation + ': you passed a client-side type authorizationKey, please grab an Api Key from the Split web console that is of type Server-side.'); + } + + return false; + }); + }; + +} diff --git a/src/sync/polling/updaters/splitChangesUpdater.ts b/src/sync/polling/updaters/splitChangesUpdater.ts new file mode 100644 index 00000000..63a89ef9 --- /dev/null +++ b/src/sync/polling/updaters/splitChangesUpdater.ts @@ -0,0 +1,173 @@ +import { SplitError } from '../../../utils/lang/errors'; +import { _Set, setToArray, ISet } from '../../../utils/lang/sets'; +import { ISegmentsCacheSync, ISplitsCacheBase } from '../../../storages/types'; +import { ISplitChangesFetcher } from '../fetchers/types'; +import { ISplit, ISplitChangesResponse } from '../../../dtos/types'; +import { ISplitsEventEmitter } from '../../../readiness/types'; +import timeout from '../../../utils/promise/timeout'; +import thenable from '../../../utils/promise/thenable'; +import { SDK_SPLITS_ARRIVED, SDK_SPLITS_CACHE_LOADED } from '../../../readiness/constants'; +import { ILogger } from '../../../logger/types'; +import { SYNC_SPLITS_FETCH, SYNC_SPLITS_NEW, SYNC_SPLITS_REMOVED, SYNC_SPLITS_SEGMENTS, SYNC_SPLITS_FETCH_FAILS, SYNC_SPLITS_FETCH_RETRY } from '../../../logger/constants'; + +type ISplitChangesUpdater = (noCache?: boolean) => Promise + +// Checks that all registered segments have been fetched (changeNumber !== -1). +// @TODO review together with Segments and MySegments storage APIs +function checkAllSegmentsExist(segmentsStorage: ISegmentsCacheSync) { + return segmentsStorage.getRegisteredSegments().every(segmentName => segmentsStorage.getChangeNumber(segmentName) !== -1); +} + +/** + * Collect segments from a raw split definition. + * Exported for testing purposes. + */ +export function parseSegments({ conditions }: ISplit): ISet { + + let segments = new _Set(); + + for (let i = 0; i < conditions.length; i++) { + const matchers = conditions[i].matcherGroup.matchers; + + matchers.forEach(matcher => { + if (matcher.matcherType === 'IN_SEGMENT') segments.add(matcher.userDefinedSegmentMatcherData.segmentName); + }); + } + + return segments; +} + +interface ISplitMutations { + added: [string, string][], + removed: string[], + segments: string[] +} + +/** + * Given the list of splits from /splitChanges endpoint, it returns the mutations, + * i.e., an object with added splits, removed splits and used segments. + * Exported for testing purposes. + */ +export function computeSplitsMutation(entries: ISplit[]): ISplitMutations { + const segments = new _Set(); + const computed = entries.reduce((accum, split) => { + if (split.status === 'ACTIVE') { + accum.added.push([split.name, JSON.stringify(split)]); + + parseSegments(split).forEach((segmentName: string) => { + segments.add(segmentName); + }); + } else { + accum.removed.push(split.name); + } + + return accum; + }, { added: [], removed: [], segments: [] } as ISplitMutations); + + computed.segments = setToArray(segments); + + return computed; +} + +/** + * factory of SplitChanges updater, a task that: + * - fetches split changes using `splitChangesFetcher` + * - updates `splitsCache` + * - uses `splitsEventEmitter` to emit events related to split data updates + * Exported for testing purposes. + */ +export function splitChangesUpdaterFactory( + log: ILogger, + splitChangesFetcher: ISplitChangesFetcher, + splitsCache: ISplitsCacheBase, + segmentsCache: ISegmentsCacheSync, + splitsEventEmitter: ISplitsEventEmitter, + requestTimeoutBeforeReady: number, + retriesOnFailureBeforeReady: number, +): ISplitChangesUpdater { + + let startingUp = true; + let readyOnAlreadyExistentState = true; + + /** timeout and telemetry decorator for `splitChangesFetcher` promise */ + function _promiseDecorator(promise: Promise) { + if (startingUp && requestTimeoutBeforeReady) promise = timeout(requestTimeoutBeforeReady, promise); + return promise; + + // @TODO telemetry + // const collectMetrics = startingUp || isNode; // If we are on the browser, only collect this metric for first fetch. On node do it always. + // splitsPromise = tracker.start(tracker.TaskNames.SPLITS_FETCH, collectMetrics ? metricCollectors : false, splitsPromise); + } + + /** + * SplitChanges updater returns a promise that resolves with a `false` boolean value if it fails to fetch splits or synchronize them with the storage. + * + * @param {boolean | undefined} noCache true to revalidate data to fetch + */ + return function splitChangesUpdater(noCache?: boolean) { + + /** + * @param {number} since current changeNumber at splitsCache + * @param {number} retry current number of retry attemps + */ + function _splitChangesUpdater(since: number, retry = 0): Promise { + log.debug(SYNC_SPLITS_FETCH, [since]); + + const fetcherPromise = splitChangesFetcher(since, noCache, _promiseDecorator) + .then((splitChanges: ISplitChangesResponse) => { + startingUp = false; + + const mutation = computeSplitsMutation(splitChanges.splits); + + log.debug(SYNC_SPLITS_NEW, [mutation.added.length]); + log.debug(SYNC_SPLITS_REMOVED, [mutation.removed.length]); + log.debug(SYNC_SPLITS_SEGMENTS, [mutation.segments.length]); + + // Write into storage + // @TODO call `setChangeNumber` only if the other storage operations have succeeded, in order to keep storage consistency + return Promise.all([ + // calling first `setChangenumber` method, to perform cache flush if split filter queryString changed + splitsCache.setChangeNumber(splitChanges.till), + splitsCache.addSplits(mutation.added), + splitsCache.removeSplits(mutation.removed), + segmentsCache.registerSegments(mutation.segments) + ]).then(() => { + // For server-side SDK, we must check that all registered segments have been fetched + if (readyOnAlreadyExistentState || (since !== splitChanges.till && checkAllSegmentsExist(segmentsCache))) { + readyOnAlreadyExistentState = false; + splitsEventEmitter.emit(SDK_SPLITS_ARRIVED); + } + return true; + }); + }) + .catch(error => { + // handle user callback errors + if (!(error instanceof SplitError)) { + setTimeout(() => { throw error; }, 0); + startingUp = false; // Stop retrying. + } + + log.warn(SYNC_SPLITS_FETCH_FAILS, [error]); + + if (startingUp && retriesOnFailureBeforeReady > retry) { + retry += 1; + log.info(SYNC_SPLITS_FETCH_RETRY, [retry, error]); + return _splitChangesUpdater(since, retry); + } else { + startingUp = false; + } + return false; + }); + + // After triggering the requests, if we have cached splits information let's notify that. + if (startingUp && splitsCache.checkCache()) splitsEventEmitter.emit(SDK_SPLITS_CACHE_LOADED); + + return fetcherPromise; + } + + const since = splitsCache.getChangeNumber(); + // Adding an extra promise to keep the fetch call asynchronous + const sincePromise: Promise = thenable(since) ? since : Promise.resolve(since); + return sincePromise.then(_splitChangesUpdater); + }; +} From 7ef9b78de25d5eba5383bfd39db442f39654f6fe Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 4 May 2021 14:22:42 -0300 Subject: [PATCH 030/144] refactor on updaters to support async splits and segments storages --- src/readiness/readinessManager.ts | 1 + .../polling/updaters/segmentChangesUpdater.ts | 95 ++++++++++--------- .../polling/updaters/splitChangesUpdater.ts | 49 ++++++---- 3 files changed, 81 insertions(+), 64 deletions(-) diff --git a/src/readiness/readinessManager.ts b/src/readiness/readinessManager.ts index 91ab79ff..226fd3cd 100644 --- a/src/readiness/readinessManager.ts +++ b/src/readiness/readinessManager.ts @@ -62,6 +62,7 @@ export function readinessManagerFactory( let isDestroyed = false; function checkIsReadyFromCache() { + // @TODO add condition to emit SDK_READY_FROM_CACHE only if SDK_READY has not been emitted if (!isReadyFromCache && splits.splitsCacheLoaded) { isReadyFromCache = true; gate.emit(SDK_READY_FROM_CACHE); diff --git a/src/sync/polling/updaters/segmentChangesUpdater.ts b/src/sync/polling/updaters/segmentChangesUpdater.ts index b33ee7e3..04eecfd7 100644 --- a/src/sync/polling/updaters/segmentChangesUpdater.ts +++ b/src/sync/polling/updaters/segmentChangesUpdater.ts @@ -1,5 +1,5 @@ import { ISegmentChangesFetcher } from '../fetchers/types'; -import { ISegmentsCacheSync } from '../../../storages/types'; +import { ISegmentsCacheBase } from '../../../storages/types'; import { IReadinessManager } from '../../../readiness/types'; import { ISegmentChangesResponse } from '../../../dtos/types'; import { findIndex } from '../../../utils/lang'; @@ -15,12 +15,17 @@ type ISegmentChangesUpdater = (segmentNames?: string[], noCache?: boolean, fetch * - fetches segment changes using `segmentChangesFetcher` * - updates `segmentsCache` * - uses `segmentsEventEmitter` to emit events related to segments data updates + * + * @param log logger instance + * @param segmentChangesFetcher fetcher of `/segmentChanges` + * @param segmentsCache segments storage, with sync or async methods + * @param readiness optional readiness manager. Not required for synchronizer or producer mode. */ export function segmentChangesUpdaterFactory( log: ILogger, segmentChangesFetcher: ISegmentChangesFetcher, - segmentsCache: ISegmentsCacheSync, - readiness: IReadinessManager, + segmentsCache: ISegmentsCacheBase, + readiness?: IReadinessManager, ): ISegmentChangesUpdater { let readyOnAlreadyExistentState = true; @@ -46,56 +51,58 @@ export function segmentChangesUpdaterFactory( log.debug(logPrefixSyncSegments + 'Started segments update'); // If not a segment name provided, read list of available segments names to be updated. - let segments = segmentNames ? segmentNames : segmentsCache.getRegisteredSegments(); - if (fetchOnlyNew) segments = segments.filter(segmentName => segmentsCache.getChangeNumber(segmentName) === -1); + let segmentsPromise = Promise.resolve(segmentNames ? segmentNames : segmentsCache.getRegisteredSegments()); - // Async fetchers are collected here. - const updaters: Promise[] = []; + return segmentsPromise.then(segments => { + if (fetchOnlyNew) segments = segments.filter(segmentName => segmentsCache.getChangeNumber(segmentName) === -1); - for (let index = 0; index < segments.length; index++) { - const segmentName = segments[index]; - const since = segmentsCache.getChangeNumber(segmentName); + // Async fetchers are collected here. + const updaters: Promise[] = []; - log.debug(logPrefixSyncSegments + `Processing segment ${segmentName}`); + for (let index = 0; index < segments.length; index++) { + const segmentName = segments[index]; + log.debug(logPrefixSyncSegments + `Processing segment ${segmentName}`); + let sincePromise = Promise.resolve(segmentsCache.getChangeNumber(segmentName)); - updaters.push(segmentChangesFetcher(since, segmentName, noCache, _promiseDecorator).then(function (changes) { - let changeNumber = -1; - changes.forEach(x => { - if (x.added.length > 0) segmentsCache.addToSegment(segmentName, x.added); - if (x.removed.length > 0) segmentsCache.removeFromSegment(segmentName, x.removed); - if (x.added.length > 0 || x.removed.length > 0) { - segmentsCache.setChangeNumber(segmentName, x.till); - changeNumber = x.till; - } + updaters.push(sincePromise.then(since => segmentChangesFetcher(since, segmentName, noCache, _promiseDecorator).then(function (changes) { + let changeNumber = -1; + changes.forEach(x => { + if (x.added.length > 0) segmentsCache.addToSegment(segmentName, x.added); + if (x.removed.length > 0) segmentsCache.removeFromSegment(segmentName, x.removed); + if (x.added.length > 0 || x.removed.length > 0) { + segmentsCache.setChangeNumber(segmentName, x.till); + changeNumber = x.till; + } - log.debug(logPrefixSyncSegments + `Processed ${segmentName} with till = ${x.till}. Added: ${x.added.length}. Removed: ${x.removed.length}`); - }); + log.debug(logPrefixSyncSegments + `Processed ${segmentName} with till = ${x.till}. Added: ${x.added.length}. Removed: ${x.removed.length}`); + }); - return changeNumber; - })); + return changeNumber; + }))); - } - - return Promise.all(updaters).then(shouldUpdateFlags => { - // if at least one segment fetch successes, mark segments ready - if (findIndex(shouldUpdateFlags, v => v !== -1) !== -1 || readyOnAlreadyExistentState) { - readyOnAlreadyExistentState = false; - readiness.segments.emit(SDK_SEGMENTS_ARRIVED); - } - // if at least one segment fetch fails, return false to indicate that there was some error (e.g., invalid json, HTTP error, etc) - if (shouldUpdateFlags.indexOf(-1) !== -1) return false; - return true; - }).catch(error => { - // handle user callback errors - if (!(error instanceof SplitError)) setTimeout(() => { throw error; }, 0); - - if (error.statusCode === 403) { - // @TODO although factory status is destroyed, synchronization is not stopped - readiness.destroy(); - log.error(logPrefixInstantiation + ': you passed a client-side type authorizationKey, please grab an Api Key from the Split web console that is of type Server-side.'); } - return false; + return Promise.all(updaters).then(shouldUpdateFlags => { + // if at least one segment fetch successes, mark segments ready + if (findIndex(shouldUpdateFlags, v => v !== -1) !== -1 || readyOnAlreadyExistentState) { + readyOnAlreadyExistentState = false; + if (readiness) readiness.segments.emit(SDK_SEGMENTS_ARRIVED); + } + // if at least one segment fetch fails, return false to indicate that there was some error (e.g., invalid json, HTTP error, etc) + if (shouldUpdateFlags.indexOf(-1) !== -1) return false; + return true; + }).catch(error => { + // handle user callback errors + if (!(error instanceof SplitError)) setTimeout(() => { throw error; }, 0); + + if (error.statusCode === 403) { + // @TODO although factory status is destroyed, synchronization is not stopped + if (readiness) readiness.destroy(); + log.error(logPrefixInstantiation + ': you passed a client-side type authorizationKey, please grab an Api Key from the Split web console that is of type Server-side.'); + } + + return false; + }); }); }; diff --git a/src/sync/polling/updaters/splitChangesUpdater.ts b/src/sync/polling/updaters/splitChangesUpdater.ts index 63a89ef9..8d3ded13 100644 --- a/src/sync/polling/updaters/splitChangesUpdater.ts +++ b/src/sync/polling/updaters/splitChangesUpdater.ts @@ -1,21 +1,24 @@ import { SplitError } from '../../../utils/lang/errors'; import { _Set, setToArray, ISet } from '../../../utils/lang/sets'; -import { ISegmentsCacheSync, ISplitsCacheBase } from '../../../storages/types'; +import { ISegmentsCacheBase, ISplitsCacheBase } from '../../../storages/types'; import { ISplitChangesFetcher } from '../fetchers/types'; import { ISplit, ISplitChangesResponse } from '../../../dtos/types'; import { ISplitsEventEmitter } from '../../../readiness/types'; import timeout from '../../../utils/promise/timeout'; -import thenable from '../../../utils/promise/thenable'; import { SDK_SPLITS_ARRIVED, SDK_SPLITS_CACHE_LOADED } from '../../../readiness/constants'; import { ILogger } from '../../../logger/types'; import { SYNC_SPLITS_FETCH, SYNC_SPLITS_NEW, SYNC_SPLITS_REMOVED, SYNC_SPLITS_SEGMENTS, SYNC_SPLITS_FETCH_FAILS, SYNC_SPLITS_FETCH_RETRY } from '../../../logger/constants'; type ISplitChangesUpdater = (noCache?: boolean) => Promise -// Checks that all registered segments have been fetched (changeNumber !== -1). +// Checks that all registered segments have been fetched (changeNumber !== -1 for every segment). // @TODO review together with Segments and MySegments storage APIs -function checkAllSegmentsExist(segmentsStorage: ISegmentsCacheSync) { - return segmentsStorage.getRegisteredSegments().every(segmentName => segmentsStorage.getChangeNumber(segmentName) !== -1); +function checkAllSegmentsExist(segmentsStorage: ISegmentsCacheBase): Promise { + let registeredSegments = Promise.resolve(segmentsStorage.getRegisteredSegments()); + return registeredSegments.then(segments => { + return Promise.all(segments.map(segmentName => segmentsStorage.getChangeNumber(segmentName))) + .then(changeNumbers => changeNumbers.every(changeNumber => changeNumber !== -1)); + }); } /** @@ -74,20 +77,26 @@ export function computeSplitsMutation(entries: ISplit[]): ISplitMutations { * - fetches split changes using `splitChangesFetcher` * - updates `splitsCache` * - uses `splitsEventEmitter` to emit events related to split data updates - * Exported for testing purposes. + * + * @param log Logger instance + * @param splitChangesFetcher Fetcher of `/splitChanges` + * @param splitsCache Splits storage, with sync or async methods + * @param segmentsCache Segments storage, with sync or async methods + * @param splitsEventEmitter Optional readiness manager. Not required for synchronizer or producer mode. + * @param requestTimeoutBeforeReady How long the updater will wait for the request to timeout. Default 0, i.e., never timeout. + * @param retriesOnFailureBeforeReady How many retries on `/splitChanges` we the updater do in case of failure or timeout. Default 0, i.e., no retries. */ export function splitChangesUpdaterFactory( log: ILogger, splitChangesFetcher: ISplitChangesFetcher, splitsCache: ISplitsCacheBase, - segmentsCache: ISegmentsCacheSync, - splitsEventEmitter: ISplitsEventEmitter, - requestTimeoutBeforeReady: number, - retriesOnFailureBeforeReady: number, + segmentsCache: ISegmentsCacheBase, + splitsEventEmitter?: ISplitsEventEmitter, + requestTimeoutBeforeReady: number = 0, + retriesOnFailureBeforeReady: number = 0, ): ISplitChangesUpdater { let startingUp = true; - let readyOnAlreadyExistentState = true; /** timeout and telemetry decorator for `splitChangesFetcher` promise */ function _promiseDecorator(promise: Promise) { @@ -132,10 +141,11 @@ export function splitChangesUpdaterFactory( splitsCache.removeSplits(mutation.removed), segmentsCache.registerSegments(mutation.segments) ]).then(() => { - // For server-side SDK, we must check that all registered segments have been fetched - if (readyOnAlreadyExistentState || (since !== splitChanges.till && checkAllSegmentsExist(segmentsCache))) { - readyOnAlreadyExistentState = false; - splitsEventEmitter.emit(SDK_SPLITS_ARRIVED); + + if (splitsEventEmitter) { + // To emit SDK_SPLITS_ARRIVED for server-side SDK, we must check that all registered segments have been fetched + Promise.resolve(!splitsEventEmitter.splitsArrived || (since !== splitChanges.till && checkAllSegmentsExist(segmentsCache))) + .then(emitSplitsArrivedEvent => { if (emitSplitsArrivedEvent) splitsEventEmitter.emit(SDK_SPLITS_ARRIVED); }); } return true; }); @@ -160,14 +170,13 @@ export function splitChangesUpdaterFactory( }); // After triggering the requests, if we have cached splits information let's notify that. - if (startingUp && splitsCache.checkCache()) splitsEventEmitter.emit(SDK_SPLITS_CACHE_LOADED); - + if (splitsEventEmitter && startingUp) { + Promise.resolve(splitsCache.checkCache()).then(cacheReady => { if (cacheReady) splitsEventEmitter.emit(SDK_SPLITS_CACHE_LOADED); }); + } return fetcherPromise; } - const since = splitsCache.getChangeNumber(); - // Adding an extra promise to keep the fetch call asynchronous - const sincePromise: Promise = thenable(since) ? since : Promise.resolve(since); + let sincePromise = Promise.resolve(splitsCache.getChangeNumber()); return sincePromise.then(_splitChangesUpdater); }; } From d4343fa9669262816e336cb1474e03549ebdcec0 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 4 May 2021 16:41:41 -0300 Subject: [PATCH 031/144] updated index --- src/index.ts | 21 ++------------------- 1 file changed, 2 insertions(+), 19 deletions(-) diff --git a/src/index.ts b/src/index.ts index 7abf494f..c6141767 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,22 +3,5 @@ export { InMemoryStorageFactory } from './storages/inMemory/InMemoryStorage'; // InMemory storage factory for standalone client-side SplitFactory export { InMemoryStorageCSFactory } from './storages/inMemory/InMemoryStorageCS'; -// InMemory caches, reused to implement new storages -export { default as SplitsCacheInMemory } from './storages/inMemory/SplitsCacheInMemory'; -export { default as MySegmentsCacheInMemory } from './storages/inMemory/MySegmentsCacheInMemory'; -export { default as SegmentsCacheInMemory } from './storages/inMemory/SegmentsCacheInMemory'; -export { default as EventsCacheInMemory } from './storages/inMemory/EventsCacheInMemory'; -export { default as ImpressionsCacheInMemory } from './storages/inMemory/ImpressionsCacheInMemory'; -export { default as LatenciesCacheInMemory } from './storages/inMemory/LatenciesCacheInMemory'; -export { default as CountsCacheInMemory } from './storages/inMemory/CountsCacheInMemory'; - -// Utils for implementing new storages for standalone SplitFactory -export { default as AbstractSplitsCacheSync, usesSegments } from './storages/AbstractSplitsCacheSync'; -export { default as AbstractSegmentsCacheSync } from './storages/AbstractSegmentsCacheSync'; - -// Evaluator: -export { evaluateFeature, evaluateFeatures } from './evaluator'; - -// Utils: -export { hash128 } from './utils/murmur3/murmur3_128'; -export { hash } from './utils/murmur3/murmur3'; +// Pluggable storage factory for consumer/producer mode +export { PluggableStorage } from './storages/pluggable'; From 1e5cfe38d4de785febe5e591f939b379ae9b5aa0 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 4 May 2021 18:27:19 -0300 Subject: [PATCH 032/144] feedback --- src/listeners/node.ts | 6 +- src/logger/constants.ts | 36 ++++----- src/logger/messages/debug.ts | 74 +++++++++---------- src/logger/messages/error.ts | 16 ++-- src/logger/messages/info.ts | 36 ++++----- src/logger/messages/warn.ts | 30 ++++---- src/sync/polling/pollingManagerSS.ts | 6 +- .../polling/updaters/segmentChangesUpdater.ts | 10 +-- src/utils/inputValidation/apiKey.ts | 8 +- src/utils/settingsValidation/splitFilters.ts | 4 +- 10 files changed, 113 insertions(+), 113 deletions(-) diff --git a/src/listeners/node.ts b/src/listeners/node.ts index 18b01519..a2f9b11e 100644 --- a/src/listeners/node.ts +++ b/src/listeners/node.ts @@ -3,7 +3,7 @@ import { ISignalListener } from './types'; import thenable from '../utils/promise/thenable'; import { MaybeThenable } from '../dtos/types'; import { ISettings } from '../types'; -import { logPrefixCleanup, CLEANUP_REGISTERING, CLEANUP_DEREGISTERING } from '../logger/constants'; +import { LOG_PREFIX_CLEANUP, CLEANUP_REGISTERING, CLEANUP_DEREGISTERING } from '../logger/constants'; const SIGTERM = 'SIGTERM'; const EVENT_NAME = 'for SIGTERM signal.'; @@ -49,14 +49,14 @@ export default class NodeSignalListener implements ISignalListener { process.kill(process.pid, SIGTERM); }; - this.settings.log.debug(logPrefixCleanup + 'Split SDK graceful shutdown after SIGTERM.'); + this.settings.log.debug(`${LOG_PREFIX_CLEANUP}Split SDK graceful shutdown after SIGTERM.`); let handlerResult = null; try { handlerResult = this.handler(); } catch (err) { - this.settings.log.error(logPrefixCleanup + `Error with Split SDK graceful shutdown: ${err}`); + this.settings.log.error(`${LOG_PREFIX_CLEANUP}Error with Split SDK graceful shutdown: ${err}`); } if (thenable(handlerResult)) { diff --git a/src/logger/constants.ts b/src/logger/constants.ts index 46cac217..9a85d37e 100644 --- a/src/logger/constants.ts +++ b/src/logger/constants.ts @@ -119,21 +119,21 @@ export const ERROR_INVALID_IMPRESSIONS_MODE = 321; export const ERROR_HTTP = 322; // Log prefixes (a.k.a. tags or categories) -export const logPrefixSettings = 'settings'; -export const logPrefixInstantiation = 'Factory instantiation'; -export const logPrefixEngine = 'engine'; -export const logPrefixEngineCombiner = logPrefixEngine + ':combiner: '; -export const logPrefixEngineMatcher = logPrefixEngine + ':matcher: '; -export const logPrefixEngineValue = logPrefixEngine + ':value: '; -export const logPrefixSync = 'sync'; -export const logPrefixSyncManager = logPrefixSync + ':sync-manager: '; -export const logPrefixSyncOffline = logPrefixSync + ':offline: '; -export const logPrefixSyncStreaming = logPrefixSync + ':streaming: '; -export const logPrefixSyncSplits = logPrefixSync + ':split-changes: '; -export const logPrefixSyncSegments = logPrefixSync + ':segment-changes: '; -export const logPrefixSyncMysegments = logPrefixSync + ':my-segments: '; -export const logPrefixSyncPolling = logPrefixSync + ':polling-manager: '; -export const logPrefixSyncSubmitters = logPrefixSync + ':submitter: '; -export const logPrefixImpressionsTracker = 'impressions-tracker: '; -export const logPrefixEventsTracker = 'events-tracker: '; -export const logPrefixCleanup = 'cleanup: '; +export const LOG_PREFIX_SETTINGS = 'settings'; +export const LOG_PREFIX_INSTANTIATION = 'Factory instantiation'; +export const LOG_PREFIX_ENGINE = 'engine'; +export const LOG_PREFIX_ENGINE_COMBINER = LOG_PREFIX_ENGINE + ':combiner: '; +export const LOG_PREFIX_ENGINE_MATCHER = LOG_PREFIX_ENGINE + ':matcher: '; +export const LOG_PREFIX_ENGINE_VALUE = LOG_PREFIX_ENGINE + ':value: '; +export const LOG_PREFIX_SYNC = 'sync'; +export const LOG_PREFIX_SYNC_MANAGER = LOG_PREFIX_SYNC + ':sync-manager: '; +export const LOG_PREFIX_SYNC_OFFLINE = LOG_PREFIX_SYNC + ':offline: '; +export const LOG_PREFIX_SYNC_STREAMING = LOG_PREFIX_SYNC + ':streaming: '; +export const LOG_PREFIX_SYNC_SPLITS = LOG_PREFIX_SYNC + ':split-changes: '; +export const LOG_PREFIX_SYNC_SEGMENTS = LOG_PREFIX_SYNC + ':segment-changes: '; +export const LOG_PREFIX_SYNC_MYSEGMENTS = LOG_PREFIX_SYNC + ':my-segments: '; +export const LOG_PREFIX_SYNC_POLLING = LOG_PREFIX_SYNC + ':polling-manager: '; +export const LOG_PREFIX_SYNC_SUBMITTERS = LOG_PREFIX_SYNC + ':submitter: '; +export const LOG_PREFIX_IMPRESSIONS_TRACKER = 'impressions-tracker: '; +export const LOG_PREFIX_EVENTS_TRACKER = 'events-tracker: '; +export const LOG_PREFIX_CLEANUP = 'cleanup: '; diff --git a/src/logger/messages/debug.ts b/src/logger/messages/debug.ts index 4f1444bc..ed8281ed 100644 --- a/src/logger/messages/debug.ts +++ b/src/logger/messages/debug.ts @@ -3,47 +3,47 @@ import { codesInfo } from './info'; export const codesDebug: [number, string][] = codesInfo.concat([ // evaluator - [c.ENGINE_COMBINER_AND, c.logPrefixEngineCombiner + '[andCombiner] evaluates to %s'], - [c.ENGINE_COMBINER_IFELSEIF, c.logPrefixEngineCombiner + 'Treatment found: %s'], - [c.ENGINE_COMBINER_IFELSEIF_NO_TREATMENT, c.logPrefixEngineCombiner + 'All predicates evaluated, no treatment found.'], - [c.ENGINE_BUCKET, c.logPrefixEngine + ': using algo "murmur" bucket %s for key %s using seed %s - treatment %s'], - [c.ENGINE_MATCHER_ALL, c.logPrefixEngineMatcher + '[allMatcher] is always true'], - [c.ENGINE_MATCHER_BETWEEN, c.logPrefixEngineMatcher + '[betweenMatcher] is %s between %s and %s? %s'], - [c.ENGINE_MATCHER_BOOLEAN, c.logPrefixEngineMatcher + '[booleanMatcher] %s === %s'], - [c.ENGINE_MATCHER_CONTAINS_ALL, c.logPrefixEngineMatcher + '[containsAllMatcher] %s contains all elements of %s? %s'], - [c.ENGINE_MATCHER_CONTAINS_ANY, c.logPrefixEngineMatcher + '[containsAnyMatcher] %s contains at least an element of %s? %s'], - [c.ENGINE_MATCHER_CONTAINS_STRING, c.logPrefixEngineMatcher + '[containsStringMatcher] %s contains %s? %s'], - [c.ENGINE_MATCHER_DEPENDENCY, c.logPrefixEngineMatcher + '[dependencyMatcher] parent split "%s" evaluated to "%s" with label "%s". %s evaluated treatment is part of [%s] ? %s.'], - [c.ENGINE_MATCHER_DEPENDENCY_PRE, c.logPrefixEngineMatcher + '[dependencyMatcher] will evaluate parent split: "%s" with key: %s %s'], - [c.ENGINE_MATCHER_EQUAL, c.logPrefixEngineMatcher + '[equalToMatcher] is %s equal to %s? %s'], - [c.ENGINE_MATCHER_EQUAL_TO_SET, c.logPrefixEngineMatcher + '[equalToSetMatcher] is %s equal to set %s? %s'], - [c.ENGINE_MATCHER_ENDS_WITH, c.logPrefixEngineMatcher + '[endsWithMatcher] %s ends with %s? %s'], - [c.ENGINE_MATCHER_GREATER, c.logPrefixEngineMatcher + '[greaterThanEqualMatcher] is %s greater than %s? %s'], - [c.ENGINE_MATCHER_LESS, c.logPrefixEngineMatcher + '[lessThanEqualMatcher] is %s less than %s? %s'], - [c.ENGINE_MATCHER_PART_OF, c.logPrefixEngineMatcher + '[partOfMatcher] %s is part of %s? %s'], - [c.ENGINE_MATCHER_SEGMENT, c.logPrefixEngineMatcher + '[segmentMatcher] evaluated %s / %s => %s'], - [c.ENGINE_MATCHER_STRING, c.logPrefixEngineMatcher + '[stringMatcher] does %s matches with %s? %s'], - [c.ENGINE_MATCHER_STRING_INVALID, c.logPrefixEngineMatcher + '[stringMatcher] %s is an invalid regex'], - [c.ENGINE_MATCHER_STARTS_WITH, c.logPrefixEngineMatcher + '[startsWithMatcher] %s starts with %s? %s'], - [c.ENGINE_MATCHER_WHITELIST, c.logPrefixEngineMatcher + '[whitelistMatcher] evaluated %s in [%s] => %s'], - [c.ENGINE_VALUE, c.logPrefixEngineValue + 'Extracted attribute [%s], [%s] will be used for matching.'], - [c.ENGINE_SANITIZE, c.logPrefixEngine + ':sanitize: Attempted to sanitize [%s] which should be of type [%s]. Sanitized and processed value => [%s]'], + [c.ENGINE_COMBINER_AND, c.LOG_PREFIX_ENGINE_COMBINER + '[andCombiner] evaluates to %s'], + [c.ENGINE_COMBINER_IFELSEIF, c.LOG_PREFIX_ENGINE_COMBINER + 'Treatment found: %s'], + [c.ENGINE_COMBINER_IFELSEIF_NO_TREATMENT, c.LOG_PREFIX_ENGINE_COMBINER + 'All predicates evaluated, no treatment found.'], + [c.ENGINE_BUCKET, c.LOG_PREFIX_ENGINE + ': using algo "murmur" bucket %s for key %s using seed %s - treatment %s'], + [c.ENGINE_MATCHER_ALL, c.LOG_PREFIX_ENGINE_MATCHER + '[allMatcher] is always true'], + [c.ENGINE_MATCHER_BETWEEN, c.LOG_PREFIX_ENGINE_MATCHER + '[betweenMatcher] is %s between %s and %s? %s'], + [c.ENGINE_MATCHER_BOOLEAN, c.LOG_PREFIX_ENGINE_MATCHER + '[booleanMatcher] %s === %s'], + [c.ENGINE_MATCHER_CONTAINS_ALL, c.LOG_PREFIX_ENGINE_MATCHER + '[containsAllMatcher] %s contains all elements of %s? %s'], + [c.ENGINE_MATCHER_CONTAINS_ANY, c.LOG_PREFIX_ENGINE_MATCHER + '[containsAnyMatcher] %s contains at least an element of %s? %s'], + [c.ENGINE_MATCHER_CONTAINS_STRING, c.LOG_PREFIX_ENGINE_MATCHER + '[containsStringMatcher] %s contains %s? %s'], + [c.ENGINE_MATCHER_DEPENDENCY, c.LOG_PREFIX_ENGINE_MATCHER + '[dependencyMatcher] parent split "%s" evaluated to "%s" with label "%s". %s evaluated treatment is part of [%s] ? %s.'], + [c.ENGINE_MATCHER_DEPENDENCY_PRE, c.LOG_PREFIX_ENGINE_MATCHER + '[dependencyMatcher] will evaluate parent split: "%s" with key: %s %s'], + [c.ENGINE_MATCHER_EQUAL, c.LOG_PREFIX_ENGINE_MATCHER + '[equalToMatcher] is %s equal to %s? %s'], + [c.ENGINE_MATCHER_EQUAL_TO_SET, c.LOG_PREFIX_ENGINE_MATCHER + '[equalToSetMatcher] is %s equal to set %s? %s'], + [c.ENGINE_MATCHER_ENDS_WITH, c.LOG_PREFIX_ENGINE_MATCHER + '[endsWithMatcher] %s ends with %s? %s'], + [c.ENGINE_MATCHER_GREATER, c.LOG_PREFIX_ENGINE_MATCHER + '[greaterThanEqualMatcher] is %s greater than %s? %s'], + [c.ENGINE_MATCHER_LESS, c.LOG_PREFIX_ENGINE_MATCHER + '[lessThanEqualMatcher] is %s less than %s? %s'], + [c.ENGINE_MATCHER_PART_OF, c.LOG_PREFIX_ENGINE_MATCHER + '[partOfMatcher] %s is part of %s? %s'], + [c.ENGINE_MATCHER_SEGMENT, c.LOG_PREFIX_ENGINE_MATCHER + '[segmentMatcher] evaluated %s / %s => %s'], + [c.ENGINE_MATCHER_STRING, c.LOG_PREFIX_ENGINE_MATCHER + '[stringMatcher] does %s matches with %s? %s'], + [c.ENGINE_MATCHER_STRING_INVALID, c.LOG_PREFIX_ENGINE_MATCHER + '[stringMatcher] %s is an invalid regex'], + [c.ENGINE_MATCHER_STARTS_WITH, c.LOG_PREFIX_ENGINE_MATCHER + '[startsWithMatcher] %s starts with %s? %s'], + [c.ENGINE_MATCHER_WHITELIST, c.LOG_PREFIX_ENGINE_MATCHER + '[whitelistMatcher] evaluated %s in [%s] => %s'], + [c.ENGINE_VALUE, c.LOG_PREFIX_ENGINE_VALUE + 'Extracted attribute [%s], [%s] will be used for matching.'], + [c.ENGINE_SANITIZE, c.LOG_PREFIX_ENGINE + ':sanitize: Attempted to sanitize [%s] which should be of type [%s]. Sanitized and processed value => [%s]'], // SDK - [c.CLEANUP_REGISTERING, c.logPrefixCleanup + 'Registering cleanup handler %s'], - [c.CLEANUP_DEREGISTERING, c.logPrefixCleanup + 'Deregistering cleanup handler %s'], + [c.CLEANUP_REGISTERING, c.LOG_PREFIX_CLEANUP + 'Registering cleanup handler %s'], + [c.CLEANUP_DEREGISTERING, c.LOG_PREFIX_CLEANUP + 'Deregistering cleanup handler %s'], [c.RETRIEVE_CLIENT_DEFAULT, ' Retrieving default SDK client.'], [c.RETRIEVE_CLIENT_EXISTING, ' Retrieving existing SDK client.'], [c.RETRIEVE_MANAGER, ' Retrieving manager instance.'], // synchronizer - [c.SYNC_OFFLINE_DATA, c.logPrefixSyncOffline + 'Splits data: \n%s'], - [c.SYNC_SPLITS_FETCH, c.logPrefixSyncSplits + 'Spin up split update using since = %s'], - [c.SYNC_SPLITS_NEW, c.logPrefixSyncSplits + 'New splits %s'], - [c.SYNC_SPLITS_REMOVED, c.logPrefixSyncSplits + 'Removed splits %s'], - [c.SYNC_SPLITS_SEGMENTS, c.logPrefixSyncSplits + 'Segment names collected %s'], - [c.STREAMING_NEW_MESSAGE, c.logPrefixSyncStreaming + 'New SSE message received, with data: %s.'], - [c.SYNC_TASK_START, c.logPrefixSync + ': Starting %s. Running each %s millis'], - [c.SYNC_TASK_EXECUTE, c.logPrefixSync + ': Running %s'], - [c.SYNC_TASK_STOP, c.logPrefixSync + ': Stopping %s'], + [c.SYNC_OFFLINE_DATA, c.LOG_PREFIX_SYNC_OFFLINE + 'Splits data: \n%s'], + [c.SYNC_SPLITS_FETCH, c.LOG_PREFIX_SYNC_SPLITS + 'Spin up split update using since = %s'], + [c.SYNC_SPLITS_NEW, c.LOG_PREFIX_SYNC_SPLITS + 'New splits %s'], + [c.SYNC_SPLITS_REMOVED, c.LOG_PREFIX_SYNC_SPLITS + 'Removed splits %s'], + [c.SYNC_SPLITS_SEGMENTS, c.LOG_PREFIX_SYNC_SPLITS + 'Segment names collected %s'], + [c.STREAMING_NEW_MESSAGE, c.LOG_PREFIX_SYNC_STREAMING + 'New SSE message received, with data: %s.'], + [c.SYNC_TASK_START, c.LOG_PREFIX_SYNC + ': Starting %s. Running each %s millis'], + [c.SYNC_TASK_EXECUTE, c.LOG_PREFIX_SYNC + ': Running %s'], + [c.SYNC_TASK_STOP, c.LOG_PREFIX_SYNC + ': Stopping %s'], // initialization / settings validation - [c.SETTINGS_SPLITS_FILTER, c.logPrefixSettings + ': splits filtering criteria is "%s".'] + [c.SETTINGS_SPLITS_FILTER, c.LOG_PREFIX_SETTINGS + ': splits filtering criteria is "%s".'] ]); diff --git a/src/logger/messages/error.ts b/src/logger/messages/error.ts index 0eea8c02..cd51e80c 100644 --- a/src/logger/messages/error.ts +++ b/src/logger/messages/error.ts @@ -2,17 +2,17 @@ import * as c from '../constants'; export const codesError: [number, string][] = [ // evaluator - [c.ERROR_ENGINE_COMBINER_IFELSEIF, c.logPrefixEngineCombiner + 'Invalid Split, no valid rules found'], + [c.ERROR_ENGINE_COMBINER_IFELSEIF, c.LOG_PREFIX_ENGINE_COMBINER + 'Invalid Split, no valid rules found'], // SDK [c.ERROR_LOGLEVEL_INVALID, 'logger: Invalid Log Level - No changes to the logs will be applied.'], [c.ERROR_CLIENT_CANNOT_GET_READY, ' The SDK will not get ready. Reason: %s'], - [c.ERROR_IMPRESSIONS_TRACKER, c.logPrefixImpressionsTracker + 'Could not store impressions bulk with %s impression(s). Error: %s'], - [c.ERROR_IMPRESSIONS_LISTENER, c.logPrefixImpressionsTracker + 'Impression listener logImpression method threw: %s.'], - [c.ERROR_EVENTS_TRACKER, c.logPrefixEventsTracker + 'Failed to queue %s'], + [c.ERROR_IMPRESSIONS_TRACKER, c.LOG_PREFIX_IMPRESSIONS_TRACKER + 'Could not store impressions bulk with %s impression(s). Error: %s'], + [c.ERROR_IMPRESSIONS_LISTENER, c.LOG_PREFIX_IMPRESSIONS_TRACKER + 'Impression listener logImpression method threw: %s.'], + [c.ERROR_EVENTS_TRACKER, c.LOG_PREFIX_EVENTS_TRACKER + 'Failed to queue %s'], // synchronizer - [c.ERROR_SYNC_OFFLINE_LOADING, c.logPrefixSyncOffline + 'There was an issue loading the mock Splits data, no changes will be applied to the current cache. %s'], - [c.ERROR_STREAMING_SSE, c.logPrefixSyncStreaming + 'Fail to connect to streaming, with error message: %s'], - [c.ERROR_STREAMING_AUTH, c.logPrefixSyncStreaming + 'Failed to authenticate for streaming. Error: "%s".'], + [c.ERROR_SYNC_OFFLINE_LOADING, c.LOG_PREFIX_SYNC_OFFLINE + 'There was an issue loading the mock Splits data, no changes will be applied to the current cache. %s'], + [c.ERROR_STREAMING_SSE, c.LOG_PREFIX_SYNC_STREAMING + 'Fail to connect to streaming, with error message: %s'], + [c.ERROR_STREAMING_AUTH, c.LOG_PREFIX_SYNC_STREAMING + 'Failed to authenticate for streaming. Error: "%s".'], [c.ERROR_HTTP, ' Response status is not OK. Status: %s. URL: %s. Message: %s'], // client status [c.ERROR_CLIENT_LISTENER, 'A listener was added for %s on the SDK, which has already fired and won\'t be emitted again. The callback won\'t be executed.'], @@ -29,5 +29,5 @@ export const codesError: [number, string][] = [ [c.ERROR_EMPTY, '%s: you passed an empty %s. It must be a non-empty string.'], [c.ERROR_EMPTY_ARRAY, '%s: %s must be a non-empty array.'], // initialization / settings validation - [c.ERROR_INVALID_IMPRESSIONS_MODE, c.logPrefixSettings + ': you passed an invalid "impressionsMode". It should be one of the following values: %s. Defaulting to "%s" mode.'], + [c.ERROR_INVALID_IMPRESSIONS_MODE, c.LOG_PREFIX_SETTINGS + ': you passed an invalid "impressionsMode". It should be one of the following values: %s. Defaulting to "%s" mode.'], ]; diff --git a/src/logger/messages/info.ts b/src/logger/messages/info.ts index 2877a9d8..8dc0909e 100644 --- a/src/logger/messages/info.ts +++ b/src/logger/messages/info.ts @@ -8,26 +8,26 @@ export const codesInfo: [number, string][] = codesWarn.concat([ [c.CLIENT_READY_FROM_CACHE, READY_MSG + ' from cache'], [c.CLIENT_READY, READY_MSG], // SDK - [c.IMPRESSION, c.logPrefixImpressionsTracker +'Split: %s. Key: %s. Evaluation: %s. Label: %s'], - [c.IMPRESSION_QUEUEING, c.logPrefixImpressionsTracker +'Queueing corresponding impression.'], + [c.IMPRESSION, c.LOG_PREFIX_IMPRESSIONS_TRACKER +'Split: %s. Key: %s. Evaluation: %s. Label: %s'], + [c.IMPRESSION_QUEUEING, c.LOG_PREFIX_IMPRESSIONS_TRACKER +'Queueing corresponding impression.'], [c.NEW_SHARED_CLIENT, ' New shared client instance created.'], [c.NEW_FACTORY, ' New Split SDK instance created.'], - [c.EVENTS_TRACKER_SUCCESS, c.logPrefixEventsTracker + 'Successfully qeued %s'], - [c.IMPRESSIONS_TRACKER_SUCCESS, c.logPrefixImpressionsTracker + 'Successfully stored %s impression(s).'], + [c.EVENTS_TRACKER_SUCCESS, c.LOG_PREFIX_EVENTS_TRACKER + 'Successfully qeued %s'], + [c.IMPRESSIONS_TRACKER_SUCCESS, c.LOG_PREFIX_IMPRESSIONS_TRACKER + 'Successfully stored %s impression(s).'], // synchronizer - [c.POLLING_SMART_PAUSING, c.logPrefixSyncPolling + 'Turning segments data polling %s.'], - [c.POLLING_START, c.logPrefixSyncPolling + 'Starting polling'], - [c.POLLING_STOP, c.logPrefixSyncPolling + 'Stopping polling'], - [c.SYNC_SPLITS_FETCH_RETRY, c.logPrefixSyncSplits + 'Retrying download of splits #%s. Reason: %s'], - [c.SUBMITTERS_PUSH_FULL_EVENTS_QUEUE, c.logPrefixSyncSubmitters + 'Flushing full events queue and reseting timer.'], - [c.SUBMITTERS_PUSH, c.logPrefixSyncSubmitters + 'Pushing %s %s.'], - [c.STREAMING_REFRESH_TOKEN, c.logPrefixSyncStreaming + 'Refreshing streaming token in %s seconds.'], - [c.STREAMING_RECONNECT, c.logPrefixSyncStreaming + 'Attempting to reconnect in %s seconds.'], - [c.STREAMING_CONNECTING, c.logPrefixSyncStreaming + 'Connecting to streaming.'], - [c.STREAMING_DISABLED, c.logPrefixSyncStreaming + 'Streaming is disabled for given Api key. Switching to polling mode.'], - [c.STREAMING_DISCONNECTING, c.logPrefixSyncStreaming + 'Disconnecting from streaming.'], - [c.SYNC_START_POLLING, c.logPrefixSyncManager + 'Streaming not available. Starting polling.'], - [c.SYNC_CONTINUE_POLLING, c.logPrefixSyncManager + 'Streaming couldn\'t connect. Continue polling.'], - [c.SYNC_STOP_POLLING, c.logPrefixSyncManager + 'Streaming (re)connected. Syncing and stopping polling.'], + [c.POLLING_SMART_PAUSING, c.LOG_PREFIX_SYNC_POLLING + 'Turning segments data polling %s.'], + [c.POLLING_START, c.LOG_PREFIX_SYNC_POLLING + 'Starting polling'], + [c.POLLING_STOP, c.LOG_PREFIX_SYNC_POLLING + 'Stopping polling'], + [c.SYNC_SPLITS_FETCH_RETRY, c.LOG_PREFIX_SYNC_SPLITS + 'Retrying download of splits #%s. Reason: %s'], + [c.SUBMITTERS_PUSH_FULL_EVENTS_QUEUE, c.LOG_PREFIX_SYNC_SUBMITTERS + 'Flushing full events queue and reseting timer.'], + [c.SUBMITTERS_PUSH, c.LOG_PREFIX_SYNC_SUBMITTERS + 'Pushing %s %s.'], + [c.STREAMING_REFRESH_TOKEN, c.LOG_PREFIX_SYNC_STREAMING + 'Refreshing streaming token in %s seconds.'], + [c.STREAMING_RECONNECT, c.LOG_PREFIX_SYNC_STREAMING + 'Attempting to reconnect in %s seconds.'], + [c.STREAMING_CONNECTING, c.LOG_PREFIX_SYNC_STREAMING + 'Connecting to streaming.'], + [c.STREAMING_DISABLED, c.LOG_PREFIX_SYNC_STREAMING + 'Streaming is disabled for given Api key. Switching to polling mode.'], + [c.STREAMING_DISCONNECTING, c.LOG_PREFIX_SYNC_STREAMING + 'Disconnecting from streaming.'], + [c.SYNC_START_POLLING, c.LOG_PREFIX_SYNC_MANAGER + 'Streaming not available. Starting polling.'], + [c.SYNC_CONTINUE_POLLING, c.LOG_PREFIX_SYNC_MANAGER + 'Streaming couldn\'t connect. Continue polling.'], + [c.SYNC_STOP_POLLING, c.LOG_PREFIX_SYNC_MANAGER + 'Streaming (re)connected. Syncing and stopping polling.'], ]); diff --git a/src/logger/messages/warn.ts b/src/logger/messages/warn.ts index 78c0c46c..ca1ff5c7 100644 --- a/src/logger/messages/warn.ts +++ b/src/logger/messages/warn.ts @@ -3,16 +3,16 @@ import { codesError } from './error'; export const codesWarn: [number, string][] = codesError.concat([ // evaluator - [c.ENGINE_VALUE_INVALID, c.logPrefixEngineValue + 'Value %s doesn\'t match with expected type.'], - [c.ENGINE_VALUE_NO_ATTRIBUTES, c.logPrefixEngineValue + 'Defined attribute [%s], no attributes received.'], + [c.ENGINE_VALUE_INVALID, c.LOG_PREFIX_ENGINE_VALUE + 'Value %s doesn\'t match with expected type.'], + [c.ENGINE_VALUE_NO_ATTRIBUTES, c.LOG_PREFIX_ENGINE_VALUE + 'Defined attribute [%s], no attributes received.'], // synchronizer - [c.SYNC_MYSEGMENTS_FETCH_RETRY, c.logPrefixSyncMysegments + 'Retrying download of segments #%s. Reason: %s'], - [c.SYNC_SPLITS_FETCH_FAILS, c.logPrefixSyncSplits + 'Error while doing fetch of Splits. %s'], - [c.STREAMING_PARSING_ERROR_FAILS, c.logPrefixSyncStreaming + 'Error parsing SSE error notification: %s'], - [c.STREAMING_PARSING_MESSAGE_FAILS, c.logPrefixSyncStreaming + 'Error parsing SSE message notification: %s'], - [c.STREAMING_FALLBACK, c.logPrefixSyncStreaming + 'Falling back to polling mode. Reason: %s'], - [c.SUBMITTERS_PUSH_FAILS, c.logPrefixSyncSubmitters + 'Droping %s %s after retry. Reason: %s.'], - [c.SUBMITTERS_PUSH_RETRY, c.logPrefixSyncSubmitters + 'Failed to push %s %s, keeping data to retry on next iteration. Reason: %s.'], + [c.SYNC_MYSEGMENTS_FETCH_RETRY, c.LOG_PREFIX_SYNC_MYSEGMENTS + 'Retrying download of segments #%s. Reason: %s'], + [c.SYNC_SPLITS_FETCH_FAILS, c.LOG_PREFIX_SYNC_SPLITS + 'Error while doing fetch of Splits. %s'], + [c.STREAMING_PARSING_ERROR_FAILS, c.LOG_PREFIX_SYNC_STREAMING + 'Error parsing SSE error notification: %s'], + [c.STREAMING_PARSING_MESSAGE_FAILS, c.LOG_PREFIX_SYNC_STREAMING + 'Error parsing SSE message notification: %s'], + [c.STREAMING_FALLBACK, c.LOG_PREFIX_SYNC_STREAMING + 'Falling back to polling mode. Reason: %s'], + [c.SUBMITTERS_PUSH_FAILS, c.LOG_PREFIX_SYNC_SUBMITTERS + 'Droping %s %s after retry. Reason: %s.'], + [c.SUBMITTERS_PUSH_RETRY, c.LOG_PREFIX_SYNC_SUBMITTERS + 'Failed to push %s %s, keeping data to retry on next iteration. Reason: %s.'], // client status [c.CLIENT_NOT_READY, '%s: the SDK is not ready, results may be incorrect. Make sure to wait for SDK readiness before using this method.'], [c.CLIENT_NO_LISTENER, 'No listeners for SDK Readiness detected. Incorrect control treatments could have been logged if you called getTreatment/s while the SDK was not yet ready.'], @@ -25,10 +25,10 @@ export const codesWarn: [number, string][] = codesError.concat([ [c.WARN_LOWERCASE_TRAFFIC_TYPE, '%s: traffic_type_name should be all lowercase - converting string to lowercase.'], [c.WARN_NOT_EXISTENT_TT, '%s: traffic type "%s" does not have any corresponding split in this environment, make sure you\'re tracking your events to a valid traffic type defined in the web console.'], // initialization / settings validation - [c.WARN_INTEGRATION_INVALID, c.logPrefixSettings+': %s integration item(s) at settings is invalid. %s'], - [c.WARN_SPLITS_FILTER_IGNORED, c.logPrefixSettings+': split filters have been configured but will have no effect if mode is not "%s", since synchronization is being deferred to an external tool.'], - [c.WARN_SPLITS_FILTER_INVALID, c.logPrefixSettings+': split filter at position %s is invalid. It must be an object with a valid filter type ("byName" or "byPrefix") and a list of "values".'], - [c.WARN_SPLITS_FILTER_EMPTY, c.logPrefixSettings+': splitFilters configuration must be a non-empty array of filter objects.'], - [c.WARN_STORAGE_INVALID, c.logPrefixSettings+': The provided storage is invalid. Fallbacking into default MEMORY storage'], - [c.WARN_API_KEY, c.logPrefixSettings+': You already have %s. We recommend keeping only one instance of the factory at all times (Singleton pattern) and reusing it throughout your application'] + [c.WARN_INTEGRATION_INVALID, c.LOG_PREFIX_SETTINGS+': %s integration item(s) at settings is invalid. %s'], + [c.WARN_SPLITS_FILTER_IGNORED, c.LOG_PREFIX_SETTINGS+': split filters have been configured but will have no effect if mode is not "%s", since synchronization is being deferred to an external tool.'], + [c.WARN_SPLITS_FILTER_INVALID, c.LOG_PREFIX_SETTINGS+': split filter at position %s is invalid. It must be an object with a valid filter type ("byName" or "byPrefix") and a list of "values".'], + [c.WARN_SPLITS_FILTER_EMPTY, c.LOG_PREFIX_SETTINGS+': splitFilters configuration must be a non-empty array of filter objects.'], + [c.WARN_STORAGE_INVALID, c.LOG_PREFIX_SETTINGS+': The provided storage is invalid. Fallbacking into default MEMORY storage'], + [c.WARN_API_KEY, c.LOG_PREFIX_SETTINGS+': You already have %s. We recommend keeping only one instance of the factory at all times (Singleton pattern) and reusing it throughout your application'] ]); diff --git a/src/sync/polling/pollingManagerSS.ts b/src/sync/polling/pollingManagerSS.ts index 24b27600..773da265 100644 --- a/src/sync/polling/pollingManagerSS.ts +++ b/src/sync/polling/pollingManagerSS.ts @@ -6,7 +6,7 @@ import { ISplitApi } from '../../services/types'; import { ISettings } from '../../types'; import { IPollingManager, ISegmentsSyncTask, ISplitsSyncTask } from './types'; import thenable from '../../utils/promise/thenable'; -import { POLLING_START, POLLING_STOP, logPrefixSyncPolling } from '../../logger/constants'; +import { POLLING_START, POLLING_STOP, LOG_PREFIX_SYNC_POLLING } from '../../logger/constants'; /** * Expose start / stop mechanism for pulling data from services. @@ -30,8 +30,8 @@ export default function pollingManagerSSFactory( // Start periodic fetching (polling) start() { log.info(POLLING_START); - log.debug(logPrefixSyncPolling + `Splits will be refreshed each ${settings.scheduler.featuresRefreshRate} millis`); - log.debug(logPrefixSyncPolling + `Segments will be refreshed each ${settings.scheduler.segmentsRefreshRate} millis`); + log.debug(LOG_PREFIX_SYNC_POLLING + `Splits will be refreshed each ${settings.scheduler.featuresRefreshRate} millis`); + log.debug(LOG_PREFIX_SYNC_POLLING + `Segments will be refreshed each ${settings.scheduler.segmentsRefreshRate} millis`); const startingUp = splitsSyncTask.start(); if (thenable(startingUp)) { diff --git a/src/sync/polling/updaters/segmentChangesUpdater.ts b/src/sync/polling/updaters/segmentChangesUpdater.ts index b33ee7e3..cbb1e4b1 100644 --- a/src/sync/polling/updaters/segmentChangesUpdater.ts +++ b/src/sync/polling/updaters/segmentChangesUpdater.ts @@ -6,7 +6,7 @@ import { findIndex } from '../../../utils/lang'; import { SplitError } from '../../../utils/lang/errors'; import { SDK_SEGMENTS_ARRIVED } from '../../../readiness/constants'; import { ILogger } from '../../../logger/types'; -import { logPrefixInstantiation, logPrefixSyncSegments } from '../../../logger/constants'; +import { LOG_PREFIX_INSTANTIATION, LOG_PREFIX_SYNC_SEGMENTS } from '../../../logger/constants'; type ISegmentChangesUpdater = (segmentNames?: string[], noCache?: boolean, fetchOnlyNew?: boolean) => Promise @@ -43,7 +43,7 @@ export function segmentChangesUpdaterFactory( * This param is used by SplitUpdateWorker on server-side SDK, to fetch new registered segments on SPLIT_UPDATE notifications. */ return function segmentChangesUpdater(segmentNames?: string[], noCache?: boolean, fetchOnlyNew?: boolean) { - log.debug(logPrefixSyncSegments + 'Started segments update'); + log.debug(`${LOG_PREFIX_SYNC_SEGMENTS}Started segments update`); // If not a segment name provided, read list of available segments names to be updated. let segments = segmentNames ? segmentNames : segmentsCache.getRegisteredSegments(); @@ -56,7 +56,7 @@ export function segmentChangesUpdaterFactory( const segmentName = segments[index]; const since = segmentsCache.getChangeNumber(segmentName); - log.debug(logPrefixSyncSegments + `Processing segment ${segmentName}`); + log.debug(`${LOG_PREFIX_SYNC_SEGMENTS}Processing segment ${segmentName}`); updaters.push(segmentChangesFetcher(since, segmentName, noCache, _promiseDecorator).then(function (changes) { let changeNumber = -1; @@ -68,7 +68,7 @@ export function segmentChangesUpdaterFactory( changeNumber = x.till; } - log.debug(logPrefixSyncSegments + `Processed ${segmentName} with till = ${x.till}. Added: ${x.added.length}. Removed: ${x.removed.length}`); + log.debug(`${LOG_PREFIX_SYNC_SEGMENTS}Processed ${segmentName} with till = ${x.till}. Added: ${x.added.length}. Removed: ${x.removed.length}`); }); return changeNumber; @@ -92,7 +92,7 @@ export function segmentChangesUpdaterFactory( if (error.statusCode === 403) { // @TODO although factory status is destroyed, synchronization is not stopped readiness.destroy(); - log.error(logPrefixInstantiation + ': you passed a client-side type authorizationKey, please grab an Api Key from the Split web console that is of type Server-side.'); + log.error(`${LOG_PREFIX_INSTANTIATION}: you passed a client-side type authorizationKey, please grab an Api Key from the Split web console that is of type Server-side.`); } return false; diff --git a/src/utils/inputValidation/apiKey.ts b/src/utils/inputValidation/apiKey.ts index f97860cf..86175189 100644 --- a/src/utils/inputValidation/apiKey.ts +++ b/src/utils/inputValidation/apiKey.ts @@ -1,4 +1,4 @@ -import { ERROR_NULL, ERROR_EMPTY, ERROR_INVALID, WARN_API_KEY, logPrefixInstantiation } from '../../logger/constants'; +import { ERROR_NULL, ERROR_EMPTY, ERROR_INVALID, WARN_API_KEY, LOG_PREFIX_INSTANTIATION } from '../../logger/constants'; import { ILogger } from '../../logger/types'; import { isString } from '../lang'; @@ -8,14 +8,14 @@ const item = 'api_key'; export function validateApiKey(log: ILogger, maybeApiKey: any): string | false { let apiKey: string | false = false; if (maybeApiKey == undefined) { // eslint-disable-line eqeqeq - log.error(ERROR_NULL, [logPrefixInstantiation, item]); + log.error(ERROR_NULL, [LOG_PREFIX_INSTANTIATION, item]); } else if (isString(maybeApiKey)) { if (maybeApiKey.length > 0) apiKey = maybeApiKey; else - log.error(ERROR_EMPTY, [logPrefixInstantiation, item]); + log.error(ERROR_EMPTY, [LOG_PREFIX_INSTANTIATION, item]); } else { - log.error(ERROR_INVALID, [logPrefixInstantiation, item]); + log.error(ERROR_INVALID, [LOG_PREFIX_INSTANTIATION, item]); } return apiKey; diff --git a/src/utils/settingsValidation/splitFilters.ts b/src/utils/settingsValidation/splitFilters.ts index 7c474952..8a7a1303 100644 --- a/src/utils/settingsValidation/splitFilters.ts +++ b/src/utils/settingsValidation/splitFilters.ts @@ -3,7 +3,7 @@ import { validateSplits } from '../inputValidation/splits'; import { ISplitFiltersValidation } from '../../dtos/types'; import { SplitIO } from '../../types'; import { ILogger } from '../../logger/types'; -import { WARN_SPLITS_FILTER_IGNORED, WARN_SPLITS_FILTER_EMPTY, WARN_SPLITS_FILTER_INVALID, SETTINGS_SPLITS_FILTER, logPrefixSettings } from '../../logger/constants'; +import { WARN_SPLITS_FILTER_IGNORED, WARN_SPLITS_FILTER_EMPTY, WARN_SPLITS_FILTER_INVALID, SETTINGS_SPLITS_FILTER, LOG_PREFIX_SETTINGS } from '../../logger/constants'; // Split filters metadata. // Ordered according to their precedency when forming the filter query string: `&names=&prefixes=` @@ -39,7 +39,7 @@ function validateFilterType(maybeFilterType: any): maybeFilterType is SplitIO.Sp */ function validateSplitFilter(log: ILogger, type: SplitIO.SplitFilterType, values: string[], maxLength: number) { // validate and remove invalid and duplicated values - let result = validateSplits(log, values, logPrefixSettings, `${type} filter`, `${type} filter value`); + let result = validateSplits(log, values, LOG_PREFIX_SETTINGS, `${type} filter`, `${type} filter value`); if (result) { // check max length From 7068ab85454c90872f673d407486b75bff370481 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 4 May 2021 18:45:14 -0300 Subject: [PATCH 033/144] feedback --- src/storages/inMemory/EventsCacheInMemory.ts | 7 ++++++- src/storages/inRedis/index.ts | 15 ++++++++------- src/storages/types.ts | 13 +++++++------ .../polling/updaters/segmentChangesUpdater.ts | 2 +- src/sync/polling/updaters/splitChangesUpdater.ts | 1 - 5 files changed, 22 insertions(+), 16 deletions(-) diff --git a/src/storages/inMemory/EventsCacheInMemory.ts b/src/storages/inMemory/EventsCacheInMemory.ts index 2b92419b..e60d00d6 100644 --- a/src/storages/inMemory/EventsCacheInMemory.ts +++ b/src/storages/inMemory/EventsCacheInMemory.ts @@ -10,7 +10,12 @@ export default class EventsCacheInMemory implements IEventsCacheSync { private queue: SplitIO.EventData[]; private queueByteSize: number; - constructor(eventsQueueSize: number) { + /** + * + * @param eventsQueueSize number of queued events to call onFullQueueCb. + * Default value is 0, that means no maximum value, in case we want to avoid this being triggered. + */ + constructor(eventsQueueSize: number = 0) { this.maxQueue = eventsQueueSize; this.queue = []; this.queueByteSize = 0; diff --git a/src/storages/inRedis/index.ts b/src/storages/inRedis/index.ts index 738d1e58..36cc33e8 100644 --- a/src/storages/inRedis/index.ts +++ b/src/storages/inRedis/index.ts @@ -22,17 +22,18 @@ export function InRedisStorage(options: InRedisStorageOptions = {}) { const prefix = options.prefix ? options.prefix + '.SPLITIO' : 'SPLITIO'; - return function InRedisStorageFactory({ log, readinessManager, metadata}: IStorageFactoryParams): IStorageAsync { + return function InRedisStorageFactory({ log, readinessManager, metadata }: IStorageFactoryParams): IStorageAsync { const keys = new KeyBuilderSS(prefix, metadata); const redisClient = new RedisAdapter(log, options.options || {}); - // subscription to Redis connect event in order to emit SDK_READY event - // @TODO pass a callback to simplify custom storages - redisClient.on('connect', () => { - readinessManager.splits.emit(SDK_SPLITS_ARRIVED); - readinessManager.segments.emit(SDK_SEGMENTS_ARRIVED); - }); + // subscription to Redis connect event in order to emit SDK_READY event on consumer mode + if (readinessManager) { + redisClient.on('connect', () => { + readinessManager.splits.emit(SDK_SPLITS_ARRIVED); + readinessManager.segments.emit(SDK_SEGMENTS_ARRIVED); + }); + } return { splits: new SplitsCacheInRedis(log, keys, redisClient), diff --git a/src/storages/types.ts b/src/storages/types.ts index 4d52d5fb..aceb23a0 100644 --- a/src/storages/types.ts +++ b/src/storages/types.ts @@ -1,4 +1,4 @@ -import { MaybeThenable, ISplitFiltersValidation, IMetadata } from '../dtos/types'; +import { MaybeThenable, IMetadata, ISplitFiltersValidation } from '../dtos/types'; import { ILogger } from '../logger/types'; import { IReadinessManager } from '../readiness/types'; import { SplitIO, ImpressionDTO } from '../types'; @@ -332,14 +332,15 @@ export type DataLoader = (storage: IStorageSync, matchingKey: string) => void export interface IStorageFactoryParams { log: ILogger, - eventsQueueSize: number, - optimize: boolean /* whether create the `impressionCounts` cache (OPTIMIZED impression mode) or not (DEBUG impression mode) */, + eventsQueueSize?: number, + optimize?: boolean /* whether create the `impressionCounts` cache (OPTIMIZED impression mode) or not (DEBUG impression mode) */, + dataLoader?: DataLoader, // ATM, only used by InLocalStorage matchingKey?: string, /* undefined on server-side SDKs */ - splitFiltersValidation: ISplitFiltersValidation, + splitFiltersValidation?: ISplitFiltersValidation, - // Used by InRedis and Pluggable Storage - readinessManager: IReadinessManager, + // ATM, only used by InRedisStorage. @TODO pass a callback to simplify custom storages. + readinessManager?: IReadinessManager, metadata: IMetadata, } diff --git a/src/sync/polling/updaters/segmentChangesUpdater.ts b/src/sync/polling/updaters/segmentChangesUpdater.ts index 83b9007e..52fe106e 100644 --- a/src/sync/polling/updaters/segmentChangesUpdater.ts +++ b/src/sync/polling/updaters/segmentChangesUpdater.ts @@ -83,7 +83,7 @@ export function segmentChangesUpdaterFactory( } return Promise.all(updaters).then(shouldUpdateFlags => { - // if at least one segment fetch successes, mark segments ready + // if at least one segment fetch succeeded, mark segments ready if (findIndex(shouldUpdateFlags, v => v !== -1) !== -1 || readyOnAlreadyExistentState) { readyOnAlreadyExistentState = false; if (readiness) readiness.segments.emit(SDK_SEGMENTS_ARRIVED); diff --git a/src/sync/polling/updaters/splitChangesUpdater.ts b/src/sync/polling/updaters/splitChangesUpdater.ts index 8d3ded13..4ba0c51b 100644 --- a/src/sync/polling/updaters/splitChangesUpdater.ts +++ b/src/sync/polling/updaters/splitChangesUpdater.ts @@ -26,7 +26,6 @@ function checkAllSegmentsExist(segmentsStorage: ISegmentsCacheBase): Promise { - let segments = new _Set(); for (let i = 0; i < conditions.length; i++) { From 4c4e88c696e08f5df5441d1a11e3727aeee4698f Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 4 May 2021 20:28:00 -0300 Subject: [PATCH 034/144] updates to integrate with JS-client --- src/index.ts | 3 +++ src/readiness/constants.ts | 6 +++--- src/readiness/types.ts | 6 +++--- src/storages/pluggable/index.ts | 6 ++++-- src/storages/types.ts | 4 ++++ .../polling/updaters/__tests__/splitChangesUpdater.spec.ts | 2 +- 6 files changed, 18 insertions(+), 9 deletions(-) diff --git a/src/index.ts b/src/index.ts index c6141767..6ffbb729 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,3 +5,6 @@ export { InMemoryStorageCSFactory } from './storages/inMemory/InMemoryStorageCS' // Pluggable storage factory for consumer/producer mode export { PluggableStorage } from './storages/pluggable'; + +// Pluggable storage factory for consumer/producer mode +export { InRedisStorage } from './storages/inRedis'; diff --git a/src/readiness/constants.ts b/src/readiness/constants.ts index 90b926de..061bbd67 100644 --- a/src/readiness/constants.ts +++ b/src/readiness/constants.ts @@ -1,9 +1,9 @@ // Splits events: -export const SDK_SPLITS_ARRIVED = 'SDK_SPLITS_ARRIVED'; -export const SDK_SPLITS_CACHE_LOADED = 'SDK_SPLITS_CACHE_LOADED'; +export const SDK_SPLITS_ARRIVED = 'state::splits-arrived'; +export const SDK_SPLITS_CACHE_LOADED = 'state::splits-cache-loaded'; // Segments events: -export const SDK_SEGMENTS_ARRIVED = 'SDK_SEGMENTS_ARRIVED'; +export const SDK_SEGMENTS_ARRIVED = 'state::segments-arrived'; // Readiness events: export const SDK_READY_TIMED_OUT = 'init::timeout'; diff --git a/src/readiness/types.ts b/src/readiness/types.ts index 062daf4e..288a2717 100644 --- a/src/readiness/types.ts +++ b/src/readiness/types.ts @@ -2,8 +2,8 @@ import { IEventEmitter, IStatusInterface } from '../types'; /** Splits data emitter */ -type SDK_SPLITS_ARRIVED = 'SDK_SPLITS_ARRIVED' -type SDK_SPLITS_CACHE_LOADED = 'SDK_SPLITS_CACHE_LOADED' +type SDK_SPLITS_ARRIVED = 'state::splits-arrived' +type SDK_SPLITS_CACHE_LOADED = 'state::splits-cache-loaded' type ISplitsEvent = SDK_SPLITS_ARRIVED | SDK_SPLITS_CACHE_LOADED export interface ISplitsEventEmitter extends IEventEmitter { @@ -16,7 +16,7 @@ export interface ISplitsEventEmitter extends IEventEmitter { /** Segments data emitter */ -type SDK_SEGMENTS_ARRIVED = 'SDK_SEGMENTS_ARRIVED' +type SDK_SEGMENTS_ARRIVED = 'state::segments-arrived' type ISegmentsEvent = SDK_SEGMENTS_ARRIVED export interface ISegmentsEventEmitter extends IEventEmitter { diff --git a/src/storages/pluggable/index.ts b/src/storages/pluggable/index.ts index 8dc3f95d..c955030b 100644 --- a/src/storages/pluggable/index.ts +++ b/src/storages/pluggable/index.ts @@ -44,8 +44,10 @@ export function PluggableStorage(options: PluggableStorageOptions) { // subscription to Wrapper connect event in order to emit SDK_READY event wrapper.connect().then(() => { - readinessManager.splits.emit(SDK_SPLITS_ARRIVED); - readinessManager.segments.emit(SDK_SEGMENTS_ARRIVED); + if (readinessManager) { + readinessManager.splits.emit(SDK_SPLITS_ARRIVED); + readinessManager.segments.emit(SDK_SEGMENTS_ARRIVED); + } }); return { diff --git a/src/storages/types.ts b/src/storages/types.ts index aceb23a0..9dd92a23 100644 --- a/src/storages/types.ts +++ b/src/storages/types.ts @@ -87,10 +87,14 @@ export interface ICustomStorageWrapper { * Return a promise that resolves with a boolean value: * - true if the operation succeeded, * - or false if the operation failed. + * + * Note: will be called once on SplitFactory instantiation. */ connect: () => Promise /** * For storages that requires to be closed, for example, to release resources. + * + * Note: will be called once on SplitFactory default client destroy. */ close: () => Promise } diff --git a/src/sync/polling/updaters/__tests__/splitChangesUpdater.spec.ts b/src/sync/polling/updaters/__tests__/splitChangesUpdater.spec.ts index cbf2e5ac..cfe8dac6 100644 --- a/src/sync/polling/updaters/__tests__/splitChangesUpdater.spec.ts +++ b/src/sync/polling/updaters/__tests__/splitChangesUpdater.spec.ts @@ -80,7 +80,7 @@ test('splitChangesUpdater / factory', (done) => { expect(removeSplits).toBeCalledTimes(1); expect(removeSplits).lastCalledWith([]); expect(registerSegments).toBeCalledTimes(1); - expect(splitsEmitSpy).toBeCalledWith('SDK_SPLITS_ARRIVED'); + expect(splitsEmitSpy).toBeCalledWith('state::splits-arrived'); expect(result).toBe(true); done(); }); From a8e9faec1ef2e56c152cdcc8a898050ed5bc2f07 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 4 May 2021 20:40:18 -0300 Subject: [PATCH 035/144] updates --- src/storages/inRedis/index.ts | 8 ++++---- src/storages/types.ts | 3 +-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/storages/inRedis/index.ts b/src/storages/inRedis/index.ts index 36cc33e8..a5523eca 100644 --- a/src/storages/inRedis/index.ts +++ b/src/storages/inRedis/index.ts @@ -28,12 +28,12 @@ export function InRedisStorage(options: InRedisStorageOptions = {}) { const redisClient = new RedisAdapter(log, options.options || {}); // subscription to Redis connect event in order to emit SDK_READY event on consumer mode - if (readinessManager) { - redisClient.on('connect', () => { + redisClient.on('connect', () => { + if (readinessManager) { readinessManager.splits.emit(SDK_SPLITS_ARRIVED); readinessManager.segments.emit(SDK_SEGMENTS_ARRIVED); - }); - } + } + }); return { splits: new SplitsCacheInRedis(log, keys, redisClient), diff --git a/src/storages/types.ts b/src/storages/types.ts index aceb23a0..1f252072 100644 --- a/src/storages/types.ts +++ b/src/storages/types.ts @@ -334,13 +334,12 @@ export interface IStorageFactoryParams { log: ILogger, eventsQueueSize?: number, optimize?: boolean /* whether create the `impressionCounts` cache (OPTIMIZED impression mode) or not (DEBUG impression mode) */, - dataLoader?: DataLoader, // ATM, only used by InLocalStorage matchingKey?: string, /* undefined on server-side SDKs */ splitFiltersValidation?: ISplitFiltersValidation, - // ATM, only used by InRedisStorage. @TODO pass a callback to simplify custom storages. + // Used by InRedis and Pluggable Storage readinessManager?: IReadinessManager, metadata: IMetadata, } From ffba1bd71face3ccf24f6a391b1275f964d1f317 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 5 May 2021 12:22:00 -0300 Subject: [PATCH 036/144] some updates --- src/index.ts | 4 ++++ src/services/splitApi.ts | 4 ++-- src/storages/inRedis/SplitsCacheInRedis.ts | 21 --------------------- 3 files changed, 6 insertions(+), 23 deletions(-) diff --git a/src/index.ts b/src/index.ts index 6ffbb729..33f4217a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,8 +1,12 @@ // InMemory storage factory for standalone server-side SplitFactory export { InMemoryStorageFactory } from './storages/inMemory/InMemoryStorage'; + // InMemory storage factory for standalone client-side SplitFactory export { InMemoryStorageCSFactory } from './storages/inMemory/InMemoryStorageCS'; +// InLocalStorage factory for standalone client-side SplitFactory in Web browsers +export { InLocalStorage } from './storages/inLocalStorage'; + // Pluggable storage factory for consumer/producer mode export { PluggableStorage } from './storages/pluggable'; diff --git a/src/services/splitApi.ts b/src/services/splitApi.ts index a8f8220b..5a09b726 100644 --- a/src/services/splitApi.ts +++ b/src/services/splitApi.ts @@ -14,9 +14,9 @@ function userKeyToQueryParam(userKey: string) { * Factory of SplitApi objects, which group the collection of Split HTTP endpoints used by the SDK * * @param settings validated settings object - * @param platform environment-specific dependencies + * @param platform object containing environment-specific `getFetch` and `getOptions` dependencies */ -export function splitApiFactory(settings: ISettings, platform: IPlatform): ISplitApi { +export function splitApiFactory(settings: ISettings, platform: Pick): ISplitApi { const urls = settings.urls; const filterQueryString = (settings as ISettingsInternal).sync.__splitFiltersValidation && (settings as ISettingsInternal).sync.__splitFiltersValidation.queryString; diff --git a/src/storages/inRedis/SplitsCacheInRedis.ts b/src/storages/inRedis/SplitsCacheInRedis.ts index d3c8d082..e8da3c00 100644 --- a/src/storages/inRedis/SplitsCacheInRedis.ts +++ b/src/storages/inRedis/SplitsCacheInRedis.ts @@ -24,20 +24,11 @@ export default class SplitsCacheInRedis implements ISplitsCacheAsync { private readonly log: ILogger; private readonly redis: Redis; private readonly keys: KeyBuilderSS; - private redisError?: string; constructor(log: ILogger, keys: KeyBuilderSS, redis: Redis) { this.log = log; this.redis = redis; this.keys = keys; - - this.redis.on('error', (e) => { - this.redisError = e; - }); - - this.redis.on('connect', () => { - this.redisError = undefined; - }); } // @TODO fix: incr/decr TT and segments for producer mode. Follow pluggable storage signature @@ -91,12 +82,6 @@ export default class SplitsCacheInRedis implements ISplitsCacheAsync { * Returned promise is rejected if redis operation fails. */ getSplit(name: string): Promise { - if (this.redisError) { - this.log.error(LOG_PREFIX + this.redisError); - - return Promise.reject(this.redisError); // no need to wrap as an SplitError - } - return this.redis.get(this.keys.buildSplitKey(name)); } @@ -180,12 +165,6 @@ export default class SplitsCacheInRedis implements ISplitsCacheAsync { * Returned promise is rejected if redis operation fails. */ getSplits(names: string[]): Promise> { - if (this.redisError) { - this.log.error(LOG_PREFIX + this.redisError); - - return Promise.reject(this.redisError); // no need to wrap as an SplitError - } - const splits: Record = {}; const keys = names.map(name => this.keys.buildSplitKey(name)); return this.redis.mget(...keys) From b796fa4933ca0d7cb025a0da02e51c804b854130 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 5 May 2021 12:24:15 -0300 Subject: [PATCH 037/144] updated version for rc --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index dd8b235b..f69a63f8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "0.1.0", + "version": "0.1.1-canary.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index c981c1b2..78f7ab23 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "0.1.0", + "version": "0.1.1-canary.0", "description": "Split Javascript SDK common components", "main": "cjs/index.js", "module": "esm/index.js", From ed6bc0439587a79f24300eb14fb708fef72ea0ae Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 5 May 2021 15:48:42 -0300 Subject: [PATCH 038/144] added unit tests --- .../metadataBuilder.spec.ts} | 4 +- src/storages/__tests__/testUtils.ts | 13 +++++ .../inLocalStorage/__tests__/index.spec.ts | 3 +- src/storages/inRedis/SplitsCacheInRedis.ts | 23 ++++++++ .../pluggable/__tests__/index.spec.ts | 58 +++++++++++++++++++ src/storages/pluggable/index.ts | 5 +- 6 files changed, 101 insertions(+), 5 deletions(-) rename src/storages/{inRedis/__tests__/index.spec.ts => __tests__/metadataBuilder.spec.ts} (91%) create mode 100644 src/storages/__tests__/testUtils.ts create mode 100644 src/storages/pluggable/__tests__/index.spec.ts diff --git a/src/storages/inRedis/__tests__/index.spec.ts b/src/storages/__tests__/metadataBuilder.spec.ts similarity index 91% rename from src/storages/inRedis/__tests__/index.spec.ts rename to src/storages/__tests__/metadataBuilder.spec.ts index 2a611419..c3365308 100644 --- a/src/storages/inRedis/__tests__/index.spec.ts +++ b/src/storages/__tests__/metadataBuilder.spec.ts @@ -1,5 +1,5 @@ -import { metadataBuilder } from '../../metadataBuilder'; -import { UNKNOWN } from '../../../utils/constants'; +import { metadataBuilder } from '../metadataBuilder'; +import { UNKNOWN } from '../../utils/constants'; describe('metadataBuilder', () => { diff --git a/src/storages/__tests__/testUtils.ts b/src/storages/__tests__/testUtils.ts new file mode 100644 index 00000000..8bee84de --- /dev/null +++ b/src/storages/__tests__/testUtils.ts @@ -0,0 +1,13 @@ +import { IStorageSync, IStorageAsync } from '../types'; + +// Assert that instances created by storage factories have the expected interface +export function assertStorageInterface(storage: IStorageSync | IStorageAsync) { + expect(typeof storage.destroy).toBe('function'); + expect(typeof storage.splits).toBe('object'); + expect(typeof storage.segments).toBe('object'); + expect(typeof storage.impressions).toBe('object'); + expect(typeof storage.events).toBe('object'); + expect(!storage.latencies || typeof storage.latencies === 'object').toBeTruthy; + expect(!storage.counts || typeof storage.counts === 'object').toBeTruthy; + expect(!storage.impressionCounts || typeof storage.impressionCounts === 'object').toBeTruthy; +} diff --git a/src/storages/inLocalStorage/__tests__/index.spec.ts b/src/storages/inLocalStorage/__tests__/index.spec.ts index 131f2eff..417c8727 100644 --- a/src/storages/inLocalStorage/__tests__/index.spec.ts +++ b/src/storages/inLocalStorage/__tests__/index.spec.ts @@ -1,6 +1,7 @@ // Mocks import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; import { IStorageFactoryParams } from '../../types'; +import { assertStorageInterface } from '../../__tests__/testUtils'; const fakeInMemoryStorage = 'fakeStorage'; const fakeInMemoryStorageFactory = jest.fn(() => fakeInMemoryStorage); jest.mock('../../inMemory/InMemoryStorageCS', () => { @@ -41,7 +42,7 @@ describe('IN LOCAL STORAGE', () => { const storageFactory = InLocalStorage({ prefix: 'prefix' }); const storage = storageFactory(internalSdkParams); - expect(typeof storage).toBe('object'); + assertStorageInterface(storage); // the instance must implement the storage interface expect(fakeInMemoryStorageFactory).not.toBeCalled(); // doesn't call InMemoryStorage factory }); diff --git a/src/storages/inRedis/SplitsCacheInRedis.ts b/src/storages/inRedis/SplitsCacheInRedis.ts index e8da3c00..1193d48d 100644 --- a/src/storages/inRedis/SplitsCacheInRedis.ts +++ b/src/storages/inRedis/SplitsCacheInRedis.ts @@ -24,11 +24,22 @@ export default class SplitsCacheInRedis implements ISplitsCacheAsync { private readonly log: ILogger; private readonly redis: Redis; private readonly keys: KeyBuilderSS; + private redisError?: string; constructor(log: ILogger, keys: KeyBuilderSS, redis: Redis) { this.log = log; this.redis = redis; this.keys = keys; + + // There is no need to listen for redis 'error' event, because in that case ioredis calls will be rejected and handled by redis storage adapters. + // But it is done just to avoid getting the ioredis message `Unhandled error event`. + this.redis.on('error', (e) => { + this.redisError = e; + }); + + this.redis.on('connect', () => { + this.redisError = undefined; + }); } // @TODO fix: incr/decr TT and segments for producer mode. Follow pluggable storage signature @@ -82,6 +93,12 @@ export default class SplitsCacheInRedis implements ISplitsCacheAsync { * Returned promise is rejected if redis operation fails. */ getSplit(name: string): Promise { + if (this.redisError) { + this.log.error(LOG_PREFIX + this.redisError); + + return Promise.reject(this.redisError); // no need to wrap as an SplitError + } + return this.redis.get(this.keys.buildSplitKey(name)); } @@ -165,6 +182,12 @@ export default class SplitsCacheInRedis implements ISplitsCacheAsync { * Returned promise is rejected if redis operation fails. */ getSplits(names: string[]): Promise> { + if (this.redisError) { + this.log.error(LOG_PREFIX + this.redisError); + + return Promise.reject(this.redisError); // no need to wrap as an SplitError + } + const splits: Record = {}; const keys = names.map(name => this.keys.buildSplitKey(name)); return this.redis.mget(...keys) diff --git a/src/storages/pluggable/__tests__/index.spec.ts b/src/storages/pluggable/__tests__/index.spec.ts new file mode 100644 index 00000000..f37a88f5 --- /dev/null +++ b/src/storages/pluggable/__tests__/index.spec.ts @@ -0,0 +1,58 @@ +// @ts-nocheck + +// Mocks +import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; +import { IStorageFactoryParams } from '../../types'; +import { wrapperMock, wrapperMockFactory } from './wrapper.mock'; + +const metadata = { s: 'version', i: 'ip', n: 'hostname' }; +const prefix = 'some_prefix'; + +// Test target +import { PluggableStorage } from '../index'; +import { assertStorageInterface } from '../../__tests__/testUtils'; + +describe('PLUGGABLE STORAGE', () => { + + const internalSdkParams: IStorageFactoryParams = { + log: loggerMock, + metadata + }; + + afterEach(() => { + wrapperMock.mockClear(); + }); + + test('creates a storage instance', async () => { + const storageFactory = PluggableStorage({ prefix, wrapper: wrapperMock }); + const storage = storageFactory(internalSdkParams); + + assertStorageInterface(storage); // the instance must implement the storage interface + expect(wrapperMock.connect).toBeCalledTimes(1); // wrapper connect method should be called once when storage is created + + expect(await storage.splits.getSplit('some_split')).toBe(null); + expect(wrapperMock.get).toBeCalledWith(`${prefix}.SPLITIO.split.some_split`); // keys prefix should be the provided one + + await storage.destroy(); + expect(wrapperMock.close).toBeCalledTimes(1); // wrapper close method should be called once when storage is destroyed + }); + + test('throws an exception if wrapper doesn\'t implement the expected interface', async () => { + + expect(() => PluggableStorage({ wrapper: wrapperMock })).not.toThrow(); // not prefix but valid wrapper is OK + + // Throws exception if no object is passed as wrapper + const errorNoWrapper = 'No `wrapper` option was provided'; + expect(() => PluggableStorage()).toThrow(errorNoWrapper); + expect(() => PluggableStorage({ wrapper: undefined })).toThrow(errorNoWrapper); + expect(() => PluggableStorage({ wrapper: 'invalid wrapper' })).toThrow(errorNoWrapper); + + // Throws exception if the given object is not a valid wrapper, informing which methods are missing + const invalidWrapper = wrapperMockFactory(); + invalidWrapper.connect = undefined; + invalidWrapper.close = 'invalid function'; + expect(() => PluggableStorage({ wrapper: invalidWrapper })).toThrow('Wrapper instance must implement the following methods: connect,close'); + expect(() => PluggableStorage({ wrapper: {} })).toThrow('Wrapper instance must implement the following methods: get,set,getAndSet,del,getKeysByPrefix,getByPrefix,incr,decr,getMany,pushItems,popItems,getItemsCount,itemContains,connect,close'); + }); + +}); diff --git a/src/storages/pluggable/index.ts b/src/storages/pluggable/index.ts index c955030b..b233e635 100644 --- a/src/storages/pluggable/index.ts +++ b/src/storages/pluggable/index.ts @@ -16,12 +16,13 @@ export interface PluggableStorageOptions { } /** - * Validate the options given to the pluggable storage factory. + * Validate pluggable storage factory options. + * * @param options user options * @throws Will throw an error if the options are invalid. Example: wrapper is not provided or doesn't have some methods. */ function validatePluggableStorageOptions(options: any) { - if (!isObject(options) || !isObject(options.wrapper)) throw new Error('No wrapper provided'); + if (!isObject(options) || !isObject(options.wrapper)) throw new Error('No `wrapper` option was provided'); const wrapper = options.wrapper; const missingMethods = METHODS_TO_PROMISE_WRAP.filter(method => typeof wrapper[method] !== 'function'); From cdd9213a9d8f3c8dc0b3d60ec86430e537a4f8f9 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 5 May 2021 20:03:41 -0300 Subject: [PATCH 039/144] emi's feedback --- src/storages/pluggable/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/storages/pluggable/index.ts b/src/storages/pluggable/index.ts index b233e635..49cb31d9 100644 --- a/src/storages/pluggable/index.ts +++ b/src/storages/pluggable/index.ts @@ -39,7 +39,6 @@ export function PluggableStorage(options: PluggableStorageOptions) { const prefix = options.prefix ? options.prefix + '.SPLITIO' : 'SPLITIO'; return function PluggableStorageFactory({ log, metadata, readinessManager }: IStorageFactoryParams): IStorageAsync { - const keys = new KeyBuilderSS(prefix, metadata); const wrapper = wrapperAdapter(log, options.wrapper); From a68f2dc794356432136b99ce6580c89d1859787e Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 6 May 2021 19:31:00 -0300 Subject: [PATCH 040/144] nico's feedback --- .../pluggable/__tests__/index.spec.ts | 13 +++---- src/storages/pluggable/index.ts | 7 ++-- .../polling/updaters/segmentChangesUpdater.ts | 35 ++++++++++--------- 3 files changed, 31 insertions(+), 24 deletions(-) diff --git a/src/storages/pluggable/__tests__/index.spec.ts b/src/storages/pluggable/__tests__/index.spec.ts index f37a88f5..8744b348 100644 --- a/src/storages/pluggable/__tests__/index.spec.ts +++ b/src/storages/pluggable/__tests__/index.spec.ts @@ -42,17 +42,18 @@ describe('PLUGGABLE STORAGE', () => { expect(() => PluggableStorage({ wrapper: wrapperMock })).not.toThrow(); // not prefix but valid wrapper is OK // Throws exception if no object is passed as wrapper - const errorNoWrapper = 'No `wrapper` option was provided'; - expect(() => PluggableStorage()).toThrow(errorNoWrapper); - expect(() => PluggableStorage({ wrapper: undefined })).toThrow(errorNoWrapper); - expect(() => PluggableStorage({ wrapper: 'invalid wrapper' })).toThrow(errorNoWrapper); + const errorNoValidWrapper = 'Expecting custom storage `wrapper` in options, but no valid wrapper instance was provided.'; + expect(() => PluggableStorage()).toThrow(errorNoValidWrapper); + expect(() => PluggableStorage({ wrapper: undefined })).toThrow(errorNoValidWrapper); + expect(() => PluggableStorage({ wrapper: 'invalid wrapper' })).toThrow(errorNoValidWrapper); // Throws exception if the given object is not a valid wrapper, informing which methods are missing const invalidWrapper = wrapperMockFactory(); invalidWrapper.connect = undefined; invalidWrapper.close = 'invalid function'; - expect(() => PluggableStorage({ wrapper: invalidWrapper })).toThrow('Wrapper instance must implement the following methods: connect,close'); - expect(() => PluggableStorage({ wrapper: {} })).toThrow('Wrapper instance must implement the following methods: get,set,getAndSet,del,getKeysByPrefix,getByPrefix,incr,decr,getMany,pushItems,popItems,getItemsCount,itemContains,connect,close'); + const errorNoValidWrapperInterface = 'The provided wrapper instance doesn’t follow the expected interface. Check our docs.'; + expect(() => PluggableStorage({ wrapper: invalidWrapper })).toThrow(errorNoValidWrapperInterface); + expect(() => PluggableStorage({ wrapper: {} })).toThrow(errorNoValidWrapperInterface); }); }); diff --git a/src/storages/pluggable/index.ts b/src/storages/pluggable/index.ts index 49cb31d9..56a04d69 100644 --- a/src/storages/pluggable/index.ts +++ b/src/storages/pluggable/index.ts @@ -10,6 +10,9 @@ import { SDK_SPLITS_ARRIVED, SDK_SEGMENTS_ARRIVED } from '../../readiness/consta import { wrapperAdapter, METHODS_TO_PROMISE_WRAP } from './wrapperAdapter'; import { isObject } from '../../utils/lang'; +const NO_VALID_WRAPPER = 'Expecting custom storage `wrapper` in options, but no valid wrapper instance was provided.'; +const NO_VALID_WRAPPER_INTERFACE = 'The provided wrapper instance doesn’t follow the expected interface. Check our docs.'; + export interface PluggableStorageOptions { prefix?: string wrapper: ICustomStorageWrapper @@ -22,11 +25,11 @@ export interface PluggableStorageOptions { * @throws Will throw an error if the options are invalid. Example: wrapper is not provided or doesn't have some methods. */ function validatePluggableStorageOptions(options: any) { - if (!isObject(options) || !isObject(options.wrapper)) throw new Error('No `wrapper` option was provided'); + if (!isObject(options) || !isObject(options.wrapper)) throw new Error(NO_VALID_WRAPPER); const wrapper = options.wrapper; const missingMethods = METHODS_TO_PROMISE_WRAP.filter(method => typeof wrapper[method] !== 'function'); - if (missingMethods.length) throw new Error(`Wrapper instance must implement the following methods: ${missingMethods}`); + if (missingMethods.length) throw new Error(`${NO_VALID_WRAPPER_INTERFACE} The following methods are missing or invalid: ${missingMethods}`); } /** diff --git a/src/sync/polling/updaters/segmentChangesUpdater.ts b/src/sync/polling/updaters/segmentChangesUpdater.ts index 52fe106e..339f12fe 100644 --- a/src/sync/polling/updaters/segmentChangesUpdater.ts +++ b/src/sync/polling/updaters/segmentChangesUpdater.ts @@ -54,8 +54,6 @@ export function segmentChangesUpdaterFactory( let segmentsPromise = Promise.resolve(segmentNames ? segmentNames : segmentsCache.getRegisteredSegments()); return segmentsPromise.then(segments => { - if (fetchOnlyNew) segments = segments.filter(segmentName => segmentsCache.getChangeNumber(segmentName) === -1); - // Async fetchers are collected here. const updaters: Promise[] = []; @@ -64,21 +62,26 @@ export function segmentChangesUpdaterFactory( log.debug(`${LOG_PREFIX_SYNC_SEGMENTS}Processing segment ${segmentName}`); let sincePromise = Promise.resolve(segmentsCache.getChangeNumber(segmentName)); - updaters.push(sincePromise.then(since => segmentChangesFetcher(since, segmentName, noCache, _promiseDecorator).then(function (changes) { - let changeNumber = -1; - changes.forEach(x => { - if (x.added.length > 0) segmentsCache.addToSegment(segmentName, x.added); - if (x.removed.length > 0) segmentsCache.removeFromSegment(segmentName, x.removed); - if (x.added.length > 0 || x.removed.length > 0) { - segmentsCache.setChangeNumber(segmentName, x.till); - changeNumber = x.till; - } - - log.debug(`${LOG_PREFIX_SYNC_SEGMENTS}Processed ${segmentName} with till = ${x.till}. Added: ${x.added.length}. Removed: ${x.removed.length}`); - }); + updaters.push(sincePromise.then(since => { + // if fetchOnlyNew flag, avoid processing already fetched segments + if (fetchOnlyNew && since !== -1) return -1; + + return segmentChangesFetcher(since, segmentName, noCache, _promiseDecorator).then(function (changes) { + let changeNumber = -1; + changes.forEach(x => { + if (x.added.length > 0) segmentsCache.addToSegment(segmentName, x.added); + if (x.removed.length > 0) segmentsCache.removeFromSegment(segmentName, x.removed); + if (x.added.length > 0 || x.removed.length > 0) { + segmentsCache.setChangeNumber(segmentName, x.till); + changeNumber = x.till; + } - return changeNumber; - }))); + log.debug(`${LOG_PREFIX_SYNC_SEGMENTS}Processed ${segmentName} with till = ${x.till}. Added: ${x.added.length}. Removed: ${x.removed.length}`); + }); + + return changeNumber; + }); + })); } From 83f92ef711e27e918033c1d61df10f0595d9dc3c Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 6 May 2021 19:58:23 -0300 Subject: [PATCH 041/144] implementation and unit test --- src/sdkFactory/index.ts | 9 +++++++-- src/storages/inRedis/index.ts | 8 ++------ src/storages/pluggable/__tests__/index.spec.ts | 5 ++++- src/storages/pluggable/index.ts | 11 +++-------- src/storages/types.ts | 3 +-- 5 files changed, 17 insertions(+), 19 deletions(-) diff --git a/src/sdkFactory/index.ts b/src/sdkFactory/index.ts index 468aa77a..3dc5ac2f 100644 --- a/src/sdkFactory/index.ts +++ b/src/sdkFactory/index.ts @@ -11,6 +11,7 @@ import { validateAndTrackApiKey } from '../utils/inputValidation/apiKey'; import { createLoggerAPI } from '../logger/sdkLogger'; import { NEW_FACTORY, RETRIEVE_MANAGER } from '../logger/constants'; import { metadataBuilder } from '../storages/metadataBuilder'; +import { SDK_SPLITS_ARRIVED, SDK_SEGMENTS_ARRIVED } from '../readiness/constants'; /** * Modular SDK factory @@ -28,6 +29,7 @@ export function sdkFactory(params: ISdkFactoryParams): SplitIO.ICsSDK | SplitIO. // @TODO handle non-recoverable error, such as, `fetch` api not available, invalid API Key, etc. const sdkReadinessManager = sdkReadinessManagerFactory(log, platform.EventEmitter, settings.startup.readyTimeout); + const readinessManager = sdkReadinessManager.readinessManager; // @TODO consider passing the settings object, so that each storage access only what it needs const storageFactoryParams = { @@ -38,8 +40,11 @@ export function sdkFactory(params: ISdkFactoryParams): SplitIO.ICsSDK | SplitIO. matchingKey: getMatching(settings.core.key), splitFiltersValidation: settings.sync.__splitFiltersValidation, - // Used by InRedis and Pluggable Storage - readinessManager: sdkReadinessManager.readinessManager, + // Used by InRedis and Pluggable Storage in consumer mode to emit SDK_READY + onReadyCb: () => { + readinessManager.splits.emit(SDK_SPLITS_ARRIVED); + readinessManager.segments.emit(SDK_SEGMENTS_ARRIVED); + }, metadata: metadataBuilder(settings), log }; diff --git a/src/storages/inRedis/index.ts b/src/storages/inRedis/index.ts index a5523eca..2ab76479 100644 --- a/src/storages/inRedis/index.ts +++ b/src/storages/inRedis/index.ts @@ -7,7 +7,6 @@ import ImpressionsCacheInRedis from './ImpressionsCacheInRedis'; import EventsCacheInRedis from './EventsCacheInRedis'; import LatenciesCacheInRedis from './LatenciesCacheInRedis'; import CountsCacheInRedis from './CountsCacheInRedis'; -import { SDK_SPLITS_ARRIVED, SDK_SEGMENTS_ARRIVED } from '../../readiness/constants'; export interface InRedisStorageOptions { prefix?: string @@ -22,17 +21,14 @@ export function InRedisStorage(options: InRedisStorageOptions = {}) { const prefix = options.prefix ? options.prefix + '.SPLITIO' : 'SPLITIO'; - return function InRedisStorageFactory({ log, readinessManager, metadata }: IStorageFactoryParams): IStorageAsync { + return function InRedisStorageFactory({ log, metadata, onReadyCb }: IStorageFactoryParams): IStorageAsync { const keys = new KeyBuilderSS(prefix, metadata); const redisClient = new RedisAdapter(log, options.options || {}); // subscription to Redis connect event in order to emit SDK_READY event on consumer mode redisClient.on('connect', () => { - if (readinessManager) { - readinessManager.splits.emit(SDK_SPLITS_ARRIVED); - readinessManager.segments.emit(SDK_SEGMENTS_ARRIVED); - } + if (onReadyCb) onReadyCb(); }); return { diff --git a/src/storages/pluggable/__tests__/index.spec.ts b/src/storages/pluggable/__tests__/index.spec.ts index 8744b348..3f49e33a 100644 --- a/src/storages/pluggable/__tests__/index.spec.ts +++ b/src/storages/pluggable/__tests__/index.spec.ts @@ -16,7 +16,8 @@ describe('PLUGGABLE STORAGE', () => { const internalSdkParams: IStorageFactoryParams = { log: loggerMock, - metadata + metadata, + onReadyCb: jest.fn() }; afterEach(() => { @@ -35,6 +36,8 @@ describe('PLUGGABLE STORAGE', () => { await storage.destroy(); expect(wrapperMock.close).toBeCalledTimes(1); // wrapper close method should be called once when storage is destroyed + + expect(internalSdkParams.onReadyCb).toBeCalledTimes(1); // onReady callback should be called when the wrapper connect resolved with true }); test('throws an exception if wrapper doesn\'t implement the expected interface', async () => { diff --git a/src/storages/pluggable/index.ts b/src/storages/pluggable/index.ts index 56a04d69..c2e4ffb3 100644 --- a/src/storages/pluggable/index.ts +++ b/src/storages/pluggable/index.ts @@ -5,8 +5,6 @@ import { SplitsCachePluggable } from './SplitsCachePluggable'; import { SegmentsCachePluggable } from './SegmentsCachePluggable'; import { ImpressionsCachePluggable } from './ImpressionsCachePluggable'; import { EventsCachePluggable } from './EventsCachePluggable'; - -import { SDK_SPLITS_ARRIVED, SDK_SEGMENTS_ARRIVED } from '../../readiness/constants'; import { wrapperAdapter, METHODS_TO_PROMISE_WRAP } from './wrapperAdapter'; import { isObject } from '../../utils/lang'; @@ -41,16 +39,13 @@ export function PluggableStorage(options: PluggableStorageOptions) { const prefix = options.prefix ? options.prefix + '.SPLITIO' : 'SPLITIO'; - return function PluggableStorageFactory({ log, metadata, readinessManager }: IStorageFactoryParams): IStorageAsync { + return function PluggableStorageFactory({ log, metadata, onReadyCb }: IStorageFactoryParams): IStorageAsync { const keys = new KeyBuilderSS(prefix, metadata); const wrapper = wrapperAdapter(log, options.wrapper); // subscription to Wrapper connect event in order to emit SDK_READY event - wrapper.connect().then(() => { - if (readinessManager) { - readinessManager.splits.emit(SDK_SPLITS_ARRIVED); - readinessManager.segments.emit(SDK_SEGMENTS_ARRIVED); - } + wrapper.connect().then((ready) => { + if (ready && onReadyCb) onReadyCb(); }); return { diff --git a/src/storages/types.ts b/src/storages/types.ts index d4c349b7..429f4542 100644 --- a/src/storages/types.ts +++ b/src/storages/types.ts @@ -1,6 +1,5 @@ import { MaybeThenable, IMetadata, ISplitFiltersValidation } from '../dtos/types'; import { ILogger } from '../logger/types'; -import { IReadinessManager } from '../readiness/types'; import { SplitIO, ImpressionDTO } from '../types'; /** @@ -344,6 +343,6 @@ export interface IStorageFactoryParams { splitFiltersValidation?: ISplitFiltersValidation, // Used by InRedis and Pluggable Storage - readinessManager?: IReadinessManager, + onReadyCb?: () => void, metadata: IMetadata, } From 39254df7111483058c12ce64d1d95221529470d9 Mon Sep 17 00:00:00 2001 From: Emiliano Fant Date: Fri, 7 May 2021 10:26:02 -0300 Subject: [PATCH 042/144] fix: tweak an Array.from exists validation --- src/utils/lang/sets.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/lang/sets.ts b/src/utils/lang/sets.ts index bcdd022c..6ecb157d 100644 --- a/src/utils/lang/sets.ts +++ b/src/utils/lang/sets.ts @@ -104,7 +104,7 @@ interface ISetConstructor { */ export function __getSetConstructor(): ISetConstructor { // eslint-disable-next-line compat/compat - if (Array.from && typeof Set === 'function' && Set.prototype && Set.prototype.values) { + if (typeof Array.from === 'function' && typeof Set === 'function' && Set.prototype && Set.prototype.values) { return Set; } return SetPoly; From e7e2b3b0536269fc967999a519aa506e35cdd830 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 10 May 2021 12:21:36 -0300 Subject: [PATCH 043/144] handling onReadyCb in InMemory storage server-side --- src/sdkFactory/index.ts | 6 +++--- src/storages/inMemory/InMemoryStorage.ts | 3 +++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/sdkFactory/index.ts b/src/sdkFactory/index.ts index 3dc5ac2f..577daed6 100644 --- a/src/sdkFactory/index.ts +++ b/src/sdkFactory/index.ts @@ -40,11 +40,11 @@ export function sdkFactory(params: ISdkFactoryParams): SplitIO.ICsSDK | SplitIO. matchingKey: getMatching(settings.core.key), splitFiltersValidation: settings.sync.__splitFiltersValidation, - // Used by InRedis and Pluggable Storage in consumer mode to emit SDK_READY - onReadyCb: () => { + // Callback used in consumer mode (`syncManagerFactory` is undefined) to emit SDK_READY + onReadyCb: !syncManagerFactory ? () => { readinessManager.splits.emit(SDK_SPLITS_ARRIVED); readinessManager.segments.emit(SDK_SEGMENTS_ARRIVED); - }, + } : undefined, metadata: metadataBuilder(settings), log }; diff --git a/src/storages/inMemory/InMemoryStorage.ts b/src/storages/inMemory/InMemoryStorage.ts index 9cdaa4d1..33b71cd1 100644 --- a/src/storages/inMemory/InMemoryStorage.ts +++ b/src/storages/inMemory/InMemoryStorage.ts @@ -12,6 +12,9 @@ import ImpressionCountsCacheInMemory from './ImpressionCountsCacheInMemory'; */ export function InMemoryStorageFactory(params: IStorageFactoryParams): IStorageSync { + // Meant for consumer mode. InMemory storage is ready asynchronously + if (params.onReadyCb) setTimeout(params.onReadyCb); + return { splits: new SplitsCacheInMemory(), segments: new SegmentsCacheInMemory(), From a5880b09c8f6765fab5d5ee58d999104dbac8067 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 10 May 2021 15:40:52 -0300 Subject: [PATCH 044/144] added comment and prepare for rc --- package-lock.json | 2 +- package.json | 2 +- src/utils/settingsValidation/index.ts | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index f69a63f8..ad3ee07c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "0.1.1-canary.0", + "version": "0.1.1-canary.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 78f7ab23..2b370980 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "0.1.1-canary.0", + "version": "0.1.1-canary.1", "description": "Split Javascript SDK common components", "main": "cjs/index.js", "module": "esm/index.js", diff --git a/src/utils/settingsValidation/index.ts b/src/utils/settingsValidation/index.ts index 4f77cc8f..0d6b1882 100644 --- a/src/utils/settingsValidation/index.ts +++ b/src/utils/settingsValidation/index.ts @@ -91,6 +91,7 @@ function fromSecondsToMillis(n: number) { /** * Validates the given config and use it to build a settings object. + * NOTE: it doesn't validate the Api Key. Call `validateApikey` or `validateAndTrackApiKey` for that after settings validation. * * @param config user defined configuration * @param validationParams defaults and fields validators used to validate and creates a settings object from a given config From 33d9b816c14282a792e865b8dbd24b9a4deb3f5b Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 10 May 2021 16:37:33 -0300 Subject: [PATCH 045/144] updated connect callback and wrapper method --- src/sdkFactory/index.ts | 7 ++++--- src/storages/inMemory/InMemoryStorage.ts | 2 +- src/storages/inRedis/index.ts | 4 ++-- src/storages/pluggable/index.ts | 8 +++++--- src/storages/types.ts | 9 ++++----- 5 files changed, 16 insertions(+), 14 deletions(-) diff --git a/src/sdkFactory/index.ts b/src/sdkFactory/index.ts index 577daed6..5b24ae12 100644 --- a/src/sdkFactory/index.ts +++ b/src/sdkFactory/index.ts @@ -2,7 +2,7 @@ import { ISdkFactoryParams } from './types'; import sdkReadinessManagerFactory from '../readiness/sdkReadinessManager'; import impressionsTrackerFactory from '../trackers/impressionsTracker'; import eventTrackerFactory from '../trackers/eventTracker'; -import { IStorageSync } from '../storages/types'; +import { IStorageFactoryParams, IStorageSync } from '../storages/types'; import { SplitIO } from '../types'; import { ISplitApi } from '../services/types'; import { getMatching } from '../utils/key'; @@ -32,7 +32,7 @@ export function sdkFactory(params: ISdkFactoryParams): SplitIO.ICsSDK | SplitIO. const readinessManager = sdkReadinessManager.readinessManager; // @TODO consider passing the settings object, so that each storage access only what it needs - const storageFactoryParams = { + const storageFactoryParams: IStorageFactoryParams = { eventsQueueSize: settings.scheduler.eventsQueueSize, optimize: shouldBeOptimized(settings), @@ -41,7 +41,8 @@ export function sdkFactory(params: ISdkFactoryParams): SplitIO.ICsSDK | SplitIO. splitFiltersValidation: settings.sync.__splitFiltersValidation, // Callback used in consumer mode (`syncManagerFactory` is undefined) to emit SDK_READY - onReadyCb: !syncManagerFactory ? () => { + onConnectCb: !syncManagerFactory ? (e) => { + if (e) return; // don't emit SDK_READY if storage fail to connect. readinessManager.splits.emit(SDK_SPLITS_ARRIVED); readinessManager.segments.emit(SDK_SEGMENTS_ARRIVED); } : undefined, diff --git a/src/storages/inMemory/InMemoryStorage.ts b/src/storages/inMemory/InMemoryStorage.ts index 33b71cd1..f8e3a5e1 100644 --- a/src/storages/inMemory/InMemoryStorage.ts +++ b/src/storages/inMemory/InMemoryStorage.ts @@ -13,7 +13,7 @@ import ImpressionCountsCacheInMemory from './ImpressionCountsCacheInMemory'; export function InMemoryStorageFactory(params: IStorageFactoryParams): IStorageSync { // Meant for consumer mode. InMemory storage is ready asynchronously - if (params.onReadyCb) setTimeout(params.onReadyCb); + if (params.onConnectCb) setTimeout(params.onConnectCb); return { splits: new SplitsCacheInMemory(), diff --git a/src/storages/inRedis/index.ts b/src/storages/inRedis/index.ts index 2ab76479..26c9131b 100644 --- a/src/storages/inRedis/index.ts +++ b/src/storages/inRedis/index.ts @@ -21,14 +21,14 @@ export function InRedisStorage(options: InRedisStorageOptions = {}) { const prefix = options.prefix ? options.prefix + '.SPLITIO' : 'SPLITIO'; - return function InRedisStorageFactory({ log, metadata, onReadyCb }: IStorageFactoryParams): IStorageAsync { + return function InRedisStorageFactory({ log, metadata, onConnectCb }: IStorageFactoryParams): IStorageAsync { const keys = new KeyBuilderSS(prefix, metadata); const redisClient = new RedisAdapter(log, options.options || {}); // subscription to Redis connect event in order to emit SDK_READY event on consumer mode redisClient.on('connect', () => { - if (onReadyCb) onReadyCb(); + if (onConnectCb) onConnectCb(); }); return { diff --git a/src/storages/pluggable/index.ts b/src/storages/pluggable/index.ts index c2e4ffb3..929d48ee 100644 --- a/src/storages/pluggable/index.ts +++ b/src/storages/pluggable/index.ts @@ -39,13 +39,15 @@ export function PluggableStorage(options: PluggableStorageOptions) { const prefix = options.prefix ? options.prefix + '.SPLITIO' : 'SPLITIO'; - return function PluggableStorageFactory({ log, metadata, onReadyCb }: IStorageFactoryParams): IStorageAsync { + return function PluggableStorageFactory({ log, metadata, onConnectCb }: IStorageFactoryParams): IStorageAsync { const keys = new KeyBuilderSS(prefix, metadata); const wrapper = wrapperAdapter(log, options.wrapper); // subscription to Wrapper connect event in order to emit SDK_READY event - wrapper.connect().then((ready) => { - if (ready && onReadyCb) onReadyCb(); + wrapper.connect().then(() => { + if (onConnectCb) onConnectCb(); + }).catch((e) => { + if (onConnectCb) onConnectCb(e || new Error('Error connecting wrapper')); }); return { diff --git a/src/storages/types.ts b/src/storages/types.ts index 429f4542..95e2d2fd 100644 --- a/src/storages/types.ts +++ b/src/storages/types.ts @@ -83,13 +83,12 @@ export interface ICustomStorageWrapper { /** * For storages that requires to be connected, like database servers. - * Return a promise that resolves with a boolean value: - * - true if the operation succeeded, - * - or false if the operation failed. + * Return a promise that resolves when the wrapper successfully connect to the underlying storage, + * or rejects if the wrapper fails to connect. * * Note: will be called once on SplitFactory instantiation. */ - connect: () => Promise + connect: () => Promise /** * For storages that requires to be closed, for example, to release resources. * @@ -343,6 +342,6 @@ export interface IStorageFactoryParams { splitFiltersValidation?: ISplitFiltersValidation, // Used by InRedis and Pluggable Storage - onReadyCb?: () => void, + onConnectCb?: (error?: any) => void, metadata: IMetadata, } From 1c442b823e9c10ea883217a91722addd5ee5affc Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 10 May 2021 16:45:27 -0300 Subject: [PATCH 046/144] rollback name --- src/sdkFactory/index.ts | 4 ++-- src/storages/inMemory/InMemoryStorage.ts | 2 +- src/storages/inRedis/index.ts | 4 ++-- src/storages/pluggable/index.ts | 6 +++--- src/storages/types.ts | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/sdkFactory/index.ts b/src/sdkFactory/index.ts index 5b24ae12..1a21e083 100644 --- a/src/sdkFactory/index.ts +++ b/src/sdkFactory/index.ts @@ -41,8 +41,8 @@ export function sdkFactory(params: ISdkFactoryParams): SplitIO.ICsSDK | SplitIO. splitFiltersValidation: settings.sync.__splitFiltersValidation, // Callback used in consumer mode (`syncManagerFactory` is undefined) to emit SDK_READY - onConnectCb: !syncManagerFactory ? (e) => { - if (e) return; // don't emit SDK_READY if storage fail to connect. + onReadyCb: !syncManagerFactory ? (e) => { + if (e) return; // don't emit SDK_READY if storage failed to connect. readinessManager.splits.emit(SDK_SPLITS_ARRIVED); readinessManager.segments.emit(SDK_SEGMENTS_ARRIVED); } : undefined, diff --git a/src/storages/inMemory/InMemoryStorage.ts b/src/storages/inMemory/InMemoryStorage.ts index f8e3a5e1..33b71cd1 100644 --- a/src/storages/inMemory/InMemoryStorage.ts +++ b/src/storages/inMemory/InMemoryStorage.ts @@ -13,7 +13,7 @@ import ImpressionCountsCacheInMemory from './ImpressionCountsCacheInMemory'; export function InMemoryStorageFactory(params: IStorageFactoryParams): IStorageSync { // Meant for consumer mode. InMemory storage is ready asynchronously - if (params.onConnectCb) setTimeout(params.onConnectCb); + if (params.onReadyCb) setTimeout(params.onReadyCb); return { splits: new SplitsCacheInMemory(), diff --git a/src/storages/inRedis/index.ts b/src/storages/inRedis/index.ts index 26c9131b..2ab76479 100644 --- a/src/storages/inRedis/index.ts +++ b/src/storages/inRedis/index.ts @@ -21,14 +21,14 @@ export function InRedisStorage(options: InRedisStorageOptions = {}) { const prefix = options.prefix ? options.prefix + '.SPLITIO' : 'SPLITIO'; - return function InRedisStorageFactory({ log, metadata, onConnectCb }: IStorageFactoryParams): IStorageAsync { + return function InRedisStorageFactory({ log, metadata, onReadyCb }: IStorageFactoryParams): IStorageAsync { const keys = new KeyBuilderSS(prefix, metadata); const redisClient = new RedisAdapter(log, options.options || {}); // subscription to Redis connect event in order to emit SDK_READY event on consumer mode redisClient.on('connect', () => { - if (onConnectCb) onConnectCb(); + if (onReadyCb) onReadyCb(); }); return { diff --git a/src/storages/pluggable/index.ts b/src/storages/pluggable/index.ts index 929d48ee..7c41621a 100644 --- a/src/storages/pluggable/index.ts +++ b/src/storages/pluggable/index.ts @@ -39,15 +39,15 @@ export function PluggableStorage(options: PluggableStorageOptions) { const prefix = options.prefix ? options.prefix + '.SPLITIO' : 'SPLITIO'; - return function PluggableStorageFactory({ log, metadata, onConnectCb }: IStorageFactoryParams): IStorageAsync { + return function PluggableStorageFactory({ log, metadata, onReadyCb }: IStorageFactoryParams): IStorageAsync { const keys = new KeyBuilderSS(prefix, metadata); const wrapper = wrapperAdapter(log, options.wrapper); // subscription to Wrapper connect event in order to emit SDK_READY event wrapper.connect().then(() => { - if (onConnectCb) onConnectCb(); + if (onReadyCb) onReadyCb(); }).catch((e) => { - if (onConnectCb) onConnectCb(e || new Error('Error connecting wrapper')); + if (onReadyCb) onReadyCb(e || new Error('Error connecting wrapper')); }); return { diff --git a/src/storages/types.ts b/src/storages/types.ts index 95e2d2fd..46773e1b 100644 --- a/src/storages/types.ts +++ b/src/storages/types.ts @@ -342,6 +342,6 @@ export interface IStorageFactoryParams { splitFiltersValidation?: ISplitFiltersValidation, // Used by InRedis and Pluggable Storage - onConnectCb?: (error?: any) => void, + onReadyCb?: (error?: any) => void, metadata: IMetadata, } From 0e840d9657dac91e36cecdd29d70b36a7c8732ea Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 10 May 2021 16:58:42 -0300 Subject: [PATCH 047/144] updated some comments --- src/storages/inMemory/InMemoryStorage.ts | 2 +- src/storages/types.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/storages/inMemory/InMemoryStorage.ts b/src/storages/inMemory/InMemoryStorage.ts index 33b71cd1..a4f29d8e 100644 --- a/src/storages/inMemory/InMemoryStorage.ts +++ b/src/storages/inMemory/InMemoryStorage.ts @@ -12,7 +12,7 @@ import ImpressionCountsCacheInMemory from './ImpressionCountsCacheInMemory'; */ export function InMemoryStorageFactory(params: IStorageFactoryParams): IStorageSync { - // Meant for consumer mode. InMemory storage is ready asynchronously + // InMemory storage is always ready if (params.onReadyCb) setTimeout(params.onReadyCb); return { diff --git a/src/storages/types.ts b/src/storages/types.ts index 46773e1b..85347d09 100644 --- a/src/storages/types.ts +++ b/src/storages/types.ts @@ -341,7 +341,8 @@ export interface IStorageFactoryParams { matchingKey?: string, /* undefined on server-side SDKs */ splitFiltersValidation?: ISplitFiltersValidation, - // Used by InRedis and Pluggable Storage + // This callback is invoked when the storage is ready to be used. If an error is passed, it means that the storge fail to connect and shouldn't be used. + // It is meant for emitting SDK_READY event in consumer mode, and for synchronizer tasks to wait before using the storage. onReadyCb?: (error?: any) => void, metadata: IMetadata, } From 561f47f05a3643370762e280f69d85f793e94707 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 10 May 2021 20:30:56 -0300 Subject: [PATCH 048/144] fixed ut --- src/storages/pluggable/__tests__/wrapper.mock.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/storages/pluggable/__tests__/wrapper.mock.ts b/src/storages/pluggable/__tests__/wrapper.mock.ts index 60e8b772..59390561 100644 --- a/src/storages/pluggable/__tests__/wrapper.mock.ts +++ b/src/storages/pluggable/__tests__/wrapper.mock.ts @@ -79,7 +79,7 @@ export function wrapperMockFactory() { }), // always connects and close - connect: jest.fn(() => Promise.resolve(true)), + connect: jest.fn(() => Promise.resolve()), close: jest.fn(() => Promise.resolve()), mockClear() { From 4dd75c82f467c6a98f2d0dcb3c223856d966007b Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 10 May 2021 20:47:54 -0300 Subject: [PATCH 049/144] rename variable for readability --- src/sdkFactory/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sdkFactory/index.ts b/src/sdkFactory/index.ts index 1a21e083..c1c64b89 100644 --- a/src/sdkFactory/index.ts +++ b/src/sdkFactory/index.ts @@ -41,8 +41,8 @@ export function sdkFactory(params: ISdkFactoryParams): SplitIO.ICsSDK | SplitIO. splitFiltersValidation: settings.sync.__splitFiltersValidation, // Callback used in consumer mode (`syncManagerFactory` is undefined) to emit SDK_READY - onReadyCb: !syncManagerFactory ? (e) => { - if (e) return; // don't emit SDK_READY if storage failed to connect. + onReadyCb: !syncManagerFactory ? (error) => { + if (error) return; // don't emit SDK_READY if storage failed to connect. readinessManager.splits.emit(SDK_SPLITS_ARRIVED); readinessManager.segments.emit(SDK_SEGMENTS_ARRIVED); } : undefined, From deee3116780da51294f2861fb38597716911998c Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 11 May 2021 19:57:13 -0300 Subject: [PATCH 050/144] fixed producer methods of splits storage inredis and pluggable. added some more tests --- src/storages/AbstractSplitsCacheAsync.ts | 65 ++++++ src/storages/KeyBuilder.ts | 1 + .../__tests__/SplitsCacheInLocal.spec.ts | 38 +++- src/storages/inRedis/SplitsCacheInRedis.ts | 141 ++++++++---- .../__tests__/SplitsCacheInRedis.spec.ts | 162 ++++++++----- .../pluggable/SplitsCachePluggable.ts | 94 ++------ .../__tests__/SplitsCachePluggable.spec.ts | 20 -- src/storages/types.ts | 215 +++++++++++------- .../polling/updaters/splitChangesUpdater.ts | 30 +-- 9 files changed, 460 insertions(+), 306 deletions(-) create mode 100644 src/storages/AbstractSplitsCacheAsync.ts diff --git a/src/storages/AbstractSplitsCacheAsync.ts b/src/storages/AbstractSplitsCacheAsync.ts new file mode 100644 index 00000000..86ccf3d6 --- /dev/null +++ b/src/storages/AbstractSplitsCacheAsync.ts @@ -0,0 +1,65 @@ +import { ISplitsCacheAsync } from './types'; +import { ISplit } from '../dtos/types'; + +/** + * This class provides a skeletal implementation of the ISplitsCacheAsync interface + * to minimize the effort required to implement this interface. + */ +export default abstract class AbstractSplitsCacheAsync implements ISplitsCacheAsync { + + abstract addSplit(name: string, split: string): Promise + abstract addSplits(entries: [string, string][]): Promise + abstract removeSplits(names: string[]): Promise + abstract getSplit(name: string): Promise + abstract getSplits(names: string[]): Promise> + abstract setChangeNumber(changeNumber: number): Promise + abstract getChangeNumber(): Promise + abstract getAll(): Promise + abstract getSplitNames(): Promise + abstract trafficTypeExists(trafficType: string): Promise + abstract clear(): Promise + + // @TODO revisit segment-related methods ('usesSegments', 'getRegisteredSegments', 'registerSegments') + // noop, just keeping the interface. This is used by client-side API only, and so only implemented by InLocalStorage. + usesSegments(): Promise { + return Promise.resolve(true); + } + + /** + * Check if the splits information is already stored in cache. + * Noop, just keeping the interface. This is used by client-side implementations only. + */ + checkCache(): Promise { + return Promise.resolve(false); + } + + /** + * Kill `name` split and set `defaultTreatment` and `changeNumber`. + * Used for SPLIT_KILL push notifications. + * + * @param {string} name + * @param {string} defaultTreatment + * @param {number} changeNumber + * @returns {Promise} a promise that is resolved once the split kill operation is performed. The fulfillment value is a boolean: `true` if the kill success updating the split or `false` if no split is updated, + * for instance, if the `changeNumber` is old, or if the split is not found (e.g., `/splitchanges` hasn't been fetched yet), or if the storage fails to apply the update. + * The promise will never be rejected. + */ + killLocally(name: string, defaultTreatment: string, changeNumber: number): Promise { + return this.getSplit(name).then(split => { + + if (split) { + const parsedSplit: ISplit = JSON.parse(split); + if (!parsedSplit.changeNumber || parsedSplit.changeNumber < changeNumber) { + parsedSplit.killed = true; + parsedSplit.defaultTreatment = defaultTreatment; + parsedSplit.changeNumber = changeNumber; + const newSplit = JSON.stringify(parsedSplit); + + return this.addSplit(name, newSplit); + } + } + return false; + }).catch(() => false); + } + +} diff --git a/src/storages/KeyBuilder.ts b/src/storages/KeyBuilder.ts index 801bab3b..16a649e0 100644 --- a/src/storages/KeyBuilder.ts +++ b/src/storages/KeyBuilder.ts @@ -37,6 +37,7 @@ export default class KeyBuilder { return `${this.prefix}.split.`; } + // @TODO remove, since it is not set by Synchronizer. ATM, only used by InLocalStorage. buildSplitsWithSegmentCountKey() { return `${this.prefix}.splits.usingSegments`; } diff --git a/src/storages/inLocalStorage/__tests__/SplitsCacheInLocal.spec.ts b/src/storages/inLocalStorage/__tests__/SplitsCacheInLocal.spec.ts index 4430afe7..99edffd3 100644 --- a/src/storages/inLocalStorage/__tests__/SplitsCacheInLocal.spec.ts +++ b/src/storages/inLocalStorage/__tests__/SplitsCacheInLocal.spec.ts @@ -2,6 +2,10 @@ import SplitsCacheInLocal from '../SplitsCacheInLocal'; import KeyBuilderCS from '../../KeyBuilderCS'; import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; +const splitWithUserTT = '{ "trafficTypeName": "user_tt" }'; +const splitWithAccountTT = '{ "trafficTypeName": "account_tt" }'; +const splitWithAccountTTAndUsesSegments = '{ "trafficTypeName": "account_tt", "conditions": [{ "matcherGroup": { "matchers": [{ "matcherType": "IN_SEGMENT" }]}}] }'; + test('SPLIT CACHE / LocalStorage', () => { const cache = new SplitsCacheInLocal(loggerMock, new KeyBuilderCS('SPLITIO', 'user')); @@ -73,12 +77,12 @@ test('SPLIT CACHE / LocalStorage / trafficTypeExists and ttcache tests', () => { const cache = new SplitsCacheInLocal(loggerMock, new KeyBuilderCS('SPLITIO', 'user')); cache.addSplits([ // loop of addSplit - ['split1', '{ "trafficTypeName": "user_tt" }'], - ['split2', '{ "trafficTypeName": "account_tt" }'], - ['split3', '{ "trafficTypeName": "user_tt" }'], + ['split1', splitWithUserTT], + ['split2', splitWithAccountTT], + ['split3', splitWithUserTT], ['malformed', '{}'] ]); - cache.addSplit('split4', '{ "trafficTypeName": "user_tt" }'); + cache.addSplit('split4', splitWithUserTT); expect(cache.trafficTypeExists('user_tt')).toBe(true); expect(cache.trafficTypeExists('account_tt')).toBe(true); @@ -99,10 +103,10 @@ test('SPLIT CACHE / LocalStorage / trafficTypeExists and ttcache tests', () => { expect(cache.trafficTypeExists('user_tt')).toBe(false); expect(cache.trafficTypeExists('account_tt')).toBe(false); - cache.addSplit('split1', '{ "trafficTypeName": "user_tt" }'); + cache.addSplit('split1', splitWithUserTT); expect(cache.trafficTypeExists('user_tt')).toBe(true); - cache.addSplit('split1', '{ "trafficTypeName": "account_tt" }'); + cache.addSplit('split1', splitWithAccountTT); expect(cache.trafficTypeExists('account_tt')).toBe(true); expect(cache.trafficTypeExists('user_tt')).toBe(false); @@ -139,3 +143,25 @@ test('SPLIT CACHE / LocalStorage / killLocally', () => { expect(lol1Split.defaultTreatment).not.toBe('some_treatment_2'); // existing split is not updated if given changeNumber is older }); + +test('SPLIT CACHE / LocalStorage / usesSegments', () => { + const cache = new SplitsCacheInLocal(loggerMock, new KeyBuilderCS('SPLITIO', 'user')); + + expect(cache.usesSegments()).toBe(true); // true initially, until data is synchronized + cache.setChangeNumber(1); // to indicate that data has been synced. + + cache.addSplits([['split1', splitWithUserTT], ['split2', splitWithAccountTT],]); + expect(cache.usesSegments()).toBe(false); // 0 splits using segments + + cache.addSplit('split3', splitWithAccountTTAndUsesSegments); + expect(cache.usesSegments()).toBe(true); // 1 split using segments + + cache.addSplit('split4', splitWithAccountTTAndUsesSegments); + expect(cache.usesSegments()).toBe(true); // 2 splits using segments + + cache.removeSplit('split3'); + expect(cache.usesSegments()).toBe(true); // 1 split using segments + + cache.removeSplit('split4'); + expect(cache.usesSegments()).toBe(false); // 0 splits using segments +}); diff --git a/src/storages/inRedis/SplitsCacheInRedis.ts b/src/storages/inRedis/SplitsCacheInRedis.ts index 1193d48d..8b21ed01 100644 --- a/src/storages/inRedis/SplitsCacheInRedis.ts +++ b/src/storages/inRedis/SplitsCacheInRedis.ts @@ -1,9 +1,11 @@ import { isFiniteNumber, isNaNNumber } from '../../utils/lang'; import KeyBuilderSS from '../KeyBuilderSS'; -import { ISplitsCacheAsync } from '../types'; import { Redis } from 'ioredis'; import { ILogger } from '../../logger/types'; import { LOG_PREFIX } from './constants'; +import { ISplit } from '../../dtos/types'; +import { SplitError } from '../../utils/lang/errors'; +import AbstractSplitsCacheAsync from '../AbstractSplitsCacheAsync'; /** * Discard errors for an answer of multiple operations. @@ -19,7 +21,7 @@ function processPipelineAnswer(results: Array<[Error | null, string]>): string[] * ISplitsCacheAsync implementation that stores split definitions in Redis. * Supported by Node. */ -export default class SplitsCacheInRedis implements ISplitsCacheAsync { +export default class SplitsCacheInRedis extends AbstractSplitsCacheAsync { private readonly log: ILogger; private readonly redis: Redis; @@ -27,6 +29,7 @@ export default class SplitsCacheInRedis implements ISplitsCacheAsync { private redisError?: string; constructor(log: ILogger, keys: KeyBuilderSS, redis: Redis) { + super(); this.log = log; this.redis = redis; this.keys = keys; @@ -42,50 +45,82 @@ export default class SplitsCacheInRedis implements ISplitsCacheAsync { }); } - // @TODO fix: incr/decr TT and segments for producer mode. Follow pluggable storage signature - addSplit(name: string, split: string): Promise { - return this.redis.set( - this.keys.buildSplitKey(name), split - ).then( - status => status === 'OK' - ); + private _decrementCounts(split: ISplit) { + if (split.trafficTypeName) { + const ttKey = this.keys.buildTrafficTypeKey(split.trafficTypeName); + return this.redis.decr(ttKey); + } } - // @TODO fix: incr/decr TT and segments for producer mode. Follow pluggable storage signature - addSplits(entries: [string, string][]): Promise { - if (entries.length) { - const cmds = entries.map(keyValuePair => ['set', this.keys.buildSplitKey(keyValuePair[0]), keyValuePair[1]]); - - return this.redis.pipeline(cmds) - .exec() - .then(processPipelineAnswer) - .then(answers => answers.map((status: string) => status === 'OK')); - } else { - return Promise.resolve([true]); + private _incrementCounts(split: ISplit) { + if (split.trafficTypeName) { + const ttKey = this.keys.buildTrafficTypeKey(split.trafficTypeName); + return this.redis.incr(ttKey); } } - // @TODO implement for producer mode. Follow pluggable storage signature - killLocally(): Promise { - throw new Error('Method not implemented.'); + /** + * Add a given split. + * The returned promise is resolved when the operation success + * or rejected with if it fails (e.g., redis operation fails) + */ + addSplit(name: string, split: string): Promise { + const splitKey = this.keys.buildSplitKey(name); + return this.redis.get(splitKey).then(splitFromStorage => { + + // handling parsing error as SplitErrors + let parsedPreviousSplit, parsedSplit; + try { + parsedPreviousSplit = splitFromStorage ? JSON.parse(splitFromStorage) : undefined; + parsedSplit = JSON.parse(split); + } catch (e) { + throw new SplitError('Error parsing split definition: ' + e); + } + + return Promise.all([ + this.redis.set(splitKey, split), + this._incrementCounts(parsedSplit), + // If it's an update, we decrement the traffic type of the existing split, + parsedPreviousSplit && this._decrementCounts(parsedPreviousSplit) + ]); + }).then(([status]) => status === 'OK'); + } + + /** + * Add a list of splits. + * The returned promise is resolved when the operation success + * or rejected with an SplitError if it fails (e.g., redis operation fails) + */ + addSplits(entries: [string, string][]): Promise { + return Promise.all(entries.map(keyValuePair => this.addSplit(keyValuePair[0], keyValuePair[1]))) + // @TODO remove if refactoring SDK error handling (it is necessary to distinguish from user cb errors). + .catch((e) => { throw new SplitError(e); }); } /** - * Remove a given split from Redis. Returns the number of deleted keys. + * Remove a given split. + * The returned promise is resolved when the operation success, with 1 or 0 indicating if the split existed or not. + * or rejected if it fails (e.g., redis operation fails). */ - removeSplit(name: string): Promise { - return this.redis.del(this.keys.buildSplitKey(name)); + removeSplit(name: string): Promise { + return this.getSplit(name).then((split) => { + if (split) { + const parsedSplit = JSON.parse(split); + this._decrementCounts(parsedSplit); + } + return this.redis.del(this.keys.buildSplitKey(name)); + }); } /** - * Bulk delete of splits from Redis. Returns the number of deleted keys. + * Remove a list of splits. + * The returned promise is resolved when the operation success, + * or rejected with an SplitError if it fails (e.g., redis operation fails). */ removeSplits(names: string[]): Promise { - if (names.length) { - return this.redis.del(names.map(n => this.keys.buildSplitKey(n))); - } else { - return Promise.resolve(0); - } + return Promise.all(names.map(name => this.removeSplit(name))) + // @TODO remove if refactoring SDK error handling (it is necessary to distinguish from user cb errors). + .catch((e) => { throw new SplitError(e); }); } /** @@ -104,29 +139,38 @@ export default class SplitsCacheInRedis implements ISplitsCacheAsync { /** * Set till number. - * - * @TODO pending error handling + * The returned promise is resolved when the operation success, + * or rejected with an SplitError if it fails. */ setChangeNumber(changeNumber: number): Promise { return this.redis.set(this.keys.buildSplitsTillKey(), changeNumber + '').then( status => status === 'OK' - ); + ) + // @TODO remove if refactoring SDK error handling (it is necessary to distinguish from user cb errors). + .catch((e) => { throw new SplitError(e); }); } /** * Get till number or -1 if it's not defined. - * - * @TODO pending error handling + * The returned promise is resolved with the changeNumber or -1 if it doesn't exist or a redis operation fails. + * The promise will never be rejected. */ getChangeNumber(): Promise { return this.redis.get(this.keys.buildSplitsTillKey()).then((value: string | null) => { const i = parseInt(value as string, 10); return isNaNNumber(i) ? -1 : i; + }).catch((e) => { + this.log.error(LOG_PREFIX + 'Could not retrieve changeNumber from storage. Error: ' + e); + return -1; }); } /** + * Get list of all split definitions. + * The returned promise is resolved with the list of split definitions, + * or rejected if redis operation fails (no need to wrap the error as a SplitError). + * * @TODO we need to benchmark which is the maximun number of commands we could * pipeline without kill redis performance. */ @@ -136,12 +180,23 @@ export default class SplitsCacheInRedis implements ISplitsCacheAsync { ).then(processPipelineAnswer); } + /** + * Get list of split names. + * The returned promise is resolved with the list of split names, + * or rejected if redis operation fails (no need to wrap the error as a SplitError). + */ getSplitNames(): Promise { return this.redis.keys(this.keys.searchPatternForSplitKeys()).then( (listOfKeys) => listOfKeys.map(this.keys.extractKey) ); } + /** + * Check traffic type existence. + * The returned promise is resolved with a boolean indicating whether the TT exist or not. + * In case of redis operation failure, the promise resolves with a true value, assuming that the TT might exist. + * It will never be rejected. + */ trafficTypeExists(trafficType: string): Promise { // If there is a number there should be > 0, otherwise the TT is considered as not existent. return this.redis.get(this.keys.buildTrafficTypeKey(trafficType)) @@ -163,11 +218,6 @@ export default class SplitsCacheInRedis implements ISplitsCacheAsync { }); } - // noop, just keeping the interface. This is used by client-side implementations only. - usesSegments(): Promise { - return Promise.resolve(true); - } - /** * Delete everything in the current database. * @@ -203,11 +253,4 @@ export default class SplitsCacheInRedis implements ISplitsCacheAsync { }); } - /** - * Check if the splits information is already stored in cache. Redis would actually be the cache. - * Noop, just keeping the interface. This is used by client-side implementations only. - */ - checkCache(): Promise { - return Promise.resolve(true); - } } diff --git a/src/storages/inRedis/__tests__/SplitsCacheInRedis.spec.ts b/src/storages/inRedis/__tests__/SplitsCacheInRedis.spec.ts index 1c03268e..831da5f3 100644 --- a/src/storages/inRedis/__tests__/SplitsCacheInRedis.spec.ts +++ b/src/storages/inRedis/__tests__/SplitsCacheInRedis.spec.ts @@ -3,82 +3,132 @@ import SplitsCacheInRedis from '../SplitsCacheInRedis'; import KeyBuilderSS from '../../KeyBuilderSS'; import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; -test('SPLITS CACHE / Redis', async () => { - const connection = new Redis(); - // @ts-expect-error - const keys = new KeyBuilderSS(); - const cache = new SplitsCacheInRedis(loggerMock, keys, connection); +const splitWithUserTT = '{ "trafficTypeName": "user_tt" }'; +const splitWithAccountTT = '{ "trafficTypeName": "account_tt" }'; - await cache.clear(); +describe('SPLITS CACHE REDIS', () => { - await cache.addSplits([ - ['lol1', 'something'], - ['lol2', 'something else'] - ]); + test('add/remove/get splits & set/get change number', async () => { + const connection = new Redis(); + // @ts-expect-error + const keys = new KeyBuilderSS(); + const cache = new SplitsCacheInRedis(loggerMock, keys, connection); - let values = await cache.getAll(); + await cache.clear(); - expect(values.indexOf('something') !== -1).toBe(true); - expect(values.indexOf('something else') !== -1).toBe(true); + await cache.addSplits([ + ['lol1', splitWithUserTT], + ['lol2', splitWithAccountTT] + ]); - let splitNames = await cache.getSplitNames(); + let values = await cache.getAll(); - expect(splitNames.indexOf('lol1') !== -1).toBe(true); - expect(splitNames.indexOf('lol2') !== -1).toBe(true); + expect(values.indexOf(splitWithUserTT) !== -1).toBe(true); + expect(values.indexOf(splitWithAccountTT) !== -1).toBe(true); - await cache.removeSplit('lol1'); + let splitNames = await cache.getSplitNames(); - values = await cache.getAll(); + expect(splitNames.indexOf('lol1') !== -1).toBe(true); + expect(splitNames.indexOf('lol2') !== -1).toBe(true); - expect(values.indexOf('something') === -1).toBe(true); - expect(values.indexOf('something else') !== -1).toBe(true); + await cache.removeSplit('lol1'); - expect(await cache.getSplit('lol1') == null).toBe(true); - expect(await cache.getSplit('lol2') === 'something else').toBe(true); + values = await cache.getAll(); - await cache.setChangeNumber(123); - expect(await cache.getChangeNumber() === 123).toBe(true); + expect(values.indexOf(splitWithUserTT) === -1).toBe(true); + expect(values.indexOf(splitWithAccountTT) !== -1).toBe(true); - splitNames = await cache.getSplitNames(); + expect(await cache.getSplit('lol1') == null).toBe(true); + expect(await cache.getSplit('lol2') === splitWithAccountTT).toBe(true); - expect(splitNames.indexOf('lol1') === -1).toBe(true); - expect(splitNames.indexOf('lol2') !== -1).toBe(true); + await cache.setChangeNumber(123); + expect(await cache.getChangeNumber() === 123).toBe(true); - const splits = await cache.getSplits(['lol1', 'lol2']); - expect(splits['lol1'] === null).toBe(true); - expect(splits['lol2'] === 'something else').toBe(true); + splitNames = await cache.getSplitNames(); - await connection.quit(); -}); + expect(splitNames.indexOf('lol1') === -1).toBe(true); + expect(splitNames.indexOf('lol2') !== -1).toBe(true); + + const splits = await cache.getSplits(['lol1', 'lol2']); + expect(splits['lol1'] === null).toBe(true); + expect(splits['lol2'] === splitWithAccountTT).toBe(true); + + await connection.quit(); + }); + + test('trafficTypeExists', async () => { + const prefix = 'splits_cache_ut'; + const connection = new Redis(); + // @ts-expect-error + const keys = new KeyBuilderSS(prefix); + const cache = new SplitsCacheInRedis(loggerMock, keys, connection); + + const testTTName = 'tt_test_name'; + const testTTNameNoCount = 'tt_test_name_2'; + const testTTNameInvalid = 'tt_test_name_3'; + const ttKey = keys.buildTrafficTypeKey(testTTName); + const ttKeyNoCount = keys.buildTrafficTypeKey(testTTNameNoCount); + const ttKeyInvalid = keys.buildTrafficTypeKey(testTTNameInvalid); + + await cache.clear(); + + await connection.set(ttKey, 3); + await connection.set(ttKeyNoCount, 0); + await connection.set(ttKeyInvalid, 'NaN'); + + expect(await cache.trafficTypeExists(testTTName)).toBe(true); + expect(await cache.trafficTypeExists(testTTNameNoCount)).toBe(false); + expect(await cache.trafficTypeExists(ttKeyInvalid)).toBe(false); + expect(await cache.trafficTypeExists('not_existent_tt')).toBe(false); + + await connection.del(ttKey); + await connection.del(ttKeyNoCount); + await connection.del(ttKeyInvalid); + + await connection.quit(); + }); + + test('killLocally', async () => { + const connection = new Redis(); + // @ts-expect-error + const keys = new KeyBuilderSS(); + const cache = new SplitsCacheInRedis(loggerMock, keys, connection); + + await cache.addSplit('lol1', splitWithUserTT); + await cache.addSplit('lol2', splitWithAccountTT); + const initialChangeNumber = await cache.getChangeNumber(); + + // kill an non-existent split + let updated = await cache.killLocally('nonexistent_split', 'other_treatment', 101); + const nonexistentSplit = await cache.getSplit('nonexistent_split'); + + expect(updated).toBe(false); // killLocally resolves without update if split doesn't exist + expect(nonexistentSplit).toBe(null); // non-existent split keeps being non-existent -test('SPLITS CACHE / Redis / trafficTypeExists tests', async () => { - const prefix = 'splits_cache_ut'; - const connection = new Redis(); - // @ts-expect-error - const keys = new KeyBuilderSS(prefix); - const cache = new SplitsCacheInRedis(loggerMock, keys, connection); + // kill an existent split + updated = await cache.killLocally('lol1', 'some_treatment', 100); + let lol1Split = JSON.parse(await cache.getSplit('lol1') as string); - const testTTName = 'tt_test_name'; - const testTTNameNoCount = 'tt_test_name_2'; - const testTTNameInvalid = 'tt_test_name_3'; - const ttKey = keys.buildTrafficTypeKey(testTTName); - const ttKeyNoCount = keys.buildTrafficTypeKey(testTTNameNoCount); - const ttKeyInvalid = keys.buildTrafficTypeKey(testTTNameInvalid); + expect(updated).toBe(true); // killLocally resolves with update if split is changed + expect(lol1Split.killed).toBe(true); // existing split must be killed + expect(lol1Split.defaultTreatment).toBe('some_treatment'); // existing split must have new default treatment + expect(lol1Split.changeNumber).toBe(100); // existing split must have the given change number + expect(await cache.getChangeNumber()).toBe(initialChangeNumber); // cache changeNumber is not changed - await cache.clear(); + // not update if changeNumber is old + updated = await cache.killLocally('lol1', 'some_treatment_2', 90); + lol1Split = JSON.parse(await cache.getSplit('lol1') as string); - await connection.set(ttKey, 3); - await connection.set(ttKeyNoCount, 0); - await connection.set(ttKeyInvalid, 'NaN'); + expect(updated).toBe(false); // killLocally resolves without update if changeNumber is old + expect(lol1Split.defaultTreatment).not.toBe('some_treatment_2'); // existing split is not updated if given changeNumber is older - expect(await cache.trafficTypeExists(testTTName)).toBe(true); - expect(await cache.trafficTypeExists(testTTNameNoCount)).toBe(false); - expect(await cache.trafficTypeExists(ttKeyInvalid)).toBe(false); - expect(await cache.trafficTypeExists('not_existent_tt')).toBe(false); + // Delete splits and TT keys + await cache.removeSplits(['lol1', 'lol2']); + await connection.del(keys.buildTrafficTypeKey('account_tt')); + await connection.del(keys.buildTrafficTypeKey('user_tt')); + expect(await connection.keys('*')).toHaveLength(0); - await connection.del(ttKey); - await connection.del(ttKeyNoCount); - await connection.del(ttKeyInvalid); + await connection.quit(); + }); - await connection.quit(); }); diff --git a/src/storages/pluggable/SplitsCachePluggable.ts b/src/storages/pluggable/SplitsCachePluggable.ts index 9362aaab..be20b546 100644 --- a/src/storages/pluggable/SplitsCachePluggable.ts +++ b/src/storages/pluggable/SplitsCachePluggable.ts @@ -1,16 +1,16 @@ -import { isFiniteNumber, toNumber, isNaNNumber } from '../../utils/lang'; +import { isFiniteNumber, isNaNNumber } from '../../utils/lang'; import KeyBuilder from '../KeyBuilder'; -import { ICustomStorageWrapper, ISplitsCacheAsync } from '../types'; +import { ICustomStorageWrapper } from '../types'; import { ILogger } from '../../logger/types'; -import { usesSegments } from '../AbstractSplitsCacheSync'; import { ISplit } from '../../dtos/types'; import { LOG_PREFIX } from './constants'; import { SplitError } from '../../utils/lang/errors'; +import AbstractSplitsCacheAsync from '../AbstractSplitsCacheAsync'; /** * ISplitsCacheAsync implementation for pluggable storages. */ -export class SplitsCachePluggable implements ISplitsCacheAsync { +export class SplitsCachePluggable extends AbstractSplitsCacheAsync { private readonly log: ILogger; private readonly keys: KeyBuilder; @@ -23,39 +23,24 @@ export class SplitsCachePluggable implements ISplitsCacheAsync { * @param wrapper Adapted wrapper storage. */ constructor(log: ILogger, keys: KeyBuilder, wrapper: ICustomStorageWrapper) { + super(); this.log = log; this.keys = keys; this.wrapper = wrapper; } private _decrementCounts(split: ISplit) { - const promises = []; if (split.trafficTypeName) { const ttKey = this.keys.buildTrafficTypeKey(split.trafficTypeName); - promises.push(this.wrapper.decr(ttKey)); + return this.wrapper.decr(ttKey); } - - if (usesSegments(split)) { - const segmentsCountKey = this.keys.buildSplitsWithSegmentCountKey(); - promises.push(this.wrapper.decr(segmentsCountKey)); - } - - return Promise.all(promises); } private _incrementCounts(split: ISplit) { - const promises = []; if (split.trafficTypeName) { const ttKey = this.keys.buildTrafficTypeKey(split.trafficTypeName); - promises.push(this.wrapper.incr(ttKey)); + return this.wrapper.incr(ttKey); } - - if (usesSegments(split)) { - const segmentsCountKey = this.keys.buildSplitsWithSegmentCountKey(); - promises.push(this.wrapper.incr(segmentsCountKey)); - } - - return Promise.all(promises); } /** @@ -76,11 +61,11 @@ export class SplitsCachePluggable implements ISplitsCacheAsync { throw new SplitError('Error parsing split definition: ' + e); } - return Promise.all([ - // If it's an update, we decrement the traffic type and segment count of the existing split, - parsedPreviousSplit && this._decrementCounts(parsedPreviousSplit), + return Promise.all([ this.wrapper.set(splitKey, split), - this._incrementCounts(parsedSplit) + this._incrementCounts(parsedSplit), + // If it's an update, we decrement the traffic type and segment count of the existing split, + parsedPreviousSplit && this._decrementCounts(parsedPreviousSplit) ]); }).then(() => true); } @@ -99,7 +84,7 @@ export class SplitsCachePluggable implements ISplitsCacheAsync { * The returned promise is resolved when the operation success, with a boolean indicating if the split existed or not. * or rejected with an SplitError if it fails (e.g., wrapper operation fails). */ - removeSplit(name: string): Promise { + removeSplit(name: string) { return this.getSplit(name).then((split) => { if (split) { const parsedSplit = JSON.parse(split); @@ -114,7 +99,7 @@ export class SplitsCachePluggable implements ISplitsCacheAsync { * The returned promise is resolved when the operation success, with a boolean array indicating if the splits existed or not. * or rejected with an SplitError if it fails (e.g., wrapper operation fails). */ - removeSplits(names: string[]): Promise { + removeSplits(names: string[]): Promise { // @ts-ignore return Promise.all(names.map(name => this.removeSplit(name))); } @@ -197,7 +182,7 @@ export class SplitsCachePluggable implements ISplitsCacheAsync { * The returned promise is resolved when the operation success, * or rejected with an SplitError if it fails (e.g., wrapper operation fails). */ - setChangeNumber(changeNumber: number): Promise { + setChangeNumber(changeNumber: number) { return this.wrapper.set(this.keys.buildSplitsTillKey(), changeNumber + ''); } @@ -217,58 +202,9 @@ export class SplitsCachePluggable implements ISplitsCacheAsync { }); } - // @TODO revisit segment-related methods ('usesSegments', 'getRegisteredSegments', 'registerSegments') - usesSegments(): Promise { - return this.wrapper.get(this.keys.buildSplitsWithSegmentCountKey()) - .then(storedCount => { - const splitsWithSegmentsCount = storedCount ? toNumber(storedCount) : 0; - if (isFiniteNumber(splitsWithSegmentsCount)) { - return splitsWithSegmentsCount > 0; - } else { - return true; // If stored valus is invalid, assume we need them. - } - }).catch(() => true); // If wrapper operation fails, assume we need them. - } - - // @TODO implement for DataLoader/Producer mode + // @TODO implement if required for DataLoader/Producer mode clear(): Promise { return Promise.resolve(true); } - /** - * Check if the splits information is already stored in cache. - * Noop, just keeping the interface. This is used by client-side implementations only. - */ - checkCache(): Promise { - return Promise.resolve(true); - } - - /** - * Kill `name` split and set `defaultTreatment` and `changeNumber`. - * Used for SPLIT_KILL push notifications. - * - * @param {string} name - * @param {string} defaultTreatment - * @param {number} changeNumber - * @returns {Promise} a promise that is resolved once the split kill operation is performed. The fulfillment value is a boolean: `true` if the kill success updating the split or `false` if no split is updated, - * for instance, if the `changeNumber` is old, or if the split is not found (e.g., `/splitchanges` hasn't been fetched yet), or if the storage fails to apply the update. - * The promise will never be rejected. - */ - killLocally(name: string, defaultTreatment: string, changeNumber: number): Promise { - return this.getSplit(name).then(split => { - - if (split) { - const parsedSplit: ISplit = JSON.parse(split); - if (!parsedSplit.changeNumber || parsedSplit.changeNumber < changeNumber) { - parsedSplit.killed = true; - parsedSplit.defaultTreatment = defaultTreatment; - parsedSplit.changeNumber = changeNumber; - const newSplit = JSON.stringify(parsedSplit); - - return this.addSplit(name, newSplit); - } - } - return false; - }).catch(() => false); - } } diff --git a/src/storages/pluggable/__tests__/SplitsCachePluggable.spec.ts b/src/storages/pluggable/__tests__/SplitsCachePluggable.spec.ts index d7b5ff00..c6e56da9 100644 --- a/src/storages/pluggable/__tests__/SplitsCachePluggable.spec.ts +++ b/src/storages/pluggable/__tests__/SplitsCachePluggable.spec.ts @@ -5,7 +5,6 @@ import { wrapperMockFactory } from './wrapper.mock'; const splitWithUserTT = '{ "trafficTypeName": "user_tt" }'; const splitWithAccountTT = '{ "trafficTypeName": "account_tt" }'; -const splitWithAccountTTAndUsesSegments = '{ "trafficTypeName": "account_tt", "conditions": [{ "matcherGroup": { "matchers": [{ "matcherType": "IN_SEGMENT" }]}}] }'; const keysBuilder = new KeyBuilder(); describe('SPLITS CACHE PLUGGABLE', () => { @@ -119,25 +118,6 @@ describe('SPLITS CACHE PLUGGABLE', () => { }); - test('usesSegments', async () => { - const cache = new SplitsCachePluggable(loggerMock, keysBuilder, wrapperMockFactory()); - - await cache.addSplits([['split1', splitWithUserTT], ['split2', splitWithAccountTT],]); - expect(await cache.usesSegments()).toBe(false); // 0 splits using segments - - await cache.addSplit('split3', splitWithAccountTTAndUsesSegments); - expect(await cache.usesSegments()).toBe(true); // 1 split using segments - - await cache.addSplit('split4', splitWithAccountTTAndUsesSegments); - expect(await cache.usesSegments()).toBe(true); // 2 splits using segments - - await cache.removeSplit('split3'); - expect(await cache.usesSegments()).toBe(true); // 1 split using segments - - await cache.removeSplit('split4'); - expect(await cache.usesSegments()).toBe(false); // 0 splits using segments - }); - test('killLocally', async () => { const cache = new SplitsCachePluggable(loggerMock, keysBuilder, wrapperMockFactory()); diff --git a/src/storages/types.ts b/src/storages/types.ts index 85347d09..af4dc526 100644 --- a/src/storages/types.ts +++ b/src/storages/types.ts @@ -3,96 +3,149 @@ import { ILogger } from '../logger/types'; import { SplitIO, ImpressionDTO } from '../types'; /** - * Interface to define a custom wrapper storage. + * Interface of a custom wrapper storage. */ export interface ICustomStorageWrapper { /** - * Return a promise that resolves with the element value associated with the specified `key`, or null if the key can't be found in the storage. + * Get the value of given `key`. + * + * @function get + * @param {string} key item to retrieve + * @returns {Promise} A promise that resolves with the element value associated with the specified `key`, or null if the key does not exist. + * The promise rejects if the operation fails. */ get: (key: string) => Promise /** - * Add or update an element with a specified `key` and `value`. - * Returns a promise that resolves with a boolean value: - * - true if the element existed and was updated, - * - or false if the element didn't exist and was added. + * Add or update an item with a specified `key` and `value`. + * + * @function set + * @param {string} key item to update + * @param {string} value value to set + * @returns {Promise} A promise that resolves if the operation success, whether the key was added or updated. + * The promise rejects if the operation fails. */ - set: (key: string, value: string) => Promise + set: (key: string, value: string) => Promise /** - * Add or update an element with a specified `key` and `value`. - * Returns a promise that resolves with the previous value associated to the given `key`, or null if not set. - * - true if the element existed and was updated, - * - or false if the element didn't exist and was added. + * Add or update an item with a specified `key` and `value`. + * + * @function getAndSet + * @param {string} key item to update + * @param {string} value value to set + * @returns {Promise} A promise that resolves with the previous value associated to the given `key`, or null if not set. + * The promise rejects if the operation fails. */ getAndSet: (key: string, value: string) => Promise /** - * Remove the specified element by `key`. - * Return a promise that resolves with a boolean value: - * - true if the element existed and has been removed, - * - or false if the element does not exist. + * Removes the specified item by `key`. + * + * @function del + * @param {string} key item to delete + * @returns {Promise} A promise that resolves if the operation success, whether the key existed and was removed or it didn't exist. + * The promise rejects if the operation fails, for example, if there is a connectin error. */ - del: (key: string) => Promise + del: (key: string) => Promise /** - * Return a promise that resolves with the list of element keys that match with the given `prefix`. + * Returns all keys matching the given prefix. + * + * @function getKeysByPrefix + * @param {string} prefix string prefix to match + * @returns {Promise} A promise that resolves with the list of keys that match the given `prefix`. + * The promise rejects if the operation fails. */ getKeysByPrefix: (prefix: string) => Promise /** - * Return a promise that resolves with the list of element values that match with the given `prefix`. + * Returns all values which keys match the given prefix. + * + * @function getByPrefix + * @param {string} prefix + * @returns {Promise} A promise that resolves with the list of values which keys match the given `prefix`. + * The promise rejects if the operation fails. */ getByPrefix: (prefix: string) => Promise /** - * Increment in 1 the given `key` value or set it in 1 if the value doesn't exist. - * Return a resolved promise with a boolean value: - * - true if the value was incremented, - * - or false if the value already existed and couldn't be parsed into a finite number. - * @param key + * Increments in 1 the given `key` value or set it in 1 if the value doesn't exist. + * + * @function incr + * @param {string} key key to increment + * @returns {Promise} A promise that resolves if the operation success. + * The promise rejects if the operation fails, for example, if there is a connectin error or the key contains a string that can not be represented as integer. */ - incr: (key: string) => Promise + incr: (key: string) => Promise /** - * Decrement in 1 the given `key` value or set it in -1 if the value doesn't exist. - * Return a promise that resolves with a boolean value: - * - true if the value was decremented, - * - or false if the value already existed and couldn't be parsed into a finite number. - * @param key + * Decrements in 1 the given `key` value or set it in -1 if the value doesn't exist. + * + * @function decr + * @param {string} key key to decrement + * @returns {Promise} A promise that resolves if the operation success. + * The promise rejects if the operation fails, for example, if there is a connectin error or the key contains a string that can not be represented as integer. */ - decr: (key: string) => Promise + decr: (key: string) => Promise /** - * Return a promise that resolves with the list of elements associated with the specified list of `keys`. + * Returns the values of all given `keys`. + * + * @function getMany + * @param {string[]} keys list of keys to retrieve + * @returns {Promise<(string | null)[]>} A promise that resolves with the list of items associated with the specified list of `keys`. For every key that does not hold a string value or does not exist, null is returned. + * The promise rejects if the operation fails. */ getMany: (keys: string[]) => Promise<(string | null)[]> /** - * Push given `items` to `key` list. If key does not exist, an empty list is created for the key before pushing the items. - * Return a promise that resolves if the operation success, - * or rejects if the operation fails, for example, if there is a connectin error or the key holds a value that is not a list. + * Inserts given items at the tail of `key` list. If `key` does not exist, an empty list is created before pushing the items. + * + * @function pushItems + * @param {string} key list key + * @param {string[]} items list of items to push + * @returns {Promise} A promise that resolves if the operation success. + * The promise rejects if the operation fails, for example, if there is a connectin error or the key holds a value that is not a list. */ pushItems: (key: string, items: string[]) => Promise /** - * Pop `count` number of items from `key` queue. - * Return a promise that resolves with the list of removed items the removed members, - * or an empty array when key does not exist or is not a list. + * Removes and returns the first `count` items from a list. If `key` does not exist, an empty list is items is returned. + * + * @function popItems + * @param {string} key list key + * @param {number} count number of items to pop + * @returns {Promise} A promise that resolves with the list of removed items from the list, or an empty array when key does not exist. + * The promise rejects if the operation fails, for example, if there is a connectin error or the key holds a value that is not a list. */ popItems: (key: string, count: number) => Promise /** - * Return a promise that resolves with the number of items at the `key` queue, or 0 when key does not exist or is not a list. + * Returns the count of items in a list, or 0 if `key` does not exist. + * + * @function getItemsCount + * @param {string} key list key + * @returns {Promise} A promise that resolves with the number of items at the `key` list, or 0 when `key` does not exist. + * The promise rejects if the operation fails, for example, if there is a connectin error or the key holds a value that is not a list. */ getItemsCount: (key: string) => Promise /** - * Return a promise that resolves with true boolean value if `item` is a member of the list stored at `key`, - * or false if it is not a member or key does not exist or is not a list. + * Returns if item is a member of a list. + * + * @function itemContains + * @param {string} key list key + * @param {string} item item value + * @returns {Promise} A promise that resolves with true boolean value if `item` is a member of the list stored at `key`, or false if it is not a member or `key` list does not exist. + * The promise rejects if the operation fails, for example, if there is a connectin error or the key holds a value that is not a list. */ itemContains: (key: string, item: string) => Promise - /** - * For storages that requires to be connected, like database servers. - * Return a promise that resolves when the wrapper successfully connect to the underlying storage, - * or rejects if the wrapper fails to connect. - * + * Connects to the underlying storage. + * It is meant for storages that requires to be connected to some database or server. Otherwise it can just return a resolved promise. * Note: will be called once on SplitFactory instantiation. + * + * @function connect + * @returns {Promise} A promise that resolves when the wrapper successfully connect to the underlying storage. + * The promise rejects with the corresponding error if the wrapper fails to connect. */ connect: () => Promise /** - * For storages that requires to be closed, for example, to release resources. + * Disconnects the underlying storage. + * It is meant for storages that requires to be closed, in order to release resources. Otherwise it can just return a resolved promise. + * Note: will be called once on SplitFactory client destroy. * - * Note: will be called once on SplitFactory default client destroy. + * @function close + * @returns {Promise} A promise that resolves when the operation ends. + * The promise never rejects. */ close: () => Promise } @@ -100,55 +153,53 @@ export interface ICustomStorageWrapper { /** Splits cache */ export interface ISplitsCacheBase { - addSplit(name: string, split: string): MaybeThenable, // @TODO remove as in spec - addSplits(entries: [string, string][]): MaybeThenable, - removeSplit(name: string): MaybeThenable, // @TODO remove as in spec - removeSplits(names: string[]): MaybeThenable, + addSplits(entries: [string, string][]): MaybeThenable, + removeSplits(names: string[]): MaybeThenable, getSplit(name: string): MaybeThenable, getSplits(names: string[]): MaybeThenable>, // `fetchMany` in spec - setChangeNumber(changeNumber: number): MaybeThenable, + setChangeNumber(changeNumber: number): MaybeThenable, + // should never reject or throw an exception. Instead return -1 by default, assuming no splits are present in the storage. getChangeNumber(): MaybeThenable, getAll(): MaybeThenable, getSplitNames(): MaybeThenable, + // should never reject or throw an exception. Instead return true by default, asssuming the TT might exist. trafficTypeExists(trafficType: string): MaybeThenable, + // only for Client-Side usesSegments(): MaybeThenable, - clear(): MaybeThenable, + clear(): MaybeThenable, + // should never reject or throw an exception. Instead return false by default, to avoid emitting SDK_READY_FROM_CACHE. checkCache(): MaybeThenable, killLocally(name: string, defaultTreatment: string, changeNumber: number): MaybeThenable } export interface ISplitsCacheSync extends ISplitsCacheBase { - addSplit(name: string, split: string): boolean, - addSplits(entries: [string, string][]): boolean[] - removeSplit(name: string): boolean - removeSplits(names: string[]): boolean[] - getSplit(name: string): string | null - getSplits(names: string[]): Record - setChangeNumber(changeNumber: number): boolean - getChangeNumber(): number - getAll(): string[] - getSplitNames(): string[] - trafficTypeExists(trafficType: string): boolean - usesSegments(): boolean - clear(): void - checkCache(): boolean + addSplits(entries: [string, string][]): boolean[], + removeSplits(names: string[]): boolean[], + getSplit(name: string): string | null, + getSplits(names: string[]): Record, + setChangeNumber(changeNumber: number): boolean, + getChangeNumber(): number, + getAll(): string[], + getSplitNames(): string[], + trafficTypeExists(trafficType: string): boolean, + usesSegments(): boolean, + clear(): void, + checkCache(): boolean, killLocally(name: string, defaultTreatment: string, changeNumber: number): boolean } export interface ISplitsCacheAsync extends ISplitsCacheBase { - addSplit(name: string, split: string): Promise, - addSplits(entries: [string, string][]): Promise, - removeSplit(name: string): Promise, - removeSplits(names: string[]): Promise, + addSplits(entries: [string, string][]): Promise, + removeSplits(names: string[]): Promise, getSplit(name: string): Promise, getSplits(names: string[]): Promise>, - setChangeNumber(changeNumber: number): Promise, + setChangeNumber(changeNumber: number): Promise, getChangeNumber(): Promise, getAll(): Promise, getSplitNames(): Promise, trafficTypeExists(trafficType: string): Promise, usesSegments(): Promise, - clear(): Promise, + clear(): Promise, checkCache(): Promise, killLocally(name: string, defaultTreatment: string, changeNumber: number): Promise } @@ -156,14 +207,14 @@ export interface ISplitsCacheAsync extends ISplitsCacheBase { /** Segments cache */ export interface ISegmentsCacheBase { - addToSegment(name: string, segmentKeys: string[]): MaybeThenable // different signature on Server and Client-Side - removeFromSegment(name: string, segmentKeys: string[]): MaybeThenable // different signature on Server and Client-Side + addToSegment(name: string, segmentKeys: string[]): MaybeThenable // different signature on Server and Client-Side + removeFromSegment(name: string, segmentKeys: string[]): MaybeThenable // different signature on Server and Client-Side isInSegment(name: string, key?: string): MaybeThenable // different signature on Server and Client-Side - registerSegments(names: string[]): MaybeThenable // only for Server-Side + registerSegments(names: string[]): MaybeThenable // only for Server-Side getRegisteredSegments(): MaybeThenable // only for Server-Side - setChangeNumber(name: string, changeNumber: number): MaybeThenable // only for Server-Side + setChangeNumber(name: string, changeNumber: number): MaybeThenable // only for Server-Side getChangeNumber(name: string): MaybeThenable // only for Server-Side - clear(): MaybeThenable + clear(): MaybeThenable } // Same API for both variants: SegmentsCache and MySegmentsCache (client-side API) @@ -180,12 +231,12 @@ export interface ISegmentsCacheSync extends ISegmentsCacheBase { } export interface ISegmentsCacheAsync extends ISegmentsCacheBase { - addToSegment(name: string, segmentKeys: string[]): Promise - removeFromSegment(name: string, segmentKeys: string[]): Promise + addToSegment(name: string, segmentKeys: string[]): Promise + removeFromSegment(name: string, segmentKeys: string[]): Promise isInSegment(name: string, key?: string): Promise - registerSegments(names: string[]): Promise + registerSegments(names: string[]): Promise getRegisteredSegments(): Promise - setChangeNumber(name: string, changeNumber: number): Promise + setChangeNumber(name: string, changeNumber: number): Promise getChangeNumber(name: string): Promise clear(): Promise } diff --git a/src/sync/polling/updaters/splitChangesUpdater.ts b/src/sync/polling/updaters/splitChangesUpdater.ts index 4ba0c51b..4565f0c6 100644 --- a/src/sync/polling/updaters/splitChangesUpdater.ts +++ b/src/sync/polling/updaters/splitChangesUpdater.ts @@ -12,11 +12,12 @@ import { SYNC_SPLITS_FETCH, SYNC_SPLITS_NEW, SYNC_SPLITS_REMOVED, SYNC_SPLITS_SE type ISplitChangesUpdater = (noCache?: boolean) => Promise // Checks that all registered segments have been fetched (changeNumber !== -1 for every segment). +// Returns a promise that could be rejected. // @TODO review together with Segments and MySegments storage APIs -function checkAllSegmentsExist(segmentsStorage: ISegmentsCacheBase): Promise { - let registeredSegments = Promise.resolve(segmentsStorage.getRegisteredSegments()); - return registeredSegments.then(segments => { - return Promise.all(segments.map(segmentName => segmentsStorage.getChangeNumber(segmentName))) +function checkAllSegmentsExist(segments: ISegmentsCacheBase): Promise { + let registeredSegments = Promise.resolve(segments.getRegisteredSegments()); + return registeredSegments.then(segmentNames => { + return Promise.all(segmentNames.map(segmentName => segments.getChangeNumber(segmentName))) .then(changeNumbers => changeNumbers.every(changeNumber => changeNumber !== -1)); }); } @@ -88,8 +89,8 @@ export function computeSplitsMutation(entries: ISplit[]): ISplitMutations { export function splitChangesUpdaterFactory( log: ILogger, splitChangesFetcher: ISplitChangesFetcher, - splitsCache: ISplitsCacheBase, - segmentsCache: ISegmentsCacheBase, + splits: ISplitsCacheBase, + segments: ISegmentsCacheBase, splitsEventEmitter?: ISplitsEventEmitter, requestTimeoutBeforeReady: number = 0, retriesOnFailureBeforeReady: number = 0, @@ -135,16 +136,17 @@ export function splitChangesUpdaterFactory( // @TODO call `setChangeNumber` only if the other storage operations have succeeded, in order to keep storage consistency return Promise.all([ // calling first `setChangenumber` method, to perform cache flush if split filter queryString changed - splitsCache.setChangeNumber(splitChanges.till), - splitsCache.addSplits(mutation.added), - splitsCache.removeSplits(mutation.removed), - segmentsCache.registerSegments(mutation.segments) + splits.setChangeNumber(splitChanges.till), + splits.addSplits(mutation.added), + splits.removeSplits(mutation.removed), + segments.registerSegments(mutation.segments) ]).then(() => { if (splitsEventEmitter) { // To emit SDK_SPLITS_ARRIVED for server-side SDK, we must check that all registered segments have been fetched - Promise.resolve(!splitsEventEmitter.splitsArrived || (since !== splitChanges.till && checkAllSegmentsExist(segmentsCache))) - .then(emitSplitsArrivedEvent => { if (emitSplitsArrivedEvent) splitsEventEmitter.emit(SDK_SPLITS_ARRIVED); }); + Promise.resolve(!splitsEventEmitter.splitsArrived || (since !== splitChanges.till && checkAllSegmentsExist(segments))) + .then(emitSplitsArrivedEvent => { if (emitSplitsArrivedEvent) splitsEventEmitter.emit(SDK_SPLITS_ARRIVED); }) + .catch(() => {/** noop. just to handle a possible `checkAllSegmentsExist` promise rejection on custom or redis storage */ }); } return true; }); @@ -170,12 +172,12 @@ export function splitChangesUpdaterFactory( // After triggering the requests, if we have cached splits information let's notify that. if (splitsEventEmitter && startingUp) { - Promise.resolve(splitsCache.checkCache()).then(cacheReady => { if (cacheReady) splitsEventEmitter.emit(SDK_SPLITS_CACHE_LOADED); }); + Promise.resolve(splits.checkCache()).then(cacheReady => { if (cacheReady) splitsEventEmitter.emit(SDK_SPLITS_CACHE_LOADED); }); } return fetcherPromise; } - let sincePromise = Promise.resolve(splitsCache.getChangeNumber()); + let sincePromise = Promise.resolve(splits.getChangeNumber()); return sincePromise.then(_splitChangesUpdater); }; } From 8d7ca1869df9ac1ea73c20d849bdccc624f07e6c Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 12 May 2021 01:42:21 -0300 Subject: [PATCH 051/144] polishing --- src/storages/__tests__/testUtils.ts | 8 ++++++++ .../inLocalStorage/__tests__/SplitsCacheInLocal.spec.ts | 5 +---- src/storages/inRedis/SplitsCacheInRedis.ts | 2 +- src/storages/inRedis/__tests__/SplitsCacheInRedis.spec.ts | 4 +--- src/storages/pluggable/SplitsCachePluggable.ts | 4 ++-- .../pluggable/__tests__/SplitsCachePluggable.spec.ts | 3 +-- src/storages/pluggable/__tests__/index.spec.ts | 2 +- 7 files changed, 15 insertions(+), 13 deletions(-) diff --git a/src/storages/__tests__/testUtils.ts b/src/storages/__tests__/testUtils.ts index 8bee84de..b09e9c1b 100644 --- a/src/storages/__tests__/testUtils.ts +++ b/src/storages/__tests__/testUtils.ts @@ -11,3 +11,11 @@ export function assertStorageInterface(storage: IStorageSync | IStorageAsync) { expect(!storage.counts || typeof storage.counts === 'object').toBeTruthy; expect(!storage.impressionCounts || typeof storage.impressionCounts === 'object').toBeTruthy; } + +// Split mocks + +export const splitWithUserTT = '{ "trafficTypeName": "user_tt", "conditions": [] }'; + +export const splitWithAccountTT = '{ "trafficTypeName": "account_tt", "conditions": [] }'; + +export const splitWithAccountTTAndUsesSegments = '{ "trafficTypeName": "account_tt", "conditions": [{ "matcherGroup": { "matchers": [{ "matcherType": "IN_SEGMENT", "userDefinedSegmentMatcherData": { "segmentName": "employees" } }]}}] }'; diff --git a/src/storages/inLocalStorage/__tests__/SplitsCacheInLocal.spec.ts b/src/storages/inLocalStorage/__tests__/SplitsCacheInLocal.spec.ts index 99edffd3..0f3172f7 100644 --- a/src/storages/inLocalStorage/__tests__/SplitsCacheInLocal.spec.ts +++ b/src/storages/inLocalStorage/__tests__/SplitsCacheInLocal.spec.ts @@ -1,10 +1,7 @@ import SplitsCacheInLocal from '../SplitsCacheInLocal'; import KeyBuilderCS from '../../KeyBuilderCS'; import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; - -const splitWithUserTT = '{ "trafficTypeName": "user_tt" }'; -const splitWithAccountTT = '{ "trafficTypeName": "account_tt" }'; -const splitWithAccountTTAndUsesSegments = '{ "trafficTypeName": "account_tt", "conditions": [{ "matcherGroup": { "matchers": [{ "matcherType": "IN_SEGMENT" }]}}] }'; +import { splitWithUserTT, splitWithAccountTT, splitWithAccountTTAndUsesSegments } from '../../__tests__/testUtils'; test('SPLIT CACHE / LocalStorage', () => { const cache = new SplitsCacheInLocal(loggerMock, new KeyBuilderCS('SPLITIO', 'user')); diff --git a/src/storages/inRedis/SplitsCacheInRedis.ts b/src/storages/inRedis/SplitsCacheInRedis.ts index 8b21ed01..008a2bc2 100644 --- a/src/storages/inRedis/SplitsCacheInRedis.ts +++ b/src/storages/inRedis/SplitsCacheInRedis.ts @@ -223,7 +223,7 @@ export default class SplitsCacheInRedis extends AbstractSplitsCacheAsync { * * @NOTE documentation says it never fails. */ - clear(): Promise { + clear() { return this.redis.flushdb().then(status => status === 'OK'); } diff --git a/src/storages/inRedis/__tests__/SplitsCacheInRedis.spec.ts b/src/storages/inRedis/__tests__/SplitsCacheInRedis.spec.ts index 831da5f3..b8dbb448 100644 --- a/src/storages/inRedis/__tests__/SplitsCacheInRedis.spec.ts +++ b/src/storages/inRedis/__tests__/SplitsCacheInRedis.spec.ts @@ -2,9 +2,7 @@ import Redis from 'ioredis'; import SplitsCacheInRedis from '../SplitsCacheInRedis'; import KeyBuilderSS from '../../KeyBuilderSS'; import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; - -const splitWithUserTT = '{ "trafficTypeName": "user_tt" }'; -const splitWithAccountTT = '{ "trafficTypeName": "account_tt" }'; +import { splitWithUserTT, splitWithAccountTT } from '../../__tests__/testUtils'; describe('SPLITS CACHE REDIS', () => { diff --git a/src/storages/pluggable/SplitsCachePluggable.ts b/src/storages/pluggable/SplitsCachePluggable.ts index be20b546..34450416 100644 --- a/src/storages/pluggable/SplitsCachePluggable.ts +++ b/src/storages/pluggable/SplitsCachePluggable.ts @@ -202,8 +202,8 @@ export class SplitsCachePluggable extends AbstractSplitsCacheAsync { }); } - // @TODO implement if required for DataLoader/Producer mode - clear(): Promise { + // @TODO implement if required by DataLoader or producer mode + clear() { return Promise.resolve(true); } diff --git a/src/storages/pluggable/__tests__/SplitsCachePluggable.spec.ts b/src/storages/pluggable/__tests__/SplitsCachePluggable.spec.ts index c6e56da9..1d275147 100644 --- a/src/storages/pluggable/__tests__/SplitsCachePluggable.spec.ts +++ b/src/storages/pluggable/__tests__/SplitsCachePluggable.spec.ts @@ -2,9 +2,8 @@ import { SplitsCachePluggable } from '../SplitsCachePluggable'; import KeyBuilder from '../../KeyBuilder'; import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; import { wrapperMockFactory } from './wrapper.mock'; +import { splitWithUserTT, splitWithAccountTT } from '../../__tests__/testUtils'; -const splitWithUserTT = '{ "trafficTypeName": "user_tt" }'; -const splitWithAccountTT = '{ "trafficTypeName": "account_tt" }'; const keysBuilder = new KeyBuilder(); describe('SPLITS CACHE PLUGGABLE', () => { diff --git a/src/storages/pluggable/__tests__/index.spec.ts b/src/storages/pluggable/__tests__/index.spec.ts index 3f49e33a..8356a3ad 100644 --- a/src/storages/pluggable/__tests__/index.spec.ts +++ b/src/storages/pluggable/__tests__/index.spec.ts @@ -5,7 +5,7 @@ import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; import { IStorageFactoryParams } from '../../types'; import { wrapperMock, wrapperMockFactory } from './wrapper.mock'; -const metadata = { s: 'version', i: 'ip', n: 'hostname' }; +const metadata = { s: 'version', i: 'ip', n: 'hostname' }; const prefix = 'some_prefix'; // Test target From d9397d764ffc4ca4648e228b3f5ec2ed0a735132 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 12 May 2021 12:08:22 -0300 Subject: [PATCH 052/144] polishing --- src/storages/AbstractSplitsCacheAsync.ts | 2 +- src/storages/KeyBuilder.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/storages/AbstractSplitsCacheAsync.ts b/src/storages/AbstractSplitsCacheAsync.ts index 86ccf3d6..e213b9a3 100644 --- a/src/storages/AbstractSplitsCacheAsync.ts +++ b/src/storages/AbstractSplitsCacheAsync.ts @@ -20,7 +20,7 @@ export default abstract class AbstractSplitsCacheAsync implements ISplitsCacheAs abstract clear(): Promise // @TODO revisit segment-related methods ('usesSegments', 'getRegisteredSegments', 'registerSegments') - // noop, just keeping the interface. This is used by client-side API only, and so only implemented by InLocalStorage. + // noop, just keeping the interface. This is used by standalone client-side API only, and so only implemented by InMemory and InLocalStorage. usesSegments(): Promise { return Promise.resolve(true); } diff --git a/src/storages/KeyBuilder.ts b/src/storages/KeyBuilder.ts index 16a649e0..5684d609 100644 --- a/src/storages/KeyBuilder.ts +++ b/src/storages/KeyBuilder.ts @@ -37,7 +37,7 @@ export default class KeyBuilder { return `${this.prefix}.split.`; } - // @TODO remove, since it is not set by Synchronizer. ATM, only used by InLocalStorage. + // Only used by InLocalStorage. buildSplitsWithSegmentCountKey() { return `${this.prefix}.splits.usingSegments`; } From f443d5f66e90268c9cc0c111d75ba48b6f85d573 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 12 May 2021 19:17:35 -0300 Subject: [PATCH 053/144] implementation based on registerSegments key approach --- .../__tests__/SegmentsCacheInMemory.spec.ts | 54 +++++++------ src/storages/inRedis/SegmentsCacheInRedis.ts | 20 +++-- .../__tests__/ImpressionsCacheInRedis.spec.ts | 1 + .../__tests__/SegmentsCacheInRedis.spec.ts | 69 ++++++++-------- src/storages/inRedis/index.ts | 2 +- .../pluggable/SegmentsCachePluggable.ts | 78 ++++++++++++------- .../__tests__/EventsCachePluggable.spec.ts | 2 +- .../ImpressionsCachePluggable.spec.ts | 4 +- .../__tests__/SegmentsCachePluggable.spec.ts | 40 +++++++--- .../pluggable/__tests__/wrapper.mock.ts | 35 ++++++++- .../__tests__/wrapperAdapter.spec.ts | 12 ++- src/storages/pluggable/wrapperAdapter.ts | 3 + src/storages/types.ts | 72 +++++++++++++---- .../polling/updaters/segmentChangesUpdater.ts | 33 ++++---- .../polling/updaters/splitChangesUpdater.ts | 4 +- 15 files changed, 290 insertions(+), 139 deletions(-) diff --git a/src/storages/inMemory/__tests__/SegmentsCacheInMemory.spec.ts b/src/storages/inMemory/__tests__/SegmentsCacheInMemory.spec.ts index c332c912..70ca6b12 100644 --- a/src/storages/inMemory/__tests__/SegmentsCacheInMemory.spec.ts +++ b/src/storages/inMemory/__tests__/SegmentsCacheInMemory.spec.ts @@ -1,33 +1,43 @@ import SegmentsCache from '../SegmentsCacheInMemory'; -test('SEGMENTS CACHE / in memory', () => { - const cache = new SegmentsCache(); +describe('SEGMENTS CACHE IN MEMORY', () => { - cache.addToSegment('mocked-segment', [ - 'a', 'b', 'c' - ]); + test('isInSegment, set/getChangeNumber, add/removeFromSegment', () => { + const cache = new SegmentsCache(); - cache.setChangeNumber('mocked-segment', 1); + cache.addToSegment('mocked-segment', [ + 'a', 'b', 'c' + ]); - cache.removeFromSegment('mocked-segment', [ - 'd' - ]); + cache.setChangeNumber('mocked-segment', 1); - expect(cache.getChangeNumber('mocked-segment') === 1).toBe(true); + cache.removeFromSegment('mocked-segment', ['d']); - cache.addToSegment('mocked-segment', [ - 'd', 'e' - ]); + expect(cache.getChangeNumber('mocked-segment') === 1).toBe(true); - cache.removeFromSegment('mocked-segment', [ - 'a', 'c' - ]); + cache.addToSegment('mocked-segment', ['d', 'e']); - expect(cache.getChangeNumber('mocked-segment') === 1).toBe(true); + cache.removeFromSegment('mocked-segment', ['a', 'c']); + + expect(cache.getChangeNumber('mocked-segment') === 1).toBe(true); + + expect(cache.isInSegment('mocked-segment', 'a')).toBe(false); + expect(cache.isInSegment('mocked-segment', 'b')).toBe(true); // b + expect(cache.isInSegment('mocked-segment', 'c')).toBe(false); // c + expect(cache.isInSegment('mocked-segment', 'd')).toBe(true); // d + expect(cache.isInSegment('mocked-segment', 'e')).toBe(true); // e + }); + + test('registerSegment / getRegisteredSegments', async () => { + const cache = new SegmentsCache(); + + await cache.registerSegments(['s1']); + await cache.registerSegments(['s2']); + await cache.registerSegments(['s2', 's3', 's4']); + + const segments = cache.getRegisteredSegments(); + + ['s1', 's2', 's3', 's4'].forEach(s => expect(segments.indexOf(s) !== -1).toBe(true)); + }); - expect(cache.isInSegment('mocked-segment', 'a')).toBe(false); - expect(cache.isInSegment('mocked-segment', 'b')).toBe(true); // b - expect(cache.isInSegment('mocked-segment', 'c')).toBe(false); // c - expect(cache.isInSegment('mocked-segment', 'd')).toBe(true); // d - expect(cache.isInSegment('mocked-segment', 'e')).toBe(true); // e }); diff --git a/src/storages/inRedis/SegmentsCacheInRedis.ts b/src/storages/inRedis/SegmentsCacheInRedis.ts index 73955761..5cb88656 100644 --- a/src/storages/inRedis/SegmentsCacheInRedis.ts +++ b/src/storages/inRedis/SegmentsCacheInRedis.ts @@ -1,14 +1,19 @@ import { Redis } from 'ioredis'; +import { ILogger } from '../../logger/types'; import { isNaNNumber } from '../../utils/lang'; +import { SplitError } from '../../utils/lang/errors'; +import { LOG_PREFIX } from '../inLocalStorage/constants'; import KeyBuilderSS from '../KeyBuilderSS'; import { ISegmentsCacheAsync } from '../types'; export default class SegmentsCacheInRedis implements ISegmentsCacheAsync { + private readonly log: ILogger; private readonly redis: Redis; private readonly keys: KeyBuilderSS; - constructor(keys: KeyBuilderSS, redis: Redis) { + constructor(log: ILogger, keys: KeyBuilderSS, redis: Redis) { + this.log = log; this.redis = redis; this.keys = keys; } @@ -50,14 +55,12 @@ export default class SegmentsCacheInRedis implements ISegmentsCacheAsync { const i = parseInt(value as string, 10); return isNaNNumber(i) ? -1 : i; + }).catch((e) => { + this.log.error(LOG_PREFIX + 'Could not retrieve changeNumber from segments storage. Error: ' + e); + return -1; }); } - // @TODO remove: not used and not part of interface - registerSegment(segment: string) { - return this.registerSegments([segment]); - } - registerSegments(segments: string[]) { if (segments.length) { return this.redis.sadd(this.keys.buildRegisteredSegmentsKey(), segments).then(() => true); @@ -67,9 +70,12 @@ export default class SegmentsCacheInRedis implements ISegmentsCacheAsync { } getRegisteredSegments() { - return this.redis.smembers(this.keys.buildRegisteredSegmentsKey()); + return this.redis.smembers(this.keys.buildRegisteredSegmentsKey()) + // @TODO remove if refactoring SDK error handling (it is necessary to distinguish from user cb errors). + .catch((e) => { throw new SplitError(e); }); } + // @TODO remove/review. It is not being used. clear() { return this.redis.flushdb().then(status => status === 'OK'); } diff --git a/src/storages/inRedis/__tests__/ImpressionsCacheInRedis.spec.ts b/src/storages/inRedis/__tests__/ImpressionsCacheInRedis.spec.ts index 36892885..6dc6c8ef 100644 --- a/src/storages/inRedis/__tests__/ImpressionsCacheInRedis.spec.ts +++ b/src/storages/inRedis/__tests__/ImpressionsCacheInRedis.spec.ts @@ -95,6 +95,7 @@ test('IMPRESSIONS CACHE IN REDIS / should not resolve track before calling expir // @ts-expect-error c.track([i1, i2]).then(() => { + connection.del(impressionsKey); connection.quit(); // Try to disconnect right away. expect(spy1).toBeCalled(); // Redis rpush was called once before executing external callback. // Following assertion fails if the expire takes place after disconnected and throws unhandledPromiseRejection diff --git a/src/storages/inRedis/__tests__/SegmentsCacheInRedis.spec.ts b/src/storages/inRedis/__tests__/SegmentsCacheInRedis.spec.ts index 57f490e4..dc3ea4b8 100644 --- a/src/storages/inRedis/__tests__/SegmentsCacheInRedis.spec.ts +++ b/src/storages/inRedis/__tests__/SegmentsCacheInRedis.spec.ts @@ -1,54 +1,59 @@ import Redis from 'ioredis'; import SegmentsCacheInRedis from '../SegmentsCacheInRedis'; import KeyBuilderSS from '../../KeyBuilderSS'; +import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; -test('SEGMENTS CACHE IN Redis / suite', async () => { - const connection = new Redis(); - // @ts-expect-error - const keys = new KeyBuilderSS(); - const cache = new SegmentsCacheInRedis(keys, connection); +// @ts-expect-error. Doesn't require metadata +const keys = new KeyBuilderSS(); - await cache.clear(); +describe('SEGMENTS CACHE IN REDIS', () => { - await cache.addToSegment('mocked-segment', ['a', 'b', 'c']); + test('isInSegment, set/getChangeNumber, add/removeFromSegment', async () => { + const connection = new Redis(); + const cache = new SegmentsCacheInRedis(loggerMock, keys, connection); - await cache.setChangeNumber('mocked-segment', 1); + await cache.clear(); - await cache.removeFromSegment('mocked-segment', ['d']); + await cache.addToSegment('mocked-segment', ['a', 'b', 'c']); - expect(await cache.getChangeNumber('mocked-segment') === 1).toBe(true); + await cache.setChangeNumber('mocked-segment', 1); - await cache.addToSegment('mocked-segment', ['d', 'e']); + await cache.removeFromSegment('mocked-segment', ['d']); - await cache.removeFromSegment('mocked-segment', ['a', 'c']); + expect(await cache.getChangeNumber('mocked-segment') === 1).toBe(true); - expect(await cache.getChangeNumber('mocked-segment') === 1).toBe(true); + expect(await cache.getChangeNumber('inexistent-segment')).toBe(-1); // -1 if the segment doesn't exist - expect(await cache.isInSegment('mocked-segment', 'a')).toBe(false); - expect(await cache.isInSegment('mocked-segment', 'b')).toBe(true); - expect(await cache.isInSegment('mocked-segment', 'c')).toBe(false); - expect(await cache.isInSegment('mocked-segment', 'd')).toBe(true); - expect(await cache.isInSegment('mocked-segment', 'e')).toBe(true); + await cache.addToSegment('mocked-segment', ['d', 'e']); - await connection.quit(); -}); + await cache.removeFromSegment('mocked-segment', ['a', 'c']); + + expect(await cache.getChangeNumber('mocked-segment') === 1).toBe(true); + + expect(await cache.isInSegment('mocked-segment', 'a')).toBe(false); + expect(await cache.isInSegment('mocked-segment', 'b')).toBe(true); + expect(await cache.isInSegment('mocked-segment', 'c')).toBe(false); + expect(await cache.isInSegment('mocked-segment', 'd')).toBe(true); + expect(await cache.isInSegment('mocked-segment', 'e')).toBe(true); + + await connection.quit(); + }); -test('SEGMENTS CACHE IN Redis / register segments', async () => { - const connection = new Redis(); - // @ts-expect-error - const keys = new KeyBuilderSS(); + test('registerSegment / getRegisteredSegments', async () => { + const connection = new Redis(); + const cache = new SegmentsCacheInRedis(loggerMock, keys, connection); - const cache = new SegmentsCacheInRedis(keys, connection); + await cache.clear(); - await cache.clear(); + await cache.registerSegments(['s1']); + await cache.registerSegments(['s2']); + await cache.registerSegments(['s2', 's3', 's4']); - await cache.registerSegment('s1'); - await cache.registerSegment('s2'); - await cache.registerSegments(['s2', 's3', 's4']); + const segments = await cache.getRegisteredSegments(); - const segments = await cache.getRegisteredSegments(); + ['s1', 's2', 's3', 's4'].forEach(s => expect(segments.indexOf(s) !== -1).toBe(true)); - ['s1', 's2', 's3', 's4'].forEach(s => expect(segments.indexOf(s) !== -1).toBe(true)); + await connection.quit(); + }); - await connection.quit(); }); diff --git a/src/storages/inRedis/index.ts b/src/storages/inRedis/index.ts index 2ab76479..d0bc35fc 100644 --- a/src/storages/inRedis/index.ts +++ b/src/storages/inRedis/index.ts @@ -33,7 +33,7 @@ export function InRedisStorage(options: InRedisStorageOptions = {}) { return { splits: new SplitsCacheInRedis(log, keys, redisClient), - segments: new SegmentsCacheInRedis(keys, redisClient), + segments: new SegmentsCacheInRedis(log, keys, redisClient), impressions: new ImpressionsCacheInRedis(keys, redisClient, metadata), events: new EventsCacheInRedis(log, keys, redisClient, metadata), latencies: new LatenciesCacheInRedis(keys, redisClient), diff --git a/src/storages/pluggable/SegmentsCachePluggable.ts b/src/storages/pluggable/SegmentsCachePluggable.ts index 612b5ff0..502b5b3e 100644 --- a/src/storages/pluggable/SegmentsCachePluggable.ts +++ b/src/storages/pluggable/SegmentsCachePluggable.ts @@ -1,10 +1,11 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable no-unused-vars */ import { isNaNNumber } from '../../utils/lang'; -import KeyBuilder from '../KeyBuilder'; +import KeyBuilderSS from '../KeyBuilderSS'; import { ICustomStorageWrapper, ISegmentsCacheAsync } from '../types'; import { ILogger } from '../../logger/types'; import { LOG_PREFIX } from './constants'; +import { setToArray, _Set } from '../../utils/lang/sets'; /** * ISegmentsCacheAsync implementation for pluggable storages. @@ -12,17 +13,48 @@ import { LOG_PREFIX } from './constants'; export class SegmentsCachePluggable implements ISegmentsCacheAsync { private readonly log: ILogger; - private readonly keys: KeyBuilder; + private readonly keys: KeyBuilderSS; private readonly wrapper: ICustomStorageWrapper; - constructor(log: ILogger, keys: KeyBuilder, wrapper: ICustomStorageWrapper) { + constructor(log: ILogger, keys: KeyBuilderSS, wrapper: ICustomStorageWrapper) { this.log = log; this.keys = keys; this.wrapper = wrapper; } /** - * Returns if `key` is part of `name` segment. + * Add a list of `segmentKeys` to the given segment `name`. + * The returned promise is resolved when the operation success + * or rejected with an SplitError if wrapper operation fails. + */ + addToSegment(name: string, segmentKeys: string[]) { + const segmentKey = this.keys.buildSegmentNameKey(name); + + if (segmentKeys.length) { + return this.wrapper.addItems(segmentKey, segmentKeys); + } else { + return Promise.resolve(); + } + } + + /** + * Remove a list of `segmentKeys` from the given segment `name`. + * The returned promise is resolved when the operation success + * or rejected with an SplitError if wrapper operation fails. + */ + removeFromSegment(name: string, segmentKeys: string[]) { + const segmentKey = this.keys.buildSegmentNameKey(name); + + if (segmentKeys.length) { + return this.wrapper.removeItems(segmentKey, segmentKeys); + } else { + return Promise.resolve(); + } + } + + /** + * Returns a promise that resolves with a boolean value indicating if `key` is part of `name` segment. + * Promise can be rejected with an SplitError if wrapper operation fails. */ isInSegment(name: string, key: string) { return this.wrapper.itemContains(this.keys.buildSegmentNameKey(name), key); @@ -47,6 +79,7 @@ export class SegmentsCachePluggable implements ISegmentsCacheAsync { getChangeNumber(name: string) { return this.wrapper.get(this.keys.buildSegmentTillKey(name)).then((value: string | null) => { const i = parseInt(value as string, 10); + return isNaNNumber(i) ? -1 : i; }).catch((e) => { this.log.error(LOG_PREFIX + 'Could not retrieve changeNumber from segments storage. Error: ' + e); @@ -55,38 +88,27 @@ export class SegmentsCachePluggable implements ISegmentsCacheAsync { } /** - * Add a list of `segmentKeys` to the given segment `name`. - * The returned promise is resolved when the operation success - * or rejected with an SplitError if it fails (e.g., wrapper operation fails, JSON parsing error) - * - * @TODO implement for DataLoader/Producer mode + * Add the given segment names to the set of registered segments. + * The returned promise is resolved when the operation success, + * or rejected with an SplitError if it fails (e.g., wrapper operation fails). */ - addToSegment(name: string, segmentKeys: string[]) { - return Promise.resolve(true); + registerSegments(segments: string[]) { + if (segments.length) { + return this.wrapper.addItems(this.keys.buildRegisteredSegmentsKey(), segments); + } else { + return Promise.resolve(); + } } /** - * Remove a list of `segmentKeys` from the given segment `name`. - * The returned promise is resolved when the operation success - * or rejected with an SplitError if it fails (e.g., wrapper operation fails, JSON parsing error) - * - * @TODO implement for DataLoader/Producer mode + * Returns a promise that resolves with the set of registered segments in a list, + * or rejected with an SplitError if it fails (e.g., wrapper operation fails). */ - removeFromSegment(name: string, segmentKeys: string[]) { - return Promise.resolve(true); - } - - /** @TODO implement for DataLoader/Producer mode */ - registerSegments(names: string[]) { - return Promise.resolve(true); - } - - /** @TODO implement for DataLoader/Producer mode */ getRegisteredSegments() { - return Promise.resolve([]); + return this.wrapper.getItems(this.keys.buildRegisteredSegmentsKey()); } - /** @TODO implement for DataLoader/Producer mode */ + /** @TODO implement if required by DataLoader or Producer mode */ clear(): Promise { return Promise.resolve(true); } diff --git a/src/storages/pluggable/__tests__/EventsCachePluggable.spec.ts b/src/storages/pluggable/__tests__/EventsCachePluggable.spec.ts index c64ab417..ea306a08 100644 --- a/src/storages/pluggable/__tests__/EventsCachePluggable.spec.ts +++ b/src/storages/pluggable/__tests__/EventsCachePluggable.spec.ts @@ -28,7 +28,7 @@ describe('PLUGGABLE EVENTS CACHE', () => { // @ts-expect-error expect(await cache.track(fakeEvent3)).toBe(true); // If the queueing operation was successful, it should resolve the returned promise with "true" - const values = wrapperMock._cache[key]; + const values = wrapperMock._cache[key] as string[]; expect(values.length).toBe(3); // After pushing we should have as many events as we have stored. expect(typeof values[0]).toBe('string'); // All elements should be strings since those are stringified JSONs. diff --git a/src/storages/pluggable/__tests__/ImpressionsCachePluggable.spec.ts b/src/storages/pluggable/__tests__/ImpressionsCachePluggable.spec.ts index f8a89a8c..699bd7e2 100644 --- a/src/storages/pluggable/__tests__/ImpressionsCachePluggable.spec.ts +++ b/src/storages/pluggable/__tests__/ImpressionsCachePluggable.spec.ts @@ -50,12 +50,12 @@ describe('PLUGGABLE IMPRESSIONS CACHE', () => { expect(await cache.track([o1])).toBe(true); // result should resolve to true - let state = wrapperMock._cache[impressionsKey]; + let state = wrapperMock._cache[impressionsKey] as string[]; expect(state.length).toBe(1); // impression should be stored expect(await cache.track([o2, o3])).toBe(true); - state = wrapperMock._cache[impressionsKey]; + state = wrapperMock._cache[impressionsKey] as string[]; expect(state.length).toBe(3); // This is testing both the track and the toJSON method. expect(state[0]).toBe(JSON.stringify({ diff --git a/src/storages/pluggable/__tests__/SegmentsCachePluggable.spec.ts b/src/storages/pluggable/__tests__/SegmentsCachePluggable.spec.ts index ad95f3ff..d0ec3a56 100644 --- a/src/storages/pluggable/__tests__/SegmentsCachePluggable.spec.ts +++ b/src/storages/pluggable/__tests__/SegmentsCachePluggable.spec.ts @@ -1,9 +1,10 @@ import { SegmentsCachePluggable } from '../SegmentsCachePluggable'; -import KeyBuilder from '../../KeyBuilder'; +import KeyBuilderSS from '../../KeyBuilderSS'; import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; import { wrapperMock } from './wrapper.mock'; -const keyBuilder = new KeyBuilder(); +// @ts-ignore. Doesn't require metadata +const keyBuilder = new KeyBuilderSS(); describe('SEGMENTS CACHE PLUGGABLE', () => { @@ -12,25 +13,44 @@ describe('SEGMENTS CACHE PLUGGABLE', () => { wrapperMock.mockClear(); }); - test('isInSegment, set/getChangeNumber', async () => { + test('isInSegment, set/getChangeNumber, add/removeFromSegment', async () => { const cache = new SegmentsCachePluggable(loggerMock, keyBuilder, wrapperMock); - // assert setChangeNumber and getChangeNumber - expect(await cache.setChangeNumber('mocked-segment', 100)).toBe(false); - expect(await cache.getChangeNumber('mocked-segment')).toBe(100); - expect(await cache.setChangeNumber('mocked-segment', 200)).toBe(true); - expect(await cache.getChangeNumber('mocked-segment')).toBe(200); + await cache.addToSegment('mocked-segment', ['a', 'b', 'c']); + + await cache.setChangeNumber('mocked-segment', 1); + + await cache.removeFromSegment('mocked-segment', ['d']); + + expect(await cache.getChangeNumber('mocked-segment') === 1).toBe(true); + expect(await cache.getChangeNumber('inexistent-segment')).toBe(-1); // -1 if the segment doesn't exist - // mock segment keys - wrapperMock._cache[keyBuilder.buildSegmentNameKey('mocked-segment')] = ['b', 'd']; + await cache.addToSegment('mocked-segment', ['d', 'e']); + + await cache.removeFromSegment('mocked-segment', ['a', 'c']); + + expect(await cache.getChangeNumber('mocked-segment') === 1).toBe(true); expect(await cache.isInSegment('mocked-segment', 'a')).toBe(false); expect(await cache.isInSegment('mocked-segment', 'b')).toBe(true); expect(await cache.isInSegment('mocked-segment', 'c')).toBe(false); expect(await cache.isInSegment('mocked-segment', 'd')).toBe(true); + expect(await cache.isInSegment('mocked-segment', 'e')).toBe(true); expect(await cache.isInSegment('inexistent-segment', 'a')).toBe(false); }); + test('registerSegment / getRegisteredSegments', async () => { + const cache = new SegmentsCachePluggable(loggerMock, keyBuilder, wrapperMock); + + await cache.registerSegments(['s1']); + await cache.registerSegments(['s2']); + await cache.registerSegments(['s2', 's3', 's4']); + + const segments = await cache.getRegisteredSegments(); + + ['s1', 's2', 's3', 's4'].forEach(s => expect(segments.indexOf(s) !== -1).toBe(true)); + }); + }); diff --git a/src/storages/pluggable/__tests__/wrapper.mock.ts b/src/storages/pluggable/__tests__/wrapper.mock.ts index 59390561..f90c9977 100644 --- a/src/storages/pluggable/__tests__/wrapper.mock.ts +++ b/src/storages/pluggable/__tests__/wrapper.mock.ts @@ -4,7 +4,7 @@ import { startsWith, toNumber } from '../../../utils/lang'; export function wrapperMockFactory() { /** Holds items and list of items */ - let _cache: Record = {}; + let _cache: Record> = {}; return { _cache, @@ -74,8 +74,34 @@ export function wrapperMockFactory() { return Promise.resolve(Array.isArray(list) ? list.length : 0); }), itemContains: jest.fn((key: string, item: string) => { - const list = _cache[key]; - return Promise.resolve(Array.isArray(list) && list.indexOf(item) > -1 ? true : false); + const set = _cache[key]; + if (!set) return Promise.resolve(false); + if (set instanceof Set) return Promise.resolve(set.has(item)); + return Promise.reject('key is not a set'); + }), + addItems: jest.fn((key: string, items: string[]) => { + if (!(key in _cache)) _cache[key] = new Set(); + const set = _cache[key]; + if (set instanceof Set) { + items.forEach(item => set.add(item)); + return Promise.resolve(); + } + return Promise.reject('key is not a set'); + }), + removeItems: jest.fn((key: string, items: string[]) => { + if (!(key in _cache)) _cache[key] = new Set(); + const set = _cache[key]; + if (set instanceof Set) { + items.forEach(item => set.delete(item)); + return Promise.resolve(); + } + return Promise.reject('key is not a set'); + }), + getItems: jest.fn((key: string) => { + const set = _cache[key]; + if (!set) return Promise.resolve([]); + if (set instanceof Set) return Promise.resolve(Array.from(set)); + return Promise.reject('key is not a set'); }), // always connects and close @@ -99,6 +125,9 @@ export function wrapperMockFactory() { this.popItems.mockClear(); this.getItemsCount.mockClear(); this.itemContains.mockClear(); + this.addItems.mockClear(); + this.removeItems.mockClear(); + this.getItems.mockClear(); } }; } diff --git a/src/storages/pluggable/__tests__/wrapperAdapter.spec.ts b/src/storages/pluggable/__tests__/wrapperAdapter.spec.ts index d8f62ecc..02b27a4d 100644 --- a/src/storages/pluggable/__tests__/wrapperAdapter.spec.ts +++ b/src/storages/pluggable/__tests__/wrapperAdapter.spec.ts @@ -36,6 +36,9 @@ export const wrapperWithIssues = { popItems: invalidThenable, getItemsCount: 'invalid value', itemContains: throwsException, + addItems: { then: 10 }, + removeItems: invalidThenable, + getItems: { then: () => { throw new Error(); } } }; const VALID_METHOD_CALLS = { @@ -51,9 +54,12 @@ const VALID_METHOD_CALLS = { 'getAndSet': ['some_key', 'some_value'], 'getByPrefix': ['some_prefix'], 'pushItems': ['some_key_list', ['item1', 'item2']], - 'popItems': ['some_key'], - 'getItemsCount': ['some_key'], - 'itemContains': ['some_key', 'some_value'], + 'popItems': ['some_key_list'], + 'getItemsCount': ['some_key_list'], + 'itemContains': ['some_key_set', 'some_value'], + 'addItems': ['some_key_set', ['item1', 'item2']], + 'removeItems': ['some_key_set', ['item1', 'item2']], + 'getItems': ['some_key_set'], }; // Test target diff --git a/src/storages/pluggable/wrapperAdapter.ts b/src/storages/pluggable/wrapperAdapter.ts index 78e56eae..14a85683 100644 --- a/src/storages/pluggable/wrapperAdapter.ts +++ b/src/storages/pluggable/wrapperAdapter.ts @@ -17,6 +17,9 @@ export const METHODS_TO_PROMISE_WRAP: string[] = [ 'popItems', 'getItemsCount', 'itemContains', + 'addItems', + 'removeItems', + 'getItems', 'connect', 'close' ]; diff --git a/src/storages/types.ts b/src/storages/types.ts index af4dc526..e04f9f2d 100644 --- a/src/storages/types.ts +++ b/src/storages/types.ts @@ -6,6 +6,8 @@ import { SplitIO, ImpressionDTO } from '../types'; * Interface of a custom wrapper storage. */ export interface ICustomStorageWrapper { + /** Key-Value operations */ + /** * Get the value of given `key`. * @@ -62,6 +64,18 @@ export interface ICustomStorageWrapper { * The promise rejects if the operation fails. */ getByPrefix: (prefix: string) => Promise + /** + * Returns the values of all given `keys`. + * + * @function getMany + * @param {string[]} keys list of keys to retrieve + * @returns {Promise<(string | null)[]>} A promise that resolves with the list of items associated with the specified list of `keys`. For every key that does not hold a string value or does not exist, null is returned. + * The promise rejects if the operation fails. + */ + getMany: (keys: string[]) => Promise<(string | null)[]> + + /** Integer operations */ + /** * Increments in 1 the given `key` value or set it in 1 if the value doesn't exist. * @@ -80,15 +94,9 @@ export interface ICustomStorageWrapper { * The promise rejects if the operation fails, for example, if there is a connectin error or the key contains a string that can not be represented as integer. */ decr: (key: string) => Promise - /** - * Returns the values of all given `keys`. - * - * @function getMany - * @param {string[]} keys list of keys to retrieve - * @returns {Promise<(string | null)[]>} A promise that resolves with the list of items associated with the specified list of `keys`. For every key that does not hold a string value or does not exist, null is returned. - * The promise rejects if the operation fails. - */ - getMany: (keys: string[]) => Promise<(string | null)[]> + + /** Queue operations */ + /** * Inserts given items at the tail of `key` list. If `key` does not exist, an empty list is created before pushing the items. * @@ -118,16 +126,52 @@ export interface ICustomStorageWrapper { * The promise rejects if the operation fails, for example, if there is a connectin error or the key holds a value that is not a list. */ getItemsCount: (key: string) => Promise + + /** Set operations */ + /** - * Returns if item is a member of a list. + * Returns if item is a member of a set. * * @function itemContains - * @param {string} key list key + * @param {string} key set key * @param {string} item item value - * @returns {Promise} A promise that resolves with true boolean value if `item` is a member of the list stored at `key`, or false if it is not a member or `key` list does not exist. - * The promise rejects if the operation fails, for example, if there is a connectin error or the key holds a value that is not a list. + * @returns {Promise} A promise that resolves with true boolean value if `item` is a member of the set stored at `key`, or false if it is not a member or `key` set does not exist. + * The promise rejects if the operation fails, for example, if there is a connectin error or the key holds a value that is not a set. */ itemContains: (key: string, item: string) => Promise + /** + * Add the specified `items` to the set stored at `key`. Those items that are already part of the set are ignored. + * If key does not exist, an empty set is created before adding the items. + * + * @function addItems + * @param {string} key set key + * @param {string} items items to add + * @returns {Promise} A promise that resolves if the operation success. + * The promise rejects if the operation fails, for example, if there is a connectin error or the key holds a value that is not a set. + */ + addItems: (key: string, items: string[]) => Promise + /** + * Remove the specified `items` from the set stored at `key`. Those items that are not part of the set are ignored. + * + * @function removeItems + * @param {string} key set key + * @param {string} items items to remove + * @returns {Promise} A promise that resolves if the operation success. If key does not exist, the promise also resolves. + * The promise rejects if the operation fails, for example, if there is a connectin error or the key holds a value that is not a set. + */ + removeItems: (key: string, items: string[]) => Promise + /** + * Returns all the items of the `key` set. + * + * @function getItems + * @param {string} key set key + * @returns {Promise} A promise that resolves with the list of items. If key does not exist, the result is an empty list. + * The promise rejects if the operation fails, for example, if there is a connectin error or the key holds a value that is not a set. + */ + getItems: (key: string) => Promise + + /** Control operations */ + /** * Connects to the underlying storage. * It is meant for storages that requires to be connected to some database or server. Otherwise it can just return a resolved promise. @@ -238,7 +282,7 @@ export interface ISegmentsCacheAsync extends ISegmentsCacheBase { getRegisteredSegments(): Promise setChangeNumber(name: string, changeNumber: number): Promise getChangeNumber(name: string): Promise - clear(): Promise + clear(): Promise } /** diff --git a/src/sync/polling/updaters/segmentChangesUpdater.ts b/src/sync/polling/updaters/segmentChangesUpdater.ts index 339f12fe..5379998a 100644 --- a/src/sync/polling/updaters/segmentChangesUpdater.ts +++ b/src/sync/polling/updaters/segmentChangesUpdater.ts @@ -1,12 +1,13 @@ import { ISegmentChangesFetcher } from '../fetchers/types'; import { ISegmentsCacheBase } from '../../../storages/types'; import { IReadinessManager } from '../../../readiness/types'; -import { ISegmentChangesResponse } from '../../../dtos/types'; +import { ISegmentChangesResponse, MaybeThenable } from '../../../dtos/types'; import { findIndex } from '../../../utils/lang'; import { SplitError } from '../../../utils/lang/errors'; import { SDK_SEGMENTS_ARRIVED } from '../../../readiness/constants'; import { ILogger } from '../../../logger/types'; import { LOG_PREFIX_INSTANTIATION, LOG_PREFIX_SYNC_SEGMENTS } from '../../../logger/constants'; +import thenable from '../../../utils/promise/thenable'; type ISegmentChangesUpdater = (segmentNames?: string[], noCache?: boolean, fetchOnlyNew?: boolean) => Promise @@ -18,13 +19,13 @@ type ISegmentChangesUpdater = (segmentNames?: string[], noCache?: boolean, fetch * * @param log logger instance * @param segmentChangesFetcher fetcher of `/segmentChanges` - * @param segmentsCache segments storage, with sync or async methods + * @param segments segments storage, with sync or async methods * @param readiness optional readiness manager. Not required for synchronizer or producer mode. */ export function segmentChangesUpdaterFactory( log: ILogger, segmentChangesFetcher: ISegmentChangesFetcher, - segmentsCache: ISegmentsCacheBase, + segments: ISegmentsCacheBase, readiness?: IReadinessManager, ): ISegmentChangesUpdater { @@ -51,16 +52,16 @@ export function segmentChangesUpdaterFactory( log.debug(`${LOG_PREFIX_SYNC_SEGMENTS}Started segments update`); // If not a segment name provided, read list of available segments names to be updated. - let segmentsPromise = Promise.resolve(segmentNames ? segmentNames : segmentsCache.getRegisteredSegments()); + let segmentsPromise = Promise.resolve(segmentNames ? segmentNames : segments.getRegisteredSegments()); - return segmentsPromise.then(segments => { + return segmentsPromise.then(segmentNames => { // Async fetchers are collected here. const updaters: Promise[] = []; - for (let index = 0; index < segments.length; index++) { - const segmentName = segments[index]; + for (let index = 0; index < segmentNames.length; index++) { + const segmentName = segmentNames[index]; log.debug(`${LOG_PREFIX_SYNC_SEGMENTS}Processing segment ${segmentName}`); - let sincePromise = Promise.resolve(segmentsCache.getChangeNumber(segmentName)); + let sincePromise = Promise.resolve(segments.getChangeNumber(segmentName)); updaters.push(sincePromise.then(since => { // if fetchOnlyNew flag, avoid processing already fetched segments @@ -68,17 +69,19 @@ export function segmentChangesUpdaterFactory( return segmentChangesFetcher(since, segmentName, noCache, _promiseDecorator).then(function (changes) { let changeNumber = -1; + const results: MaybeThenable[] = []; changes.forEach(x => { - if (x.added.length > 0) segmentsCache.addToSegment(segmentName, x.added); - if (x.removed.length > 0) segmentsCache.removeFromSegment(segmentName, x.removed); + if (x.added.length > 0) results.push(segments.addToSegment(segmentName, x.added)); + if (x.removed.length > 0) results.push(segments.removeFromSegment(segmentName, x.removed)); if (x.added.length > 0 || x.removed.length > 0) { - segmentsCache.setChangeNumber(segmentName, x.till); + results.push(segments.setChangeNumber(segmentName, x.till)); changeNumber = x.till; } log.debug(`${LOG_PREFIX_SYNC_SEGMENTS}Processed ${segmentName} with till = ${x.till}. Added: ${x.added.length}. Removed: ${x.removed.length}`); }); - + // If at least one storage operation result is a promise, join all in a single promise. + if (results.some(result => thenable(result))) return Promise.all(results).then(() => changeNumber); return changeNumber; }); })); @@ -94,7 +97,10 @@ export function segmentChangesUpdaterFactory( // if at least one segment fetch fails, return false to indicate that there was some error (e.g., invalid json, HTTP error, etc) if (shouldUpdateFlags.indexOf(-1) !== -1) return false; return true; - }).catch(error => { + }); + }) + // Handles rejected promises at `segmentChangesFetcher`, `segments.getRegisteredSegments` and other segment storage operations. + .catch(error => { // handle user callback errors if (!(error instanceof SplitError)) setTimeout(() => { throw error; }, 0); @@ -106,7 +112,6 @@ export function segmentChangesUpdaterFactory( return false; }); - }); }; } diff --git a/src/sync/polling/updaters/splitChangesUpdater.ts b/src/sync/polling/updaters/splitChangesUpdater.ts index 4565f0c6..eeeede95 100644 --- a/src/sync/polling/updaters/splitChangesUpdater.ts +++ b/src/sync/polling/updaters/splitChangesUpdater.ts @@ -80,8 +80,8 @@ export function computeSplitsMutation(entries: ISplit[]): ISplitMutations { * * @param log Logger instance * @param splitChangesFetcher Fetcher of `/splitChanges` - * @param splitsCache Splits storage, with sync or async methods - * @param segmentsCache Segments storage, with sync or async methods + * @param splits Splits storage, with sync or async methods + * @param segments Segments storage, with sync or async methods * @param splitsEventEmitter Optional readiness manager. Not required for synchronizer or producer mode. * @param requestTimeoutBeforeReady How long the updater will wait for the request to timeout. Default 0, i.e., never timeout. * @param retriesOnFailureBeforeReady How many retries on `/splitChanges` we the updater do in case of failure or timeout. Default 0, i.e., no retries. From 9bc62b2a2c0a7d53f0602f4f2e338bccffe882b3 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 12 May 2021 19:22:09 -0300 Subject: [PATCH 054/144] added missing entry in README list of SDKs --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 37b4e232..fd90814d 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ Split has built and maintains SDKs for: * Java [Github](https://github.com/splitio/java-client) [Docs](https://help.split.io/hc/en-us/articles/360020405151-Java-SDK) * Javascript [Github](https://github.com/splitio/javascript-client) [Docs](https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK) * Node [Github](https://github.com/splitio/javascript-client) [Docs](https://help.split.io/hc/en-us/articles/360020564931-Node-js-SDK) +* Javascript for Browser [Github](https://github.com/splitio/javascript-browser-client) [Docs](https://help.split.io/hc/en-us/articles/360058730852) * PHP [Github](https://github.com/splitio/php-client) [Docs](https://help.split.io/hc/en-us/articles/360020350372-PHP-SDK) * Python [Github](https://github.com/splitio/python-client) [Docs](https://help.split.io/hc/en-us/articles/360020359652-Python-SDK) * React [Github](https://github.com/splitio/react-client) [Docs](https://help.split.io/hc/en-us/articles/360038825091-React-SDK) From f5ac00849d0addaf6109bc76cc2106ffced56c6e Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 12 May 2021 21:25:15 -0300 Subject: [PATCH 055/144] fixed issue with user cb errors --- src/storages/inRedis/SegmentsCacheInRedis.ts | 2 +- src/sync/polling/updaters/splitChangesUpdater.ts | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/storages/inRedis/SegmentsCacheInRedis.ts b/src/storages/inRedis/SegmentsCacheInRedis.ts index 5cb88656..965f2acb 100644 --- a/src/storages/inRedis/SegmentsCacheInRedis.ts +++ b/src/storages/inRedis/SegmentsCacheInRedis.ts @@ -71,7 +71,7 @@ export default class SegmentsCacheInRedis implements ISegmentsCacheAsync { getRegisteredSegments() { return this.redis.smembers(this.keys.buildRegisteredSegmentsKey()) - // @TODO remove if refactoring SDK error handling (it is necessary to distinguish from user cb errors). + // Wrapping error to distinguish from user cb errors. @TODO remove if refactoring SDK error handling. .catch((e) => { throw new SplitError(e); }); } diff --git a/src/sync/polling/updaters/splitChangesUpdater.ts b/src/sync/polling/updaters/splitChangesUpdater.ts index eeeede95..84f5d7c4 100644 --- a/src/sync/polling/updaters/splitChangesUpdater.ts +++ b/src/sync/polling/updaters/splitChangesUpdater.ts @@ -144,9 +144,13 @@ export function splitChangesUpdaterFactory( if (splitsEventEmitter) { // To emit SDK_SPLITS_ARRIVED for server-side SDK, we must check that all registered segments have been fetched - Promise.resolve(!splitsEventEmitter.splitsArrived || (since !== splitChanges.till && checkAllSegmentsExist(segments))) - .then(emitSplitsArrivedEvent => { if (emitSplitsArrivedEvent) splitsEventEmitter.emit(SDK_SPLITS_ARRIVED); }) - .catch(() => {/** noop. just to handle a possible `checkAllSegmentsExist` promise rejection on custom or redis storage */ }); + return Promise.resolve(!splitsEventEmitter.splitsArrived || (since !== splitChanges.till && checkAllSegmentsExist(segments))) + .catch(() => false /** noop. just to handle a possible `checkAllSegmentsExist` rejection, before emitting SDK event */) + .then(emitSplitsArrivedEvent => { + // emit SDK events + if (emitSplitsArrivedEvent) splitsEventEmitter.emit(SDK_SPLITS_ARRIVED); + return true; + }); } return true; }); From de33734a6fb4555515085e3684c25a9f282f1b8a Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Fri, 14 May 2021 10:43:12 -0300 Subject: [PATCH 056/144] implementation and some interface updates --- src/listeners/browser.ts | 4 +- src/storages/inRedis/EventsCacheInRedis.ts | 34 +++-- .../inRedis/ImpressionsCacheInRedis.ts | 41 +++++- src/storages/inRedis/RedisAdapter.ts | 2 +- .../__tests__/EventsCacheInRedis.spec.ts | 21 ++- .../__tests__/ImpressionsCacheInRedis.spec.ts | 27 ++-- src/storages/inRedis/index.ts | 4 +- .../pluggable/EventsCachePluggable.ts | 39 +++++- .../pluggable/ImpressionsCachePluggable.ts | 40 +++++- .../__tests__/EventsCachePluggable.spec.ts | 14 +- .../ImpressionsCachePluggable.spec.ts | 8 +- src/storages/pluggable/index.ts | 4 +- src/storages/types.ts | 124 +++++++++--------- src/sync/submitters/impressionsSyncTask.ts | 2 +- src/sync/submitters/submitterSyncTask.ts | 6 +- src/sync/submitters/types.ts | 68 ++++++++-- src/trackers/impressionsTracker.ts | 4 +- src/utils/inputValidation/eventProperties.ts | 4 +- 18 files changed, 290 insertions(+), 156 deletions(-) diff --git a/src/listeners/browser.ts b/src/listeners/browser.ts index d937448e..6dac033d 100644 --- a/src/listeners/browser.ts +++ b/src/listeners/browser.ts @@ -1,7 +1,7 @@ /* eslint-disable no-undef */ // @TODO eventually migrate to JS-Browser-SDK package. import { ISignalListener } from './types'; -import { IRecorderCacheConsumerSync, IStorageSync } from '../storages/types'; +import { IRecorderCacheProducerSync, IStorageSync } from '../storages/types'; import { fromImpressionsCollector } from '../sync/submitters/impressionsSyncTask'; import { fromImpressionCountsCollector } from '../sync/submitters/impressionCountsSyncTask'; import { ISplitApi } from '../services/types'; @@ -74,7 +74,7 @@ export default class BrowserSignalListener implements ISignalListener { if (this.storage.impressionCounts) this._flushData(eventsUrl + '/testImpressions/count/beacon', this.storage.impressionCounts, this.serviceApi.postTestImpressionsCount, fromImpressionCountsCollector); } - private _flushData(url: string, cache: IRecorderCacheConsumerSync, postService: (body: string) => Promise, fromCacheToPayload?: (cacheData: TState) => any, extraMetadata?: {}) { + private _flushData(url: string, cache: IRecorderCacheProducerSync, postService: (body: string) => Promise, fromCacheToPayload?: (cacheData: TState) => any, extraMetadata?: {}) { // if there is data in cache, send it to backend if (!cache.isEmpty()) { const dataPayload = fromCacheToPayload ? fromCacheToPayload(cache.state()) : cache.state(); diff --git a/src/storages/inRedis/EventsCacheInRedis.ts b/src/storages/inRedis/EventsCacheInRedis.ts index ff711140..8002d711 100644 --- a/src/storages/inRedis/EventsCacheInRedis.ts +++ b/src/storages/inRedis/EventsCacheInRedis.ts @@ -1,24 +1,23 @@ import { IEventsCacheAsync } from '../types'; import { IMetadata } from '../../dtos/types'; -import KeyBuilderSS from '../KeyBuilderSS'; import { Redis } from 'ioredis'; import { SplitIO } from '../../types'; import { ILogger } from '../../logger/types'; import { LOG_PREFIX } from './constants'; +import { StoredEventWithMetadata } from '../../sync/submitters/types'; export default class EventsCacheInRedis implements IEventsCacheAsync { + private readonly log: ILogger; + private readonly key: string; private readonly redis: Redis; - private readonly keys: KeyBuilderSS; private readonly metadata: IMetadata; - private readonly eventsKey: string; - constructor(private readonly log: ILogger, keys: KeyBuilderSS, redis: Redis, metadata: IMetadata) { - this.keys = keys; + constructor(log: ILogger, key: string, redis: Redis, metadata: IMetadata) { + this.log = log; + this.key = key; this.redis = redis; this.metadata = metadata; - - this.eventsKey = keys.buildEventsKey(); } /** @@ -26,7 +25,7 @@ export default class EventsCacheInRedis implements IEventsCacheAsync { */ track(eventData: SplitIO.EventData): Promise { return this.redis.rpush( - this.eventsKey, + this.key, this._toJSON(eventData) ) // We use boolean values to signal successful queueing @@ -44,6 +43,25 @@ export default class EventsCacheInRedis implements IEventsCacheAsync { return JSON.stringify({ m: this.metadata, e: eventData + } as StoredEventWithMetadata); + } + + count(): Promise { + return this.redis.llen(this.key).catch(() => 0); + } + + drop(count?: number): Promise { + if (!count) return this.redis.del(this.key); + + return this.redis.ltrim(this.key, count, -1); + } + + // @TODO follow Go implementation + popNWithMetadata(count: number): Promise { + return this.redis.lrange(this.key, 0, count - 1).then(items => { + return this.redis.ltrim(this.key, items.length, -1).then(() => { + return items.map(item => JSON.parse(item) as StoredEventWithMetadata); + }); }); } diff --git a/src/storages/inRedis/ImpressionsCacheInRedis.ts b/src/storages/inRedis/ImpressionsCacheInRedis.ts index 9013478f..d353a212 100644 --- a/src/storages/inRedis/ImpressionsCacheInRedis.ts +++ b/src/storages/inRedis/ImpressionsCacheInRedis.ts @@ -1,29 +1,34 @@ import { IImpressionsCacheAsync } from '../types'; import { IMetadata } from '../../dtos/types'; import { ImpressionDTO } from '../../types'; -import KeyBuilderSS from '../KeyBuilderSS'; import { Redis } from 'ioredis'; +import { StoredImpressionWithMetadata } from '../../sync/submitters/types'; +import { ILogger } from '../../logger/types'; + +const IMPRESSIONS_TTL_REFRESH = 3600; // 1 hr export default class ImpressionsCacheInRedis implements IImpressionsCacheAsync { + private readonly log: ILogger; + private readonly key: string; private readonly redis: Redis; - private readonly keys: KeyBuilderSS; private readonly metadata: IMetadata; - constructor(keys: KeyBuilderSS, redis: Redis, metadata: IMetadata) { - this.keys = keys; + constructor(log: ILogger, key: string, redis: Redis, metadata: IMetadata) { + this.log = log; + this.key = key; this.redis = redis; this.metadata = metadata; } track(impressions: ImpressionDTO[]): Promise { return this.redis.rpush( - this.keys.buildImpressionsKey(), + this.key, this._toJSON(impressions) ).then(queuedCount => { // If this is the creation of the key on Redis, set the expiration for it in 1hr. if (queuedCount === impressions.length) { - return this.redis.expire(this.keys.buildImpressionsKey(), 3600).then(result => result === 1); // 1 if the timeout was set. 0 if key does not exist. + return this.redis.expire(this.key, IMPRESSIONS_TTL_REFRESH).then(result => result === 1); // 1 if the timeout was set. 0 if key does not exist. } return true; }); @@ -50,4 +55,28 @@ export default class ImpressionsCacheInRedis implements IImpressionsCacheAsync { }); } + count(): Promise { + return this.redis.llen(this.key).catch(() => 0); + } + + drop(count?: number): Promise { + if (!count) return this.redis.del(this.key); + + return this.redis.ltrim(this.key, count, -1); + } + + popNWithMetadata(count: number): Promise { + return this.redis.lrange(this.key, 0, count - 1).then(items => { + return this.redis.ltrim(this.key, items.length, -1).then(() => { + // This operation will simply do nothing if the key no longer exists (queue is empty) + // It's only done in the "successful" exit path so that the TTL is not overriden if impressons weren't + // popped correctly. This will result in impressions getting lost but will prevent the queue from taking + // a huge amount of memory. + this.redis.expire(this.key, IMPRESSIONS_TTL_REFRESH).catch(() => { }); // noop catch handler + + return items.map(item => JSON.parse(item) as StoredImpressionWithMetadata); + }); + }); + } + } diff --git a/src/storages/inRedis/RedisAdapter.ts b/src/storages/inRedis/RedisAdapter.ts index de0abee3..99dccd45 100644 --- a/src/storages/inRedis/RedisAdapter.ts +++ b/src/storages/inRedis/RedisAdapter.ts @@ -8,7 +8,7 @@ import timeout from '../../utils/promise/timeout'; const LOG_PREFIX = 'storage:redis-adapter: '; // If we ever decide to fully wrap every method, there's a Commander.getBuiltinCommands from ioredis. -const METHODS_TO_PROMISE_WRAP = ['set', 'exec', 'del', 'get', 'keys', 'sadd', 'srem', 'sismember', 'smembers', 'incr', 'rpush', 'pipeline', 'expire', 'mget']; +const METHODS_TO_PROMISE_WRAP = ['set', 'exec', 'del', 'get', 'keys', 'sadd', 'srem', 'sismember', 'smembers', 'incr', 'rpush', 'pipeline', 'expire', 'mget', 'lrange', 'ltrim']; // Not part of the settings since it'll vary on each storage. We should be removing storage specific logic from elsewhere. const DEFAULT_OPTIONS = { diff --git a/src/storages/inRedis/__tests__/EventsCacheInRedis.spec.ts b/src/storages/inRedis/__tests__/EventsCacheInRedis.spec.ts index 64ed72a0..20ce856e 100644 --- a/src/storages/inRedis/__tests__/EventsCacheInRedis.spec.ts +++ b/src/storages/inRedis/__tests__/EventsCacheInRedis.spec.ts @@ -2,36 +2,31 @@ import Redis from 'ioredis'; import find from 'lodash/find'; import isEqual from 'lodash/isEqual'; import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; -import KeyBuilderSS from '../../KeyBuilderSS'; import EventsCacheInRedis from '../EventsCacheInRedis'; const prefix = 'events_cache_ut'; +const eventsKey = `${prefix}.impressions`; const fakeMetadata = { s: 'js_someversion', i: 'some_ip', n: 'some_hostname' }; test('EVENTS CACHE IN REDIS / should incrementally store values in redis', async () => { const connection = new Redis(); - // This piece is being tested elsewhere. - const keys = new KeyBuilderSS(prefix, fakeMetadata); - const key = keys.buildEventsKey(); const fakeEvent1 = { event: 1 }; const fakeEvent2 = { event: '2' }; const fakeEvent3 = { event: null }; // Clean up in case there are still keys there. - await connection.del(key); + await connection.del(eventsKey); - let redisValues = await connection.lrange(key, 0, -1); + let redisValues = await connection.lrange(eventsKey, 0, -1); expect(redisValues.length).toBe(0); // control assertion, there are no events previously queued. - const cache = new EventsCacheInRedis(loggerMock, keys, connection, fakeMetadata); + const cache = new EventsCacheInRedis(loggerMock, eventsKey, connection, fakeMetadata); // I'll use a "bad" instance so I can force an issue with the rpush command. I'll store an integer and will make the cache try to use rpush there. await connection.set('non-list-key', 10); - // @ts-expect-error - const faultyCache = new EventsCacheInRedis(loggerMock, { - buildEventsKey: () => 'non-list-key' - }, connection, fakeMetadata); + + const faultyCache = new EventsCacheInRedis(loggerMock, 'non-list-key', connection, fakeMetadata); // @ts-expect-error expect(await cache.track(fakeEvent1)).toBe(true); // If the queueing operation was successful, it should resolve the returned promise with "true" @@ -46,7 +41,7 @@ test('EVENTS CACHE IN REDIS / should incrementally store values in redis', async // @ts-expect-error expect(await faultyCache.track(fakeEvent3)).toBe(false); // If the queueing operation was NOT successful, it should resolve the returned promise with "false" instead of rejecting it. - redisValues = await connection.lrange(key, 0, -1); + redisValues = await connection.lrange(eventsKey, 0, -1); expect(redisValues.length).toBe(3); // After pushing we should have on Redis as many events as we have stored. expect(typeof redisValues[0]).toBe('string'); // All elements should be strings since those are stringified JSONs. @@ -69,6 +64,6 @@ test('EVENTS CACHE IN REDIS / should incrementally store values in redis', async expect(foundEv3).not.toBe(undefined); // Events stored on redis matched the values we are expecting. // Clean up then end. - await connection.del(key); + await connection.del(eventsKey); await connection.quit(); }); diff --git a/src/storages/inRedis/__tests__/ImpressionsCacheInRedis.spec.ts b/src/storages/inRedis/__tests__/ImpressionsCacheInRedis.spec.ts index 6dc6c8ef..83eb886b 100644 --- a/src/storages/inRedis/__tests__/ImpressionsCacheInRedis.spec.ts +++ b/src/storages/inRedis/__tests__/ImpressionsCacheInRedis.spec.ts @@ -1,17 +1,15 @@ import Redis from '../RedisAdapter'; -import KeyBuilderSS from '../../KeyBuilderSS'; import ImpressionsCacheInRedis from '../ImpressionsCacheInRedis'; import IORedis, { BooleanResponse } from 'ioredis'; import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; +const fakeMetadata = { s: 'js_someversion', i: 'some_ip', n: 'some_hostname' }; + test('IMPRESSIONS CACHE IN REDIS / should incrementally store values', async () => { - const prefix = 'impr_cache_ut'; - const impressionsKey = `${prefix}.impressions`; - const testMeta = { thisIsTheMeta: true }; const connection = new Redis(loggerMock, {}); - // @ts-expect-error - const keys = new KeyBuilderSS(prefix); // @ts-expect-error - const c = new ImpressionsCacheInRedis(keys, connection, testMeta); + const impressionsKey = 'impr_cache_ut.impressions'; + + const c = new ImpressionsCacheInRedis(loggerMock, impressionsKey, connection, fakeMetadata); const o1 = { feature: 'test1', @@ -48,15 +46,15 @@ test('IMPRESSIONS CACHE IN REDIS / should incrementally store values', async () const state = await connection.lrange(impressionsKey, 0, -1); // This is testing both the track and the toJSON method. expect(state[0]).toBe(JSON.stringify({ - m: testMeta, + m: fakeMetadata, i: { k: o1.keyName, f: o1.feature, t: o1.treatment, c: o1.changeNumber, m: o1.time } })); expect(state[1]).toBe(JSON.stringify({ - m: testMeta, + m: fakeMetadata, i: { k: o2.keyName, b: o2.bucketingKey, f: o2.feature, t: o2.treatment, r: o2.label, c: o2.changeNumber, m: o2.time } })); expect(state[2]).toBe(JSON.stringify({ - m: testMeta, + m: fakeMetadata, i: { k: o3.keyName, f: o3.feature, t: o3.treatment, c: o3.changeNumber, m: o3.time } })); @@ -65,13 +63,10 @@ test('IMPRESSIONS CACHE IN REDIS / should incrementally store values', async () }); test('IMPRESSIONS CACHE IN REDIS / should not resolve track before calling expire', async (done) => { - const prefix = 'impr_cache_ut_2'; - const impressionsKey = `${prefix}.impressions`; - const testMeta = { thisIsTheMeta: true }; + const impressionsKey = 'impr_cache_ut_2.impressions'; const connection = new Redis(loggerMock, {}); - // @ts-expect-error - const keys = new KeyBuilderSS(prefix); // @ts-expect-error - const c = new ImpressionsCacheInRedis(keys, connection, testMeta); + + const c = new ImpressionsCacheInRedis(loggerMock, impressionsKey, connection, fakeMetadata); const i1 = { feature: 'test4', keyName: 'nicolas@split.io', treatment: 'off', time: Date.now(), changeNumber: 1 }; const i2 = { feature: 'test5', keyName: 'matias@split.io', treatment: 'on', time: Date.now(), changeNumber: 2 }; diff --git a/src/storages/inRedis/index.ts b/src/storages/inRedis/index.ts index d0bc35fc..3896e882 100644 --- a/src/storages/inRedis/index.ts +++ b/src/storages/inRedis/index.ts @@ -34,8 +34,8 @@ export function InRedisStorage(options: InRedisStorageOptions = {}) { return { splits: new SplitsCacheInRedis(log, keys, redisClient), segments: new SegmentsCacheInRedis(log, keys, redisClient), - impressions: new ImpressionsCacheInRedis(keys, redisClient, metadata), - events: new EventsCacheInRedis(log, keys, redisClient, metadata), + impressions: new ImpressionsCacheInRedis(log, keys.buildImpressionsKey(), redisClient, metadata), + events: new EventsCacheInRedis(log, keys.buildEventsKey(), redisClient, metadata), latencies: new LatenciesCacheInRedis(keys, redisClient), counts: new CountsCacheInRedis(keys, redisClient), diff --git a/src/storages/pluggable/EventsCachePluggable.ts b/src/storages/pluggable/EventsCachePluggable.ts index b706d04e..eb24e2fb 100644 --- a/src/storages/pluggable/EventsCachePluggable.ts +++ b/src/storages/pluggable/EventsCachePluggable.ts @@ -1,20 +1,20 @@ import { ICustomStorageWrapper, IEventsCacheAsync } from '../types'; import { IMetadata } from '../../dtos/types'; -import KeyBuilderSS from '../KeyBuilderSS'; import { SplitIO } from '../../types'; import { ILogger } from '../../logger/types'; import { LOG_PREFIX } from './constants'; +import { StoredEventWithMetadata } from '../../sync/submitters/types'; export class EventsCachePluggable implements IEventsCacheAsync { private readonly log: ILogger; private readonly wrapper: ICustomStorageWrapper; - private readonly keys: KeyBuilderSS; + private readonly key: string; private readonly metadata: IMetadata; - constructor(log: ILogger, keys: KeyBuilderSS, wrapper: ICustomStorageWrapper, metadata: IMetadata) { + constructor(log: ILogger, key: string, wrapper: ICustomStorageWrapper, metadata: IMetadata) { this.log = log; - this.keys = keys; + this.key = key; this.wrapper = wrapper; this.metadata = metadata; } @@ -27,7 +27,7 @@ export class EventsCachePluggable implements IEventsCacheAsync { */ track(eventData: SplitIO.EventData): Promise { return this.wrapper.pushItems( - this.keys.buildEventsKey(), + this.key, [this._toJSON(eventData)] ) // We use boolean values to signal successful queueing @@ -42,6 +42,35 @@ export class EventsCachePluggable implements IEventsCacheAsync { return JSON.stringify({ m: this.metadata, e: eventData + } as StoredEventWithMetadata); + } + + /** + * Returns a promise that resolves with the count of stored events, or 0 if there was some error. + * The promise will never be rejected. + */ + count(): Promise { + return this.wrapper.getItemsCount(this.key).catch(() => 0); + } + + /** + * Removes the given number of events from the store. If a number is not provided, it deletes all items. + * The returned promise rejects if the wrapper operation fails. + */ + drop(count?: number): Promise { + if (!count) return this.wrapper.del(this.key); + + return this.wrapper.popItems(this.key, count).then(() => { }); + } + + /** + * Pop the given number of events from the store. + * The returned promise rejects if the wrapper operation fails. + */ + // @TODO follow Go implementation + popNWithMetadata(count: number): Promise { + return this.wrapper.popItems(this.key, count).then((items) => { + return items.map(item => JSON.parse(item) as StoredEventWithMetadata); }); } diff --git a/src/storages/pluggable/ImpressionsCachePluggable.ts b/src/storages/pluggable/ImpressionsCachePluggable.ts index 3f039623..0ddad1c2 100644 --- a/src/storages/pluggable/ImpressionsCachePluggable.ts +++ b/src/storages/pluggable/ImpressionsCachePluggable.ts @@ -1,20 +1,20 @@ import { ICustomStorageWrapper, IImpressionsCacheAsync } from '../types'; import { IMetadata } from '../../dtos/types'; import { ImpressionDTO } from '../../types'; -import KeyBuilderSS from '../KeyBuilderSS'; import { ILogger } from '../../logger/types'; import { LOG_PREFIX } from './constants'; +import { StoredImpressionWithMetadata } from '../../sync/submitters/types'; export class ImpressionsCachePluggable implements IImpressionsCacheAsync { private readonly log: ILogger; - private readonly keys: KeyBuilderSS; + private readonly key: string; private readonly wrapper: ICustomStorageWrapper; private readonly metadata: IMetadata; - constructor(log: ILogger, keys: KeyBuilderSS, wrapper: ICustomStorageWrapper, metadata: IMetadata) { + constructor(log: ILogger, key: string, wrapper: ICustomStorageWrapper, metadata: IMetadata) { this.log = log; - this.keys = keys; + this.key = key; this.wrapper = wrapper; this.metadata = metadata; } @@ -27,7 +27,7 @@ export class ImpressionsCachePluggable implements IImpressionsCacheAsync { */ track(impressions: ImpressionDTO[]): Promise { return this.wrapper.pushItems( - this.keys.buildImpressionsKey(), + this.key, this._toJSON(impressions) ) // We use boolean values to signal successful queueing @@ -55,10 +55,36 @@ export class ImpressionsCachePluggable implements IImpressionsCacheAsync { c: changeNumber, m: time } - }); + } as StoredImpressionWithMetadata); }); } - // @TODO implement producer methods + /** + * Returns a promise that resolves with the count of stored impressions, or 0 if there was some error. + * The promise will never be rejected. + */ + count(): Promise { + return this.wrapper.getItemsCount(this.key).catch(() => 0); + } + + /** + * Removes the given number of impressions from the store. If a number is not provided, it deletes all items. + * The returned promise rejects if the wrapper operation fails. + */ + drop(count?: number): Promise { + if (!count) return this.wrapper.del(this.key); + + return this.wrapper.popItems(this.key, count).then(() => { }); + } + + /** + * Pop the given number of impressions from the store. + * The returned promise rejects if the wrapper operation fails. + */ + popNWithMetadata(count: number): Promise { + return this.wrapper.popItems(this.key, count).then((items) => { + return items.map(item => JSON.parse(item) as StoredImpressionWithMetadata); + }); + } } diff --git a/src/storages/pluggable/__tests__/EventsCachePluggable.spec.ts b/src/storages/pluggable/__tests__/EventsCachePluggable.spec.ts index ea306a08..e1fd3a6d 100644 --- a/src/storages/pluggable/__tests__/EventsCachePluggable.spec.ts +++ b/src/storages/pluggable/__tests__/EventsCachePluggable.spec.ts @@ -1,15 +1,13 @@ import find from 'lodash/find'; import isEqual from 'lodash/isEqual'; import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; -import KeyBuilderSS from '../../KeyBuilderSS'; import { EventsCachePluggable } from '../EventsCachePluggable'; import { wrapperMockFactory } from './wrapper.mock'; import { wrapperAdapter } from '../wrapperAdapter'; const prefix = 'events_cache_ut'; +const eventsKey = `${prefix}.impressions`; const fakeMetadata = { s: 'js_someversion', i: 'some_ip', n: 'some_hostname' }; -const keys = new KeyBuilderSS(prefix, fakeMetadata); -const key = keys.buildEventsKey(); const fakeEvent1 = { event: 1 }; const fakeEvent2 = { event: '2' }; @@ -19,7 +17,7 @@ describe('PLUGGABLE EVENTS CACHE', () => { test('`track` method should push values into the pluggable storage', async () => { const wrapperMock = wrapperMockFactory(); - const cache = new EventsCachePluggable(loggerMock, keys, wrapperMock, fakeMetadata); + const cache = new EventsCachePluggable(loggerMock, eventsKey, wrapperMock, fakeMetadata); // @ts-expect-error expect(await cache.track(fakeEvent1)).toBe(true); // If the queueing operation was successful, it should resolve the returned promise with "true" @@ -28,7 +26,7 @@ describe('PLUGGABLE EVENTS CACHE', () => { // @ts-expect-error expect(await cache.track(fakeEvent3)).toBe(true); // If the queueing operation was successful, it should resolve the returned promise with "true" - const values = wrapperMock._cache[key] as string[]; + const values = wrapperMock._cache[eventsKey] as string[]; expect(values.length).toBe(3); // After pushing we should have as many events as we have stored. expect(typeof values[0]).toBe('string'); // All elements should be strings since those are stringified JSONs. @@ -57,10 +55,8 @@ describe('PLUGGABLE EVENTS CACHE', () => { const wrapperMock = wrapperMockFactory(); // @ts-ignore. I'll use a "bad" queue to force an exception with the pushItems wrapper operation. wrapperMock._cache['non-list-key'] = 10; - // @ts-expect-error. wrapperMock is adapted this time to properly handle unexpected exceptions - const faultyCache = new EventsCachePluggable(loggerMock, { - buildEventsKey: () => 'non-list-key' - }, wrapperAdapter(loggerMock, wrapperMock), fakeMetadata); + // wrapperMock is adapted this time to properly handle unexpected exceptions + const faultyCache = new EventsCachePluggable(loggerMock, 'non-list-key', wrapperAdapter(loggerMock, wrapperMock), fakeMetadata); // @ts-expect-error expect(await faultyCache.track(fakeEvent1)).toBe(false); // If the queueing operation was NOT successful, it should resolve the returned promise with "false" instead of rejecting it. diff --git a/src/storages/pluggable/__tests__/ImpressionsCachePluggable.spec.ts b/src/storages/pluggable/__tests__/ImpressionsCachePluggable.spec.ts index 699bd7e2..20693579 100644 --- a/src/storages/pluggable/__tests__/ImpressionsCachePluggable.spec.ts +++ b/src/storages/pluggable/__tests__/ImpressionsCachePluggable.spec.ts @@ -1,5 +1,4 @@ -import KeyBuilderSS from '../../KeyBuilderSS'; import { ImpressionsCachePluggable } from '../ImpressionsCachePluggable'; import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; import { wrapperMock } from './wrapper.mock'; @@ -7,8 +6,7 @@ import { IMetadata } from '../../../dtos/types'; const prefix = 'impr_cache_ut'; const impressionsKey = `${prefix}.impressions`; -const testMeta: IMetadata = { i: 'some_ip', n: 'some_host', s: 'some_sdk_version' }; // @ts-ignore -const keys = new KeyBuilderSS(prefix, testMeta); +const testMeta: IMetadata = { i: 'some_ip', n: 'some_host', s: 'some_sdk_version' }; const o1 = { feature: 'test1', @@ -46,7 +44,7 @@ describe('PLUGGABLE IMPRESSIONS CACHE', () => { }); test('`track` method should push values into the pluggable storage', async () => { - const cache = new ImpressionsCachePluggable(loggerMock, keys, wrapperMock, testMeta); + const cache = new ImpressionsCachePluggable(loggerMock, impressionsKey, wrapperMock, testMeta); expect(await cache.track([o1])).toBe(true); // result should resolve to true @@ -76,7 +74,7 @@ describe('PLUGGABLE IMPRESSIONS CACHE', () => { test('`track` method result should not reject if wrapper operation fails', async () => { // make wrapper operation fail wrapperMock.pushItems.mockImplementation(() => Promise.reject()); - const cache = new ImpressionsCachePluggable(loggerMock, keys, wrapperMock, testMeta); + const cache = new ImpressionsCachePluggable(loggerMock, impressionsKey, wrapperMock, testMeta); expect(await cache.track([o1])).toBe(false); // result should resolve to false }); diff --git a/src/storages/pluggable/index.ts b/src/storages/pluggable/index.ts index 7c41621a..8f5cfb46 100644 --- a/src/storages/pluggable/index.ts +++ b/src/storages/pluggable/index.ts @@ -53,8 +53,8 @@ export function PluggableStorage(options: PluggableStorageOptions) { return { splits: new SplitsCachePluggable(log, keys, wrapper), segments: new SegmentsCachePluggable(log, keys, wrapper), - impressions: new ImpressionsCachePluggable(log, keys, wrapper, metadata), - events: new EventsCachePluggable(log, keys, wrapper, metadata), + impressions: new ImpressionsCachePluggable(log, keys.buildImpressionsKey(), wrapper, metadata), + events: new EventsCachePluggable(log, keys.buildEventsKey(), wrapper, metadata), // @TODO add telemetry cache when required // Disconnect the underlying storage, to release its resources (such as open files, database connections, etc). diff --git a/src/storages/types.ts b/src/storages/types.ts index e04f9f2d..31f27234 100644 --- a/src/storages/types.ts +++ b/src/storages/types.ts @@ -1,5 +1,6 @@ import { MaybeThenable, IMetadata, ISplitFiltersValidation } from '../dtos/types'; import { ILogger } from '../logger/types'; +import { StoredEventWithMetadata, StoredImpressionWithMetadata } from '../sync/submitters/types'; import { SplitIO, ImpressionDTO } from '../types'; /** @@ -285,94 +286,100 @@ export interface ISegmentsCacheAsync extends ISegmentsCacheBase { clear(): Promise } -/** - * Producer API of Recorder caches (Impressions, Events and Metrics cache), used by recorders to push data. - */ -export interface IRecorderCacheProducerBase { - track(...args: TArgs): MaybeThenable -} - -/** - * Consumer API of Recorder caches, used by submitters to pop data and post it to Split BE. - */ -export interface IRecorderCacheConsumerSync { - isEmpty(): boolean // check if cache is empty. Return true if the cache was just created or cleared. - clear(): void // clear cache data - state(): TState // get cache data -} +/** Recorder storages (impressions, events and telemetry) */ -export interface IRecorderCacheSync extends IRecorderCacheProducerBase, IRecorderCacheConsumerSync { - track(...args: TArgs): boolean | void +export interface IImpressionsCacheBase { + // Consumer API method, used by impressions tracker, in standalone and consumer modes, to push impressions into the storage. + track(data: ImpressionDTO[]): MaybeThenable } -export interface IRecorderCacheAsync extends IRecorderCacheProducerBase { - track(...args: TArgs): Promise +export interface IEventsCacheBase { + // Consumer API method, used by events tracker, in standalone and consumer modes, to push events into the storage. + track(data: SplitIO.EventData, size?: number): MaybeThenable } -/** Impressions cache */ +/** Impressions and events cache for standalone mode (sync) */ -export interface IImpressionsCacheBase extends IRecorderCacheProducerBase<[ImpressionDTO[]]> { - track(data: ImpressionDTO[]): MaybeThenable +// Producer API methods for sync recorder storages, used by submitters in standalone mode to pop data and post it to Split BE. +export interface IRecorderCacheProducerSync { + // @TODO names are inconsistent with spec + /* Checks if cache is empty. Returns true if the cache was just created or cleared */ + isEmpty(): boolean + /* clears cache data */ + clear(): void + /* Gets cache data */ + state(): T } -export interface IImpressionsCacheSync extends IImpressionsCacheBase, IRecorderCacheSync<[ImpressionDTO[]], ImpressionDTO[]> { + +export interface IImpressionsCacheSync extends IImpressionsCacheBase, IRecorderCacheProducerSync { track(data: ImpressionDTO[]): boolean } -export interface IImpressionsCacheAsync extends IImpressionsCacheBase, IRecorderCacheAsync<[ImpressionDTO[]]> { - track(data: ImpressionDTO[]): Promise +export interface IEventsCacheSync extends IEventsCacheBase, IRecorderCacheProducerSync { + track(data: SplitIO.EventData, size?: number): boolean + setOnFullQueueCb(cb: () => void): void } -/** Impression counts cache */ +/** Impressions and events cache for consumer and producer mode (async) */ -export interface IImpressionCountsCacheBase extends IRecorderCacheProducerBase<[string, number, number]> { - track(featureName: string, timeFrame: number, amount: number): MaybeThenable +// Producer API methods for async recorder storages, used by submitters in producer mode to pop data and post it to Split BE. +export interface IRecorderCacheProducerAsync { + /* returns the number of stored items */ + count(): Promise + /* removes the given number of items from the store. If not provided, it deletes all items */ + drop(count?: number): Promise + /* pops the given number of items from the store */ + popNWithMetadata(count: number): Promise } -export interface IImpressionCountsCacheSync extends IImpressionCountsCacheBase, IRecorderCacheSync<[string, number, number], Record> { - track(featureName: string, timeFrame: number, amount: number): void -} - -export interface IImpressionCountsCacheAsync extends IImpressionCountsCacheBase, IRecorderCacheAsync<[string, number, number]> { - track(featureName: string, timeFrame: number, amount: number): Promise +export interface IImpressionsCacheAsync extends IImpressionsCacheBase, IRecorderCacheProducerAsync { + // Consumer API method, used by impressions tracker (in standalone and consumer modes) to push data into. + track(data: ImpressionDTO[]): Promise } -/** Events cache */ - -export interface IEventsCacheBase extends IRecorderCacheProducerBase<[SplitIO.EventData, number | undefined]> { - track(data: SplitIO.EventData, size?: number): MaybeThenable +export interface IEventsCacheAsync extends IEventsCacheBase, IRecorderCacheProducerAsync { + // Consumer API method, used by events tracker (in standalone and consumer modes) to push data into. + track(data: SplitIO.EventData, size?: number): Promise } -export interface IEventsCacheSync extends IEventsCacheBase, IRecorderCacheSync<[SplitIO.EventData, number | undefined], SplitIO.EventData[]> { - track(data: SplitIO.EventData, size?: number): boolean, - setOnFullQueueCb(cb: () => void): void -} +/** + * Impression counts cache for impressions dedup in standalone and producer mode. + * Only in memory. Named `ImpressionsCounter` in spec. + */ +export interface IImpressionCountsCacheSync extends IRecorderCacheProducerSync> { + // Used by impressions tracker + track(featureName: string, timeFrame: number, amount: number): void -export interface IEventsCacheAsync extends IEventsCacheBase, IRecorderCacheAsync<[SplitIO.EventData, number | undefined]> { - track(data: SplitIO.EventData, size?: number): Promise + // Used by impressions count submitter in standalone and producer mode + isEmpty(): boolean // check if cache is empty. Return true if the cache was just created or cleared. + clear(): void // clear cache data + state(): Record // get cache data } -/** Latencies cache */ -export interface ILatenciesCacheBase extends IRecorderCacheProducerBase<[string, number]> { } +/** Latencies and metric counts cache */ +// @TODO remove. They are deprecated. -export interface ILatenciesCacheSync extends ILatenciesCacheBase, IRecorderCacheSync<[string, number], Record> { +export interface ILatenciesCacheSync extends IRecorderCacheProducerSync> { track(metricName: string, latency: number): boolean + isEmpty(): boolean + clear(): void + state(): Record } -export interface ILatenciesCacheAsync extends ILatenciesCacheBase, IRecorderCacheAsync<[string, number]> { +export interface ILatenciesCacheAsync { track(metricName: string, latency: number): Promise } -/** Counts cache */ - -export interface ICountsCacheBase extends IRecorderCacheProducerBase<[string]> { } - -export interface ICountsCacheSync extends ICountsCacheBase, IRecorderCacheSync<[string], Record> { +export interface ICountsCacheSync extends IRecorderCacheProducerSync> { track(metricName: string): boolean + isEmpty(): boolean + clear(): void + state(): Record } -export interface ICountsCacheAsync extends ICountsCacheBase, IRecorderCacheAsync<[string]> { +export interface ICountsCacheAsync { track(metricName: string): Promise } @@ -384,15 +391,14 @@ export interface IStorageBase< TSplitsCache extends ISplitsCacheBase, TSegmentsCache extends ISegmentsCacheBase, TImpressionsCache extends IImpressionsCacheBase, - TImpressionCountsCache extends IImpressionCountsCacheBase, TEventsCache extends IEventsCacheBase, - TLatenciesCache extends ILatenciesCacheBase, - TCountsCache extends ICountsCacheBase, + TLatenciesCache extends ILatenciesCacheSync | ILatenciesCacheAsync, + TCountsCache extends ICountsCacheSync | ICountsCacheAsync, > { splits: TSplitsCache, segments: TSegmentsCache, impressions: TImpressionsCache, - impressionCounts?: TImpressionCountsCache, + impressionCounts?: IImpressionCountsCacheSync, events: TEventsCache, latencies?: TLatenciesCache, counts?: TCountsCache, @@ -403,7 +409,6 @@ export type IStorageSync = IStorageBase< ISplitsCacheSync, ISegmentsCacheSync, IImpressionsCacheSync, - IImpressionCountsCacheSync, IEventsCacheSync, ILatenciesCacheSync, ICountsCacheSync @@ -417,7 +422,6 @@ export type IStorageAsync = IStorageBase< ISplitsCacheAsync, ISegmentsCacheAsync, IImpressionsCacheAsync, - IImpressionCountsCacheAsync, IEventsCacheAsync, ILatenciesCacheAsync, ICountsCacheAsync diff --git a/src/sync/submitters/impressionsSyncTask.ts b/src/sync/submitters/impressionsSyncTask.ts index c5ff1d02..cf6b6068 100644 --- a/src/sync/submitters/impressionsSyncTask.ts +++ b/src/sync/submitters/impressionsSyncTask.ts @@ -12,7 +12,7 @@ import { ILogger } from '../../logger/types'; */ export function fromImpressionsCollector(sendLabels: boolean, data: ImpressionDTO[]): ImpressionsPayload { let groupedByFeature = groupBy(data, 'feature'); - let dto: any[] = []; + let dto: ImpressionsPayload = []; // using forOwn instead of for...in since the last also iterates over prototype enumerables forOwn(groupedByFeature, (value, name) => { diff --git a/src/sync/submitters/submitterSyncTask.ts b/src/sync/submitters/submitterSyncTask.ts index cbee1913..ccff1b77 100644 --- a/src/sync/submitters/submitterSyncTask.ts +++ b/src/sync/submitters/submitterSyncTask.ts @@ -1,6 +1,6 @@ import syncTaskFactory from '../syncTask'; import { ISyncTask, ITimeTracker } from '../types'; -import { IRecorderCacheConsumerSync } from '../../storages/types'; +import { IRecorderCacheProducerSync } from '../../storages/types'; import { ILogger } from '../../logger/types'; import { SUBMITTERS_PUSH, SUBMITTERS_PUSH_FAILS, SUBMITTERS_PUSH_RETRY } from '../../logger/constants'; @@ -10,13 +10,13 @@ import { SUBMITTERS_PUSH, SUBMITTERS_PUSH_FAILS, SUBMITTERS_PUSH_RETRY } from '. export function submitterSyncTaskFactory( log: ILogger, postClient: (body: string) => Promise, - sourceCache: IRecorderCacheConsumerSync, + sourceCache: IRecorderCacheProducerSync, postRate: number, dataName: string, latencyTracker?: ITimeTracker, fromCacheToPayload?: (cacheData: TState) => any, maxRetries: number = 0, -): ISyncTask { +): ISyncTask<[], void> { let retries = 0; diff --git a/src/sync/submitters/types.ts b/src/sync/submitters/types.ts index 368f486e..6ab697c1 100644 --- a/src/sync/submitters/types.ts +++ b/src/sync/submitters/types.ts @@ -1,20 +1,64 @@ +import { IMetadata } from '../../dtos/types'; +import { SplitIO } from '../../types'; + export type ImpressionsPayload = { - f: string, // Split name - i: { // Key Impressions - k: string; // Key - t: string; // Treatment - m: number; // Timestamp - c: number; // ChangeNumber - r?: string; // Rule label - b?: string; // Bucketing Key - pt?: number; // Previous time + /** Split name */ + f: string, + /** Key Impressions */ + i: { + /** Key */ + k: string; + /** Treatment */ + t: string; + /** Timestamp */ + m: number; + /** ChangeNumber */ + c: number; + /** Rule label */ + r?: string; + /** Bucketing Key */ + b?: string; + /** Previous time */ + pt?: number; }[] }[] export type ImpressionCountsPayload = { pf: { - f: string // Split name - m: number // Time Frame - rc: number // Count + /** Split name */ + f: string + /** Time Frame */ + m: number + /** Count */ + rc: number }[] } + +export type StoredImpressionWithMetadata = { + /** Metadata */ + m: IMetadata, + /** Stored impression */ + i: { + /** keyName */ + k: string, + /** bucketingKey */ + b?: string, + /** Split name */ + f: string, + /** treatment */ + t: string, + /** label */ + r: string, + /** changeNumber */ + c: number, + /** time */ + m: number + } +} + +export type StoredEventWithMetadata = { + /** Metadata */ + m: IMetadata, + /** Stored event */ + e: SplitIO.EventData +} diff --git a/src/trackers/impressionsTracker.ts b/src/trackers/impressionsTracker.ts index 226e94c4..1c43f7fb 100644 --- a/src/trackers/impressionsTracker.ts +++ b/src/trackers/impressionsTracker.ts @@ -1,7 +1,7 @@ import objectAssign from 'object-assign'; import thenable from '../utils/promise/thenable'; import { truncateTimeFrame } from '../utils/time'; -import { IImpressionCountsCacheBase, IImpressionsCacheBase } from '../storages/types'; +import { IImpressionCountsCacheSync, IImpressionsCacheBase } from '../storages/types'; import { IImpressionsHandler, IImpressionsTracker } from './types'; import { SplitIO, ImpressionDTO, ISettings } from '../types'; import { IImpressionObserver } from './impressionObserver/types'; @@ -30,7 +30,7 @@ export default function impressionsTrackerFactory( // if observer is provided, it implies `shouldAddPreviousTime` flag (i.e., if impressions previous time should be added or not) observer?: IImpressionObserver, // if countsCache is provided, it implies `isOptimized` flag (i.e., if impressions should be deduped or not) - countsCache?: IImpressionCountsCacheBase + countsCache?: IImpressionCountsCacheSync ): IImpressionsTracker { return { diff --git a/src/utils/inputValidation/eventProperties.ts b/src/utils/inputValidation/eventProperties.ts index 8a599a97..b2bf20e6 100644 --- a/src/utils/inputValidation/eventProperties.ts +++ b/src/utils/inputValidation/eventProperties.ts @@ -10,7 +10,7 @@ const ECMA_SIZES = { NUMBER: 8 }; const MAX_PROPERTIES_AMOUNT = 300; -const MAX_PROPERTIES_SIZE = 1024 * 32; +const MAX_EVENT_SIZE = 1024 * 32; const BASE_EVENT_SIZE = 1024; // We assume 1kb events without properties (avg measured) export function validateEventProperties(log: ILogger, maybeProperties: any, method: string): { properties: SplitIO.Properties | null | false, size: number } { @@ -55,7 +55,7 @@ export function validateEventProperties(log: ILogger, maybeProperties: any, meth else if (isBoolVal) output.size += ECMA_SIZES.BOOLEAN; else if (isStringVal) output.size += val.length * ECMA_SIZES.STRING; - if (output.size > MAX_PROPERTIES_SIZE) { + if (output.size > MAX_EVENT_SIZE) { log.error(ERROR_SIZE_EXCEEDED, [method]); output.properties = false; break; From 552a9d708cdaea1ceb21c76049f1aa968d447152 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Fri, 14 May 2021 14:12:33 -0300 Subject: [PATCH 057/144] updated UTs --- .../inMemory/ImpressionsCacheInMemory.ts | 1 - src/storages/inRedis/EventsCacheInRedis.ts | 3 +- .../inRedis/ImpressionsCacheInRedis.ts | 5 +- src/storages/inRedis/SplitsCacheInRedis.ts | 2 +- .../__tests__/ImpressionsCacheInRedis.spec.ts | 169 ++++++++---------- .../pluggable/EventsCachePluggable.ts | 4 +- .../pluggable/ImpressionsCachePluggable.ts | 17 +- .../ImpressionsCachePluggable.spec.ts | 66 ++++--- src/storages/types.ts | 6 +- 9 files changed, 134 insertions(+), 139 deletions(-) diff --git a/src/storages/inMemory/ImpressionsCacheInMemory.ts b/src/storages/inMemory/ImpressionsCacheInMemory.ts index 2e4b0b53..9b9e3faa 100644 --- a/src/storages/inMemory/ImpressionsCacheInMemory.ts +++ b/src/storages/inMemory/ImpressionsCacheInMemory.ts @@ -10,7 +10,6 @@ export default class ImpressionsCacheInMemory implements IImpressionsCacheSync { */ track(data: ImpressionDTO[]) { this.queue.push(...data); - return true; } /** diff --git a/src/storages/inRedis/EventsCacheInRedis.ts b/src/storages/inRedis/EventsCacheInRedis.ts index 8002d711..929d53c4 100644 --- a/src/storages/inRedis/EventsCacheInRedis.ts +++ b/src/storages/inRedis/EventsCacheInRedis.ts @@ -22,6 +22,7 @@ export default class EventsCacheInRedis implements IEventsCacheAsync { /** * Add a new event object into the queue. + * Unlike `impressions::track`, result promise is never rejected. */ track(eventData: SplitIO.EventData): Promise { return this.redis.rpush( @@ -31,7 +32,7 @@ export default class EventsCacheInRedis implements IEventsCacheAsync { // We use boolean values to signal successful queueing .then(() => true) .catch(err => { - this.log.error(LOG_PREFIX + `Error adding event to queue: ${err}.`); + this.log.error(`${LOG_PREFIX}Error adding event to queue: ${err}.`); return false; }); } diff --git a/src/storages/inRedis/ImpressionsCacheInRedis.ts b/src/storages/inRedis/ImpressionsCacheInRedis.ts index d353a212..7f399e64 100644 --- a/src/storages/inRedis/ImpressionsCacheInRedis.ts +++ b/src/storages/inRedis/ImpressionsCacheInRedis.ts @@ -21,16 +21,15 @@ export default class ImpressionsCacheInRedis implements IImpressionsCacheAsync { this.metadata = metadata; } - track(impressions: ImpressionDTO[]): Promise { + track(impressions: ImpressionDTO[]): Promise { // @ts-ignore return this.redis.rpush( this.key, this._toJSON(impressions) ).then(queuedCount => { // If this is the creation of the key on Redis, set the expiration for it in 1hr. if (queuedCount === impressions.length) { - return this.redis.expire(this.key, IMPRESSIONS_TTL_REFRESH).then(result => result === 1); // 1 if the timeout was set. 0 if key does not exist. + return this.redis.expire(this.key, IMPRESSIONS_TTL_REFRESH); } - return true; }); } diff --git a/src/storages/inRedis/SplitsCacheInRedis.ts b/src/storages/inRedis/SplitsCacheInRedis.ts index 008a2bc2..2e9d750f 100644 --- a/src/storages/inRedis/SplitsCacheInRedis.ts +++ b/src/storages/inRedis/SplitsCacheInRedis.ts @@ -62,7 +62,7 @@ export default class SplitsCacheInRedis extends AbstractSplitsCacheAsync { /** * Add a given split. * The returned promise is resolved when the operation success - * or rejected with if it fails (e.g., redis operation fails) + * or rejected with an SplitError if it fails (e.g., redis operation fails) */ addSplit(name: string, split: string): Promise { const splitKey = this.keys.buildSplitKey(name); diff --git a/src/storages/inRedis/__tests__/ImpressionsCacheInRedis.spec.ts b/src/storages/inRedis/__tests__/ImpressionsCacheInRedis.spec.ts index 83eb886b..b738840a 100644 --- a/src/storages/inRedis/__tests__/ImpressionsCacheInRedis.spec.ts +++ b/src/storages/inRedis/__tests__/ImpressionsCacheInRedis.spec.ts @@ -2,105 +2,90 @@ import Redis from '../RedisAdapter'; import ImpressionsCacheInRedis from '../ImpressionsCacheInRedis'; import IORedis, { BooleanResponse } from 'ioredis'; import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; +import { fakeMetadata, o1, o2, o3, o1stored, o2stored, o3stored } from '../../pluggable/__tests__/ImpressionsCachePluggable.spec'; -const fakeMetadata = { s: 'js_someversion', i: 'some_ip', n: 'some_hostname' }; - -test('IMPRESSIONS CACHE IN REDIS / should incrementally store values', async () => { - const connection = new Redis(loggerMock, {}); - const impressionsKey = 'impr_cache_ut.impressions'; - - const c = new ImpressionsCacheInRedis(loggerMock, impressionsKey, connection, fakeMetadata); - - const o1 = { - feature: 'test1', - keyName: 'facundo@split.io', - treatment: 'on', - time: Date.now(), - changeNumber: 1 - }; - - const o2 = { - feature: 'test2', - keyName: 'pepep@split.io', - treatment: 'A', - time: Date.now(), - bucketingKey: '1234-5678', - label: 'is in segment', - changeNumber: 1 - }; - - const o3 = { - feature: 'test3', - keyName: 'pipiip@split.io', - treatment: 'B', - time: Date.now(), - changeNumber: 1 - }; - - // cleanup - await connection.del(impressionsKey); - - // @ts-expect-error - await c.track([o1]); // @ts-expect-error - await c.track([o2, o3]); - const state = await connection.lrange(impressionsKey, 0, -1); - // This is testing both the track and the toJSON method. - expect(state[0]).toBe(JSON.stringify({ - m: fakeMetadata, - i: { k: o1.keyName, f: o1.feature, t: o1.treatment, c: o1.changeNumber, m: o1.time } - })); - expect(state[1]).toBe(JSON.stringify({ - m: fakeMetadata, - i: { k: o2.keyName, b: o2.bucketingKey, f: o2.feature, t: o2.treatment, r: o2.label, c: o2.changeNumber, m: o2.time } - })); - expect(state[2]).toBe(JSON.stringify({ - m: fakeMetadata, - i: { k: o3.keyName, f: o3.feature, t: o3.treatment, c: o3.changeNumber, m: o3.time } - })); - - await connection.del(impressionsKey); - await connection.quit(); -}); +describe('IMPRESSIONS CACHE IN REDIS', () => { + + test('`track`, `count`, `popNWithMetadata` and `drop` methods', async () => { + const connection = new Redis(loggerMock, {}); + const impressionsKey = 'impr_cache_ut.impressions'; + + const c = new ImpressionsCacheInRedis(loggerMock, impressionsKey, connection, fakeMetadata); + + // cleanup + await connection.del(impressionsKey); -test('IMPRESSIONS CACHE IN REDIS / should not resolve track before calling expire', async (done) => { - const impressionsKey = 'impr_cache_ut_2.impressions'; - const connection = new Redis(loggerMock, {}); + // Testing track and count methods. + await c.track([o1]); + expect(await c.count()).toBe(1); // count should return stored items - const c = new ImpressionsCacheInRedis(loggerMock, impressionsKey, connection, fakeMetadata); + await c.track([o2, o3]); + expect(await c.count()).toBe(3); // count should return stored items - const i1 = { feature: 'test4', keyName: 'nicolas@split.io', treatment: 'off', time: Date.now(), changeNumber: 1 }; - const i2 = { feature: 'test5', keyName: 'matias@split.io', treatment: 'on', time: Date.now(), changeNumber: 2 }; + // Impressions should be in redis + const state = await connection.lrange(impressionsKey, 0, -1); + expect(state[0]).toBe(JSON.stringify(o1stored)); + expect(state[1]).toBe(JSON.stringify(o2stored)); + expect(state[2]).toBe(JSON.stringify(o3stored)); - const spy1 = jest.spyOn(connection, 'rpush'); - const spy2 = jest.spyOn(connection, 'expire'); + // Testing popNWithMetadata and private toJSON methods. + expect(await c.popNWithMetadata(2)).toEqual([o1stored, o2stored]); // impressions are pop in FIFO order + expect(await c.count()).toBe(1); - // Crap so we can reproduce the latency as we would have on a remote server. - const originalExpire = connection.expire; + expect(await c.popNWithMetadata(1)).toEqual([o3stored]); + expect(await c.count()).toBe(0); - connection.expire = function patchedForTestRedisExpire(...args: [key: IORedis.KeyType, seconds: number]) { - return new Promise((res, rej) => { - setTimeout(() => { - originalExpire.apply(connection, args).then(res).catch(rej); - }, 150); // 150ms of delay on the expire + // Testing drop method + await c.track([o1, o2, o3]); + expect(await c.count()).toBe(3); + await c.drop(); + expect(await c.count()).toBe(0); // storage should be empty after droping the storage + + await connection.del(impressionsKey); + await connection.quit(); + }); + + test('`track` should not resolve before calling expire', async (done) => { + const impressionsKey = 'impr_cache_ut_2.impressions'; + const connection = new Redis(loggerMock, {}); + + const c = new ImpressionsCacheInRedis(loggerMock, impressionsKey, connection, fakeMetadata); + + const i1 = { feature: 'test4', keyName: 'nicolas@split.io', treatment: 'off', time: Date.now(), changeNumber: 1 }; + const i2 = { feature: 'test5', keyName: 'matias@split.io', treatment: 'on', time: Date.now(), changeNumber: 2 }; + + const spy1 = jest.spyOn(connection, 'rpush'); + const spy2 = jest.spyOn(connection, 'expire'); + + // Crap so we can reproduce the latency as we would have on a remote server. + const originalExpire = connection.expire; + + connection.expire = function patchedForTestRedisExpire(...args: [key: IORedis.KeyType, seconds: number]) { + return new Promise((res, rej) => { + setTimeout(() => { + originalExpire.apply(connection, args).then(res).catch(rej); + }, 150); // 150ms of delay on the expire + }); + }; + + // cleanup prior to test. + await connection.del(impressionsKey); + + // @ts-expect-error + c.track([i1, i2]).then(() => { + connection.del(impressionsKey); + connection.quit(); // Try to disconnect right away. + expect(spy1).toBeCalled(); // Redis rpush was called once before executing external callback. + // Following assertion fails if the expire takes place after disconnected and throws unhandledPromiseRejection + expect(spy2).toBeCalled(); // Redis expire was called once before executing external callback. + }).catch(e => { + throw new Error(`An error was generated from the redis expire tests: ${e}`); + }).then(() => { + // Finally clean up and wrap up. + spy1.mockRestore(); + spy2.mockRestore(); + done(); }); - }; - - // cleanup prior to test. - await connection.del(impressionsKey); - - // @ts-expect-error - c.track([i1, i2]).then(() => { - connection.del(impressionsKey); - connection.quit(); // Try to disconnect right away. - expect(spy1).toBeCalled(); // Redis rpush was called once before executing external callback. - // Following assertion fails if the expire takes place after disconnected and throws unhandledPromiseRejection - expect(spy2).toBeCalled(); // Redis expire was called once before executing external callback. - }).catch(e => { - throw new Error(`An error was generated from the redis expire tests: ${e}`); - }).then(() => { - // Finally clean up and wrap up. - spy1.mockRestore(); - spy2.mockRestore(); - done(); }); + }); diff --git a/src/storages/pluggable/EventsCachePluggable.ts b/src/storages/pluggable/EventsCachePluggable.ts index eb24e2fb..2df563ce 100644 --- a/src/storages/pluggable/EventsCachePluggable.ts +++ b/src/storages/pluggable/EventsCachePluggable.ts @@ -23,7 +23,7 @@ export class EventsCachePluggable implements IEventsCacheAsync { * Push given event to the storage. * @param eventData Event item to push. * @returns A promise that is resolved with a boolean value indicating if the push operation succeeded or failed. - * The promise will never be rejected. + * Unlike `impressions::track`, The promise will never be rejected. */ track(eventData: SplitIO.EventData): Promise { return this.wrapper.pushItems( @@ -33,7 +33,7 @@ export class EventsCachePluggable implements IEventsCacheAsync { // We use boolean values to signal successful queueing .then(() => true) .catch(e => { - this.log.error(LOG_PREFIX + ` Error adding event to queue: ${e}.`); + this.log.error(`${LOG_PREFIX}Error adding event to queue: ${e}.`); return false; }); } diff --git a/src/storages/pluggable/ImpressionsCachePluggable.ts b/src/storages/pluggable/ImpressionsCachePluggable.ts index 0ddad1c2..debe30ed 100644 --- a/src/storages/pluggable/ImpressionsCachePluggable.ts +++ b/src/storages/pluggable/ImpressionsCachePluggable.ts @@ -2,7 +2,6 @@ import { ICustomStorageWrapper, IImpressionsCacheAsync } from '../types'; import { IMetadata } from '../../dtos/types'; import { ImpressionDTO } from '../../types'; import { ILogger } from '../../logger/types'; -import { LOG_PREFIX } from './constants'; import { StoredImpressionWithMetadata } from '../../sync/submitters/types'; export class ImpressionsCachePluggable implements IImpressionsCacheAsync { @@ -22,20 +21,14 @@ export class ImpressionsCachePluggable implements IImpressionsCacheAsync { /** * Push given impressions to the storage. * @param impressions List of impresions to push. - * @returns A promise that is resolved with a boolean value indicating if the push operation succeeded or failed. - * The promise will never be rejected. + * @returns A promise that is resolved if the push operation succeeded + * or rejected if the wrapper operation fails. */ - track(impressions: ImpressionDTO[]): Promise { + track(impressions: ImpressionDTO[]): Promise { return this.wrapper.pushItems( this.key, this._toJSON(impressions) - ) - // We use boolean values to signal successful queueing - .then(() => true) - .catch((e) => { - this.log.error(LOG_PREFIX + ` Error adding event to queue: ${e}.`); - return false; - }); + ); } private _toJSON(impressions: ImpressionDTO[]): string[] { @@ -71,7 +64,7 @@ export class ImpressionsCachePluggable implements IImpressionsCacheAsync { * Removes the given number of impressions from the store. If a number is not provided, it deletes all items. * The returned promise rejects if the wrapper operation fails. */ - drop(count?: number): Promise { + drop(count?: number): Promise { // @ts-ignore if (!count) return this.wrapper.del(this.key); return this.wrapper.popItems(this.key, count).then(() => { }); diff --git a/src/storages/pluggable/__tests__/ImpressionsCachePluggable.spec.ts b/src/storages/pluggable/__tests__/ImpressionsCachePluggable.spec.ts index 20693579..01384d9f 100644 --- a/src/storages/pluggable/__tests__/ImpressionsCachePluggable.spec.ts +++ b/src/storages/pluggable/__tests__/ImpressionsCachePluggable.spec.ts @@ -1,4 +1,3 @@ - import { ImpressionsCachePluggable } from '../ImpressionsCachePluggable'; import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; import { wrapperMock } from './wrapper.mock'; @@ -6,7 +5,8 @@ import { IMetadata } from '../../../dtos/types'; const prefix = 'impr_cache_ut'; const impressionsKey = `${prefix}.impressions`; -const testMeta: IMetadata = { i: 'some_ip', n: 'some_host', s: 'some_sdk_version' }; + +const fakeMetadata: IMetadata = { s: 'js_someversion', i: 'some_ip', n: 'some_hostname' }; const o1 = { feature: 'test1', @@ -17,6 +17,11 @@ const o1 = { changeNumber: 1 }; +const o1stored = { + m: fakeMetadata, + i: { k: o1.keyName, f: o1.feature, t: o1.treatment, r: o1.label, c: o1.changeNumber, m: o1.time } +}; + const o2 = { feature: 'test2', keyName: 'pepep@split.io', @@ -27,6 +32,11 @@ const o2 = { changeNumber: 1 }; +const o2stored = { + m: fakeMetadata, + i: { k: o2.keyName, b: o2.bucketingKey, f: o2.feature, t: o2.treatment, r: o2.label, c: o2.changeNumber, m: o2.time } +}; + const o3 = { feature: 'test3', keyName: 'pipiip@split.io', @@ -36,6 +46,13 @@ const o3 = { changeNumber: 1 }; +const o3stored = { + m: fakeMetadata, + i: { k: o3.keyName, f: o3.feature, t: o3.treatment, r: o3.label, c: o3.changeNumber, m: o3.time } +}; + +export { fakeMetadata, o1, o2, o3, o1stored, o2stored, o3stored }; + describe('PLUGGABLE IMPRESSIONS CACHE', () => { afterEach(() => { @@ -43,40 +60,41 @@ describe('PLUGGABLE IMPRESSIONS CACHE', () => { wrapperMock.mockClear(); }); - test('`track` method should push values into the pluggable storage', async () => { - const cache = new ImpressionsCachePluggable(loggerMock, impressionsKey, wrapperMock, testMeta); - - expect(await cache.track([o1])).toBe(true); // result should resolve to true + test('`track`, `count`, `popNWithMetadata` and `drop` methods', async () => { + const cache = new ImpressionsCachePluggable(loggerMock, impressionsKey, wrapperMock, fakeMetadata); + // Testing track and count methods. + await cache.track([o1]); let state = wrapperMock._cache[impressionsKey] as string[]; expect(state.length).toBe(1); // impression should be stored + expect(await cache.count()).toBe(1); // count should return stored items - expect(await cache.track([o2, o3])).toBe(true); - + await cache.track([o2, o3]); state = wrapperMock._cache[impressionsKey] as string[]; expect(state.length).toBe(3); - // This is testing both the track and the toJSON method. - expect(state[0]).toBe(JSON.stringify({ - m: testMeta, - i: { k: o1.keyName, f: o1.feature, t: o1.treatment, r: o1.label, c: o1.changeNumber, m: o1.time } - })); - expect(state[1]).toBe(JSON.stringify({ - m: testMeta, - i: { k: o2.keyName, b: o2.bucketingKey, f: o2.feature, t: o2.treatment, r: o2.label, c: o2.changeNumber, m: o2.time } - })); - expect(state[2]).toBe(JSON.stringify({ - m: testMeta, - i: { k: o3.keyName, f: o3.feature, t: o3.treatment, r: o3.label, c: o3.changeNumber, m: o3.time } - })); + expect(await cache.count()).toBe(3); // count should return stored items + + // Testing popNWithMetadata and private toJSON methods. + expect(await cache.popNWithMetadata(2)).toEqual([o1stored, o2stored]); // impressions are pop in FIFO order + expect(await cache.count()).toBe(1); + + expect(await cache.popNWithMetadata(1)).toEqual([o3stored]); + expect(await cache.count()).toBe(0); + + // Testing drop method + await cache.track([o1, o2, o3]); + expect(await cache.count()).toBe(3); + await cache.drop(); + expect(await cache.count()).toBe(0); // storage should be empty after droping the storage }); - test('`track` method result should not reject if wrapper operation fails', async () => { + test('`track` method rejects if wrapper operation fails', (done) => { // make wrapper operation fail wrapperMock.pushItems.mockImplementation(() => Promise.reject()); - const cache = new ImpressionsCachePluggable(loggerMock, impressionsKey, wrapperMock, testMeta); + const cache = new ImpressionsCachePluggable(loggerMock, impressionsKey, wrapperMock, fakeMetadata); - expect(await cache.track([o1])).toBe(false); // result should resolve to false + cache.track([o1]).catch(done); // result should rejects }); }); diff --git a/src/storages/types.ts b/src/storages/types.ts index 31f27234..b97e33e5 100644 --- a/src/storages/types.ts +++ b/src/storages/types.ts @@ -290,7 +290,7 @@ export interface ISegmentsCacheAsync extends ISegmentsCacheBase { export interface IImpressionsCacheBase { // Consumer API method, used by impressions tracker, in standalone and consumer modes, to push impressions into the storage. - track(data: ImpressionDTO[]): MaybeThenable + track(data: ImpressionDTO[]): MaybeThenable } export interface IEventsCacheBase { @@ -313,7 +313,7 @@ export interface IRecorderCacheProducerSync { export interface IImpressionsCacheSync extends IImpressionsCacheBase, IRecorderCacheProducerSync { - track(data: ImpressionDTO[]): boolean + track(data: ImpressionDTO[]): void } export interface IEventsCacheSync extends IEventsCacheBase, IRecorderCacheProducerSync { @@ -335,7 +335,7 @@ export interface IRecorderCacheProducerAsync { export interface IImpressionsCacheAsync extends IImpressionsCacheBase, IRecorderCacheProducerAsync { // Consumer API method, used by impressions tracker (in standalone and consumer modes) to push data into. - track(data: ImpressionDTO[]): Promise + track(data: ImpressionDTO[]): Promise } export interface IEventsCacheAsync extends IEventsCacheBase, IRecorderCacheProducerAsync { From 400c21e40b3e45350d242f843147296754935cd2 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Fri, 14 May 2021 15:32:00 -0300 Subject: [PATCH 058/144] feedback --- src/storages/types.ts | 87 ++++++++++++++++++++++--------------------- 1 file changed, 45 insertions(+), 42 deletions(-) diff --git a/src/storages/types.ts b/src/storages/types.ts index e04f9f2d..49673fb3 100644 --- a/src/storages/types.ts +++ b/src/storages/types.ts @@ -6,23 +6,24 @@ import { SplitIO, ImpressionDTO } from '../types'; * Interface of a custom wrapper storage. */ export interface ICustomStorageWrapper { + /** Key-Value operations */ /** * Get the value of given `key`. * * @function get - * @param {string} key item to retrieve - * @returns {Promise} A promise that resolves with the element value associated with the specified `key`, or null if the key does not exist. - * The promise rejects if the operation fails. + * @param {string} key Item to retrieve + * @returns {Promise} A promise that resolves with the element value associated with the specified `key`, + * or null if the key does not exist. The promise rejects if the operation fails. */ get: (key: string) => Promise /** * Add or update an item with a specified `key` and `value`. * * @function set - * @param {string} key item to update - * @param {string} value value to set + * @param {string} key Item to update + * @param {string} value Value to set * @returns {Promise} A promise that resolves if the operation success, whether the key was added or updated. * The promise rejects if the operation fails. */ @@ -31,8 +32,8 @@ export interface ICustomStorageWrapper { * Add or update an item with a specified `key` and `value`. * * @function getAndSet - * @param {string} key item to update - * @param {string} value value to set + * @param {string} key Item to update + * @param {string} value Value to set * @returns {Promise} A promise that resolves with the previous value associated to the given `key`, or null if not set. * The promise rejects if the operation fails. */ @@ -41,16 +42,16 @@ export interface ICustomStorageWrapper { * Removes the specified item by `key`. * * @function del - * @param {string} key item to delete + * @param {string} key Item to delete * @returns {Promise} A promise that resolves if the operation success, whether the key existed and was removed or it didn't exist. - * The promise rejects if the operation fails, for example, if there is a connectin error. + * The promise rejects if the operation fails, for example, if there is a connection error. */ del: (key: string) => Promise /** * Returns all keys matching the given prefix. * * @function getKeysByPrefix - * @param {string} prefix string prefix to match + * @param {string} prefix String prefix to match * @returns {Promise} A promise that resolves with the list of keys that match the given `prefix`. * The promise rejects if the operation fails. */ @@ -59,7 +60,7 @@ export interface ICustomStorageWrapper { * Returns all values which keys match the given prefix. * * @function getByPrefix - * @param {string} prefix + * @param {string} prefix String prefix to match * @returns {Promise} A promise that resolves with the list of values which keys match the given `prefix`. * The promise rejects if the operation fails. */ @@ -68,9 +69,9 @@ export interface ICustomStorageWrapper { * Returns the values of all given `keys`. * * @function getMany - * @param {string[]} keys list of keys to retrieve - * @returns {Promise<(string | null)[]>} A promise that resolves with the list of items associated with the specified list of `keys`. For every key that does not hold a string value or does not exist, null is returned. - * The promise rejects if the operation fails. + * @param {string[]} keys List of keys to retrieve + * @returns {Promise<(string | null)[]>} A promise that resolves with the list of items associated with the specified list of `keys`. + * For every key that does not hold a string value or does not exist, null is returned. The promise rejects if the operation fails. */ getMany: (keys: string[]) => Promise<(string | null)[]> @@ -80,18 +81,18 @@ export interface ICustomStorageWrapper { * Increments in 1 the given `key` value or set it in 1 if the value doesn't exist. * * @function incr - * @param {string} key key to increment - * @returns {Promise} A promise that resolves if the operation success. - * The promise rejects if the operation fails, for example, if there is a connectin error or the key contains a string that can not be represented as integer. + * @param {string} key Key to increment + * @returns {Promise} A promise that resolves if the operation success. The promise rejects if the operation fails, + * for example, if there is a connection error or the key contains a string that can not be represented as integer. */ incr: (key: string) => Promise /** * Decrements in 1 the given `key` value or set it in -1 if the value doesn't exist. * * @function decr - * @param {string} key key to decrement - * @returns {Promise} A promise that resolves if the operation success. - * The promise rejects if the operation fails, for example, if there is a connectin error or the key contains a string that can not be represented as integer. + * @param {string} key Key to decrement + * @returns {Promise} A promise that resolves if the operation success. The promise rejects if the operation fails, + * for example, if there is a connection error or the key contains a string that can not be represented as integer. */ decr: (key: string) => Promise @@ -101,29 +102,29 @@ export interface ICustomStorageWrapper { * Inserts given items at the tail of `key` list. If `key` does not exist, an empty list is created before pushing the items. * * @function pushItems - * @param {string} key list key - * @param {string[]} items list of items to push + * @param {string} key List key + * @param {string[]} items List of items to push * @returns {Promise} A promise that resolves if the operation success. - * The promise rejects if the operation fails, for example, if there is a connectin error or the key holds a value that is not a list. + * The promise rejects if the operation fails, for example, if there is a connection error or the key holds a value that is not a list. */ pushItems: (key: string, items: string[]) => Promise /** * Removes and returns the first `count` items from a list. If `key` does not exist, an empty list is items is returned. * * @function popItems - * @param {string} key list key - * @param {number} count number of items to pop + * @param {string} key List key + * @param {number} count Number of items to pop * @returns {Promise} A promise that resolves with the list of removed items from the list, or an empty array when key does not exist. - * The promise rejects if the operation fails, for example, if there is a connectin error or the key holds a value that is not a list. + * The promise rejects if the operation fails, for example, if there is a connection error or the key holds a value that is not a list. */ popItems: (key: string, count: number) => Promise /** * Returns the count of items in a list, or 0 if `key` does not exist. * * @function getItemsCount - * @param {string} key list key + * @param {string} key List key * @returns {Promise} A promise that resolves with the number of items at the `key` list, or 0 when `key` does not exist. - * The promise rejects if the operation fails, for example, if there is a connectin error or the key holds a value that is not a list. + * The promise rejects if the operation fails, for example, if there is a connection error or the key holds a value that is not a list. */ getItemsCount: (key: string) => Promise @@ -133,10 +134,11 @@ export interface ICustomStorageWrapper { * Returns if item is a member of a set. * * @function itemContains - * @param {string} key set key - * @param {string} item item value - * @returns {Promise} A promise that resolves with true boolean value if `item` is a member of the set stored at `key`, or false if it is not a member or `key` set does not exist. - * The promise rejects if the operation fails, for example, if there is a connectin error or the key holds a value that is not a set. + * @param {string} key Set key + * @param {string} item Item value + * @returns {Promise} A promise that resolves with true boolean value if `item` is a member of the set stored at `key`, + * or false if it is not a member or `key` set does not exist. The promise rejects if the operation fails, for example, + * if there is a connection error or the key holds a value that is not a set. */ itemContains: (key: string, item: string) => Promise /** @@ -144,29 +146,29 @@ export interface ICustomStorageWrapper { * If key does not exist, an empty set is created before adding the items. * * @function addItems - * @param {string} key set key - * @param {string} items items to add + * @param {string} key Set key + * @param {string} items Items to add * @returns {Promise} A promise that resolves if the operation success. - * The promise rejects if the operation fails, for example, if there is a connectin error or the key holds a value that is not a set. + * The promise rejects if the operation fails, for example, if there is a connection error or the key holds a value that is not a set. */ addItems: (key: string, items: string[]) => Promise /** * Remove the specified `items` from the set stored at `key`. Those items that are not part of the set are ignored. * * @function removeItems - * @param {string} key set key - * @param {string} items items to remove + * @param {string} key Set key + * @param {string} items Items to remove * @returns {Promise} A promise that resolves if the operation success. If key does not exist, the promise also resolves. - * The promise rejects if the operation fails, for example, if there is a connectin error or the key holds a value that is not a set. + * The promise rejects if the operation fails, for example, if there is a connection error or the key holds a value that is not a set. */ removeItems: (key: string, items: string[]) => Promise /** * Returns all the items of the `key` set. * * @function getItems - * @param {string} key set key + * @param {string} key Set key * @returns {Promise} A promise that resolves with the list of items. If key does not exist, the result is an empty list. - * The promise rejects if the operation fails, for example, if there is a connectin error or the key holds a value that is not a set. + * The promise rejects if the operation fails, for example, if there is a connection error or the key holds a value that is not a set. */ getItems: (key: string) => Promise @@ -436,8 +438,9 @@ export interface IStorageFactoryParams { matchingKey?: string, /* undefined on server-side SDKs */ splitFiltersValidation?: ISplitFiltersValidation, - // This callback is invoked when the storage is ready to be used. If an error is passed, it means that the storge fail to connect and shouldn't be used. - // It is meant for emitting SDK_READY event in consumer mode, and for synchronizer tasks to wait before using the storage. + // This callback is invoked when the storage is ready to be used. Error-first callback style: if an error is passed, + // it means that the storge fail to connect and shouldn't be used. + // It is meant for emitting SDK_READY event in consumer mode, and for synchronizer to wait before using the storage. onReadyCb?: (error?: any) => void, metadata: IMetadata, } From ddee304c8b1efa832e53e7ffc18dd1521d86c4d8 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Fri, 14 May 2021 17:02:49 -0300 Subject: [PATCH 059/144] updated UTs --- src/sdkClient/client.ts | 4 +- src/storages/inRedis/EventsCacheInRedis.ts | 8 ++- .../__tests__/EventsCacheInRedis.spec.ts | 46 +++++--------- .../__tests__/ImpressionsCacheInRedis.spec.ts | 6 +- .../pluggable/EventsCachePluggable.ts | 6 +- .../__tests__/EventsCachePluggable.spec.ts | 62 +++++++++---------- .../ImpressionsCachePluggable.spec.ts | 5 +- src/types.ts | 2 +- 8 files changed, 68 insertions(+), 71 deletions(-) diff --git a/src/sdkClient/client.ts b/src/sdkClient/client.ts index e9f178d8..c6889678 100644 --- a/src/sdkClient/client.ts +++ b/src/sdkClient/client.ts @@ -103,7 +103,7 @@ export default function clientFactory(params: IClientFactoryParams): SplitIO.ICl function track(key: SplitIO.SplitKey, trafficTypeName: string, eventTypeId: string, value?: number, properties?: SplitIO.Properties, size = 1024) { const matchingKey = getMatching(key); const timestamp = Date.now(); - const eventData = { + const eventData: SplitIO.EventData = { eventTypeId, trafficTypeName, value, @@ -115,7 +115,7 @@ export default function clientFactory(params: IClientFactoryParams): SplitIO.ICl // This may be async but we only warn, we don't actually care if it is valid or not in terms of queueing the event. validateTrafficTypeExistance(log, readinessManager, storage.splits, mode, trafficTypeName, 'track'); - return eventTracker.track(eventData as SplitIO.EventData, size); + return eventTracker.track(eventData, size); } return { diff --git a/src/storages/inRedis/EventsCacheInRedis.ts b/src/storages/inRedis/EventsCacheInRedis.ts index 929d53c4..f4bc133b 100644 --- a/src/storages/inRedis/EventsCacheInRedis.ts +++ b/src/storages/inRedis/EventsCacheInRedis.ts @@ -57,7 +57,13 @@ export default class EventsCacheInRedis implements IEventsCacheAsync { return this.redis.ltrim(this.key, count, -1); } - // @TODO follow Go implementation + /** + * Pop the given number of events from the storage. + * The returned promise rejects if the wrapper operation fails. + * + * NOTE: this method doesn't take into account MAX_EVENT_SIZE or MAX_QUEUE_BYTE_SIZE limits. + * It is the submitter responsability to handle that. + */ popNWithMetadata(count: number): Promise { return this.redis.lrange(this.key, 0, count - 1).then(items => { return this.redis.ltrim(this.key, items.length, -1).then(() => { diff --git a/src/storages/inRedis/__tests__/EventsCacheInRedis.spec.ts b/src/storages/inRedis/__tests__/EventsCacheInRedis.spec.ts index 20ce856e..e78b8ae9 100644 --- a/src/storages/inRedis/__tests__/EventsCacheInRedis.spec.ts +++ b/src/storages/inRedis/__tests__/EventsCacheInRedis.spec.ts @@ -1,20 +1,14 @@ import Redis from 'ioredis'; -import find from 'lodash/find'; -import isEqual from 'lodash/isEqual'; import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; import EventsCacheInRedis from '../EventsCacheInRedis'; +import { fakeMetadata, fakeEvent1, fakeEvent1stored, fakeEvent2, fakeEvent2stored, fakeEvent3, fakeEvent3stored } from '../../pluggable/__tests__/EventsCachePluggable.spec'; const prefix = 'events_cache_ut'; -const eventsKey = `${prefix}.impressions`; -const fakeMetadata = { s: 'js_someversion', i: 'some_ip', n: 'some_hostname' }; +const eventsKey = `${prefix}.events`; -test('EVENTS CACHE IN REDIS / should incrementally store values in redis', async () => { +test('EVENTS CACHE IN REDIS / `track`, `count`, `popNWithMetadata` and `drop` methods', async () => { const connection = new Redis(); - const fakeEvent1 = { event: 1 }; - const fakeEvent2 = { event: '2' }; - const fakeEvent3 = { event: null }; - // Clean up in case there are still keys there. await connection.del(eventsKey); @@ -28,40 +22,34 @@ test('EVENTS CACHE IN REDIS / should incrementally store values in redis', async const faultyCache = new EventsCacheInRedis(loggerMock, 'non-list-key', connection, fakeMetadata); - // @ts-expect-error expect(await cache.track(fakeEvent1)).toBe(true); // If the queueing operation was successful, it should resolve the returned promise with "true" - // @ts-expect-error expect(await cache.track(fakeEvent2)).toBe(true); // If the queueing operation was successful, it should resolve the returned promise with "true" - // @ts-expect-error expect(await cache.track(fakeEvent3)).toBe(true); // If the queueing operation was successful, it should resolve the returned promise with "true" - // @ts-expect-error expect(await faultyCache.track(fakeEvent1)).toBe(false); // If the queueing operation was NOT successful, it should resolve the returned promise with "false" instead of rejecting it. - // @ts-expect-error expect(await faultyCache.track(fakeEvent2)).toBe(false); // If the queueing operation was NOT successful, it should resolve the returned promise with "false" instead of rejecting it. - // @ts-expect-error expect(await faultyCache.track(fakeEvent3)).toBe(false); // If the queueing operation was NOT successful, it should resolve the returned promise with "false" instead of rejecting it. + // Events should be in redis redisValues = await connection.lrange(eventsKey, 0, -1); - expect(redisValues.length).toBe(3); // After pushing we should have on Redis as many events as we have stored. expect(typeof redisValues[0]).toBe('string'); // All elements should be strings since those are stringified JSONs. expect(typeof redisValues[1]).toBe('string'); // All elements should be strings since those are stringified JSONs. expect(typeof redisValues[2]).toBe('string'); // All elements should be strings since those are stringified JSONs. - const findMatchingElem = (event: any) => { - return find(redisValues, elem => { - const parsedElem = JSON.parse(elem); - return isEqual(parsedElem.e, event) && isEqual(parsedElem.m, fakeMetadata); - }); - }; + // Testing popNWithMetadata + expect(await cache.popNWithMetadata(2)).toEqual([fakeEvent1stored, fakeEvent2stored]); // events are removed in FIFO order + expect(await cache.count()).toBe(1); + + expect(await cache.popNWithMetadata(1)).toEqual([fakeEvent3stored]); + expect(await cache.count()).toBe(0); + + expect(await cache.popNWithMetadata(100)).toEqual([]); // no more events - /* If the elements are found, then the values are correct. */ - const foundEv1 = findMatchingElem(fakeEvent1); - const foundEv2 = findMatchingElem(fakeEvent2); - const foundEv3 = findMatchingElem(fakeEvent3); - expect(foundEv1).not.toBe(undefined); // Events stored on redis matched the values we are expecting. - expect(foundEv2).not.toBe(undefined); // Events stored on redis matched the values we are expecting. - expect(foundEv3).not.toBe(undefined); // Events stored on redis matched the values we are expecting. + // Testing drop method + await Promise.all([cache.track(fakeEvent1), cache.track(fakeEvent2), cache.track(fakeEvent3)]); + expect(await cache.count()).toBe(3); + await cache.drop(); + expect(await cache.count()).toBe(0); // storage should be empty after droping it // Clean up then end. await connection.del(eventsKey); diff --git a/src/storages/inRedis/__tests__/ImpressionsCacheInRedis.spec.ts b/src/storages/inRedis/__tests__/ImpressionsCacheInRedis.spec.ts index b738840a..71591429 100644 --- a/src/storages/inRedis/__tests__/ImpressionsCacheInRedis.spec.ts +++ b/src/storages/inRedis/__tests__/ImpressionsCacheInRedis.spec.ts @@ -24,22 +24,24 @@ describe('IMPRESSIONS CACHE IN REDIS', () => { // Impressions should be in redis const state = await connection.lrange(impressionsKey, 0, -1); + expect(state.length).toBe(3); // After pushing we should have on Redis as many impressions as we have stored. expect(state[0]).toBe(JSON.stringify(o1stored)); expect(state[1]).toBe(JSON.stringify(o2stored)); expect(state[2]).toBe(JSON.stringify(o3stored)); // Testing popNWithMetadata and private toJSON methods. - expect(await c.popNWithMetadata(2)).toEqual([o1stored, o2stored]); // impressions are pop in FIFO order + expect(await c.popNWithMetadata(2)).toEqual([o1stored, o2stored]); // impressions are removed in FIFO order expect(await c.count()).toBe(1); expect(await c.popNWithMetadata(1)).toEqual([o3stored]); expect(await c.count()).toBe(0); + expect(await c.popNWithMetadata(100)).toEqual([]); // no more impressions // Testing drop method await c.track([o1, o2, o3]); expect(await c.count()).toBe(3); await c.drop(); - expect(await c.count()).toBe(0); // storage should be empty after droping the storage + expect(await c.count()).toBe(0); // storage should be empty after droping it await connection.del(impressionsKey); await connection.quit(); diff --git a/src/storages/pluggable/EventsCachePluggable.ts b/src/storages/pluggable/EventsCachePluggable.ts index 2df563ce..ff0b19dc 100644 --- a/src/storages/pluggable/EventsCachePluggable.ts +++ b/src/storages/pluggable/EventsCachePluggable.ts @@ -64,10 +64,12 @@ export class EventsCachePluggable implements IEventsCacheAsync { } /** - * Pop the given number of events from the store. + * Pop the given number of events from the storage. * The returned promise rejects if the wrapper operation fails. + * + * NOTE: this method doesn't take into account MAX_EVENT_SIZE or MAX_QUEUE_BYTE_SIZE limits. + * It is the submitter responsability to handle that. */ - // @TODO follow Go implementation popNWithMetadata(count: number): Promise { return this.wrapper.popItems(this.key, count).then((items) => { return items.map(item => JSON.parse(item) as StoredEventWithMetadata); diff --git a/src/storages/pluggable/__tests__/EventsCachePluggable.spec.ts b/src/storages/pluggable/__tests__/EventsCachePluggable.spec.ts index e1fd3a6d..09d8c84a 100644 --- a/src/storages/pluggable/__tests__/EventsCachePluggable.spec.ts +++ b/src/storages/pluggable/__tests__/EventsCachePluggable.spec.ts @@ -1,52 +1,53 @@ -import find from 'lodash/find'; -import isEqual from 'lodash/isEqual'; import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; import { EventsCachePluggable } from '../EventsCachePluggable'; import { wrapperMockFactory } from './wrapper.mock'; import { wrapperAdapter } from '../wrapperAdapter'; const prefix = 'events_cache_ut'; -const eventsKey = `${prefix}.impressions`; +const eventsKey = `${prefix}.events`; const fakeMetadata = { s: 'js_someversion', i: 'some_ip', n: 'some_hostname' }; -const fakeEvent1 = { event: 1 }; -const fakeEvent2 = { event: '2' }; -const fakeEvent3 = { event: null }; +const fakeEvent1 = { eventTypeId: '1', timestamp: 111 }; +const fakeEvent1stored = { m: fakeMetadata, e: fakeEvent1 }; + +const fakeEvent2 = { eventTypeId: '2', timestamp: 222 }; +const fakeEvent2stored = { m: fakeMetadata, e: fakeEvent2 }; + +const fakeEvent3 = { eventTypeId: '3', timestamp: 333 }; +const fakeEvent3stored = { m: fakeMetadata, e: fakeEvent3 }; + +export { fakeMetadata, fakeEvent1, fakeEvent1stored, fakeEvent2, fakeEvent2stored, fakeEvent3, fakeEvent3stored }; describe('PLUGGABLE EVENTS CACHE', () => { - test('`track` method should push values into the pluggable storage', async () => { + test('`track`, `count`, `popNWithMetadata` and `drop` methods', async () => { const wrapperMock = wrapperMockFactory(); const cache = new EventsCachePluggable(loggerMock, eventsKey, wrapperMock, fakeMetadata); - // @ts-expect-error + // Testing track and count methods. expect(await cache.track(fakeEvent1)).toBe(true); // If the queueing operation was successful, it should resolve the returned promise with "true" - // @ts-expect-error + expect(await cache.count()).toBe(1); // count should return stored items expect(await cache.track(fakeEvent2)).toBe(true); // If the queueing operation was successful, it should resolve the returned promise with "true" - // @ts-expect-error expect(await cache.track(fakeEvent3)).toBe(true); // If the queueing operation was successful, it should resolve the returned promise with "true" + expect(await cache.count()).toBe(3); // count should return stored items const values = wrapperMock._cache[eventsKey] as string[]; + expect(values.length).toBe(3); // After pushing we should have events in the wrapper. + + // Testing popNWithMetadata + expect(await cache.popNWithMetadata(2)).toEqual([fakeEvent1stored, fakeEvent2stored]); // events are removed in FIFO order + expect(await cache.count()).toBe(1); + + expect(await cache.popNWithMetadata(1)).toEqual([fakeEvent3stored]); + expect(await cache.count()).toBe(0); + + expect(await cache.popNWithMetadata(100)).toEqual([]); // no more events - expect(values.length).toBe(3); // After pushing we should have as many events as we have stored. - expect(typeof values[0]).toBe('string'); // All elements should be strings since those are stringified JSONs. - expect(typeof values[1]).toBe('string'); // All elements should be strings since those are stringified JSONs. - expect(typeof values[2]).toBe('string'); // All elements should be strings since those are stringified JSONs. - - const findMatchingElem = (event: any) => { - return find(values, elem => { - const parsedElem = JSON.parse(elem); - return isEqual(parsedElem.e, event) && isEqual(parsedElem.m, fakeMetadata); - }); - }; - - /* If the elements are found, then the values are correct. */ - const foundEv1 = findMatchingElem(fakeEvent1); - const foundEv2 = findMatchingElem(fakeEvent2); - const foundEv3 = findMatchingElem(fakeEvent3); - expect(foundEv1).not.toBe(undefined); // stored events matched the values we are expecting. - expect(foundEv2).not.toBe(undefined); // stored events matched the values we are expecting. - expect(foundEv3).not.toBe(undefined); // stored events matched the values we are expecting. + // Testing drop method + await Promise.all([cache.track(fakeEvent1), cache.track(fakeEvent2), cache.track(fakeEvent3)]); + expect(await cache.count()).toBe(3); + await cache.drop(); + expect(await cache.count()).toBe(0); // storage should be empty after droping it wrapperMock.mockClear(); }); @@ -58,11 +59,8 @@ describe('PLUGGABLE EVENTS CACHE', () => { // wrapperMock is adapted this time to properly handle unexpected exceptions const faultyCache = new EventsCachePluggable(loggerMock, 'non-list-key', wrapperAdapter(loggerMock, wrapperMock), fakeMetadata); - // @ts-expect-error expect(await faultyCache.track(fakeEvent1)).toBe(false); // If the queueing operation was NOT successful, it should resolve the returned promise with "false" instead of rejecting it. - // @ts-expect-error expect(await faultyCache.track(fakeEvent2)).toBe(false); // If the queueing operation was NOT successful, it should resolve the returned promise with "false" instead of rejecting it. - // @ts-expect-error expect(await faultyCache.track(fakeEvent3)).toBe(false); // If the queueing operation was NOT successful, it should resolve the returned promise with "false" instead of rejecting it. wrapperMock.mockClear(); diff --git a/src/storages/pluggable/__tests__/ImpressionsCachePluggable.spec.ts b/src/storages/pluggable/__tests__/ImpressionsCachePluggable.spec.ts index 01384d9f..d978be1b 100644 --- a/src/storages/pluggable/__tests__/ImpressionsCachePluggable.spec.ts +++ b/src/storages/pluggable/__tests__/ImpressionsCachePluggable.spec.ts @@ -75,17 +75,18 @@ describe('PLUGGABLE IMPRESSIONS CACHE', () => { expect(await cache.count()).toBe(3); // count should return stored items // Testing popNWithMetadata and private toJSON methods. - expect(await cache.popNWithMetadata(2)).toEqual([o1stored, o2stored]); // impressions are pop in FIFO order + expect(await cache.popNWithMetadata(2)).toEqual([o1stored, o2stored]); // impressions are removed in FIFO order expect(await cache.count()).toBe(1); expect(await cache.popNWithMetadata(1)).toEqual([o3stored]); expect(await cache.count()).toBe(0); + expect(await cache.popNWithMetadata(100)).toEqual([]); // no more impressions // Testing drop method await cache.track([o1, o2, o3]); expect(await cache.count()).toBe(3); await cache.drop(); - expect(await cache.count()).toBe(0); // storage should be empty after droping the storage + expect(await cache.count()).toBe(0); // storage should be empty after droping it }); diff --git a/src/types.ts b/src/types.ts index a7473280..02def5dc 100644 --- a/src/types.ts +++ b/src/types.ts @@ -592,7 +592,7 @@ export namespace SplitIO { export type EventData = { eventTypeId: string; value?: number; - properties: Properties; + properties?: Properties; trafficTypeName?: string; key?: string; // matching user key timestamp: number; From 3af5fe22c1afd04691f7edd103d10bd222c7396b Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Fri, 14 May 2021 17:45:32 -0300 Subject: [PATCH 060/144] fix issue with a test --- src/integrations/ga/__tests__/GaToSplit.spec.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/integrations/ga/__tests__/GaToSplit.spec.ts b/src/integrations/ga/__tests__/GaToSplit.spec.ts index 21c4a1ef..0a7ef567 100644 --- a/src/integrations/ga/__tests__/GaToSplit.spec.ts +++ b/src/integrations/ga/__tests__/GaToSplit.spec.ts @@ -122,6 +122,7 @@ function customMapper(model: UniversalAnalytics.Model, defaultEvent: SplitIO.Eve } // Updates defaultEvent function customMapper2(model: UniversalAnalytics.Model, defaultEvent: SplitIO.EventData) { + // @ts-ignore. The defaultEvent has a property value, that might be empty depending on the hitType defaultEvent.properties['someProp2'] = 'someProp2'; return defaultEvent; } From 9f6c3724d535d61d2f011a8c16166fd1d93f9cd8 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Fri, 14 May 2021 17:57:43 -0300 Subject: [PATCH 061/144] prepare rc --- package-lock.json | 14 +++++++------- package.json | 4 ++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index ad3ee07c..b68e7898 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "0.1.1-canary.1", + "version": "0.1.1-canary.2", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -4044,9 +4044,9 @@ } }, "hosted-git-info": { - "version": "2.8.8", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz", - "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==", + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", "dev": true }, "html-encoding-sniffer": { @@ -6563,9 +6563,9 @@ } }, "lodash": { - "version": "4.17.20", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", - "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "dev": true }, "lodash.defaults": { diff --git a/package.json b/package.json index 2b370980..48362392 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "0.1.1-canary.1", + "version": "0.1.1-canary.2", "description": "Split Javascript SDK common components", "main": "cjs/index.js", "module": "esm/index.js", @@ -65,7 +65,7 @@ "jest": "^26.6.3", "jest-localstorage-mock": "^2.4.3", "js-yaml": "^3.14.0", - "lodash": "^4.17.20", + "lodash": "^4.17.21", "node-fetch": "^2.6.1", "redis-server": "1.2.2", "rimraf": "^3.0.2", From 83b22a26b9005bd166882e7a6d6d038b2add925a Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 17 May 2021 13:13:20 -0300 Subject: [PATCH 062/144] updated redis dependency, and fixed validateTrafficTypeExistance --- package-lock.json | 48 ++++++++----------- package.json | 2 +- .../polling/updaters/mySegmentsUpdater.ts | 1 + .../polling/updaters/segmentChangesUpdater.ts | 3 ++ .../polling/updaters/splitChangesUpdater.ts | 3 +- .../inputValidation/trafficTypeExistance.ts | 8 ++-- 6 files changed, 29 insertions(+), 36 deletions(-) diff --git a/package-lock.json b/package-lock.json index b68e7898..a508814a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4147,37 +4147,21 @@ "dev": true }, "ioredis": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-4.18.0.tgz", - "integrity": "sha512-wXlB60wD+ayJxbD7t+RFBanXinhHyYpfKUxTEEXNOpd0wb+nC8GLH2r7SaZ6sSBOxr8x6jDfBiuMaiK3bPYABw==", + "version": "4.27.2", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-4.27.2.tgz", + "integrity": "sha512-7OpYymIthonkC2Jne5uGWXswdhlua1S1rWGAERaotn0hGJWTSURvxdHA9G6wNbT/qKCloCja/FHsfKXW8lpTmg==", "dev": true, "requires": { "cluster-key-slot": "^1.1.0", - "debug": "^4.1.1", + "debug": "^4.3.1", "denque": "^1.1.0", "lodash.defaults": "^4.2.0", "lodash.flatten": "^4.4.0", - "redis-commands": "1.6.0", + "p-map": "^2.1.0", + "redis-commands": "1.7.0", "redis-errors": "^1.2.0", "redis-parser": "^3.0.0", - "standard-as-callback": "^2.0.1" - }, - "dependencies": { - "debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } + "standard-as-callback": "^2.1.0" } }, "is-accessor-descriptor": { @@ -6999,6 +6983,12 @@ "p-limit": "^2.2.0" } }, + "p-map": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz", + "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==", + "dev": true + }, "p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", @@ -7313,9 +7303,9 @@ } }, "redis-commands": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.6.0.tgz", - "integrity": "sha512-2jnZ0IkjZxvguITjFTrGiLyzQZcTvaw8DAaCXxZq/dsHXz7KfMQ3OUJy7Tz9vnRtZRVz6VRCPDvruvU8Ts44wQ==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.7.0.tgz", + "integrity": "sha512-nJWqw3bTFy21hX/CPKHth6sfhZbdiHP6bTawSgQBlKOVRG7EZkfHbbHwQJnrE4vsQf0CMNE+3gJ4Fmm16vdVlQ==", "dev": true }, "redis-errors": { @@ -8040,9 +8030,9 @@ } }, "standard-as-callback": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.0.1.tgz", - "integrity": "sha512-NQOxSeB8gOI5WjSaxjBgog2QFw55FV8TkS6Y07BiB3VJ8xNTvUYm0wl0s8ObgQ5NhdpnNfigMIKjgPESzgr4tg==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", + "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==", "dev": true }, "static-extend": { diff --git a/package.json b/package.json index 48362392..f8174c08 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "eslint": "^7.9.0", "eslint-plugin-compat": "3.7.0", "fetch-mock": "^9.10.7", - "ioredis": "4.18.0", + "ioredis": "^4.26.0", "jest": "^26.6.3", "jest-localstorage-mock": "^2.4.3", "js-yaml": "^3.14.0", diff --git a/src/sync/polling/updaters/mySegmentsUpdater.ts b/src/sync/polling/updaters/mySegmentsUpdater.ts index dae0f660..fc18bf45 100644 --- a/src/sync/polling/updaters/mySegmentsUpdater.ts +++ b/src/sync/polling/updaters/mySegmentsUpdater.ts @@ -81,6 +81,7 @@ export function mySegmentsUpdaterFactory( /** * MySegments updater returns a promise that resolves with a `false` boolean value if it fails to fetch mySegments or synchronize them with the storage. + * Returned promise will not be rejected. * * @param {string[] | undefined} segmentList list of mySegments names to sync in the storage. If the list is `undefined`, it fetches them before syncing in the storage. * @param {boolean | undefined} noCache true to revalidate data to fetch diff --git a/src/sync/polling/updaters/segmentChangesUpdater.ts b/src/sync/polling/updaters/segmentChangesUpdater.ts index 5379998a..3480939f 100644 --- a/src/sync/polling/updaters/segmentChangesUpdater.ts +++ b/src/sync/polling/updaters/segmentChangesUpdater.ts @@ -42,6 +42,7 @@ export function segmentChangesUpdaterFactory( /** * Segments updater returns a promise that resolves with a `false` boolean value if it fails at least to fetch a segment or synchronize it with the storage. * Thus, a false result doesn't imply that SDK_SEGMENTS_ARRIVED was not emitted. + * Returned promise will not be rejected. * * @param {string[] | undefined} segmentNames list of segment names to fetch. By passing `undefined` it fetches the list of segments registered at the storage * @param {boolean | undefined} noCache true to revalidate data to fetch on a SEGMENT_UPDATE notifications. @@ -108,6 +109,8 @@ export function segmentChangesUpdaterFactory( // @TODO although factory status is destroyed, synchronization is not stopped if (readiness) readiness.destroy(); log.error(`${LOG_PREFIX_INSTANTIATION}: you passed a client-side type authorizationKey, please grab an Api Key from the Split web console that is of type Server-side.`); + } else { + log.warn(`${LOG_PREFIX_SYNC_SEGMENTS}Error while doing fetch of segments. ${error}`); } return false; diff --git a/src/sync/polling/updaters/splitChangesUpdater.ts b/src/sync/polling/updaters/splitChangesUpdater.ts index 84f5d7c4..3d48e4f2 100644 --- a/src/sync/polling/updaters/splitChangesUpdater.ts +++ b/src/sync/polling/updaters/splitChangesUpdater.ts @@ -110,6 +110,7 @@ export function splitChangesUpdaterFactory( /** * SplitChanges updater returns a promise that resolves with a `false` boolean value if it fails to fetch splits or synchronize them with the storage. + * Returned promise will not be rejected. * * @param {boolean | undefined} noCache true to revalidate data to fetch */ @@ -181,7 +182,7 @@ export function splitChangesUpdaterFactory( return fetcherPromise; } - let sincePromise = Promise.resolve(splits.getChangeNumber()); + let sincePromise = Promise.resolve(splits.getChangeNumber()); // `getChangeNumber` never rejects or throws error return sincePromise.then(_splitChangesUpdater); }; } diff --git a/src/utils/inputValidation/trafficTypeExistance.ts b/src/utils/inputValidation/trafficTypeExistance.ts index eb64c0d3..99d9f705 100644 --- a/src/utils/inputValidation/trafficTypeExistance.ts +++ b/src/utils/inputValidation/trafficTypeExistance.ts @@ -19,17 +19,15 @@ export function validateTrafficTypeExistance(log: ILogger, readinessManager: IRe // If not ready or in localhost mode, we won't run the validation if (!readinessManager.isReady() || mode === LOCALHOST_MODE) return true; - const res = splitsCache.trafficTypeExists(maybeTT); + const res = splitsCache.trafficTypeExists(maybeTT); // never rejects or throws error if (thenable(res)) { - res.then(function (isValid) { + return res.then(function (isValid) { if (!isValid) logTTExistanceWarning(log, maybeTT, method); - return isValid; // propagate result }); } else { if (!res) logTTExistanceWarning(log, maybeTT, method); + return res; } - - return res; } From a57d9f2bcfb63cd7656100f00540fc387eedff3b Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 18 May 2021 18:53:59 -0300 Subject: [PATCH 063/144] updated manager to handle errors on pluggable storage calls --- .../__tests__/index.asyncCache.spec.ts | 87 ++++++++++++------- src/sdkManager/index.ts | 13 ++- 2 files changed, 65 insertions(+), 35 deletions(-) diff --git a/src/sdkManager/__tests__/index.asyncCache.spec.ts b/src/sdkManager/__tests__/index.asyncCache.spec.ts index 46cffb11..a6433ec8 100644 --- a/src/sdkManager/__tests__/index.asyncCache.spec.ts +++ b/src/sdkManager/__tests__/index.asyncCache.spec.ts @@ -3,6 +3,8 @@ import splitObject from './mocks/input.json'; import splitView from './mocks/output.json'; import { sdkManagerFactory } from '../index'; import SplitsCacheInRedis from '../../storages/inRedis/SplitsCacheInRedis'; +import { SplitsCachePluggable } from '../../storages/pluggable/SplitsCachePluggable'; +import { wrapperAdapter } from '../../storages/pluggable/wrapperAdapter'; import KeyBuilderSS from '../../storages/KeyBuilderSS'; import { ISdkReadinessManager } from '../../readiness/types'; import { loggerMock } from '../../logger/__tests__/sdkLogger.mock'; @@ -16,48 +18,71 @@ const sdkReadinessManagerMock = { sdkStatus: jest.fn() } as ISdkReadinessManager; -test('MANAGER API / Async cache (In Redis)', async () => { +// @ts-expect-error +const keys = new KeyBuilderSS(); + +describe('MANAGER API', () => { + + afterEach(() => { loggerMock.mockClear(); }); + + test('Async cache (In Redis)', async () => { + + /** Setup: create manager */ + const connection = new Redis({}); + const cache = new SplitsCacheInRedis(loggerMock, keys, connection); + const manager = sdkManagerFactory(loggerMock, cache, sdkReadinessManagerMock); + await cache.clear(); + await cache.addSplit(splitObject.name, JSON.stringify(splitObject)); + + /** List all splits */ + const views = await manager.splits(); + expect(views[0]).toEqual(splitView); + + /** Read only one split by name */ + + const split = await manager.split(splitObject.name); + expect(split).toEqual(splitView); + + + /** List all the split names */ - /** Setup: create manager */ - const connection = new Redis({}); // @ts-expect-error - const keys = new KeyBuilderSS(); - const cache = new SplitsCacheInRedis(loggerMock, keys, connection); - const manager = sdkManagerFactory(loggerMock, cache, sdkReadinessManagerMock); - await cache.clear(); - await cache.addSplit(splitObject.name, JSON.stringify(splitObject)); + const names = await manager.names(); + expect(names.indexOf(splitObject.name) !== -1).toBe(true); - /** List all splits */ - const views = await manager.splits(); - expect(views[0]).toEqual(splitView); + /** Input Validation */ - /** Read only one split by name */ + // control assertions to verify that the manager is connected with that cache. + expect((await manager.split(splitObject.name)) != null).toBe(true); // control assertion for split. + expect((await manager.splits()).length > 0).toBe(true); // control assertion for splits. + expect((await manager.names()).length > 0).toBe(true); // control assertion for names. - const split = await manager.split(splitObject.name); - expect(split).toEqual(splitView); + // @ts-expect-error + expect(await manager.split(undefined)).toBe(null); // If the split name is invalid, `manager.split(invalidName)` returns null. + // This is kind of tied to the implementation of the isOperational validator. + (sdkReadinessManagerMock.readinessManager.isDestroyed as jest.Mock).mockImplementation(() => true); - /** List all the split names */ + expect(await manager.split(splitObject.name)).toBe(null); // If the factory/client is destroyed, `manager.split(validName)` will return null either way since the storage is not valid. + expect(await manager.splits()).toEqual([]); // If the factory/client is destroyed, `manager.splits()` will return empty array either way since the storage is not valid. + expect(await manager.names()).toEqual([]); // If the factory/client is destroyed, `manager.names()` will return empty array either way since the storage is not valid. - const names = await manager.names(); - expect(names.indexOf(splitObject.name) !== -1).toBe(true); - /** Input Validation */ - // control assertions to verify that the manager is connected with that cache. - expect((await manager.split(splitObject.name)) != null).toBe(true); // control assertion for split. - expect((await manager.splits()).length > 0).toBe(true); // control assertion for splits. - expect((await manager.names()).length > 0).toBe(true); // control assertion for names. + /** Teardown */ + await connection.quit(); + }); - // @ts-expect-error - expect(await manager.split(undefined)).toBe(null); // If the split name is invalid, `manager.split(invalidName)` returns null. + test('Async cache with error', async () => { + // passing an empty object as wrapper, to make method calls of splits cache fail returning a rejected promise. + // @ts-expect-error + const cache = new SplitsCachePluggable(loggerMock, keys, wrapperAdapter(loggerMock, {})); + const manager = sdkManagerFactory(loggerMock, cache, sdkReadinessManagerMock); - // This is kind of tied to the implementation of the isOperational validator. - (sdkReadinessManagerMock.readinessManager.isDestroyed as jest.Mock).mockImplementation(() => true); + expect(await manager.split('some_spplit')).toEqual(null); + expect(await manager.splits()).toEqual([]); + expect(await manager.names()).toEqual([]); - expect(await manager.split(splitObject.name)).toBe(null); // If the factory/client is destroyed, `manager.split(validName)` will return null either way since the storage is not valid. - expect(await manager.splits()).toEqual([]); // If the factory/client is destroyed, `manager.splits()` will return empty array either way since the storage is not valid. - expect(await manager.names()).toEqual([]); // If the factory/client is destroyed, `manager.names()` will return empty array either way since the storage is not valid. + expect(loggerMock.error).toBeCalledTimes(3); // 3 error logs, one for each attempt to call a wrapper method + }); - /** Teardown */ - await connection.quit(); }); diff --git a/src/sdkManager/index.ts b/src/sdkManager/index.ts index a929d082..2536f121 100644 --- a/src/sdkManager/index.ts +++ b/src/sdkManager/index.ts @@ -74,7 +74,7 @@ export function sdkManagerFactory { + return split.catch(() => null).then(result => { // handle possible rejections when using pluggable storage validateSplitExistance(log, readinessManager, splitName, result, SPLIT_FN_LABEL); return objectToView(result); }); @@ -93,8 +93,9 @@ export function sdkManagerFactory []).then(objectsToViews) : // handle possible rejections when using pluggable storage + objectsToViews(currentSplits); }, /** * Get the Split names present on the factory storage @@ -103,7 +104,11 @@ export function sdkManagerFactory []) : // handle possible rejections when using pluggable storage + splitNames; } } ); From bd6bc260f7584758f2567806f8a3583a2ba814cf Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 18 May 2021 19:31:29 -0300 Subject: [PATCH 064/144] added inMemoryWrapper --- src/storages/pluggable/inMemoryWrapper.ts | 117 ++++++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 src/storages/pluggable/inMemoryWrapper.ts diff --git a/src/storages/pluggable/inMemoryWrapper.ts b/src/storages/pluggable/inMemoryWrapper.ts new file mode 100644 index 00000000..47a6eb80 --- /dev/null +++ b/src/storages/pluggable/inMemoryWrapper.ts @@ -0,0 +1,117 @@ +import { ICustomStorageWrapper } from '../types'; +import { startsWith, toNumber } from '../../utils/lang'; +import { ISet, setToArray, _Set } from '../../utils/lang/sets'; + +/** + * Creates a ICustomStorageWrapper implementation that stores items in memory. + * The `_cache` property is the object were items are stored. + * Intended for testing purposes. + */ +export function inMemoryWrapperFactory(): ICustomStorageWrapper & { _cache: Record> } { + + let _cache: Record> = {}; + + return { + /** Holds items (for key-value operations), list of items (for list operations) and set of items (for set operations) */ + _cache, + + get(key) { + return Promise.resolve(key in _cache ? _cache[key] as string : null); + }, + set(key: string, value: string) { + const result = key in _cache; + _cache[key] = value; + return Promise.resolve(result); + }, + getAndSet(key: string, value: string) { + const result = key in _cache ? _cache[key] as string : null; + _cache[key] = value; + return Promise.resolve(result); + }, + del(key: string) { + const result = key in _cache; + delete _cache[key]; + return Promise.resolve(result); + }, + getKeysByPrefix(prefix: string) { + return Promise.resolve(Object.keys(_cache).filter(key => startsWith(key, prefix))); + }, + getByPrefix(prefix: string) { + return Promise.resolve(Object.keys(_cache).filter(key => startsWith(key, prefix)).map(key => _cache[key] as string)); + }, + incr(key: string) { + if (key in _cache) { + const count = toNumber(_cache[key]) + 1; + if (isNaN(count)) return Promise.resolve(false); + _cache[key] = count + ''; + } else { + _cache[key] = '1'; + } + return Promise.resolve(true); + }, + decr(key: string) { + if (key in _cache) { + const count = toNumber(_cache[key]) - 1; + if (isNaN(count)) return Promise.resolve(false); + _cache[key] = count + ''; + } else { + _cache[key] = '-1'; + } + return Promise.resolve(true); + }, + getMany(keys: string[]) { + return Promise.resolve(keys.map(key => _cache[key] ? _cache[key] as string : null)); + }, + pushItems(key: string, items: string[]) { // @ts-ignore + if (!(key in _cache)) _cache[key] = []; + const list = _cache[key]; + if (Array.isArray(list)) { + list.push(...items); + return Promise.resolve(); + } + return Promise.reject('key is not a list'); + }, + popItems(key: string, count: number) { + const list = _cache[key]; + return Promise.resolve(Array.isArray(list) ? list.splice(0, count) : []); + }, + getItemsCount(key: string) { + const list = _cache[key]; + return Promise.resolve(Array.isArray(list) ? list.length : 0); + }, + itemContains(key: string, item: string) { + const set = _cache[key]; + if (!set) return Promise.resolve(false); + if (set instanceof _Set) return Promise.resolve(set.has(item)); + return Promise.reject('key is not a set'); + }, + addItems(key: string, items: string[]) { + if (!(key in _cache)) _cache[key] = new _Set(); + const set = _cache[key]; + if (set instanceof _Set) { + items.forEach(item => set.add(item)); + return Promise.resolve(); + } + return Promise.reject('key is not a set'); + }, + removeItems(key: string, items: string[]) { + if (!(key in _cache)) _cache[key] = new _Set(); + const set = _cache[key]; + if (set instanceof _Set) { + items.forEach(item => set.delete(item)); + return Promise.resolve(); + } + return Promise.reject('key is not a set'); + }, + getItems(key: string) { + const set = _cache[key]; + if (!set) return Promise.resolve([]); + if (set instanceof _Set) return Promise.resolve(setToArray(set)); + return Promise.reject('key is not a set'); + }, + + // always connects and close + connect() { return Promise.resolve(); }, + close() { return Promise.resolve(); }, + }; +} From 59a14d9b687d97966276dc7a6b89f6b697bd6050 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 19 May 2021 15:28:43 -0300 Subject: [PATCH 065/144] updated comments --- src/storages/inRedis/SegmentsCacheInRedis.ts | 5 +-- src/storages/inRedis/SplitsCacheInRedis.ts | 33 ++++++++----------- .../pluggable/SegmentsCachePluggable.ts | 12 +++---- .../pluggable/SplitsCachePluggable.ts | 23 +++++++------ .../__tests__/wrapperAdapter.spec.ts | 4 +-- src/storages/pluggable/wrapperAdapter.ts | 7 ++-- 6 files changed, 35 insertions(+), 49 deletions(-) diff --git a/src/storages/inRedis/SegmentsCacheInRedis.ts b/src/storages/inRedis/SegmentsCacheInRedis.ts index 965f2acb..daee13a5 100644 --- a/src/storages/inRedis/SegmentsCacheInRedis.ts +++ b/src/storages/inRedis/SegmentsCacheInRedis.ts @@ -1,7 +1,6 @@ import { Redis } from 'ioredis'; import { ILogger } from '../../logger/types'; import { isNaNNumber } from '../../utils/lang'; -import { SplitError } from '../../utils/lang/errors'; import { LOG_PREFIX } from '../inLocalStorage/constants'; import KeyBuilderSS from '../KeyBuilderSS'; import { ISegmentsCacheAsync } from '../types'; @@ -70,9 +69,7 @@ export default class SegmentsCacheInRedis implements ISegmentsCacheAsync { } getRegisteredSegments() { - return this.redis.smembers(this.keys.buildRegisteredSegmentsKey()) - // Wrapping error to distinguish from user cb errors. @TODO remove if refactoring SDK error handling. - .catch((e) => { throw new SplitError(e); }); + return this.redis.smembers(this.keys.buildRegisteredSegmentsKey()); } // @TODO remove/review. It is not being used. diff --git a/src/storages/inRedis/SplitsCacheInRedis.ts b/src/storages/inRedis/SplitsCacheInRedis.ts index 2e9d750f..dced1ae4 100644 --- a/src/storages/inRedis/SplitsCacheInRedis.ts +++ b/src/storages/inRedis/SplitsCacheInRedis.ts @@ -4,7 +4,6 @@ import { Redis } from 'ioredis'; import { ILogger } from '../../logger/types'; import { LOG_PREFIX } from './constants'; import { ISplit } from '../../dtos/types'; -import { SplitError } from '../../utils/lang/errors'; import AbstractSplitsCacheAsync from '../AbstractSplitsCacheAsync'; /** @@ -62,19 +61,19 @@ export default class SplitsCacheInRedis extends AbstractSplitsCacheAsync { /** * Add a given split. * The returned promise is resolved when the operation success - * or rejected with an SplitError if it fails (e.g., redis operation fails) + * or rejected if it fails (e.g., redis operation fails) */ addSplit(name: string, split: string): Promise { const splitKey = this.keys.buildSplitKey(name); return this.redis.get(splitKey).then(splitFromStorage => { - // handling parsing error as SplitErrors + // handling parsing errors let parsedPreviousSplit, parsedSplit; try { parsedPreviousSplit = splitFromStorage ? JSON.parse(splitFromStorage) : undefined; parsedSplit = JSON.parse(split); } catch (e) { - throw new SplitError('Error parsing split definition: ' + e); + throw new Error('Error parsing split definition: ' + e); } return Promise.all([ @@ -89,12 +88,10 @@ export default class SplitsCacheInRedis extends AbstractSplitsCacheAsync { /** * Add a list of splits. * The returned promise is resolved when the operation success - * or rejected with an SplitError if it fails (e.g., redis operation fails) + * or rejected if it fails (e.g., redis operation fails) */ addSplits(entries: [string, string][]): Promise { - return Promise.all(entries.map(keyValuePair => this.addSplit(keyValuePair[0], keyValuePair[1]))) - // @TODO remove if refactoring SDK error handling (it is necessary to distinguish from user cb errors). - .catch((e) => { throw new SplitError(e); }); + return Promise.all(entries.map(keyValuePair => this.addSplit(keyValuePair[0], keyValuePair[1]))); } /** @@ -115,12 +112,10 @@ export default class SplitsCacheInRedis extends AbstractSplitsCacheAsync { /** * Remove a list of splits. * The returned promise is resolved when the operation success, - * or rejected with an SplitError if it fails (e.g., redis operation fails). + * or rejected if it fails (e.g., redis operation fails). */ removeSplits(names: string[]): Promise { - return Promise.all(names.map(name => this.removeSplit(name))) - // @TODO remove if refactoring SDK error handling (it is necessary to distinguish from user cb errors). - .catch((e) => { throw new SplitError(e); }); + return Promise.all(names.map(name => this.removeSplit(name))); } /** @@ -131,7 +126,7 @@ export default class SplitsCacheInRedis extends AbstractSplitsCacheAsync { if (this.redisError) { this.log.error(LOG_PREFIX + this.redisError); - return Promise.reject(this.redisError); // no need to wrap as an SplitError + return Promise.reject(this.redisError); } return this.redis.get(this.keys.buildSplitKey(name)); @@ -140,14 +135,12 @@ export default class SplitsCacheInRedis extends AbstractSplitsCacheAsync { /** * Set till number. * The returned promise is resolved when the operation success, - * or rejected with an SplitError if it fails. + * or rejected if it fails. */ setChangeNumber(changeNumber: number): Promise { return this.redis.set(this.keys.buildSplitsTillKey(), changeNumber + '').then( status => status === 'OK' - ) - // @TODO remove if refactoring SDK error handling (it is necessary to distinguish from user cb errors). - .catch((e) => { throw new SplitError(e); }); + ); } /** @@ -169,7 +162,7 @@ export default class SplitsCacheInRedis extends AbstractSplitsCacheAsync { /** * Get list of all split definitions. * The returned promise is resolved with the list of split definitions, - * or rejected if redis operation fails (no need to wrap the error as a SplitError). + * or rejected if redis operation fails. * * @TODO we need to benchmark which is the maximun number of commands we could * pipeline without kill redis performance. @@ -183,7 +176,7 @@ export default class SplitsCacheInRedis extends AbstractSplitsCacheAsync { /** * Get list of split names. * The returned promise is resolved with the list of split names, - * or rejected if redis operation fails (no need to wrap the error as a SplitError). + * or rejected if redis operation fails. */ getSplitNames(): Promise { return this.redis.keys(this.keys.searchPatternForSplitKeys()).then( @@ -235,7 +228,7 @@ export default class SplitsCacheInRedis extends AbstractSplitsCacheAsync { if (this.redisError) { this.log.error(LOG_PREFIX + this.redisError); - return Promise.reject(this.redisError); // no need to wrap as an SplitError + return Promise.reject(this.redisError); } const splits: Record = {}; diff --git a/src/storages/pluggable/SegmentsCachePluggable.ts b/src/storages/pluggable/SegmentsCachePluggable.ts index 502b5b3e..5d40938c 100644 --- a/src/storages/pluggable/SegmentsCachePluggable.ts +++ b/src/storages/pluggable/SegmentsCachePluggable.ts @@ -25,7 +25,7 @@ export class SegmentsCachePluggable implements ISegmentsCacheAsync { /** * Add a list of `segmentKeys` to the given segment `name`. * The returned promise is resolved when the operation success - * or rejected with an SplitError if wrapper operation fails. + * or rejected if wrapper operation fails. */ addToSegment(name: string, segmentKeys: string[]) { const segmentKey = this.keys.buildSegmentNameKey(name); @@ -40,7 +40,7 @@ export class SegmentsCachePluggable implements ISegmentsCacheAsync { /** * Remove a list of `segmentKeys` from the given segment `name`. * The returned promise is resolved when the operation success - * or rejected with an SplitError if wrapper operation fails. + * or rejected if wrapper operation fails. */ removeFromSegment(name: string, segmentKeys: string[]) { const segmentKey = this.keys.buildSegmentNameKey(name); @@ -54,7 +54,7 @@ export class SegmentsCachePluggable implements ISegmentsCacheAsync { /** * Returns a promise that resolves with a boolean value indicating if `key` is part of `name` segment. - * Promise can be rejected with an SplitError if wrapper operation fails. + * Promise can be rejected if wrapper operation fails. */ isInSegment(name: string, key: string) { return this.wrapper.itemContains(this.keys.buildSegmentNameKey(name), key); @@ -63,7 +63,7 @@ export class SegmentsCachePluggable implements ISegmentsCacheAsync { /** * Set till number for the given segment `name`. * The returned promise is resolved when the operation success, - * or rejected with an SplitError if it fails (e.g., wrapper operation fails). + * or rejected if it fails (e.g., wrapper operation fails). */ setChangeNumber(name: string, changeNumber: number) { return this.wrapper.set( @@ -90,7 +90,7 @@ export class SegmentsCachePluggable implements ISegmentsCacheAsync { /** * Add the given segment names to the set of registered segments. * The returned promise is resolved when the operation success, - * or rejected with an SplitError if it fails (e.g., wrapper operation fails). + * or rejected if it fails (e.g., wrapper operation fails). */ registerSegments(segments: string[]) { if (segments.length) { @@ -102,7 +102,7 @@ export class SegmentsCachePluggable implements ISegmentsCacheAsync { /** * Returns a promise that resolves with the set of registered segments in a list, - * or rejected with an SplitError if it fails (e.g., wrapper operation fails). + * or rejected if it fails (e.g., wrapper operation fails). */ getRegisteredSegments() { return this.wrapper.getItems(this.keys.buildRegisteredSegmentsKey()); diff --git a/src/storages/pluggable/SplitsCachePluggable.ts b/src/storages/pluggable/SplitsCachePluggable.ts index 34450416..97be1e6b 100644 --- a/src/storages/pluggable/SplitsCachePluggable.ts +++ b/src/storages/pluggable/SplitsCachePluggable.ts @@ -4,7 +4,6 @@ import { ICustomStorageWrapper } from '../types'; import { ILogger } from '../../logger/types'; import { ISplit } from '../../dtos/types'; import { LOG_PREFIX } from './constants'; -import { SplitError } from '../../utils/lang/errors'; import AbstractSplitsCacheAsync from '../AbstractSplitsCacheAsync'; /** @@ -46,19 +45,19 @@ export class SplitsCachePluggable extends AbstractSplitsCacheAsync { /** * Add a given split. * The returned promise is resolved when the operation success - * or rejected with an SplitError if it fails (e.g., wrapper operation fails) + * or rejected if it fails (e.g., wrapper operation fails) */ addSplit(name: string, split: string): Promise { const splitKey = this.keys.buildSplitKey(name); return this.wrapper.get(splitKey).then(splitFromStorage => { - // handling parsing error as SplitErrors + // handling parsing error let parsedPreviousSplit, parsedSplit; try { parsedPreviousSplit = splitFromStorage ? JSON.parse(splitFromStorage) : undefined; parsedSplit = JSON.parse(split); } catch (e) { - throw new SplitError('Error parsing split definition: ' + e); + throw new Error('Error parsing split definition: ' + e); } return Promise.all([ @@ -73,7 +72,7 @@ export class SplitsCachePluggable extends AbstractSplitsCacheAsync { /** * Add a list of splits. * The returned promise is resolved when the operation success - * or rejected with an SplitError if it fails (e.g., wrapper operation fails) + * or rejected if it fails (e.g., wrapper operation fails) */ addSplits(entries: [string, string][]): Promise { return Promise.all(entries.map(keyValuePair => this.addSplit(keyValuePair[0], keyValuePair[1]))); @@ -82,7 +81,7 @@ export class SplitsCachePluggable extends AbstractSplitsCacheAsync { /** * Remove a given split. * The returned promise is resolved when the operation success, with a boolean indicating if the split existed or not. - * or rejected with an SplitError if it fails (e.g., wrapper operation fails). + * or rejected if it fails (e.g., wrapper operation fails). */ removeSplit(name: string) { return this.getSplit(name).then((split) => { @@ -97,7 +96,7 @@ export class SplitsCachePluggable extends AbstractSplitsCacheAsync { /** * Remove a list of splits. * The returned promise is resolved when the operation success, with a boolean array indicating if the splits existed or not. - * or rejected with an SplitError if it fails (e.g., wrapper operation fails). + * or rejected if it fails (e.g., wrapper operation fails). */ removeSplits(names: string[]): Promise { // @ts-ignore return Promise.all(names.map(name => this.removeSplit(name))); @@ -106,7 +105,7 @@ export class SplitsCachePluggable extends AbstractSplitsCacheAsync { /** * Get split. * The returned promise is resolved with the split definition or null if it's not defined, - * or rejected with an SplitError if wrapper operation fails. + * or rejected if wrapper operation fails. */ getSplit(name: string): Promise { return this.wrapper.get(this.keys.buildSplitKey(name)); @@ -115,7 +114,7 @@ export class SplitsCachePluggable extends AbstractSplitsCacheAsync { /** * Get list of splits. * The returned promise is resolved with a map of split names to their split definition or null if it's not defined, - * or rejected with an SplitError if wrapper operation fails. + * or rejected if wrapper operation fails. */ getSplits(names: string[]): Promise> { const keys = names.map(name => this.keys.buildSplitKey(name)); @@ -132,7 +131,7 @@ export class SplitsCachePluggable extends AbstractSplitsCacheAsync { /** * Get list of all split definitions. * The returned promise is resolved with the list of split definitions, - * or rejected with an SplitError if wrapper operation fails. + * or rejected if wrapper operation fails. */ getAll(): Promise { return this.wrapper.getKeysByPrefix(this.keys.buildSplitKeyPrefix()).then( @@ -143,7 +142,7 @@ export class SplitsCachePluggable extends AbstractSplitsCacheAsync { /** * Get list of split names. * The returned promise is resolved with the list of split names, - * or rejected with an SplitError if wrapper operation fails. + * or rejected if wrapper operation fails. */ getSplitNames(): Promise { return this.wrapper.getKeysByPrefix(this.keys.buildSplitKeyPrefix()).then( @@ -180,7 +179,7 @@ export class SplitsCachePluggable extends AbstractSplitsCacheAsync { /** * Set till number. * The returned promise is resolved when the operation success, - * or rejected with an SplitError if it fails (e.g., wrapper operation fails). + * or rejected if it fails (e.g., wrapper operation fails). */ setChangeNumber(changeNumber: number) { return this.wrapper.set(this.keys.buildSplitsTillKey(), changeNumber + ''); diff --git a/src/storages/pluggable/__tests__/wrapperAdapter.spec.ts b/src/storages/pluggable/__tests__/wrapperAdapter.spec.ts index 02b27a4d..1dbf3ff9 100644 --- a/src/storages/pluggable/__tests__/wrapperAdapter.spec.ts +++ b/src/storages/pluggable/__tests__/wrapperAdapter.spec.ts @@ -2,7 +2,6 @@ import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; import thenable from '../../../utils/promise/thenable'; import { LOG_PREFIX } from '../constants'; -import { SplitError } from '../../../utils/lang/errors'; /** Mocks */ @@ -102,8 +101,7 @@ describe('Wrapper Adapter', () => { await result; expect(true).toBe(false); // promise shouldn't be resolved } catch (e) { - expect(e).toBeInstanceOf(SplitError); - expect(loggerMock.error).toHaveBeenCalledWith(`${LOG_PREFIX} Wrapper '${method}' operation threw an error. Message: ${e.message}`); + expect(loggerMock.error).toHaveBeenCalledWith(`${LOG_PREFIX} Wrapper '${method}' operation threw an error. Message: ${e}`); } } diff --git a/src/storages/pluggable/wrapperAdapter.ts b/src/storages/pluggable/wrapperAdapter.ts index 14a85683..e398d4f6 100644 --- a/src/storages/pluggable/wrapperAdapter.ts +++ b/src/storages/pluggable/wrapperAdapter.ts @@ -1,5 +1,4 @@ import { ILogger } from '../../logger/types'; -import { SplitError } from '../../utils/lang/errors'; import { ICustomStorageWrapper } from '../types'; import { LOG_PREFIX } from './constants'; @@ -26,7 +25,7 @@ export const METHODS_TO_PROMISE_WRAP: string[] = [ /** * Adapter of the Custom Storage Wrapper. - * Used to properly handle exception and rejected promise results: logs error and wrap them into SplitErrors. + * Used to handle exceptions as rejected promises, in order to simplify the error handling on storages. * * @param log logger instance * @param wrapper custom storage wrapper to adapt @@ -38,10 +37,10 @@ export function wrapperAdapter(log: ILogger, wrapper: ICustomStorageWrapper): IC METHODS_TO_PROMISE_WRAP.forEach((method) => { - // Logs error and wraps it into an SplitError object (required to handle user callback errors in SDK readiness events) + // Logs error and wraps it into a rejected promise. function handleError(e: any) { log.error(`${LOG_PREFIX} Wrapper '${method}' operation threw an error. Message: ${e}`); - return Promise.reject(new SplitError(e)); + return Promise.reject(e); } wrapperAdapter[method] = function () { From d166b0af5da0878152b8710cca2a61b0f42dbbdb Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 19 May 2021 15:36:38 -0300 Subject: [PATCH 066/144] polishing --- src/services/splitHttpClient.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/services/splitHttpClient.ts b/src/services/splitHttpClient.ts index be7ed63f..0f08ec4b 100644 --- a/src/services/splitHttpClient.ts +++ b/src/services/splitHttpClient.ts @@ -67,8 +67,8 @@ export function splitHttpClientFactory(settings: Pick Date: Wed, 19 May 2021 17:42:39 -0300 Subject: [PATCH 067/144] typescript polishing --- .eslintrc | 1 - src/listeners/browser.ts | 4 +- src/services/splitHttpClient.ts | 4 +- src/services/types.ts | 65 ++++++++++++------- .../polling/fetchers/mySegmentsFetcher.ts | 4 +- .../polling/fetchers/splitChangesFetcher.ts | 4 +- src/sync/polling/fetchers/types.ts | 5 +- .../polling/updaters/mySegmentsUpdater.ts | 2 +- .../polling/updaters/splitChangesUpdater.ts | 2 +- src/sync/submitters/submitterSyncTask.ts | 3 +- src/utils/timeTracker/__tests__/index.spec.ts | 6 +- src/utils/timeTracker/index.ts | 5 +- 12 files changed, 64 insertions(+), 41 deletions(-) diff --git a/.eslintrc b/.eslintrc index 9231c4df..d080d815 100644 --- a/.eslintrc +++ b/.eslintrc @@ -15,7 +15,6 @@ "globals": { // global TS types - "Response": "readonly", "EventSource": "readonly", // @TODO remove. Configure as a type declaration "MessageEvent": "readonly", // @TODO remove. Configure as a type declaration "Event": "readonly", // @TODO remove. Configure as a type declaration diff --git a/src/listeners/browser.ts b/src/listeners/browser.ts index 6dac033d..378dea29 100644 --- a/src/listeners/browser.ts +++ b/src/listeners/browser.ts @@ -4,7 +4,7 @@ import { ISignalListener } from './types'; import { IRecorderCacheProducerSync, IStorageSync } from '../storages/types'; import { fromImpressionsCollector } from '../sync/submitters/impressionsSyncTask'; import { fromImpressionCountsCollector } from '../sync/submitters/impressionCountsSyncTask'; -import { ISplitApi } from '../services/types'; +import { IResponse, ISplitApi } from '../services/types'; import { ImpressionDTO, ISettings } from '../types'; import { ImpressionsPayload } from '../sync/submitters/types'; import { MaybeThenable } from '../dtos/types'; @@ -74,7 +74,7 @@ export default class BrowserSignalListener implements ISignalListener { if (this.storage.impressionCounts) this._flushData(eventsUrl + '/testImpressions/count/beacon', this.storage.impressionCounts, this.serviceApi.postTestImpressionsCount, fromImpressionCountsCollector); } - private _flushData(url: string, cache: IRecorderCacheProducerSync, postService: (body: string) => Promise, fromCacheToPayload?: (cacheData: TState) => any, extraMetadata?: {}) { + private _flushData(url: string, cache: IRecorderCacheProducerSync, postService: (body: string) => Promise, fromCacheToPayload?: (cacheData: TState) => any, extraMetadata?: {}) { // if there is data in cache, send it to backend if (!cache.isEmpty()) { const dataPayload = fromCacheToPayload ? fromCacheToPayload(cache.state()) : cache.state(); diff --git a/src/services/splitHttpClient.ts b/src/services/splitHttpClient.ts index b6e8952a..c952f8cb 100644 --- a/src/services/splitHttpClient.ts +++ b/src/services/splitHttpClient.ts @@ -1,4 +1,4 @@ -import { IFetch, IRequestOptions, ISplitHttpClient } from './types'; +import { IFetch, IRequestOptions, IResponse, ISplitHttpClient } from './types'; import { SplitError, SplitNetworkError } from '../utils/lang/errors'; import objectAssign from 'object-assign'; import { ERROR_HTTP, ERROR_CLIENT_CANNOT_GET_READY } from '../logger/constants'; @@ -32,7 +32,7 @@ export function splitHttpClientFactory(settings: Pick { + return function httpClient(url: string, reqOpts: IRequestOptions = {}, logErrorsAsInfo: boolean = false): Promise { const request = objectAssign({ headers: reqOpts.headers ? objectAssign({}, headers, reqOpts.headers) : headers, diff --git a/src/services/types.ts b/src/services/types.ts index 7e45c9df..9a73d436 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -1,40 +1,59 @@ export type IRequestOptions = { - method?: string, - headers?: Record, - body?: string + method?: string, + headers?: Record, + body?: string }; +export type IResponse = { + ok: boolean, + status: number, + json: () => Promise, + + /** Other available properties when using Unfetch */ + // statusText: string, + // url: string, + // text: () => Promise, + // blob: () => Promise, + // clone: () => IResponse, + // headers: { + // keys: () => string[], + // entries: () => Array<[string, string]>, + // get: (key: string) => string | undefined, + // has: (key: string) => boolean, + // } +} + // Reduced version of Fetch API -export type IFetch = (url: string, options?: IRequestOptions) => Promise +export type IFetch = (url: string, options?: IRequestOptions) => Promise -export type ISplitHttpClient = (url: string, options?: IRequestOptions, logErrorsAsInfo?: boolean) => Promise +export type ISplitHttpClient = (url: string, options?: IRequestOptions, logErrorsAsInfo?: boolean) => Promise -export type IFetchAuth = (userKeys?: string[]) => Promise +export type IFetchAuth = (userKeys?: string[]) => Promise -export type IFetchSplitChanges = (since: number, noCache?: boolean) => Promise +export type IFetchSplitChanges = (since: number, noCache?: boolean) => Promise -export type IFetchSegmentChanges = (since: number, segmentName: string, noCache?: boolean) => Promise +export type IFetchSegmentChanges = (since: number, segmentName: string, noCache?: boolean) => Promise -export type IFetchMySegments = (userMatchingKey: string, noCache?: boolean) => Promise +export type IFetchMySegments = (userMatchingKey: string, noCache?: boolean) => Promise -export type IPostEventsBulk = (body: string) => Promise +export type IPostEventsBulk = (body: string) => Promise -export type IPostTestImpressionsBulk = (body: string) => Promise +export type IPostTestImpressionsBulk = (body: string) => Promise -export type IPostTestImpressionsCount = (body: string) => Promise +export type IPostTestImpressionsCount = (body: string) => Promise -export type IPostMetricsCounters = (body: string) => Promise +export type IPostMetricsCounters = (body: string) => Promise -export type IPostMetricsTimes = (body: string) => Promise +export type IPostMetricsTimes = (body: string) => Promise export interface ISplitApi { - fetchAuth: IFetchAuth - fetchSplitChanges: IFetchSplitChanges - fetchSegmentChanges: IFetchSegmentChanges - fetchMySegments: IFetchMySegments - postEventsBulk: IPostEventsBulk - postTestImpressionsBulk: IPostTestImpressionsBulk - postTestImpressionsCount: IPostTestImpressionsCount - postMetricsCounters: IPostMetricsCounters - postMetricsTimes: IPostMetricsTimes + fetchAuth: IFetchAuth + fetchSplitChanges: IFetchSplitChanges + fetchSegmentChanges: IFetchSegmentChanges + fetchMySegments: IFetchMySegments + postEventsBulk: IPostEventsBulk + postTestImpressionsBulk: IPostTestImpressionsBulk + postTestImpressionsCount: IPostTestImpressionsCount + postMetricsCounters: IPostMetricsCounters + postMetricsTimes: IPostMetricsTimes } diff --git a/src/sync/polling/fetchers/mySegmentsFetcher.ts b/src/sync/polling/fetchers/mySegmentsFetcher.ts index 2ba6b7b5..ad0e4789 100644 --- a/src/sync/polling/fetchers/mySegmentsFetcher.ts +++ b/src/sync/polling/fetchers/mySegmentsFetcher.ts @@ -1,5 +1,5 @@ import { SplitError } from '../../../utils/lang/errors'; -import { IFetchMySegments } from '../../../services/types'; +import { IFetchMySegments, IResponse } from '../../../services/types'; import { IMySegmentsResponseItem } from '../../../dtos/types'; import { IMySegmentsFetcher } from './types'; @@ -12,7 +12,7 @@ export default function mySegmentsFetcherFactory(fetchMySegments: IFetchMySegmen return function mySegmentsFetcher( noCache?: boolean, // Optional decorator for `fetchMySegments` promise, such as timeout or time tracker - decorator?: (promise: Promise) => Promise + decorator?: (promise: Promise) => Promise ): Promise { let mySegmentsPromise = fetchMySegments(userMatchingKey, noCache); diff --git a/src/sync/polling/fetchers/splitChangesFetcher.ts b/src/sync/polling/fetchers/splitChangesFetcher.ts index d8679c31..61c29682 100644 --- a/src/sync/polling/fetchers/splitChangesFetcher.ts +++ b/src/sync/polling/fetchers/splitChangesFetcher.ts @@ -1,5 +1,5 @@ import { SplitError } from '../../../utils/lang/errors'; -import { IFetchSplitChanges } from '../../../services/types'; +import { IFetchSplitChanges, IResponse } from '../../../services/types'; import { ISplitChangesFetcher } from './types'; /** @@ -12,7 +12,7 @@ export default function splitChangesFetcherFactory(fetchSplitChanges: IFetchSpli since: number, noCache?: boolean, // Optional decorator for `fetchSplitChanges` promise, such as timeout or time tracker - decorator?: (promise: Promise) => Promise + decorator?: (promise: Promise) => Promise ) { let splitsPromise = fetchSplitChanges(since, noCache); diff --git a/src/sync/polling/fetchers/types.ts b/src/sync/polling/fetchers/types.ts index e39cb85a..4d1272f0 100644 --- a/src/sync/polling/fetchers/types.ts +++ b/src/sync/polling/fetchers/types.ts @@ -1,9 +1,10 @@ import { ISplitChangesResponse, ISegmentChangesResponse } from '../../../dtos/types'; +import { IResponse } from '../../../services/types'; export type ISplitChangesFetcher = ( since: number, noCache?: boolean, - decorator?: (promise: Promise) => Promise + decorator?: (promise: Promise) => Promise ) => Promise export type ISegmentChangesFetcher = ( @@ -15,5 +16,5 @@ export type ISegmentChangesFetcher = ( export type IMySegmentsFetcher = ( noCache?: boolean, - decorator?: (promise: Promise) => Promise + decorator?: (promise: Promise) => Promise ) => Promise diff --git a/src/sync/polling/updaters/mySegmentsUpdater.ts b/src/sync/polling/updaters/mySegmentsUpdater.ts index fc18bf45..f03754f1 100644 --- a/src/sync/polling/updaters/mySegmentsUpdater.ts +++ b/src/sync/polling/updaters/mySegmentsUpdater.ts @@ -29,7 +29,7 @@ export function mySegmentsUpdaterFactory( let startingUp = true; /** timeout and telemetry decorator for `splitChangesFetcher` promise */ - function _promiseDecorator(promise: Promise) { + function _promiseDecorator(promise: Promise) { if (startingUp) promise = timeout(requestTimeoutBeforeReady, promise); return promise; diff --git a/src/sync/polling/updaters/splitChangesUpdater.ts b/src/sync/polling/updaters/splitChangesUpdater.ts index 3d48e4f2..a1b67d11 100644 --- a/src/sync/polling/updaters/splitChangesUpdater.ts +++ b/src/sync/polling/updaters/splitChangesUpdater.ts @@ -99,7 +99,7 @@ export function splitChangesUpdaterFactory( let startingUp = true; /** timeout and telemetry decorator for `splitChangesFetcher` promise */ - function _promiseDecorator(promise: Promise) { + function _promiseDecorator(promise: Promise) { if (startingUp && requestTimeoutBeforeReady) promise = timeout(requestTimeoutBeforeReady, promise); return promise; diff --git a/src/sync/submitters/submitterSyncTask.ts b/src/sync/submitters/submitterSyncTask.ts index ccff1b77..e0149bc8 100644 --- a/src/sync/submitters/submitterSyncTask.ts +++ b/src/sync/submitters/submitterSyncTask.ts @@ -3,13 +3,14 @@ import { ISyncTask, ITimeTracker } from '../types'; import { IRecorderCacheProducerSync } from '../../storages/types'; import { ILogger } from '../../logger/types'; import { SUBMITTERS_PUSH, SUBMITTERS_PUSH_FAILS, SUBMITTERS_PUSH_RETRY } from '../../logger/constants'; +import { IResponse } from '../../services/types'; /** * Base function to create submitter sync tasks, such as ImpressionsSyncTask and EventsSyncTask */ export function submitterSyncTaskFactory( log: ILogger, - postClient: (body: string) => Promise, + postClient: (body: string) => Promise, sourceCache: IRecorderCacheProducerSync, postRate: number, dataName: string, diff --git a/src/utils/timeTracker/__tests__/index.spec.ts b/src/utils/timeTracker/__tests__/index.spec.ts index d9348e27..e95def88 100644 --- a/src/utils/timeTracker/__tests__/index.spec.ts +++ b/src/utils/timeTracker/__tests__/index.spec.ts @@ -1,6 +1,7 @@ import timer from '../timer'; import tracker from '../index'; import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; +import { IResponse } from '../../../services/types'; test('TIMER / should count the time between two tasks', (done) => { const timerDuration = Math.floor(Math.random() * 1000); // In millis @@ -24,18 +25,19 @@ describe('TIME TRACKER', () => { }); test('start() / should return the correct type', () => { - const promise = new Promise(res => { + const promise = new Promise(res => { setTimeout(res, 1000); }); const startNormal = tracker.start(loggerMock, tracker.TaskNames.SDK_READY); const startNormalFake = tracker.start(loggerMock, 'fakeTask3'); - const startWithPromise = tracker.start(loggerMock, 'fakeTask4', undefined, promise) as Promise; + const startWithPromise = tracker.start(loggerMock, 'fakeTask4', undefined, promise); expect(typeof startNormal).toBe('function'); // If we call start without a promise, it will return the stop function, // @ts-expect-error expect(typeof startNormal.setCollectorForTask).toBe('function'); // that has a function as well for setting the collector at a defered time, because it has a registered cb but no collector received. // @ts-expect-error expect(typeof startNormalFake.setCollectorForTask).toBe('undefined'); // If no callback is registered for the task, no collectors setup function is attached to returned one. + // @ts-expect-error expect(typeof startWithPromise.then).toBe('function'); // But if we pass a promise, we will get a promise back, with the necessary callbacks already handled. }); diff --git a/src/utils/timeTracker/index.ts b/src/utils/timeTracker/index.ts index 27dd3896..c6899659 100644 --- a/src/utils/timeTracker/index.ts +++ b/src/utils/timeTracker/index.ts @@ -2,6 +2,7 @@ import { uniqueId } from '../lang'; import timer from './timer'; import thenable from '../promise/thenable'; import { ILogger } from '../../logger/types'; +import { IResponse } from '../../services/types'; // Based on ProducerMetricsCollector and ClientCollector classes interface MetricsCollector { @@ -120,7 +121,7 @@ const TrackerAPI = { * @param {string} task - The name of the task. * @param {number | string} modifier - (optional) The modifier for the task, if any. */ - __attachToPromise(log: ILogger, promise: Promise, task: string, collector: false | MetricsCollector, modifier?: number | string) { + __attachToPromise(log: ILogger, promise: Promise, task: string, collector: false | MetricsCollector, modifier?: number | string) { return promise.then(resp => { this.stop(log, task, modifier); @@ -146,7 +147,7 @@ const TrackerAPI = { * @param {Promise} promise - (optional) The promise we are tracking. * @return {Function | Promise} The stop function for this specific task or the promise received with the callbacks registered. */ - start(log: ILogger, task: string, collectors?: Record, promise?: Promise, now?: () => number): Promise | (() => number) { + start(log: ILogger, task: string, collectors?: Record, promise?: Promise, now?: () => number): Promise | (() => number) { const taskUniqueId = uniqueId(); const taskCollector = getCollectorForTask(task, collectors); let result; From a1dbf7da5b1db9d46212d5369e78308a8625b5ca Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 20 May 2021 11:01:59 -0300 Subject: [PATCH 068/144] polishing --- src/services/__tests__/splitApi.spec.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/services/__tests__/splitApi.spec.ts b/src/services/__tests__/splitApi.spec.ts index c177fe07..e2c6d4ca 100644 --- a/src/services/__tests__/splitApi.spec.ts +++ b/src/services/__tests__/splitApi.spec.ts @@ -60,6 +60,7 @@ describe('splitApi', () => { // Invoking any Service method, returns a rejected promise with Split error splitApi.fetchAuth().catch(error => { + expect(error).toBeInstanceOf(Error); expect(error.message).toBe('Global fetch API is not available.'); done(); }); From ca2edeed2ae6ed77bd26a7b3d75b78f044c78732 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 20 May 2021 13:27:30 -0300 Subject: [PATCH 069/144] prepare rc --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index a508814a..27ce1499 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "0.1.1-canary.2", + "version": "0.1.1-canary.3", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index f8174c08..93a95b6d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "0.1.1-canary.2", + "version": "0.1.1-canary.3", "description": "Split Javascript SDK common components", "main": "cjs/index.js", "module": "esm/index.js", From 58912634fe95616e7d7cad01c6d448cf088fc920 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 20 May 2021 18:26:47 -0300 Subject: [PATCH 070/144] extended splitApi to let overwrite default headers for LW synchronizer --- src/services/splitApi.ts | 8 ++++---- src/services/types.ts | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/services/splitApi.ts b/src/services/splitApi.ts index 5a09b726..b7892f56 100644 --- a/src/services/splitApi.ts +++ b/src/services/splitApi.ts @@ -55,16 +55,16 @@ export function splitApiFactory(settings: ISettings, platform: Pick) { const url = `${urls.events}/events/bulk`; - return splitHttpClient(url, { method: 'POST', body }); + return splitHttpClient(url, { method: 'POST', body, headers }); }, - postTestImpressionsBulk(body: string) { + postTestImpressionsBulk(body: string, headers?: Record) { const url = `${urls.events}/testImpressions/bulk`; return splitHttpClient(url, { // Adding extra headers to send impressions in OPTIMIZED or DEBUG modes. - method: 'POST', body, headers: { SplitSDKImpressionsMode } + method: 'POST', body, headers: headers || { SplitSDKImpressionsMode } }); }, diff --git a/src/services/types.ts b/src/services/types.ts index 9a73d436..3d473cee 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -26,6 +26,7 @@ export type IResponse = { // Reduced version of Fetch API export type IFetch = (url: string, options?: IRequestOptions) => Promise +// IFetch specialization export type ISplitHttpClient = (url: string, options?: IRequestOptions, logErrorsAsInfo?: boolean) => Promise export type IFetchAuth = (userKeys?: string[]) => Promise From 97757714512765e79424df6a640cd51e7d1c7f44 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 26 May 2021 13:50:47 -0300 Subject: [PATCH 071/144] rc --- package-lock.json | 2 +- package.json | 2 +- src/services/__tests__/splitApi.spec.ts | 30 ++++++++++++++++++------- src/services/splitApi.ts | 3 ++- 4 files changed, 26 insertions(+), 11 deletions(-) diff --git a/package-lock.json b/package-lock.json index 27ce1499..95b39465 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "0.1.1-canary.3", + "version": "0.1.1-canary.4", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 93a95b6d..4666bb5b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "0.1.1-canary.3", + "version": "0.1.1-canary.4", "description": "Split Javascript SDK common components", "main": "cjs/index.js", "module": "esm/index.js", diff --git a/src/services/__tests__/splitApi.spec.ts b/src/services/__tests__/splitApi.spec.ts index e2c6d4ca..01330d87 100644 --- a/src/services/__tests__/splitApi.spec.ts +++ b/src/services/__tests__/splitApi.spec.ts @@ -5,8 +5,6 @@ import { settingsSplitApi } from '../../utils/settingsValidation/__tests__/setti const settingsWithRuntime = { ...settingsSplitApi, runtime: { ip: 'ip', hostname: 'hostname' } } as ISettings; -const fetchMock = jest.fn(() => Promise.resolve({ ok: true })); - function assertHeaders(settings: ISettings, headers: Record) { expect(headers['Accept']).toBe('application/json'); expect(headers['Content-Type']).toBe('application/json'); @@ -21,6 +19,7 @@ describe('splitApi', () => { test.each([settingsSplitApi, settingsWithRuntime])('performs requests with expected headers', (settings) => { + const fetchMock = jest.fn(() => Promise.resolve({ ok: true })); const splitApi = splitApiFactory(settings, { getFetch: () => fetchMock }); splitApi.fetchAuth(); @@ -38,17 +37,17 @@ describe('splitApi', () => { splitApi.postEventsBulk('fake-body'); assertHeaders(settings, fetchMock.mock.calls[4][1].headers); - splitApi.postMetricsCounters('fake-body'); + splitApi.postTestImpressionsBulk('fake-body'); assertHeaders(settings, fetchMock.mock.calls[5][1].headers); + expect(fetchMock.mock.calls[5][1].headers['SplitSDKImpressionsMode']).toBe(settings.sync.impressionsMode); - splitApi.postMetricsTimes('fake-body'); + splitApi.postTestImpressionsCount('fake-body'); assertHeaders(settings, fetchMock.mock.calls[6][1].headers); - splitApi.postTestImpressionsBulk('fake-body'); + // Deprecated + splitApi.postMetricsCounters('fake-body'); assertHeaders(settings, fetchMock.mock.calls[7][1].headers); - expect(fetchMock.mock.calls[7][1].headers['SplitSDKImpressionsMode']).toBe(settings.sync.impressionsMode); - - splitApi.postTestImpressionsCount('fake-body'); + splitApi.postMetricsTimes('fake-body'); assertHeaders(settings, fetchMock.mock.calls[8][1].headers); fetchMock.mockClear(); @@ -67,4 +66,19 @@ describe('splitApi', () => { }); + test('performs requests with overwritten headers', () => { + const fetchMock = jest.fn(() => Promise.resolve({ ok: true })); + const splitApi = splitApiFactory(settingsWithRuntime, { getFetch: () => fetchMock }); + + const newHeaders = { SplitSDKVersion: 'newVersion', SplitSDKMachineIP: 'newIp', SplitSDKMachineName: 'newHostname' }; + const expectedHeaders = { ...settingsWithRuntime, version: newHeaders.SplitSDKVersion, runtime: { ip: newHeaders.SplitSDKMachineIP, hostname: newHeaders.SplitSDKMachineName } }; + + splitApi.postEventsBulk('fake-body', newHeaders); + assertHeaders(expectedHeaders, fetchMock.mock.calls[0][1].headers); + + splitApi.postTestImpressionsBulk('fake-body', newHeaders); + assertHeaders(expectedHeaders, fetchMock.mock.calls[1][1].headers); + expect(fetchMock.mock.calls[1][1].headers['SplitSDKImpressionsMode']).toBe(settingsWithRuntime.sync.impressionsMode); + }); + }); diff --git a/src/services/splitApi.ts b/src/services/splitApi.ts index b7892f56..d36fe82f 100644 --- a/src/services/splitApi.ts +++ b/src/services/splitApi.ts @@ -3,6 +3,7 @@ import { ISettings } from '../types'; import { splitHttpClientFactory } from './splitHttpClient'; import { ISplitApi } from './types'; import { ISettingsInternal } from '../utils/settingsValidation/types'; +import objectAssign from 'object-assign'; const noCacheHeaderOptions = { headers: { 'Cache-Control': 'no-cache' } }; @@ -64,7 +65,7 @@ export function splitApiFactory(settings: ISettings, platform: Pick Date: Wed, 26 May 2021 16:18:07 -0300 Subject: [PATCH 072/144] updated types --- src/services/splitApi.ts | 12 ++++++++++++ src/services/types.ts | 4 ++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/services/splitApi.ts b/src/services/splitApi.ts index d36fe82f..4adef581 100644 --- a/src/services/splitApi.ts +++ b/src/services/splitApi.ts @@ -56,11 +56,23 @@ export function splitApiFactory(settings: ISettings, platform: Pick) { const url = `${urls.events}/events/bulk`; return splitHttpClient(url, { method: 'POST', body, headers }); }, + /** + * Post impressions. + * + * @param body Impressions bulk payload + * @param headers Optionals headers to overwrite default ones. For example, it is used in producer mode to overwrite metadata headers. + */ postTestImpressionsBulk(body: string, headers?: Record) { const url = `${urls.events}/testImpressions/bulk`; return splitHttpClient(url, { diff --git a/src/services/types.ts b/src/services/types.ts index 3d473cee..a3ff47db 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -39,9 +39,9 @@ export type IFetchMySegments = (userMatchingKey: string, noCache?: boolean) => P export type IPostEventsBulk = (body: string) => Promise -export type IPostTestImpressionsBulk = (body: string) => Promise +export type IPostTestImpressionsBulk = (body: string, headers?: Record) => Promise -export type IPostTestImpressionsCount = (body: string) => Promise +export type IPostTestImpressionsCount = (body: string, headers?: Record) => Promise export type IPostMetricsCounters = (body: string) => Promise From 9812a71ff8529595dfddceb30a33115774658802 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 26 May 2021 19:20:08 -0300 Subject: [PATCH 073/144] updated types --- src/services/splitApi.ts | 10 ++++++++-- src/services/types.ts | 2 +- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/services/splitApi.ts b/src/services/splitApi.ts index 4adef581..3f9dbfc5 100644 --- a/src/services/splitApi.ts +++ b/src/services/splitApi.ts @@ -81,9 +81,15 @@ export function splitApiFactory(settings: ISettings, platform: Pick) { const url = `${urls.events}/testImpressions/count`; - return splitHttpClient(url, { method: 'POST', body }); + return splitHttpClient(url, { method: 'POST', body, headers }); }, postMetricsCounters(body: string) { diff --git a/src/services/types.ts b/src/services/types.ts index a3ff47db..f62fcd4b 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -37,7 +37,7 @@ export type IFetchSegmentChanges = (since: number, segmentName: string, noCache? export type IFetchMySegments = (userMatchingKey: string, noCache?: boolean) => Promise -export type IPostEventsBulk = (body: string) => Promise +export type IPostEventsBulk = (body: string, headers?: Record) => Promise export type IPostTestImpressionsBulk = (body: string, headers?: Record) => Promise From ad526d3c68bd91081c197f27c223cfac78a3c24e Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 31 May 2021 15:38:10 -0300 Subject: [PATCH 074/144] some fixes and updates for server-side --- package-lock.json | 2 +- package.json | 2 +- src/storages/KeyBuilder.ts | 16 ++++++--- src/storages/inLocalStorage/index.ts | 3 +- src/storages/inRedis/index.ts | 3 +- src/storages/pluggable/index.ts | 3 +- src/sync/__tests__/syncTask.spec.ts | 52 ++++++++++++++++++++++++++++ src/sync/streaming/pushManager.ts | 8 ++--- src/sync/streaming/pushManagerSS.ts | 2 +- src/sync/syncTask.ts | 19 +++++++--- 10 files changed, 91 insertions(+), 19 deletions(-) create mode 100644 src/sync/__tests__/syncTask.spec.ts diff --git a/package-lock.json b/package-lock.json index 95b39465..31c767f5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "0.1.1-canary.4", + "version": "0.1.1-canary.5", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 4666bb5b..410e2de9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "0.1.1-canary.4", + "version": "0.1.1-canary.5", "description": "Split Javascript SDK common components", "main": "cjs/index.js", "module": "esm/index.js", diff --git a/src/storages/KeyBuilder.ts b/src/storages/KeyBuilder.ts index 5684d609..b761b7dd 100644 --- a/src/storages/KeyBuilder.ts +++ b/src/storages/KeyBuilder.ts @@ -1,14 +1,22 @@ -import { startsWith } from '../utils/lang'; +import { endsWith, startsWith } from '../utils/lang'; const everythingAtTheEnd = /[^.]+$/; -export default class KeyBuilder { +const DEFAULT_PREFIX = 'SPLITIO'; + +export function validatePrefix(prefix: unknown) { + return prefix && typeof prefix === 'string' ? + endsWith(prefix, '.' + DEFAULT_PREFIX) ? + prefix : // suffix already appended + prefix + '.' + DEFAULT_PREFIX : // append suffix + DEFAULT_PREFIX; // use default prefix if none is provided +} - static DEFAULT_PREFIX = 'SPLITIO'; +export default class KeyBuilder { protected readonly prefix: string; - constructor(prefix: string = KeyBuilder.DEFAULT_PREFIX) { + constructor(prefix: string = DEFAULT_PREFIX) { this.prefix = prefix; } diff --git a/src/storages/inLocalStorage/index.ts b/src/storages/inLocalStorage/index.ts index a26587d1..4cefbf38 100644 --- a/src/storages/inLocalStorage/index.ts +++ b/src/storages/inLocalStorage/index.ts @@ -2,6 +2,7 @@ import ImpressionsCacheInMemory from '../inMemory/ImpressionsCacheInMemory'; import ImpressionCountsCacheInMemory from '../inMemory/ImpressionCountsCacheInMemory'; import EventsCacheInMemory from '../inMemory/EventsCacheInMemory'; import { IStorageFactoryParams, IStorageSyncCS } from '../types'; +import { validatePrefix } from '../KeyBuilder'; import KeyBuilderCS from '../KeyBuilderCS'; import { isLocalStorageAvailable } from '../../utils/env/isLocalStorageAvailable'; import SplitsCacheInLocal from './SplitsCacheInLocal'; @@ -21,7 +22,7 @@ export interface InLocalStorageOptions { */ export function InLocalStorage(options: InLocalStorageOptions = {}) { - const prefix = options.prefix ? options.prefix + '.SPLITIO' : 'SPLITIO'; + const prefix = validatePrefix(options.prefix); return function InLocalStorageCSFactory(params: IStorageFactoryParams): IStorageSyncCS { diff --git a/src/storages/inRedis/index.ts b/src/storages/inRedis/index.ts index 3896e882..e12e537c 100644 --- a/src/storages/inRedis/index.ts +++ b/src/storages/inRedis/index.ts @@ -1,5 +1,6 @@ import RedisAdapter from './RedisAdapter'; import { IStorageAsync, IStorageFactoryParams } from '../types'; +import { validatePrefix } from '../KeyBuilder'; import KeyBuilderSS from '../KeyBuilderSS'; import SplitsCacheInRedis from './SplitsCacheInRedis'; import SegmentsCacheInRedis from './SegmentsCacheInRedis'; @@ -19,7 +20,7 @@ export interface InRedisStorageOptions { */ export function InRedisStorage(options: InRedisStorageOptions = {}) { - const prefix = options.prefix ? options.prefix + '.SPLITIO' : 'SPLITIO'; + const prefix = validatePrefix(options.prefix); return function InRedisStorageFactory({ log, metadata, onReadyCb }: IStorageFactoryParams): IStorageAsync { diff --git a/src/storages/pluggable/index.ts b/src/storages/pluggable/index.ts index 8f5cfb46..6689f0f5 100644 --- a/src/storages/pluggable/index.ts +++ b/src/storages/pluggable/index.ts @@ -7,6 +7,7 @@ import { ImpressionsCachePluggable } from './ImpressionsCachePluggable'; import { EventsCachePluggable } from './EventsCachePluggable'; import { wrapperAdapter, METHODS_TO_PROMISE_WRAP } from './wrapperAdapter'; import { isObject } from '../../utils/lang'; +import { validatePrefix } from '../KeyBuilder'; const NO_VALID_WRAPPER = 'Expecting custom storage `wrapper` in options, but no valid wrapper instance was provided.'; const NO_VALID_WRAPPER_INTERFACE = 'The provided wrapper instance doesn’t follow the expected interface. Check our docs.'; @@ -37,7 +38,7 @@ export function PluggableStorage(options: PluggableStorageOptions) { validatePluggableStorageOptions(options); - const prefix = options.prefix ? options.prefix + '.SPLITIO' : 'SPLITIO'; + const prefix = validatePrefix(options.prefix); return function PluggableStorageFactory({ log, metadata, onReadyCb }: IStorageFactoryParams): IStorageAsync { const keys = new KeyBuilderSS(prefix, metadata); diff --git a/src/sync/__tests__/syncTask.spec.ts b/src/sync/__tests__/syncTask.spec.ts new file mode 100644 index 00000000..39856814 --- /dev/null +++ b/src/sync/__tests__/syncTask.spec.ts @@ -0,0 +1,52 @@ +import syncTaskFactory from '../syncTask'; +import { loggerMock } from '../../logger/__tests__/sdkLogger.mock'; + +const period = 50; +const taskResult = 'taskResult'; +const asyncTask = jest.fn(() => Promise.resolve(taskResult)); + +test('syncTaskFactory', (done) => { + + const syncTask = syncTaskFactory(loggerMock, asyncTask, period); + + // The initial state of the syncTask is not "running" periodically neither "executing" + expect(syncTask.isExecuting()).toBe(false); + expect(syncTask.isRunning()).toBe(false); + + // Calling `start` periodically executes the given task and returns its result + const args = [1, 2]; // @ts-ignore + syncTask.start(...args).then((result) => { + expect(syncTask.isRunning()).toBe(true); // Running periodically + expect(syncTask.isExecuting()).toBe(false); // Already executed + expect(result).toBe(taskResult); + expect(asyncTask).toBeCalledWith(...args); + + // Calling `start` again has not effect + expect(syncTask.start(...args)).toBe(undefined); + + // Calling `execute` inmediatelly executes the given task and returns its result + syncTask.execute(5, 6).then((result) => { + expect(result).toBe(taskResult); + expect(asyncTask).toHaveBeenLastCalledWith(5, 6); + expect(syncTask.isExecuting()).toBe(false); // Already executed + }); + expect(syncTask.isExecuting()).toBe(true); // Executing + }); + + setTimeout(() => { + expect(asyncTask).toHaveBeenLastCalledWith(...args); // Periodic call should be done with the initial `start` arguments + expect(asyncTask).toBeCalledTimes(3); // The task was executed 3 times: twice due to periodic execution and once due to execute call + + // Calling `stop` stops the periodic execution of the given task + expect(syncTask.isRunning()).toBe(true); // Running periodically + syncTask.stop(); + expect(syncTask.isRunning()).toBe(false); // Stop running periodically + + setTimeout(() => { + expect(asyncTask).toBeCalledTimes(3); // Stopped task should not be called again + syncTask.stop(); + done(); + }, period + 10); + }, period + 10); + +}); diff --git a/src/sync/streaming/pushManager.ts b/src/sync/streaming/pushManager.ts index 0e7f1559..5e65ff14 100644 --- a/src/sync/streaming/pushManager.ts +++ b/src/sync/streaming/pushManager.ts @@ -58,15 +58,15 @@ export default function pushManagerFactory( userKeyHashes[hash] = userKey; } + // init workers + const segmentsUpdateWorker = userKey ? new MySegmentsUpdateWorker(pollingManager.segmentsSyncTask) : new SegmentsUpdateWorker(storage.segments, pollingManager.segmentsSyncTask); + // [Only for server-side] we pass the segmentsSyncTask, used by SplitsUpdateWorker to fetch new segments + const splitsUpdateWorker = new SplitsUpdateWorker(storage.splits, pollingManager.splitsSyncTask, readiness.splits, userKey ? undefined : pollingManager.segmentsSyncTask); // [Only for client-side] map of user keys to their corresponding MySegmentsUpdateWorkers. It has a two-fold intention: // - stop workers all together when push is disconnected // - keep the current list of user keys to authenticate const workers: Record = {}; - - // init workers - const segmentsUpdateWorker = userKey ? new MySegmentsUpdateWorker(pollingManager.segmentsSyncTask) : new SegmentsUpdateWorker(storage.segments, pollingManager.segmentsSyncTask); if (userKey) workers[userKey] = segmentsUpdateWorker; - const splitsUpdateWorker = new SplitsUpdateWorker(storage.splits, pollingManager.splitsSyncTask, readiness.splits); // [Only for client-side] variable to flag that a new client was added. It is needed to reconnect streaming. let connectForNewClient = false; diff --git a/src/sync/streaming/pushManagerSS.ts b/src/sync/streaming/pushManagerSS.ts index 37de7fce..e2b43df8 100644 --- a/src/sync/streaming/pushManagerSS.ts +++ b/src/sync/streaming/pushManagerSS.ts @@ -45,7 +45,7 @@ export default function pushManagerSSFactory( sseClient.setEventHandler(sseHandler); // init workers - const splitsUpdateWorker = new SplitsUpdateWorker(storage.splits, pollingManager.splitsSyncTask, readiness.splits); + const splitsUpdateWorker = new SplitsUpdateWorker(storage.splits, pollingManager.splitsSyncTask, readiness.splits, pollingManager.segmentsSyncTask); const segmentsUpdateWorker = new SegmentsUpdateWorker(storage.segments, pollingManager.segmentsSyncTask); // flag that indicates if `disconnectPush` was called, either by the SyncManager (when the client is destroyed) or by a PUSH_NONRETRYABLE_ERROR error. diff --git a/src/sync/syncTask.ts b/src/sync/syncTask.ts index 321ea99e..f1e3c69a 100644 --- a/src/sync/syncTask.ts +++ b/src/sync/syncTask.ts @@ -3,25 +3,33 @@ import { ILogger } from '../logger/types'; import { ISyncTask } from './types'; /** - * factory of sync tasks + * Creates a syncTask that handles the periodic execution of a givan task ("start" and "stop" methods). + * The task can be executed once calling the "execute" method. + * NOTE: Multiple calls to "execute" are not queued. Use "isExecuting" method to handle synchronization. + * + * @param log logger instance + * @param task task to execute that returns a promise that NEVER REJECTS. Otherwise, periodic execution can result in Unhandled Promise Rejections. + * @param period period in milliseconds to execute the task + * @param taskName optional task name for logging */ -export default function syncTaskFactory(log: ILogger, task: (...args: Input) => Promise, period: number, taskName = 'task'): ISyncTask { +export default function syncTaskFactory(log: ILogger, task: (...args: Input) => Promise, period: number, taskName = 'task'): ISyncTask { // flag that indicates if the task is being executed let executing = false; - // flag that indicates if the task periodic execution has been started/stopped. We cannot use timeoutID because it is set after the first execution of the task. + // flag that indicates if the task periodic execution has been started/stopped. let running = false; let timeoutID: number | undefined; + // @TODO check if we need to queued `execute` calls, to avoid some corner-case race conditions on submitters and updaters with streaming. function execute(...args: Input) { executing = true; log.debug(SYNC_TASK_EXECUTE, [taskName]); return task(...args).then(result => { executing = false; - if (running) timeoutID = setTimeout(execute, period, ...args); return result; }); + // No need to handle promise rejection because it is a pre-condition that provided task never rejects. } return { @@ -35,6 +43,7 @@ export default function syncTaskFactory(log: ILogge if (!running) { running = true; log.debug(SYNC_TASK_START, [taskName, period]); + timeoutID = setInterval(execute, period, ...args); return execute(...args); } }, @@ -43,7 +52,7 @@ export default function syncTaskFactory(log: ILogge running = false; if (timeoutID) { log.debug(SYNC_TASK_STOP, [taskName]); - clearTimeout(timeoutID); + clearInterval(timeoutID); timeoutID = undefined; } }, From 623d045b342c08e9e61b8623f67bc9d1a20b1ea6 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 31 May 2021 15:47:34 -0300 Subject: [PATCH 075/144] comment update --- src/sync/syncTask.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/sync/syncTask.ts b/src/sync/syncTask.ts index f1e3c69a..6d8c25af 100644 --- a/src/sync/syncTask.ts +++ b/src/sync/syncTask.ts @@ -7,10 +7,11 @@ import { ISyncTask } from './types'; * The task can be executed once calling the "execute" method. * NOTE: Multiple calls to "execute" are not queued. Use "isExecuting" method to handle synchronization. * - * @param log logger instance - * @param task task to execute that returns a promise that NEVER REJECTS. Otherwise, periodic execution can result in Unhandled Promise Rejections. - * @param period period in milliseconds to execute the task - * @param taskName optional task name for logging + * @param log Logger instance. + * @param task Task to execute that returns a promise that NEVER REJECTS. Otherwise, periodic execution can result in Unhandled Promise Rejections. + * @param period Period in milliseconds to execute the task. + * @param taskName Optional task name for logging. + * @returns A sync task that wraps the given task. */ export default function syncTaskFactory(log: ILogger, task: (...args: Input) => Promise, period: number, taskName = 'task'): ISyncTask { From 715c6f8b4fc59efd2e640a0b7220eedb4ba70709 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 31 May 2021 19:13:40 -0300 Subject: [PATCH 076/144] fixed issue with syncTask factory --- package-lock.json | 2 +- package.json | 2 +- src/sync/__tests__/syncTask.spec.ts | 72 +++++++++++++++++++++-------- src/sync/syncTask.ts | 28 ++++++++--- 4 files changed, 75 insertions(+), 29 deletions(-) diff --git a/package-lock.json b/package-lock.json index 31c767f5..7e4471e2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "0.1.1-canary.5", + "version": "0.1.1-canary.6", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 410e2de9..e117ce74 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "0.1.1-canary.5", + "version": "0.1.1-canary.6", "description": "Split Javascript SDK common components", "main": "cjs/index.js", "module": "esm/index.js", diff --git a/src/sync/__tests__/syncTask.spec.ts b/src/sync/__tests__/syncTask.spec.ts index 39856814..b947e0a3 100644 --- a/src/sync/__tests__/syncTask.spec.ts +++ b/src/sync/__tests__/syncTask.spec.ts @@ -1,7 +1,7 @@ import syncTaskFactory from '../syncTask'; import { loggerMock } from '../../logger/__tests__/sdkLogger.mock'; -const period = 50; +const period = 30; const taskResult = 'taskResult'; const asyncTask = jest.fn(() => Promise.resolve(taskResult)); @@ -14,38 +14,70 @@ test('syncTaskFactory', (done) => { expect(syncTask.isRunning()).toBe(false); // Calling `start` periodically executes the given task and returns its result - const args = [1, 2]; // @ts-ignore - syncTask.start(...args).then((result) => { + const startArgs = [1, 2]; // @ts-ignore + syncTask.start(...startArgs).then(async (result) => { expect(syncTask.isRunning()).toBe(true); // Running periodically expect(syncTask.isExecuting()).toBe(false); // Already executed expect(result).toBe(taskResult); - expect(asyncTask).toBeCalledWith(...args); + expect(asyncTask).toBeCalledWith(...startArgs); // Calling `start` again has not effect - expect(syncTask.start(...args)).toBe(undefined); + expect(syncTask.start(...startArgs)).toBe(undefined); // Calling `execute` inmediatelly executes the given task and returns its result - syncTask.execute(5, 6).then((result) => { - expect(result).toBe(taskResult); - expect(asyncTask).toHaveBeenLastCalledWith(5, 6); - expect(syncTask.isExecuting()).toBe(false); // Already executed - }); - expect(syncTask.isExecuting()).toBe(true); // Executing + result = await syncTask.execute(3, 4); + expect(result).toBe(taskResult); + expect(asyncTask).toHaveBeenLastCalledWith(3, 4); + expect(syncTask.isExecuting()).toBe(false); // Already executed + + // Calling `execute` again result in a new execution of the given task + result = await syncTask.execute(5, 6); + expect(result).toBe(taskResult); + expect(asyncTask).toHaveBeenLastCalledWith(5, 6); + expect(syncTask.isExecuting()).toBe(false); // Already executed + }); - setTimeout(() => { - expect(asyncTask).toHaveBeenLastCalledWith(...args); // Periodic call should be done with the initial `start` arguments - expect(asyncTask).toBeCalledTimes(3); // The task was executed 3 times: twice due to periodic execution and once due to execute call + expect(syncTask.isExecuting()).toBe(true); // Executing - // Calling `stop` stops the periodic execution of the given task - expect(syncTask.isRunning()).toBe(true); // Running periodically - syncTask.stop(); - expect(syncTask.isRunning()).toBe(false); // Stop running periodically + setTimeout(() => { + expect(asyncTask).toHaveBeenLastCalledWith(...startArgs); // Periodic call should be done with the initial `start` arguments + expect(asyncTask).toBeCalledTimes(4); // The task was executed 4 times: twice due to periodic execution and twice due to execute call setTimeout(() => { - expect(asyncTask).toBeCalledTimes(3); // Stopped task should not be called again + expect(asyncTask).toHaveBeenLastCalledWith(...startArgs); // Periodic call should be done with the initial `start` arguments + expect(asyncTask).toBeCalledTimes(5); // The task was executed 5 times: 3 due to periodic execution and twice due to execute call + + // Calling `stop` stops the periodic execution of the given task + expect(syncTask.isRunning()).toBe(true); // Running periodically syncTask.stop(); - done(); + expect(syncTask.isRunning()).toBe(false); // Stop running periodically + + setTimeout(() => { + expect(asyncTask).toBeCalledTimes(5); // Stopped task should not be called again + + // Stopping and starting + syncTask.stop(); + syncTask.start(); // Inmediatelly call task + syncTask.stop(); + syncTask.start(); // Inmediatelly call task + syncTask.stop(); + expect(asyncTask).toBeCalledTimes(7); + + // // Resume periodic execution + syncTask.start(); // Inmediatelly call task + syncTask.start(); // No effect + expect(asyncTask).toBeCalledTimes(8); + + setTimeout(() => { + expect(asyncTask).toBeCalledTimes(9); // Stopped task should not be called again + expect(syncTask.isRunning()).toBe(true); // Running periodically + syncTask.stop(); // Finally stop to finish the test + expect(syncTask.isRunning()).toBe(false); // Stop running periodically + + done(); + }, period + 10); + }, period + 10); }, period + 10); }, period + 10); diff --git a/src/sync/syncTask.ts b/src/sync/syncTask.ts index 6d8c25af..f06c9c0b 100644 --- a/src/sync/syncTask.ts +++ b/src/sync/syncTask.ts @@ -15,14 +15,17 @@ import { ISyncTask } from './types'; */ export default function syncTaskFactory(log: ILogger, task: (...args: Input) => Promise, period: number, taskName = 'task'): ISyncTask { - // flag that indicates if the task is being executed + // Flag that indicates if the task is being executed let executing = false; - // flag that indicates if the task periodic execution has been started/stopped. let running = false; - let timeoutID: number | undefined; + // Auxiliar counter used to avoid race condition when calling `start` & `stop` intermittently + let runningId = 0; + // Params passed to `task` when called periodically + let runningArgs: Input; + // Id of the periodic call timeout + let timeoutID: any; - // @TODO check if we need to queued `execute` calls, to avoid some corner-case race conditions on submitters and updaters with streaming. function execute(...args: Input) { executing = true; log.debug(SYNC_TASK_EXECUTE, [taskName]); @@ -33,7 +36,17 @@ export default function syncTaskFactory(log: // No need to handle promise rejection because it is a pre-condition that provided task never rejects. } + function periodicExecute(currentRunningId: number) { + return execute(...runningArgs).then((result) => { + // Call `setTimeout` if periodic execution was started and `currentRunningId === runningId` + // to avoid a race condition when calling `start`, `stop` and `start` again + if (running && currentRunningId === runningId) timeoutID = setTimeout(periodicExecute, period, currentRunningId); + return result; + }); + } + return { + // @TODO check if we need to queued `execute` calls, to avoid possible race conditions on submitters and updaters with streaming. execute, isExecuting() { @@ -43,9 +56,10 @@ export default function syncTaskFactory(log: start(...args: Input) { if (!running) { running = true; + runningId++; + runningArgs = args; log.debug(SYNC_TASK_START, [taskName, period]); - timeoutID = setInterval(execute, period, ...args); - return execute(...args); + return periodicExecute(runningId); } }, @@ -53,7 +67,7 @@ export default function syncTaskFactory(log: running = false; if (timeoutID) { log.debug(SYNC_TASK_STOP, [taskName]); - clearInterval(timeoutID); + clearTimeout(timeoutID); timeoutID = undefined; } }, From 59d0a256cd6408731355dcca72a1203ae880d815 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 9 Jun 2021 19:28:47 -0300 Subject: [PATCH 077/144] Emi's feedback --- src/sync/syncTask.ts | 2 +- src/utils/lang/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sync/syncTask.ts b/src/sync/syncTask.ts index f06c9c0b..d86cb9f4 100644 --- a/src/sync/syncTask.ts +++ b/src/sync/syncTask.ts @@ -3,7 +3,7 @@ import { ILogger } from '../logger/types'; import { ISyncTask } from './types'; /** - * Creates a syncTask that handles the periodic execution of a givan task ("start" and "stop" methods). + * Creates a syncTask that handles the periodic execution of a given task ("start" and "stop" methods). * The task can be executed once calling the "execute" method. * NOTE: Multiple calls to "execute" are not queued. Use "isExecuting" method to handle synchronization. * diff --git a/src/utils/lang/index.ts b/src/utils/lang/index.ts index 6074a1c1..340d3ccc 100644 --- a/src/utils/lang/index.ts +++ b/src/utils/lang/index.ts @@ -89,7 +89,7 @@ export function get(obj: any, prop: any, val: any): any { /** * Parses an array into a map of different arrays, grouping by the specified prop value. */ -export function groupBy(source: any, prop: string): Record { +export function groupBy >(source: T[], prop: string): Record { const map: Record = {}; if (Array.isArray(source) && isString(prop)) { From 5dc9f748f873720153af8652e3b1ab357b539652 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 9 Jun 2021 19:38:54 -0300 Subject: [PATCH 078/144] polish test --- src/utils/lang/__tests__/index.spec.ts | 30 ++++++++++---------------- 1 file changed, 11 insertions(+), 19 deletions(-) diff --git a/src/utils/lang/__tests__/index.spec.ts b/src/utils/lang/__tests__/index.spec.ts index 51a62ccc..613ad573 100644 --- a/src/utils/lang/__tests__/index.spec.ts +++ b/src/utils/lang/__tests__/index.spec.ts @@ -1,3 +1,4 @@ +// @ts-nocheck import { startsWith, endsWith, @@ -32,7 +33,6 @@ test('LANG UTILS / startsWith', () => { expect(startsWith('myStr', ' yS')).toBe(false); expect(startsWith('myStr', null)).toBe(false); expect(startsWith(false, null)).toBe(false); - // @ts-ignore expect(startsWith()).toBe(false); expect(startsWith(null, 'ys')).toBe(false); @@ -55,7 +55,6 @@ test('LANG UTILS / endsWith', () => { expect(endsWith('myStr', 'str', false)).toBe(false); expect(endsWith('myStr', null)).toBe(false); expect(endsWith(false, null)).toBe(false); - // @ts-ignore expect(endsWith()).toBe(false); expect(endsWith(null, 'ys')).toBe(false); @@ -93,12 +92,10 @@ test('LANG UTILS / get', () => { test('LANG UTILS / getGlobal', () => { // positive expect(getGlobal('console')).toBe(console); // If the global property exists, return it. - // @ts-ignore global.something = 'something'; expect(getGlobal('something')).toBe('something'); // If the global property exists, return it. // negative - // @ts-ignore expect(getGlobal()).toBe(undefined); expect(getGlobal(null)).toBe(undefined); // If the passed value is an invalid key type (neither a number or a string), return undefined. expect(getGlobal('non-existent-property')).toBe(undefined); // If the global property doesn't exist, return undefined. @@ -107,13 +104,9 @@ test('LANG UTILS / getGlobal', () => { test('LANG UTILS / findIndex', () => { const arr = [1, 2, 3, 4, 3]; - // @ts-ignore expect(findIndex()).toBe(-1); // If the parameters for findIndex are wrong it returns -1. - // @ts-ignore expect(findIndex(null, () => { })).toBe(-1); // If the parameters for findIndex are wrong it returns -1. - // @ts-ignore expect(findIndex({}, () => { })).toBe(-1); // If the parameters for findIndex are wrong it returns -1. - // @ts-ignore expect(findIndex({}, false)).toBe(-1); // If the parameters for findIndex are wrong it returns -1. expect(findIndex(arr, () => false)).toBe(-1); // If no element causes iteratee to return truthy, it returns -1. @@ -128,7 +121,6 @@ test('LANG UTILS / findIndex', () => { }); test('LANG UTILS / find', () => { - // @ts-ignore expect(find()).toBe(undefined); // We cant find the element if the collection is wrong type, so we return undefined. expect(find(null, () => true)).toBe(undefined); // We cant find the element if the collection is wrong type, so we return undefined. @@ -157,11 +149,12 @@ test('LANG UTILS / find', () => { }); test('LANG UTILS / isString', () => { + // positive expect(isString('')).toBe(true); // Should return true for strings. expect(isString('asd')).toBe(true); // Should return true for strings. expect(isString(new String('asdf'))).toBe(true); // Should return true for strings. - // @ts-ignore + // negative expect(isString()).toBe(false); // Should return false for non-strings. expect(isString(null)).toBe(false); // Should return false for non-strings. expect(isString([])).toBe(false); // Should return false for non-strings. @@ -171,12 +164,13 @@ test('LANG UTILS / isString', () => { }); test('LANG UTILS / isFiniteNumber', () => { + // positive expect(isFiniteNumber(1)).toBe(true); // Should return true for finite numbers. expect(isFiniteNumber(-0.5)).toBe(true); // Should return true for finite numbers. expect(isFiniteNumber(Number(0.5))).toBe(true); // Should return true for finite numbers. expect(isFiniteNumber(new Number(4))).toBe(true); // Should return true for finite numbers. - // @ts-ignore + // negative expect(isFiniteNumber()).toBe(false); // Should return false for anything that is not a finite number. expect(isFiniteNumber(Infinity)).toBe(false); // Should return false for anything that is not a finite number. expect(isFiniteNumber(-Infinity)).toBe(false); // Should return false for anything that is not a finite number. @@ -192,11 +186,12 @@ test('LANG UTILS / isFiniteNumber', () => { }); test('LANG UTILS / isNaNNumber', () => { + // positive expect(isNaNNumber(NaN)).toBe(true); // Should return true for NaN numbers of "number" type. expect(isNaNNumber(Number(NaN))).toBe(true); // Should return true for NaN numbers of "number" type. expect(isNaNNumber(new Number(NaN))).toBe(true); // Should return true for NaN Number objects. - // @ts-ignore + // negative expect(isNaNNumber()).toBe(false); // Should return false for anything that is not a NaN number. expect(isNaNNumber(Infinity)).toBe(false); // Should return false for anything that is not a NaN number. expect(isNaNNumber(-Infinity)).toBe(false); // Should return false for anything that is not a NaN number. @@ -211,12 +206,13 @@ test('LANG UTILS / isNaNNumber', () => { }); test('LANG UTILS / isIntegerNumber', () => { + // positive expect(isIntegerNumber(1)).toBe(true); // Should return true for integer numbers of "number" type. expect(isIntegerNumber(Number.MIN_SAFE_INTEGER)).toBe(true); // Should return true for integer numbers of "number" type. expect(isIntegerNumber(Number(4))).toBe(true); // Should return true for integer numbers of "number" type. expect(isIntegerNumber(new Number(4))).toBe(true); // Should return true for integer Number objects. - // @ts-ignore + // negative expect(isIntegerNumber()).toBe(false); // Should return false for anything that is not an integer numbers. expect(isIntegerNumber(Infinity)).toBe(false); // Should return false for anything that is not an integer numbers. expect(isIntegerNumber(-Infinity)).toBe(false); // Should return false for anything that is not an integer numbers. @@ -234,12 +230,14 @@ test('LANG UTILS / isIntegerNumber', () => { }); test('LANG UTILS / isObject', () => { + // positive expect(isObject({})).toBe(true); // Should return true for map objects. expect(isObject({ a: true })).toBe(true); // Should return true for map objects. expect(isObject(new Object())).toBe(true); // Should return true for map objects. expect(isObject(Object.create({}))).toBe(true); // Should return true for map objects. expect(isObject(Object.create(Object.prototype))).toBe(true); // Should return true for map objects. + // negative expect(isObject([])).toBe(false); // Should return false for anything that is not a map object. expect(isObject(() => { })).toBe(false); // Should return false for anything that is not a map object. expect(isObject(true)).toBe(false); // Should return false for anything that is not a map object. @@ -435,20 +433,17 @@ test('LANG UTILS / uniqAsStrings', () => { const stringifier = (value: any) => { return Object.keys(value).sort().join(','); }; // returns a string with the ordered keys of the passed object expect(uniqAsStrings([{ a: '3' }, { a: '3', b: '2' }, { a: '3' }, { b: '2', a: '3' }], stringifier)).toEqual([{ a: '3' }, { a: '3', b: '2' }]); // uniqAsStrings should remove all duplicate values from array. - // @ts-ignore expect(() => { uniqAsStrings({}); }).toThrow(); // uniqAsStrings should be called with an array. }); test('LANG UTILS / toString', () => { - // @ts-ignore expect(typeof toString()).toBe('string'); // It should ALWAYS return a string. expect(typeof toString(null)).toBe('string'); // It should ALWAYS return a string. expect(typeof toString(250)).toBe('string'); // It should ALWAYS return a string. expect(typeof toString('asdad')).toBe('string'); // It should ALWAYS return a string. expect(typeof toString(/regex/)).toBe('string'); // It should ALWAYS return a string. - // @ts-ignore expect(toString()).toBe(''); // And the returned string should be correct expect(toString('it is just me')).toBe('it is just me'); // And the returned string should be correct expect(toString(5)).toBe('5'); // And the returned string should be correct @@ -466,7 +461,6 @@ test('LANG UTILS / toNumber', () => { expect(typeof toNumber('asdad')).toBe('number'); // It should ALWAYS return a number. expect(typeof toNumber(/regex/)).toBe('number'); // It should ALWAYS return a number. - // @ts-ignore expect(Number.isNaN(toNumber())).toBe(true); // The returned number should be NaN for values that cannot be converted expect(Number.isNaN(toNumber(/regex/))).toBe(true); // The returned number should be NaN for values that cannot be converted expect(Number.isNaN(toNumber({}))).toBe(true); // The returned number should be NaN for values that cannot be converted @@ -522,9 +516,7 @@ test('LANG UTILS / groupBy', () => { expect(groupBy(true, 'team')).toEqual({}); // If the input is empty or wrong type, it will return an empty object. expect(groupBy('string', 'team')).toEqual({}); // If the input is empty or wrong type, it will return an empty object. expect(groupBy({}, 'team')).toEqual({}); // If the input is empty or wrong type, it will return an empty object. - // @ts-ignore expect(groupBy({ something: 1 }, null)).toEqual({}); // If the input is empty or wrong type, it will return an empty object. - // @ts-ignore expect(groupBy({ something: 1 })).toEqual({}); // If the input is empty or wrong type, it will return an empty object. }); From 5201d30c9de5a2f26e056695637cd2a0687dd125 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 9 Jun 2021 21:08:23 -0300 Subject: [PATCH 079/144] sse metadata implementation --- src/__tests__/testUtils/eventSourceMock.ts | 8 ++- .../SSEClient/__tests__/index.spec.ts | 60 +++++++++++++++---- src/sync/streaming/SSEClient/index.ts | 47 ++++++++++++--- src/sync/streaming/pushManager.ts | 7 ++- src/sync/streaming/pushManagerCS.ts | 3 +- src/sync/streaming/pushManagerSS.ts | 2 +- .../__tests__/settings.mocks.ts | 2 +- 7 files changed, 103 insertions(+), 26 deletions(-) diff --git a/src/__tests__/testUtils/eventSourceMock.ts b/src/__tests__/testUtils/eventSourceMock.ts index 87369bc0..7e74f985 100644 --- a/src/__tests__/testUtils/eventSourceMock.ts +++ b/src/__tests__/testUtils/eventSourceMock.ts @@ -47,7 +47,8 @@ export default class EventSource { static readonly CLOSED: ReadyStateType = 2; private readonly __emitter: IEventEmitter; - onerror?: (evt: MessageEvent) => any;; + private readonly __eventSourceInitDict: EventSourceInitDict; + onerror?: (evt: MessageEvent) => any; onmessage?: (evt: MessageEvent) => any; onopen?: (evt?: MessageEvent) => any; readyState: ReadyStateType; @@ -56,13 +57,14 @@ export default class EventSource { constructor( url: string, - configuration: EventSourceInitDict = defaultOptions + eventSourceInitDict: EventSourceInitDict = defaultOptions ) { this.url = url; - this.withCredentials = configuration.withCredentials; + this.withCredentials = eventSourceInitDict.withCredentials; this.readyState = 0; // @ts-ignore this.__emitter = new EventEmitter(); + this.__eventSourceInitDict = arguments[1]; sources[url] = this; if (__listener) setTimeout(__listener, 0, this); } diff --git a/src/sync/streaming/SSEClient/__tests__/index.spec.ts b/src/sync/streaming/SSEClient/__tests__/index.spec.ts index c660ad09..f0c35d23 100644 --- a/src/sync/streaming/SSEClient/__tests__/index.spec.ts +++ b/src/sync/streaming/SSEClient/__tests__/index.spec.ts @@ -1,18 +1,28 @@ // @ts-nocheck import EventSourceMock from '../../../../__tests__/testUtils/eventSourceMock'; import { authDataSample, channelsQueryParamSample } from '../../AuthClient/__tests__/dataMocks'; +import { fullSettings as settings } from '../../../../utils/settingsValidation/__tests__/settings.mocks'; +import { url } from '../../../../utils/settingsValidation/url'; import SSClient from '../index'; -const URL = 'someurl'; +const EXPECTED_URL = url(settings, '/sse') + + '?channels=' + channelsQueryParamSample + + '&accessToken=' + authDataSample.token + + '&v=1.1&heartbeats=true'; + +const EXPECTED_HEADERS = { + SplitSDKClientKey: '1234', + SplitSDKVersion: settings.version +}; test('SSClient / instance creation throws error if EventSource is not provided', () => { - expect(() => { new SSClient(URL); }).toThrow(Error); - expect(() => { new SSClient(URL, () => undefined); }).toThrow(Error); + expect(() => { new SSClient(settings); }).toThrow(Error); + expect(() => { new SSClient(settings, false, () => undefined); }).toThrow(Error); }); test('SSClient / instance creation success if EventSource is provided', () => { - const instance = new SSClient(URL, () => EventSourceMock); + const instance = new SSClient(settings, false, () => EventSourceMock); expect(instance.eventSource).toBe(EventSourceMock); }); @@ -25,7 +35,7 @@ test('SSClient / setEventHandler, open and close methods', () => { }; // instance SSEClient - const instance = new SSClient(URL, () => EventSourceMock); + const instance = new SSClient(settings, false, () => EventSourceMock); instance.setEventHandler(handler); // error on first open without authToken @@ -78,16 +88,44 @@ test('SSClient / setEventHandler, open and close methods', () => { }); -test('SSClient / open method: URL', () => { +test('SSClient / open method: URL with metadata query params', () => { - const instance = new SSClient(URL, () => EventSourceMock); + const instance = new SSClient(settings, false, () => EventSourceMock); instance.open(authDataSample); - const EXPECTED_URL = URL + '/sse' + - '?channels=' + channelsQueryParamSample + - '&accessToken=' + authDataSample.token + - '&v=1.1&heartbeats=true'; + const EXPECTED_BROWSER_URL = EXPECTED_URL + `&SplitSDKVersion=${settings.version}&SplitSDKClientKey=${EXPECTED_HEADERS.SplitSDKClientKey}`; + + expect(instance.connection.url).toBe(EXPECTED_BROWSER_URL); // URL is properly set for streaming connection + expect(instance.connection.__eventSourceInitDict).toBe(undefined); // No headers are passed for streaming connection +}); + +test('SSClient / open method: URL and metadata headers with IP and Hostname', () => { + + const settingsWithRuntime = { + ...settings, + runtime: { + ip: 'some ip', + hostname: 'some hostname' + } + }; + const instance = new SSClient(settingsWithRuntime, true, () => EventSourceMock); + instance.open(authDataSample); expect(instance.connection.url).toBe(EXPECTED_URL); // URL is properly set for streaming connection + expect(instance.connection.__eventSourceInitDict).toEqual({ + headers: { + ...EXPECTED_HEADERS, + SplitSDKMachineIP: settingsWithRuntime.runtime.ip, + SplitSDKMachineName: settingsWithRuntime.runtime.hostname + } + }); // Headers are properly set for streaming connection +}); + +test('SSClient / open method: URL and metadata headers without IP and Hostname', () => { + const instance = new SSClient(settings, true, () => EventSourceMock); + instance.open(authDataSample); + + expect(instance.connection.url).toBe(EXPECTED_URL); // URL is properly set for streaming connection + expect(instance.connection.__eventSourceInitDict).toEqual({ headers: EXPECTED_HEADERS }); // Headers are properly set for streaming connection }); diff --git a/src/sync/streaming/SSEClient/index.ts b/src/sync/streaming/SSEClient/index.ts index 8f75a514..5ccc5b8f 100644 --- a/src/sync/streaming/SSEClient/index.ts +++ b/src/sync/streaming/SSEClient/index.ts @@ -1,3 +1,4 @@ +import { ISettings } from '../../../types'; import { IAuthTokenPushEnabled } from '../AuthClient/types'; import { ISSEClient, ISseEventHandler } from './types'; @@ -5,6 +6,26 @@ const VERSION = '1.1'; const CONTROL_CHANNEL_REGEX = /^control_/; +/** + * Build metadata headers for SSE connection. + * + * @param {ISettings} settings Validated settings. + * @returns {Record} Headers object + */ +function buildSSEHeaders(settings: ISettings) { + const headers: Record = { + SplitSDKClientKey: settings.core.authorizationKey.slice(-4), + SplitSDKVersion: settings.version, + }; + + // ip and hostname are false if IPAddressesEnabled is false + const { ip, hostname } = settings.runtime; + if (ip) headers['SplitSDKMachineIP'] = ip; + if (hostname) headers['SplitSDKMachineName'] = hostname; + + return headers; +} + /** * Handles streaming connections with EventSource API */ @@ -15,22 +36,27 @@ export default class SSEClient implements ISSEClient { connection?: InstanceType; handler?: ISseEventHandler; authToken?: IAuthTokenPushEnabled; + useHeaders?: boolean; + headers: Record; /** * SSEClient constructor. * - * @param streamingUrl URL - * @param eventSource optional EventSource constructor to use instead of the global one - * @throws 'EventSource API is not available. ' if EventSource is not passed and global one is not available + * @param settings Validated settings. + * @param useHeaders True to send metadata as headers or false to send as query params. If `true`, the provided EventSource must support headers. + * @param getEventSource Function to get the EventSource constructor. + * @throws 'EventSource API is not available. ' if EventSource is not available. */ - constructor(streamingUrl: string, getEventSource?: () => typeof EventSource | undefined) { + constructor(settings: ISettings, useHeaders?: boolean, getEventSource?: () => typeof EventSource | undefined) { // @ts-expect-error this.eventSource = getEventSource && getEventSource(); // if eventSource is not available, throw an exception if (!this.eventSource) throw new Error('EventSource API is not available. '); - this.streamingUrl = streamingUrl + '/sse'; + this.streamingUrl = settings.urls.streaming + '/sse'; this.reopen = this.reopen.bind(this); + this.useHeaders = useHeaders; + this.headers = buildSSEHeaders(settings); } setEventHandler(handler: ISseEventHandler) { @@ -41,7 +67,7 @@ export default class SSEClient implements ISSEClient { * Open the connection with a given authToken * * @param {IAuthTokenPushEnabled} authToken - * @throws {TypeError} if `authToken` is undefined + * @throws {TypeError} Will throw an error if `authToken` is undefined */ open(authToken: IAuthTokenPushEnabled) { this.close(); // it closes connection if previously opened @@ -56,7 +82,14 @@ export default class SSEClient implements ISSEClient { ).join(','); const url = `${this.streamingUrl}?channels=${channelsQueryParam}&accessToken=${authToken.token}&v=${VERSION}&heartbeats=true`; // same results using `&heartbeats=false` - this.connection = new this.eventSource(url); + // this.connection = new this.eventSource(url); + this.connection = new this.eventSource( + // For client-side SDKs, SplitSDKClientKey and SplitSDKClientKey metadata is passed as query params, + // because native EventSource implementations for browser doesn't support headers. + this.useHeaders ? url : url + `&SplitSDKVersion=${this.headers.SplitSDKVersion}&SplitSDKClientKey=${this.headers.SplitSDKClientKey}`, + // @ts-ignore. For server-side SDKs, metadata is passed via headers. EventSource must support headers, like 'eventsource' package for Node. + this.useHeaders ? { headers: this.headers } : undefined + ); if (this.handler) { // no need to check if SSEClient is used only by PushManager this.connection.onopen = this.handler.handleOpen; diff --git a/src/sync/streaming/pushManager.ts b/src/sync/streaming/pushManager.ts index 5e65ff14..43b1dd40 100644 --- a/src/sync/streaming/pushManager.ts +++ b/src/sync/streaming/pushManager.ts @@ -34,11 +34,15 @@ export default function pushManagerFactory( settings: ISettings, ): IPushManagerCS | undefined { + // `userKey` is the matching key of main client in client-side SDK. + // It can be used to check if running on client-side or server-side SDK. + const userKey = settings.core.key ? getMatching(settings.core.key) : undefined; // const log = settings.log; let sseClient: ISSEClient; try { - sseClient = new SSEClient(settings.urls.streaming, platform.getEventSource); + // `useHeaders` false for client-side, even if the platform EventSource supports headers (e.g., React Native). + sseClient = new SSEClient(settings, userKey ? false : true, platform.getEventSource); } catch (e) { log.warn(STREAMING_FALLBACK, [e]); return; @@ -52,7 +56,6 @@ export default function pushManagerFactory( // [Only for client-side] map of hashes to user keys, to dispatch MY_SEGMENTS_UPDATE events to the corresponding MySegmentsUpdateWorker const userKeyHashes: Record = {}; - const userKey = settings.core.key ? getMatching(settings.core.key) : undefined; // matching key of main client if (userKey) { const hash = hashUserKey(userKey); userKeyHashes[hash] = userKey; diff --git a/src/sync/streaming/pushManagerCS.ts b/src/sync/streaming/pushManagerCS.ts index 5611eaa0..e5c78143 100644 --- a/src/sync/streaming/pushManagerCS.ts +++ b/src/sync/streaming/pushManagerCS.ts @@ -36,7 +36,8 @@ export default function pushManagerCSFactory( let sseClient: ISSEClient; try { - sseClient = new SSEClient(settings.urls.streaming, platform.getEventSource); + // `useHeaders` false for client-side, even if the platform EventSource supports headers (e.g., React Native). + sseClient = new SSEClient(settings, false, platform.getEventSource); } catch (e) { log.warn(STREAMING_FALLBACK, [e]); return; diff --git a/src/sync/streaming/pushManagerSS.ts b/src/sync/streaming/pushManagerSS.ts index e2b43df8..d58929a8 100644 --- a/src/sync/streaming/pushManagerSS.ts +++ b/src/sync/streaming/pushManagerSS.ts @@ -32,7 +32,7 @@ export default function pushManagerSSFactory( let sseClient: ISSEClient; try { - sseClient = new SSEClient(settings.urls.streaming, platform.getEventSource); + sseClient = new SSEClient(settings, true, platform.getEventSource); } catch (e) { log.warn(STREAMING_FALLBACK, [e]); return; diff --git a/src/utils/settingsValidation/__tests__/settings.mocks.ts b/src/utils/settingsValidation/__tests__/settings.mocks.ts index 98fef431..4b8a10e4 100644 --- a/src/utils/settingsValidation/__tests__/settings.mocks.ts +++ b/src/utils/settingsValidation/__tests__/settings.mocks.ts @@ -38,7 +38,7 @@ export const settingsWithKeyObject = { export const fullSettings: ISettings = { core: { - authorizationKey: 'asd', + authorizationKey: 'aaaabbbbcccc1234', key: 'some_key', labelsEnabled: false, IPAddressesEnabled: false From 4331cc717feb5704ea5d8e128c9a1cdcea09fd48 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 9 Jun 2021 22:29:59 -0300 Subject: [PATCH 080/144] sse close on unload browser event implementation --- src/listeners/__tests__/browser.spec.ts | 99 ++++++++++++------------- src/listeners/__tests__/node.spec.ts | 36 ++++----- src/listeners/browser.ts | 7 +- src/listeners/node.ts | 16 +++- src/sdkFactory/index.ts | 2 +- src/sdkFactory/types.ts | 3 +- src/sync/streaming/pushManager.ts | 2 +- src/sync/streaming/pushManagerCS.ts | 2 +- src/sync/streaming/pushManagerSS.ts | 2 +- src/sync/syncManagerOnline.ts | 1 + src/sync/types.ts | 4 +- 11 files changed, 94 insertions(+), 80 deletions(-) diff --git a/src/listeners/__tests__/browser.spec.ts b/src/listeners/__tests__/browser.spec.ts index 32c69f59..3bd83169 100644 --- a/src/listeners/__tests__/browser.spec.ts +++ b/src/listeners/__tests__/browser.spec.ts @@ -105,7 +105,7 @@ function triggerUnloadEvent() { /* Mocks end */ -test('Browser JS listener / Impressions optimized mode', (done) => { +test('Browser JS listener / Impressions optimized mode', () => { const listener = new BrowserSignalListener(undefined, fullSettings, fakeStorageOptimized as IStorageSync, fakeSplitApi); @@ -116,63 +116,64 @@ test('Browser JS listener / Impressions optimized mode', (done) => { triggerUnloadEvent(); - setTimeout(() => { - // Unload event was triggered. Thus sendBeacon method should have been called three times. - expect(global.window.navigator.sendBeacon).toBeCalledTimes(3); + // Unload event was triggered. Thus sendBeacon method should have been called three times. + expect(global.window.navigator.sendBeacon).toBeCalledTimes(3); - // Http post services should have not been called - expect(fakeSplitApi.postTestImpressionsBulk).not.toBeCalled(); - expect(fakeSplitApi.postEventsBulk).not.toBeCalled(); - expect(fakeSplitApi.postTestImpressionsCount).not.toBeCalled(); + // Http post services should have not been called + expect(fakeSplitApi.postTestImpressionsBulk).not.toBeCalled(); + expect(fakeSplitApi.postEventsBulk).not.toBeCalled(); + expect(fakeSplitApi.postTestImpressionsCount).not.toBeCalled(); - // pre-check and call stop - expect(global.window.removeEventListener).not.toBeCalled(); - listener.stop(); + // pre-check and call stop + expect(global.window.removeEventListener).not.toBeCalled(); + listener.stop(); - // removed correct listener from correct signal on stop. - expect((global.window.removeEventListener as jest.Mock).mock.calls).toEqual([[UNLOAD_DOM_EVENT, listener.flushData]]); - - done(); - }, 0); + // removed correct listener from correct signal on stop. + expect((global.window.removeEventListener as jest.Mock).mock.calls).toEqual([[UNLOAD_DOM_EVENT, listener.flushData]]); }); -test('Browser JS listener / Impressions debug mode', (done) => { +test('Browser JS listener / Impressions debug mode', () => { + const syncManagerMockWithPushManager = { pushManager: { stop: jest.fn() } }; - const listener = new BrowserSignalListener(undefined, fullSettings, fakeStorageDebug as IStorageSync, fakeSplitApi); + // @ts-expect-error + const listener = new BrowserSignalListener(syncManagerMockWithPushManager, fullSettings, fakeStorageDebug as IStorageSync, fakeSplitApi); listener.start(); // Assigned right function to right signal. expect((global.window.addEventListener as jest.Mock).mock.calls).toEqual([[UNLOAD_DOM_EVENT, listener.flushData]]); + expect(syncManagerMockWithPushManager.pushManager.stop).not.toBeCalled(); + triggerUnloadEvent(); - setTimeout(() => { - // Unload event was triggered. Thus sendBeacon method should have been called twice. - expect(global.window.navigator.sendBeacon).toBeCalledTimes(2); + // Unload event was triggered. Thus sendBeacon method should have been called twice. + expect(global.window.navigator.sendBeacon).toBeCalledTimes(2); - // Http post services should have not been called - expect(fakeSplitApi.postTestImpressionsBulk).not.toBeCalled(); - expect(fakeSplitApi.postEventsBulk).not.toBeCalled(); - expect(fakeSplitApi.postTestImpressionsCount).not.toBeCalled(); + // And since we passed a syncManager with a pushManager, its stop method should have been called. + expect(syncManagerMockWithPushManager.pushManager.stop).toBeCalledTimes(1); - // pre-check and call stop - expect(global.window.removeEventListener).not.toBeCalled(); - listener.stop(); + // Http post services should have not been called + expect(fakeSplitApi.postTestImpressionsBulk).not.toBeCalled(); + expect(fakeSplitApi.postEventsBulk).not.toBeCalled(); + expect(fakeSplitApi.postTestImpressionsCount).not.toBeCalled(); - // removed correct listener from correct signal on stop. - expect((global.window.removeEventListener as jest.Mock).mock.calls).toEqual([[UNLOAD_DOM_EVENT, listener.flushData]]); + // pre-check and call stop + expect(global.window.removeEventListener).not.toBeCalled(); + listener.stop(); - done(); - }, 0); + // removed correct listener from correct signal on stop. + expect((global.window.removeEventListener as jest.Mock).mock.calls).toEqual([[UNLOAD_DOM_EVENT, listener.flushData]]); }); -test('Browser JS listener / Impressions debug mode without sendBeacon API', (done) => { +test('Browser JS listener / Impressions debug mode without sendBeacon API', () => { // remove sendBeacon API const sendBeacon = global.navigator.sendBeacon; // @ts-expect-error global.navigator.sendBeacon = undefined; + const syncManagerMockWithoutPushManager = {}; - const listener = new BrowserSignalListener(undefined, fullSettings, fakeStorageDebug as IStorageSync, fakeSplitApi); + // @ts-expect-error + const listener = new BrowserSignalListener(syncManagerMockWithoutPushManager, fullSettings, fakeStorageDebug as IStorageSync, fakeSplitApi); listener.start(); @@ -181,25 +182,21 @@ test('Browser JS listener / Impressions debug mode without sendBeacon API', (don triggerUnloadEvent(); - setTimeout(() => { - // Unload event was triggered. Thus sendBeacon method should have been called twice. - expect(sendBeacon).not.toBeCalled(); - - // Http post services should have not been called - expect(fakeSplitApi.postTestImpressionsBulk).toBeCalledTimes(1); - expect(fakeSplitApi.postEventsBulk).toBeCalledTimes(1); - expect(fakeSplitApi.postTestImpressionsCount).not.toBeCalled(); + // Unload event was triggered. Thus sendBeacon method should have been called twice. + expect(sendBeacon).not.toBeCalled(); - // pre-check and call stop - expect(global.window.removeEventListener).not.toBeCalled(); - listener.stop(); + // Http post services should have not been called + expect(fakeSplitApi.postTestImpressionsBulk).toBeCalledTimes(1); + expect(fakeSplitApi.postEventsBulk).toBeCalledTimes(1); + expect(fakeSplitApi.postTestImpressionsCount).not.toBeCalled(); - // removed correct listener from correct signal on stop. - expect((global.window.removeEventListener as jest.Mock).mock.calls).toEqual([[UNLOAD_DOM_EVENT, listener.flushData]]); + // pre-check and call stop + expect(global.window.removeEventListener).not.toBeCalled(); + listener.stop(); - // restore sendBeacon API - global.navigator.sendBeacon = sendBeacon; + // removed correct listener from correct signal on stop. + expect((global.window.removeEventListener as jest.Mock).mock.calls).toEqual([[UNLOAD_DOM_EVENT, listener.flushData]]); - done(); - }, 0); + // restore sendBeacon API + global.navigator.sendBeacon = sendBeacon; }); diff --git a/src/listeners/__tests__/node.spec.ts b/src/listeners/__tests__/node.spec.ts index cf5794ed..8f0805e3 100644 --- a/src/listeners/__tests__/node.spec.ts +++ b/src/listeners/__tests__/node.spec.ts @@ -7,8 +7,8 @@ const processKillSpy = jest.spyOn(process, 'kill').mockImplementation(() => true test('Node JS listener / Signal Listener class methods and start/stop functionality', () => { - const handlerMock = jest.fn(); - const listener = new NodeSignalListener(handlerMock, fullSettings); + const syncManagerMock = { flush: jest.fn() }; // @ts-expect-error + const listener = new NodeSignalListener(syncManagerMock, fullSettings); listener.start(); @@ -27,8 +27,8 @@ test('Node JS listener / Signal Listener class methods and start/stop functional test('Node JS listener / Signal Listener SIGTERM callback with sync handler', () => { - const handlerMock = jest.fn(); - const listener = new NodeSignalListener(handlerMock, fullSettings); + const syncManagerMock = { flush: jest.fn() }; // @ts-expect-error + const listener = new NodeSignalListener(syncManagerMock, fullSettings); listener.start(); // Stub stop function since we don't want side effects on test. @@ -36,7 +36,7 @@ test('Node JS listener / Signal Listener SIGTERM callback with sync handler', () // Control asserts. expect(listener.stop).not.toBeCalled(); - expect(handlerMock).not.toBeCalled(); + expect(syncManagerMock.flush).not.toBeCalled(); expect(processKillSpy).not.toBeCalled(); // Call function @@ -44,7 +44,7 @@ test('Node JS listener / Signal Listener SIGTERM callback with sync handler', () listener._sigtermHandler(); // Handler was properly called. - expect(handlerMock).toBeCalledTimes(1); + expect(syncManagerMock.flush).toBeCalledTimes(1); // Clean up is called. expect(listener.stop).toBeCalledTimes(1); @@ -56,8 +56,8 @@ test('Node JS listener / Signal Listener SIGTERM callback with sync handler', () }); test('Node JS listener / Signal Listener SIGTERM callback with sync handler that throws an error', () => { - const handlerMock = jest.fn(() => { throw 'some error'; }); - const listener = new NodeSignalListener(handlerMock, fullSettings); + const syncManagerMock = { flush: jest.fn(() => { throw 'some error'; }) }; // @ts-expect-error + const listener = new NodeSignalListener(syncManagerMock, fullSettings); listener.start(); // Stub stop function since we don't want side effects on test. @@ -65,7 +65,7 @@ test('Node JS listener / Signal Listener SIGTERM callback with sync handler that // Control asserts. expect(listener.stop).not.toBeCalled(); - expect(handlerMock).not.toBeCalled(); + expect(syncManagerMock.flush).not.toBeCalled(); expect(processKillSpy).not.toBeCalled(); // Call function. @@ -73,7 +73,7 @@ test('Node JS listener / Signal Listener SIGTERM callback with sync handler that listener._sigtermHandler(); // Handler was properly called. - expect(handlerMock).toBeCalledTimes(1); + expect(syncManagerMock.flush).toBeCalledTimes(1); // Even if the handler throws, clean up is called. expect(listener.stop).toBeCalledTimes(1); @@ -92,9 +92,9 @@ test('Node JS listener / Signal Listener SIGTERM callback with async handler', a res(); }, 0); }); - const handlerMock = jest.fn(() => fakePromise); - - const listener = new NodeSignalListener(handlerMock, fullSettings); + const syncManagerMock = { flush: jest.fn(() => fakePromise) }; + // @ts-expect-error + const listener = new NodeSignalListener(syncManagerMock, fullSettings); // Stub stop function since we don't want side effects on test. jest.spyOn(listener, 'stop'); @@ -107,7 +107,7 @@ test('Node JS listener / Signal Listener SIGTERM callback with async handler', a listener._sigtermHandler(); // Handler was properly called. - expect(handlerMock).toBeCalledTimes(1); + expect(syncManagerMock.flush).toBeCalledTimes(1); // Check that the wrap up is waiting for the promise to be resolved. expect(listener.stop).not.toBeCalled(); @@ -136,9 +136,9 @@ test('Node JS listener / Signal Listener SIGTERM callback with async handler tha rej(); }, 0); }); - const handlerMock = jest.fn(() => fakePromise); - - const listener = new NodeSignalListener(handlerMock, fullSettings); + const syncManagerMock = { flush: jest.fn(() => fakePromise) }; + // @ts-expect-error + const listener = new NodeSignalListener(syncManagerMock, fullSettings); // Stub stop function since we don't want side effects on test. jest.spyOn(listener, 'stop'); @@ -151,7 +151,7 @@ test('Node JS listener / Signal Listener SIGTERM callback with async handler tha const handlerPromise = listener._sigtermHandler(); // Handler was properly called. - expect(handlerMock).toBeCalledTimes(1); + expect(syncManagerMock.flush).toBeCalledTimes(1); // Check that the wrap up is waiting for the promise to be resolved. expect(listener.stop).not.toBeCalled(); diff --git a/src/listeners/browser.ts b/src/listeners/browser.ts index 378dea29..c5c4fdfd 100644 --- a/src/listeners/browser.ts +++ b/src/listeners/browser.ts @@ -7,10 +7,10 @@ import { fromImpressionCountsCollector } from '../sync/submitters/impressionCoun import { IResponse, ISplitApi } from '../services/types'; import { ImpressionDTO, ISettings } from '../types'; import { ImpressionsPayload } from '../sync/submitters/types'; -import { MaybeThenable } from '../dtos/types'; import { OPTIMIZED, DEBUG } from '../utils/constants'; import objectAssign from 'object-assign'; import { CLEANUP_REGISTERING, CLEANUP_DEREGISTERING } from '../logger/constants'; +import { ISyncManager } from '../sync/types'; // 'unload' event is used instead of 'beforeunload', since 'unload' is not a cancelable event, so no other listeners can stop the event from occurring. const UNLOAD_DOM_EVENT = 'unload'; @@ -24,7 +24,7 @@ export default class BrowserSignalListener implements ISignalListener { private fromImpressionsCollector: (data: ImpressionDTO[]) => ImpressionsPayload; constructor( - handler: (() => MaybeThenable) | undefined, // Not used, but passed by sdkFactory for NodeSignalListener + private syncManager: ISyncManager | undefined, private settings: ISettings, private storage: IStorageSync, private serviceApi: ISplitApi, @@ -72,6 +72,9 @@ export default class BrowserSignalListener implements ISignalListener { this._flushData(eventsUrl + '/testImpressions/beacon', this.storage.impressions, this.serviceApi.postTestImpressionsBulk, this.fromImpressionsCollector, extraMetadata); this._flushData(eventsUrl + '/events/beacon', this.storage.events, this.serviceApi.postEventsBulk); if (this.storage.impressionCounts) this._flushData(eventsUrl + '/testImpressions/count/beacon', this.storage.impressionCounts, this.serviceApi.postTestImpressionsCount, fromImpressionCountsCollector); + + // Close streaming connection + if (this.syncManager && this.syncManager.pushManager) this.syncManager.pushManager.stop(); } private _flushData(url: string, cache: IRecorderCacheProducerSync, postService: (body: string) => Promise, fromCacheToPayload?: (cacheData: TState) => any, extraMetadata?: {}) { diff --git a/src/listeners/node.ts b/src/listeners/node.ts index a2f9b11e..9d8a22e9 100644 --- a/src/listeners/node.ts +++ b/src/listeners/node.ts @@ -4,6 +4,7 @@ import thenable from '../utils/promise/thenable'; import { MaybeThenable } from '../dtos/types'; import { ISettings } from '../types'; import { LOG_PREFIX_CLEANUP, CLEANUP_REGISTERING, CLEANUP_DEREGISTERING } from '../logger/constants'; +import { ISyncManager } from '../sync/types'; const SIGTERM = 'SIGTERM'; const EVENT_NAME = 'for SIGTERM signal.'; @@ -17,10 +18,21 @@ const EVENT_NAME = 'for SIGTERM signal.'; */ export default class NodeSignalListener implements ISignalListener { + private handler: () => MaybeThenable; + private settings: ISettings; + constructor( - private handler: () => MaybeThenable, - private settings: ISettings + syncManager: ISyncManager | undefined, // private handler: () => MaybeThenable, + settings: ISettings ) { + // @TODO review handler logic when implementing Node SDK + this.handler = function () { + if (syncManager) { + // syncManager.stop(); + return syncManager.flush(); + } + }; + this.settings = settings; this._sigtermHandler = this._sigtermHandler.bind(this); } diff --git a/src/sdkFactory/index.ts b/src/sdkFactory/index.ts index c1c64b89..0f89f516 100644 --- a/src/sdkFactory/index.ts +++ b/src/sdkFactory/index.ts @@ -72,7 +72,7 @@ export function sdkFactory(params: ISdkFactoryParams): SplitIO.ICsSDK | SplitIO. const eventTracker = eventTrackerFactory(log, storage.events, integrationsManager); // signal listener - const signalListener = SignalListener && new SignalListener(syncManager && syncManager.flush, settings, storage, splitApi); + const signalListener = SignalListener && new SignalListener(syncManager, settings, storage, splitApi); // Sdk client and manager const clientMethod = sdkClientMethodFactory({ eventTracker, impressionsTracker, sdkReadinessManager, settings, storage, syncManager, signalListener }); diff --git a/src/sdkFactory/types.ts b/src/sdkFactory/types.ts index 32c26954..e63300cf 100644 --- a/src/sdkFactory/types.ts +++ b/src/sdkFactory/types.ts @@ -1,4 +1,3 @@ -import { MaybeThenable } from '../dtos/types'; import { IIntegrationManager, IIntegrationFactoryParams } from '../integrations/types'; import { ISignalListener } from '../listeners/types'; import { ILogger } from '../logger/types'; @@ -59,7 +58,7 @@ export interface ISdkFactoryParams { // Optional signal listener constructor. Used to handle special app states, like shutdown, app paused or resumed. // Pass only if `syncManager` (used by Node listener) and `splitApi` (used by Browser listener) are passed. SignalListener?: new ( - handler: (() => MaybeThenable) | undefined, // Used by NodeSignalListener + syncManager: ISyncManager | undefined, // Used by NodeSignalListener to flush data, and by BrowserSignalListener to close streaming connection. settings: ISettings, // Used by BrowserSignalListener storage: IStorageSync | IStorageAsync, // Used by BrowserSignalListener serviceApi: ISplitApi | undefined) => ISignalListener, // Used by BrowserSignalListener diff --git a/src/sync/streaming/pushManager.ts b/src/sync/streaming/pushManager.ts index 43b1dd40..dee2fec3 100644 --- a/src/sync/streaming/pushManager.ts +++ b/src/sync/streaming/pushManager.ts @@ -141,9 +141,9 @@ export default function pushManagerFactory( // close SSE connection and cancel scheduled tasks function disconnectPush() { + sseClient.close(); disconnected = true; log.info(STREAMING_DISCONNECTING); - sseClient.close(); if (timeoutId) clearTimeout(timeoutId); connectPushRetryBackoff.reset(); diff --git a/src/sync/streaming/pushManagerCS.ts b/src/sync/streaming/pushManagerCS.ts index e5c78143..63bc0995 100644 --- a/src/sync/streaming/pushManagerCS.ts +++ b/src/sync/streaming/pushManagerCS.ts @@ -135,9 +135,9 @@ export default function pushManagerCSFactory( // close SSE connection and cancel scheduled tasks function disconnectPush() { + sseClient.close(); disconnected = true; log.info(STREAMING_DISCONNECTING); - sseClient.close(); if (timeoutId) clearTimeout(timeoutId); connectPushRetryBackoff.reset(); diff --git a/src/sync/streaming/pushManagerSS.ts b/src/sync/streaming/pushManagerSS.ts index d58929a8..ab9485e5 100644 --- a/src/sync/streaming/pushManagerSS.ts +++ b/src/sync/streaming/pushManagerSS.ts @@ -111,9 +111,9 @@ export default function pushManagerSSFactory( // close SSE connection and cancel scheduled tasks function disconnectPush() { + sseClient.close(); disconnected = true; log.info(STREAMING_DISCONNECTING); - sseClient.close(); if (timeoutId) clearTimeout(timeoutId); connectPushRetryBackoff.reset(); diff --git a/src/sync/syncManagerOnline.ts b/src/sync/syncManagerOnline.ts index 04a0c98f..fd1625f9 100644 --- a/src/sync/syncManagerOnline.ts +++ b/src/sync/syncManagerOnline.ts @@ -78,6 +78,7 @@ export function syncManagerOnlineFactory( let running = false; return { + pushManager, start() { // start syncing splits and segments diff --git a/src/sync/types.ts b/src/sync/types.ts index 4af95e62..55566ef1 100644 --- a/src/sync/types.ts +++ b/src/sync/types.ts @@ -3,6 +3,7 @@ import { IPlatform } from '../sdkFactory/types'; import { ISplitApi } from '../services/types'; import { IStorageSync } from '../storages/types'; import { ISettings } from '../types'; +import { IPushManager } from './streaming/types'; export interface ITask { /** @@ -41,7 +42,8 @@ export interface ITimeTracker { /** SyncManager */ export interface ISyncManager extends ITask { - flush(): Promise + flush(): Promise, + pushManager?: IPushManager } export interface ISyncManagerCS extends ISyncManager { From a0201901324a154596af889e9792740b7b28cb92 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Fri, 18 Jun 2021 19:39:36 -0300 Subject: [PATCH 081/144] fix: fixed segments updater to resolve properly --- package-lock.json | 2 +- package.json | 2 +- src/storages/pluggable/SegmentsCachePluggable.ts | 2 +- src/sync/polling/fetchers/segmentChangesFetcher.ts | 7 ------- src/sync/polling/updaters/segmentChangesUpdater.ts | 5 ++--- src/sync/streaming/SSEClient/index.ts | 1 + 6 files changed, 6 insertions(+), 13 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7e4471e2..dc023e36 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "0.1.1-canary.6", + "version": "0.1.1-canary.7", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index e117ce74..856cda24 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "0.1.1-canary.6", + "version": "0.1.1-canary.7", "description": "Split Javascript SDK common components", "main": "cjs/index.js", "module": "esm/index.js", diff --git a/src/storages/pluggable/SegmentsCachePluggable.ts b/src/storages/pluggable/SegmentsCachePluggable.ts index 5d40938c..ec4c3fc3 100644 --- a/src/storages/pluggable/SegmentsCachePluggable.ts +++ b/src/storages/pluggable/SegmentsCachePluggable.ts @@ -5,7 +5,7 @@ import KeyBuilderSS from '../KeyBuilderSS'; import { ICustomStorageWrapper, ISegmentsCacheAsync } from '../types'; import { ILogger } from '../../logger/types'; import { LOG_PREFIX } from './constants'; -import { setToArray, _Set } from '../../utils/lang/sets'; +import { _Set } from '../../utils/lang/sets'; /** * ISegmentsCacheAsync implementation for pluggable storages. diff --git a/src/sync/polling/fetchers/segmentChangesFetcher.ts b/src/sync/polling/fetchers/segmentChangesFetcher.ts index e95b7c37..0bf14400 100644 --- a/src/sync/polling/fetchers/segmentChangesFetcher.ts +++ b/src/sync/polling/fetchers/segmentChangesFetcher.ts @@ -14,13 +14,6 @@ function greedyFetch(fetchSegmentChanges: IFetchSegmentChanges, since: number, s return [flatMe[0], ...flatMe[1]]; }); } - }) - .catch(err => { - // If the operation is forbidden it may be due to permissions, don't recover and propagate the error. - if (err.statusCode === 403) throw err; - // if something goes wrong with the request to the server, we are going to - // stop requesting information till the next round of downloading - return []; }); } diff --git a/src/sync/polling/updaters/segmentChangesUpdater.ts b/src/sync/polling/updaters/segmentChangesUpdater.ts index 648e02e7..eefedf88 100644 --- a/src/sync/polling/updaters/segmentChangesUpdater.ts +++ b/src/sync/polling/updaters/segmentChangesUpdater.ts @@ -94,14 +94,13 @@ export function segmentChangesUpdaterFactory( readyOnAlreadyExistentState = false; if (readiness) readiness.segments.emit(SDK_SEGMENTS_ARRIVED); } - // if at least one segment fetch fails, return false to indicate that there was some error (e.g., invalid json, HTTP error, etc) - if (shouldUpdateFlags.indexOf(-1) !== -1) return false; return true; }); }) // Handles rejected promises at `segmentChangesFetcher`, `segments.getRegisteredSegments` and other segment storage operations. .catch(error => { - if (error.statusCode === 403) { + if (error && error.statusCode === 403) { + // If the operation is forbidden, it may be due to permissions. Destroy the SDK instance. // @TODO although factory status is destroyed, synchronization is not stopped if (readiness) readiness.destroy(); log.error(`${LOG_PREFIX_INSTANTIATION}: you passed a client-side type authorizationKey, please grab an Api Key from the Split web console that is of type Server-side.`); diff --git a/src/sync/streaming/SSEClient/index.ts b/src/sync/streaming/SSEClient/index.ts index 5ccc5b8f..32ba24f0 100644 --- a/src/sync/streaming/SSEClient/index.ts +++ b/src/sync/streaming/SSEClient/index.ts @@ -55,6 +55,7 @@ export default class SSEClient implements ISSEClient { this.streamingUrl = settings.urls.streaming + '/sse'; this.reopen = this.reopen.bind(this); + // @TODO get `useHeaders` flag from `getEventSource`, to use EventSource headers on client-side SDKs when possible. this.useHeaders = useHeaders; this.headers = buildSSEHeaders(settings); } From ab4066c5c03b719b7fb638f81e303600bc8ae1c8 Mon Sep 17 00:00:00 2001 From: Emiliano Fant Date: Tue, 29 Jun 2021 17:10:23 -0300 Subject: [PATCH 082/144] feat: added APIs health check to SplitAPI service --- src/services/__tests__/splitApi.spec.ts | 17 ++++++++++++++++- src/services/splitApi.ts | 10 ++++++++++ src/services/types.ts | 4 ++++ 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/src/services/__tests__/splitApi.spec.ts b/src/services/__tests__/splitApi.spec.ts index 01330d87..067440e0 100644 --- a/src/services/__tests__/splitApi.spec.ts +++ b/src/services/__tests__/splitApi.spec.ts @@ -53,7 +53,7 @@ describe('splitApi', () => { fetchMock.mockClear(); }); - test('reject requests if fetch Api is not provided', (done) => { + test('rejects requests if fetch Api is not provided', (done) => { const splitApi = splitApiFactory(settingsSplitApi, { getFetch: () => undefined }); @@ -81,4 +81,19 @@ describe('splitApi', () => { expect(fetchMock.mock.calls[1][1].headers['SplitSDKImpressionsMode']).toBe(settingsWithRuntime.sync.impressionsMode); }); + test('performs APIs health service check', (done) => { + const fetchMock = jest.fn(() => Promise.resolve({ ok: true })); + const splitApi = splitApiFactory(settingsWithRuntime, { getFetch: () => fetchMock }); + + splitApi.getSplitAPIHealthCheck().then((res) => { + expect(res).toEqual(true); + }); + expect(fetchMock.mock.calls[0][0]).toMatch('sdk/version'); + + splitApi.getEventsAPIHealthCheck().then((res) => { + expect(res).toEqual(true); + done(); + }); + expect(fetchMock.mock.calls[1][0]).toMatch('events/version'); + }); }); diff --git a/src/services/splitApi.ts b/src/services/splitApi.ts index 3f9dbfc5..41f40e5f 100644 --- a/src/services/splitApi.ts +++ b/src/services/splitApi.ts @@ -25,6 +25,16 @@ export function splitApiFactory(settings: ISettings, platform: Pick true).catch(() => false); + }, + + getEventsAPIHealthCheck() { + const url = `${urls.events}/version`; + return splitHttpClient(url).then(() => true).catch(() => false); + }, + fetchAuth(userMatchingKeys?: string[]) { let url = `${urls.auth}/auth`; if (userMatchingKeys) { // accounting the possibility that `userMatchingKeys` is undefined (server-side API) diff --git a/src/services/types.ts b/src/services/types.ts index f62fcd4b..4d28ee32 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -27,6 +27,8 @@ export type IResponse = { export type IFetch = (url: string, options?: IRequestOptions) => Promise // IFetch specialization +export type IHealthCheckAPI = () => Promise + export type ISplitHttpClient = (url: string, options?: IRequestOptions, logErrorsAsInfo?: boolean) => Promise export type IFetchAuth = (userKeys?: string[]) => Promise @@ -48,6 +50,8 @@ export type IPostMetricsCounters = (body: string) => Promise export type IPostMetricsTimes = (body: string) => Promise export interface ISplitApi { + getSplitAPIHealthCheck: IHealthCheckAPI; + getEventsAPIHealthCheck: IHealthCheckAPI; fetchAuth: IFetchAuth fetchSplitChanges: IFetchSplitChanges fetchSegmentChanges: IFetchSegmentChanges From abcc416717d48a3f6e55d3723525251b11c38119 Mon Sep 17 00:00:00 2001 From: Emiliano Fant Date: Wed, 30 Jun 2021 16:09:51 -0300 Subject: [PATCH 083/144] chore: changed version for canary release --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index dc023e36..aafbd77f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "0.1.1-canary.7", + "version": "0.1.1-canary.8", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 856cda24..f1e1afe8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "0.1.1-canary.7", + "version": "0.1.1-canary.8", "description": "Split Javascript SDK common components", "main": "cjs/index.js", "module": "esm/index.js", From 7f08d553e4951ca6cf43dec1881fcf15b4659eeb Mon Sep 17 00:00:00 2001 From: Emiliano Fant Date: Thu, 1 Jul 2021 10:24:46 -0300 Subject: [PATCH 084/144] refactor: changed API check function's name --- src/services/__tests__/splitApi.spec.ts | 2 +- src/services/splitApi.ts | 2 +- src/services/types.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/services/__tests__/splitApi.spec.ts b/src/services/__tests__/splitApi.spec.ts index 067440e0..22671475 100644 --- a/src/services/__tests__/splitApi.spec.ts +++ b/src/services/__tests__/splitApi.spec.ts @@ -85,7 +85,7 @@ describe('splitApi', () => { const fetchMock = jest.fn(() => Promise.resolve({ ok: true })); const splitApi = splitApiFactory(settingsWithRuntime, { getFetch: () => fetchMock }); - splitApi.getSplitAPIHealthCheck().then((res) => { + splitApi.getSplitSDKHealthCheck().then((res) => { expect(res).toEqual(true); }); expect(fetchMock.mock.calls[0][0]).toMatch('sdk/version'); diff --git a/src/services/splitApi.ts b/src/services/splitApi.ts index 41f40e5f..80fe1f2f 100644 --- a/src/services/splitApi.ts +++ b/src/services/splitApi.ts @@ -25,7 +25,7 @@ export function splitApiFactory(settings: ISettings, platform: Pick true).catch(() => false); }, diff --git a/src/services/types.ts b/src/services/types.ts index 4d28ee32..6afa9197 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -50,7 +50,7 @@ export type IPostMetricsCounters = (body: string) => Promise export type IPostMetricsTimes = (body: string) => Promise export interface ISplitApi { - getSplitAPIHealthCheck: IHealthCheckAPI; + getSplitSDKHealthCheck: IHealthCheckAPI; getEventsAPIHealthCheck: IHealthCheckAPI; fetchAuth: IFetchAuth fetchSplitChanges: IFetchSplitChanges From a49f58c2a16cf16a45c6e2688ed27518f6685c08 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 1 Jul 2021 12:35:56 -0300 Subject: [PATCH 085/144] refactor: renamed splitApi endpoint for consistency --- src/services/__tests__/splitApi.spec.ts | 2 +- src/services/splitApi.ts | 2 +- src/services/types.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/services/__tests__/splitApi.spec.ts b/src/services/__tests__/splitApi.spec.ts index 22671475..c014e678 100644 --- a/src/services/__tests__/splitApi.spec.ts +++ b/src/services/__tests__/splitApi.spec.ts @@ -85,7 +85,7 @@ describe('splitApi', () => { const fetchMock = jest.fn(() => Promise.resolve({ ok: true })); const splitApi = splitApiFactory(settingsWithRuntime, { getFetch: () => fetchMock }); - splitApi.getSplitSDKHealthCheck().then((res) => { + splitApi.getSdkAPIHealthCheck().then((res) => { expect(res).toEqual(true); }); expect(fetchMock.mock.calls[0][0]).toMatch('sdk/version'); diff --git a/src/services/splitApi.ts b/src/services/splitApi.ts index 80fe1f2f..301d054b 100644 --- a/src/services/splitApi.ts +++ b/src/services/splitApi.ts @@ -25,7 +25,7 @@ export function splitApiFactory(settings: ISettings, platform: Pick true).catch(() => false); }, diff --git a/src/services/types.ts b/src/services/types.ts index 6afa9197..1f66225d 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -50,7 +50,7 @@ export type IPostMetricsCounters = (body: string) => Promise export type IPostMetricsTimes = (body: string) => Promise export interface ISplitApi { - getSplitSDKHealthCheck: IHealthCheckAPI; + getSdkAPIHealthCheck: IHealthCheckAPI; getEventsAPIHealthCheck: IHealthCheckAPI; fetchAuth: IFetchAuth fetchSplitChanges: IFetchSplitChanges From cb359540b0d37a6688dc441dd3b1eb7e8a1fe62b Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 1 Jul 2021 13:04:32 -0300 Subject: [PATCH 086/144] updated SSEClient to use addEventListener --- src/__tests__/testUtils/eventSourceMock.ts | 22 +++++++++++++--------- src/sync/streaming/SSEClient/index.ts | 8 ++++---- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/src/__tests__/testUtils/eventSourceMock.ts b/src/__tests__/testUtils/eventSourceMock.ts index 7e74f985..281421b3 100644 --- a/src/__tests__/testUtils/eventSourceMock.ts +++ b/src/__tests__/testUtils/eventSourceMock.ts @@ -83,24 +83,28 @@ export default class EventSource { emit(eventName: string, messageEvent?: MessageEvent) { this.__emitter.emit(eventName, messageEvent); + + let listener: ((evt: any) => any) | undefined; + switch (eventName) { + case 'error': listener = this.onerror; break; + case 'open': listener = this.onopen; break; + case 'message': listener = this.onmessage; break; + } + if (typeof listener === 'function') { + listener(messageEvent); + } } emitError(error: MessageEvent) { - if (typeof this.onerror === 'function') { - this.onerror(error); - } + this.emit('error', error); } emitOpen() { this.readyState = 1; - if (typeof this.onopen === 'function') { - this.onopen(); - } + this.emit('open'); } emitMessage(message: MessageEvent) { - if (typeof this.onmessage === 'function') { - this.onmessage(message); - } + this.emit('message', message); } } diff --git a/src/sync/streaming/SSEClient/index.ts b/src/sync/streaming/SSEClient/index.ts index 32ba24f0..c613629d 100644 --- a/src/sync/streaming/SSEClient/index.ts +++ b/src/sync/streaming/SSEClient/index.ts @@ -83,7 +83,6 @@ export default class SSEClient implements ISSEClient { ).join(','); const url = `${this.streamingUrl}?channels=${channelsQueryParam}&accessToken=${authToken.token}&v=${VERSION}&heartbeats=true`; // same results using `&heartbeats=false` - // this.connection = new this.eventSource(url); this.connection = new this.eventSource( // For client-side SDKs, SplitSDKClientKey and SplitSDKClientKey metadata is passed as query params, // because native EventSource implementations for browser doesn't support headers. @@ -93,9 +92,9 @@ export default class SSEClient implements ISSEClient { ); if (this.handler) { // no need to check if SSEClient is used only by PushManager - this.connection.onopen = this.handler.handleOpen; - this.connection.onmessage = this.handler.handleMessage; - this.connection.onerror = this.handler.handleError; + this.connection.addEventListener('open', this.handler.handleOpen); + this.connection.addEventListener('message', this.handler.handleMessage); + this.connection.addEventListener('error', this.handler.handleError); } } @@ -108,6 +107,7 @@ export default class SSEClient implements ISSEClient { * Re-open the connection with the last given authToken. * * @throws {TypeError} if `open` has not been previously called with an authToken + * @TODO this method is not used currently and could be removed. */ reopen() { this.open(this.authToken as IAuthTokenPushEnabled); From 814014457060ed15b6b3519ad34147ccc11ee2b9 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 1 Jul 2021 13:10:26 -0300 Subject: [PATCH 087/144] signal listener is responsible of starting the sync manager --- src/listeners/__tests__/browser.spec.ts | 10 ++++++++-- src/listeners/__tests__/node.spec.ts | 13 ++++++++----- src/listeners/browser.ts | 2 +- src/listeners/node.ts | 3 ++- src/sdkFactory/index.ts | 4 +++- 5 files changed, 22 insertions(+), 10 deletions(-) diff --git a/src/listeners/__tests__/browser.spec.ts b/src/listeners/__tests__/browser.spec.ts index 3bd83169..0841f9b0 100644 --- a/src/listeners/__tests__/browser.spec.ts +++ b/src/listeners/__tests__/browser.spec.ts @@ -133,13 +133,16 @@ test('Browser JS listener / Impressions optimized mode', () => { }); test('Browser JS listener / Impressions debug mode', () => { - const syncManagerMockWithPushManager = { pushManager: { stop: jest.fn() } }; + const syncManagerMockWithPushManager = { start: jest.fn(), pushManager: { stop: jest.fn() } }; // @ts-expect-error const listener = new BrowserSignalListener(syncManagerMockWithPushManager, fullSettings, fakeStorageDebug as IStorageSync, fakeSplitApi); listener.start(); + // starting the listener must start the syncManager + expect(syncManagerMockWithPushManager.start).toBeCalled(); + // Assigned right function to right signal. expect((global.window.addEventListener as jest.Mock).mock.calls).toEqual([[UNLOAD_DOM_EVENT, listener.flushData]]); @@ -170,13 +173,16 @@ test('Browser JS listener / Impressions debug mode without sendBeacon API', () = // remove sendBeacon API const sendBeacon = global.navigator.sendBeacon; // @ts-expect-error global.navigator.sendBeacon = undefined; - const syncManagerMockWithoutPushManager = {}; + const syncManagerMockWithoutPushManager = { start: jest.fn() }; // @ts-expect-error const listener = new BrowserSignalListener(syncManagerMockWithoutPushManager, fullSettings, fakeStorageDebug as IStorageSync, fakeSplitApi); listener.start(); + // starting the listener must start the syncManager + expect(syncManagerMockWithoutPushManager.start).toBeCalled(); + // Assigned right function to right signal. expect((global.window.addEventListener as jest.Mock).mock.calls).toEqual([[UNLOAD_DOM_EVENT, listener.flushData]]); diff --git a/src/listeners/__tests__/node.spec.ts b/src/listeners/__tests__/node.spec.ts index 8f0805e3..70d4ab36 100644 --- a/src/listeners/__tests__/node.spec.ts +++ b/src/listeners/__tests__/node.spec.ts @@ -7,11 +7,14 @@ const processKillSpy = jest.spyOn(process, 'kill').mockImplementation(() => true test('Node JS listener / Signal Listener class methods and start/stop functionality', () => { - const syncManagerMock = { flush: jest.fn() }; // @ts-expect-error + const syncManagerMock = { start: jest.fn(), flush: jest.fn() }; // @ts-expect-error const listener = new NodeSignalListener(syncManagerMock, fullSettings); listener.start(); + // starting the listener must start the syncManager + expect(syncManagerMock.start).toBeCalled(); + // Assigned right function to right signal. // @ts-expect-error expect(processOnSpy.mock.calls).toEqual([['SIGTERM', listener._sigtermHandler]]); @@ -27,7 +30,7 @@ test('Node JS listener / Signal Listener class methods and start/stop functional test('Node JS listener / Signal Listener SIGTERM callback with sync handler', () => { - const syncManagerMock = { flush: jest.fn() }; // @ts-expect-error + const syncManagerMock = { start: jest.fn(), flush: jest.fn() }; // @ts-expect-error const listener = new NodeSignalListener(syncManagerMock, fullSettings); listener.start(); @@ -56,7 +59,7 @@ test('Node JS listener / Signal Listener SIGTERM callback with sync handler', () }); test('Node JS listener / Signal Listener SIGTERM callback with sync handler that throws an error', () => { - const syncManagerMock = { flush: jest.fn(() => { throw 'some error'; }) }; // @ts-expect-error + const syncManagerMock = { start: jest.fn(), flush: jest.fn(() => { throw 'some error'; }) }; // @ts-expect-error const listener = new NodeSignalListener(syncManagerMock, fullSettings); listener.start(); @@ -92,7 +95,7 @@ test('Node JS listener / Signal Listener SIGTERM callback with async handler', a res(); }, 0); }); - const syncManagerMock = { flush: jest.fn(() => fakePromise) }; + const syncManagerMock = { start: jest.fn(), flush: jest.fn(() => fakePromise) }; // @ts-expect-error const listener = new NodeSignalListener(syncManagerMock, fullSettings); @@ -136,7 +139,7 @@ test('Node JS listener / Signal Listener SIGTERM callback with async handler tha rej(); }, 0); }); - const syncManagerMock = { flush: jest.fn(() => fakePromise) }; + const syncManagerMock = { start: jest.fn(), flush: jest.fn(() => fakePromise) }; // @ts-expect-error const listener = new NodeSignalListener(syncManagerMock, fullSettings); diff --git a/src/listeners/browser.ts b/src/listeners/browser.ts index c5c4fdfd..a67b1d2f 100644 --- a/src/listeners/browser.ts +++ b/src/listeners/browser.ts @@ -39,6 +39,7 @@ export default class BrowserSignalListener implements ISignalListener { * We add a handler on unload events. The handler flushes remaining impressions and events to the backend. */ start() { + this.syncManager?.start(); if (typeof window !== 'undefined' && window.addEventListener) { this.settings.log.debug(CLEANUP_REGISTERING, [EVENT_NAME]); window.addEventListener(UNLOAD_DOM_EVENT, this.flushData); @@ -48,7 +49,6 @@ export default class BrowserSignalListener implements ISignalListener { /** * stop method. * Called when client is destroyed. - * We need to remove the handler for unload events, since it can break if called when Split context was destroyed. */ stop() { if (typeof window !== 'undefined' && window.removeEventListener) { diff --git a/src/listeners/node.ts b/src/listeners/node.ts index 9d8a22e9..d596a1de 100644 --- a/src/listeners/node.ts +++ b/src/listeners/node.ts @@ -22,7 +22,7 @@ export default class NodeSignalListener implements ISignalListener { private settings: ISettings; constructor( - syncManager: ISyncManager | undefined, // private handler: () => MaybeThenable, + private syncManager: ISyncManager | undefined, // private handler: () => MaybeThenable, settings: ISettings ) { // @TODO review handler logic when implementing Node SDK @@ -37,6 +37,7 @@ export default class NodeSignalListener implements ISignalListener { } start() { + this.syncManager?.start(); this.settings.log.debug(CLEANUP_REGISTERING, [EVENT_NAME]); // eslint-disable-next-line no-undef process.on(SIGTERM, this._sigtermHandler); diff --git a/src/sdkFactory/index.ts b/src/sdkFactory/index.ts index 0f89f516..2f1d6f34 100644 --- a/src/sdkFactory/index.ts +++ b/src/sdkFactory/index.ts @@ -78,7 +78,9 @@ export function sdkFactory(params: ISdkFactoryParams): SplitIO.ICsSDK | SplitIO. const clientMethod = sdkClientMethodFactory({ eventTracker, impressionsTracker, sdkReadinessManager, settings, storage, syncManager, signalListener }); const managerInstance = sdkManagerFactory(log, storage.splits, sdkReadinessManager); - syncManager && syncManager.start(); + // If there is a signalListener, it is in charge of starting the syncManager. + // It is required for RN to consider the app state (foreground/background). + if (syncManager && !signalListener) syncManager.start(); signalListener && signalListener.start(); log.info(NEW_FACTORY); From f8319a1bad38eefc7bc5082d0a14ab015e75b522 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 1 Jul 2021 13:20:47 -0300 Subject: [PATCH 088/144] support pausing/resuming the sync manager --- src/sync/streaming/pushManager.ts | 3 +++ src/sync/syncManagerOnline.ts | 20 +++++++++++++++----- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/src/sync/streaming/pushManager.ts b/src/sync/streaming/pushManager.ts index dee2fec3..2f880fb6 100644 --- a/src/sync/streaming/pushManager.ts +++ b/src/sync/streaming/pushManager.ts @@ -212,6 +212,9 @@ export default function pushManagerFactory( stop: disconnectPush, // `handleNonRetryableError` cannot be used as `stop`, because it emits PUSH_SUBSYSTEM_DOWN event, which start polling. start() { + // Guard condition to avoid calling `connectPush` again if the `start` method is called multiple times. + if (disconnected === false) return; + disconnected = false; // Run in next event-loop cycle for optimization on client-side: if multiple clients are created in the same cycle than the factory, only one authentication is performed. setTimeout(connectPush); }, diff --git a/src/sync/syncManagerOnline.ts b/src/sync/syncManagerOnline.ts index fd1625f9..40e78189 100644 --- a/src/sync/syncManagerOnline.ts +++ b/src/sync/syncManagerOnline.ts @@ -75,18 +75,25 @@ export function syncManagerOnlineFactory( pollingManager.syncAll(); } - let running = false; + let running = false; // flag that indicates whether the syncManager has been started (true) or stopped (false) + let startFirstTime = true; // flag to distinguish calling the `start` method for the first time, to support pausing and resuming the synchronization return { pushManager, + /** + * Method used to start the syncManager for the first time, or resume it after being stopped. + */ start() { // start syncing splits and segments if (pushManager) { - pollingManager.syncAll(); - pushManager.on(PUSH_SUBSYSTEM_UP, stopPollingAndSyncAll); - pushManager.on(PUSH_SUBSYSTEM_DOWN, startPolling); - // Run in next event-loop cycle as in client-side SyncManager + // Avoid calling `syncAll` when the syncManager is resumed + if (startFirstTime) { + pollingManager.syncAll(); + pushManager.on(PUSH_SUBSYSTEM_UP, stopPollingAndSyncAll); + pushManager.on(PUSH_SUBSYSTEM_DOWN, startPolling); + startFirstTime = false; + } pushManager.start(); } else { pollingManager.start(); @@ -97,6 +104,9 @@ export function syncManagerOnlineFactory( running = true; }, + /** + * Method used to stop/pause the syncManager. + */ stop() { // stop syncing if (pushManager) pushManager.stop(); From d52b437f213d557cdc4c27a0d84db3665df2e4e1 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 5 Jul 2021 11:13:51 -0300 Subject: [PATCH 089/144] rc --- package-lock.json | 2 +- package.json | 2 +- src/sdkFactory/index.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index aafbd77f..78b1cddf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "0.1.1-canary.8", + "version": "0.1.1-canary.9", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index f1e1afe8..b78f5f05 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "0.1.1-canary.8", + "version": "0.1.1-canary.9", "description": "Split Javascript SDK common components", "main": "cjs/index.js", "module": "esm/index.js", diff --git a/src/sdkFactory/index.ts b/src/sdkFactory/index.ts index 2f1d6f34..3e720244 100644 --- a/src/sdkFactory/index.ts +++ b/src/sdkFactory/index.ts @@ -79,7 +79,7 @@ export function sdkFactory(params: ISdkFactoryParams): SplitIO.ICsSDK | SplitIO. const managerInstance = sdkManagerFactory(log, storage.splits, sdkReadinessManager); // If there is a signalListener, it is in charge of starting the syncManager. - // It is required for RN to consider the app state (foreground/background). + // It is required for RN to consider the app state when the SDK is instantiated (foreground/background). if (syncManager && !signalListener) syncManager.start(); signalListener && signalListener.start(); From 032bcab7bccaed57c8833e69fe34d7934de779cb Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 6 Jul 2021 18:56:38 -0300 Subject: [PATCH 090/144] revert commit 8140144 --- src/listeners/__tests__/browser.spec.ts | 10 ++-------- src/listeners/__tests__/node.spec.ts | 13 +++++-------- src/listeners/browser.ts | 2 +- src/listeners/node.ts | 3 +-- src/sdkFactory/index.ts | 4 +--- 5 files changed, 10 insertions(+), 22 deletions(-) diff --git a/src/listeners/__tests__/browser.spec.ts b/src/listeners/__tests__/browser.spec.ts index 0841f9b0..3bd83169 100644 --- a/src/listeners/__tests__/browser.spec.ts +++ b/src/listeners/__tests__/browser.spec.ts @@ -133,16 +133,13 @@ test('Browser JS listener / Impressions optimized mode', () => { }); test('Browser JS listener / Impressions debug mode', () => { - const syncManagerMockWithPushManager = { start: jest.fn(), pushManager: { stop: jest.fn() } }; + const syncManagerMockWithPushManager = { pushManager: { stop: jest.fn() } }; // @ts-expect-error const listener = new BrowserSignalListener(syncManagerMockWithPushManager, fullSettings, fakeStorageDebug as IStorageSync, fakeSplitApi); listener.start(); - // starting the listener must start the syncManager - expect(syncManagerMockWithPushManager.start).toBeCalled(); - // Assigned right function to right signal. expect((global.window.addEventListener as jest.Mock).mock.calls).toEqual([[UNLOAD_DOM_EVENT, listener.flushData]]); @@ -173,16 +170,13 @@ test('Browser JS listener / Impressions debug mode without sendBeacon API', () = // remove sendBeacon API const sendBeacon = global.navigator.sendBeacon; // @ts-expect-error global.navigator.sendBeacon = undefined; - const syncManagerMockWithoutPushManager = { start: jest.fn() }; + const syncManagerMockWithoutPushManager = {}; // @ts-expect-error const listener = new BrowserSignalListener(syncManagerMockWithoutPushManager, fullSettings, fakeStorageDebug as IStorageSync, fakeSplitApi); listener.start(); - // starting the listener must start the syncManager - expect(syncManagerMockWithoutPushManager.start).toBeCalled(); - // Assigned right function to right signal. expect((global.window.addEventListener as jest.Mock).mock.calls).toEqual([[UNLOAD_DOM_EVENT, listener.flushData]]); diff --git a/src/listeners/__tests__/node.spec.ts b/src/listeners/__tests__/node.spec.ts index 70d4ab36..8f0805e3 100644 --- a/src/listeners/__tests__/node.spec.ts +++ b/src/listeners/__tests__/node.spec.ts @@ -7,14 +7,11 @@ const processKillSpy = jest.spyOn(process, 'kill').mockImplementation(() => true test('Node JS listener / Signal Listener class methods and start/stop functionality', () => { - const syncManagerMock = { start: jest.fn(), flush: jest.fn() }; // @ts-expect-error + const syncManagerMock = { flush: jest.fn() }; // @ts-expect-error const listener = new NodeSignalListener(syncManagerMock, fullSettings); listener.start(); - // starting the listener must start the syncManager - expect(syncManagerMock.start).toBeCalled(); - // Assigned right function to right signal. // @ts-expect-error expect(processOnSpy.mock.calls).toEqual([['SIGTERM', listener._sigtermHandler]]); @@ -30,7 +27,7 @@ test('Node JS listener / Signal Listener class methods and start/stop functional test('Node JS listener / Signal Listener SIGTERM callback with sync handler', () => { - const syncManagerMock = { start: jest.fn(), flush: jest.fn() }; // @ts-expect-error + const syncManagerMock = { flush: jest.fn() }; // @ts-expect-error const listener = new NodeSignalListener(syncManagerMock, fullSettings); listener.start(); @@ -59,7 +56,7 @@ test('Node JS listener / Signal Listener SIGTERM callback with sync handler', () }); test('Node JS listener / Signal Listener SIGTERM callback with sync handler that throws an error', () => { - const syncManagerMock = { start: jest.fn(), flush: jest.fn(() => { throw 'some error'; }) }; // @ts-expect-error + const syncManagerMock = { flush: jest.fn(() => { throw 'some error'; }) }; // @ts-expect-error const listener = new NodeSignalListener(syncManagerMock, fullSettings); listener.start(); @@ -95,7 +92,7 @@ test('Node JS listener / Signal Listener SIGTERM callback with async handler', a res(); }, 0); }); - const syncManagerMock = { start: jest.fn(), flush: jest.fn(() => fakePromise) }; + const syncManagerMock = { flush: jest.fn(() => fakePromise) }; // @ts-expect-error const listener = new NodeSignalListener(syncManagerMock, fullSettings); @@ -139,7 +136,7 @@ test('Node JS listener / Signal Listener SIGTERM callback with async handler tha rej(); }, 0); }); - const syncManagerMock = { start: jest.fn(), flush: jest.fn(() => fakePromise) }; + const syncManagerMock = { flush: jest.fn(() => fakePromise) }; // @ts-expect-error const listener = new NodeSignalListener(syncManagerMock, fullSettings); diff --git a/src/listeners/browser.ts b/src/listeners/browser.ts index a67b1d2f..c5c4fdfd 100644 --- a/src/listeners/browser.ts +++ b/src/listeners/browser.ts @@ -39,7 +39,6 @@ export default class BrowserSignalListener implements ISignalListener { * We add a handler on unload events. The handler flushes remaining impressions and events to the backend. */ start() { - this.syncManager?.start(); if (typeof window !== 'undefined' && window.addEventListener) { this.settings.log.debug(CLEANUP_REGISTERING, [EVENT_NAME]); window.addEventListener(UNLOAD_DOM_EVENT, this.flushData); @@ -49,6 +48,7 @@ export default class BrowserSignalListener implements ISignalListener { /** * stop method. * Called when client is destroyed. + * We need to remove the handler for unload events, since it can break if called when Split context was destroyed. */ stop() { if (typeof window !== 'undefined' && window.removeEventListener) { diff --git a/src/listeners/node.ts b/src/listeners/node.ts index d596a1de..9d8a22e9 100644 --- a/src/listeners/node.ts +++ b/src/listeners/node.ts @@ -22,7 +22,7 @@ export default class NodeSignalListener implements ISignalListener { private settings: ISettings; constructor( - private syncManager: ISyncManager | undefined, // private handler: () => MaybeThenable, + syncManager: ISyncManager | undefined, // private handler: () => MaybeThenable, settings: ISettings ) { // @TODO review handler logic when implementing Node SDK @@ -37,7 +37,6 @@ export default class NodeSignalListener implements ISignalListener { } start() { - this.syncManager?.start(); this.settings.log.debug(CLEANUP_REGISTERING, [EVENT_NAME]); // eslint-disable-next-line no-undef process.on(SIGTERM, this._sigtermHandler); diff --git a/src/sdkFactory/index.ts b/src/sdkFactory/index.ts index 3e720244..0f89f516 100644 --- a/src/sdkFactory/index.ts +++ b/src/sdkFactory/index.ts @@ -78,9 +78,7 @@ export function sdkFactory(params: ISdkFactoryParams): SplitIO.ICsSDK | SplitIO. const clientMethod = sdkClientMethodFactory({ eventTracker, impressionsTracker, sdkReadinessManager, settings, storage, syncManager, signalListener }); const managerInstance = sdkManagerFactory(log, storage.splits, sdkReadinessManager); - // If there is a signalListener, it is in charge of starting the syncManager. - // It is required for RN to consider the app state when the SDK is instantiated (foreground/background). - if (syncManager && !signalListener) syncManager.start(); + syncManager && syncManager.start(); signalListener && signalListener.start(); log.info(NEW_FACTORY); From b1a05a9f6d71fc21850d88c08139da82929f5dcf Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 6 Jul 2021 19:11:55 -0300 Subject: [PATCH 091/144] polishing in syncManager --- src/sync/streaming/pushManager.ts | 2 +- src/sync/syncManagerOnline.ts | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/sync/streaming/pushManager.ts b/src/sync/streaming/pushManager.ts index 2f880fb6..3eba2f88 100644 --- a/src/sync/streaming/pushManager.ts +++ b/src/sync/streaming/pushManager.ts @@ -209,7 +209,7 @@ export default function pushManagerFactory( Object.create(pushEmitter), { // Expose functionality for starting and stoping push mode: - stop: disconnectPush, // `handleNonRetryableError` cannot be used as `stop`, because it emits PUSH_SUBSYSTEM_DOWN event, which start polling. + stop: disconnectPush, // `handleNonRetryableError` cannot be used as `stop`, because it emits PUSH_SUBSYSTEM_DOWN event, which starts polling. start() { // Guard condition to avoid calling `connectPush` again if the `start` method is called multiple times. diff --git a/src/sync/syncManagerOnline.ts b/src/sync/syncManagerOnline.ts index 40e78189..b0199a9c 100644 --- a/src/sync/syncManagerOnline.ts +++ b/src/sync/syncManagerOnline.ts @@ -75,6 +75,11 @@ export function syncManagerOnlineFactory( pollingManager.syncAll(); } + if (pushManager) { + pushManager.on(PUSH_SUBSYSTEM_UP, stopPollingAndSyncAll); + pushManager.on(PUSH_SUBSYSTEM_DOWN, startPolling); + } + let running = false; // flag that indicates whether the syncManager has been started (true) or stopped (false) let startFirstTime = true; // flag to distinguish calling the `start` method for the first time, to support pausing and resuming the synchronization @@ -87,11 +92,9 @@ export function syncManagerOnlineFactory( start() { // start syncing splits and segments if (pushManager) { - // Avoid calling `syncAll` when the syncManager is resumed + // Doesn't call `syncAll` when the syncManager is resuming if (startFirstTime) { pollingManager.syncAll(); - pushManager.on(PUSH_SUBSYSTEM_UP, stopPollingAndSyncAll); - pushManager.on(PUSH_SUBSYSTEM_DOWN, startPolling); startFirstTime = false; } pushManager.start(); From a29a78b9112906721c63c6c270128639fd955c66 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 8 Jul 2021 00:53:54 -0300 Subject: [PATCH 092/144] ignore pushManager start call if streaming is disabled --- .../streaming/__tests__/pushManager.spec.ts | 36 +++++++++++++++++++ src/sync/streaming/pushManager.ts | 12 ++++--- 2 files changed, 44 insertions(+), 4 deletions(-) create mode 100644 src/sync/streaming/__tests__/pushManager.spec.ts diff --git a/src/sync/streaming/__tests__/pushManager.spec.ts b/src/sync/streaming/__tests__/pushManager.spec.ts new file mode 100644 index 00000000..ef7b9b73 --- /dev/null +++ b/src/sync/streaming/__tests__/pushManager.spec.ts @@ -0,0 +1,36 @@ +import pushManagerFactory from '../pushManager'; +import EventEmitter from '../../../utils/MinEvents'; +import { fullSettings } from '../../../utils/settingsValidation/__tests__/settings.mocks'; +import { IPushManagerCS } from '../types'; + +const platformMock = { + getEventSource: jest.fn(() => { return () => { }; }), + EventEmitter +}; + +test('pushManagerFactory returns undefined if EventSource is not available', () => { + // @ts-ignore + const pushManager = pushManagerFactory({}, {}, {}, () => { }, { + getEventSource: () => undefined, + EventEmitter + }, fullSettings); + + expect(pushManager).toBe(undefined); +}); + +test('pushManager does not connect if it is stopped inmediatelly after being started', (done) => { + const fetchAuthMock = jest.fn(); + + // @ts-ignore + const pushManager = pushManagerFactory({}, {}, {}, fetchAuthMock, platformMock, fullSettings) as IPushManagerCS; + + pushManager.start(); + pushManager.start(); + pushManager.stop(); + pushManager.stop(); + + setTimeout(() => { + expect(fetchAuthMock).not.toBeCalled(); + done(); + }); +}); diff --git a/src/sync/streaming/pushManager.ts b/src/sync/streaming/pushManager.ts index 3eba2f88..f3e04c0f 100644 --- a/src/sync/streaming/pushManager.ts +++ b/src/sync/streaming/pushManager.ts @@ -74,9 +74,11 @@ export default function pushManagerFactory( // [Only for client-side] variable to flag that a new client was added. It is needed to reconnect streaming. let connectForNewClient = false; - // flag that indicates if `disconnectPush` was called, either by the SyncManager (when the client is destroyed) or by a PUSH_NONRETRYABLE_ERROR error. + // flag that indicates if `disconnectPush` was called (stopped), either by the SyncManager when the client is destroyed or by a PUSH_NONRETRYABLE_ERROR error. // It is used to halt the `connectPush` process if it was in progress. let disconnected: boolean | undefined; + // flag that indicates a PUSH_NONRETRYABLE_ERROR, condition with which starting streaming is ignored. + let disabled: boolean | undefined; /** PushManager functions related to initialization */ @@ -97,7 +99,8 @@ export default function pushManagerFactory( } function connectPush() { - disconnected = false; + if (disconnected) return; + log.info(STREAMING_CONNECTING); const userKeys = userKey ? Object.keys(workers) : undefined; @@ -166,6 +169,7 @@ export default function pushManagerFactory( /** Fallbacking without retry due to: STREAMING_DISABLED control event, or 'pushEnabled: false', or non-recoverable SSE and Authentication errors */ pushEmitter.on(PUSH_NONRETRYABLE_ERROR, function handleNonRetryableError() { + disabled = true; // Note: `stopWorkers` is been called twice, but it is not harmful disconnectPush(); pushEmitter.emit(PUSH_SUBSYSTEM_DOWN); // no harm if polling already @@ -212,8 +216,8 @@ export default function pushManagerFactory( stop: disconnectPush, // `handleNonRetryableError` cannot be used as `stop`, because it emits PUSH_SUBSYSTEM_DOWN event, which starts polling. start() { - // Guard condition to avoid calling `connectPush` again if the `start` method is called multiple times. - if (disconnected === false) return; + // Guard condition to avoid calling `connectPush` again if the `start` method is called multiple times or if push has been disabled. + if (disabled || disconnected === false) return; disconnected = false; // Run in next event-loop cycle for optimization on client-side: if multiple clients are created in the same cycle than the factory, only one authentication is performed. setTimeout(connectPush); From 2f6a1a8f99adc129f425c54e14be0e0f2cae38fa Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 8 Jul 2021 00:59:14 -0300 Subject: [PATCH 093/144] polishing --- src/sync/streaming/__tests__/pushManager.spec.ts | 2 +- src/sync/streaming/pushManager.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/sync/streaming/__tests__/pushManager.spec.ts b/src/sync/streaming/__tests__/pushManager.spec.ts index ef7b9b73..9b6fb784 100644 --- a/src/sync/streaming/__tests__/pushManager.spec.ts +++ b/src/sync/streaming/__tests__/pushManager.spec.ts @@ -18,7 +18,7 @@ test('pushManagerFactory returns undefined if EventSource is not available', () expect(pushManager).toBe(undefined); }); -test('pushManager does not connect if it is stopped inmediatelly after being started', (done) => { +test('pushManager does not connect to streaming if it is stopped inmediatelly after being started', (done) => { const fetchAuthMock = jest.fn(); // @ts-ignore diff --git a/src/sync/streaming/pushManager.ts b/src/sync/streaming/pushManager.ts index f3e04c0f..b84785bb 100644 --- a/src/sync/streaming/pushManager.ts +++ b/src/sync/streaming/pushManager.ts @@ -74,10 +74,10 @@ export default function pushManagerFactory( // [Only for client-side] variable to flag that a new client was added. It is needed to reconnect streaming. let connectForNewClient = false; - // flag that indicates if `disconnectPush` was called (stopped), either by the SyncManager when the client is destroyed or by a PUSH_NONRETRYABLE_ERROR error. + // flag that indicates if `stop/disconnectPush` was called, either by the SyncManager, when the client is destroyed, or due to a PUSH_NONRETRYABLE_ERROR error. // It is used to halt the `connectPush` process if it was in progress. let disconnected: boolean | undefined; - // flag that indicates a PUSH_NONRETRYABLE_ERROR, condition with which starting streaming is ignored. + // flag that indicates a PUSH_NONRETRYABLE_ERROR, condition with which starting pushManager again is ignored. let disabled: boolean | undefined; /** PushManager functions related to initialization */ From 296aff4d999d2c8013341ef1c9408ec2c29a1d8d Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 8 Jul 2021 19:13:20 -0300 Subject: [PATCH 094/144] rc --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 78b1cddf..df48e364 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "0.1.1-canary.9", + "version": "0.1.1-canary.10", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index b78f5f05..de5f3eae 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "0.1.1-canary.9", + "version": "0.1.1-canary.10", "description": "Split Javascript SDK common components", "main": "cjs/index.js", "module": "esm/index.js", From 72c1c0616ac8c24a85417d1bb981ff6c6d9b275c Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Fri, 16 Jul 2021 20:11:00 -0300 Subject: [PATCH 095/144] polishing --- src/sync/streaming/pushManager.ts | 2 +- src/sync/streaming/pushManagerCS.ts | 2 +- src/sync/streaming/pushManagerSS.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/sync/streaming/pushManager.ts b/src/sync/streaming/pushManager.ts index b84785bb..589d0ee6 100644 --- a/src/sync/streaming/pushManager.ts +++ b/src/sync/streaming/pushManager.ts @@ -144,8 +144,8 @@ export default function pushManagerFactory( // close SSE connection and cancel scheduled tasks function disconnectPush() { - sseClient.close(); disconnected = true; + sseClient.close(); log.info(STREAMING_DISCONNECTING); if (timeoutId) clearTimeout(timeoutId); diff --git a/src/sync/streaming/pushManagerCS.ts b/src/sync/streaming/pushManagerCS.ts index 63bc0995..ee1a59cf 100644 --- a/src/sync/streaming/pushManagerCS.ts +++ b/src/sync/streaming/pushManagerCS.ts @@ -135,8 +135,8 @@ export default function pushManagerCSFactory( // close SSE connection and cancel scheduled tasks function disconnectPush() { - sseClient.close(); disconnected = true; + sseClient.close(); log.info(STREAMING_DISCONNECTING); if (timeoutId) clearTimeout(timeoutId); diff --git a/src/sync/streaming/pushManagerSS.ts b/src/sync/streaming/pushManagerSS.ts index ab9485e5..17674746 100644 --- a/src/sync/streaming/pushManagerSS.ts +++ b/src/sync/streaming/pushManagerSS.ts @@ -111,8 +111,8 @@ export default function pushManagerSSFactory( // close SSE connection and cancel scheduled tasks function disconnectPush() { - sseClient.close(); disconnected = true; + sseClient.close(); log.info(STREAMING_DISCONNECTING); if (timeoutId) clearTimeout(timeoutId); From fd82270f9c04cc61a96ca3202119a89de46a0ff8 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Fri, 16 Jul 2021 20:13:26 -0300 Subject: [PATCH 096/144] more refactors for RN --- package-lock.json | 2 +- package.json | 2 +- src/logger/messages/error.ts | 2 +- src/sync/streaming/SSEClient/index.ts | 7 +++++-- src/sync/streaming/SSEClient/types.ts | 1 + src/sync/streaming/SSEHandler/index.ts | 14 +++++++++++++- src/sync/streaming/SSEHandler/types.ts | 2 +- 7 files changed, 23 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index df48e364..f7cfa1c8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "0.1.1-canary.10", + "version": "0.1.1-canary.11", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index de5f3eae..0a4257ba 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "0.1.1-canary.10", + "version": "0.1.1-canary.11", "description": "Split Javascript SDK common components", "main": "cjs/index.js", "module": "esm/index.js", diff --git a/src/logger/messages/error.ts b/src/logger/messages/error.ts index cd51e80c..3df90b60 100644 --- a/src/logger/messages/error.ts +++ b/src/logger/messages/error.ts @@ -11,7 +11,7 @@ export const codesError: [number, string][] = [ [c.ERROR_EVENTS_TRACKER, c.LOG_PREFIX_EVENTS_TRACKER + 'Failed to queue %s'], // synchronizer [c.ERROR_SYNC_OFFLINE_LOADING, c.LOG_PREFIX_SYNC_OFFLINE + 'There was an issue loading the mock Splits data, no changes will be applied to the current cache. %s'], - [c.ERROR_STREAMING_SSE, c.LOG_PREFIX_SYNC_STREAMING + 'Fail to connect to streaming, with error message: %s'], + [c.ERROR_STREAMING_SSE, c.LOG_PREFIX_SYNC_STREAMING + 'Failed to connect or error on streaming connection, with error message: %s'], [c.ERROR_STREAMING_AUTH, c.LOG_PREFIX_SYNC_STREAMING + 'Failed to authenticate for streaming. Error: "%s".'], [c.ERROR_HTTP, ' Response status is not OK. Status: %s. URL: %s. Message: %s'], // client status diff --git a/src/sync/streaming/SSEClient/index.ts b/src/sync/streaming/SSEClient/index.ts index c613629d..4b5f9397 100644 --- a/src/sync/streaming/SSEClient/index.ts +++ b/src/sync/streaming/SSEClient/index.ts @@ -47,7 +47,7 @@ export default class SSEClient implements ISSEClient { * @param getEventSource Function to get the EventSource constructor. * @throws 'EventSource API is not available. ' if EventSource is not available. */ - constructor(settings: ISettings, useHeaders?: boolean, getEventSource?: () => typeof EventSource | undefined) { + constructor(settings: ISettings, useHeaders?: boolean, getEventSource?: () => (typeof EventSource | undefined)) { // @ts-expect-error this.eventSource = getEventSource && getEventSource(); // if eventSource is not available, throw an exception @@ -100,7 +100,10 @@ export default class SSEClient implements ISSEClient { /** Close connection */ close() { - if (this.connection) this.connection.close(); + if (this.connection) { + if (this.handler) this.handler.handleClose(); + this.connection.close(); + } } /** diff --git a/src/sync/streaming/SSEClient/types.ts b/src/sync/streaming/SSEClient/types.ts index aba3c03f..55e25608 100644 --- a/src/sync/streaming/SSEClient/types.ts +++ b/src/sync/streaming/SSEClient/types.ts @@ -4,6 +4,7 @@ export interface ISseEventHandler { handleError: (ev: Event) => any; handleMessage: (ev: MessageEvent) => any; handleOpen: () => any; + handleClose: () => any; } export interface ISSEClient { diff --git a/src/sync/streaming/SSEHandler/index.ts b/src/sync/streaming/SSEHandler/index.ts index 716564d8..f772e0c5 100644 --- a/src/sync/streaming/SSEHandler/index.ts +++ b/src/sync/streaming/SSEHandler/index.ts @@ -29,13 +29,25 @@ export default function SSEHandlerFactory(log: ILogger, pushEmitter: IPushEventE const notificationKeeper = notificationKeeperFactory(pushEmitter); + // Whether the SSEClient connection has been explicitly closed or not + let closeMethodInvoked = false; + return { handleOpen() { + closeMethodInvoked = false; notificationKeeper.handleOpen(); }, + /* Called when SSEClient.close method is called */ + handleClose() { + closeMethodInvoked = true; + }, + /* HTTP & Network errors */ handleError(error) { + // Ignore EventSource errors if `close` method has been called, for those implementations that emit an error when closing. + if (closeMethodInvoked) return; + let errorWithParsedData: INotificationError = error; try { errorWithParsedData = errorParser(error); @@ -43,7 +55,7 @@ export default function SSEHandlerFactory(log: ILogger, pushEmitter: IPushEventE log.warn(STREAMING_PARSING_ERROR_FAILS, [err]); } - let errorMessage = errorWithParsedData.parsedData && errorWithParsedData.parsedData.message; + let errorMessage = (errorWithParsedData.parsedData && errorWithParsedData.parsedData.message) || errorWithParsedData.message; log.error(ERROR_STREAMING_SSE, [errorMessage]); if (isRetryableError(errorWithParsedData)) { diff --git a/src/sync/streaming/SSEHandler/types.ts b/src/sync/streaming/SSEHandler/types.ts index 039c1675..f5583c9c 100644 --- a/src/sync/streaming/SSEHandler/types.ts +++ b/src/sync/streaming/SSEHandler/types.ts @@ -40,4 +40,4 @@ export interface IOccupancyData { export type INotificationData = IMySegmentsUpdateData | ISegmentUpdateData | ISplitUpdateData | ISplitKillData | IControlData | IOccupancyData export type INotificationMessage = { parsedData: INotificationData, channel: string, timestamp: number, data: string } -export type INotificationError = Event & { parsedData?: any } +export type INotificationError = Event & { parsedData?: any, message?: string } From 5d65143db69c68cf6547db8989c347ced962eaf2 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Fri, 16 Jul 2021 20:46:26 -0300 Subject: [PATCH 097/144] updated tests --- .../SSEClient/__tests__/index.spec.ts | 2 ++ .../SSEHandler/__tests__/index.spec.ts | 34 +++++++++++++++---- 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/src/sync/streaming/SSEClient/__tests__/index.spec.ts b/src/sync/streaming/SSEClient/__tests__/index.spec.ts index f0c35d23..7eab6e08 100644 --- a/src/sync/streaming/SSEClient/__tests__/index.spec.ts +++ b/src/sync/streaming/SSEClient/__tests__/index.spec.ts @@ -32,6 +32,7 @@ test('SSClient / setEventHandler, open and close methods', () => { handleOpen: jest.fn(), handleError: jest.fn(), handleMessage: jest.fn(), + handleClose: jest.fn(), }; // instance SSEClient @@ -63,6 +64,7 @@ test('SSClient / setEventHandler, open and close methods', () => { // close connection instance.close(); expect(instance.connection.readyState).toBe(2); // connection readyState is CLOSED (2) + expect(handler.handleClose).toBeCalledTimes(1); // handleClose called when connection close method is called // open attempt without open event emitted instance.open(authDataSample); diff --git a/src/sync/streaming/SSEHandler/__tests__/index.spec.ts b/src/sync/streaming/SSEHandler/__tests__/index.spec.ts index 52473d0d..dcdff4ea 100644 --- a/src/sync/streaming/SSEHandler/__tests__/index.spec.ts +++ b/src/sync/streaming/SSEHandler/__tests__/index.spec.ts @@ -13,17 +13,17 @@ import mySegmentsUpdateMessage from '../../../../__tests__/mocks/message.MY_SEGM import occupancy1ControlPri from '../../../../__tests__/mocks/message.OCCUPANCY.1.control_pri.1586987434450.json'; import occupancy0ControlPri from '../../../../__tests__/mocks/message.OCCUPANCY.0.control_pri.1586987434550.json'; import occupancy2ControlPri from '../../../../__tests__/mocks/message.OCCUPANCY.2.control_pri.1586987434650.json'; -const occupancy1ControlSec = {...occupancy1ControlPri, data: occupancy1ControlPri.data.replace('control_pri', 'control_sec')}; -const occupancy0ControlSec = {...occupancy0ControlPri, data: occupancy0ControlPri.data.replace('control_pri', 'control_sec')}; -const occupancy2ControlSec = {...occupancy2ControlPri, data: occupancy2ControlPri.data.replace('control_pri', 'control_sec')}; +const occupancy1ControlSec = { ...occupancy1ControlPri, data: occupancy1ControlPri.data.replace('control_pri', 'control_sec') }; +const occupancy0ControlSec = { ...occupancy0ControlPri, data: occupancy0ControlPri.data.replace('control_pri', 'control_sec') }; +const occupancy2ControlSec = { ...occupancy2ControlPri, data: occupancy2ControlPri.data.replace('control_pri', 'control_sec') }; // control messages import controlStreamingPaused from '../../../../__tests__/mocks/message.CONTROL.STREAMING_PAUSED.control_pri.1586987434750.json'; import controlStreamingResumed from '../../../../__tests__/mocks/message.CONTROL.STREAMING_RESUMED.control_pri.1586987434850.json'; import controlStreamingDisabled from '../../../../__tests__/mocks/message.CONTROL.STREAMING_DISABLED.control_pri.1586987434950.json'; -const controlStreamingPausedSec = {...controlStreamingPaused, data: controlStreamingPaused.data.replace('control_pri', 'control_sec')}; -const controlStreamingResumedSec = {...controlStreamingResumed, data: controlStreamingResumed.data.replace('control_pri', 'control_sec')}; -const controlStreamingDisabledSec = {...controlStreamingDisabled, data: controlStreamingDisabled.data.replace('control_pri', 'control_sec')}; +const controlStreamingPausedSec = { ...controlStreamingPaused, data: controlStreamingPaused.data.replace('control_pri', 'control_sec') }; +const controlStreamingResumedSec = { ...controlStreamingResumed, data: controlStreamingResumed.data.replace('control_pri', 'control_sec') }; +const controlStreamingDisabledSec = { ...controlStreamingDisabled, data: controlStreamingDisabled.data.replace('control_pri', 'control_sec') }; const pushEmitter = { emit: jest.fn() }; @@ -181,3 +181,25 @@ test('handlerMessage: ignore invalid events', () => { expect(pushEmitter.emit).toBeCalledTimes(1); // must handle massage if valid }); + +test('handleClose: ignore errors after closing', () => { + const sseHandler = SSEHandlerFactory(loggerMock, pushEmitter); + sseHandler.handleOpen(); + pushEmitter.emit.mockClear(); + + const error = 'some error'; + sseHandler.handleError(error); + expect(pushEmitter.emit).toBeCalledTimes(1); // + PUSH_SUBSYSTEM_UP, + PUSH_RETRYABLE_ERROR + + sseHandler.handleClose(); + sseHandler.handleError(error); + expect(pushEmitter.emit).toBeCalledTimes(1); + + sseHandler.handleOpen(); + sseHandler.handleError(error); + expect(pushEmitter.emit).toBeCalledTimes(3); // + PUSH_SUBSYSTEM_UP, + PUSH_RETRYABLE_ERROR + + sseHandler.handleClose(); + sseHandler.handleError(error); + expect(pushEmitter.emit).toBeCalledTimes(3); +}); From 3b7a99e6541b5c2f6c78e456f62065b916ea7edd Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 21 Jul 2021 19:23:52 -0300 Subject: [PATCH 098/144] fix: avoiding warning 'Error parsing SSE message notification' for keepalive message events --- src/sync/streaming/SSEHandler/NotificationParser.ts | 6 ++++-- src/sync/streaming/SSEHandler/index.ts | 5 +++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/sync/streaming/SSEHandler/NotificationParser.ts b/src/sync/streaming/SSEHandler/NotificationParser.ts index a7403a0e..b9c593cf 100644 --- a/src/sync/streaming/SSEHandler/NotificationParser.ts +++ b/src/sync/streaming/SSEHandler/NotificationParser.ts @@ -22,10 +22,12 @@ export function errorParser(error: Event): INotificationError { * Also assigns the type OCCUPANCY, if it corresponds, so that all supported messages (e.g., SPLIT_UPDATE, CONTROL) have a type. * * @param message - * @returns parsed notification message + * @returns parsed notification message or undefined if the given event has not a non-empty data string. + * For example, the EventSource implementation of React-Native for iOS emits a message event with empty data for each Ably keepalive comment. * @throws {SyntaxError} if `message.data` or `JSON.parse(message.data).data` are invalid JSON strings */ -export function messageParser(message: MessageEvent): INotificationMessage { +export function messageParser(message: MessageEvent): INotificationMessage | undefined { + if (!message.data) return; const messageData = JSON.parse(message.data); messageData.parsedData = JSON.parse(messageData.data); diff --git a/src/sync/streaming/SSEHandler/index.ts b/src/sync/streaming/SSEHandler/index.ts index f772e0c5..5f436ace 100644 --- a/src/sync/streaming/SSEHandler/index.ts +++ b/src/sync/streaming/SSEHandler/index.ts @@ -3,7 +3,7 @@ import notificationKeeperFactory from './NotificationKeeper'; import { PUSH_RETRYABLE_ERROR, PUSH_NONRETRYABLE_ERROR, OCCUPANCY, CONTROL, MY_SEGMENTS_UPDATE, SEGMENT_UPDATE, SPLIT_KILL, SPLIT_UPDATE } from '../constants'; import { IPushEventEmitter } from '../types'; import { ISseEventHandler } from '../SSEClient/types'; -import { INotificationError } from './types'; +import { INotificationError, INotificationMessage } from './types'; import { ILogger } from '../../../logger/types'; import { STREAMING_PARSING_ERROR_FAILS, ERROR_STREAMING_SSE, STREAMING_PARSING_MESSAGE_FAILS, STREAMING_NEW_MESSAGE } from '../../../logger/constants'; @@ -67,9 +67,10 @@ export default function SSEHandlerFactory(log: ILogger, pushEmitter: IPushEventE /* NotificationProcessor */ handleMessage(message) { - let messageWithParsedData; + let messageWithParsedData: INotificationMessage | undefined; try { messageWithParsedData = messageParser(message); + if (!messageWithParsedData) return; // keepalive event messages are ignored } catch (err) { log.warn(STREAMING_PARSING_MESSAGE_FAILS, [err]); return; From 514671cb1b1e2c692e35a785f11b97d45cbe0b95 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Fri, 23 Jul 2021 13:45:09 -0300 Subject: [PATCH 099/144] chore: comments polishing --- src/sync/streaming/SSEHandler/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sync/streaming/SSEHandler/index.ts b/src/sync/streaming/SSEHandler/index.ts index 5f436ace..f018f501 100644 --- a/src/sync/streaming/SSEHandler/index.ts +++ b/src/sync/streaming/SSEHandler/index.ts @@ -45,7 +45,7 @@ export default function SSEHandlerFactory(log: ILogger, pushEmitter: IPushEventE /* HTTP & Network errors */ handleError(error) { - // Ignore EventSource errors if `close` method has been called, for those implementations that emit an error when closing. + // Ignore EventSource errors if `close` method has been called, for those implementations that emit an error when closing (e.g., RN Android). if (closeMethodInvoked) return; let errorWithParsedData: INotificationError = error; @@ -70,7 +70,7 @@ export default function SSEHandlerFactory(log: ILogger, pushEmitter: IPushEventE let messageWithParsedData: INotificationMessage | undefined; try { messageWithParsedData = messageParser(message); - if (!messageWithParsedData) return; // keepalive event messages are ignored + if (!messageWithParsedData) return; // Messages with empty data are ignored } catch (err) { log.warn(STREAMING_PARSING_MESSAGE_FAILS, [err]); return; From 5b058418b3d5cdc5f9490978a2e03047806d1779 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 27 Jul 2021 16:24:23 -0300 Subject: [PATCH 100/144] bug fix --- package-lock.json | 2 +- package.json | 2 +- .../SSEClient/__tests__/index.spec.ts | 2 -- src/sync/streaming/SSEClient/index.ts | 5 +---- src/sync/streaming/SSEClient/types.ts | 1 - .../SSEHandler/NotificationParser.ts | 4 ++-- .../SSEHandler/__tests__/index.spec.ts | 22 ------------------- src/sync/streaming/SSEHandler/index.ts | 12 ---------- 8 files changed, 5 insertions(+), 45 deletions(-) diff --git a/package-lock.json b/package-lock.json index f7cfa1c8..0d60f926 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "0.1.1-canary.11", + "version": "0.1.1-canary.12", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 0a4257ba..5d60ca1e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "0.1.1-canary.11", + "version": "0.1.1-canary.12", "description": "Split Javascript SDK common components", "main": "cjs/index.js", "module": "esm/index.js", diff --git a/src/sync/streaming/SSEClient/__tests__/index.spec.ts b/src/sync/streaming/SSEClient/__tests__/index.spec.ts index 7eab6e08..f0c35d23 100644 --- a/src/sync/streaming/SSEClient/__tests__/index.spec.ts +++ b/src/sync/streaming/SSEClient/__tests__/index.spec.ts @@ -32,7 +32,6 @@ test('SSClient / setEventHandler, open and close methods', () => { handleOpen: jest.fn(), handleError: jest.fn(), handleMessage: jest.fn(), - handleClose: jest.fn(), }; // instance SSEClient @@ -64,7 +63,6 @@ test('SSClient / setEventHandler, open and close methods', () => { // close connection instance.close(); expect(instance.connection.readyState).toBe(2); // connection readyState is CLOSED (2) - expect(handler.handleClose).toBeCalledTimes(1); // handleClose called when connection close method is called // open attempt without open event emitted instance.open(authDataSample); diff --git a/src/sync/streaming/SSEClient/index.ts b/src/sync/streaming/SSEClient/index.ts index 4b5f9397..585f0d17 100644 --- a/src/sync/streaming/SSEClient/index.ts +++ b/src/sync/streaming/SSEClient/index.ts @@ -100,10 +100,7 @@ export default class SSEClient implements ISSEClient { /** Close connection */ close() { - if (this.connection) { - if (this.handler) this.handler.handleClose(); - this.connection.close(); - } + if (this.connection) this.connection.close(); } /** diff --git a/src/sync/streaming/SSEClient/types.ts b/src/sync/streaming/SSEClient/types.ts index 55e25608..aba3c03f 100644 --- a/src/sync/streaming/SSEClient/types.ts +++ b/src/sync/streaming/SSEClient/types.ts @@ -4,7 +4,6 @@ export interface ISseEventHandler { handleError: (ev: Event) => any; handleMessage: (ev: MessageEvent) => any; handleOpen: () => any; - handleClose: () => any; } export interface ISSEClient { diff --git a/src/sync/streaming/SSEHandler/NotificationParser.ts b/src/sync/streaming/SSEHandler/NotificationParser.ts index b9c593cf..749fc332 100644 --- a/src/sync/streaming/SSEHandler/NotificationParser.ts +++ b/src/sync/streaming/SSEHandler/NotificationParser.ts @@ -22,8 +22,8 @@ export function errorParser(error: Event): INotificationError { * Also assigns the type OCCUPANCY, if it corresponds, so that all supported messages (e.g., SPLIT_UPDATE, CONTROL) have a type. * * @param message - * @returns parsed notification message or undefined if the given event has not a non-empty data string. - * For example, the EventSource implementation of React-Native for iOS emits a message event with empty data for each Ably keepalive comment. + * @returns parsed notification message or undefined if the given event data is falsy (e.g, '' or undefined). + * For example, the EventSource implementation of React-Native for iOS emits a message event with empty data for Ably keepalive comments. * @throws {SyntaxError} if `message.data` or `JSON.parse(message.data).data` are invalid JSON strings */ export function messageParser(message: MessageEvent): INotificationMessage | undefined { diff --git a/src/sync/streaming/SSEHandler/__tests__/index.spec.ts b/src/sync/streaming/SSEHandler/__tests__/index.spec.ts index dcdff4ea..9ff2a73b 100644 --- a/src/sync/streaming/SSEHandler/__tests__/index.spec.ts +++ b/src/sync/streaming/SSEHandler/__tests__/index.spec.ts @@ -181,25 +181,3 @@ test('handlerMessage: ignore invalid events', () => { expect(pushEmitter.emit).toBeCalledTimes(1); // must handle massage if valid }); - -test('handleClose: ignore errors after closing', () => { - const sseHandler = SSEHandlerFactory(loggerMock, pushEmitter); - sseHandler.handleOpen(); - pushEmitter.emit.mockClear(); - - const error = 'some error'; - sseHandler.handleError(error); - expect(pushEmitter.emit).toBeCalledTimes(1); // + PUSH_SUBSYSTEM_UP, + PUSH_RETRYABLE_ERROR - - sseHandler.handleClose(); - sseHandler.handleError(error); - expect(pushEmitter.emit).toBeCalledTimes(1); - - sseHandler.handleOpen(); - sseHandler.handleError(error); - expect(pushEmitter.emit).toBeCalledTimes(3); // + PUSH_SUBSYSTEM_UP, + PUSH_RETRYABLE_ERROR - - sseHandler.handleClose(); - sseHandler.handleError(error); - expect(pushEmitter.emit).toBeCalledTimes(3); -}); diff --git a/src/sync/streaming/SSEHandler/index.ts b/src/sync/streaming/SSEHandler/index.ts index f018f501..fbbf999c 100644 --- a/src/sync/streaming/SSEHandler/index.ts +++ b/src/sync/streaming/SSEHandler/index.ts @@ -29,25 +29,13 @@ export default function SSEHandlerFactory(log: ILogger, pushEmitter: IPushEventE const notificationKeeper = notificationKeeperFactory(pushEmitter); - // Whether the SSEClient connection has been explicitly closed or not - let closeMethodInvoked = false; - return { handleOpen() { - closeMethodInvoked = false; notificationKeeper.handleOpen(); }, - /* Called when SSEClient.close method is called */ - handleClose() { - closeMethodInvoked = true; - }, - /* HTTP & Network errors */ handleError(error) { - // Ignore EventSource errors if `close` method has been called, for those implementations that emit an error when closing (e.g., RN Android). - if (closeMethodInvoked) return; - let errorWithParsedData: INotificationError = error; try { errorWithParsedData = errorParser(error); From 91c126637b69bc0a1186ac47292c831c6dada8d4 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 27 Jul 2021 16:31:22 -0300 Subject: [PATCH 101/144] fixed some vulnerabilities --- package-lock.json | 107 ++++++++++++++++++---------------------------- 1 file changed, 42 insertions(+), 65 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0d60f926..250ea6a4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -108,41 +108,17 @@ }, "dependencies": { "browserslist": { - "version": "4.16.3", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.3.tgz", - "integrity": "sha512-vIyhWmIkULaq04Gt93txdh+j02yX/JzlyhLYbV3YQCn/zvES3JnY7TifHHvvr1w5hTDluNKMkV05cs4vy8Q7sw==", + "version": "4.16.6", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.6.tgz", + "integrity": "sha512-Wspk/PqO+4W9qp5iUTJsa1B/QrYn1keNCcEP5OvP7WBwT4KaDly0uONYmC6Xa3Z5IqnUgS0KcgLYu1l74x0ZXQ==", "dev": true, "requires": { - "caniuse-lite": "^1.0.30001181", - "colorette": "^1.2.1", - "electron-to-chromium": "^1.3.649", + "caniuse-lite": "^1.0.30001219", + "colorette": "^1.2.2", + "electron-to-chromium": "^1.3.723", "escalade": "^3.1.1", - "node-releases": "^1.1.70" + "node-releases": "^1.1.71" } - }, - "caniuse-lite": { - "version": "1.0.30001204", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001204.tgz", - "integrity": "sha512-JUdjWpcxfJ9IPamy2f5JaRDCaqJOxDzOSKtbdx4rH9VivMd1vIzoPumsJa9LoMIi4Fx2BV2KZOxWhNkBjaYivQ==", - "dev": true - }, - "electron-to-chromium": { - "version": "1.3.702", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.702.tgz", - "integrity": "sha512-qJVUKFWQnF6wP7MmTngDkmm8/KPzaiTXNFOAg5j7DSa6J7kPou7mTBqC8jpUOxauQWwHR3pn4dMRdV8IE1xdtA==", - "dev": true - }, - "escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "dev": true - }, - "node-releases": { - "version": "1.1.71", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.71.tgz", - "integrity": "sha512-zR6HoT6LrLCRBwukmrVbHv0EpEQjksO6GmFcZQQuCAy139BEsoVKPYnf3jongYW83fAa1torLGYwxxky/p28sg==", - "dev": true } } }, @@ -2412,15 +2388,16 @@ "dev": true }, "browserslist": { - "version": "4.14.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.14.0.tgz", - "integrity": "sha512-pUsXKAF2lVwhmtpeA3LJrZ76jXuusrNyhduuQs7CDFf9foT4Y38aQOserd2lMe5DSSrjf3fx34oHwryuvxAUgQ==", + "version": "4.16.6", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.6.tgz", + "integrity": "sha512-Wspk/PqO+4W9qp5iUTJsa1B/QrYn1keNCcEP5OvP7WBwT4KaDly0uONYmC6Xa3Z5IqnUgS0KcgLYu1l74x0ZXQ==", "dev": true, "requires": { - "caniuse-lite": "^1.0.30001111", - "electron-to-chromium": "^1.3.523", - "escalade": "^3.0.2", - "node-releases": "^1.1.60" + "caniuse-lite": "^1.0.30001219", + "colorette": "^1.2.2", + "electron-to-chromium": "^1.3.723", + "escalade": "^3.1.1", + "node-releases": "^1.1.71" } }, "bs-logger": { @@ -2483,9 +2460,9 @@ "dev": true }, "caniuse-lite": { - "version": "1.0.30001118", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001118.tgz", - "integrity": "sha512-RNKPLojZo74a0cP7jFMidQI7nvLER40HgNfgKQEJ2PFm225L0ectUungNQoK3Xk3StQcFbpBPNEvoWD59436Hg==", + "version": "1.0.30001247", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001247.tgz", + "integrity": "sha512-4rS7co+7+AoOSPRPOPUt5/GdaqZc0EsUpWk66ofE3HJTAajUK2Ss2VwoNzVN69ghg8lYYlh0an0Iy4LIHHo9UQ==", "dev": true }, "capture-exit": { @@ -2885,9 +2862,9 @@ } }, "electron-to-chromium": { - "version": "1.3.550", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.550.tgz", - "integrity": "sha512-MWDHoJsSieBaJ6mObvi3V0eKZufR1iMJl56VIGdXCgFHyCUbVDGm2IMXUNjDxgbGbKa3tA5+zJmHuBQqRxGiLA==", + "version": "1.3.788", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.788.tgz", + "integrity": "sha512-dbMIpX4E4/Gk4gzOh1GYS7ls1vGsByWKpIqLviJi1mSmSt5BvrWLLtSqpFE5BaC7Ef4NnI0GMaiddNX2Brw6zA==", "dev": true }, "emittery": { @@ -2930,9 +2907,9 @@ } }, "escalade": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.0.2.tgz", - "integrity": "sha512-gPYAU37hYCUhW5euPeR+Y74F7BL+IBsV93j5cvGriSaD1aG6MGsqsV1yamRdrWrb2j3aiZvb0X+UBOWpx3JWtQ==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", "dev": true }, "escape-string-regexp": { @@ -3057,15 +3034,6 @@ "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", "dev": true }, - "glob-parent": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", - "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", - "dev": true, - "requires": { - "is-glob": "^4.0.1" - } - }, "globals": { "version": "12.4.0", "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", @@ -3619,9 +3587,9 @@ } }, "glob-parent": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", - "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, "requires": { "is-glob": "^4.0.1" @@ -3913,6 +3881,15 @@ "path-is-absolute": "^1.0.0" } }, + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, "glob-to-regexp": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", @@ -6807,9 +6784,9 @@ } }, "node-releases": { - "version": "1.1.60", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.60.tgz", - "integrity": "sha512-gsO4vjEdQaTusZAEebUWp2a5d7dF5DYoIpDG7WySnk7BuZDW+GPpHXoXXuYawRBr/9t5q54tirPz79kFIWg4dA==", + "version": "1.1.73", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.73.tgz", + "integrity": "sha512-uW7fodD6pyW2FZNZnp/Z3hvWKeEW1Y8R1+1CnErE8cXFXzl5blBOoVB41CvMer6P6Q0S5FXDwcHgFd1Wj0U9zg==", "dev": true }, "normalize-package-data": { @@ -8729,9 +8706,9 @@ } }, "ws": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.4.tgz", - "integrity": "sha512-Qm8k8ojNQIMx7S+Zp8u/uHOx7Qazv3Yv4q68MiWWWOJhiwG5W3x7iqmRtJo8xxrciZUY4vRxUTJCKuRnF28ZZw==", + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.3.tgz", + "integrity": "sha512-kQ/dHIzuLrS6Je9+uv81ueZomEwH0qVYstcAQ4/Z93K8zeko9gtAbttJWzoC5ukqXY1PpoouV3+VSOqEAFt5wg==", "dev": true }, "xml-name-validator": { From a258429d6ea2927fc2a9b27c8d843e61a56a3869 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 2 Aug 2021 19:00:17 -0300 Subject: [PATCH 102/144] promise wrapper update to support runtimes where Promise.prototype.then is not writable --- src/utils/promise/wrapper.ts | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/utils/promise/wrapper.ts b/src/utils/promise/wrapper.ts index 266c083f..42844d51 100644 --- a/src/utils/promise/wrapper.ts +++ b/src/utils/promise/wrapper.ts @@ -37,16 +37,18 @@ export default function promiseWrapper(customPromise: Promise, defaultOnRe const originalThen = newPromise.then; - newPromise.then = function (onfulfilled, onrejected) { - const result: Promise = originalThen.call(newPromise, onfulfilled, onrejected); - if (typeof onfulfilled === 'function') hasOnFulfilled = true; - if (typeof onrejected === 'function') { - hasOnRejected = true; - return result; - } else { - return chain(result); + Object.defineProperty(newPromise, 'then', { + value: function (onfulfilled: any, onrejected: any) { + const result: Promise = originalThen.call(newPromise, onfulfilled, onrejected); + if (typeof onfulfilled === 'function') hasOnFulfilled = true; + if (typeof onrejected === 'function') { + hasOnRejected = true; + return result; + } else { + return chain(result); + } } - }; + }); return newPromise; } From c0d8aa91ea0d46479d77266a231478b0e53ed96a Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Fri, 6 Aug 2021 16:46:51 -0300 Subject: [PATCH 103/144] fixed log message for HTTP errors --- src/logger/messages/error.ts | 2 +- src/sdkFactory/types.ts | 3 ++- src/services/splitHttpClient.ts | 11 ++++++----- src/services/types.ts | 6 +++--- 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/logger/messages/error.ts b/src/logger/messages/error.ts index 3df90b60..9a6de5c3 100644 --- a/src/logger/messages/error.ts +++ b/src/logger/messages/error.ts @@ -12,7 +12,7 @@ export const codesError: [number, string][] = [ // synchronizer [c.ERROR_SYNC_OFFLINE_LOADING, c.LOG_PREFIX_SYNC_OFFLINE + 'There was an issue loading the mock Splits data, no changes will be applied to the current cache. %s'], [c.ERROR_STREAMING_SSE, c.LOG_PREFIX_SYNC_STREAMING + 'Failed to connect or error on streaming connection, with error message: %s'], - [c.ERROR_STREAMING_AUTH, c.LOG_PREFIX_SYNC_STREAMING + 'Failed to authenticate for streaming. Error: "%s".'], + [c.ERROR_STREAMING_AUTH, c.LOG_PREFIX_SYNC_STREAMING + 'Failed to authenticate for streaming. Error: %s.'], [c.ERROR_HTTP, ' Response status is not OK. Status: %s. URL: %s. Message: %s'], // client status [c.ERROR_CLIENT_LISTENER, 'A listener was added for %s on the SDK, which has already fired and won\'t be emitted again. The callback won\'t be executed.'], diff --git a/src/sdkFactory/types.ts b/src/sdkFactory/types.ts index e63300cf..d96d5b9c 100644 --- a/src/sdkFactory/types.ts +++ b/src/sdkFactory/types.ts @@ -11,7 +11,8 @@ import { SplitIO, ISettings, IEventEmitter } from '../types'; import { ISettingsInternal } from '../utils/settingsValidation/types'; /** - * Environment related dependencies + * Environment related dependencies. + * These getters are called only once per factory instantiation. */ export interface IPlatform { getOptions?: () => object diff --git a/src/services/splitHttpClient.ts b/src/services/splitHttpClient.ts index 9788acf9..8f413f5e 100644 --- a/src/services/splitHttpClient.ts +++ b/src/services/splitHttpClient.ts @@ -43,8 +43,8 @@ export function splitHttpClientFactory(settings: Pick { - if (!response.ok) { // eslint-disable-next-line no-throw-literal - throw { response }; + if (!response.ok) { + return response.text().then(message => Promise.reject({ response, message })); } return response; }) @@ -56,18 +56,19 @@ export function splitHttpClientFactory(settings: Pick Promise, + json: () => Promise, // Used to parse OK response body. Promise rejects if body cannot be parsed + text: () => Promise, // Used to read Not OK response body. Promise never rejects /** Other available properties when using Unfetch */ - // statusText: string, + // statusText: string, // `undefined` in Web fetch if using HTTP/2: it doesn't have reason phrases anymore. `node-fetch` overwrites it according to the status code // url: string, - // text: () => Promise, // blob: () => Promise, // clone: () => IResponse, // headers: { From ece665784a226584227d760719e7d809566651da Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 9 Aug 2021 14:11:03 -0300 Subject: [PATCH 104/144] chore: comments polishing --- src/sdkFactory/types.ts | 2 +- src/services/types.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sdkFactory/types.ts b/src/sdkFactory/types.ts index d96d5b9c..eb834930 100644 --- a/src/sdkFactory/types.ts +++ b/src/sdkFactory/types.ts @@ -12,7 +12,7 @@ import { ISettingsInternal } from '../utils/settingsValidation/types'; /** * Environment related dependencies. - * These getters are called only once per factory instantiation. + * These getters are called a fixed number of times per factory instantiation. */ export interface IPlatform { getOptions?: () => object diff --git a/src/services/types.ts b/src/services/types.ts index 49cd6460..0521b795 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -11,7 +11,7 @@ export type IResponse = { text: () => Promise, // Used to read Not OK response body. Promise never rejects /** Other available properties when using Unfetch */ - // statusText: string, // `undefined` in Web fetch if using HTTP/2: it doesn't have reason phrases anymore. `node-fetch` overwrites it according to the status code + // statusText: string, // `undefined` in Web fetch since HTTP/2 doesn't have reason phrases anymore. `node-fetch` overwrites it depending on the status code // url: string, // blob: () => Promise, // clone: () => IResponse, From 35a2ed94b5d5fbfc2dd694a9530a5bf24379435f Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 11 Aug 2021 15:02:48 -0300 Subject: [PATCH 105/144] remove TT values from storage when reaching to 0 --- src/storages/inRedis/SplitsCacheInRedis.ts | 4 +++- src/storages/pluggable/SplitsCachePluggable.ts | 4 +++- src/storages/pluggable/inMemoryWrapper.ts | 10 ++++++---- src/storages/types.ts | 8 ++++---- 4 files changed, 16 insertions(+), 10 deletions(-) diff --git a/src/storages/inRedis/SplitsCacheInRedis.ts b/src/storages/inRedis/SplitsCacheInRedis.ts index dced1ae4..92e5c09c 100644 --- a/src/storages/inRedis/SplitsCacheInRedis.ts +++ b/src/storages/inRedis/SplitsCacheInRedis.ts @@ -47,7 +47,9 @@ export default class SplitsCacheInRedis extends AbstractSplitsCacheAsync { private _decrementCounts(split: ISplit) { if (split.trafficTypeName) { const ttKey = this.keys.buildTrafficTypeKey(split.trafficTypeName); - return this.redis.decr(ttKey); + return this.redis.decr(ttKey).then(count => { + if (count === 0) return this.redis.del(ttKey); + }); } } diff --git a/src/storages/pluggable/SplitsCachePluggable.ts b/src/storages/pluggable/SplitsCachePluggable.ts index 97be1e6b..e4e29ce4 100644 --- a/src/storages/pluggable/SplitsCachePluggable.ts +++ b/src/storages/pluggable/SplitsCachePluggable.ts @@ -31,7 +31,9 @@ export class SplitsCachePluggable extends AbstractSplitsCacheAsync { private _decrementCounts(split: ISplit) { if (split.trafficTypeName) { const ttKey = this.keys.buildTrafficTypeKey(split.trafficTypeName); - return this.wrapper.decr(ttKey); + return this.wrapper.decr(ttKey).then(count => { + if (count === 0) return this.wrapper.del(ttKey); + }); } } diff --git a/src/storages/pluggable/inMemoryWrapper.ts b/src/storages/pluggable/inMemoryWrapper.ts index 47a6eb80..939d87b8 100644 --- a/src/storages/pluggable/inMemoryWrapper.ts +++ b/src/storages/pluggable/inMemoryWrapper.ts @@ -42,22 +42,24 @@ export function inMemoryWrapperFactory(): ICustomStorageWrapper & { _cache: Reco incr(key: string) { if (key in _cache) { const count = toNumber(_cache[key]) + 1; - if (isNaN(count)) return Promise.resolve(false); + if (isNaN(count)) return Promise.reject('Given key is not a number'); _cache[key] = count + ''; + return Promise.resolve(count); } else { _cache[key] = '1'; + return Promise.resolve(1); } - return Promise.resolve(true); }, decr(key: string) { if (key in _cache) { const count = toNumber(_cache[key]) - 1; - if (isNaN(count)) return Promise.resolve(false); + if (isNaN(count)) return Promise.reject('Given key is not a number'); _cache[key] = count + ''; + return Promise.resolve(count); } else { _cache[key] = '-1'; + return Promise.resolve(-1); } - return Promise.resolve(true); }, getMany(keys: string[]) { return Promise.resolve(keys.map(key => _cache[key] ? _cache[key] as string : null)); diff --git a/src/storages/types.ts b/src/storages/types.ts index 31b71d5f..a732b446 100644 --- a/src/storages/types.ts +++ b/src/storages/types.ts @@ -83,19 +83,19 @@ export interface ICustomStorageWrapper { * * @function incr * @param {string} key Key to increment - * @returns {Promise} A promise that resolves if the operation success. The promise rejects if the operation fails, + * @returns {Promise} A promise that resolves with the value of key after the increment. The promise rejects if the operation fails, * for example, if there is a connection error or the key contains a string that can not be represented as integer. */ - incr: (key: string) => Promise + incr: (key: string) => Promise /** * Decrements in 1 the given `key` value or set it in -1 if the value doesn't exist. * * @function decr * @param {string} key Key to decrement - * @returns {Promise} A promise that resolves if the operation success. The promise rejects if the operation fails, + * @returns {Promise} A promise that resolves with the value of key after the decrement. The promise rejects if the operation fails, * for example, if there is a connection error or the key contains a string that can not be represented as integer. */ - decr: (key: string) => Promise + decr: (key: string) => Promise /** Queue operations */ From 845d0110e268a2b88a0d6cb8a8b88f32aaded9d2 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 11 Aug 2021 15:49:05 -0300 Subject: [PATCH 106/144] added UTs asserts --- .../__tests__/SplitsCacheInRedis.spec.ts | 59 +++++++++++++------ .../__tests__/SplitsCachePluggable.spec.ts | 7 ++- .../pluggable/__tests__/wrapper.mock.ts | 10 ++-- 3 files changed, 52 insertions(+), 24 deletions(-) diff --git a/src/storages/inRedis/__tests__/SplitsCacheInRedis.spec.ts b/src/storages/inRedis/__tests__/SplitsCacheInRedis.spec.ts index b8dbb448..6b743ed6 100644 --- a/src/storages/inRedis/__tests__/SplitsCacheInRedis.spec.ts +++ b/src/storages/inRedis/__tests__/SplitsCacheInRedis.spec.ts @@ -58,32 +58,53 @@ describe('SPLITS CACHE REDIS', () => { const prefix = 'splits_cache_ut'; const connection = new Redis(); // @ts-expect-error - const keys = new KeyBuilderSS(prefix); - const cache = new SplitsCacheInRedis(loggerMock, keys, connection); + const keysBuilder = new KeyBuilderSS(prefix); + const cache = new SplitsCacheInRedis(loggerMock, keysBuilder, connection); - const testTTName = 'tt_test_name'; - const testTTNameNoCount = 'tt_test_name_2'; - const testTTNameInvalid = 'tt_test_name_3'; - const ttKey = keys.buildTrafficTypeKey(testTTName); - const ttKeyNoCount = keys.buildTrafficTypeKey(testTTNameNoCount); - const ttKeyInvalid = keys.buildTrafficTypeKey(testTTNameInvalid); + await cache.addSplits([ + ['split1', splitWithUserTT], + ['split2', splitWithAccountTT], + ['split3', splitWithUserTT], + ['malformed', '{}'] + ]); + await cache.addSplit('split4', splitWithUserTT); + await cache.addSplit('split4', splitWithUserTT); // trying to add the same definition for an already added split will not have effect - await cache.clear(); + expect(await cache.trafficTypeExists('user_tt')).toBe(true); + expect(await cache.trafficTypeExists('account_tt')).toBe(true); + expect(await cache.trafficTypeExists('not_existent_tt')).toBe(false); - await connection.set(ttKey, 3); - await connection.set(ttKeyNoCount, 0); - await connection.set(ttKeyInvalid, 'NaN'); + await cache.removeSplit('split4'); - expect(await cache.trafficTypeExists(testTTName)).toBe(true); - expect(await cache.trafficTypeExists(testTTNameNoCount)).toBe(false); - expect(await cache.trafficTypeExists(ttKeyInvalid)).toBe(false); - expect(await cache.trafficTypeExists('not_existent_tt')).toBe(false); + expect(await cache.trafficTypeExists('user_tt')).toBe(true); + expect(await cache.trafficTypeExists('account_tt')).toBe(true); + + expect(await connection.get(keysBuilder.buildTrafficTypeKey('account_tt'))).toBe('1'); - await connection.del(ttKey); - await connection.del(ttKeyNoCount); - await connection.del(ttKeyInvalid); + await cache.removeSplits(['split3', 'split2']); // it'll invoke a loop of removeSplit + expect(await cache.trafficTypeExists('user_tt')).toBe(true); + expect(await cache.trafficTypeExists('account_tt')).toBe(false); + + expect(await connection.get(keysBuilder.buildTrafficTypeKey('account_tt'))).toBe(null); // TT entry should be removed in the wrapper + + await cache.removeSplit('split1'); + + expect(await cache.trafficTypeExists('user_tt')).toBe(false); + expect(await cache.trafficTypeExists('account_tt')).toBe(false); + + await cache.addSplit('split1', splitWithUserTT); + expect(await cache.trafficTypeExists('user_tt')).toBe(true); + + await cache.addSplit('split1', splitWithAccountTT); + expect(await cache.trafficTypeExists('account_tt')).toBe(true); + expect(await cache.trafficTypeExists('user_tt')).toBe(false); + + await connection.del(keysBuilder.buildTrafficTypeKey('account_tt')); + await connection.del(keysBuilder.buildSplitKey('malformed')); + await connection.del(keysBuilder.buildSplitKey('split1')); await connection.quit(); + }); test('killLocally', async () => { diff --git a/src/storages/pluggable/__tests__/SplitsCachePluggable.spec.ts b/src/storages/pluggable/__tests__/SplitsCachePluggable.spec.ts index 1d275147..99069585 100644 --- a/src/storages/pluggable/__tests__/SplitsCachePluggable.spec.ts +++ b/src/storages/pluggable/__tests__/SplitsCachePluggable.spec.ts @@ -78,7 +78,8 @@ describe('SPLITS CACHE PLUGGABLE', () => { }); test('trafficTypeExists', async () => { - const cache = new SplitsCachePluggable(loggerMock, keysBuilder, wrapperMockFactory()); + const wrapper = wrapperMockFactory(); + const cache = new SplitsCachePluggable(loggerMock, keysBuilder, wrapper); await cache.addSplits([ ['split1', splitWithUserTT], @@ -98,11 +99,15 @@ describe('SPLITS CACHE PLUGGABLE', () => { expect(await cache.trafficTypeExists('user_tt')).toBe(true); expect(await cache.trafficTypeExists('account_tt')).toBe(true); + expect(await wrapper.get(keysBuilder.buildTrafficTypeKey('account_tt'))).toBe('1'); + await cache.removeSplits(['split3', 'split2']); // it'll invoke a loop of removeSplit expect(await cache.trafficTypeExists('user_tt')).toBe(true); expect(await cache.trafficTypeExists('account_tt')).toBe(false); + expect(await wrapper.get(keysBuilder.buildTrafficTypeKey('account_tt'))).toBe(null); // TT entry should be removed in the wrapper + await cache.removeSplit('split1'); expect(await cache.trafficTypeExists('user_tt')).toBe(false); diff --git a/src/storages/pluggable/__tests__/wrapper.mock.ts b/src/storages/pluggable/__tests__/wrapper.mock.ts index f90c9977..23eac758 100644 --- a/src/storages/pluggable/__tests__/wrapper.mock.ts +++ b/src/storages/pluggable/__tests__/wrapper.mock.ts @@ -36,22 +36,24 @@ export function wrapperMockFactory() { incr: jest.fn((key: string) => { if (key in _cache) { const count = toNumber(_cache[key]) + 1; - if (isNaN(count)) return Promise.resolve(false); + if (isNaN(count)) return Promise.reject('Given key is not a number'); _cache[key] = count + ''; + return Promise.resolve(count); } else { _cache[key] = '1'; + return Promise.resolve(1); } - return Promise.resolve(true); }), decr: jest.fn((key: string) => { if (key in _cache) { const count = toNumber(_cache[key]) - 1; - if (isNaN(count)) return Promise.resolve(false); + if (isNaN(count)) return Promise.reject('Given key is not a number'); _cache[key] = count + ''; + return Promise.resolve(count); } else { _cache[key] = '-1'; + return Promise.resolve(-1); } - return Promise.resolve(true); }), getMany: jest.fn((keys: string[]) => { return Promise.resolve(keys.map(key => _cache[key] ? _cache[key] as string : null)); From cbd2cb2243d3573c15f4d1e135adf72c4e4401e3 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 11 Aug 2021 16:12:20 -0300 Subject: [PATCH 107/144] fixed UT --- src/storages/inRedis/__tests__/SplitsCacheInRedis.spec.ts | 7 +++++-- src/utils/promise/wrapper.ts | 1 + 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/storages/inRedis/__tests__/SplitsCacheInRedis.spec.ts b/src/storages/inRedis/__tests__/SplitsCacheInRedis.spec.ts index 6b743ed6..4bdf9809 100644 --- a/src/storages/inRedis/__tests__/SplitsCacheInRedis.spec.ts +++ b/src/storages/inRedis/__tests__/SplitsCacheInRedis.spec.ts @@ -9,8 +9,8 @@ describe('SPLITS CACHE REDIS', () => { test('add/remove/get splits & set/get change number', async () => { const connection = new Redis(); // @ts-expect-error - const keys = new KeyBuilderSS(); - const cache = new SplitsCacheInRedis(loggerMock, keys, connection); + const keysBuilder = new KeyBuilderSS(); + const cache = new SplitsCacheInRedis(loggerMock, keysBuilder, connection); await cache.clear(); @@ -51,6 +51,9 @@ describe('SPLITS CACHE REDIS', () => { expect(splits['lol1'] === null).toBe(true); expect(splits['lol2'] === splitWithAccountTT).toBe(true); + await connection.del(keysBuilder.buildTrafficTypeKey('account_tt')); + await connection.del(keysBuilder.buildSplitKey('lol2')); + await connection.del(keysBuilder.buildSplitsTillKey()); await connection.quit(); }); diff --git a/src/utils/promise/wrapper.ts b/src/utils/promise/wrapper.ts index 42844d51..7c7779ec 100644 --- a/src/utils/promise/wrapper.ts +++ b/src/utils/promise/wrapper.ts @@ -37,6 +37,7 @@ export default function promiseWrapper(customPromise: Promise, defaultOnRe const originalThen = newPromise.then; + // Using `defineProperty` in case Promise.prototype.then property is not writable Object.defineProperty(newPromise, 'then', { value: function (onfulfilled: any, onrejected: any) { const result: Promise = originalThen.call(newPromise, onfulfilled, onrejected); From e72930126f31e0f3df1f0ef9df82b380544f1965 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 11 Aug 2021 18:02:35 -0300 Subject: [PATCH 108/144] removed unused getByPrefix method --- src/storages/pluggable/__tests__/wrapper.mock.ts | 4 ---- src/storages/pluggable/__tests__/wrapperAdapter.spec.ts | 2 -- src/storages/pluggable/inMemoryWrapper.ts | 3 --- src/storages/pluggable/wrapperAdapter.ts | 1 - src/storages/types.ts | 9 --------- 5 files changed, 19 deletions(-) diff --git a/src/storages/pluggable/__tests__/wrapper.mock.ts b/src/storages/pluggable/__tests__/wrapper.mock.ts index 23eac758..da4b2122 100644 --- a/src/storages/pluggable/__tests__/wrapper.mock.ts +++ b/src/storages/pluggable/__tests__/wrapper.mock.ts @@ -30,9 +30,6 @@ export function wrapperMockFactory() { getKeysByPrefix: jest.fn((prefix: string) => { return Promise.resolve(Object.keys(_cache).filter(key => startsWith(key, prefix))); }), - getByPrefix: jest.fn((prefix: string) => { - return Promise.resolve(Object.keys(_cache).filter(key => startsWith(key, prefix)).map(key => _cache[key] as string)); - }), incr: jest.fn((key: string) => { if (key in _cache) { const count = toNumber(_cache[key]) + 1; @@ -122,7 +119,6 @@ export function wrapperMockFactory() { this.connect.mockClear(); this.close.mockClear(); this.getAndSet.mockClear(); - this.getByPrefix.mockClear(); this.pushItems.mockClear(); this.popItems.mockClear(); this.getItemsCount.mockClear(); diff --git a/src/storages/pluggable/__tests__/wrapperAdapter.spec.ts b/src/storages/pluggable/__tests__/wrapperAdapter.spec.ts index 1dbf3ff9..dd081b6f 100644 --- a/src/storages/pluggable/__tests__/wrapperAdapter.spec.ts +++ b/src/storages/pluggable/__tests__/wrapperAdapter.spec.ts @@ -30,7 +30,6 @@ export const wrapperWithIssues = { connect: rejectedPromise, close: invalidThenable, getAndSet: 'invalid value', - getByPrefix: throwsException, pushItems: rejectedPromise, popItems: invalidThenable, getItemsCount: 'invalid value', @@ -51,7 +50,6 @@ const VALID_METHOD_CALLS = { 'connect': [], 'close': [], 'getAndSet': ['some_key', 'some_value'], - 'getByPrefix': ['some_prefix'], 'pushItems': ['some_key_list', ['item1', 'item2']], 'popItems': ['some_key_list'], 'getItemsCount': ['some_key_list'], diff --git a/src/storages/pluggable/inMemoryWrapper.ts b/src/storages/pluggable/inMemoryWrapper.ts index 939d87b8..efb2c6db 100644 --- a/src/storages/pluggable/inMemoryWrapper.ts +++ b/src/storages/pluggable/inMemoryWrapper.ts @@ -36,9 +36,6 @@ export function inMemoryWrapperFactory(): ICustomStorageWrapper & { _cache: Reco getKeysByPrefix(prefix: string) { return Promise.resolve(Object.keys(_cache).filter(key => startsWith(key, prefix))); }, - getByPrefix(prefix: string) { - return Promise.resolve(Object.keys(_cache).filter(key => startsWith(key, prefix)).map(key => _cache[key] as string)); - }, incr(key: string) { if (key in _cache) { const count = toNumber(_cache[key]) + 1; diff --git a/src/storages/pluggable/wrapperAdapter.ts b/src/storages/pluggable/wrapperAdapter.ts index e398d4f6..0b93f494 100644 --- a/src/storages/pluggable/wrapperAdapter.ts +++ b/src/storages/pluggable/wrapperAdapter.ts @@ -8,7 +8,6 @@ export const METHODS_TO_PROMISE_WRAP: string[] = [ 'getAndSet', 'del', 'getKeysByPrefix', - 'getByPrefix', 'incr', 'decr', 'getMany', diff --git a/src/storages/types.ts b/src/storages/types.ts index a732b446..22ca98a9 100644 --- a/src/storages/types.ts +++ b/src/storages/types.ts @@ -57,15 +57,6 @@ export interface ICustomStorageWrapper { * The promise rejects if the operation fails. */ getKeysByPrefix: (prefix: string) => Promise - /** - * Returns all values which keys match the given prefix. - * - * @function getByPrefix - * @param {string} prefix String prefix to match - * @returns {Promise} A promise that resolves with the list of values which keys match the given `prefix`. - * The promise rejects if the operation fails. - */ - getByPrefix: (prefix: string) => Promise /** * Returns the values of all given `keys`. * From 92cba9dae7b3285f5073168c4414f5453b53f5b2 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 11 Aug 2021 18:05:25 -0300 Subject: [PATCH 109/144] prepare rc --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 250ea6a4..5182ab54 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "0.1.1-canary.12", + "version": "0.1.1-canary.13", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -7024,9 +7024,9 @@ "dev": true }, "path-parse": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", - "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, "path-to-regexp": { diff --git a/package.json b/package.json index 5d60ca1e..ac276f38 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "0.1.1-canary.12", + "version": "0.1.1-canary.13", "description": "Split Javascript SDK common components", "main": "cjs/index.js", "module": "esm/index.js", From 3d60d407e095bd54b93377ba5cae353225d0bbde Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 1 Sep 2021 17:47:28 -0300 Subject: [PATCH 110/144] migrations of localhost mode updates --- package-lock.json | 2 +- package.json | 2 +- .../__tests__/index.asyncCache.spec.ts | 1 + .../__tests__/EventsCacheInRedis.spec.ts | 7 +- .../splitsParser/splitsParserFromFile.ts | 212 +++++++++--------- .../splitsParser/splitsParserFromSettings.ts | 86 +++---- .../offline/syncTasks/fromObjectSyncTask.ts | 7 +- src/sync/syncManagerOffline.ts | 6 +- 8 files changed, 169 insertions(+), 154 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5182ab54..371af40b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "0.1.1-canary.13", + "version": "0.1.1-canary.14", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index ac276f38..9c6308ff 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "0.1.1-canary.13", + "version": "0.1.1-canary.14", "description": "Split Javascript SDK common components", "main": "cjs/index.js", "module": "esm/index.js", diff --git a/src/sdkManager/__tests__/index.asyncCache.spec.ts b/src/sdkManager/__tests__/index.asyncCache.spec.ts index a6433ec8..9829f9e2 100644 --- a/src/sdkManager/__tests__/index.asyncCache.spec.ts +++ b/src/sdkManager/__tests__/index.asyncCache.spec.ts @@ -69,6 +69,7 @@ describe('MANAGER API', () => { /** Teardown */ + await cache.removeSplit(splitObject.name); await connection.quit(); }); diff --git a/src/storages/inRedis/__tests__/EventsCacheInRedis.spec.ts b/src/storages/inRedis/__tests__/EventsCacheInRedis.spec.ts index e78b8ae9..2ed55687 100644 --- a/src/storages/inRedis/__tests__/EventsCacheInRedis.spec.ts +++ b/src/storages/inRedis/__tests__/EventsCacheInRedis.spec.ts @@ -5,6 +5,7 @@ import { fakeMetadata, fakeEvent1, fakeEvent1stored, fakeEvent2, fakeEvent2store const prefix = 'events_cache_ut'; const eventsKey = `${prefix}.events`; +const nonListKey = 'non-list-key'; test('EVENTS CACHE IN REDIS / `track`, `count`, `popNWithMetadata` and `drop` methods', async () => { const connection = new Redis(); @@ -18,9 +19,9 @@ test('EVENTS CACHE IN REDIS / `track`, `count`, `popNWithMetadata` and `drop` me const cache = new EventsCacheInRedis(loggerMock, eventsKey, connection, fakeMetadata); // I'll use a "bad" instance so I can force an issue with the rpush command. I'll store an integer and will make the cache try to use rpush there. - await connection.set('non-list-key', 10); + await connection.set(nonListKey, 10); - const faultyCache = new EventsCacheInRedis(loggerMock, 'non-list-key', connection, fakeMetadata); + const faultyCache = new EventsCacheInRedis(loggerMock, nonListKey, connection, fakeMetadata); expect(await cache.track(fakeEvent1)).toBe(true); // If the queueing operation was successful, it should resolve the returned promise with "true" expect(await cache.track(fakeEvent2)).toBe(true); // If the queueing operation was successful, it should resolve the returned promise with "true" @@ -52,6 +53,6 @@ test('EVENTS CACHE IN REDIS / `track`, `count`, `popNWithMetadata` and `drop` me expect(await cache.count()).toBe(0); // storage should be empty after droping it // Clean up then end. - await connection.del(eventsKey); + await connection.del(eventsKey, nonListKey); await connection.quit(); }); diff --git a/src/sync/offline/splitsParser/splitsParserFromFile.ts b/src/sync/offline/splitsParser/splitsParserFromFile.ts index b56052c6..62de09f4 100644 --- a/src/sync/offline/splitsParser/splitsParserFromFile.ts +++ b/src/sync/offline/splitsParser/splitsParserFromFile.ts @@ -16,8 +16,6 @@ type IYamlSplitEntry = Record const DEFAULT_FILENAME = '.split'; -let previousMock = 'NO_MOCK_LOADED'; - function configFilesPath(configFilePath?: SplitIO.MockedFeaturesFilePath): SplitIO.MockedFeaturesFilePath { if (configFilePath === DEFAULT_FILENAME || !isString(configFilePath)) { let root = process.env.HOME; @@ -39,96 +37,6 @@ function configFilesPath(configFilePath?: SplitIO.MockedFeaturesFilePath): Split return configFilePath as SplitIO.MockedFeaturesFilePath; } -// Parse `.split` configuration file and return a map of "Split Objects" -function readSplitConfigFile(log: ILogger, filePath: SplitIO.MockedFeaturesFilePath): false | Record { - const SPLIT_POSITION = 0; - const TREATMENT_POSITION = 1; - let data; - - try { - data = fs.readFileSync(filePath, 'utf-8'); - } catch (e) { - log.error(e.message); - - return {}; - } - - if (data === previousMock) return false; - previousMock = data; - - const splitObjects = data.split(/\r?\n/).reduce((accum, line, index) => { - let tuple: string | string[] = line.trim(); - - if (tuple === '' || tuple.charAt(0) === '#') { - log.debug(logPrefix + `Ignoring empty line or comment at #${index}`); - } else { - tuple = tuple.split(/\s+/); - - if (tuple.length !== 2) { - log.debug(logPrefix + `Ignoring line since it does not have exactly two columns #${index}`); - } else { - const splitName = tuple[SPLIT_POSITION]; - const condition = parseCondition({ treatment: tuple[TREATMENT_POSITION] }); - accum[splitName] = { conditions: [condition], configurations: {}, trafficTypeName: 'localhost' }; - } - } - - return accum; - }, {} as Record); - - return splitObjects; -} - -// Parse `.yml` or `.yaml` configuration files and return a map of "Split Objects" -function readYAMLConfigFile(log: ILogger, filePath: SplitIO.MockedFeaturesFilePath): false | Record { - let data = ''; - let yamldoc = null; - - try { - data = fs.readFileSync(filePath, 'utf8'); - - if (data === previousMock) return false; - previousMock = data; - - yamldoc = yaml.safeLoad(data); - } catch (e) { - log.error(e); - - return {}; - } - - // Each entry will be mapped to a condition, but we'll also keep the configurations map. - const mocksData = (yamldoc as IYamlSplitEntry[]).reduce((accum, splitEntry) => { - const splitName = Object.keys(splitEntry)[0]; - - if (!splitName || !isString(splitEntry[splitName].treatment)) - log.error(logPrefix + 'Ignoring entry on YAML since the format is incorrect.'); - - const mockData = splitEntry[splitName]; - - // "Template" for each split accumulated data - if (!accum[splitName]) { - accum[splitName] = { - configurations: {}, conditions: [], treatments: [], trafficTypeName: 'localhost' - }; - } - - // Assign the config if there is one on the mock - if (mockData.config) accum[splitName].configurations[mockData.treatment] = mockData.config; - // Parse the condition from the entry. - const condition = parseCondition(mockData); - accum[splitName].conditions[condition.conditionType === 'ROLLOUT' ? 'push' : 'unshift'](condition); - // Also keep track of the treatments, will be useful for manager functionality. - accum[splitName].treatments.push(mockData.treatment); - - return accum; - }, {} as Record & { treatments: string[] }>); - - arrangeConditions(mocksData); - - return mocksData; -} - // This function is not pure nor meant to be. Here we apply modifications to cover // for behaviour that's ensured by the BE. function arrangeConditions(mocksData: Record & { treatments: string[] }>) { @@ -160,18 +68,114 @@ function arrangeConditions(mocksData: Record & { }); } -// Load the content of a configuration file into an Object -export default function splitsParserFromFile({ features, log }: { features?: SplitIO.MockedFeaturesFilePath, log: ILogger }): false | Record { - const filePath = configFilesPath(features); - let mockData: false | Record; - - // If we have a filePath, it means the extension is correct, choose the parser. - if (endsWith(filePath, '.split')) { - log.warn(logPrefix + '.split mocks will be deprecated soon in favor of YAML files, which provide more targeting power. Take a look in our documentation.'); - mockData = readSplitConfigFile(log, filePath); - } else { - mockData = readYAMLConfigFile(log, filePath); +export function splitsParserFromFileFactory() { + + let previousMock = 'NO_MOCK_LOADED'; + + // Parse `.split` configuration file and return a map of "Split Objects" + function readSplitConfigFile(log: ILogger, filePath: SplitIO.MockedFeaturesFilePath): false | Record { + const SPLIT_POSITION = 0; + const TREATMENT_POSITION = 1; + let data; + + try { + data = fs.readFileSync(filePath, 'utf-8'); + } catch (e) { + log.error(e.message); + + return {}; + } + + if (data === previousMock) return false; + previousMock = data; + + const splitObjects = data.split(/\r?\n/).reduce((accum, line, index) => { + let tuple: string | string[] = line.trim(); + + if (tuple === '' || tuple.charAt(0) === '#') { + log.debug(logPrefix + `Ignoring empty line or comment at #${index}`); + } else { + tuple = tuple.split(/\s+/); + + if (tuple.length !== 2) { + log.debug(logPrefix + `Ignoring line since it does not have exactly two columns #${index}`); + } else { + const splitName = tuple[SPLIT_POSITION]; + const condition = parseCondition({ treatment: tuple[TREATMENT_POSITION] }); + accum[splitName] = { conditions: [condition], configurations: {}, trafficTypeName: 'localhost' }; + } + } + + return accum; + }, {} as Record); + + return splitObjects; + } + + // Parse `.yml` or `.yaml` configuration files and return a map of "Split Objects" + function readYAMLConfigFile(log: ILogger, filePath: SplitIO.MockedFeaturesFilePath): false | Record { + let data = ''; + let yamldoc = null; + + try { + data = fs.readFileSync(filePath, 'utf8'); + + if (data === previousMock) return false; + previousMock = data; + + yamldoc = yaml.safeLoad(data); + } catch (e) { + log.error(e); + + return {}; + } + + // Each entry will be mapped to a condition, but we'll also keep the configurations map. + const mocksData = (yamldoc as IYamlSplitEntry[]).reduce((accum, splitEntry) => { + const splitName = Object.keys(splitEntry)[0]; + + if (!splitName || !isString(splitEntry[splitName].treatment)) + log.error(logPrefix + 'Ignoring entry on YAML since the format is incorrect.'); + + const mockData = splitEntry[splitName]; + + // "Template" for each split accumulated data + if (!accum[splitName]) { + accum[splitName] = { + configurations: {}, conditions: [], treatments: [], trafficTypeName: 'localhost' + }; + } + + // Assign the config if there is one on the mock + if (mockData.config) accum[splitName].configurations[mockData.treatment] = mockData.config; + // Parse the condition from the entry. + const condition = parseCondition(mockData); + accum[splitName].conditions[condition.conditionType === 'ROLLOUT' ? 'push' : 'unshift'](condition); + // Also keep track of the treatments, will be useful for manager functionality. + accum[splitName].treatments.push(mockData.treatment); + + return accum; + }, {} as Record & { treatments: string[] }>); + + arrangeConditions(mocksData); + + return mocksData; } - return mockData; + // Load the content of a configuration file into an Object + return function splitsParserFromFile({ features, log }: { features?: SplitIO.MockedFeaturesFilePath, log: ILogger }): false | Record { + const filePath = configFilesPath(features); + let mockData: false | Record; + + // If we have a filePath, it means the extension is correct, choose the parser. + if (endsWith(filePath, '.split')) { + log.warn(logPrefix + '.split mocks will be deprecated soon in favor of YAML files, which provide more targeting power. Take a look in our documentation.'); + mockData = readSplitConfigFile(log, filePath); + } else { + mockData = readYAMLConfigFile(log, filePath); + } + + return mockData; + }; + } diff --git a/src/sync/offline/splitsParser/splitsParserFromSettings.ts b/src/sync/offline/splitsParser/splitsParserFromSettings.ts index 14b9a059..f9cf9175 100644 --- a/src/sync/offline/splitsParser/splitsParserFromSettings.ts +++ b/src/sync/offline/splitsParser/splitsParserFromSettings.ts @@ -3,8 +3,6 @@ import { ISettings, SplitIO } from '../../../types'; import { isObject, forOwn } from '../../../utils/lang'; import parseCondition from './parseCondition'; -let previousMock: SplitIO.MockedFeaturesMap = { 'emptyMock': '1' }; - function hasTreatmentChanged(prev: string | SplitIO.TreatmentWithConfig, curr: string | SplitIO.TreatmentWithConfig) { if (typeof prev !== typeof curr) return true; @@ -15,54 +13,60 @@ function hasTreatmentChanged(prev: string | SplitIO.TreatmentWithConfig, curr: s } } -function mockUpdated(currentData: SplitIO.MockedFeaturesMap) { - const names = Object.keys(currentData); +export function splitsParserFromSettingsFactory() { - // Different amount of items - if (names.length !== Object.keys(previousMock).length) { - previousMock = currentData; - return true; - } + let previousMock: SplitIO.MockedFeaturesMap = { 'emptyMock': '1' }; - return names.some(name => { - const newSplit = !previousMock[name]; - const newTreatment = hasTreatmentChanged(previousMock[name], currentData[name]); - const changed = newSplit || newTreatment; + function mockUpdated(currentData: SplitIO.MockedFeaturesMap) { + const names = Object.keys(currentData); - if (changed) previousMock = currentData; + // Different amount of items + if (names.length !== Object.keys(previousMock).length) { + previousMock = currentData; + return true; + } - return changed; - }); -} + return names.some(name => { + const newSplit = !previousMock[name]; + const newTreatment = hasTreatmentChanged(previousMock[name], currentData[name]); + const changed = newSplit || newTreatment; -/** - * - * @param features validated object with mocked features mapping. - */ -export default function splitsParserFromSettings(settings: ISettings): false | Record { - const features = settings.features as SplitIO.MockedFeaturesMap || {}; + if (changed) previousMock = currentData; - if (!mockUpdated(features)) return false; + return changed; + }); + } - const splitObjects: Record = {}; + /** + * + * @param settings validated object with mocked features mapping. + */ + return function splitsParserFromSettings(settings: ISettings): false | Record { + const features = settings.features as SplitIO.MockedFeaturesMap || {}; - forOwn(features, (data, splitName) => { - let treatment = data; - let config = null; + if (!mockUpdated(features)) return false; - if (isObject(data)) { - treatment = (data as SplitIO.TreatmentWithConfig).treatment; - config = (data as SplitIO.TreatmentWithConfig).config || config; - } - const configurations: Record = {}; - if (config !== null) configurations[treatment as string] = config; + const splitObjects: Record = {}; + + forOwn(features, (data, splitName) => { + let treatment = data; + let config = null; + + if (isObject(data)) { + treatment = (data as SplitIO.TreatmentWithConfig).treatment; + config = (data as SplitIO.TreatmentWithConfig).config || config; + } + const configurations: Record = {}; + if (config !== null) configurations[treatment as string] = config; + + splitObjects[splitName] = { + trafficTypeName: 'localhost', + conditions: [parseCondition({ treatment: treatment as string })], + configurations + }; + }); - splitObjects[splitName] = { - trafficTypeName: 'localhost', - conditions: [parseCondition({ treatment: treatment as string })], - configurations - }; - }); + return splitObjects; + }; - return splitObjects; } diff --git a/src/sync/offline/syncTasks/fromObjectSyncTask.ts b/src/sync/offline/syncTasks/fromObjectSyncTask.ts index 66c51db1..0ae98763 100644 --- a/src/sync/offline/syncTasks/fromObjectSyncTask.ts +++ b/src/sync/offline/syncTasks/fromObjectSyncTask.ts @@ -21,6 +21,7 @@ export function fromObjectUpdaterFactory( ): () => Promise { const log = settings.log; + let firstTime = true; return function objectUpdater() { const splits: [string, string][] = []; @@ -57,7 +58,11 @@ export function fromObjectUpdaterFactory( storage.splits.addSplits(splits) ]).then(() => { readiness.splits.emit(SDK_SPLITS_ARRIVED); - readiness.segments.emit(SDK_SEGMENTS_ARRIVED); + // Only emits SDK_SEGMENTS_ARRIVED the first time for SDK_READY + if (firstTime) { + firstTime = false; + readiness.segments.emit(SDK_SEGMENTS_ARRIVED); + } return true; }); } else { diff --git a/src/sync/syncManagerOffline.ts b/src/sync/syncManagerOffline.ts index 20a9b074..2ef909ee 100644 --- a/src/sync/syncManagerOffline.ts +++ b/src/sync/syncManagerOffline.ts @@ -16,7 +16,7 @@ function flush() { * @param splitsParser e.g., `splitsParserFromFile`, `splitsParserFromSettings`. */ export function syncManagerOfflineFactory( - splitsParser: ISplitsParser + splitsParserFactory: () => ISplitsParser ): (params: ISyncManagerFactoryParams) => ISyncManagerCS { /** @@ -29,7 +29,7 @@ export function syncManagerOfflineFactory( }: ISyncManagerFactoryParams): ISyncManagerCS { return objectAssign( - fromObjectSyncTaskFactory(splitsParser, storage, readiness, settings), + fromObjectSyncTaskFactory(splitsParserFactory(), storage, readiness, settings), { // fake flush, that resolves immediately flush, @@ -38,7 +38,7 @@ export function syncManagerOfflineFactory( shared(matchingKey: string, readinessManager: IReadinessManager): ISyncManager { return { start() { - // In LOCALHOST mode, shared clients are ready in the next event cycle than created + // In LOCALHOST mode, shared clients are ready in the next event-loop cycle than created // SDK_READY cannot be emitted directly because this will not update the readiness status setTimeout(() => { readinessManager.segments.emit(SDK_SEGMENTS_ARRIVED); // SDK_SPLITS_ARRIVED emitted by main SyncManager From 048b58ef8b70fb73bb25683616a9b9a1099f0d93 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 1 Sep 2021 18:24:21 -0300 Subject: [PATCH 111/144] updated redis test to reduce test flakiness --- src/storages/inRedis/__tests__/SplitsCacheInRedis.spec.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/storages/inRedis/__tests__/SplitsCacheInRedis.spec.ts b/src/storages/inRedis/__tests__/SplitsCacheInRedis.spec.ts index 4bdf9809..7ab8384b 100644 --- a/src/storages/inRedis/__tests__/SplitsCacheInRedis.spec.ts +++ b/src/storages/inRedis/__tests__/SplitsCacheInRedis.spec.ts @@ -4,12 +4,14 @@ import KeyBuilderSS from '../../KeyBuilderSS'; import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; import { splitWithUserTT, splitWithAccountTT } from '../../__tests__/testUtils'; +const prefix = 'splits_cache_ut'; + describe('SPLITS CACHE REDIS', () => { test('add/remove/get splits & set/get change number', async () => { const connection = new Redis(); // @ts-expect-error - const keysBuilder = new KeyBuilderSS(); + const keysBuilder = new KeyBuilderSS(prefix); const cache = new SplitsCacheInRedis(loggerMock, keysBuilder, connection); await cache.clear(); @@ -113,7 +115,7 @@ describe('SPLITS CACHE REDIS', () => { test('killLocally', async () => { const connection = new Redis(); // @ts-expect-error - const keys = new KeyBuilderSS(); + const keys = new KeyBuilderSS(prefix); const cache = new SplitsCacheInRedis(loggerMock, keys, connection); await cache.addSplit('lol1', splitWithUserTT); @@ -148,7 +150,7 @@ describe('SPLITS CACHE REDIS', () => { await cache.removeSplits(['lol1', 'lol2']); await connection.del(keys.buildTrafficTypeKey('account_tt')); await connection.del(keys.buildTrafficTypeKey('user_tt')); - expect(await connection.keys('*')).toHaveLength(0); + expect(await connection.keys(`${prefix}*`)).toHaveLength(0); await connection.quit(); }); From 2e0d0cf8e8e380c4fef6d00a77beed5b9a678bd0 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 7 Sep 2021 21:22:34 -0300 Subject: [PATCH 112/144] implementation and tests --- package-lock.json | 2 +- package.json | 2 +- .../mocks/message.STREAMING_RESET.json | 4 + ...message.V2.BOUNDED.GZIP.1457552651000.json | 4 + ...message.V2.KEYLIST.GZIP.1457552652000.json | 4 + ...sage.V2.SEGMENT_REMOVAL.1457552653000.json | 4 + .../message.V2.UNBOUNDED.1457552650000.json | 4 + src/logger/constants.ts | 1 + src/logger/messages/info.ts | 4 +- src/logger/messages/warn.ts | 4 +- src/services/splitApi.ts | 2 +- src/services/types.ts | 4 +- src/storages/types.ts | 4 +- src/sync/polling/types.ts | 3 +- .../polling/updaters/mySegmentsUpdater.ts | 38 +- .../AuthClient/__tests__/dataMocks.ts | 205 +++++++++ .../AuthClient/__tests__/index.spec.ts | 18 +- src/sync/streaming/AuthClient/types.ts | 3 + .../SSEClient/__tests__/index.spec.ts | 9 - src/sync/streaming/SSEClient/index.ts | 14 - src/sync/streaming/SSEClient/types.ts | 1 - .../SSEHandler/__tests__/index.spec.ts | 70 ++- src/sync/streaming/SSEHandler/index.ts | 17 +- src/sync/streaming/SSEHandler/types.ts | 43 +- .../UpdateWorkers/MySegmentsUpdateWorker.ts | 13 +- .../__tests__/mySegmentsV2utils.spec.ts | 53 +++ src/sync/streaming/constants.ts | 3 + src/sync/streaming/mySegmentsV2utils.ts | 77 ++++ src/sync/streaming/pushManager.ts | 179 ++++++-- src/sync/streaming/types.ts | 10 +- .../__tests__/clientSideObserver.spec.ts | 3 - .../__tests__/serverSideObserver.spec.ts | 3 - src/trackers/impressionObserver/buildKey.ts | 18 +- .../impressionObserverCS.ts | 4 +- .../impressionObserverSS.ts | 4 +- src/utils/decompress/index.ts | 429 ++++++++++++++++++ src/utils/murmur3/__tests__/murmur3.spec.ts | 62 +++ .../murmur3/__tests__/murmur3_128.spec.ts | 48 -- src/utils/murmur3/{commons.ts => common.ts} | 6 +- src/utils/murmur3/murmur3.ts | 10 +- src/utils/murmur3/murmur3_128.ts | 187 +------- src/utils/murmur3/murmur3_128_x86.ts | 189 ++++++++ src/utils/murmur3/murmur3_64.ts | 36 ++ src/utils/murmur3/utfx.ts | 202 ++++----- src/utils/settingsValidation/url.ts | 2 +- 45 files changed, 1506 insertions(+), 496 deletions(-) create mode 100644 src/__tests__/mocks/message.STREAMING_RESET.json create mode 100644 src/__tests__/mocks/message.V2.BOUNDED.GZIP.1457552651000.json create mode 100644 src/__tests__/mocks/message.V2.KEYLIST.GZIP.1457552652000.json create mode 100644 src/__tests__/mocks/message.V2.SEGMENT_REMOVAL.1457552653000.json create mode 100644 src/__tests__/mocks/message.V2.UNBOUNDED.1457552650000.json create mode 100644 src/sync/streaming/__tests__/mySegmentsV2utils.spec.ts create mode 100644 src/sync/streaming/mySegmentsV2utils.ts create mode 100644 src/utils/decompress/index.ts delete mode 100644 src/utils/murmur3/__tests__/murmur3_128.spec.ts rename src/utils/murmur3/{commons.ts => common.ts} (89%) create mode 100644 src/utils/murmur3/murmur3_128_x86.ts create mode 100644 src/utils/murmur3/murmur3_64.ts diff --git a/package-lock.json b/package-lock.json index 371af40b..e1ea1f9c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "0.1.1-canary.14", + "version": "0.1.1-canary.15", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 9c6308ff..a8341a1f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "0.1.1-canary.14", + "version": "0.1.1-canary.15", "description": "Split Javascript SDK common components", "main": "cjs/index.js", "module": "esm/index.js", diff --git a/src/__tests__/mocks/message.STREAMING_RESET.json b/src/__tests__/mocks/message.STREAMING_RESET.json new file mode 100644 index 00000000..ee1c13ea --- /dev/null +++ b/src/__tests__/mocks/message.STREAMING_RESET.json @@ -0,0 +1,4 @@ +{ + "type": "message", + "data": "{\"timestamp\":1457552660000,\"data\":\"{\\\"type\\\":\\\"STREAMING_RESET\\\"}\"}" +} \ No newline at end of file diff --git a/src/__tests__/mocks/message.V2.BOUNDED.GZIP.1457552651000.json b/src/__tests__/mocks/message.V2.BOUNDED.GZIP.1457552651000.json new file mode 100644 index 00000000..97c2a73c --- /dev/null +++ b/src/__tests__/mocks/message.V2.BOUNDED.GZIP.1457552651000.json @@ -0,0 +1,4 @@ +{ + "type": "message", + "data": "{\"data\":\"{\\\"type\\\":\\\"MY_SEGMENTS_UPDATE_V2\\\",\\\"changeNumber\\\":1457552651000,\\\"segmentName\\\":\\\"\\\",\\\"c\\\": 1,\\\"u\\\": 1,\\\"d\\\":\\\"H4sIAAAAAAAA/2JABxzYeIxQLguYFIBLN8Bl4EABjc+EzOnAsA4QAAD//8YBvWeAAAAA\\\"}\"}" +} \ No newline at end of file diff --git a/src/__tests__/mocks/message.V2.KEYLIST.GZIP.1457552652000.json b/src/__tests__/mocks/message.V2.KEYLIST.GZIP.1457552652000.json new file mode 100644 index 00000000..c44ee3ac --- /dev/null +++ b/src/__tests__/mocks/message.V2.KEYLIST.GZIP.1457552652000.json @@ -0,0 +1,4 @@ +{ + "type": "message", + "data": "{\"data\":\"{\\\"type\\\":\\\"MY_SEGMENTS_UPDATE_V2\\\",\\\"changeNumber\\\":1457552652000,\\\"segmentName\\\":\\\"splitters\\\",\\\"c\\\": 1,\\\"u\\\": 2,\\\"d\\\":\\\"H4sIAAAAAAAA/wTAsRHDUAgD0F2ofwEIkPAqPhdZIW0uu/v97GPXHU004ULuMGrYR6XUbIjlXULPPse+dt1yhJibBODjrTmj3GJ4emduuDDP/w0AAP//18WLsl0AAAA=\\\"}\"}" +} \ No newline at end of file diff --git a/src/__tests__/mocks/message.V2.SEGMENT_REMOVAL.1457552653000.json b/src/__tests__/mocks/message.V2.SEGMENT_REMOVAL.1457552653000.json new file mode 100644 index 00000000..aaf1a3f3 --- /dev/null +++ b/src/__tests__/mocks/message.V2.SEGMENT_REMOVAL.1457552653000.json @@ -0,0 +1,4 @@ +{ + "type": "message", + "data": "{\"data\":\"{\\\"type\\\":\\\"MY_SEGMENTS_UPDATE_V2\\\",\\\"changeNumber\\\":1457552653000,\\\"segmentName\\\":\\\"splitters\\\",\\\"c\\\": 0,\\\"u\\\": 3,\\\"d\\\":\\\"\\\"}\"}" +} \ No newline at end of file diff --git a/src/__tests__/mocks/message.V2.UNBOUNDED.1457552650000.json b/src/__tests__/mocks/message.V2.UNBOUNDED.1457552650000.json new file mode 100644 index 00000000..a7a2e793 --- /dev/null +++ b/src/__tests__/mocks/message.V2.UNBOUNDED.1457552650000.json @@ -0,0 +1,4 @@ +{ + "type": "message", + "data": "{\"data\":\"{\\\"type\\\":\\\"MY_SEGMENTS_UPDATE_V2\\\",\\\"changeNumber\\\":1457552650000,\\\"segmentName\\\":\\\"\\\",\\\"c\\\": 0,\\\"u\\\": 0,\\\"d\\\":\\\"\\\"}\"}" +} \ No newline at end of file diff --git a/src/logger/constants.ts b/src/logger/constants.ts index 9a85d37e..79f629c2 100644 --- a/src/logger/constants.ts +++ b/src/logger/constants.ts @@ -93,6 +93,7 @@ export const WARN_SPLITS_FILTER_INVALID = 220; export const WARN_SPLITS_FILTER_EMPTY = 221; export const WARN_STORAGE_INVALID = 222; export const WARN_API_KEY = 223; +export const STREAMING_PARSING_MY_SEGMENTS_UPDATE_V2 = 224; export const ERROR_ENGINE_COMBINER_IFELSEIF = 300; export const ERROR_LOGLEVEL_INVALID = 301; diff --git a/src/logger/messages/info.ts b/src/logger/messages/info.ts index 9a0637d5..22083bf6 100644 --- a/src/logger/messages/info.ts +++ b/src/logger/messages/info.ts @@ -22,9 +22,9 @@ export const codesInfo: [number, string][] = codesWarn.concat([ [c.SYNC_SPLITS_FETCH_RETRY, c.LOG_PREFIX_SYNC_SPLITS + 'Retrying download of splits #%s. Reason: %s'], [c.SUBMITTERS_PUSH_FULL_EVENTS_QUEUE, c.LOG_PREFIX_SYNC_SUBMITTERS + 'Flushing full events queue and reseting timer.'], [c.SUBMITTERS_PUSH, c.LOG_PREFIX_SYNC_SUBMITTERS + 'Pushing %s %s.'], - [c.STREAMING_REFRESH_TOKEN, c.LOG_PREFIX_SYNC_STREAMING + 'Refreshing streaming token in %s seconds.'], + [c.STREAMING_REFRESH_TOKEN, c.LOG_PREFIX_SYNC_STREAMING + 'Refreshing streaming token in %s seconds, and connecting streaming in %s seconds.'], [c.STREAMING_RECONNECT, c.LOG_PREFIX_SYNC_STREAMING + 'Attempting to reconnect in %s seconds.'], - [c.STREAMING_CONNECTING, c.LOG_PREFIX_SYNC_STREAMING + 'Connecting to streaming.'], + [c.STREAMING_CONNECTING, c.LOG_PREFIX_SYNC_STREAMING + '%sConnecting to streaming.'], [c.STREAMING_DISABLED, c.LOG_PREFIX_SYNC_STREAMING + 'Streaming is disabled for given Api key. Switching to polling mode.'], [c.STREAMING_DISCONNECTING, c.LOG_PREFIX_SYNC_STREAMING + 'Disconnecting from streaming.'], [c.SYNC_START_POLLING, c.LOG_PREFIX_SYNC_MANAGER + 'Streaming not available. Starting polling.'], diff --git a/src/logger/messages/warn.ts b/src/logger/messages/warn.ts index ca1ff5c7..350eeb43 100644 --- a/src/logger/messages/warn.ts +++ b/src/logger/messages/warn.ts @@ -30,5 +30,7 @@ export const codesWarn: [number, string][] = codesError.concat([ [c.WARN_SPLITS_FILTER_INVALID, c.LOG_PREFIX_SETTINGS+': split filter at position %s is invalid. It must be an object with a valid filter type ("byName" or "byPrefix") and a list of "values".'], [c.WARN_SPLITS_FILTER_EMPTY, c.LOG_PREFIX_SETTINGS+': splitFilters configuration must be a non-empty array of filter objects.'], [c.WARN_STORAGE_INVALID, c.LOG_PREFIX_SETTINGS+': The provided storage is invalid. Fallbacking into default MEMORY storage'], - [c.WARN_API_KEY, c.LOG_PREFIX_SETTINGS+': You already have %s. We recommend keeping only one instance of the factory at all times (Singleton pattern) and reusing it throughout your application'] + [c.WARN_API_KEY, c.LOG_PREFIX_SETTINGS+': You already have %s. We recommend keeping only one instance of the factory at all times (Singleton pattern) and reusing it throughout your application'], + + [c.STREAMING_PARSING_MY_SEGMENTS_UPDATE_V2, c.LOG_PREFIX_SYNC_STREAMING + 'Fetching MySegments due to an error processing %s notification: %s'], ]); diff --git a/src/services/splitApi.ts b/src/services/splitApi.ts index 301d054b..e2d6e3b6 100644 --- a/src/services/splitApi.ts +++ b/src/services/splitApi.ts @@ -36,7 +36,7 @@ export function splitApiFactory(settings: ISettings, platform: Pick Promise export type IPostMetricsTimes = (body: string) => Promise export interface ISplitApi { - getSdkAPIHealthCheck: IHealthCheckAPI; - getEventsAPIHealthCheck: IHealthCheckAPI; + getSdkAPIHealthCheck: IHealthCheckAPI + getEventsAPIHealthCheck: IHealthCheckAPI fetchAuth: IFetchAuth fetchSplitChanges: IFetchSplitChanges fetchSegmentChanges: IFetchSegmentChanges diff --git a/src/storages/types.ts b/src/storages/types.ts index 22ca98a9..5a1a8113 100644 --- a/src/storages/types.ts +++ b/src/storages/types.ts @@ -257,8 +257,8 @@ export interface ISegmentsCacheBase { // Same API for both variants: SegmentsCache and MySegmentsCache (client-side API) export interface ISegmentsCacheSync extends ISegmentsCacheBase { - addToSegment(name: string, segmentKeys: string[]): boolean - removeFromSegment(name: string, segmentKeys: string[]): boolean + addToSegment(name: string, segmentKeys?: string[]): boolean + removeFromSegment(name: string, segmentKeys?: string[]): boolean isInSegment(name: string, key?: string): boolean registerSegments(names: string[]): boolean getRegisteredSegments(): string[] diff --git a/src/sync/polling/types.ts b/src/sync/polling/types.ts index dd56fbcf..f3cf3639 100644 --- a/src/sync/polling/types.ts +++ b/src/sync/polling/types.ts @@ -2,11 +2,12 @@ import { IReadinessManager } from '../../readiness/types'; import { ISplitApi } from '../../services/types'; import { IStorageSync } from '../../storages/types'; import { ISettings } from '../../types'; +import { SegmentsData } from '../streaming/SSEHandler/types'; import { ITask, ISyncTask } from '../types'; export interface ISplitsSyncTask extends ISyncTask<[noCache?: boolean], boolean> { } -export interface ISegmentsSyncTask extends ISyncTask<[segmentNames?: string[], noCache?: boolean, fetchOnlyNew?: boolean], boolean> { } +export interface ISegmentsSyncTask extends ISyncTask<[segmentNames?: SegmentsData, noCache?: boolean, fetchOnlyNew?: boolean], boolean> { } export interface IPollingManager extends ITask { syncAll(): Promise diff --git a/src/sync/polling/updaters/mySegmentsUpdater.ts b/src/sync/polling/updaters/mySegmentsUpdater.ts index cbe74984..4ceb4ab3 100644 --- a/src/sync/polling/updaters/mySegmentsUpdater.ts +++ b/src/sync/polling/updaters/mySegmentsUpdater.ts @@ -5,6 +5,7 @@ import timeout from '../../../utils/promise/timeout'; import { SDK_SEGMENTS_ARRIVED } from '../../../readiness/constants'; import { ILogger } from '../../../logger/types'; import { SYNC_MYSEGMENTS_FETCH_RETRY } from '../../../logger/constants'; +import { SegmentsData } from '../../streaming/SSEHandler/types'; type IMySegmentsUpdater = (segmentList?: string[], noCache?: boolean) => Promise @@ -38,9 +39,23 @@ export function mySegmentsUpdaterFactory( } // @TODO if allowing custom storages, handle async execution - function updateSegments(segments: string[]) { - // Update the list of segment names available - const shouldNotifyUpdate = mySegmentsCache.resetSegments(segments); + function updateSegments(segmentsData: SegmentsData) { + + let shouldNotifyUpdate; + if (Array.isArray(segmentsData)) { + // Update the list of segment names available + shouldNotifyUpdate = mySegmentsCache.resetSegments(segmentsData); + } else { + // Add/Delete the segment + const { name, add } = segmentsData; + if (mySegmentsCache.isInSegment(name) !== add) { + shouldNotifyUpdate = true; + if (add) mySegmentsCache.addToSegment(name); + else mySegmentsCache.removeFromSegment(name); + } else { + shouldNotifyUpdate = false; + } + } // Notify update if required if (splitsCache.usesSegments() && (shouldNotifyUpdate || readyOnAlreadyExistentState)) { @@ -49,10 +64,10 @@ export function mySegmentsUpdaterFactory( } } - function _mySegmentsUpdater(retry: number, segmentList?: string[], noCache?: boolean): Promise { - const updaterPromise: Promise = segmentList ? - // If segmentList is provided, there is no need to fetch mySegments - new Promise((res) => { updateSegments(segmentList); res(true); }) : + function _mySegmentsUpdater(retry: number, segmentsData?: SegmentsData, noCache?: boolean): Promise { + const updaterPromise: Promise = segmentsData ? + // If segmentsData is provided, there is no need to fetch mySegments + new Promise((res) => { updateSegments(segmentsData); res(true); }) : // If not provided, fetch mySegments mySegmentsFetcher(noCache, _promiseDecorator).then(segments => { // Only when we have downloaded segments completely, we should not keep retrying anymore @@ -79,11 +94,14 @@ export function mySegmentsUpdaterFactory( * MySegments updater returns a promise that resolves with a `false` boolean value if it fails to fetch mySegments or synchronize them with the storage. * Returned promise will not be rejected. * - * @param {string[] | undefined} segmentList list of mySegments names to sync in the storage. If the list is `undefined`, it fetches them before syncing in the storage. + * @param {SegmentsData | undefined} segmentsData it can be: + * (1) the list of mySegments names to sync in the storage, + * (2) an object with a segment name and action (true: add, or false: delete) to update the storage, + * (3) or `undefined`, for which the updater will fetch mySegments in order to sync the storage. * @param {boolean | undefined} noCache true to revalidate data to fetch */ - return function mySegmentsUpdater(segmentList?: string[], noCache?: boolean) { - return _mySegmentsUpdater(0, segmentList, noCache); + return function mySegmentsUpdater(segmentsData?: SegmentsData, noCache?: boolean) { + return _mySegmentsUpdater(0, segmentsData, noCache); }; } diff --git a/src/sync/streaming/AuthClient/__tests__/dataMocks.ts b/src/sync/streaming/AuthClient/__tests__/dataMocks.ts index 3e354c8e..7d7ea334 100644 --- a/src/sync/streaming/AuthClient/__tests__/dataMocks.ts +++ b/src/sync/streaming/AuthClient/__tests__/dataMocks.ts @@ -44,3 +44,208 @@ export const userKeySample = 'emi@split.io'; export const userKeyBase64HashSample = 'MjAxNjU2NDU5Mw=='; export const channelsQueryParamSample = 'NzM2MDI5Mzc0_MzQyODU4NDUyNg%3D%3D_segments,NzM2MDI5Mzc0_MzQyODU4NDUyNg%3D%3D_splits,control'; + +export const keylists = [ + { + compression: 1, // GZIP + keyListDataCompressed: 'H4sIAAAAAAAA/wTAsRHDUAgD0F2ofwEIkPAqPhdZIW0uu/v97GPXHU004ULuMGrYR6XUbIjlXULPPse+dt1yhJibBODjrTmj3GJ4emduuDDP/w0AAP//18WLsl0AAAA=', + keyListData: { a: ['1573573083296714675', '8482869187405483569'], r: ['8031872927333060586', '6829471020522910836'] }, + addedUserKeys: ['key1', 'key2'], + removedUserKeys: ['key3', 'key4'], + otherUserKeys: ['key5', 'key6'] + } +]; + +export const bitmaps = [ + { + compression: 1, // GZIP + bitmapDataCompressed: 'H4sIAAAAAAAA/2JABxzYeIxQLguYFIBLN8Bl4EABjc+EzOnAsA4QAAD//8YBvWeAAAAA', + bitmapData: new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 136, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), + trueUserKeys: [ + '88f8b33b-f858-4aea-bea2-a5f066bab3ce', + '603516ce-1243-400b-b919-0dce5d8aecfd', + '4588c4f6-3d18-452a-bc4a-47d7abfd23df', + '42bcfe02-d268-472f-8ed5-e6341c33b4f7', + '4b0b0467-3fe1-43d1-a3d5-937c0a5473b1', + '2a7cae0e-85a2-443e-9d7c-7157b7c5960a', + '18c936ad-0cd2-490d-8663-03eaa23a5ef1', + '09025e90-d396-433a-9292-acef23cf0ad1', + 'bfd4a824-0cde-4f11-9700-2b4c5ad6f719', + '2b79d5df-b65a-d25d-75d7-05877f69d976', + '375903c8-6f62-4272-88f1-f8bcd304c7ae' + ], + falseUserKeys: [ + '88f8b33b-f858-4aea-bea2-a5f066bab3c0', + '603516ce-1243-400b-b919-0dce5d8aecf0', + '4588c4f6-3d18-452a-bc4a-47d7abfd23d0', + '42bcfe02-d268-472f-8ed5-e6341c33b4fc', + '4b0b0467-3fe1-43d1-a3d5-937c0a5473b5', + '2a7cae0e-85a2-443e-9d7c-7157b7c59606', + '18c936ad-0cd2-490d-8663-03eaa23a5ef0', + '09025e90-d396-433a-9292-acef23cf0ad0', + 'bfd4a824-0cde-4f11-9700-2b4c5ad6f710', + '2b79d5df-b65a-d25d-75d7-05877f69d970', + '375903c8-6f62-4272-88f1-f8bcd304c7a1' + ] + }, { + compression: 2, // ZLIB + bitmapDataCompressed: 'eJxiGAX4AMdAO2AU4AeMA+2AAQACA+0AuoORGMvDBDANtAPoDBQG2gGDGQz16pRloB0wCkbBKBgFo4As0EBYyZCqoojwDwEACAAA//+W/QFR', + trueUserKeys: [ + '88f8b33b-f858-4aea-bea2-a5f066bab3ce', + '603516ce-1243-400b-b919-0dce5d8aecfd', + '4588c4f6-3d18-452a-bc4a-47d7abfd23df', + '42bcfe02-d268-472f-8ed5-e6341c33b4f7', + '4b0b0467-3fe1-43d1-a3d5-937c0a5473b1', + '2a7cae0e-85a2-443e-9d7c-7157b7c5960a', + '18c936ad-0cd2-490d-8663-03eaa23a5ef1', + '09025e90-d396-433a-9292-acef23cf0ad1', + 'bfd4a824-0cde-4f11-9700-2b4c5ad6f719', + '2b79d5df-b65a-d25d-75d7-05877f69d976', + '375903c8-6f62-4272-88f1-f8bcd304c7ae' + ], + falseUserKeys: [ + '88f8b33b-f858-4aea-bea2-a5f066bab3c1', + '603516ce-1243-400b-b919-0dce5d8aecf2', + '4588c4f6-3d18-452a-bc4a-47d7abfd23d3', + '42bcfe02-d268-472f-8ed5-e6341c33b4f4', + '4b0b0467-3fe1-43d1-a3d5-937c0a5473b5', + '2a7cae0e-85a2-443e-9d7c-7157b7c59606', + '18c936ad-0cd2-490d-8663-03eaa23a5ef7', + '09025e90-d396-433a-9292-acef23cf0ad8', + 'bfd4a824-0cde-4f11-9700-2b4c5ad6f710', + '2b79d5df-b65a-d25d-75d7-05877f69d97a', + '375903c8-6f62-4272-88f1-f8bcd304c7ab' + ] + }, { + compression: 2, // ZLIB + bitmapDataCompressed: 'eJzMVk3OhDAIVdNFl9/22zVzEo8yR5mjT6LGsRTKg2LiW8yPUnjQB+2kIwM2ThTIKtVU1oknFcRzufz+YGYM/phnHW8sdPvs9EzXW2I+HFzhNyTNgCD/PpW9xpGiHD0Bw1U5HLSS644FbGZgoPovmjpmX5wAzhIxJyN7IAnFQWX1htj+LUl6ZQRV3umMqYG1LCrOJGLPV8+IidBQZFt6sOUA6CqsX5iEFY2gqufs2mfqRtsVWytRnO+iYMN7xIBqJhDqAydV+HidkGOGEJYvk4fhe/8iIukphG/XfFcfVxnMVcALCOF77qL/EU7ODepxlLST6qxFLYRdOyW8EBY4BqVjObnm3V5ZMkZIKf++8+hM7zM1Kd3aFqVZeSHzDQAA//+QUQ3a', + trueUserKeys: [ + 'user1', + 'user2', + 'user3', + 'user4', + 'user5', + 'user6', + 'user7', + 'user8', + 'user9', + 'user10', + 'user11', + 'user12', + 'user13', + 'user14', + 'user15', + 'user16', + 'user17', + 'user18', + 'user19', + 'user20', + 'user21', + 'user22', + 'user23', + 'user24', + 'user25', + 'user26', + 'user27', + 'user28', + 'user29', + 'user30', + 'user31', + 'user32', + 'user33', + 'user34', + 'user35', + 'user36', + 'user37', + 'user38', + 'user39', + 'user40', + 'user41', + 'user42', + 'user43', + 'user44', + 'user45', + 'user46', + 'user47', + 'user48', + 'user49', + 'user50', + 'user51', + 'user52', + 'user53', + 'user54', + 'user55', + 'user56', + 'user57', + 'user58', + 'user59', + 'user60', + 'user61', + 'user62', + 'user63', + 'user64', + 'user65', + 'user66', + 'user67', + 'user68', + 'user69', + 'user70', + 'user71', + 'user72', + 'user73', + 'user74', + 'user75', + 'user76', + 'user77', + 'user78', + 'user79', + 'user80', + 'user81', + 'user82', + 'user83', + 'user84', + 'user85', + 'user86', + 'user87', + 'user88', + 'user89', + 'user90', + 'user91', + 'user92', + 'user93', + 'user94', + 'user95', + 'user96', + 'user97', + 'user98', + 'user99', + 'user100', + 'user101', + 'user102', + 'user103', + 'user104', + 'user105', + 'user106', + 'user107', + 'user108', + 'user109', + 'user110', + 'user111', + 'user112', + 'user113', + 'user114', + 'user115', + 'user116', + 'user117', + 'user118', + 'user119', + 'user120', + ], + falseUserKeys: [ + 'user', + 'user0', + 'user121', + 'user122', + ] + } +]; diff --git a/src/sync/streaming/AuthClient/__tests__/index.spec.ts b/src/sync/streaming/AuthClient/__tests__/index.spec.ts index 98182a59..28675540 100644 --- a/src/sync/streaming/AuthClient/__tests__/index.spec.ts +++ b/src/sync/streaming/AuthClient/__tests__/index.spec.ts @@ -21,7 +21,7 @@ test('hashUserKey', () => { test('authenticate / success in node (200)', done => { - fetchMock.getOnce(authUrl + '/auth', (url, opts) => { + fetchMock.getOnce(authUrl + '/v2/auth', (url, opts) => { // @ts-ignore expect(opts.headers['Authorization']).toBe(`Bearer ${authorizationKey}`); // auth request must contain Authorization header with config authorizationKey return { status: 200, body: authDataResponseSample }; @@ -40,7 +40,7 @@ test('authenticate / success in browser (200)', done => { const userKeys = ['emi@split.io', 'maldo@split.io']; - fetchMock.getOnce(authUrl + '/auth?users=emi%40split.io&users=maldo%40split.io', (url, opts) => { + fetchMock.getOnce(authUrl + '/v2/auth?users=emi%40split.io&users=maldo%40split.io', (url, opts) => { // @ts-ignore expect(opts.headers['Authorization']).toBe(`Bearer ${authorizationKey}`); // auth request must contain Authorization header with config authorizationKey return { status: 200, body: authDataResponseSample }; @@ -57,7 +57,7 @@ test('authenticate / success in browser (200)', done => { test('authenticate / bad request in browser due to no user keys (400)', done => { - fetchMock.getOnce(authUrl + '/auth', { status: 400, body: '"no user specified"' }); + fetchMock.getOnce(authUrl + '/v2/auth', { status: 400, body: '"no user specified"' }); authenticate([]).then(() => { throw new Error('if bad request, promise is rejected'); @@ -70,7 +70,7 @@ test('authenticate / bad request in browser due to no user keys (400)', done => test('authenticate / Invalid credentials (401)', done => { - fetchMock.getOnce(authUrl + '/auth', { status: 401, body: '"Invalid credentials"' }); + fetchMock.getOnce(authUrl + '/v2/auth', { status: 401, body: '"Invalid credentials"' }); authenticate([]).then(() => { throw new Error('if invalid credential, promise should be rejected'); @@ -85,7 +85,7 @@ test('authenticate / HTTP error (other than 401)', done => { const NOT_OK_STATUS_CODE = 500; - fetchMock.getOnce(authUrl + '/auth', { status: NOT_OK_STATUS_CODE, body: 'some error message' }); + fetchMock.getOnce(authUrl + '/v2/auth', { status: NOT_OK_STATUS_CODE, body: 'some error message' }); authenticate([]).then(() => { throw new Error('if an HTTP error, promise is rejected'); @@ -98,7 +98,7 @@ test('authenticate / HTTP error (other than 401)', done => { test('authenticate / Network error (e.g., timeout)', done => { - fetchMock.getOnce(authUrl + '/auth', { throws: new TypeError('Network error') }); + fetchMock.getOnce(authUrl + '/v2/auth', { throws: new TypeError('Network error') }); authenticate([]).then(() => { throw new Error('if network error, promise is rejected'); @@ -111,9 +111,9 @@ test('authenticate / Network error (e.g., timeout)', done => { test('authenticate / Error parsing token', done => { - fetchMock.getOnce(authUrl + '/auth', { status: 200, body: { pushEnabled: true, token: jwtSampleInvalid } }); - fetchMock.getOnce(authUrl + '/auth', { status: 200, body: { pushEnabled: true, token: jwtSampleNoChannels } }); - fetchMock.getOnce(authUrl + '/auth', { status: 200, body: { pushEnabled: true, token: jwtSampleNoIat } }); + fetchMock.getOnce(authUrl + '/v2/auth', { status: 200, body: { pushEnabled: true, token: jwtSampleInvalid } }); + fetchMock.getOnce(authUrl + '/v2/auth', { status: 200, body: { pushEnabled: true, token: jwtSampleNoChannels } }); + fetchMock.getOnce(authUrl + '/v2/auth', { status: 200, body: { pushEnabled: true, token: jwtSampleNoIat } }); authenticate().then(() => { throw new Error('if invalid token, promise is rejected'); diff --git a/src/sync/streaming/AuthClient/types.ts b/src/sync/streaming/AuthClient/types.ts index cf8f78f3..53bc2b03 100644 --- a/src/sync/streaming/AuthClient/types.ts +++ b/src/sync/streaming/AuthClient/types.ts @@ -5,6 +5,7 @@ export interface IAuthTokenPushEnabled { token: string decodedToken: IDecodedJWTToken channels: { [channel: string]: string[] } + connDelay?: number } export interface IAuthTokenPushDisabled { @@ -15,3 +16,5 @@ export interface IAuthTokenPushDisabled { export type IAuthToken = IAuthTokenPushDisabled | IAuthTokenPushEnabled export type IAuthenticate = (userKeys?: string[]) => Promise + +export type IAuthenticateV2 = (isClientSide?: boolean) => Promise diff --git a/src/sync/streaming/SSEClient/__tests__/index.spec.ts b/src/sync/streaming/SSEClient/__tests__/index.spec.ts index f0c35d23..bf01bbf4 100644 --- a/src/sync/streaming/SSEClient/__tests__/index.spec.ts +++ b/src/sync/streaming/SSEClient/__tests__/index.spec.ts @@ -38,9 +38,6 @@ test('SSClient / setEventHandler, open and close methods', () => { const instance = new SSClient(settings, false, () => EventSourceMock); instance.setEventHandler(handler); - // error on first open without authToken - expect(instance.reopen).toThrow(Error); // throw error if reopen is invoked without a previous open call - // open connection instance.open(authDataSample); let esconnection = instance.connection; // instance of EventSource used to mock events @@ -73,12 +70,6 @@ test('SSClient / setEventHandler, open and close methods', () => { instance.connection.emitOpen(); expect(handler.handleOpen).toBeCalledTimes(1); // handleOpen called when connection is open - // reopen the connection - handler.handleOpen.mockClear(); - instance.reopen(); - instance.connection.emitOpen(); - expect(handler.handleOpen).toBeCalledTimes(1); // handleOpen called if connection is reopen - // remove event handler before opening a new connection handler.handleOpen.mockClear(); instance.setEventHandler(undefined); diff --git a/src/sync/streaming/SSEClient/index.ts b/src/sync/streaming/SSEClient/index.ts index 585f0d17..7e3cc4bb 100644 --- a/src/sync/streaming/SSEClient/index.ts +++ b/src/sync/streaming/SSEClient/index.ts @@ -35,7 +35,6 @@ export default class SSEClient implements ISSEClient { streamingUrl: string; connection?: InstanceType; handler?: ISseEventHandler; - authToken?: IAuthTokenPushEnabled; useHeaders?: boolean; headers: Record; @@ -54,7 +53,6 @@ export default class SSEClient implements ISSEClient { if (!this.eventSource) throw new Error('EventSource API is not available. '); this.streamingUrl = settings.urls.streaming + '/sse'; - this.reopen = this.reopen.bind(this); // @TODO get `useHeaders` flag from `getEventSource`, to use EventSource headers on client-side SDKs when possible. this.useHeaders = useHeaders; this.headers = buildSSEHeaders(settings); @@ -73,8 +71,6 @@ export default class SSEClient implements ISSEClient { open(authToken: IAuthTokenPushEnabled) { this.close(); // it closes connection if previously opened - this.authToken = authToken; - const channelsQueryParam = Object.keys(authToken.channels).map( function (channel) { const params = CONTROL_CHANNEL_REGEX.test(channel) ? '[?occupancy=metrics.publishers]' : ''; @@ -102,14 +98,4 @@ export default class SSEClient implements ISSEClient { close() { if (this.connection) this.connection.close(); } - - /** - * Re-open the connection with the last given authToken. - * - * @throws {TypeError} if `open` has not been previously called with an authToken - * @TODO this method is not used currently and could be removed. - */ - reopen() { - this.open(this.authToken as IAuthTokenPushEnabled); - } } diff --git a/src/sync/streaming/SSEClient/types.ts b/src/sync/streaming/SSEClient/types.ts index aba3c03f..3d614080 100644 --- a/src/sync/streaming/SSEClient/types.ts +++ b/src/sync/streaming/SSEClient/types.ts @@ -8,7 +8,6 @@ export interface ISseEventHandler { export interface ISSEClient { open(authToken: IAuthTokenPushEnabled): void, - reopen(): void, close(): void, setEventHandler(handler: ISseEventHandler): void } diff --git a/src/sync/streaming/SSEHandler/__tests__/index.spec.ts b/src/sync/streaming/SSEHandler/__tests__/index.spec.ts index 9ff2a73b..c463ea95 100644 --- a/src/sync/streaming/SSEHandler/__tests__/index.spec.ts +++ b/src/sync/streaming/SSEHandler/__tests__/index.spec.ts @@ -1,6 +1,6 @@ // @ts-nocheck import SSEHandlerFactory from '..'; -import { PUSH_SUBSYSTEM_UP, PUSH_NONRETRYABLE_ERROR, PUSH_SUBSYSTEM_DOWN, PUSH_RETRYABLE_ERROR, MY_SEGMENTS_UPDATE, SEGMENT_UPDATE, SPLIT_KILL, SPLIT_UPDATE } from '../../constants'; +import { PUSH_SUBSYSTEM_UP, PUSH_NONRETRYABLE_ERROR, PUSH_SUBSYSTEM_DOWN, PUSH_RETRYABLE_ERROR, MY_SEGMENTS_UPDATE, SEGMENT_UPDATE, SPLIT_KILL, SPLIT_UPDATE, MY_SEGMENTS_UPDATE_V2, STREAMING_RESET } from '../../constants'; import { loggerMock } from '../../../../logger/__tests__/sdkLogger.mock'; // update messages @@ -9,6 +9,13 @@ import splitKillMessage from '../../../../__tests__/mocks/message.SPLIT_KILL.145 import segmentUpdateMessage from '../../../../__tests__/mocks/message.SEGMENT_UPDATE.1457552640000.json'; import mySegmentsUpdateMessage from '../../../../__tests__/mocks/message.MY_SEGMENTS_UPDATE.nicolas@split.io.1457552640000.json'; +// update messages MY_SEGMENTS_UPDATE_V2 +import unboundedMessage from '../../../../__tests__/mocks/message.V2.UNBOUNDED.1457552650000'; +import boundedGzipMessage from '../../../../__tests__/mocks/message.V2.BOUNDED.GZIP.1457552651000'; +import keylistGzipMessage from '../../../../__tests__/mocks/message.V2.KEYLIST.GZIP.1457552652000'; +import segmentRemovalMessage from '../../../../__tests__/mocks/message.V2.SEGMENT_REMOVAL.1457552653000'; +import { keylists, bitmaps } from '../../AuthClient/__tests__/dataMocks'; + // occupancy messages import occupancy1ControlPri from '../../../../__tests__/mocks/message.OCCUPANCY.1.control_pri.1586987434450.json'; import occupancy0ControlPri from '../../../../__tests__/mocks/message.OCCUPANCY.0.control_pri.1586987434550.json'; @@ -25,6 +32,9 @@ const controlStreamingPausedSec = { ...controlStreamingPaused, data: controlStre const controlStreamingResumedSec = { ...controlStreamingResumed, data: controlStreamingResumed.data.replace('control_pri', 'control_sec') }; const controlStreamingDisabledSec = { ...controlStreamingDisabled, data: controlStreamingDisabled.data.replace('control_pri', 'control_sec') }; +// streaming reset message, from `{orgHash}_{envHash}_control` channel +import streamingReset from '../../../../__tests__/mocks/message.STREAMING_RESET'; + const pushEmitter = { emit: jest.fn() }; test('`handleOpen` and `handlerMessage` for OCCUPANCY notifications (NotificationKeeper)', () => { @@ -37,7 +47,7 @@ test('`handleOpen` and `handlerMessage` for OCCUPANCY notifications (Notificatio expect(pushEmitter.emit.mock.calls[0][0]).toBe(PUSH_SUBSYSTEM_UP); // must emit PUSH_SUBSYSTEM_UP sseHandler.handleMessage({ data: '{ "data": "{\\"type\\":\\"SPLIT_UPDATE\\",\\"changeNumber\\":1457552620999 }" }' }); - expect(pushEmitter.emit).toHaveBeenLastCalledWith(SPLIT_UPDATE, 1457552620999); // must handle update massage if streaming on + expect(pushEmitter.emit).toHaveBeenLastCalledWith(SPLIT_UPDATE, 1457552620999); // must handle update message if streaming on // OCCUPANCY messages @@ -49,25 +59,29 @@ test('`handleOpen` and `handlerMessage` for OCCUPANCY notifications (Notificatio expect(pushEmitter.emit).toHaveBeenLastCalledWith(PUSH_SUBSYSTEM_DOWN); // must emit PUSH_SUBSYSTEM_DOWN if streaming on and OCCUPANCY 0 in both control channels sseHandler.handleMessage({ data: '{ "data": "{\\"type\\":\\"SPLIT_UPDATE\\",\\"changeNumber\\":1457552620999 }" }' }); - expect(pushEmitter.emit).toBeCalledTimes(3); // must not handle update massage if streaming off after an OCCUPANCY message + expect(pushEmitter.emit).toBeCalledTimes(3); // must not handle update message if streaming off after an OCCUPANCY message + + sseHandler.handleMessage(streamingReset); + expect(pushEmitter.emit).toBeCalledTimes(4); // but must handle streaming reset message sseHandler.handleMessage(occupancy0ControlPri); sseHandler.handleMessage(occupancy0ControlSec); - expect(pushEmitter.emit).toBeCalledTimes(3); // must not emit PUSH_SUBSYSTEM_DOWN if streaming off + expect(pushEmitter.emit).toBeCalledTimes(4); // must not emit PUSH_SUBSYSTEM_DOWN if streaming off sseHandler.handleMessage(occupancy1ControlPri); sseHandler.handleMessage(occupancy1ControlSec); - expect(pushEmitter.emit).toBeCalledTimes(3); // must ignore OCCUPANCY message if its timestamp is older + expect(pushEmitter.emit).toBeCalledTimes(4); // must ignore OCCUPANCY message if its timestamp is older sseHandler.handleMessage(occupancy2ControlSec); expect(pushEmitter.emit).toHaveBeenLastCalledWith(PUSH_SUBSYSTEM_UP); // must emit PUSH_SUBSYSTEM_UP if streaming off and OCCUPANCY mayor than 0 in at least one channel sseHandler.handleMessage({ data: '{ "data": "{\\"type\\":\\"SPLIT_UPDATE\\",\\"changeNumber\\":1457552620999 }" }' }); sseHandler.handleMessage(occupancy2ControlPri); // must not emit PUSH_SUBSYSTEM_UP, since streaming is already on - expect(pushEmitter.emit).toHaveBeenLastCalledWith(SPLIT_UPDATE, 1457552620999); // must handle update massage if streaming on after an OCCUPANCY event - - expect(pushEmitter.emit).toBeCalledTimes(5); // must not emit PUSH_SUBSYSTEM_UP if streaming is already on and another channel has publishers + expect(pushEmitter.emit).toHaveBeenLastCalledWith(SPLIT_UPDATE, 1457552620999); // must handle update message if streaming on after an OCCUPANCY event + sseHandler.handleMessage(streamingReset); + expect(pushEmitter.emit).toHaveBeenLastCalledWith(STREAMING_RESET); // must handle streaming reset + expect(pushEmitter.emit).toBeCalledTimes(7); // must not emit PUSH_SUBSYSTEM_UP if streaming is already on and another channel has publishers }); test('`handlerMessage` for CONTROL notifications (NotificationKeeper)', () => { @@ -81,21 +95,26 @@ test('`handlerMessage` for CONTROL notifications (NotificationKeeper)', () => { expect(pushEmitter.emit).toHaveBeenLastCalledWith(PUSH_SUBSYSTEM_DOWN); // must emit PUSH_SUBSYSTEM_DOWN if streaming on and received a STREAMING_PAUSED control message sseHandler.handleMessage({ data: '{ "data": "{\\"type\\":\\"SPLIT_UPDATE\\",\\"changeNumber\\":1457552620999 }" }' }); - expect(pushEmitter.emit).toBeCalledTimes(2); // must not handle update massage if streaming off after a CONTROL message + expect(pushEmitter.emit).toBeCalledTimes(2); // must not handle update message if streaming off after a CONTROL message + + sseHandler.handleMessage(streamingReset); + expect(pushEmitter.emit).toBeCalledTimes(3); // but must handle streaming reset message sseHandler.handleMessage(controlStreamingPaused); sseHandler.handleMessage(controlStreamingPausedSec); - expect(pushEmitter.emit).toBeCalledTimes(2); // must not emit PUSH_SUBSYSTEM_DOWN if streaming off + expect(pushEmitter.emit).toBeCalledTimes(3); // must not emit PUSH_SUBSYSTEM_DOWN if streaming off sseHandler.handleMessage(controlStreamingResumedSec); // testing STREAMING_RESUMED with second region expect(pushEmitter.emit).toHaveBeenLastCalledWith(PUSH_SUBSYSTEM_UP); // must emit PUSH_SUBSYSTEM_UP if streaming off and received a STREAMING_RESUMED control message'); sseHandler.handleMessage(controlStreamingResumed); sseHandler.handleMessage(controlStreamingResumedSec); - expect(pushEmitter.emit).toBeCalledTimes(3); // must not emit PUSH_SUBSYSTEM_UP if streaming on + expect(pushEmitter.emit).toBeCalledTimes(4); // must not emit PUSH_SUBSYSTEM_UP if streaming on sseHandler.handleMessage({ data: '{ "data": "{\\"type\\":\\"SPLIT_UPDATE\\",\\"changeNumber\\":1457552620999 }" }' }); - expect(pushEmitter.emit).toHaveBeenLastCalledWith(SPLIT_UPDATE, 1457552620999); // must handle update massage if streaming on after a CONTROL event + expect(pushEmitter.emit).toHaveBeenLastCalledWith(SPLIT_UPDATE, 1457552620999); // must handle update message if streaming on after a CONTROL event + sseHandler.handleMessage(streamingReset); + expect(pushEmitter.emit).toHaveBeenLastCalledWith(STREAMING_RESET); // must handle streaming reset sseHandler.handleMessage(controlStreamingDisabledSec); // testing STREAMING_DISABLED with second region expect(pushEmitter.emit).toHaveBeenLastCalledWith(PUSH_NONRETRYABLE_ERROR); // must emit PUSH_NONRETRYABLE_ERROR if received a STREAMING_DISABLED control message @@ -111,7 +130,7 @@ test('`handlerMessage` for CONTROL notifications (NotificationKeeper)', () => { }); -test('`handlerMessage` for update notifications (NotificationProcessor)', () => { +test('`handlerMessage` for update notifications (NotificationProcessor) and streaming reset', () => { const sseHandler = SSEHandlerFactory(loggerMock, pushEmitter); sseHandler.handleOpen(); pushEmitter.emit.mockClear(); @@ -132,6 +151,25 @@ test('`handlerMessage` for update notifications (NotificationProcessor)', () => sseHandler.handleMessage(mySegmentsUpdateMessage); expect(pushEmitter.emit).toHaveBeenLastCalledWith(MY_SEGMENTS_UPDATE, ...expectedParams); // must emit MY_SEGMENTS_UPDATE with the message parsed data and channel + expectedParams = [{ type: 'MY_SEGMENTS_UPDATE_V2', changeNumber: 1457552650000, c: 0, d: '', u: 0, segmentName: '' }]; + sseHandler.handleMessage(unboundedMessage); + expect(pushEmitter.emit).toHaveBeenLastCalledWith(MY_SEGMENTS_UPDATE_V2, ...expectedParams); // must emit MY_SEGMENTS_UPDATE_V2 with the message parsed data + + expectedParams = [{ type: 'MY_SEGMENTS_UPDATE_V2', changeNumber: 1457552651000, c: 1, d: bitmaps[0].bitmapDataCompressed, u: 1, segmentName: '' }]; + sseHandler.handleMessage(boundedGzipMessage); + expect(pushEmitter.emit).toHaveBeenLastCalledWith(MY_SEGMENTS_UPDATE_V2, ...expectedParams); // must emit MY_SEGMENTS_UPDATE_V2 with the message parsed data + + expectedParams = [{ type: 'MY_SEGMENTS_UPDATE_V2', changeNumber: 1457552652000, c: 1, d: keylists[0].keyListDataCompressed, u: 2, segmentName: 'splitters' }]; + sseHandler.handleMessage(keylistGzipMessage); + expect(pushEmitter.emit).toHaveBeenLastCalledWith(MY_SEGMENTS_UPDATE_V2, ...expectedParams); // must emit MY_SEGMENTS_UPDATE_V2 with the message parsed data + + expectedParams = [{ type: 'MY_SEGMENTS_UPDATE_V2', changeNumber: 1457552653000, c: 0, d: '', u: 3, segmentName: 'splitters' }]; + sseHandler.handleMessage(segmentRemovalMessage); + expect(pushEmitter.emit).toHaveBeenLastCalledWith(MY_SEGMENTS_UPDATE_V2, ...expectedParams); // must emit MY_SEGMENTS_UPDATE_V2 with the message parsed data + + sseHandler.handleMessage(streamingReset); + expect(pushEmitter.emit).toHaveBeenLastCalledWith(STREAMING_RESET); // must emit STREAMING_RESET + }); test('handleError', () => { @@ -172,12 +210,12 @@ test('handlerMessage: ignore invalid events', () => { sseHandler.handleMessage('invalid message'); sseHandler.handleMessage({ data: '{ data: %invalid json\'\'}' }); - expect(pushEmitter.emit).toBeCalledTimes(0); // must ignore massage if invalid + expect(pushEmitter.emit).toBeCalledTimes(0); // must ignore message if invalid sseHandler.handleMessage({ data: '{ "data": "{\\"type\\":\\"UNSUPPORTED_TYPE\\",\\"changeNumber\\":1457552620999 }" }' }); - expect(pushEmitter.emit).toBeCalledTimes(0); // must ignore massage if it has an invalid type + expect(pushEmitter.emit).toBeCalledTimes(0); // must ignore message if it has an invalid type sseHandler.handleMessage({ data: '{ "data": "{\\"type\\":\\"SPLIT_UPDATE\\",\\"changeNumber\\":1457552620999 }" }' }); - expect(pushEmitter.emit).toBeCalledTimes(1); // must handle massage if valid + expect(pushEmitter.emit).toBeCalledTimes(1); // must handle message if valid }); diff --git a/src/sync/streaming/SSEHandler/index.ts b/src/sync/streaming/SSEHandler/index.ts index fbbf999c..a43495f3 100644 --- a/src/sync/streaming/SSEHandler/index.ts +++ b/src/sync/streaming/SSEHandler/index.ts @@ -1,6 +1,6 @@ import { errorParser, messageParser } from './NotificationParser'; import notificationKeeperFactory from './NotificationKeeper'; -import { PUSH_RETRYABLE_ERROR, PUSH_NONRETRYABLE_ERROR, OCCUPANCY, CONTROL, MY_SEGMENTS_UPDATE, SEGMENT_UPDATE, SPLIT_KILL, SPLIT_UPDATE } from '../constants'; +import { PUSH_RETRYABLE_ERROR, PUSH_NONRETRYABLE_ERROR, OCCUPANCY, CONTROL, STREAMING_RESET, MY_SEGMENTS_UPDATE, MY_SEGMENTS_UPDATE_V2, SEGMENT_UPDATE, SPLIT_KILL, SPLIT_UPDATE } from '../constants'; import { IPushEventEmitter } from '../types'; import { ISseEventHandler } from '../SSEClient/types'; import { INotificationError, INotificationMessage } from './types'; @@ -68,7 +68,7 @@ export default function SSEHandlerFactory(log: ILogger, pushEmitter: IPushEventE log.debug(STREAMING_NEW_MESSAGE, [data]); // we only handle update events if streaming is up. - if (!notificationKeeper.isStreamingUp() && parsedData.type !== OCCUPANCY && parsedData.type !== CONTROL) + if (!notificationKeeper.isStreamingUp() && [OCCUPANCY, CONTROL, STREAMING_RESET].indexOf(parsedData.type) === -1) return; switch (parsedData.type) { @@ -82,11 +82,16 @@ export default function SSEHandlerFactory(log: ILogger, pushEmitter: IPushEventE parsedData.changeNumber, parsedData.segmentName); break; + // @TODO remove case MY_SEGMENTS_UPDATE: pushEmitter.emit(MY_SEGMENTS_UPDATE, parsedData, channel); break; + case MY_SEGMENTS_UPDATE_V2: + pushEmitter.emit(MY_SEGMENTS_UPDATE_V2, + parsedData); + break; case SPLIT_KILL: pushEmitter.emit(SPLIT_KILL, parsedData.changeNumber, @@ -101,6 +106,14 @@ export default function SSEHandlerFactory(log: ILogger, pushEmitter: IPushEventE case CONTROL: notificationKeeper.handleControlEvent(parsedData.controlType, channel, timestamp); break; + + /* STREAMING_RESET event is handled by PushManager directly since it doesn't require + tracking timestamp and state like OCCUPANCY or CONTROL. It also ignores previous + OCCUPANCY and CONTROL notifications, and whether PUSH_SUBSYSTEM_DOWN has been emitted or not */ + case STREAMING_RESET: + pushEmitter.emit(STREAMING_RESET); + break; + default: break; } diff --git a/src/sync/streaming/SSEHandler/types.ts b/src/sync/streaming/SSEHandler/types.ts index f5583c9c..446b6060 100644 --- a/src/sync/streaming/SSEHandler/types.ts +++ b/src/sync/streaming/SSEHandler/types.ts @@ -1,5 +1,5 @@ import { ControlType } from '../constants'; -import { MY_SEGMENTS_UPDATE, SEGMENT_UPDATE, SPLIT_UPDATE, SPLIT_KILL, CONTROL, OCCUPANCY } from '../types'; +import { MY_SEGMENTS_UPDATE, MY_SEGMENTS_UPDATE_V2, SEGMENT_UPDATE, SPLIT_UPDATE, SPLIT_KILL, CONTROL, OCCUPANCY, STREAMING_RESET } from '../types'; export interface IMySegmentsUpdateData { type: MY_SEGMENTS_UPDATE, @@ -8,6 +8,33 @@ export interface IMySegmentsUpdateData { segmentList?: string[] } +export const enum Compression { + None = 0, + Gzip = 1, + Zlib = 2 +} + +export const enum UpdateStrategy { + UnboundedFetchRequest = 0, + BoundedFetchRequest = 1, + KeyList = 2, + SegmentRemoval = 3 +} + +export interface KeyList { + a?: string[], // decimal hash64 of user keys + r?: string[], // decimal hash64 of user keys +} + +export interface IMySegmentsUpdateV2Data { + type: MY_SEGMENTS_UPDATE_V2, + changeNumber: number, + segmentName: string, + c: Compression, + d: string, + u: UpdateStrategy, +} + export interface ISegmentUpdateData { type: SEGMENT_UPDATE, changeNumber: number, @@ -38,6 +65,18 @@ export interface IOccupancyData { } } -export type INotificationData = IMySegmentsUpdateData | ISegmentUpdateData | ISplitUpdateData | ISplitKillData | IControlData | IOccupancyData +export interface IStreamingResetData { + type: STREAMING_RESET +} + +export type INotificationData = IMySegmentsUpdateData | IMySegmentsUpdateV2Data | ISegmentUpdateData | ISplitUpdateData | ISplitKillData | IControlData | IOccupancyData | IStreamingResetData export type INotificationMessage = { parsedData: INotificationData, channel: string, timestamp: number, data: string } export type INotificationError = Event & { parsedData?: any, message?: string } + +// @TODO remove `string[]` from the union type when removing support for MY_SEGMENTS_UPDATE notifications +export type SegmentsData = string[] | { + /* segment name */ + name: string, + /* action: `true` for add, and `false` for delete */ + add: boolean +} diff --git a/src/sync/streaming/UpdateWorkers/MySegmentsUpdateWorker.ts b/src/sync/streaming/UpdateWorkers/MySegmentsUpdateWorker.ts index fb52b403..192e81fa 100644 --- a/src/sync/streaming/UpdateWorkers/MySegmentsUpdateWorker.ts +++ b/src/sync/streaming/UpdateWorkers/MySegmentsUpdateWorker.ts @@ -1,6 +1,7 @@ import { ISegmentsSyncTask } from '../../polling/types'; import Backoff from '../../../utils/Backoff'; import { IUpdateWorker } from './types'; +import { SegmentsData } from '../SSEHandler/types'; /** * MySegmentsUpdateWorker class @@ -10,7 +11,7 @@ export default class MySegmentsUpdateWorker implements IUpdateWorker { private readonly mySegmentsSyncTask: ISegmentsSyncTask; private maxChangeNumber: number; private handleNewEvent: boolean; - private segmentList?: string[]; + private segmentsData?: SegmentsData; private currentChangeNumber: number; readonly backoff: Backoff; @@ -21,7 +22,7 @@ export default class MySegmentsUpdateWorker implements IUpdateWorker { this.mySegmentsSyncTask = mySegmentsSyncTask; this.maxChangeNumber = 0; // keeps the maximum changeNumber among queued events this.handleNewEvent = false; - this.segmentList = undefined; // keeps the segmentList (if included in payload) from the queued event with maximum changeNumber + this.segmentsData = undefined; // keeps the segmentsData (if included in notification payload) from the queued event with maximum changeNumber this.currentChangeNumber = -1; // @TODO: remove once `/mySegments` endpoint provides the changeNumber this.put = this.put.bind(this); this.__handleMySegmentsUpdateCall = this.__handleMySegmentsUpdateCall.bind(this); @@ -36,7 +37,7 @@ export default class MySegmentsUpdateWorker implements IUpdateWorker { const currentMaxChangeNumber = this.maxChangeNumber; // fetch mySegments revalidating data if cached - this.mySegmentsSyncTask.execute(this.segmentList, true).then((result) => { + this.mySegmentsSyncTask.execute(this.segmentsData, true).then((result) => { if (result !== false) // Unlike `Splits|SegmentsUpdateWorker`, we cannot use `mySegmentsCache.getChangeNumber` since `/mySegments` endpoint doesn't provide this value. this.currentChangeNumber = Math.max(this.currentChangeNumber, currentMaxChangeNumber); // use `currentMaxChangeNumber`, in case that `this.maxChangeNumber` was updated during fetch. if (this.handleNewEvent) { @@ -52,15 +53,15 @@ export default class MySegmentsUpdateWorker implements IUpdateWorker { * Invoked by NotificationProcessor on MY_SEGMENTS_UPDATE event * * @param {number} changeNumber change number of the MY_SEGMENTS_UPDATE notification - * @param {string[] | undefined} segmentList might be undefined + * @param {SegmentsData | undefined} segmentsData might be undefined */ - put(changeNumber: number, segmentList?: string[]) { + put(changeNumber: number, segmentsData?: SegmentsData) { if (changeNumber <= this.currentChangeNumber || changeNumber <= this.maxChangeNumber) return; this.maxChangeNumber = changeNumber; this.handleNewEvent = true; this.backoff.reset(); - this.segmentList = segmentList; + this.segmentsData = segmentsData; if (this.mySegmentsSyncTask.isExecuting()) return; diff --git a/src/sync/streaming/__tests__/mySegmentsV2utils.spec.ts b/src/sync/streaming/__tests__/mySegmentsV2utils.spec.ts new file mode 100644 index 00000000..480ec625 --- /dev/null +++ b/src/sync/streaming/__tests__/mySegmentsV2utils.spec.ts @@ -0,0 +1,53 @@ +import { hash64 } from '../../../utils/murmur3/murmur3_64'; +import { keylists, bitmaps } from '../AuthClient/__tests__/dataMocks'; + +import { parseKeyList, parseBitmap, isInBitmap } from '../mySegmentsV2utils'; +import { _Set } from '../../../utils/lang/sets'; + +test('parseKeyList', () => { + keylists.forEach(keylist => { + const { compression, keyListData, keyListDataCompressed, addedUserKeys, removedUserKeys, otherUserKeys } = keylist; + + expect(parseKeyList(keyListDataCompressed, compression)).toEqual(keyListData); // decompress KeyList + + const added = new _Set(keyListData.a); + const removed = new _Set(keyListData.r); + + addedUserKeys.forEach(userKey => { + const hash = hash64(userKey); + expect(added.has(hash.dec)).toBe(true); // key hash belongs to added list + expect(removed.has(hash.dec)).toBe(false); // t belong to removed list + }); + + removedUserKeys.forEach(userKey => { + const hash = hash64(userKey); + expect(added.has(hash.dec)).toBe(false); // t belong to added list + expect(removed.has(hash.dec)).toBe(true); // key hash belongs to removed list + }); + + otherUserKeys.forEach(userKey => { + const hash = hash64(userKey); + expect(added.has(hash.dec)).toBe(false); // t belong to added list + expect(removed.has(hash.dec)).toBe(false); // t belong to removed list + }); + }); +}); + +test('parseBitmap & isInBitmap', () => { + bitmaps.forEach(bitmap => { + const { compression, bitmapData, bitmapDataCompressed, trueUserKeys, falseUserKeys } = bitmap; + + const actualBitmap = parseBitmap(bitmapDataCompressed, compression); + if (bitmapData) expect(actualBitmap).toEqual(bitmapData); // decompress Bitmap + + trueUserKeys.forEach(userKey => { + const hash = hash64(userKey); + expect(isInBitmap(actualBitmap, hash.hex)).toBe(true); // key hash belongs to Bitmap + }); + + falseUserKeys.forEach(userKey => { + const hash = hash64(userKey); + expect(isInBitmap(actualBitmap, hash.hex)).toBe(false); // t belong to Bitmap + }); + }); +}); diff --git a/src/sync/streaming/constants.ts b/src/sync/streaming/constants.ts index 8b69840d..da381707 100644 --- a/src/sync/streaming/constants.ts +++ b/src/sync/streaming/constants.ts @@ -26,6 +26,7 @@ export const PUSH_SUBSYSTEM_DOWN = 'PUSH_SUBSYSTEM_DOWN'; // Update-type push notifications, handled by NotificationProcessor export const MY_SEGMENTS_UPDATE = 'MY_SEGMENTS_UPDATE'; +export const MY_SEGMENTS_UPDATE_V2 = 'MY_SEGMENTS_UPDATE_V2'; export const SEGMENT_UPDATE = 'SEGMENT_UPDATE'; export const SPLIT_KILL = 'SPLIT_KILL'; export const SPLIT_UPDATE = 'SPLIT_UPDATE'; @@ -34,6 +35,8 @@ export const SPLIT_UPDATE = 'SPLIT_UPDATE'; export const CONTROL = 'CONTROL'; export const OCCUPANCY = 'OCCUPANCY'; +export const STREAMING_RESET = 'STREAMING_RESET'; + export enum ControlType { STREAMING_DISABLED = 'STREAMING_DISABLED', STREAMING_PAUSED = 'STREAMING_PAUSED', diff --git a/src/sync/streaming/mySegmentsV2utils.ts b/src/sync/streaming/mySegmentsV2utils.ts new file mode 100644 index 00000000..df3adff6 --- /dev/null +++ b/src/sync/streaming/mySegmentsV2utils.ts @@ -0,0 +1,77 @@ +import { algorithms } from '../../utils/decompress'; +import { decodeFromBase64 } from '../../utils/base64'; +import { Compression, KeyList } from './SSEHandler/types'; + +const GZIP = 1; +const ZLIB = 2; + +function Uint8ArrayToString(myUint8Arr: Uint8Array) { // @ts-ignore + return String.fromCharCode.apply(null, myUint8Arr); +} + +function StringToUint8Array(myString: string) { + const charCodes = myString.split('').map((e) => e.charCodeAt(0)); + return new Uint8Array(charCodes); +} + +/** + * Decode and decompress 'data' with 'compression' algorithm + * + * @param {string} data + * @param {number} compression 1 GZIP, 2 ZLIB + * @returns {Uint8Array} + * @throws if data string cannot be decoded, decompressed or the provided compression value is invalid (not 1 or 2) + */ +function decompress(data: string, compression: Compression) { + let compressData = decodeFromBase64(data); + const binData = StringToUint8Array(compressData); + + if (typeof algorithms === 'string') throw new Error(algorithms); + if (compression === GZIP) return algorithms.gunzipSync(binData); + if (compression === ZLIB) return algorithms.unzlibSync(binData); + throw new Error(`Invalid compression algorithm #${compression}`); +} + +/** + * Decode, decompress and parse the provided 'data' into a KeyList object + * + * @param {string} data + * @param {number} compression + * @returns {{a?: string[], r?: string[] }} + * @throws if data string cannot be decoded, decompressed or parsed + */ +export function parseKeyList(data: string, compression: Compression): KeyList { + const binKeyList = decompress(data, compression); + const strKeyList = Uint8ArrayToString(binKeyList); + + // replace numbers to strings, to avoid losing precision + return JSON.parse(strKeyList.replace(/\d+/g, '"$&"')); +} + +/** + * Decode, decompress and parse the provided 'data' into a Bitmap object + * + * @param {string} data + * @param {number} compression + * @returns {Uint8Array} + * @throws if data string cannot be decoded or decompressed + */ +export function parseBitmap(data: string, compression: Compression) { + return decompress(data, compression); +} + +/** + * Check if the 'bitmap' bit at 'hash64hex' position is 1 + * + * @param {Uint8Array} bitmap + * @param {string} hash64hex 16-chars string, representing a number in hexa + * @returns {boolean} + */ +export function isInBitmap(bitmap: Uint8Array, hash64hex: string) { + // using the lowest 32 bits as index, to avoid losing precision when converting to number + const index = parseInt(hash64hex.slice(8), 16) % (bitmap.length * 8); + + const internal = Math.floor(index / 8); + const offset = index % 8; + return (bitmap[internal] & 1 << offset) > 0; +} diff --git a/src/sync/streaming/pushManager.ts b/src/sync/streaming/pushManager.ts index 589d0ee6..7a60d258 100644 --- a/src/sync/streaming/pushManager.ts +++ b/src/sync/streaming/pushManager.ts @@ -3,7 +3,6 @@ import { ISSEClient } from './SSEClient/types'; import { IStorageSync } from '../../storages/types'; import { IReadinessManager } from '../../readiness/types'; import { ISegmentsSyncTask, IPollingManager } from '../polling/types'; -import { IUpdateWorker } from './UpdateWorkers/types'; import objectAssign from 'object-assign'; import Backoff from '../../utils/Backoff'; import SSEHandlerFactory from './SSEHandler'; @@ -16,9 +15,14 @@ import SSEClient from './SSEClient'; import { IFetchAuth } from '../../services/types'; import { ISettings } from '../../types'; import { getMatching } from '../../utils/key'; -import { MY_SEGMENTS_UPDATE, PUSH_NONRETRYABLE_ERROR, PUSH_SUBSYSTEM_DOWN, SECONDS_BEFORE_EXPIRATION, SEGMENT_UPDATE, SPLIT_KILL, SPLIT_UPDATE, PUSH_RETRYABLE_ERROR, PUSH_SUBSYSTEM_UP } from './constants'; +import { MY_SEGMENTS_UPDATE, MY_SEGMENTS_UPDATE_V2, PUSH_NONRETRYABLE_ERROR, PUSH_SUBSYSTEM_DOWN, SECONDS_BEFORE_EXPIRATION, SEGMENT_UPDATE, SPLIT_KILL, SPLIT_UPDATE, PUSH_RETRYABLE_ERROR, PUSH_SUBSYSTEM_UP, STREAMING_RESET } from './constants'; import { IPlatform } from '../../sdkFactory/types'; -import { STREAMING_FALLBACK, STREAMING_REFRESH_TOKEN, STREAMING_CONNECTING, STREAMING_DISABLED, ERROR_STREAMING_AUTH, STREAMING_DISCONNECTING, STREAMING_RECONNECT } from '../../logger/constants'; +import { STREAMING_FALLBACK, STREAMING_REFRESH_TOKEN, STREAMING_CONNECTING, STREAMING_DISABLED, ERROR_STREAMING_AUTH, STREAMING_DISCONNECTING, STREAMING_RECONNECT, STREAMING_PARSING_MY_SEGMENTS_UPDATE_V2 } from '../../logger/constants'; +import { KeyList, UpdateStrategy } from './SSEHandler/types'; +import { isInBitmap, parseBitmap, parseKeyList } from './mySegmentsV2utils'; +import { ISet, _Set } from '../../utils/lang/sets'; +import { Hash64, hash64 } from '../../utils/murmur3/murmur3_64'; +import { IAuthTokenPushEnabled } from './AuthClient/types'; /** * PushManager factory: @@ -54,23 +58,22 @@ export default function pushManagerFactory( const sseHandler = SSEHandlerFactory(log, pushEmitter); sseClient.setEventHandler(sseHandler); + // init workers + const segmentsUpdateWorker = userKey ? new MySegmentsUpdateWorker(pollingManager.segmentsSyncTask) : new SegmentsUpdateWorker(storage.segments, pollingManager.segmentsSyncTask); + // For server-side we pass the segmentsSyncTask, used by SplitsUpdateWorker to fetch new segments + const splitsUpdateWorker = new SplitsUpdateWorker(storage.splits, pollingManager.splitsSyncTask, readiness.splits, userKey ? undefined : pollingManager.segmentsSyncTask); + // [Only for client-side] map of hashes to user keys, to dispatch MY_SEGMENTS_UPDATE events to the corresponding MySegmentsUpdateWorker const userKeyHashes: Record = {}; + // [Only for client-side] map of user keys to their corresponding hash64 and MySegmentsUpdateWorkers. + // Hash64 is used to process MY_SEGMENTS_UPDATE_V2 events and dispatch actions to the corresponding MySegmentsUpdateWorker. + const clients: Record = {}; if (userKey) { const hash = hashUserKey(userKey); userKeyHashes[hash] = userKey; + clients[userKey] = { hash64: hash64(userKey), worker: segmentsUpdateWorker as MySegmentsUpdateWorker }; } - // init workers - const segmentsUpdateWorker = userKey ? new MySegmentsUpdateWorker(pollingManager.segmentsSyncTask) : new SegmentsUpdateWorker(storage.segments, pollingManager.segmentsSyncTask); - // [Only for server-side] we pass the segmentsSyncTask, used by SplitsUpdateWorker to fetch new segments - const splitsUpdateWorker = new SplitsUpdateWorker(storage.splits, pollingManager.splitsSyncTask, readiness.splits, userKey ? undefined : pollingManager.segmentsSyncTask); - // [Only for client-side] map of user keys to their corresponding MySegmentsUpdateWorkers. It has a two-fold intention: - // - stop workers all together when push is disconnected - // - keep the current list of user keys to authenticate - const workers: Record = {}; - if (userKey) workers[userKey] = segmentsUpdateWorker; - // [Only for client-side] variable to flag that a new client was added. It is needed to reconnect streaming. let connectForNewClient = false; @@ -78,32 +81,44 @@ export default function pushManagerFactory( // It is used to halt the `connectPush` process if it was in progress. let disconnected: boolean | undefined; // flag that indicates a PUSH_NONRETRYABLE_ERROR, condition with which starting pushManager again is ignored. - let disabled: boolean | undefined; + let disabled: boolean | undefined; // `disabled` implies `disconnected === true` /** PushManager functions related to initialization */ const connectPushRetryBackoff = new Backoff(connectPush, settings.scheduler.pushRetryBackoffBase); - let timeoutId: ReturnType; + let timeoutIdTokenRefresh: ReturnType; + let timeoutIdSseOpen: ReturnType; + + function scheduleTokenRefreshAndSse(authData: IAuthTokenPushEnabled) { + // clear scheduled tasks if exist + if (timeoutIdTokenRefresh) clearTimeout(timeoutIdTokenRefresh); + if (timeoutIdSseOpen) clearTimeout(timeoutIdSseOpen); - function scheduleTokenRefresh(issuedAt: number, expirationTime: number) { - // clear scheduled token refresh if exists (needed when resuming PUSH) - if (timeoutId) clearTimeout(timeoutId); + // Set token refresh 10 minutes before `expirationTime - issuedAt` + const decodedToken = authData.decodedToken; + const refreshTokenDelay = decodedToken.exp - decodedToken.iat - SECONDS_BEFORE_EXPIRATION; + // connDelay is present in AuthV2 but not in AuthV1 + const connDelay = authData.connDelay || 0; - // Set token refresh 10 minutes before expirationTime - const delayInSeconds = expirationTime - issuedAt - SECONDS_BEFORE_EXPIRATION; + log.info(STREAMING_REFRESH_TOKEN, [refreshTokenDelay, connDelay]); - log.info(STREAMING_REFRESH_TOKEN, [delayInSeconds]); + timeoutIdTokenRefresh = setTimeout(connectPush, refreshTokenDelay * 1000); - timeoutId = setTimeout(connectPush, delayInSeconds * 1000); + timeoutIdSseOpen = setTimeout(() => { + // halt if disconnected + if (disconnected) return; + sseClient.open(authData); + }, connDelay * 1000); } function connectPush() { + // Guard condition in case `stop/disconnectPush` has been called (e.g., calling SDK destroy, or app signal close/background) if (disconnected) return; + log.info(STREAMING_CONNECTING, [disconnected === undefined ? '' : 'Re-']); + disconnected = false; - log.info(STREAMING_CONNECTING); - - const userKeys = userKey ? Object.keys(workers) : undefined; + const userKeys = userKey ? Object.keys(clients) : undefined; authenticate(userKeys).then( function (authData) { if (disconnected) return; @@ -117,12 +132,10 @@ export default function pushManagerFactory( } // [Only for client-side] don't open SSE connection if a new shared client was added, since it means that a new authentication is taking place - if (userKeys && userKeys.length < Object.keys(workers).length) return; + if (userKeys && userKeys.length < Object.keys(clients).length) return; - // Connect to SSE and schedule refresh token - const decodedToken = authData.decodedToken; - sseClient.open(authData); - scheduleTokenRefresh(decodedToken.iat, decodedToken.exp); + // Schedule SSE connection and refresh token + scheduleTokenRefreshAndSse(authData); } ).catch( function (error) { @@ -144,11 +157,15 @@ export default function pushManagerFactory( // close SSE connection and cancel scheduled tasks function disconnectPush() { + // Halt disconnecting, just to avoid redundant logs if called multiple times + if (disconnected) return; disconnected = true; + sseClient.close(); log.info(STREAMING_DISCONNECTING); - if (timeoutId) clearTimeout(timeoutId); + if (timeoutIdTokenRefresh) clearTimeout(timeoutIdTokenRefresh); + if (timeoutIdSseOpen) clearTimeout(timeoutIdSseOpen); connectPushRetryBackoff.reset(); stopWorkers(); @@ -157,14 +174,18 @@ export default function pushManagerFactory( // cancel scheduled fetch retries of Splits, Segments, and MySegments Update Workers function stopWorkers() { splitsUpdateWorker.backoff.reset(); - if (userKey) forOwn(workers, worker => worker.backoff.reset()); + if (userKey) forOwn(clients, ({ worker }) => worker.backoff.reset()); else segmentsUpdateWorker.backoff.reset(); } pushEmitter.on(PUSH_SUBSYSTEM_DOWN, stopWorkers); - // restart backoff retry counter once push is connected - pushEmitter.on(PUSH_SUBSYSTEM_UP, () => { connectPushRetryBackoff.reset(); }); + // Only required when streaming connects after a PUSH_RETRYABLE_ERROR. + // Otherwise it is unnecessary (e.g, STREAMING_RESUMED). + pushEmitter.on(PUSH_SUBSYSTEM_UP, () => { + connectPushRetryBackoff.reset(); + stopWorkers(); + }); /** Fallbacking without retry due to: STREAMING_DISABLED control event, or 'pushEnabled: false', or non-recoverable SSE and Authentication errors */ @@ -189,21 +210,101 @@ export default function pushManagerFactory( pushEmitter.emit(PUSH_SUBSYSTEM_DOWN); // no harm if polling already }); + /** STREAMING_RESET notification. Unlike a PUSH_RETRYABLE_ERROR, it doesn't emit PUSH_SUBSYSTEM_DOWN to fallback polling */ + + pushEmitter.on(STREAMING_RESET, function handleStreamingReset() { + if (disconnected) return; // should never happen + + // Minimum required clean-up. + // `disconnectPush` cannot be called because it sets `disconnected` and thus `connectPush` will not execute + if (timeoutIdTokenRefresh) clearTimeout(timeoutIdTokenRefresh); + + connectPush(); + }); + /** Functions related to synchronization (Queues and Workers in the spec) */ pushEmitter.on(SPLIT_KILL, splitsUpdateWorker.killSplit); pushEmitter.on(SPLIT_UPDATE, splitsUpdateWorker.put); if (userKey) { + // @TODO remove pushEmitter.on(MY_SEGMENTS_UPDATE, function handleMySegmentsUpdate(parsedData, channel) { const userKeyHash = channel.split('_')[2]; const userKey = userKeyHashes[userKeyHash]; - if (userKey && workers[userKey]) { // check context since it can be undefined if client has been destroyed - const mySegmentsUpdateWorker = workers[userKey]; - mySegmentsUpdateWorker.put( + if (userKey && clients[userKey]) { // check existence since it can be undefined if client has been destroyed + clients[userKey].worker.put( parsedData.changeNumber, parsedData.includesPayload ? parsedData.segmentList ? parsedData.segmentList : [] : undefined); } }); + pushEmitter.on(MY_SEGMENTS_UPDATE_V2, function handleMySegmentsUpdate(parsedData) { + switch (parsedData.u) { + case UpdateStrategy.BoundedFetchRequest: { + let bitmap: Uint8Array; + try { + bitmap = parseBitmap(parsedData.d, parsedData.c); + } catch (e) { + log.warn(STREAMING_PARSING_MY_SEGMENTS_UPDATE_V2, ['BoundedFetchRequest', e]); + break; + } + + forOwn(clients, ({ hash64, worker }) => { + if (isInBitmap(bitmap, hash64.hex)) { + // fetch mySegments + worker.put(parsedData.changeNumber); + } + }); + return; + } + case UpdateStrategy.KeyList: { + let keyList: KeyList, added: ISet, removed: ISet; + try { + keyList = parseKeyList(parsedData.d, parsedData.c); + added = new _Set(keyList.a); + removed = new _Set(keyList.r); + } catch (e) { + log.warn(STREAMING_PARSING_MY_SEGMENTS_UPDATE_V2, ['KeyList', e]); + break; + } + + forOwn(clients, ({ hash64, worker }) => { + if (added.has(hash64.dec)) { + worker.put(parsedData.changeNumber, { + name: parsedData.segmentName, + add: true + }); + return; + } + if (removed.has(hash64.dec)) { + worker.put(parsedData.changeNumber, { + name: parsedData.segmentName, + add: false + }); + return; + } + }); + return; + } + case UpdateStrategy.SegmentRemoval: + if (!parsedData.segmentName) { + log.warn(STREAMING_PARSING_MY_SEGMENTS_UPDATE_V2, ['SegmentRemoval', 'No segment name was provided']); + break; + } + + forOwn(clients, ({ worker }) => + worker.put(parsedData.changeNumber, { + name: parsedData.segmentName, + add: false + }) + ); + return; + } + + // `UpdateStrategy.UnboundedFetchRequest` and fallbacks of other cases + forOwn(clients, ({ worker }) => { + worker.put(parsedData.changeNumber); + }); + }); } else { pushEmitter.on(SEGMENT_UPDATE, (segmentsUpdateWorker as SegmentsUpdateWorker).put); } @@ -225,8 +326,7 @@ export default function pushManagerFactory( // [Only for client-side] add(userKey: string, mySegmentsSyncTask: ISegmentsSyncTask) { - const mySegmentsUpdateWorker = new MySegmentsUpdateWorker(mySegmentsSyncTask); - workers[userKey] = mySegmentsUpdateWorker; + clients[userKey] = { hash64: hash64(userKey), worker: new MySegmentsUpdateWorker(mySegmentsSyncTask) }; const hash = hashUserKey(userKey); @@ -249,6 +349,7 @@ export default function pushManagerFactory( remove(userKey: string) { const hash = hashUserKey(userKey); delete userKeyHashes[hash]; + delete clients[userKey]; } } ); diff --git a/src/sync/streaming/types.ts b/src/sync/streaming/types.ts index df348f70..69d45219 100644 --- a/src/sync/streaming/types.ts +++ b/src/sync/streaming/types.ts @@ -1,4 +1,4 @@ -import { IMySegmentsUpdateData } from './SSEHandler/types'; +import { IMySegmentsUpdateData, IMySegmentsUpdateV2Data } from './SSEHandler/types'; import { ITask } from '../types'; import { IPollingManager, ISegmentsSyncTask } from '../polling/types'; import { IReadinessManager } from '../../readiness/types'; @@ -15,6 +15,7 @@ export type PUSH_RETRYABLE_ERROR = 'PUSH_RETRYABLE_ERROR' // Update-type push notifications, handled by NotificationProcessor export type MY_SEGMENTS_UPDATE = 'MY_SEGMENTS_UPDATE'; +export type MY_SEGMENTS_UPDATE_V2 = 'MY_SEGMENTS_UPDATE_V2'; export type SEGMENT_UPDATE = 'SEGMENT_UPDATE'; export type SPLIT_KILL = 'SPLIT_KILL'; export type SPLIT_UPDATE = 'SPLIT_UPDATE'; @@ -23,7 +24,9 @@ export type SPLIT_UPDATE = 'SPLIT_UPDATE'; export type CONTROL = 'CONTROL'; export type OCCUPANCY = 'OCCUPANCY'; -export type IPushEvent = PUSH_SUBSYSTEM_UP | PUSH_SUBSYSTEM_DOWN | PUSH_NONRETRYABLE_ERROR | PUSH_RETRYABLE_ERROR | MY_SEGMENTS_UPDATE | SEGMENT_UPDATE | SPLIT_UPDATE | SPLIT_KILL +export type STREAMING_RESET = 'STREAMING_RESET'; + +export type IPushEvent = PUSH_SUBSYSTEM_UP | PUSH_SUBSYSTEM_DOWN | PUSH_NONRETRYABLE_ERROR | PUSH_RETRYABLE_ERROR | MY_SEGMENTS_UPDATE | MY_SEGMENTS_UPDATE_V2 | SEGMENT_UPDATE | SPLIT_UPDATE | SPLIT_KILL | STREAMING_RESET /** * EventEmitter used as Feedback Loop between the SyncManager and PushManager, @@ -32,6 +35,7 @@ export type IPushEvent = PUSH_SUBSYSTEM_UP | PUSH_SUBSYSTEM_DOWN | PUSH_NONRETRY export interface IPushEventEmitter extends IEventEmitter { once(event: T, listener: (...args: T extends MY_SEGMENTS_UPDATE ? [parsedData: IMySegmentsUpdateData, channel: string] : + T extends MY_SEGMENTS_UPDATE_V2 ? [parsedData: IMySegmentsUpdateV2Data] : T extends SEGMENT_UPDATE ? [changeNumber: number, segmentName: string] : T extends SPLIT_UPDATE ? [changeNumber: number] : T extends SPLIT_KILL ? [changeNumber: number, splitName: string, defaultTreatment: string] : @@ -39,6 +43,7 @@ export interface IPushEventEmitter extends IEventEmitter { on(event: T, listener: (...args: T extends MY_SEGMENTS_UPDATE ? [parsedData: IMySegmentsUpdateData, channel: string] : + T extends MY_SEGMENTS_UPDATE_V2 ? [parsedData: IMySegmentsUpdateV2Data] : T extends SEGMENT_UPDATE ? [changeNumber: number, segmentName: string] : T extends SPLIT_UPDATE ? [changeNumber: number] : T extends SPLIT_KILL ? [changeNumber: number, splitName: string, defaultTreatment: string] : @@ -46,6 +51,7 @@ export interface IPushEventEmitter extends IEventEmitter { emit(event: T, ...args: T extends MY_SEGMENTS_UPDATE ? [parsedData: IMySegmentsUpdateData, channel: string] : + T extends MY_SEGMENTS_UPDATE_V2 ? [parsedData: IMySegmentsUpdateV2Data] : T extends SEGMENT_UPDATE ? [changeNumber: number, segmentName: string] : T extends SPLIT_UPDATE ? [changeNumber: number] : T extends SPLIT_KILL ? [changeNumber: number, splitName: string, defaultTreatment: string] : diff --git a/src/trackers/impressionObserver/__tests__/clientSideObserver.spec.ts b/src/trackers/impressionObserver/__tests__/clientSideObserver.spec.ts index 37a177be..c4740a8a 100644 --- a/src/trackers/impressionObserver/__tests__/clientSideObserver.spec.ts +++ b/src/trackers/impressionObserver/__tests__/clientSideObserver.spec.ts @@ -72,9 +72,6 @@ test('Hasher 32 / Impression Hasher Does Not Crash', () => { // @ts-ignore imp1.treatment = null; expect(hashImpression32(imp1)).not.toBe(null); - - // @ts-ignore - expect(hashImpression32(null)).toBe(null); }); test('Client-side (Browser JS) / Impression Observer Basic Functionality', () => { diff --git a/src/trackers/impressionObserver/__tests__/serverSideObserver.spec.ts b/src/trackers/impressionObserver/__tests__/serverSideObserver.spec.ts index 7f6c026b..7f14cfe8 100644 --- a/src/trackers/impressionObserver/__tests__/serverSideObserver.spec.ts +++ b/src/trackers/impressionObserver/__tests__/serverSideObserver.spec.ts @@ -72,9 +72,6 @@ test('Hasher 128 / Impression Hasher Does Not Crash', () => { // @ts-ignore imp1.treatment = null; expect(hashImpression128(imp1)).not.toBe(null); - - // @ts-ignore - expect(hashImpression128(null)).toBe(null); }); test('Server-side (Node JS) / Impression Observer Basic Functionality', () => { diff --git a/src/trackers/impressionObserver/buildKey.ts b/src/trackers/impressionObserver/buildKey.ts index 4267ce0b..8adedb82 100644 --- a/src/trackers/impressionObserver/buildKey.ts +++ b/src/trackers/impressionObserver/buildKey.ts @@ -1,19 +1,5 @@ import { ImpressionDTO } from '../../types'; -const UNKNOWN = 'UNKNOWN'; - -function _unknownIfNull(s: any) { - return s ? s : UNKNOWN; -} - -function _zeroIfNull(l: any) { - return l ? l : 0; -} - -export default function buildKey(impression: ImpressionDTO) { - return `${_unknownIfNull(impression.keyName)} - :${_unknownIfNull(impression.feature)} - :${_unknownIfNull(impression.treatment)} - :${_unknownIfNull(impression.label)} - :${_zeroIfNull(impression.changeNumber)}`; +export function buildKey(impression: ImpressionDTO) { + return `${impression.keyName}:${impression.feature}:${impression.treatment}:${impression.label}:${impression.changeNumber}`; } diff --git a/src/trackers/impressionObserver/impressionObserverCS.ts b/src/trackers/impressionObserver/impressionObserverCS.ts index 168195f9..b6e64c6c 100644 --- a/src/trackers/impressionObserver/impressionObserverCS.ts +++ b/src/trackers/impressionObserver/impressionObserverCS.ts @@ -1,10 +1,10 @@ import ImpressionObserver from './ImpressionObserver'; import { hash } from '../../utils/murmur3/murmur3'; -import buildKey from './buildKey'; +import { buildKey } from './buildKey'; import { ImpressionDTO } from '../../types'; export function hashImpression32(impression: ImpressionDTO) { - return impression ? hash(buildKey(impression)).toString() : null; + return hash(buildKey(impression)).toString(); } const LAST_SEEN_CACHE_SIZE = 500; // cache up to 500 impression hashes diff --git a/src/trackers/impressionObserver/impressionObserverSS.ts b/src/trackers/impressionObserver/impressionObserverSS.ts index 89ecf4c6..d3385c94 100644 --- a/src/trackers/impressionObserver/impressionObserverSS.ts +++ b/src/trackers/impressionObserver/impressionObserverSS.ts @@ -1,10 +1,10 @@ import ImpressionObserver from './ImpressionObserver'; import { hash128 } from '../../utils/murmur3/murmur3_128'; -import buildKey from './buildKey'; +import { buildKey } from './buildKey'; import { ImpressionDTO } from '../../types'; export function hashImpression128(impression: ImpressionDTO) { - return impression ? hash128(buildKey(impression)).toString() : null; + return hash128(buildKey(impression)); } const LAST_SEEN_CACHE_SIZE = 500000; // cache up to 500k impression hashes diff --git a/src/utils/decompress/index.ts b/src/utils/decompress/index.ts new file mode 100644 index 00000000..e0f5c2cd --- /dev/null +++ b/src/utils/decompress/index.ts @@ -0,0 +1,429 @@ +/* +Trimmed version of "fflate" library (https://www.npmjs.com/package/fflate/v/0.7.1) +used for GZIP/Zlib decompression in JavaScript, with the minimal modules used by the SDK. + +MIT License + +Copyright (c) 2020 Arjun Barrett + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ +/* eslint-disable */ +// @ts-nocheck + +export const algorithms = (function iifeDecompress() { + + // Handle runtimes without typed arrays + if ([typeof Uint8Array, typeof Uint16Array, typeof Uint32Array].some(v => v != 'function')) { + return 'global Uint8Array object is not available for gzip/zlib decompression'; + } + + // DEFLATE is a complex format; to read this code, you should probably check the RFC first: + + // aliases for shorter compressed code (most minifers don't do this) + var u8 = Uint8Array, u16 = Uint16Array, u32 = Uint32Array; + // fixed length extra bits + var fleb = new u8([0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0, /* unused */ 0, 0, /* impossible */ 0]); + // fixed distance extra bits + // see fleb note + var fdeb = new u8([0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13, /* unused */ 0, 0]); + // code length index map + var clim = new u8([16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15]); + // get base, reverse index map from extra bits + var freb = function (eb, start) { + var b = new u16(31); + for (var i = 0; i < 31; ++i) { + b[i] = start += 1 << eb[i - 1]; + } + // numbers here are at max 18 bits + var r = new u32(b[30]); + for (var i = 1; i < 30; ++i) { + for (var j = b[i]; j < b[i + 1]; ++j) { + r[j] = ((j - b[i]) << 5) | i; + } + } + return [b, r]; + }; + var _a = freb(fleb, 2), fl = _a[0], revfl = _a[1]; + // we can ignore the fact that the other numbers are wrong; they never happen anyway + fl[28] = 258, revfl[258] = 28; + var _b = freb(fdeb, 0), fd = _b[0]; + // map of value to reverse (assuming 16 bits) + var rev = new u16(32768); + for (var i = 0; i < 32768; ++i) { + // reverse table algorithm from SO + var x = ((i & 0xAAAA) >>> 1) | ((i & 0x5555) << 1); + x = ((x & 0xCCCC) >>> 2) | ((x & 0x3333) << 2); + x = ((x & 0xF0F0) >>> 4) | ((x & 0x0F0F) << 4); + rev[i] = (((x & 0xFF00) >>> 8) | ((x & 0x00FF) << 8)) >>> 1; + } + // create huffman tree from u8 "map": index -> code length for code index + // mb (max bits) must be at most 15 + // TODO: optimize/split up? + var hMap = (function (cd, mb, r) { + var s = cd.length; + // index + var i = 0; + // u16 "map": index -> # of codes with bit length = index + var l = new u16(mb); + // length of cd must be 288 (total # of codes) + for (; i < s; ++i) + ++l[cd[i] - 1]; + // u16 "map": index -> minimum code for bit length = index + var le = new u16(mb); + for (i = 0; i < mb; ++i) { + le[i] = (le[i - 1] + l[i - 1]) << 1; + } + var co; + if (r) { + // u16 "map": index -> number of actual bits, symbol for code + co = new u16(1 << mb); + // bits to remove for reverser + var rvb = 15 - mb; + for (i = 0; i < s; ++i) { + // ignore 0 lengths + if (cd[i]) { + // num encoding both symbol and bits read + var sv = (i << 4) | cd[i]; + // free bits + var r_1 = mb - cd[i]; + // start value + var v = le[cd[i] - 1]++ << r_1; + // m is end value + for (var m = v | ((1 << r_1) - 1); v <= m; ++v) { + // every 16 bit value starting with the code yields the same result + co[rev[v] >>> rvb] = sv; + } + } + } + } + else { + co = new u16(s); + for (i = 0; i < s; ++i) { + if (cd[i]) { + co[i] = rev[le[cd[i] - 1]++] >>> (15 - cd[i]); + } + } + } + return co; + }); + // fixed length tree + var flt = new u8(288); + for (var i = 0; i < 144; ++i) + flt[i] = 8; + for (var i = 144; i < 256; ++i) + flt[i] = 9; + for (var i = 256; i < 280; ++i) + flt[i] = 7; + for (var i = 280; i < 288; ++i) + flt[i] = 8; + // fixed distance tree + var fdt = new u8(32); + for (var i = 0; i < 32; ++i) + fdt[i] = 5; + // fixed length map + var flrm = /*#__PURE__*/ hMap(flt, 9, 1); + // fixed distance map + var fdrm = /*#__PURE__*/ hMap(fdt, 5, 1); + // find max of array + var max = function (a) { + var m = a[0]; + for (var i = 1; i < a.length; ++i) { + if (a[i] > m) + m = a[i]; + } + return m; + }; + // read d, starting at bit p and mask with m + var bits = function (d, p, m) { + var o = (p / 8) | 0; + return ((d[o] | (d[o + 1] << 8)) >> (p & 7)) & m; + }; + // read d, starting at bit p continuing for at least 16 bits + var bits16 = function (d, p) { + var o = (p / 8) | 0; + return ((d[o] | (d[o + 1] << 8) | (d[o + 2] << 16)) >> (p & 7)); + }; + // get end of byte + var shft = function (p) { return ((p + 7) / 8) | 0; }; + // typed array slice - allows garbage collector to free original reference, + // while being more compatible than .slice + var slc = function (v, s, e) { + if (s == null || s < 0) + s = 0; + if (e == null || e > v.length) + e = v.length; + // can't use .constructor in case user-supplied + var n = new (v instanceof u16 ? u16 : v instanceof u32 ? u32 : u8)(e - s); + n.set(v.subarray(s, e)); + return n; + }; + // error codes + var ec = [ + 'unexpected EOF', + 'invalid block type', + 'invalid length/literal', + 'invalid distance', + 'stream finished', + 'no stream handler', + , + 'no callback', + 'invalid UTF-8 data', + 'extra field too long', + 'date not in range 1980-2099', + 'filename too long', + 'stream finishing', + 'invalid zip data' + // determined by unknown compression method + ]; + var err = function (ind, msg, nt) { + var e = new Error(msg || ec[ind]); + e.code = ind; + if (Error.captureStackTrace) + Error.captureStackTrace(e, err); + if (!nt) + throw e; + return e; + }; + // expands raw DEFLATE data + var inflt = function (dat, buf, st) { + // source length + var sl = dat.length; + if (!sl || (st && st.f && !st.l)) + return buf || new u8(0); + // have to estimate size + var noBuf = !buf || st; + // no state + var noSt = !st || st.i; + if (!st) + st = {}; + // Assumes roughly 33% compression ratio average + if (!buf) + buf = new u8(sl * 3); + // ensure buffer can fit at least l elements + var cbuf = function (l) { + var bl = buf.length; + // need to increase size to fit + if (l > bl) { + // Double or set to necessary, whichever is greater + var nbuf = new u8(Math.max(bl * 2, l)); + nbuf.set(buf); + buf = nbuf; + } + }; + // last chunk bitpos bytes + var final = st.f || 0, pos = st.p || 0, bt = st.b || 0, lm = st.l, dm = st.d, lbt = st.m, dbt = st.n; + // total bits + var tbts = sl * 8; + do { + if (!lm) { + // BFINAL - this is only 1 when last chunk is next + final = bits(dat, pos, 1); + // type: 0 = no compression, 1 = fixed huffman, 2 = dynamic huffman + var type = bits(dat, pos + 1, 3); + pos += 3; + if (!type) { + // go to end of byte boundary + var s = shft(pos) + 4, l = dat[s - 4] | (dat[s - 3] << 8), t = s + l; + if (t > sl) { + if (noSt) + err(0); + break; + } + // ensure size + if (noBuf) + cbuf(bt + l); + // Copy over uncompressed data + buf.set(dat.subarray(s, t), bt); + // Get new bitpos, update byte count + st.b = bt += l, st.p = pos = t * 8, st.f = final; + continue; + } + else if (type == 1) + lm = flrm, dm = fdrm, lbt = 9, dbt = 5; + else if (type == 2) { + // literal lengths + var hLit = bits(dat, pos, 31) + 257, hcLen = bits(dat, pos + 10, 15) + 4; + var tl = hLit + bits(dat, pos + 5, 31) + 1; + pos += 14; + // length+distance tree + var ldt = new u8(tl); + // code length tree + var clt = new u8(19); + for (var i = 0; i < hcLen; ++i) { + // use index map to get real code + clt[clim[i]] = bits(dat, pos + i * 3, 7); + } + pos += hcLen * 3; + // code lengths bits + var clb = max(clt), clbmsk = (1 << clb) - 1; + // code lengths map + var clm = hMap(clt, clb, 1); + for (var i = 0; i < tl;) { + var r = clm[bits(dat, pos, clbmsk)]; + // bits read + pos += r & 15; + // symbol + var s = r >>> 4; + // code length to copy + if (s < 16) { + ldt[i++] = s; + } + else { + // copy count + var c = 0, n = 0; + if (s == 16) + n = 3 + bits(dat, pos, 3), pos += 2, c = ldt[i - 1]; + else if (s == 17) + n = 3 + bits(dat, pos, 7), pos += 3; + else if (s == 18) + n = 11 + bits(dat, pos, 127), pos += 7; + while (n--) + ldt[i++] = c; + } + } + // length tree distance tree + var lt = ldt.subarray(0, hLit), dt = ldt.subarray(hLit); + // max length bits + lbt = max(lt); + // max dist bits + dbt = max(dt); + lm = hMap(lt, lbt, 1); + dm = hMap(dt, dbt, 1); + } + else + err(1); + if (pos > tbts) { + if (noSt) + err(0); + break; + } + } + // Make sure the buffer can hold this + the largest possible addition + // Maximum chunk size (practically, theoretically infinite) is 2^17; + if (noBuf) + cbuf(bt + 131072); + var lms = (1 << lbt) - 1, dms = (1 << dbt) - 1; + var lpos = pos; + for (; ; lpos = pos) { + // bits read, code + var c = lm[bits16(dat, pos) & lms], sym = c >>> 4; + pos += c & 15; + if (pos > tbts) { + if (noSt) + err(0); + break; + } + if (!c) + err(2); + if (sym < 256) + buf[bt++] = sym; + else if (sym == 256) { + lpos = pos, lm = null; + break; + } + else { + var add = sym - 254; + // no extra bits needed if less + if (sym > 264) { + // index + var i = sym - 257, b = fleb[i]; + add = bits(dat, pos, (1 << b) - 1) + fl[i]; + pos += b; + } + // dist + var d = dm[bits16(dat, pos) & dms], dsym = d >>> 4; + if (!d) + err(3); + pos += d & 15; + var dt = fd[dsym]; + if (dsym > 3) { + var b = fdeb[dsym]; + dt += bits16(dat, pos) & ((1 << b) - 1), pos += b; + } + if (pos > tbts) { + if (noSt) + err(0); + break; + } + if (noBuf) + cbuf(bt + 131072); + var end = bt + add; + for (; bt < end; bt += 4) { + buf[bt] = buf[bt - dt]; + buf[bt + 1] = buf[bt + 1 - dt]; + buf[bt + 2] = buf[bt + 2 - dt]; + buf[bt + 3] = buf[bt + 3 - dt]; + } + bt = end; + } + } + st.l = lm, st.p = lpos, st.b = bt, st.f = final; + if (lm) + final = 1, st.m = lbt, st.d = dm, st.n = dbt; + } while (!final); + return bt == buf.length ? buf : slc(buf, 0, bt); + }; + // gzip footer: -8 to -4 = CRC, -4 to -0 is length + // gzip start + var gzs = function (d) { + if (d[0] != 31 || d[1] != 139 || d[2] != 8) + err(6, 'invalid gzip data'); + var flg = d[3]; + var st = 10; + if (flg & 4) + st += d[10] | (d[11] << 8) + 2; + for (var zs = (flg >> 3 & 1) + (flg >> 4 & 1); zs > 0; zs -= !d[st++]) + ; + return st + (flg & 2); + }; + // gzip length + var gzl = function (d) { + var l = d.length; + return ((d[l - 4] | d[l - 3] << 8 | d[l - 2] << 16) | (d[l - 1] << 24)) >>> 0; + }; + // zlib valid + var zlv = function (d) { + if ((d[0] & 15) != 8 || (d[0] >>> 4) > 7 || ((d[0] << 8 | d[1]) % 31)) + err(6, 'invalid zlib data'); + if (d[1] & 32) + err(6, 'invalid zlib data: preset dictionaries not supported'); + }; + + return { + /** + * Expands GZIP data + * @param data The data to decompress + * @param out Where to write the data. GZIP already encodes the output size, so providing this doesn't save memory. + * @returns The decompressed version of the data + */ + gunzipSync(data: Uint8Array, out?: Uint8Array): Uint8Array { + return inflt(data.subarray(gzs(data), -8), out || new u8(gzl(data))); + }, + /** + * Expands Zlib data + * @param data The data to decompress + * @param out Where to write the data. Saves memory if you know the decompressed size and provide an output buffer of that length. + * @returns The decompressed version of the data + */ + unzlibSync(data: Uint8Array, out?: Uint8Array): Uint8Array { + return inflt((zlv(data), data.subarray(2, -4)), out); + } + } +})(); diff --git a/src/utils/murmur3/__tests__/murmur3.spec.ts b/src/utils/murmur3/__tests__/murmur3.spec.ts index 2246b450..1200cb9e 100644 --- a/src/utils/murmur3/__tests__/murmur3.spec.ts +++ b/src/utils/murmur3/__tests__/murmur3.spec.ts @@ -1,6 +1,8 @@ import fs from 'fs'; import rl from 'readline'; import * as murmur3 from '../murmur3'; +import { hash128 } from '../murmur3_128'; +import { hash64 } from '../murmur3_64'; [ 'murmur3-sample-v4.csv', @@ -35,3 +37,63 @@ import * as murmur3 from '../murmur3'; }, 30000); // timeout of 30 seconds, since this test can take more time when reporting test coverage }); + +function dec2hex(str: any) { + let sum = []; + const dec = str.toString().split(''); + const hex = []; + while (dec.length) { + let s = 1 * dec.shift(); + for (let i = 0; s || i < sum.length; i++) { + s += (sum[i] || 0) * 10; + sum[i] = s % 16; + s = (s - sum[i]) / 16; + } + } + while (sum.length) { + hex.push((sum.pop() as any).toString(16)); + } + return hex.join(''); +} + +[ + 'murmur3_64_uuids.csv', +].forEach(filename => { + + test('MURMUR3 128 / validate hashing behavior using sample data', (done) => { + const parser = rl.createInterface({ + terminal: false, + input: fs.createReadStream(require.resolve(`./mocks/${filename}`)) + }); + + parser + .on('line', line => { + const parts = line.split(','); + + if (parts.length === 3) { + let key = parts[0]; + let seed = parseInt(parts[1], 10); + let hash = parts[2]; + const result = hash128(key, seed); + + expect(result.substring(0, 16)).toBe(dec2hex(hash).padStart(16, '0')); + } + }) + .on('close', done); + }, 30000); // timeout of 30 seconds, since this test can take more time when reporting test coverage +}); + +test('MURMUR3 128 higher 64 bits', () => { + + [ + ['key1', { hex: '15d67461d2044fb3', dec: '1573573083296714675' }], + ['key2', { hex: '75b93494ef690e31', dec: '8482869187405483569' }], + ['key3', { hex: '6f76f1df6ac38fea', dec: '8031872927333060586' }], + ['key4', { hex: '5ec727b58617b474', dec: '6829471020522910836' }], + ['key5', { hex: 'b07087d10f0143b8', dec: '12713811080036565944' }], + ['key6', { hex: 'ddcd11333e54c85c', dec: '15982449564394506332' }], + ['', { hex: '0000000000000000', dec: '0' }], + ].forEach(([key, hash]) => { + expect(hash64(key as string)).toEqual(hash); + }); +}); diff --git a/src/utils/murmur3/__tests__/murmur3_128.spec.ts b/src/utils/murmur3/__tests__/murmur3_128.spec.ts deleted file mode 100644 index dc8bed45..00000000 --- a/src/utils/murmur3/__tests__/murmur3_128.spec.ts +++ /dev/null @@ -1,48 +0,0 @@ -import fs from 'fs'; -import rl from 'readline'; -import { hash128 } from '../murmur3_128'; - -function dec2hex(str: any) { - let sum = []; - const dec = str.toString().split(''); - const hex = []; - while (dec.length) { - let s = 1 * dec.shift(); - for (let i = 0; s || i < sum.length; i++) { - s += (sum[i] || 0) * 10; - sum[i] = s % 16; - s = (s - sum[i]) / 16; - } - } - while (sum.length) { - hex.push((sum.pop() as any).toString(16)); - } - return hex.join(''); -} - -[ - 'murmur3_64_uuids.csv', -].forEach(filename => { - - test('MURMUR3 128 / validate hashing behavior using sample data', (done) => { - const parser = rl.createInterface({ - terminal: false, - input: fs.createReadStream(require.resolve(`./mocks/${filename}`)) - }); - - parser - .on('line', line => { - const parts = line.split(','); - - if (parts.length === 3) { - let key = parts[0]; - let seed = parseInt(parts[1], 10); - let hash = parts[2]; - const result = hash128(key, seed); - - expect(result.substring(0, 16)).toBe(dec2hex(hash).padStart(16, '0')); - } - }) - .on('close', done); - }, 30000); // timeout of 30 seconds, since this test can take more time when reporting test coverage -}); diff --git a/src/utils/murmur3/commons.ts b/src/utils/murmur3/common.ts similarity index 89% rename from src/utils/murmur3/commons.ts rename to src/utils/murmur3/common.ts index 2b733301..03a4363b 100644 --- a/src/utils/murmur3/commons.ts +++ b/src/utils/murmur3/common.ts @@ -1,8 +1,4 @@ -import utfx from './utfx'; - -const stringSource = utfx.stringSource; -const stringDestination = utfx.stringDestination; -const encodeUTF16toUTF8 = utfx.encodeUTF16toUTF8; +import { stringSource, stringDestination, encodeUTF16toUTF8 } from './utfx'; export function UTF16ToUTF8(key: string) { let sd; diff --git a/src/utils/murmur3/murmur3.ts b/src/utils/murmur3/murmur3.ts index 434fcbd3..ea6ed6fd 100644 --- a/src/utils/murmur3/murmur3.ts +++ b/src/utils/murmur3/murmur3.ts @@ -1,5 +1,5 @@ /* eslint-disable no-fallthrough */ -import { UTF16ToUTF8, x86Fmix, x86Multiply, x86Rotl } from './commons'; +import { UTF16ToUTF8, x86Fmix, x86Multiply, x86Rotl } from './common'; /* * +----------------------------------------------------------------------------------+ @@ -65,11 +65,11 @@ function hash32(key?: string, seed?: number) { return h1 >>> 0; } -export function hash(str?: string, seed?: number): number { - // @ts-ignore - return hash32(UTF16ToUTF8(str), seed >>> 0); +export function hash(str: string, seed?: number): number { + + return hash32(UTF16ToUTF8(str), seed as number >>> 0); } -export function bucket(str?: string, seed?: number): number { +export function bucket(str: string, seed?: number): number { return Math.abs(hash(str, seed) % 100) + 1; } diff --git a/src/utils/murmur3/murmur3_128.ts b/src/utils/murmur3/murmur3_128.ts index 18140e29..1a719c82 100644 --- a/src/utils/murmur3/murmur3_128.ts +++ b/src/utils/murmur3/murmur3_128.ts @@ -1,10 +1,5 @@ /* eslint-disable no-fallthrough */ -import { UTF16ToUTF8, x86Fmix, x86Multiply, x86Rotl } from './commons'; - -const X64 = 'x64'; -const X86 = 'x86'; -const X64_ARCHS = ['arm64', 'ppc64', 'x64', 's390x', 'mipsel']; -const isX64 = getArchType() === X64; +import { UTF16ToUTF8 } from './common'; /* * +----------------------------------------------------------------------------------+ @@ -149,171 +144,6 @@ function _x64Fmix(h: number[]): number[] { // PUBLIC FUNCTIONS // ---------------- -function hash128x86(key?: string, seed?: number) { - // - // Given a string and an optional seed as an int, returns a 128 bit - // hash using the x86 flavor of MurmurHash3, as an unsigned hex. - // - - key = key || ''; - seed = seed || 0; - - var remainder = key.length % 16; - var bytes = key.length - remainder; - - var h1 = seed; - var h2 = seed; - var h3 = seed; - var h4 = seed; - - var k1 = 0; - var k2 = 0; - var k3 = 0; - var k4 = 0; - - var c1 = 0x239b961b; - var c2 = 0xab0e9789; - var c3 = 0x38b34ae5; - var c4 = 0xa1e38b93; - - for (var i = 0; i < bytes; i = i + 16) { - k1 = ((key.charCodeAt(i) & 0xff)) | ((key.charCodeAt(i + 1) & 0xff) << 8) | ((key.charCodeAt(i + 2) & 0xff) << 16) | ((key.charCodeAt(i + 3) & 0xff) << 24); - k2 = ((key.charCodeAt(i + 4) & 0xff)) | ((key.charCodeAt(i + 5) & 0xff) << 8) | ((key.charCodeAt(i + 6) & 0xff) << 16) | ((key.charCodeAt(i + 7) & 0xff) << 24); - k3 = ((key.charCodeAt(i + 8) & 0xff)) | ((key.charCodeAt(i + 9) & 0xff) << 8) | ((key.charCodeAt(i + 10) & 0xff) << 16) | ((key.charCodeAt(i + 11) & 0xff) << 24); - k4 = ((key.charCodeAt(i + 12) & 0xff)) | ((key.charCodeAt(i + 13) & 0xff) << 8) | ((key.charCodeAt(i + 14) & 0xff) << 16) | ((key.charCodeAt(i + 15) & 0xff) << 24); - - k1 = x86Multiply(k1, c1); - k1 = x86Rotl(k1, 15); - k1 = x86Multiply(k1, c2); - h1 ^= k1; - - h1 = x86Rotl(h1, 19); - h1 += h2; - h1 = x86Multiply(h1, 5) + 0x561ccd1b; - - k2 = x86Multiply(k2, c2); - k2 = x86Rotl(k2, 16); - k2 = x86Multiply(k2, c3); - h2 ^= k2; - - h2 = x86Rotl(h2, 17); - h2 += h3; - h2 = x86Multiply(h2, 5) + 0x0bcaa747; - - k3 = x86Multiply(k3, c3); - k3 = x86Rotl(k3, 17); - k3 = x86Multiply(k3, c4); - h3 ^= k3; - - h3 = x86Rotl(h3, 15); - h3 += h4; - h3 = x86Multiply(h3, 5) + 0x96cd1c35; - - k4 = x86Multiply(k4, c4); - k4 = x86Rotl(k4, 18); - k4 = x86Multiply(k4, c1); - h4 ^= k4; - - h4 = x86Rotl(h4, 13); - h4 += h1; - h4 = x86Multiply(h4, 5) + 0x32ac3b17; - } - - k1 = 0; - k2 = 0; - k3 = 0; - k4 = 0; - - switch (remainder) { - case 15: - k4 ^= key.charCodeAt(i + 14) << 16; - - case 14: - k4 ^= key.charCodeAt(i + 13) << 8; - - case 13: - k4 ^= key.charCodeAt(i + 12); - k4 = x86Multiply(k4, c4); - k4 = x86Rotl(k4, 18); - k4 = x86Multiply(k4, c1); - h4 ^= k4; - - case 12: - k3 ^= key.charCodeAt(i + 11) << 24; - - case 11: - k3 ^= key.charCodeAt(i + 10) << 16; - - case 10: - k3 ^= key.charCodeAt(i + 9) << 8; - - case 9: - k3 ^= key.charCodeAt(i + 8); - k3 = x86Multiply(k3, c3); - k3 = x86Rotl(k3, 17); - k3 = x86Multiply(k3, c4); - h3 ^= k3; - - case 8: - k2 ^= key.charCodeAt(i + 7) << 24; - - case 7: - k2 ^= key.charCodeAt(i + 6) << 16; - - case 6: - k2 ^= key.charCodeAt(i + 5) << 8; - - case 5: - k2 ^= key.charCodeAt(i + 4); - k2 = x86Multiply(k2, c2); - k2 = x86Rotl(k2, 16); - k2 = x86Multiply(k2, c3); - h2 ^= k2; - - case 4: - k1 ^= key.charCodeAt(i + 3) << 24; - - case 3: - k1 ^= key.charCodeAt(i + 2) << 16; - - case 2: - k1 ^= key.charCodeAt(i + 1) << 8; - - case 1: - k1 ^= key.charCodeAt(i); - k1 = x86Multiply(k1, c1); - k1 = x86Rotl(k1, 15); - k1 = x86Multiply(k1, c2); - h1 ^= k1; - } - - h1 ^= key.length; - h2 ^= key.length; - h3 ^= key.length; - h4 ^= key.length; - - h1 += h2; - h1 += h3; - h1 += h4; - h2 += h1; - h3 += h1; - h4 += h1; - - h1 = x86Fmix(h1); - h2 = x86Fmix(h2); - h3 = x86Fmix(h3); - h4 = x86Fmix(h4); - - h1 += h2; - h1 += h3; - h1 += h4; - h2 += h1; - h3 += h1; - h4 += h1; - - return ('00000000' + (h1 >>> 0).toString(16)).slice(-8) + ('00000000' + (h2 >>> 0).toString(16)).slice(-8) + ('00000000' + (h3 >>> 0).toString(16)).slice(-8) + ('00000000' + (h4 >>> 0).toString(16)).slice(-8); -} - function hash128x64(key?: string, seed?: number) { // // Given a string and an optional seed as an int, returns a 128 bit @@ -433,14 +263,11 @@ function hash128x64(key?: string, seed?: number) { return ('00000000' + (h1[0] >>> 0).toString(16)).slice(-8) + ('00000000' + (h1[1] >>> 0).toString(16)).slice(-8) + ('00000000' + (h2[0] >>> 0).toString(16)).slice(-8) + ('00000000' + (h2[1] >>> 0).toString(16)).slice(-8); } -function getArchType() { - // Values listed from https://nodejs.org/api/process.html#process_process_arch - // @TODO Review when supporting alternatives of Node - // eslint-disable-next-line no-undef - const arch = typeof process !== 'undefined' && process.arch ? process.arch : X86; - return (X64_ARCHS.indexOf(arch) > -1) ? X64 : X86; -} - +/** + * x64 version of Murmur3 for 128bits. + * Used in hashImpression128. + * @param {string} str + */ export function hash128(str: string, seed?: number): string { - return isX64 ? hash128x64(UTF16ToUTF8(str), (seed as number) >>> 0) : hash128x86(UTF16ToUTF8(str), (seed as number) >>> 0); + return hash128x64(UTF16ToUTF8(str), (seed as number) >>> 0); } diff --git a/src/utils/murmur3/murmur3_128_x86.ts b/src/utils/murmur3/murmur3_128_x86.ts new file mode 100644 index 00000000..cda04228 --- /dev/null +++ b/src/utils/murmur3/murmur3_128_x86.ts @@ -0,0 +1,189 @@ +/* eslint-disable no-fallthrough */ +import { UTF16ToUTF8, x86Fmix, x86Multiply, x86Rotl } from './common'; + +/*! + * +----------------------------------------------------------------------------------+ + * | murmurHash3.js v3.0.0 (https://github.com/karanlyons/murmurHash3.js) | + * | A TypeScript/JavaScript implementation of MurmurHash3's hashing algorithms. | + * |----------------------------------------------------------------------------------| + * | Copyright (c) 2012-2020 Karan Lyons. Freely distributable under the MIT license. | + * +----------------------------------------------------------------------------------+ + */ + +// PUBLIC FUNCTIONS +// ---------------- +function hash128x86(key?: string, seed?: number): string { + // + // Given a string and an optional seed as an int, returns a 128 bit + // hash using the x86 flavor of MurmurHash3, as an unsigned hex. + // + + key = key || ''; + seed = seed || 0; + + var remainder = key.length % 16; + var bytes = key.length - remainder; + + var h1 = seed; + var h2 = seed; + var h3 = seed; + var h4 = seed; + + var k1 = 0; + var k2 = 0; + var k3 = 0; + var k4 = 0; + + var c1 = 0x239b961b; + var c2 = 0xab0e9789; + var c3 = 0x38b34ae5; + var c4 = 0xa1e38b93; + + for (var i = 0; i < bytes; i = i + 16) { + k1 = ((key.charCodeAt(i) & 0xff)) | ((key.charCodeAt(i + 1) & 0xff) << 8) | ((key.charCodeAt(i + 2) & 0xff) << 16) | ((key.charCodeAt(i + 3) & 0xff) << 24); + k2 = ((key.charCodeAt(i + 4) & 0xff)) | ((key.charCodeAt(i + 5) & 0xff) << 8) | ((key.charCodeAt(i + 6) & 0xff) << 16) | ((key.charCodeAt(i + 7) & 0xff) << 24); + k3 = ((key.charCodeAt(i + 8) & 0xff)) | ((key.charCodeAt(i + 9) & 0xff) << 8) | ((key.charCodeAt(i + 10) & 0xff) << 16) | ((key.charCodeAt(i + 11) & 0xff) << 24); + k4 = ((key.charCodeAt(i + 12) & 0xff)) | ((key.charCodeAt(i + 13) & 0xff) << 8) | ((key.charCodeAt(i + 14) & 0xff) << 16) | ((key.charCodeAt(i + 15) & 0xff) << 24); + + k1 = x86Multiply(k1, c1); + k1 = x86Rotl(k1, 15); + k1 = x86Multiply(k1, c2); + h1 ^= k1; + + h1 = x86Rotl(h1, 19); + h1 += h2; + h1 = x86Multiply(h1, 5) + 0x561ccd1b; + + k2 = x86Multiply(k2, c2); + k2 = x86Rotl(k2, 16); + k2 = x86Multiply(k2, c3); + h2 ^= k2; + + h2 = x86Rotl(h2, 17); + h2 += h3; + h2 = x86Multiply(h2, 5) + 0x0bcaa747; + + k3 = x86Multiply(k3, c3); + k3 = x86Rotl(k3, 17); + k3 = x86Multiply(k3, c4); + h3 ^= k3; + + h3 = x86Rotl(h3, 15); + h3 += h4; + h3 = x86Multiply(h3, 5) + 0x96cd1c35; + + k4 = x86Multiply(k4, c4); + k4 = x86Rotl(k4, 18); + k4 = x86Multiply(k4, c1); + h4 ^= k4; + + h4 = x86Rotl(h4, 13); + h4 += h1; + h4 = x86Multiply(h4, 5) + 0x32ac3b17; + } + + k1 = 0; + k2 = 0; + k3 = 0; + k4 = 0; + + switch (remainder) { + case 15: + k4 ^= key.charCodeAt(i + 14) << 16; + + case 14: + k4 ^= key.charCodeAt(i + 13) << 8; + + case 13: + k4 ^= key.charCodeAt(i + 12); + k4 = x86Multiply(k4, c4); + k4 = x86Rotl(k4, 18); + k4 = x86Multiply(k4, c1); + h4 ^= k4; + + case 12: + k3 ^= key.charCodeAt(i + 11) << 24; + + case 11: + k3 ^= key.charCodeAt(i + 10) << 16; + + case 10: + k3 ^= key.charCodeAt(i + 9) << 8; + + case 9: + k3 ^= key.charCodeAt(i + 8); + k3 = x86Multiply(k3, c3); + k3 = x86Rotl(k3, 17); + k3 = x86Multiply(k3, c4); + h3 ^= k3; + + case 8: + k2 ^= key.charCodeAt(i + 7) << 24; + + case 7: + k2 ^= key.charCodeAt(i + 6) << 16; + + case 6: + k2 ^= key.charCodeAt(i + 5) << 8; + + case 5: + k2 ^= key.charCodeAt(i + 4); + k2 = x86Multiply(k2, c2); + k2 = x86Rotl(k2, 16); + k2 = x86Multiply(k2, c3); + h2 ^= k2; + + case 4: + k1 ^= key.charCodeAt(i + 3) << 24; + + case 3: + k1 ^= key.charCodeAt(i + 2) << 16; + + case 2: + k1 ^= key.charCodeAt(i + 1) << 8; + + case 1: + k1 ^= key.charCodeAt(i); + k1 = x86Multiply(k1, c1); + k1 = x86Rotl(k1, 15); + k1 = x86Multiply(k1, c2); + h1 ^= k1; + } + + h1 ^= key.length; + h2 ^= key.length; + h3 ^= key.length; + h4 ^= key.length; + + h1 += h2; + h1 += h3; + h1 += h4; + h2 += h1; + h3 += h1; + h4 += h1; + + h1 = x86Fmix(h1); + h2 = x86Fmix(h2); + h3 = x86Fmix(h3); + h4 = x86Fmix(h4); + + h1 += h2; + h1 += h3; + h1 += h4; + h2 += h1; + h3 += h1; + h4 += h1; + + return ('00000000' + (h1 >>> 0).toString(16)).slice(-8) + ('00000000' + (h2 >>> 0).toString(16)).slice(-8) + ('00000000' + (h3 >>> 0).toString(16)).slice(-8) + ('00000000' + (h4 >>> 0).toString(16)).slice(-8); +} + +/** + * x86 version of Murmur3 for 128bits. + * @param {string} str + * + * @TODO Test and use in hashImpression128 instead of the x64 version, + * because it is more time efficient in JS, no matter the underlying CPU arch. + */ +export function hash128(str: string, seed?: number): string { + return hash128x86(UTF16ToUTF8(str), (seed as number) >>> 0); +} diff --git a/src/utils/murmur3/murmur3_64.ts b/src/utils/murmur3/murmur3_64.ts new file mode 100644 index 00000000..aba873c7 --- /dev/null +++ b/src/utils/murmur3/murmur3_64.ts @@ -0,0 +1,36 @@ +import { hash128 } from './murmur3_128'; + +export type Hash64 = { hex: string, dec: string }; + +/** + * Use instead of parseInt, to not lose precision when converting big integers (greater than 2^53 - 1) + */ +function hex2dec(s: string): string { + let i, j, digits = [0], carry; + for (i = 0; i < s.length; i += 1) { + carry = parseInt(s.charAt(i), 16); + for (j = digits.length - 1; j >= 0; j -= 1) { + digits[j] = digits[j] * 16 + carry; + carry = digits[j] / 10 | 0; + digits[j] %= 10; + } + while (carry > 0) { + digits.unshift(carry % 10); + carry = carry / 10 | 0; + } + } + return digits.join(''); +} + +/** + * Gets the higher 64 bits of the x64 version of Murmur3 for 128bits, as decimal and hexadecimal number strings. + * Used for MySegments channel V2 notifications. + * @param {string} str + */ +export function hash64(str: string): Hash64 { + const hex = hash128(str).slice(0, 16); + return { + hex, // BoundedFetchRequest notification + dec: hex2dec(hex) // KeyList notification + }; +} diff --git a/src/utils/murmur3/utfx.ts b/src/utils/murmur3/utfx.ts index 81a60c21..9bc44270 100644 --- a/src/utils/murmur3/utfx.ts +++ b/src/utils/murmur3/utfx.ts @@ -8,128 +8,110 @@ */ export interface utfx { - stringSource(key: string): () => number | null, - stringDestination(): (...args: number[]) => string | undefined, encodeUTF16toUTF8(src: () => number | null, dst: (...args: number[]) => string | undefined): void, - // Internal use - UTF16toUTF8(src: () => number | null, dst: (cp: number) => void): void, - encodeUTF8(src: (() => number | null) | number, dst: (cp: number) => void): void } -export default (function () { - /** - * utfx namespace. - * @exports utfx - * @type {!Object.} - */ - // @ts-ignore - var utfx: utfx = {}; - - /** - * Encodes UTF8 code points to UTF8 bytes. - * @param {(!function():number|null) | number} src Code points source, either as a function returning the next code point - * respectively `null` if there are no more code points left or a single numeric code point. - * @param {!function(number)} dst Bytes destination as a function successively called with the next byte - * @expose - */ - utfx.encodeUTF8 = function (src, dst) { - var cp = null; - if (typeof src === 'number') - cp = src, src = function () { return null; }; - while (cp !== null || (cp = src()) !== null) { - if (cp < 0x80) - dst(cp & 0x7F); - else if (cp < 0x800) - dst(((cp >> 6) & 0x1F) | 0xC0), dst((cp & 0x3F) | 0x80); - else if (cp < 0x10000) - dst(((cp >> 12) & 0x0F) | 0xE0), dst(((cp >> 6) & 0x3F) | 0x80), dst((cp & 0x3F) | 0x80); - else - dst(((cp >> 18) & 0x07) | 0xF0), dst(((cp >> 12) & 0x3F) | 0x80), dst(((cp >> 6) & 0x3F) | 0x80), dst((cp & 0x3F) | 0x80); - cp = null; - } - }; +/** + * Encodes UTF8 code points to UTF8 bytes. + * @param {(!function():number|null) | number} src Code points source, either as a function returning the next code point + * respectively `null` if there are no more code points left or a single numeric code point. + * @param {!function(number)} dst Bytes destination as a function successively called with the next byte + * @expose + */ +function encodeUTF8(src: (() => number | null) | number, dst: (cp: number) => void): void { + var cp = null; + if (typeof src === 'number') + cp = src, src = function () { return null; }; + while (cp !== null || (cp = src()) !== null) { + if (cp < 0x80) + dst(cp & 0x7F); + else if (cp < 0x800) + dst(((cp >> 6) & 0x1F) | 0xC0), dst((cp & 0x3F) | 0x80); + else if (cp < 0x10000) + dst(((cp >> 12) & 0x0F) | 0xE0), dst(((cp >> 6) & 0x3F) | 0x80), dst((cp & 0x3F) | 0x80); + else + dst(((cp >> 18) & 0x07) | 0xF0), dst(((cp >> 12) & 0x3F) | 0x80), dst(((cp >> 6) & 0x3F) | 0x80), dst((cp & 0x3F) | 0x80); + cp = null; + } +} - /** - * Converts UTF16 characters to UTF8 code points. - * @param {!function():number|null} src Characters source as a function returning the next char code respectively - * `null` if there are no more characters left. - * @param {!function(number)} dst Code points destination as a function successively called with each converted code - * point. - * @expose - */ - utfx.UTF16toUTF8 = function (src, dst) { - var c1, c2 = null; - // eslint-disable-next-line no-constant-condition - while (true) { - if ((c1 = c2 !== null ? c2 : src()) === null) - break; - if (c1 >= 0xD800 && c1 <= 0xDFFF) { - if ((c2 = src()) !== null) { - if (c2 >= 0xDC00 && c2 <= 0xDFFF) { - dst((c1 - 0xD800) * 0x400 + c2 - 0xDC00 + 0x10000); - c2 = null; continue; - } +/** + * Converts UTF16 characters to UTF8 code points. + * @param {!function():number|null} src Characters source as a function returning the next char code respectively + * `null` if there are no more characters left. + * @param {!function(number)} dst Code points destination as a function successively called with each converted code + * point. + * @expose + */ +function UTF16toUTF8(src: () => number | null, dst: (cp: number) => void): void { + var c1, c2 = null; + // eslint-disable-next-line no-constant-condition + while (true) { + if ((c1 = c2 !== null ? c2 : src()) === null) + break; + if (c1 >= 0xD800 && c1 <= 0xDFFF) { + if ((c2 = src()) !== null) { + if (c2 >= 0xDC00 && c2 <= 0xDFFF) { + dst((c1 - 0xD800) * 0x400 + c2 - 0xDC00 + 0x10000); + c2 = null; continue; } } - dst(c1); } - if (c2 !== null) dst(c2); - }; + dst(c1); + } + if (c2 !== null) dst(c2); +} - /** - * Converts and encodes UTF16 characters to UTF8 bytes. - * @param {!function():number|null} src Characters source as a function returning the next char code respectively `null` - * if there are no more characters left. - * @param {!function(number)} dst Bytes destination as a function successively called with the next byte. - * @expose - */ - utfx.encodeUTF16toUTF8 = function (src, dst) { - utfx.UTF16toUTF8(src, function (cp) { - utfx.encodeUTF8(cp, dst); - }); - }; +/** + * Converts and encodes UTF16 characters to UTF8 bytes. + * @param {!function():number|null} src Characters source as a function returning the next char code respectively `null` + * if there are no more characters left. + * @param {!function(number)} dst Bytes destination as a function successively called with the next byte. + * @expose + */ +export function encodeUTF16toUTF8(src: () => number | null, dst: (...args: number[]) => string | undefined): void { + UTF16toUTF8(src, function (cp) { + encodeUTF8(cp, dst); + }); +} - /** - * String.fromCharCode reference for compile time renaming. - * @type {!function(...[number]):string} - * @inner - */ - var stringFromCharCode = String.fromCharCode; +/** + * String.fromCharCode reference for compile time renaming. + * @type {!function(...[number]):string} + * @inner + */ +var stringFromCharCode = String.fromCharCode; - /** - * Creates a source function for a string. - * @param {string} s String to read from - * @returns {!function():number|null} Source function returning the next char code respectively `null` if there are - * no more characters left. - * @throws {TypeError} If the argument is invalid - * @expose - */ - utfx.stringSource = function (s) { - if (typeof s !== 'string') - throw TypeError('Illegal argument: ' + (typeof s)); - var i = 0; return function () { - return i >= s.length ? null : s.charCodeAt(i++); - }; +/** + * Creates a source function for a string. + * @param {string} s String to read from + * @returns {!function():number|null} Source function returning the next char code respectively `null` if there are + * no more characters left. + * @throws {TypeError} If the argument is invalid + * @expose + */ +export function stringSource(s: string): () => number | null { + if (typeof s !== 'string') + throw TypeError('Illegal argument: ' + (typeof s)); + var i = 0; return function () { + return i >= s.length ? null : s.charCodeAt(i++); }; +} - /** - * Creates a destination function for a string. - * @returns {function(number=):undefined|string} Destination function successively called with the next char code. - * Returns the final string when called without arguments. - * @expose - */ - utfx.stringDestination = function () { - const cs: number[] = [], ps: string[] = []; return function () { - if (arguments.length === 0) - return ps.join('') + stringFromCharCode.apply(String, cs); - if (cs.length + arguments.length > 1024) - ps.push(stringFromCharCode.apply(String, cs)), cs.length = 0; // @ts-ignore - Array.prototype.push.apply(cs, arguments); - }; +/** + * Creates a destination function for a string. + * @returns {function(number=):undefined|string} Destination function successively called with the next char code. + * Returns the final string when called without arguments. + * @expose + */ +export function stringDestination(): (...args: number[]) => string | undefined { + const cs: number[] = [], ps: string[] = []; return function () { + if (arguments.length === 0) + return ps.join('') + stringFromCharCode.apply(String, cs); + if (cs.length + arguments.length > 1024) + ps.push(stringFromCharCode.apply(String, cs)), cs.length = 0; // @ts-ignore + Array.prototype.push.apply(cs, arguments); }; - - return utfx; - -})(); +} diff --git a/src/utils/settingsValidation/url.ts b/src/utils/settingsValidation/url.ts index 0be606b8..796f5423 100644 --- a/src/utils/settingsValidation/url.ts +++ b/src/utils/settingsValidation/url.ts @@ -1,7 +1,7 @@ import { ISettings } from '../../types'; const eventsEndpointMatcher = /^\/(testImpressions|metrics|events)/; -const authEndpointMatcher = /^\/auth/; +const authEndpointMatcher = /^\/v2\/auth/; const streamingEndpointMatcher = /^\/(sse|event-stream)/; /** From b2324a48a81586da0eb486eb3fb1b8bbc0530daa Mon Sep 17 00:00:00 2001 From: splitadricejas Date: Mon, 13 Sep 2021 11:44:52 -0300 Subject: [PATCH 113/144] CI workflow added and travis yml deleted --- .github/workflows/ci.yml | 39 +++++++++++++++++++++++++++++++++++++++ .travis.yml | 20 -------------------- 2 files changed, 39 insertions(+), 20 deletions(-) create mode 100644 .github/workflows/ci.yml delete mode 100644 .travis.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..fb2a2183 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,39 @@ +name: ci +on: + push: + branches: + - development + pull_request: + branches: + - development + +jobs: + build: + name: Build + runs-on: ubuntu-latest + services: + redis: + image: redis + ports: + - 6379:6379 + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Set up nodejs + uses: actions/setup-node@v2 + with: + node-version: 'lts/*' + cache: 'npm' + + - name: npm CI + run: npm ci + + - name: npm Check + run: npm run check + + - name: npm Test + run: npm run test + + - name: npm Build + run: npm run build diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 5ab3da26..00000000 --- a/.travis.yml +++ /dev/null @@ -1,20 +0,0 @@ -language: node_js - -node_js: - - "lts/*" - -cache: npm - -services: - - redis-server - -git: - depth: false - -before_script: - - npm install - -script: - - npm run check - - npm run test - - npm run build From f169ccaf3130a00ae07eb20957eefb1dd327f834 Mon Sep 17 00:00:00 2001 From: splitadricejas Date: Mon, 13 Sep 2021 13:43:57 -0300 Subject: [PATCH 114/144] build coditions removed --- .github/workflows/ci.yml | 7 ------- 1 file changed, 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fb2a2183..98a9a779 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,11 +1,4 @@ name: ci -on: - push: - branches: - - development - pull_request: - branches: - - development jobs: build: From 61da96323ebacad2bef1056f57c5010d1332b170 Mon Sep 17 00:00:00 2001 From: splitadricejas Date: Mon, 13 Sep 2021 13:48:24 -0300 Subject: [PATCH 115/144] build codition fixed --- .github/workflows/ci.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 98a9a779..1fac0178 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,4 +1,11 @@ name: ci +on: + push: + branches: + - '*' + pull_request: + branches: + - '*' jobs: build: From 6af7766b94c544bcbcf508d5de398356f2b8c61e Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 16 Sep 2021 10:32:03 -0300 Subject: [PATCH 116/144] Nico's feedback --- src/sync/streaming/AuthClient/__tests__/index.spec.ts | 2 +- src/sync/streaming/SSEClient/__tests__/index.spec.ts | 2 +- src/sync/streaming/SSEHandler/__tests__/index.spec.ts | 2 +- src/sync/streaming/SSEHandler/types.ts | 1 - src/sync/streaming/{AuthClient => }/__tests__/dataMocks.ts | 0 src/sync/streaming/__tests__/mySegmentsV2utils.spec.ts | 2 +- src/sync/streaming/pushManager.ts | 1 - src/utils/base64/__tests__/index.spec.ts | 2 +- src/utils/jwt/__tests__/index.spec.ts | 2 +- 9 files changed, 6 insertions(+), 8 deletions(-) rename src/sync/streaming/{AuthClient => }/__tests__/dataMocks.ts (100%) diff --git a/src/sync/streaming/AuthClient/__tests__/index.spec.ts b/src/sync/streaming/AuthClient/__tests__/index.spec.ts index 28675540..afaf5f73 100644 --- a/src/sync/streaming/AuthClient/__tests__/index.spec.ts +++ b/src/sync/streaming/AuthClient/__tests__/index.spec.ts @@ -1,6 +1,6 @@ // mocks and dependencies import { splitApiFactory } from '../../../../services/splitApi'; -import { authDataResponseSample, authDataSample, jwtSampleInvalid, jwtSampleNoChannels, jwtSampleNoIat, userKeySample, userKeyBase64HashSample } from './dataMocks'; +import { authDataResponseSample, authDataSample, jwtSampleInvalid, jwtSampleNoChannels, jwtSampleNoIat, userKeySample, userKeyBase64HashSample } from '../../__tests__/dataMocks'; import fetchMock from '../../../../__tests__/testUtils/fetchMock'; import { settingsSplitApi } from '../../../../utils/settingsValidation/__tests__/settings.mocks'; import EventEmitter from '../../../../utils/MinEvents'; diff --git a/src/sync/streaming/SSEClient/__tests__/index.spec.ts b/src/sync/streaming/SSEClient/__tests__/index.spec.ts index bf01bbf4..6155224b 100644 --- a/src/sync/streaming/SSEClient/__tests__/index.spec.ts +++ b/src/sync/streaming/SSEClient/__tests__/index.spec.ts @@ -1,6 +1,6 @@ // @ts-nocheck import EventSourceMock from '../../../../__tests__/testUtils/eventSourceMock'; -import { authDataSample, channelsQueryParamSample } from '../../AuthClient/__tests__/dataMocks'; +import { authDataSample, channelsQueryParamSample } from '../../__tests__/dataMocks'; import { fullSettings as settings } from '../../../../utils/settingsValidation/__tests__/settings.mocks'; import { url } from '../../../../utils/settingsValidation/url'; diff --git a/src/sync/streaming/SSEHandler/__tests__/index.spec.ts b/src/sync/streaming/SSEHandler/__tests__/index.spec.ts index c463ea95..16013bf7 100644 --- a/src/sync/streaming/SSEHandler/__tests__/index.spec.ts +++ b/src/sync/streaming/SSEHandler/__tests__/index.spec.ts @@ -14,7 +14,7 @@ import unboundedMessage from '../../../../__tests__/mocks/message.V2.UNBOUNDED.1 import boundedGzipMessage from '../../../../__tests__/mocks/message.V2.BOUNDED.GZIP.1457552651000'; import keylistGzipMessage from '../../../../__tests__/mocks/message.V2.KEYLIST.GZIP.1457552652000'; import segmentRemovalMessage from '../../../../__tests__/mocks/message.V2.SEGMENT_REMOVAL.1457552653000'; -import { keylists, bitmaps } from '../../AuthClient/__tests__/dataMocks'; +import { keylists, bitmaps } from '../../__tests__/dataMocks'; // occupancy messages import occupancy1ControlPri from '../../../../__tests__/mocks/message.OCCUPANCY.1.control_pri.1586987434450.json'; diff --git a/src/sync/streaming/SSEHandler/types.ts b/src/sync/streaming/SSEHandler/types.ts index 446b6060..db271614 100644 --- a/src/sync/streaming/SSEHandler/types.ts +++ b/src/sync/streaming/SSEHandler/types.ts @@ -73,7 +73,6 @@ export type INotificationData = IMySegmentsUpdateData | IMySegmentsUpdateV2Data export type INotificationMessage = { parsedData: INotificationData, channel: string, timestamp: number, data: string } export type INotificationError = Event & { parsedData?: any, message?: string } -// @TODO remove `string[]` from the union type when removing support for MY_SEGMENTS_UPDATE notifications export type SegmentsData = string[] | { /* segment name */ name: string, diff --git a/src/sync/streaming/AuthClient/__tests__/dataMocks.ts b/src/sync/streaming/__tests__/dataMocks.ts similarity index 100% rename from src/sync/streaming/AuthClient/__tests__/dataMocks.ts rename to src/sync/streaming/__tests__/dataMocks.ts diff --git a/src/sync/streaming/__tests__/mySegmentsV2utils.spec.ts b/src/sync/streaming/__tests__/mySegmentsV2utils.spec.ts index 480ec625..b2826848 100644 --- a/src/sync/streaming/__tests__/mySegmentsV2utils.spec.ts +++ b/src/sync/streaming/__tests__/mySegmentsV2utils.spec.ts @@ -1,5 +1,5 @@ import { hash64 } from '../../../utils/murmur3/murmur3_64'; -import { keylists, bitmaps } from '../AuthClient/__tests__/dataMocks'; +import { keylists, bitmaps } from './dataMocks'; import { parseKeyList, parseBitmap, isInBitmap } from '../mySegmentsV2utils'; import { _Set } from '../../../utils/lang/sets'; diff --git a/src/sync/streaming/pushManager.ts b/src/sync/streaming/pushManager.ts index 7a60d258..428eb8ac 100644 --- a/src/sync/streaming/pushManager.ts +++ b/src/sync/streaming/pushManager.ts @@ -227,7 +227,6 @@ export default function pushManagerFactory( pushEmitter.on(SPLIT_KILL, splitsUpdateWorker.killSplit); pushEmitter.on(SPLIT_UPDATE, splitsUpdateWorker.put); if (userKey) { - // @TODO remove pushEmitter.on(MY_SEGMENTS_UPDATE, function handleMySegmentsUpdate(parsedData, channel) { const userKeyHash = channel.split('_')[2]; const userKey = userKeyHashes[userKeyHash]; diff --git a/src/utils/base64/__tests__/index.spec.ts b/src/utils/base64/__tests__/index.spec.ts index 8eff2bdc..3af8571e 100644 --- a/src/utils/base64/__tests__/index.spec.ts +++ b/src/utils/base64/__tests__/index.spec.ts @@ -1,4 +1,4 @@ -import { base64sample, decodedBase64sample } from '../../../sync/streaming/AuthClient/__tests__/dataMocks'; +import { base64sample, decodedBase64sample } from '../../../sync/streaming/__tests__/dataMocks'; import { encodeToBase64, decodeFromBase64 } from '../index'; diff --git a/src/utils/jwt/__tests__/index.spec.ts b/src/utils/jwt/__tests__/index.spec.ts index 35d27390..7073739b 100644 --- a/src/utils/jwt/__tests__/index.spec.ts +++ b/src/utils/jwt/__tests__/index.spec.ts @@ -1,4 +1,4 @@ -import { jwtSample, decodedJwtPayloadSample } from '../../../sync/streaming/AuthClient/__tests__/dataMocks'; +import { jwtSample, decodedJwtPayloadSample } from '../../../sync/streaming/__tests__/dataMocks'; import { decodeJWTtoken } from '..'; test('decodeJWTtoken', () => { From d6f4227536d8f201f9f992dbd57d3dcbd0daff8f Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 16 Sep 2021 13:28:06 -0300 Subject: [PATCH 117/144] More Nico's feedback --- .../SSEHandler/__tests__/index.spec.ts | 22 +- src/sync/streaming/SSEHandler/index.ts | 24 +- .../UpdateWorkers/SegmentsUpdateWorker.ts | 3 +- .../UpdateWorkers/SplitsUpdateWorker.ts | 7 +- .../__tests__/SegmentsUpdateWorker.spec.ts | 16 +- .../__tests__/SplitsUpdateWorker.spec.ts | 16 +- src/sync/streaming/UpdateWorkers/types.ts | 2 +- src/sync/streaming/pushManagerCS.ts | 238 ------------------ src/sync/streaming/pushManagerSS.ts | 177 ------------- src/sync/streaming/types.ts | 35 +-- .../impressionObserver/ImpressionObserver.ts | 8 +- .../impressionObserverCS.ts | 2 +- 12 files changed, 54 insertions(+), 496 deletions(-) delete mode 100644 src/sync/streaming/pushManagerCS.ts delete mode 100644 src/sync/streaming/pushManagerSS.ts diff --git a/src/sync/streaming/SSEHandler/__tests__/index.spec.ts b/src/sync/streaming/SSEHandler/__tests__/index.spec.ts index 16013bf7..9324bf84 100644 --- a/src/sync/streaming/SSEHandler/__tests__/index.spec.ts +++ b/src/sync/streaming/SSEHandler/__tests__/index.spec.ts @@ -10,10 +10,10 @@ import segmentUpdateMessage from '../../../../__tests__/mocks/message.SEGMENT_UP import mySegmentsUpdateMessage from '../../../../__tests__/mocks/message.MY_SEGMENTS_UPDATE.nicolas@split.io.1457552640000.json'; // update messages MY_SEGMENTS_UPDATE_V2 -import unboundedMessage from '../../../../__tests__/mocks/message.V2.UNBOUNDED.1457552650000'; -import boundedGzipMessage from '../../../../__tests__/mocks/message.V2.BOUNDED.GZIP.1457552651000'; -import keylistGzipMessage from '../../../../__tests__/mocks/message.V2.KEYLIST.GZIP.1457552652000'; -import segmentRemovalMessage from '../../../../__tests__/mocks/message.V2.SEGMENT_REMOVAL.1457552653000'; +import unboundedMessage from '../../../../__tests__/mocks/message.V2.UNBOUNDED.1457552650000.json'; +import boundedGzipMessage from '../../../../__tests__/mocks/message.V2.BOUNDED.GZIP.1457552651000.json'; +import keylistGzipMessage from '../../../../__tests__/mocks/message.V2.KEYLIST.GZIP.1457552652000.json'; +import segmentRemovalMessage from '../../../../__tests__/mocks/message.V2.SEGMENT_REMOVAL.1457552653000.json'; import { keylists, bitmaps } from '../../__tests__/dataMocks'; // occupancy messages @@ -33,7 +33,7 @@ const controlStreamingResumedSec = { ...controlStreamingResumed, data: controlSt const controlStreamingDisabledSec = { ...controlStreamingDisabled, data: controlStreamingDisabled.data.replace('control_pri', 'control_sec') }; // streaming reset message, from `{orgHash}_{envHash}_control` channel -import streamingReset from '../../../../__tests__/mocks/message.STREAMING_RESET'; +import streamingReset from '../../../../__tests__/mocks/message.STREAMING_RESET.json'; const pushEmitter = { emit: jest.fn() }; @@ -47,7 +47,7 @@ test('`handleOpen` and `handlerMessage` for OCCUPANCY notifications (Notificatio expect(pushEmitter.emit.mock.calls[0][0]).toBe(PUSH_SUBSYSTEM_UP); // must emit PUSH_SUBSYSTEM_UP sseHandler.handleMessage({ data: '{ "data": "{\\"type\\":\\"SPLIT_UPDATE\\",\\"changeNumber\\":1457552620999 }" }' }); - expect(pushEmitter.emit).toHaveBeenLastCalledWith(SPLIT_UPDATE, 1457552620999); // must handle update message if streaming on + expect(pushEmitter.emit).toHaveBeenLastCalledWith(SPLIT_UPDATE, { type: 'SPLIT_UPDATE', changeNumber: 1457552620999 }); // must handle update message if streaming on // OCCUPANCY messages @@ -78,7 +78,7 @@ test('`handleOpen` and `handlerMessage` for OCCUPANCY notifications (Notificatio sseHandler.handleMessage({ data: '{ "data": "{\\"type\\":\\"SPLIT_UPDATE\\",\\"changeNumber\\":1457552620999 }" }' }); sseHandler.handleMessage(occupancy2ControlPri); // must not emit PUSH_SUBSYSTEM_UP, since streaming is already on - expect(pushEmitter.emit).toHaveBeenLastCalledWith(SPLIT_UPDATE, 1457552620999); // must handle update message if streaming on after an OCCUPANCY event + expect(pushEmitter.emit).toHaveBeenLastCalledWith(SPLIT_UPDATE, { type: 'SPLIT_UPDATE', changeNumber: 1457552620999 }); // must handle update message if streaming on after an OCCUPANCY event sseHandler.handleMessage(streamingReset); expect(pushEmitter.emit).toHaveBeenLastCalledWith(STREAMING_RESET); // must handle streaming reset expect(pushEmitter.emit).toBeCalledTimes(7); // must not emit PUSH_SUBSYSTEM_UP if streaming is already on and another channel has publishers @@ -112,7 +112,7 @@ test('`handlerMessage` for CONTROL notifications (NotificationKeeper)', () => { expect(pushEmitter.emit).toBeCalledTimes(4); // must not emit PUSH_SUBSYSTEM_UP if streaming on sseHandler.handleMessage({ data: '{ "data": "{\\"type\\":\\"SPLIT_UPDATE\\",\\"changeNumber\\":1457552620999 }" }' }); - expect(pushEmitter.emit).toHaveBeenLastCalledWith(SPLIT_UPDATE, 1457552620999); // must handle update message if streaming on after a CONTROL event + expect(pushEmitter.emit).toHaveBeenLastCalledWith(SPLIT_UPDATE, { type: 'SPLIT_UPDATE', changeNumber: 1457552620999 }); // must handle update message if streaming on after a CONTROL event sseHandler.handleMessage(streamingReset); expect(pushEmitter.emit).toHaveBeenLastCalledWith(STREAMING_RESET); // must handle streaming reset @@ -135,15 +135,15 @@ test('`handlerMessage` for update notifications (NotificationProcessor) and stre sseHandler.handleOpen(); pushEmitter.emit.mockClear(); - let expectedParams = [1457552620999]; + let expectedParams = [{ type: 'SPLIT_UPDATE', changeNumber: 1457552620999 }]; sseHandler.handleMessage(splitUpdateMessage); expect(pushEmitter.emit).toHaveBeenLastCalledWith(SPLIT_UPDATE, ...expectedParams); // must emit SPLIT_UPDATE with the message change number - expectedParams = [1457552650000, 'whitelist', 'not_allowed']; + expectedParams = [{ type: 'SPLIT_KILL', changeNumber: 1457552650000, splitName: 'whitelist', defaultTreatment: 'not_allowed' }]; sseHandler.handleMessage(splitKillMessage); expect(pushEmitter.emit).toHaveBeenLastCalledWith(SPLIT_KILL, ...expectedParams); // must emit SPLIT_KILL with the message change number, split name and default treatment - expectedParams = [1457552640000, 'splitters']; + expectedParams = [{ type: 'SEGMENT_UPDATE', changeNumber: 1457552640000, segmentName: 'splitters' }]; sseHandler.handleMessage(segmentUpdateMessage); expect(pushEmitter.emit).toHaveBeenLastCalledWith(SEGMENT_UPDATE, ...expectedParams); // must emit SEGMENT_UPDATE with the message change number and segment name diff --git a/src/sync/streaming/SSEHandler/index.ts b/src/sync/streaming/SSEHandler/index.ts index a43495f3..de9b5eba 100644 --- a/src/sync/streaming/SSEHandler/index.ts +++ b/src/sync/streaming/SSEHandler/index.ts @@ -74,29 +74,13 @@ export default function SSEHandlerFactory(log: ILogger, pushEmitter: IPushEventE switch (parsedData.type) { /* update events */ case SPLIT_UPDATE: - pushEmitter.emit(SPLIT_UPDATE, - parsedData.changeNumber); - break; case SEGMENT_UPDATE: - pushEmitter.emit(SEGMENT_UPDATE, - parsedData.changeNumber, - parsedData.segmentName); - break; - // @TODO remove - case MY_SEGMENTS_UPDATE: - pushEmitter.emit(MY_SEGMENTS_UPDATE, - parsedData, - channel); - break; case MY_SEGMENTS_UPDATE_V2: - pushEmitter.emit(MY_SEGMENTS_UPDATE_V2, - parsedData); - break; case SPLIT_KILL: - pushEmitter.emit(SPLIT_KILL, - parsedData.changeNumber, - parsedData.splitName, - parsedData.defaultTreatment); + pushEmitter.emit(parsedData.type, parsedData); + break; + case MY_SEGMENTS_UPDATE: + pushEmitter.emit(parsedData.type, parsedData, channel); break; /* occupancy & control events, handled by NotificationManagerKeeper */ diff --git a/src/sync/streaming/UpdateWorkers/SegmentsUpdateWorker.ts b/src/sync/streaming/UpdateWorkers/SegmentsUpdateWorker.ts index aa900e9d..3a299024 100644 --- a/src/sync/streaming/UpdateWorkers/SegmentsUpdateWorker.ts +++ b/src/sync/streaming/UpdateWorkers/SegmentsUpdateWorker.ts @@ -1,6 +1,7 @@ import { ISegmentsCacheSync } from '../../../storages/types'; import Backoff from '../../../utils/Backoff'; import { ISegmentsSyncTask } from '../../polling/types'; +import { ISegmentUpdateData } from '../SSEHandler/types'; import { IUpdateWorker } from './types'; /** @@ -66,7 +67,7 @@ export default class SegmentsUpdateWorker implements IUpdateWorker { * @param {number} changeNumber change number of the SEGMENT_UPDATE notification * @param {string} segmentName segment name of the SEGMENT_UPDATE notification */ - put(changeNumber: number, segmentName: string) { + put({ changeNumber, segmentName }: ISegmentUpdateData) { const currentChangeNumber = this.segmentsCache.getChangeNumber(segmentName); if (changeNumber <= currentChangeNumber || changeNumber <= this.maxChangeNumbers[segmentName]) return; diff --git a/src/sync/streaming/UpdateWorkers/SplitsUpdateWorker.ts b/src/sync/streaming/UpdateWorkers/SplitsUpdateWorker.ts index 7c93b476..944d5776 100644 --- a/src/sync/streaming/UpdateWorkers/SplitsUpdateWorker.ts +++ b/src/sync/streaming/UpdateWorkers/SplitsUpdateWorker.ts @@ -3,6 +3,7 @@ import { ISplitsEventEmitter } from '../../../readiness/types'; import { ISplitsCacheSync } from '../../../storages/types'; import Backoff from '../../../utils/Backoff'; import { ISegmentsSyncTask, ISplitsSyncTask } from '../../polling/types'; +import { ISplitKillData, ISplitUpdateData } from '../SSEHandler/types'; import { IUpdateWorker } from './types'; /** @@ -60,7 +61,7 @@ export default class SplitsUpdateWorker implements IUpdateWorker { * * @param {number} changeNumber change number of the SPLIT_UPDATE notification */ - put(changeNumber: number) { + put({ changeNumber }: Pick) { const currentChangeNumber = this.splitsCache.getChangeNumber(); if (changeNumber <= currentChangeNumber || changeNumber <= this.maxChangeNumber) return; @@ -81,14 +82,14 @@ export default class SplitsUpdateWorker implements IUpdateWorker { * @param {string} splitName name of split to kill * @param {string} defaultTreatment default treatment value */ - killSplit(changeNumber: number, splitName: string, defaultTreatment: string) { + killSplit({ changeNumber, splitName, defaultTreatment }: ISplitKillData) { // @TODO handle retry due to errors in storage, once we allow the definition of custom async storages if (this.splitsCache.killLocally(splitName, defaultTreatment, changeNumber)) { // trigger an SDK_UPDATE if Split was killed locally this.splitsEventEmitter.emit(SDK_SPLITS_ARRIVED, true); } // queues the SplitChanges fetch (only if changeNumber is newer) - this.put(changeNumber); + this.put({ changeNumber }); } } diff --git a/src/sync/streaming/UpdateWorkers/__tests__/SegmentsUpdateWorker.spec.ts b/src/sync/streaming/UpdateWorkers/__tests__/SegmentsUpdateWorker.spec.ts index 50eddd88..5e6acb6a 100644 --- a/src/sync/streaming/UpdateWorkers/__tests__/SegmentsUpdateWorker.spec.ts +++ b/src/sync/streaming/UpdateWorkers/__tests__/SegmentsUpdateWorker.spec.ts @@ -54,18 +54,18 @@ describe('SegmentsUpdateWorker ', () => { // assert calling `segmentsSyncTask.execute` if `isExecuting` is false expect(segmentsSyncTask.isExecuting()).toBe(false); - segmentsUpdateWorker.put(100, 'mocked_segment_1'); + segmentsUpdateWorker.put({ changeNumber: 100, segmentName: 'mocked_segment_1' }); expect(segmentsUpdateWorker.maxChangeNumbers).toEqual({ 'mocked_segment_1': 100 }); // queues events (changeNumbers) if they are mayor than storage changeNumbers and maxChangeNumbers expect(segmentsSyncTask.execute).toBeCalledTimes(1); // synchronizes segment if `isExecuting` is false expect(segmentsSyncTask.execute.mock.calls).toEqual([[['mocked_segment_1'], true]]); // synchronizes segment with given name // assert queueing items if `isExecuting` is true expect(segmentsSyncTask.isExecuting()).toBe(true); - segmentsUpdateWorker.put(95, 'mocked_segment_1'); - segmentsUpdateWorker.put(100, 'mocked_segment_2'); - segmentsUpdateWorker.put(105, 'mocked_segment_1'); - segmentsUpdateWorker.put(94, 'mocked_segment_1'); - segmentsUpdateWorker.put(94, 'mocked_segment_3'); + segmentsUpdateWorker.put({ changeNumber: 95, segmentName: 'mocked_segment_1' }); + segmentsUpdateWorker.put({ changeNumber: 100, segmentName: 'mocked_segment_2' }); + segmentsUpdateWorker.put({ changeNumber: 105, segmentName: 'mocked_segment_1' }); + segmentsUpdateWorker.put({ changeNumber: 94, segmentName: 'mocked_segment_1' }); + segmentsUpdateWorker.put({ changeNumber: 94, segmentName: 'mocked_segment_3' }); expect(segmentsUpdateWorker.maxChangeNumbers).toEqual({ 'mocked_segment_1': 105, 'mocked_segment_2': 100, 'mocked_segment_3': 94 }); // queues events expect(segmentsSyncTask.execute).toBeCalledTimes(1); // doesn't synchronize segment if `isExecuting` is true @@ -80,7 +80,7 @@ describe('SegmentsUpdateWorker ', () => { // assert not rescheduling synchronization if some changeNumber is not updated as expected, // but rescheduling if a new item was queued with a greater changeNumber while the fetch was pending. - segmentsUpdateWorker.put(110, 'mocked_segment_1'); + segmentsUpdateWorker.put({ changeNumber: 110, segmentName: 'mocked_segment_1' }); segmentsSyncTask.__resolveSegmentsUpdaterCall(1, { 'mocked_segment_1': 100, 'mocked_segment_2': 100, 'mocked_segment_3': 94 }); setTimeout(() => { expect(segmentsSyncTask.execute).toBeCalledTimes(3); // re-synchronizes segment if a new item was queued with a greater changeNumber while the fetch was pending @@ -94,7 +94,7 @@ describe('SegmentsUpdateWorker ', () => { expect(segmentsUpdateWorker.maxChangeNumbers).toEqual({ 'mocked_segment_1': -1, 'mocked_segment_2': -1, 'mocked_segment_3': -1 }); // maxChangeNumbers were cleaned // assert restarting retries, when a newer event is queued - segmentsUpdateWorker.put(110, 'mocked_segment_1'); // queued + segmentsUpdateWorker.put({ changeNumber: 110, segmentName: 'mocked_segment_1' }); // queued expect(segmentsUpdateWorker.backoff.attempts).toBe(0); // backoff scheduler for retries is reset if a new event is queued done(); diff --git a/src/sync/streaming/UpdateWorkers/__tests__/SplitsUpdateWorker.spec.ts b/src/sync/streaming/UpdateWorkers/__tests__/SplitsUpdateWorker.spec.ts index 43aec4e1..bf28fa2e 100644 --- a/src/sync/streaming/UpdateWorkers/__tests__/SplitsUpdateWorker.spec.ts +++ b/src/sync/streaming/UpdateWorkers/__tests__/SplitsUpdateWorker.spec.ts @@ -62,16 +62,16 @@ describe('SplitsUpdateWorker', () => { // assert calling `splitsSyncTask.execute` if `isExecuting` is false expect(splitsSyncTask.isExecuting()).toBe(false); - splitUpdateWorker.put(100); // queued + splitUpdateWorker.put({ changeNumber: 100 }); // queued expect(splitUpdateWorker.maxChangeNumber).toBe(100); // queues changeNumber if it is mayor than storage changeNumber and maxChangeNumber expect(splitsSyncTask.execute).toBeCalledTimes(1); // synchronizes splits if `isExecuting` is false // assert queueing changeNumber if `isExecuting` is true expect(splitsSyncTask.isExecuting()).toBe(true); - splitUpdateWorker.put(105); // queued - splitUpdateWorker.put(104); // not queued - splitUpdateWorker.put(106); // queued - splitUpdateWorker.put(103); // not queued + splitUpdateWorker.put({ changeNumber: 105 }); // queued + splitUpdateWorker.put({ changeNumber: 104 }); // not queued + splitUpdateWorker.put({ changeNumber: 106 }); // queued + splitUpdateWorker.put({ changeNumber: 103 }); // not queued expect(splitsSyncTask.execute).toBeCalledTimes(1); // doesn't synchronize splits while `isExecuting` is true expect(splitUpdateWorker.maxChangeNumber).toBe(106); // queues changeNumber if it is mayor than currently maxChangeNumber and storage changeNumber @@ -96,7 +96,7 @@ describe('SplitsUpdateWorker', () => { expect(splitUpdateWorker.maxChangeNumber).toBe(106); // maxChangeNumber // assert restarting retries, when a newer event is queued - splitUpdateWorker.put(107); // queued + splitUpdateWorker.put({ changeNumber: 107 }); // queued expect(splitUpdateWorker.backoff.attempts).toBe(0); // backoff scheduler for retries is reset if a new event is queued done(); @@ -116,7 +116,7 @@ describe('SplitsUpdateWorker', () => { const splitUpdateWorker = new SplitsUpdateWorker(cache, splitsSyncTask, splitsEventEmitterMock); // assert killing split locally, emitting SDK_SPLITS_ARRIVED event, and synchronizing splits if changeNumber is new - splitUpdateWorker.killSplit(100, 'lol1', 'off'); // splitsCache.killLocally is synchronous + splitUpdateWorker.killSplit({ changeNumber: 100, splitName: 'lol1', defaultTreatment: 'off' }); // splitsCache.killLocally is synchronous expect(splitUpdateWorker.maxChangeNumber).toBe(100); // queues changeNumber if split kill resolves with update expect(splitsSyncTask.execute).toBeCalledTimes(1); // synchronizes splits if `isExecuting` is false expect(splitsEventEmitterMock.emit.mock.calls).toEqual([[SDK_SPLITS_ARRIVED, true]]); // emits `SDK_SPLITS_ARRIVED` with `isSplitKill` flag in true, if split kill resolves with update @@ -127,7 +127,7 @@ describe('SplitsUpdateWorker', () => { setTimeout(() => { splitsSyncTask.execute.mockClear(); splitsEventEmitterMock.emit.mockClear(); - splitUpdateWorker.killSplit(90, 'lol1', 'on'); + splitUpdateWorker.killSplit({ changeNumber: 90, splitName: 'lol1', defaultTreatment: 'on' }); setTimeout(() => { expect(splitUpdateWorker.maxChangeNumber).toBe(100); // doesn't queues changeNumber if killLocally resolved without update (its changeNumber was minor than the split changeNumber diff --git a/src/sync/streaming/UpdateWorkers/types.ts b/src/sync/streaming/UpdateWorkers/types.ts index e8d6762e..ea91a154 100644 --- a/src/sync/streaming/UpdateWorkers/types.ts +++ b/src/sync/streaming/UpdateWorkers/types.ts @@ -2,5 +2,5 @@ import Backoff from '../../../utils/Backoff'; export interface IUpdateWorker { readonly backoff: Backoff, - put(changeNumber: number, ...args: any[]): void + put(...args: any[]): void } diff --git a/src/sync/streaming/pushManagerCS.ts b/src/sync/streaming/pushManagerCS.ts deleted file mode 100644 index ee1a59cf..00000000 --- a/src/sync/streaming/pushManagerCS.ts +++ /dev/null @@ -1,238 +0,0 @@ -import { IPushEventEmitter, IPushManagerCS } from './types'; -import { ISSEClient } from './SSEClient/types'; -import { IStorageSync } from '../../storages/types'; -import { IReadinessManager } from '../../readiness/types'; -import { ISegmentsSyncTask, IPollingManager } from '../polling/types'; -import { IUpdateWorker } from './UpdateWorkers/types'; -import objectAssign from 'object-assign'; -import { MY_SEGMENTS_UPDATE, PUSH_NONRETRYABLE_ERROR, PUSH_SUBSYSTEM_DOWN, SECONDS_BEFORE_EXPIRATION, SPLIT_KILL, SPLIT_UPDATE, PUSH_RETRYABLE_ERROR, PUSH_SUBSYSTEM_UP } from './constants'; -import Backoff from '../../utils/Backoff'; -import SSEHandlerFactory from './SSEHandler'; -import MySegmentsUpdateWorker from './UpdateWorkers/MySegmentsUpdateWorker'; -import SplitsUpdateWorker from './UpdateWorkers/SplitsUpdateWorker'; -import { authenticateFactory, hashUserKey } from './AuthClient'; -import { forOwn } from '../../utils/lang'; -import SSEClient from './SSEClient'; -import { IFetchAuth } from '../../services/types'; -import { ISettings } from '../../types'; -import { getMatching } from '../../utils/key'; -import { IPlatform } from '../../sdkFactory/types'; -import { STREAMING_FALLBACK, STREAMING_REFRESH_TOKEN, STREAMING_CONNECTING, STREAMING_DISABLED, ERROR_STREAMING_AUTH, STREAMING_DISCONNECTING, STREAMING_RECONNECT } from '../../logger/constants'; - -/** - * PushManager factory for client-side, with support for multiple clients. - * It assumes settings contains a key. - */ -export default function pushManagerCSFactory( - pollingManager: IPollingManager, - storage: IStorageSync, - readiness: IReadinessManager, - fetchAuth: IFetchAuth, - platform: IPlatform, - settings: ISettings -): IPushManagerCS | undefined { - - const log = settings.log; - - let sseClient: ISSEClient; - try { - // `useHeaders` false for client-side, even if the platform EventSource supports headers (e.g., React Native). - sseClient = new SSEClient(settings, false, platform.getEventSource); - } catch (e) { - log.warn(STREAMING_FALLBACK, [e]); - return; - } - const authenticate = authenticateFactory(fetchAuth); - - // init feedback loop - const pushEmitter = new platform.EventEmitter() as IPushEventEmitter; - const sseHandler = SSEHandlerFactory(log, pushEmitter); - sseClient.setEventHandler(sseHandler); - - // [Only for client-side] map of hashes to user keys, to dispatch MY_SEGMENTS_UPDATE events to the corresponding MySegmentsUpdateWorker - const userKeyHashes: Record = {}; - const userKey = getMatching(settings.core.key); // matching key of main client - const hash = hashUserKey(userKey); - userKeyHashes[hash] = userKey; - - // [Only for client-side] map of user keys to their corresponding MySegmentsUpdateWorkers. It has a two-fold intention: - // - stop workers all together when push is disconnected - // - keep the current list of user keys to authenticate - const workers: Record = {}; - - // init workers - const mySegmentsUpdateWorker = new MySegmentsUpdateWorker(pollingManager.segmentsSyncTask); - workers[userKey] = mySegmentsUpdateWorker; - const splitsUpdateWorker = new SplitsUpdateWorker(storage.splits, pollingManager.splitsSyncTask, readiness.splits); - - // [Only for client-side] variable to flag that a new client was added. It is needed to reconnect streaming. - let connectForNewClient = false; - - // flag that indicates if `disconnectPush` was called, either by the SyncManager (when the client is destroyed) or by a PUSH_NONRETRYABLE_ERROR error. - // It is used to halt the `connectPush` process if it was in progress. - let disconnected: boolean | undefined; - - /** PushManager functions related to initialization */ - - const connectPushRetryBackoff = new Backoff(connectPush, settings.scheduler.pushRetryBackoffBase); - - let timeoutId: ReturnType; - - function scheduleTokenRefresh(issuedAt: number, expirationTime: number) { - // clear scheduled token refresh if exists (needed when resuming PUSH) - if (timeoutId) clearTimeout(timeoutId); - - // Set token refresh 10 minutes before expirationTime - const delayInSeconds = expirationTime - issuedAt - SECONDS_BEFORE_EXPIRATION; - - log.info(STREAMING_REFRESH_TOKEN, [delayInSeconds]); - - timeoutId = setTimeout(connectPush, delayInSeconds * 1000); - } - - function connectPush() { - disconnected = false; - log.info(STREAMING_CONNECTING); - - const userKeys = Object.keys(workers); // [Only for client-side] - authenticate(userKeys).then( - function (authData) { - if (disconnected) return; - - // 'pushEnabled: false' is handled as a PUSH_NONRETRYABLE_ERROR instead of PUSH_SUBSYSTEM_DOWN, in order to - // close the sseClient in case the org has been bloqued while the instance was connected to streaming - if (!authData.pushEnabled) { - log.info(STREAMING_DISABLED); - pushEmitter.emit(PUSH_NONRETRYABLE_ERROR); - return; - } - - // [Only for client-side] don't open SSE connection if a new shared client was added, since it means that a new authentication is taking place - if (userKeys && userKeys.length < Object.keys(workers).length) return; - - // Connect to SSE and schedule refresh token - const decodedToken = authData.decodedToken; - sseClient.open(authData); - scheduleTokenRefresh(decodedToken.iat, decodedToken.exp); - } - ).catch( - function (error) { - if (disconnected) return; - - log.error(ERROR_STREAMING_AUTH, [error.message]); - - // Handle 4XX HTTP errors: 401 (invalid API Key) or 400 (using incorrect API Key, i.e., client-side API Key on server-side) - if (error.statusCode >= 400 && error.statusCode < 500) { - pushEmitter.emit(PUSH_NONRETRYABLE_ERROR); - return; - } - - // Handle other HTTP and network errors as recoverable errors - pushEmitter.emit(PUSH_RETRYABLE_ERROR); - } - ); - } - - // close SSE connection and cancel scheduled tasks - function disconnectPush() { - disconnected = true; - sseClient.close(); - log.info(STREAMING_DISCONNECTING); - - if (timeoutId) clearTimeout(timeoutId); - connectPushRetryBackoff.reset(); - - stopWorkers(); - } - - // cancel scheduled fetch retries of Splits, Segments, and MySegments Update Workers - function stopWorkers() { - splitsUpdateWorker.backoff.reset(); - forOwn(workers, worker => worker.backoff.reset()); - } - - pushEmitter.on(PUSH_SUBSYSTEM_DOWN, stopWorkers); - - // restart backoff retry counter once push is connected - pushEmitter.on(PUSH_SUBSYSTEM_UP, () => { connectPushRetryBackoff.reset(); }); - - /** Fallbacking without retry due to: STREAMING_DISABLED control event, or 'pushEnabled: false', or non-recoverable SSE and Authentication errors */ - - pushEmitter.on(PUSH_NONRETRYABLE_ERROR, function handleNonRetryableError() { - // Note: `stopWorkers` is been called twice, but it is not harmful - disconnectPush(); - pushEmitter.emit(PUSH_SUBSYSTEM_DOWN); // no harm if polling already - }); - - /** Fallbacking with retry due to recoverable SSE and Authentication errors */ - - pushEmitter.on(PUSH_RETRYABLE_ERROR, function handleRetryableError() { // HTTP or network error in SSE connection - // SSE connection is closed to avoid repeated errors due to retries - sseClient.close(); - - // retry streaming reconnect with backoff algorithm - let delayInMillis = connectPushRetryBackoff.scheduleCall(); - - log.info(STREAMING_RECONNECT, [delayInMillis / 1000]); - - pushEmitter.emit(PUSH_SUBSYSTEM_DOWN); // no harm if polling already - }); - - /** Functions related to synchronization (Queues and Workers in the spec) */ - - pushEmitter.on(SPLIT_KILL, splitsUpdateWorker.killSplit); - pushEmitter.on(SPLIT_UPDATE, splitsUpdateWorker.put); - // [Only for client-side] - pushEmitter.on(MY_SEGMENTS_UPDATE, function handleMySegmentsUpdate(parsedData, channel) { - const userKeyHash = channel.split('_')[2]; - const userKey = userKeyHashes[userKeyHash]; - if (userKey && workers[userKey]) { // check context since it can be undefined if client has been destroyed - const mySegmentsUpdateWorker = workers[userKey]; - mySegmentsUpdateWorker.put( - parsedData.changeNumber, - parsedData.includesPayload ? parsedData.segmentList ? parsedData.segmentList : [] : undefined); - } - }); - - return objectAssign( - // Expose Event Emitter functionality and Event constants - Object.create(pushEmitter), - { - // Expose functionality for starting and stoping push mode: - stop: disconnectPush, // `handleNonRetryableError` cannot be used as `stop`, because it emits PUSH_SUBSYSTEM_DOWN event, which start polling. - - start() { - // Run in next event-loop cycle for optimization: if multiple clients are created in the same cycle than the factory, only one authentication is performed. - setTimeout(connectPush); - }, - - // [Only for client-side] - add(userKey: string, mySegmentsSyncTask: ISegmentsSyncTask) { - const mySegmentsUpdateWorker = new MySegmentsUpdateWorker(mySegmentsSyncTask); - workers[userKey] = mySegmentsUpdateWorker; - - const hash = hashUserKey(userKey); - - if (!userKeyHashes[hash]) { - userKeyHashes[hash] = userKey; - connectForNewClient = true; // we must reconnect on start, to listen the channel for the new user key - } - - // Reconnects in case of a new client. - // Run in next event-loop cycle to save authentication calls - // in case the user is creating several clients in the current cycle. - setTimeout(function checkForReconnect() { - if (connectForNewClient) { - connectForNewClient = false; - connectPush(); - } - }, 0); - }, - // [Only for client-side] - remove(userKey: string) { - const hash = hashUserKey(userKey); - delete userKeyHashes[hash]; - } - } - ); -} diff --git a/src/sync/streaming/pushManagerSS.ts b/src/sync/streaming/pushManagerSS.ts deleted file mode 100644 index 17674746..00000000 --- a/src/sync/streaming/pushManagerSS.ts +++ /dev/null @@ -1,177 +0,0 @@ -import { IPushEventEmitter, IPushManager } from './types'; -import { ISSEClient } from './SSEClient/types'; -import { IStorageSync } from '../../storages/types'; -import { IPollingManager } from '../polling/types'; -import { IReadinessManager } from '../../readiness/types'; -import objectAssign from 'object-assign'; -import { PUSH_NONRETRYABLE_ERROR, PUSH_SUBSYSTEM_DOWN, SECONDS_BEFORE_EXPIRATION, SEGMENT_UPDATE, SPLIT_KILL, SPLIT_UPDATE, PUSH_RETRYABLE_ERROR, PUSH_SUBSYSTEM_UP } from './constants'; -import Backoff from '../../utils/Backoff'; -import SSEHandlerFactory from './SSEHandler'; -import SegmentsUpdateWorker from './UpdateWorkers/SegmentsUpdateWorker'; -import SplitsUpdateWorker from './UpdateWorkers/SplitsUpdateWorker'; -import { IFetchAuth } from '../../services/types'; -import { authenticateFactory } from './AuthClient'; -import SSEClient from './SSEClient'; -import { ISettings } from '../../types'; -import { IPlatform } from '../../sdkFactory/types'; -import { STREAMING_FALLBACK, STREAMING_REFRESH_TOKEN, STREAMING_CONNECTING, STREAMING_DISABLED, ERROR_STREAMING_AUTH, STREAMING_DISCONNECTING, STREAMING_RECONNECT } from '../../logger/constants'; - -/** - * PushManager factory for server-side - */ -export default function pushManagerSSFactory( - pollingManager: IPollingManager, - storage: IStorageSync, - readiness: IReadinessManager, - fetchAuth: IFetchAuth, - platform: IPlatform, - settings: ISettings -): IPushManager | undefined { - - const log = settings.log; - - let sseClient: ISSEClient; - try { - sseClient = new SSEClient(settings, true, platform.getEventSource); - } catch (e) { - log.warn(STREAMING_FALLBACK, [e]); - return; - } - const authenticate = authenticateFactory(fetchAuth); - - // init feedback loop (pushEmitter) - const pushEmitter = new platform.EventEmitter() as IPushEventEmitter; - const sseHandler = SSEHandlerFactory(log, pushEmitter); - sseClient.setEventHandler(sseHandler); - - // init workers - const splitsUpdateWorker = new SplitsUpdateWorker(storage.splits, pollingManager.splitsSyncTask, readiness.splits, pollingManager.segmentsSyncTask); - const segmentsUpdateWorker = new SegmentsUpdateWorker(storage.segments, pollingManager.segmentsSyncTask); - - // flag that indicates if `disconnectPush` was called, either by the SyncManager (when the client is destroyed) or by a PUSH_NONRETRYABLE_ERROR error. - // It is used to halt the `connectPush` process if it was in progress. - let disconnected: boolean | undefined; - - /** PushManager functions related to initialization */ - - const connectPushRetryBackoff = new Backoff(connectPush, settings.scheduler.pushRetryBackoffBase); - - let timeoutId: ReturnType; - - function scheduleTokenRefresh(issuedAt: number, expirationTime: number) { - // clear scheduled token refresh if exists (needed when resuming PUSH) - if (timeoutId) clearTimeout(timeoutId); - - // Set token refresh 10 minutes before expirationTime - const delayInSeconds = expirationTime - issuedAt - SECONDS_BEFORE_EXPIRATION; - - log.info(STREAMING_REFRESH_TOKEN, [delayInSeconds]); - - timeoutId = setTimeout(connectPush, delayInSeconds * 1000); - } - - function connectPush() { - disconnected = false; - log.info(STREAMING_CONNECTING); - - authenticate().then( - function (authData) { - if (disconnected) return; - - // 'pushEnabled: false' is handled as a PUSH_NONRETRYABLE_ERROR instead of PUSH_SUBSYSTEM_DOWN, in order to - // close the sseClient in case the org has been bloqued while the instance was connected to streaming - if (!authData.pushEnabled) { - log.info(STREAMING_DISABLED); - pushEmitter.emit(PUSH_NONRETRYABLE_ERROR); - return; - } - - // Connect to SSE and schedule refresh token - const decodedToken = authData.decodedToken; - sseClient.open(authData); - scheduleTokenRefresh(decodedToken.iat, decodedToken.exp); - } - ).catch( - function (error) { - if (disconnected) return; - - log.error(ERROR_STREAMING_AUTH, [error.message]); - - // Handle 4XX HTTP errors: 401 (invalid API Key) or 400 (using incorrect API Key, i.e., client-side API Key on server-side) - if (error.statusCode >= 400 && error.statusCode < 500) { - pushEmitter.emit(PUSH_NONRETRYABLE_ERROR); - return; - } - - // Handle other HTTP and network errors as recoverable errors - pushEmitter.emit(PUSH_RETRYABLE_ERROR); - } - ); - } - - // close SSE connection and cancel scheduled tasks - function disconnectPush() { - disconnected = true; - sseClient.close(); - log.info(STREAMING_DISCONNECTING); - - if (timeoutId) clearTimeout(timeoutId); - connectPushRetryBackoff.reset(); - - stopWorkers(); - } - - // cancel scheduled fetch retries of Splits, Segments, and MySegments Update Workers - function stopWorkers() { - splitsUpdateWorker.backoff.reset(); - segmentsUpdateWorker.backoff.reset(); - } - - pushEmitter.on(PUSH_SUBSYSTEM_DOWN, stopWorkers); - - // restart backoff retry counter once push is connected - pushEmitter.on(PUSH_SUBSYSTEM_UP, () => { connectPushRetryBackoff.reset(); }); - - /** Fallbacking without retry due to: STREAMING_DISABLED control event, or 'pushEnabled: false', or non-recoverable SSE and Authentication errors */ - - pushEmitter.on(PUSH_NONRETRYABLE_ERROR, function handleNonRetryableError() { - // Note: `stopWorkers` is been called twice, but it is not harmful - disconnectPush(); - pushEmitter.emit(PUSH_SUBSYSTEM_DOWN); // no harm if polling already - }); - - /** Fallbacking with retry due to recoverable SSE and Authentication errors */ - - pushEmitter.on(PUSH_RETRYABLE_ERROR, function handleRetryableError() { // HTTP or network error in SSE connection - // SSE connection is closed to avoid repeated errors due to retries - sseClient.close(); - - // retry streaming reconnect with backoff algorithm - let delayInMillis = connectPushRetryBackoff.scheduleCall(); - - log.info(STREAMING_RECONNECT, [delayInMillis / 1000]); - - pushEmitter.emit(PUSH_SUBSYSTEM_DOWN); // no harm if polling already - }); - - /** Functions related to synchronization (Queues and Workers in the spec) */ - - pushEmitter.on(SPLIT_KILL, splitsUpdateWorker.killSplit); - pushEmitter.on(SPLIT_UPDATE, splitsUpdateWorker.put); - // [Only for server-side] - pushEmitter.on(SEGMENT_UPDATE, segmentsUpdateWorker.put); - - return objectAssign( - // Expose Event Emitter functionality and Event constants - Object.create(pushEmitter), - { - // Expose functionality for starting and stoping push mode: - stop: disconnectPush, // `handleNonRetryableError` cannot be used as `stop`, because it emits PUSH_SUBSYSTEM_DOWN event, which start polling. - - start() { - // Run in next event-loop cycle as in browser - setTimeout(connectPush); - } - } - ); -} diff --git a/src/sync/streaming/types.ts b/src/sync/streaming/types.ts index 69d45219..8c8b59e4 100644 --- a/src/sync/streaming/types.ts +++ b/src/sync/streaming/types.ts @@ -1,4 +1,4 @@ -import { IMySegmentsUpdateData, IMySegmentsUpdateV2Data } from './SSEHandler/types'; +import { IMySegmentsUpdateData, IMySegmentsUpdateV2Data, ISegmentUpdateData, ISplitUpdateData, ISplitKillData } from './SSEHandler/types'; import { ITask } from '../types'; import { IPollingManager, ISegmentsSyncTask } from '../polling/types'; import { IReadinessManager } from '../../readiness/types'; @@ -28,34 +28,21 @@ export type STREAMING_RESET = 'STREAMING_RESET'; export type IPushEvent = PUSH_SUBSYSTEM_UP | PUSH_SUBSYSTEM_DOWN | PUSH_NONRETRYABLE_ERROR | PUSH_RETRYABLE_ERROR | MY_SEGMENTS_UPDATE | MY_SEGMENTS_UPDATE_V2 | SEGMENT_UPDATE | SPLIT_UPDATE | SPLIT_KILL | STREAMING_RESET +type IParsedData = + T extends MY_SEGMENTS_UPDATE ? IMySegmentsUpdateData : + T extends MY_SEGMENTS_UPDATE_V2 ? IMySegmentsUpdateV2Data : + T extends SEGMENT_UPDATE ? ISegmentUpdateData : + T extends SPLIT_UPDATE ? ISplitUpdateData : + T extends SPLIT_KILL ? ISplitKillData : undefined; + /** * EventEmitter used as Feedback Loop between the SyncManager and PushManager, * where the latter pushes messages and the former consumes it */ export interface IPushEventEmitter extends IEventEmitter { - once(event: T, listener: (...args: - T extends MY_SEGMENTS_UPDATE ? [parsedData: IMySegmentsUpdateData, channel: string] : - T extends MY_SEGMENTS_UPDATE_V2 ? [parsedData: IMySegmentsUpdateV2Data] : - T extends SEGMENT_UPDATE ? [changeNumber: number, segmentName: string] : - T extends SPLIT_UPDATE ? [changeNumber: number] : - T extends SPLIT_KILL ? [changeNumber: number, splitName: string, defaultTreatment: string] : - any[]) => void): this; - - on(event: T, listener: (...args: - T extends MY_SEGMENTS_UPDATE ? [parsedData: IMySegmentsUpdateData, channel: string] : - T extends MY_SEGMENTS_UPDATE_V2 ? [parsedData: IMySegmentsUpdateV2Data] : - T extends SEGMENT_UPDATE ? [changeNumber: number, segmentName: string] : - T extends SPLIT_UPDATE ? [changeNumber: number] : - T extends SPLIT_KILL ? [changeNumber: number, splitName: string, defaultTreatment: string] : - any[]) => void): this; - - emit(event: T, ...args: - T extends MY_SEGMENTS_UPDATE ? [parsedData: IMySegmentsUpdateData, channel: string] : - T extends MY_SEGMENTS_UPDATE_V2 ? [parsedData: IMySegmentsUpdateV2Data] : - T extends SEGMENT_UPDATE ? [changeNumber: number, segmentName: string] : - T extends SPLIT_UPDATE ? [changeNumber: number] : - T extends SPLIT_KILL ? [changeNumber: number, splitName: string, defaultTreatment: string] : - any[]): boolean; + once(event: T, listener: (parsedData: IParsedData, channel: T extends MY_SEGMENTS_UPDATE ? string : undefined) => void): this; + on(event: T, listener: (parsedData: IParsedData, channel: T extends MY_SEGMENTS_UPDATE ? string : undefined) => void): this; + emit(event: T, parsedData?: IParsedData, channel?: T extends MY_SEGMENTS_UPDATE ? string : undefined): boolean; } /** diff --git a/src/trackers/impressionObserver/ImpressionObserver.ts b/src/trackers/impressionObserver/ImpressionObserver.ts index 887c73a7..3a1a5d1d 100644 --- a/src/trackers/impressionObserver/ImpressionObserver.ts +++ b/src/trackers/impressionObserver/ImpressionObserver.ts @@ -2,11 +2,11 @@ import { ImpressionDTO } from '../../types'; import LRUCache from '../../utils/LRUCache'; import { IImpressionObserver } from './types'; -export default class ImpressionObserver implements IImpressionObserver { - private cache: LRUCache; - private hasher: (impression: ImpressionDTO) => string | null; +export default class ImpressionObserver implements IImpressionObserver { + private cache: LRUCache; + private hasher: (impression: ImpressionDTO) => K; - constructor(size: number, hasher: (impression: ImpressionDTO) => string | null) { + constructor(size: number, hasher: (impression: ImpressionDTO) => K) { this.cache = new LRUCache(size); this.hasher = hasher; } diff --git a/src/trackers/impressionObserver/impressionObserverCS.ts b/src/trackers/impressionObserver/impressionObserverCS.ts index b6e64c6c..bf86409e 100644 --- a/src/trackers/impressionObserver/impressionObserverCS.ts +++ b/src/trackers/impressionObserver/impressionObserverCS.ts @@ -4,7 +4,7 @@ import { buildKey } from './buildKey'; import { ImpressionDTO } from '../../types'; export function hashImpression32(impression: ImpressionDTO) { - return hash(buildKey(impression)).toString(); + return hash(buildKey(impression)); } const LAST_SEEN_CACHE_SIZE = 500; // cache up to 500 impression hashes From 78e45c57de5dcd4cc7cd2d94c5b75256f3de1dbe Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 16 Sep 2021 15:40:58 -0300 Subject: [PATCH 118/144] added missed UT --- src/sync/streaming/pushManager.ts | 4 ++-- .../impressionObserver/__tests__/buildKey.spec.ts | 9 +++++++++ 2 files changed, 11 insertions(+), 2 deletions(-) create mode 100644 src/trackers/impressionObserver/__tests__/buildKey.spec.ts diff --git a/src/sync/streaming/pushManager.ts b/src/sync/streaming/pushManager.ts index 428eb8ac..52371696 100644 --- a/src/sync/streaming/pushManager.ts +++ b/src/sync/streaming/pushManager.ts @@ -226,6 +226,7 @@ export default function pushManagerFactory( pushEmitter.on(SPLIT_KILL, splitsUpdateWorker.killSplit); pushEmitter.on(SPLIT_UPDATE, splitsUpdateWorker.put); + if (userKey) { pushEmitter.on(MY_SEGMENTS_UPDATE, function handleMySegmentsUpdate(parsedData, channel) { const userKeyHash = channel.split('_')[2]; @@ -249,8 +250,7 @@ export default function pushManagerFactory( forOwn(clients, ({ hash64, worker }) => { if (isInBitmap(bitmap, hash64.hex)) { - // fetch mySegments - worker.put(parsedData.changeNumber); + worker.put(parsedData.changeNumber); // fetch mySegments } }); return; diff --git a/src/trackers/impressionObserver/__tests__/buildKey.spec.ts b/src/trackers/impressionObserver/__tests__/buildKey.spec.ts new file mode 100644 index 00000000..4217c109 --- /dev/null +++ b/src/trackers/impressionObserver/__tests__/buildKey.spec.ts @@ -0,0 +1,9 @@ +import { buildKey } from '../buildKey'; +import { generateImpressions } from './testUtils'; + +test('buildKey', () => { // @ts-ignore + expect(buildKey({})).toBe('undefined:undefined:undefined:undefined:undefined'); + + const imp = generateImpressions(1)[0]; + expect(buildKey(imp)).toBe('key_0:feature_0:someTreatment:in segment all:0'); +}); From bd499edcf0bb27546fe6969063d47671971b184a Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Fri, 17 Sep 2021 11:04:00 -0300 Subject: [PATCH 119/144] localhost with SDK_READY_FROM_CACHE --- package-lock.json | 2 +- package.json | 2 +- src/readiness/readinessManager.ts | 16 +++++++------ src/storages/AbstractSplitsCacheSync.ts | 2 +- .../offline/syncTasks/fromObjectSyncTask.ts | 24 ++++++++++++------- src/sync/submitters/metricsSyncTask.ts | 2 +- src/sync/submitters/submitterSyncTask.ts | 3 ++- 7 files changed, 30 insertions(+), 21 deletions(-) diff --git a/package-lock.json b/package-lock.json index e1ea1f9c..0b9bc9f7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "0.1.1-canary.15", + "version": "0.1.1-canary.16", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index a8341a1f..35b9b864 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "0.1.1-canary.15", + "version": "0.1.1-canary.16", "description": "Split Javascript SDK common components", "main": "cjs/index.js", "module": "esm/index.js", diff --git a/src/readiness/readinessManager.ts b/src/readiness/readinessManager.ts index 2c149708..394ba056 100644 --- a/src/readiness/readinessManager.ts +++ b/src/readiness/readinessManager.ts @@ -41,7 +41,7 @@ export function readinessManagerFactory( // emit SDK_READY_FROM_CACHE let isReadyFromCache = false; - if (splits.splitsCacheLoaded) setTimeout(checkIsReadyFromCache, 0); // don't check status inmediately, to allow attach listeners + if (splits.splitsCacheLoaded) isReadyFromCache = true; // ready from cache, but doesn't emit SDK_READY_FROM_CACHE else splits.once(SDK_SPLITS_CACHE_LOADED, checkIsReadyFromCache); // emit SDK_READY_TIMED_OUT @@ -62,13 +62,15 @@ export function readinessManagerFactory( let isDestroyed = false; function checkIsReadyFromCache() { - // @TODO add condition to emit SDK_READY_FROM_CACHE only if SDK_READY has not been emitted - if (!isReadyFromCache && splits.splitsCacheLoaded) { - // Make it async, in case user callbacks throw an exception - setTimeout(() => { - isReadyFromCache = true; + isReadyFromCache = true; + // Don't emit SDK_READY_FROM_CACHE if SDK_READY has been emitted + if (!isReady) { + try { gate.emit(SDK_READY_FROM_CACHE); - }, 0); + } catch (e) { + // throws user callback exceptions in next tick + setTimeout(() => { throw e; }, 0); + } } } diff --git a/src/storages/AbstractSplitsCacheSync.ts b/src/storages/AbstractSplitsCacheSync.ts index 3c0c251e..9bc31467 100644 --- a/src/storages/AbstractSplitsCacheSync.ts +++ b/src/storages/AbstractSplitsCacheSync.ts @@ -50,7 +50,7 @@ export default abstract class AbstractSplitsCacheSync implements ISplitsCacheSyn * It is used as condition to emit SDK_SPLITS_CACHE_LOADED, and then SDK_READY_FROM_CACHE. */ checkCache(): boolean { - return this.getChangeNumber() > -1; + return false; } /** diff --git a/src/sync/offline/syncTasks/fromObjectSyncTask.ts b/src/sync/offline/syncTasks/fromObjectSyncTask.ts index 0ae98763..0f1b3597 100644 --- a/src/sync/offline/syncTasks/fromObjectSyncTask.ts +++ b/src/sync/offline/syncTasks/fromObjectSyncTask.ts @@ -7,7 +7,7 @@ import syncTaskFactory from '../../syncTask'; import { ISyncTask } from '../../types'; import { ISettings } from '../../../types'; import { CONTROL } from '../../../utils/constants'; -import { SDK_SPLITS_ARRIVED, SDK_SEGMENTS_ARRIVED } from '../../../readiness/constants'; +import { SDK_SPLITS_ARRIVED, SDK_SEGMENTS_ARRIVED, SDK_SPLITS_CACHE_LOADED } from '../../../readiness/constants'; import { SYNC_OFFLINE_DATA, ERROR_SYNC_OFFLINE_LOADING } from '../../../logger/constants'; /** @@ -20,8 +20,8 @@ export function fromObjectUpdaterFactory( settings: ISettings, ): () => Promise { - const log = settings.log; - let firstTime = true; + const log = settings.log, splitsCache = storage.splits; + let startingUp = true; return function objectUpdater() { const splits: [string, string][] = []; @@ -54,14 +54,20 @@ export function fromObjectUpdaterFactory( }); return Promise.all([ - storage.splits.clear(), - storage.splits.addSplits(splits) + splitsCache.clear(), // required to sync removed splits from mock + splitsCache.setChangeNumber(Date.now()), + splitsCache.addSplits(splits) ]).then(() => { readiness.splits.emit(SDK_SPLITS_ARRIVED); - // Only emits SDK_SEGMENTS_ARRIVED the first time for SDK_READY - if (firstTime) { - firstTime = false; - readiness.segments.emit(SDK_SEGMENTS_ARRIVED); + + if (startingUp) { + startingUp = false; + Promise.resolve(splitsCache.checkCache()).then(cacheReady => { + // Emits SDK_READY_FROM_CACHE + if (cacheReady) readiness.splits.emit(SDK_SPLITS_CACHE_LOADED); + // Emits SDK_READY + readiness.segments.emit(SDK_SEGMENTS_ARRIVED); + }); } return true; }); diff --git a/src/sync/submitters/metricsSyncTask.ts b/src/sync/submitters/metricsSyncTask.ts index 2f743884..a5264b14 100644 --- a/src/sync/submitters/metricsSyncTask.ts +++ b/src/sync/submitters/metricsSyncTask.ts @@ -45,5 +45,5 @@ export function latenciesSyncTaskFactory( ): ISyncTask { // don't retry metrics. - return submitterSyncTaskFactory(log, postMetricsLatencies, latenciesCache, metricsRefreshRate, 'latency metrics', latencyTracker, fromCache('latencies')); + return submitterSyncTaskFactory(log, postMetricsLatencies, latenciesCache, metricsRefreshRate, 'latency metrics', latencyTracker, fromCache('latencies'), 0, true); } diff --git a/src/sync/submitters/submitterSyncTask.ts b/src/sync/submitters/submitterSyncTask.ts index e0149bc8..bf92b2e9 100644 --- a/src/sync/submitters/submitterSyncTask.ts +++ b/src/sync/submitters/submitterSyncTask.ts @@ -17,6 +17,7 @@ export function submitterSyncTaskFactory( latencyTracker?: ITimeTracker, fromCacheToPayload?: (cacheData: TState) => any, maxRetries: number = 0, + debugLogs?: boolean ): ISyncTask<[], void> { let retries = 0; @@ -27,7 +28,7 @@ export function submitterSyncTaskFactory( const data = sourceCache.state(); const dataCount: number | '' = typeof data.length === 'number' ? data.length : ''; - log.info(SUBMITTERS_PUSH, [dataCount, dataName]); + log[debugLogs ? 'debug' : 'info'](SUBMITTERS_PUSH, [dataCount, dataName]); const latencyTrackerStop = latencyTracker && latencyTracker.start(); const jsonPayload = JSON.stringify(fromCacheToPayload ? fromCacheToPayload(data) : data); From 5ec5ca22423192dc9eb43162fed85bae97d85af5 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 21 Sep 2021 18:22:17 -0300 Subject: [PATCH 120/144] STREAMING_RESET update --- package-lock.json | 2 +- package.json | 2 +- src/__tests__/mocks/message.STREAMING_RESET.json | 2 +- src/sync/streaming/SSEHandler/NotificationKeeper.ts | 8 ++++++++ src/sync/streaming/SSEHandler/__tests__/index.spec.ts | 8 ++++---- src/sync/streaming/SSEHandler/index.ts | 11 ++--------- src/sync/streaming/SSEHandler/types.ts | 8 ++------ src/sync/streaming/constants.ts | 3 +-- src/sync/streaming/pushManager.ts | 4 ++-- src/sync/streaming/types.ts | 5 ++--- 10 files changed, 24 insertions(+), 29 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0b9bc9f7..8e9ed3bb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "0.1.1-canary.16", + "version": "0.1.1-canary.17", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 35b9b864..6e44d0cb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "0.1.1-canary.16", + "version": "0.1.1-canary.17", "description": "Split Javascript SDK common components", "main": "cjs/index.js", "module": "esm/index.js", diff --git a/src/__tests__/mocks/message.STREAMING_RESET.json b/src/__tests__/mocks/message.STREAMING_RESET.json index ee1c13ea..791d9ded 100644 --- a/src/__tests__/mocks/message.STREAMING_RESET.json +++ b/src/__tests__/mocks/message.STREAMING_RESET.json @@ -1,4 +1,4 @@ { "type": "message", - "data": "{\"timestamp\":1457552660000,\"data\":\"{\\\"type\\\":\\\"STREAMING_RESET\\\"}\"}" + "data": "{\"timestamp\":1457552660000,\"data\":\"{\\\"type\\\":\\\"CONTROL\\\",\\\"controlType\\\":\\\"STREAMING_RESET\\\"}\"}" } \ No newline at end of file diff --git a/src/sync/streaming/SSEHandler/NotificationKeeper.ts b/src/sync/streaming/SSEHandler/NotificationKeeper.ts index ce2ba8f1..1df88250 100644 --- a/src/sync/streaming/SSEHandler/NotificationKeeper.ts +++ b/src/sync/streaming/SSEHandler/NotificationKeeper.ts @@ -62,6 +62,14 @@ export default function notificationKeeperFactory(pushEmitter: IPushEventEmitter }, handleControlEvent(controlType: ControlType, channel: string, timestamp: number) { + /* STREAMING_RESET control event is handled by PushManager directly since it doesn't require + * tracking timestamp and state like OCCUPANCY or CONTROL. It also ignores previous + * OCCUPANCY and CONTROL notifications, and whether PUSH_SUBSYSTEM_DOWN has been emitted or not */ + if (controlType === ControlType.STREAMING_RESET) { + pushEmitter.emit(controlType); + return; + } + for (let i = 0; i < channels.length; i++) { const c = channels[i]; if (c.regex.test(channel)) { diff --git a/src/sync/streaming/SSEHandler/__tests__/index.spec.ts b/src/sync/streaming/SSEHandler/__tests__/index.spec.ts index 9324bf84..79f8c573 100644 --- a/src/sync/streaming/SSEHandler/__tests__/index.spec.ts +++ b/src/sync/streaming/SSEHandler/__tests__/index.spec.ts @@ -1,6 +1,6 @@ // @ts-nocheck import SSEHandlerFactory from '..'; -import { PUSH_SUBSYSTEM_UP, PUSH_NONRETRYABLE_ERROR, PUSH_SUBSYSTEM_DOWN, PUSH_RETRYABLE_ERROR, MY_SEGMENTS_UPDATE, SEGMENT_UPDATE, SPLIT_KILL, SPLIT_UPDATE, MY_SEGMENTS_UPDATE_V2, STREAMING_RESET } from '../../constants'; +import { PUSH_SUBSYSTEM_UP, PUSH_NONRETRYABLE_ERROR, PUSH_SUBSYSTEM_DOWN, PUSH_RETRYABLE_ERROR, MY_SEGMENTS_UPDATE, SEGMENT_UPDATE, SPLIT_KILL, SPLIT_UPDATE, MY_SEGMENTS_UPDATE_V2, ControlType } from '../../constants'; import { loggerMock } from '../../../../logger/__tests__/sdkLogger.mock'; // update messages @@ -80,7 +80,7 @@ test('`handleOpen` and `handlerMessage` for OCCUPANCY notifications (Notificatio expect(pushEmitter.emit).toHaveBeenLastCalledWith(SPLIT_UPDATE, { type: 'SPLIT_UPDATE', changeNumber: 1457552620999 }); // must handle update message if streaming on after an OCCUPANCY event sseHandler.handleMessage(streamingReset); - expect(pushEmitter.emit).toHaveBeenLastCalledWith(STREAMING_RESET); // must handle streaming reset + expect(pushEmitter.emit).toHaveBeenLastCalledWith(ControlType.STREAMING_RESET); // must handle streaming reset expect(pushEmitter.emit).toBeCalledTimes(7); // must not emit PUSH_SUBSYSTEM_UP if streaming is already on and another channel has publishers }); @@ -114,7 +114,7 @@ test('`handlerMessage` for CONTROL notifications (NotificationKeeper)', () => { sseHandler.handleMessage({ data: '{ "data": "{\\"type\\":\\"SPLIT_UPDATE\\",\\"changeNumber\\":1457552620999 }" }' }); expect(pushEmitter.emit).toHaveBeenLastCalledWith(SPLIT_UPDATE, { type: 'SPLIT_UPDATE', changeNumber: 1457552620999 }); // must handle update message if streaming on after a CONTROL event sseHandler.handleMessage(streamingReset); - expect(pushEmitter.emit).toHaveBeenLastCalledWith(STREAMING_RESET); // must handle streaming reset + expect(pushEmitter.emit).toHaveBeenLastCalledWith(ControlType.STREAMING_RESET); // must handle streaming reset sseHandler.handleMessage(controlStreamingDisabledSec); // testing STREAMING_DISABLED with second region expect(pushEmitter.emit).toHaveBeenLastCalledWith(PUSH_NONRETRYABLE_ERROR); // must emit PUSH_NONRETRYABLE_ERROR if received a STREAMING_DISABLED control message @@ -168,7 +168,7 @@ test('`handlerMessage` for update notifications (NotificationProcessor) and stre expect(pushEmitter.emit).toHaveBeenLastCalledWith(MY_SEGMENTS_UPDATE_V2, ...expectedParams); // must emit MY_SEGMENTS_UPDATE_V2 with the message parsed data sseHandler.handleMessage(streamingReset); - expect(pushEmitter.emit).toHaveBeenLastCalledWith(STREAMING_RESET); // must emit STREAMING_RESET + expect(pushEmitter.emit).toHaveBeenLastCalledWith(ControlType.STREAMING_RESET); // must emit STREAMING_RESET }); diff --git a/src/sync/streaming/SSEHandler/index.ts b/src/sync/streaming/SSEHandler/index.ts index de9b5eba..b0cd7069 100644 --- a/src/sync/streaming/SSEHandler/index.ts +++ b/src/sync/streaming/SSEHandler/index.ts @@ -1,6 +1,6 @@ import { errorParser, messageParser } from './NotificationParser'; import notificationKeeperFactory from './NotificationKeeper'; -import { PUSH_RETRYABLE_ERROR, PUSH_NONRETRYABLE_ERROR, OCCUPANCY, CONTROL, STREAMING_RESET, MY_SEGMENTS_UPDATE, MY_SEGMENTS_UPDATE_V2, SEGMENT_UPDATE, SPLIT_KILL, SPLIT_UPDATE } from '../constants'; +import { PUSH_RETRYABLE_ERROR, PUSH_NONRETRYABLE_ERROR, OCCUPANCY, CONTROL, MY_SEGMENTS_UPDATE, MY_SEGMENTS_UPDATE_V2, SEGMENT_UPDATE, SPLIT_KILL, SPLIT_UPDATE } from '../constants'; import { IPushEventEmitter } from '../types'; import { ISseEventHandler } from '../SSEClient/types'; import { INotificationError, INotificationMessage } from './types'; @@ -68,7 +68,7 @@ export default function SSEHandlerFactory(log: ILogger, pushEmitter: IPushEventE log.debug(STREAMING_NEW_MESSAGE, [data]); // we only handle update events if streaming is up. - if (!notificationKeeper.isStreamingUp() && [OCCUPANCY, CONTROL, STREAMING_RESET].indexOf(parsedData.type) === -1) + if (!notificationKeeper.isStreamingUp() && [OCCUPANCY, CONTROL].indexOf(parsedData.type) === -1) return; switch (parsedData.type) { @@ -91,13 +91,6 @@ export default function SSEHandlerFactory(log: ILogger, pushEmitter: IPushEventE notificationKeeper.handleControlEvent(parsedData.controlType, channel, timestamp); break; - /* STREAMING_RESET event is handled by PushManager directly since it doesn't require - tracking timestamp and state like OCCUPANCY or CONTROL. It also ignores previous - OCCUPANCY and CONTROL notifications, and whether PUSH_SUBSYSTEM_DOWN has been emitted or not */ - case STREAMING_RESET: - pushEmitter.emit(STREAMING_RESET); - break; - default: break; } diff --git a/src/sync/streaming/SSEHandler/types.ts b/src/sync/streaming/SSEHandler/types.ts index db271614..937fb280 100644 --- a/src/sync/streaming/SSEHandler/types.ts +++ b/src/sync/streaming/SSEHandler/types.ts @@ -1,5 +1,5 @@ import { ControlType } from '../constants'; -import { MY_SEGMENTS_UPDATE, MY_SEGMENTS_UPDATE_V2, SEGMENT_UPDATE, SPLIT_UPDATE, SPLIT_KILL, CONTROL, OCCUPANCY, STREAMING_RESET } from '../types'; +import { MY_SEGMENTS_UPDATE, MY_SEGMENTS_UPDATE_V2, SEGMENT_UPDATE, SPLIT_UPDATE, SPLIT_KILL, CONTROL, OCCUPANCY } from '../types'; export interface IMySegmentsUpdateData { type: MY_SEGMENTS_UPDATE, @@ -65,11 +65,7 @@ export interface IOccupancyData { } } -export interface IStreamingResetData { - type: STREAMING_RESET -} - -export type INotificationData = IMySegmentsUpdateData | IMySegmentsUpdateV2Data | ISegmentUpdateData | ISplitUpdateData | ISplitKillData | IControlData | IOccupancyData | IStreamingResetData +export type INotificationData = IMySegmentsUpdateData | IMySegmentsUpdateV2Data | ISegmentUpdateData | ISplitUpdateData | ISplitKillData | IControlData | IOccupancyData export type INotificationMessage = { parsedData: INotificationData, channel: string, timestamp: number, data: string } export type INotificationError = Event & { parsedData?: any, message?: string } diff --git a/src/sync/streaming/constants.ts b/src/sync/streaming/constants.ts index da381707..8afb41a6 100644 --- a/src/sync/streaming/constants.ts +++ b/src/sync/streaming/constants.ts @@ -35,10 +35,9 @@ export const SPLIT_UPDATE = 'SPLIT_UPDATE'; export const CONTROL = 'CONTROL'; export const OCCUPANCY = 'OCCUPANCY'; -export const STREAMING_RESET = 'STREAMING_RESET'; - export enum ControlType { STREAMING_DISABLED = 'STREAMING_DISABLED', STREAMING_PAUSED = 'STREAMING_PAUSED', STREAMING_RESUMED = 'STREAMING_RESUMED', + STREAMING_RESET = 'STREAMING_RESET', } diff --git a/src/sync/streaming/pushManager.ts b/src/sync/streaming/pushManager.ts index 52371696..a86673d1 100644 --- a/src/sync/streaming/pushManager.ts +++ b/src/sync/streaming/pushManager.ts @@ -15,7 +15,7 @@ import SSEClient from './SSEClient'; import { IFetchAuth } from '../../services/types'; import { ISettings } from '../../types'; import { getMatching } from '../../utils/key'; -import { MY_SEGMENTS_UPDATE, MY_SEGMENTS_UPDATE_V2, PUSH_NONRETRYABLE_ERROR, PUSH_SUBSYSTEM_DOWN, SECONDS_BEFORE_EXPIRATION, SEGMENT_UPDATE, SPLIT_KILL, SPLIT_UPDATE, PUSH_RETRYABLE_ERROR, PUSH_SUBSYSTEM_UP, STREAMING_RESET } from './constants'; +import { MY_SEGMENTS_UPDATE, MY_SEGMENTS_UPDATE_V2, PUSH_NONRETRYABLE_ERROR, PUSH_SUBSYSTEM_DOWN, SECONDS_BEFORE_EXPIRATION, SEGMENT_UPDATE, SPLIT_KILL, SPLIT_UPDATE, PUSH_RETRYABLE_ERROR, PUSH_SUBSYSTEM_UP, ControlType } from './constants'; import { IPlatform } from '../../sdkFactory/types'; import { STREAMING_FALLBACK, STREAMING_REFRESH_TOKEN, STREAMING_CONNECTING, STREAMING_DISABLED, ERROR_STREAMING_AUTH, STREAMING_DISCONNECTING, STREAMING_RECONNECT, STREAMING_PARSING_MY_SEGMENTS_UPDATE_V2 } from '../../logger/constants'; import { KeyList, UpdateStrategy } from './SSEHandler/types'; @@ -212,7 +212,7 @@ export default function pushManagerFactory( /** STREAMING_RESET notification. Unlike a PUSH_RETRYABLE_ERROR, it doesn't emit PUSH_SUBSYSTEM_DOWN to fallback polling */ - pushEmitter.on(STREAMING_RESET, function handleStreamingReset() { + pushEmitter.on(ControlType.STREAMING_RESET, function handleStreamingReset() { if (disconnected) return; // should never happen // Minimum required clean-up. diff --git a/src/sync/streaming/types.ts b/src/sync/streaming/types.ts index 8c8b59e4..6832a335 100644 --- a/src/sync/streaming/types.ts +++ b/src/sync/streaming/types.ts @@ -6,6 +6,7 @@ import { IFetchAuth } from '../../services/types'; import { IStorageSync } from '../../storages/types'; import { IEventEmitter, ISettings } from '../../types'; import { IPlatform } from '../../sdkFactory/types'; +import { ControlType } from './constants'; // Internal SDK events, subscribed by SyncManager and PushManager export type PUSH_SUBSYSTEM_UP = 'PUSH_SUBSYSTEM_UP' @@ -24,9 +25,7 @@ export type SPLIT_UPDATE = 'SPLIT_UPDATE'; export type CONTROL = 'CONTROL'; export type OCCUPANCY = 'OCCUPANCY'; -export type STREAMING_RESET = 'STREAMING_RESET'; - -export type IPushEvent = PUSH_SUBSYSTEM_UP | PUSH_SUBSYSTEM_DOWN | PUSH_NONRETRYABLE_ERROR | PUSH_RETRYABLE_ERROR | MY_SEGMENTS_UPDATE | MY_SEGMENTS_UPDATE_V2 | SEGMENT_UPDATE | SPLIT_UPDATE | SPLIT_KILL | STREAMING_RESET +export type IPushEvent = PUSH_SUBSYSTEM_UP | PUSH_SUBSYSTEM_DOWN | PUSH_NONRETRYABLE_ERROR | PUSH_RETRYABLE_ERROR | MY_SEGMENTS_UPDATE | MY_SEGMENTS_UPDATE_V2 | SEGMENT_UPDATE | SPLIT_UPDATE | SPLIT_KILL | ControlType.STREAMING_RESET type IParsedData = T extends MY_SEGMENTS_UPDATE ? IMySegmentsUpdateData : From 84d627be190e5d6869e32229b4aac9d48ac0d0c5 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 27 Sep 2021 15:29:21 -0300 Subject: [PATCH 121/144] 60 secs as default connDelay --- src/sync/streaming/pushManager.ts | 4 ++-- src/utils/settingsValidation/__tests__/splitFilters.spec.ts | 6 +++--- src/utils/settingsValidation/splitFilters.ts | 1 - 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/sync/streaming/pushManager.ts b/src/sync/streaming/pushManager.ts index a86673d1..93d47127 100644 --- a/src/sync/streaming/pushManager.ts +++ b/src/sync/streaming/pushManager.ts @@ -98,8 +98,8 @@ export default function pushManagerFactory( // Set token refresh 10 minutes before `expirationTime - issuedAt` const decodedToken = authData.decodedToken; const refreshTokenDelay = decodedToken.exp - decodedToken.iat - SECONDS_BEFORE_EXPIRATION; - // connDelay is present in AuthV2 but not in AuthV1 - const connDelay = authData.connDelay || 0; + // Default connDelay of 60 secs + const connDelay = typeof authData.connDelay === 'number' && authData.connDelay >= 0 ? authData.connDelay : 60; log.info(STREAMING_REFRESH_TOKEN, [refreshTokenDelay, connDelay]); diff --git a/src/utils/settingsValidation/__tests__/splitFilters.spec.ts b/src/utils/settingsValidation/__tests__/splitFilters.spec.ts index 3aa994dd..1e23b752 100644 --- a/src/utils/settingsValidation/__tests__/splitFilters.spec.ts +++ b/src/utils/settingsValidation/__tests__/splitFilters.spec.ts @@ -11,7 +11,7 @@ import { SETTINGS_SPLITS_FILTER, ERROR_INVALID, ERROR_EMPTY_ARRAY, WARN_SPLITS_F describe('validateSplitFilters', () => { - let defaultOutput = { + const defaultOutput = { validFilters: [], queryString: null, groupedFilters: { byName: [], byPrefix: [] } @@ -38,11 +38,11 @@ describe('validateSplitFilters', () => { test('Returns object with null queryString, if `splitFilters` contains invalid filters or contains filters with no values or invalid values', () => { - let splitFilters: any[] = [ + const splitFilters: any[] = [ { type: 'byName', values: [] }, { type: 'byName', values: [] }, { type: 'byPrefix', values: [] }]; - let output = { + const output = { validFilters: [...splitFilters], queryString: null, groupedFilters: { byName: [], byPrefix: [] } diff --git a/src/utils/settingsValidation/splitFilters.ts b/src/utils/settingsValidation/splitFilters.ts index 8a7a1303..eb7b0371 100644 --- a/src/utils/settingsValidation/splitFilters.ts +++ b/src/utils/settingsValidation/splitFilters.ts @@ -107,7 +107,6 @@ export function validateSplitFilters(log: ILogger, maybeSplitFilters: any, mode: } // Validate filters and group their values by filter type inside `groupedFilters` object - // Assign the valid filters to the output of the validator by using filter function res.validFilters = maybeSplitFilters.filter((filter, index) => { if (filter && validateFilterType(filter.type) && Array.isArray(filter.values)) { res.groupedFilters[filter.type as SplitIO.SplitFilterType] = res.groupedFilters[filter.type as SplitIO.SplitFilterType].concat(filter.values); From 51cd16a22ef1e0ad8cb207faf2bc758b3e55a66a Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Fri, 1 Oct 2021 15:23:17 -0300 Subject: [PATCH 122/144] Fix typescript compiler errors related to const enum usage in RN --- .eslintrc | 16 +++++++++++++++- src/sync/streaming/SSEHandler/types.ts | 4 ++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/.eslintrc b/.eslintrc index d080d815..9a4d4f70 100644 --- a/.eslintrc +++ b/.eslintrc @@ -65,7 +65,21 @@ "plugin:compat/recommended" ], "rules": { - "no-restricted-syntax": ["error", "ForOfStatement", "ForInStatement"], + "no-restricted-syntax": [ + "error", + { + "selector": "ForOfStatement", + "message": "Avoid for-of syntax in favor of a smaller transpiled code" + }, + { + "selector": "ForInStatement", + "message": "Don't use for-in syntax in libraries, because it might iterate over properties polluted by external code" + }, + { + "selector": "TSEnumDeclaration[const=true]", + "message": "Don't declare const enums, because they are not supported by Babel wich is used for building RN SDK" + } + ], "compat/compat": ["error", "defaults, not ie < 10, not node < 6"], "no-throw-literal": "error" }, diff --git a/src/sync/streaming/SSEHandler/types.ts b/src/sync/streaming/SSEHandler/types.ts index 937fb280..293689ab 100644 --- a/src/sync/streaming/SSEHandler/types.ts +++ b/src/sync/streaming/SSEHandler/types.ts @@ -8,13 +8,13 @@ export interface IMySegmentsUpdateData { segmentList?: string[] } -export const enum Compression { +export enum Compression { None = 0, Gzip = 1, Zlib = 2 } -export const enum UpdateStrategy { +export enum UpdateStrategy { UnboundedFetchRequest = 0, BoundedFetchRequest = 1, KeyList = 2, From f578d4d1fbcb6867d229689ad1a0774f4211c9db Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Fri, 1 Oct 2021 15:49:46 -0300 Subject: [PATCH 123/144] performance improvements --- src/sync/streaming/pushManager.ts | 13 +- .../impressionObserver/ImpressionObserver.ts | 2 - .../impressionObserverSS.ts | 2 +- .../__tests__/mocks/murmur3_86_uuids.csv | 10000 ++++++++++++++++ src/utils/murmur3/__tests__/murmur3.spec.ts | 8 +- src/utils/murmur3/murmur3_128.ts | 2 +- src/utils/murmur3/murmur3_128_x86.ts | 5 +- 7 files changed, 10012 insertions(+), 20 deletions(-) create mode 100644 src/utils/murmur3/__tests__/mocks/murmur3_86_uuids.csv diff --git a/src/sync/streaming/pushManager.ts b/src/sync/streaming/pushManager.ts index 93d47127..e7b7caac 100644 --- a/src/sync/streaming/pushManager.ts +++ b/src/sync/streaming/pushManager.ts @@ -267,19 +267,12 @@ export default function pushManagerFactory( } forOwn(clients, ({ hash64, worker }) => { - if (added.has(hash64.dec)) { + const add = added.has(hash64.dec) ? true : removed.has(hash64.dec) ? false : undefined; + if (add !== undefined) { worker.put(parsedData.changeNumber, { name: parsedData.segmentName, - add: true + add }); - return; - } - if (removed.has(hash64.dec)) { - worker.put(parsedData.changeNumber, { - name: parsedData.segmentName, - add: false - }); - return; } }); return; diff --git a/src/trackers/impressionObserver/ImpressionObserver.ts b/src/trackers/impressionObserver/ImpressionObserver.ts index 3a1a5d1d..666fa463 100644 --- a/src/trackers/impressionObserver/ImpressionObserver.ts +++ b/src/trackers/impressionObserver/ImpressionObserver.ts @@ -12,8 +12,6 @@ export default class ImpressionObserver implements II } testAndSet(impression: ImpressionDTO) { - if (!impression) return; - const hash = this.hasher(impression); const previous = this.cache.get(hash); this.cache.set(hash, impression.time); diff --git a/src/trackers/impressionObserver/impressionObserverSS.ts b/src/trackers/impressionObserver/impressionObserverSS.ts index d3385c94..828cfc21 100644 --- a/src/trackers/impressionObserver/impressionObserverSS.ts +++ b/src/trackers/impressionObserver/impressionObserverSS.ts @@ -1,5 +1,5 @@ import ImpressionObserver from './ImpressionObserver'; -import { hash128 } from '../../utils/murmur3/murmur3_128'; +import { hash128 } from '../../utils/murmur3/murmur3_128_x86'; import { buildKey } from './buildKey'; import { ImpressionDTO } from '../../types'; diff --git a/src/utils/murmur3/__tests__/mocks/murmur3_86_uuids.csv b/src/utils/murmur3/__tests__/mocks/murmur3_86_uuids.csv new file mode 100644 index 00000000..287abf27 --- /dev/null +++ b/src/utils/murmur3/__tests__/mocks/murmur3_86_uuids.csv @@ -0,0 +1,10000 @@ +d4a54f9e-f21c-0988-bcc8-e89da126ff2d,572008525,6110769680312780039 +d37fc58c-1037-7431-3cee-03426979dd8e,4164581979,11951443564137380472 +92bd9de9-3e9d-6756-6020-c304eec1105b,834179676,6521191437051114291 +04242919-7259-c375-415a-819dc78f069b,1281492683,11624151202307826664 +7effb24e-cc24-94c3-9c02-b229d6fe5912,1654206287,17515355730787830281 +903420ab-41fb-700f-9b80-630be9ae6fab,2539827769,4748916454346960012 +73a48474-ad55-432c-e814-72e7138e92fb,3711496963,4158959184796757093 +650d17a7-13a8-1dc4-5207-b037155d7ddd,3881768690,16078635943815928471 +002da80f-2874-4c3a-3123-9d299857be0a,3695857284,14421021020809728893 +2db99d28-0151-2cdc-32bd-89ba863ac96f,131324347,13610619508425419599 +b60b9a0e-d1b2-1fa1-5c1b-bd33cded8086,4232273712,1735637088762833411 +aa27d5df-83ea-10cd-a4ac-103e701fe984,2659416369,10075660319729239292 +8b8c06cb-b964-8fa0-cf6a-87cfabe5063a,3170763384,12019499630719827728 +d6c28c78-429f-3fe0-5298-e31253017d77,3307510501,7536749457398765968 +ce68fe0a-f737-788d-d02d-b6135a3bad45,2224726756,16385898734448800912 +59e21f39-e338-c87d-d409-38aa95d197f7,2358482912,17338517665879570430 +a0e2d55c-7105-2e25-8f6f-bd2caa15ed61,2947432973,16864144094594945306 +ad271ab2-a5b9-7914-f14e-12ba0b9aead8,2872868023,13183944309034098153 +95ad42e9-a82b-2497-9f64-c2ef8ca6f7d1,2092831961,13791114210372761095 +2dd5e9f1-c438-22c7-ab52-e6b63ba96de4,243472212,8325961748031818171 +d2b85cde-0e06-04bb-bbe3-0b455b8a70f2,1556243367,3740769227826616178 +9fbdb977-22ac-8545-fcac-a393f13cd391,2964441184,9026286771441902056 +0c90c03e-a7b9-2354-1cc4-03455d55064f,664473457,16616431203056497209 +3a25073d-8acd-fd0c-77ca-cabe9bbf2ee2,451046140,15026085476621529029 +7cab66eb-3900-6c5e-ef65-48fb72177738,1419494605,14007620125094865074 +0e6a5de5-62d1-4469-3e4b-bfbee623e422,2527770838,6437992571180654186 +b93c6f2a-97ab-b6dd-9516-627dd2e6584d,3640358391,8968042437174876802 +fabea4c3-946a-b7b9-f734-5a161ca00f9d,25651457,15778762564457029284 +3e78935a-da64-f52c-07a4-9b590ebd9997,231127444,1477103003870813632 +13b7a26b-5b38-1411-a753-271f1373311c,219052932,15585597177397388509 +58c345c5-0dc7-145f-b998-af41f9d05c5f,3282203814,13938294911937099893 +23e1e994-db6c-dc7c-453b-81470aacb410,1313424997,8827652388504993981 +f6574ef4-eefa-01aa-f849-9423f77b0fd6,1920720379,15135750343001570779 +25256927-52e1-46ab-e485-7c5ecfcbd962,2861211123,7228689118696639643 +6cd3053f-53ff-d3a6-0a0b-81a3a2bd9d95,891573851,14095614401064479831 +6af221f6-c14e-08f1-0011-81a1fda7ad75,3389906649,12895569853495670094 +4494c47f-c79e-3a94-ea73-247f827985a4,2114124684,3593790374077401577 +981e5700-24f3-623a-250b-a7e31592519d,3538730704,18434329787190665237 +6d04c277-4b50-7fef-3ec3-eb7b6b55e285,3277413154,10664477280267924328 +0ebf2849-fcfd-291b-5a4b-30553a239a61,3449525661,11531115310881941062 +1fca4498-443d-e6cc-1411-515a0023f176,735702659,5603065018432516928 +5f83f43e-82ad-fbbb-5a9b-9d07151ba58c,3723386557,951073065887448214 +4535653f-fdf5-c963-4c9e-53f6231237a1,805478918,13834191545195498866 +f81044c4-f05a-bdb9-e7a9-38c49f74ea45,1587765005,2012955415412816891 +9bc85c35-2c70-6193-7327-4a8c7cc6e8bb,534096939,14814165426867102771 +40269280-7cb3-75a6-1117-c76784ffba0c,4146278695,13032939822837403572 +431fc362-21be-f205-9953-76e68dc25241,2174155412,6800609813657331379 +fc7cda4f-de9e-7e7f-62a0-f782b26493e7,2704683504,1836509424651348083 +3526d4bf-e658-c701-45d4-d8023f5cd8d3,4010876178,18053895776109965919 +7127a7bd-b884-86b8-15e6-1f24b172166a,4034492745,6639238365325412744 +fce3bd25-a196-c641-26b2-ef685cdcc389,850328618,2850557489867056504 +a6c98eb8-4467-a5e2-660d-591168bac82c,1747579497,2603632599240934080 +f71ed75e-b76f-c6cc-fa24-c7f5b789d802,3471539634,15426031105526821351 +a0f290bb-bbb1-6417-675c-e51ab5530c6f,4260160582,16318365611039628900 +f33994a7-27f0-d41e-e0bb-64a8db92f4cf,789982897,5627716230030979071 +997af189-a491-31dd-96d9-4c3323b2836a,4222325228,2718006380727985819 +ced335bc-e50c-2c7b-d8c4-0689e24217c4,1382900212,2963258459539661954 +5aff02da-5366-b993-dcd1-3048b5a6c758,2834638765,14157408770736027667 +3e261b4c-577a-d7d6-1142-bfc9a4c14b9c,797859111,9083544606452081763 +d5798960-5abf-5d59-5776-7709b8b5a143,1938299602,15864725264242373257 +11792af1-4e45-1e05-dbd0-68f74396c4fd,953335631,12937609759299565176 +e8de0af1-e528-dd4e-0a0a-315e1bbb47cb,1477102656,6803047079640003590 +93e24216-ed73-d891-ac52-1d0fae7a81fe,745787363,16250825406789648589 +ee7e030a-8a27-9a4c-d29d-20475e1884e1,471291089,13896594461305347796 +b3b9609a-9ee5-5200-0923-e90623408ae9,2547125905,9491865599752642657 +9ff6b679-14e4-24c8-a52b-ccb5691148c7,2474740247,15024398727503542865 +dbfa7d42-e484-4845-aecb-2289ef21fd01,243912091,1372274219717515911 +1d1d1e7f-6efd-a6df-d344-0ca409625499,2982631495,12846036845666019944 +47f4b325-41fa-f1d6-9fc4-7972ac945358,3169714675,17269890794755951979 +270b793b-85ec-973d-740a-d6ac39780031,1328548839,10425194033778738297 +415be986-f35b-7beb-d94b-2b5484b4ddd2,205794869,10031360501332530022 +9b31da17-65e0-3f0a-2b84-3f5869c0e634,3903596548,8135959220925945448 +0b4834b1-f7c6-9beb-0dae-35e377a5310d,3632987778,7703596245154917347 +12c55fc1-61db-9203-070c-cada3b532734,923009852,12568970089567305631 +506b80f7-d52b-91c9-7744-6320cd897514,2250696931,10222982395374936548 +13829fe5-cd35-c29f-6754-ab39b270b9bc,89461565,3780642068297662899 +2b1727cd-5651-9e05-2cd9-7f5ff4290e25,2813252572,9351764614575524523 +c1ab6eb6-bc14-aa31-2579-66256f8b2036,1221532356,8587576694411269605 +3e3bb83e-fe4c-5dbb-5ccf-2fff8037c810,2529305952,9535016289104292122 +d4117ce3-bb07-f63f-8337-e7bb8aabac7f,2409227254,10603682998588707688 +b49fd9b9-efaf-122a-6bdd-359a51c1e6ce,2745223350,4720702529687986254 +c9808773-52e9-31b9-2828-59f8a87b94c8,1034857454,1082393566351213171 +1f4984fa-0800-076c-cd14-98ec35590ebf,925687488,5795072768010566870 +61e3a2b9-6e72-57e7-abb3-b6642e04e18a,174168821,2212146776777814906 +92af2d05-448e-02a8-2956-65a4b97a04bb,27292304,1507919516693427818 +051e65eb-8ffa-afc2-8cbf-c51eb7e84046,2958359173,6234228223196626897 +a1097b5b-fa72-8290-a5e5-4e3f7b85edbf,2398851142,11217177613832380574 +84b1c94b-8588-18c3-7c7a-a9a0e2cdd6be,723475327,14410651180552238409 +9ddd0dda-941c-2da6-dc75-4d75e3efba4f,1916974770,7975517106874244439 +e3ee6dfc-beb0-1153-61fd-c4a1c173ac95,3779654817,4429028600606595783 +dfa84446-f880-3465-60c4-d6c4774fdaa0,4239550761,16224168269144047830 +3bc7decc-8298-4d33-5b43-c489fbd936ad,192788597,9079703930021675129 +0286afd9-3ec6-9b45-f019-2ea9734be8fd,2782252641,12311044188492560886 +f8b6b965-bc06-826d-318d-cba9026a5a1d,1968842709,13188615772528556694 +962d08e5-0e72-f478-cb64-4fdcbde2f259,3774521966,8872090316205972053 +c5cc7a19-f441-a629-1b42-577b1ead7131,4140728323,15128037282167073728 +bc8497df-50d5-6cba-d1a4-9fa8daa2a174,4173310189,3536453024399711317 +b28831dd-7dac-aa12-dfee-4c93245841b5,6439639,8079967594791246923 +d2049679-b0dd-5630-690c-168f774d392e,89198147,13410621222603863127 +2729e1eb-9aff-73eb-25c3-0dcf6f445b35,1512562349,6657516799837946038 +f1af56a1-a355-3696-d715-2592050d0428,1781886991,292701526608530807 +3a273ecf-d86c-781f-46a4-1a61dc62a074,2948452345,1814994012980786948 +22fb0253-9a6c-f4ff-f730-79c722a2b152,103186744,17743885859573385090 +dd0e1b3e-e7f2-6e00-0e93-784745c804a8,2549537664,3485107529088992904 +398bb6de-66f5-b56a-db36-581262ca4f6a,1213951991,2686258663298480477 +d8918a56-68cb-812a-90ca-5f16e6407481,4248919027,10943170681903136659 +e7118548-d66f-8a9a-bcde-1adc602c5d0f,1369328405,9014527485617625905 +3510b94c-6715-29ce-a3ef-795e045dcece,3180769216,9403334992504306610 +0de5124a-563d-cc42-eacc-ef3583fee605,756062226,13734442449303511586 +01b6e2b8-e32f-dcec-8757-85bf093b2a2e,2562323741,9426024825518264087 +d0a88675-f7eb-bcb1-bbe3-c4a6bbeb22ba,2186868918,9753610423884826181 +5218337d-7dd5-39e1-cf2c-7d613e506145,2994650164,14187635336348946445 +1fd94a88-e975-da9f-b4e8-d357f98f3665,2608613472,13278152499682684526 +15881f75-4e42-d9ca-78a0-77cca15d9dd3,720488490,954848535188394438 +a6e44f12-910a-a6f5-0979-0c55ac87a01a,441032980,5834761742826089216 +b9cae979-016b-5559-b734-9ffc97e5cb4c,1294203818,6160021562930596171 +ddb8df76-e3e3-1629-f411-90bba2ab6b2d,562493824,8754882671493246036 +2a2d1510-e627-2b52-e06a-a92ab9f7e48b,2255125327,5634188235137559440 +27c43443-a4cd-0ebf-76a7-6fed96a40d09,4225263772,3720085863578308976 +c0b946a5-c90c-1605-5c7e-f8971fe6f70f,3777505029,16036995156229967249 +44b2f4d0-98b6-f34e-1ef7-22f010d2b627,3850338653,11637511597807476061 +c5c4aee3-702b-481a-97ee-0781517e980a,89181772,16570302944786701195 +70f44c08-9f7d-db78-70b7-d731ffeed493,3665497153,3159218070692154597 +48556661-2fe0-7557-559f-56ac46675e7a,1141224319,4886068688429608008 +1cc74dca-a6f3-3e16-f0d6-aa2a3b4b6002,2406926676,90106159773718615 +43b312b3-1f7b-be9d-adff-68053911fe80,3412830058,10460575761238287449 +3df19c3f-0859-e6a9-775b-1703cbdb7492,3413323537,2572647050174715384 +65364246-03a1-403a-983a-24d96469d330,3176406699,7918014503628737138 +4df5a8cd-f2f9-76bf-a61e-c745f06254df,3128995098,2237848883135832258 +98d069c8-7983-a606-0d2d-e0316e164307,2671651659,14065439130604171443 +fb1bdf12-79c9-0533-2019-4906a2c69262,945778795,4507776024720458247 +70d337c4-e692-985c-d724-aa00f86c9cab,3152974945,6381140575419600190 +e4f21110-7123-7526-0233-dc114b19e076,1863319659,12215870986886901064 +caeb293f-869f-750a-39f7-1b029453daa5,720644639,14036664169219113709 +8a61225e-bbfe-2739-1eb4-2c90698dc9ae,3290998422,18054586122416800064 +beb2eb5c-88e6-d208-2679-0d7e3af9e6d8,952469850,4238065208086612103 +b0a62188-1417-0cdb-7eb4-0ad81e931958,467987846,3386278304718196359 +23c0f104-1a67-0747-d572-be3801efd1ba,2412172023,4303450617369245532 +f28a37a7-6f51-1562-d58d-52ac7dbb75af,3758681382,8655377841190135733 +ffad8bd3-5ec9-d9da-6c64-5f6cc66fd291,4131480030,15897669890218010730 +91d92931-3283-fc71-8887-01e665c1470c,691892354,17583334213471329586 +4c96fc05-8ea5-e745-60f3-701c7b81fa08,1576913519,16500572831432450878 +07f312a8-9f3a-97d2-5c0e-c636fbafc535,2922255229,15858246961302994344 +23971cc7-1bd0-1a70-eae9-d157ab2183bc,879506936,5951046717783431528 +664a373a-ba45-fcd6-5909-9622b4c27ad0,1413751368,5206701727331705809 +1fc1c19a-93c2-d789-04a4-aae7575c6393,1309015557,2957335615855777231 +62953524-09bd-a853-d02b-1174b834427d,4290586552,18434871909940703716 +19bf581c-6e21-8d62-c21f-1f8244026137,684206723,11316388959895161025 +041c0877-28e7-69a4-bbbc-c6c192806ee5,1050869753,919188375348944608 +f09e433d-0f50-07e9-e8fd-0100c8c0f9a4,3512326689,2177480390508745280 +c5184cac-a122-5830-af33-0b8eb61d3fc5,1351280051,6067887123618139194 +ce2da986-d5ee-6133-5b65-a9bf307f9517,2114379210,11456761596372511389 +dc00a6d5-fd78-af84-a729-3e82683b4e59,2650431095,12430072767625227248 +619193eb-7096-8810-8a52-fd3c870ea23c,216799690,4120066397623368263 +462d7332-f419-3710-700d-931ff4940274,1928597509,17655492965161092740 +9cde9ab0-1bb9-5e90-1f9a-24af4ddbdd54,534403519,12880495304150899193 +743fbd36-18d3-fc8c-22e2-51c99b4b80b8,555160233,7328132803344035186 +109f32e1-d852-64d9-4b39-c5dcdfc4ef53,1364679224,2819661580276117815 +4ce115b3-ce71-98ef-cc0c-e6997f2c1a14,1700041510,17744609210287439046 +3bcf62d6-d448-3344-e795-f6b506b56c3e,4073752286,10589516690185181220 +4827e151-4076-1f70-739f-d69e6c5c225f,3083768274,16808088896448163775 +0c418115-7a3d-b8d8-bb4e-09ae507448c0,3826760116,13249138125785352920 +0c13eb2c-9b6a-a053-5b3f-5d0ba33fb089,4142962976,13425872843717176114 +e94a77ae-cfae-fc24-f301-ecff215cac76,1776181759,6637995112133226199 +1479d56f-cfba-8d64-a3cd-ea2d3df68f19,3910166530,2429771408042063591 +8747301f-3628-d148-5a92-bafce4aef464,4019454994,12039001515258931788 +93924d79-5c11-4bc6-f5b0-42e7eacda890,3139801433,3791033996064640991 +482e4f3d-05c4-8ab8-ed0a-3ab0e6c7b891,2116653219,4052679207738689668 +417c7553-0bdb-040f-46c1-9361717c3ff1,1102696967,11103786694474178846 +1e86c322-de7b-5f76-3702-6e43e6dceb14,1116770907,18334982877397743861 +dc96cc73-cbba-c43f-2ac1-9d64c03de1cd,451956168,17950656278775831188 +3c938e61-e69f-985f-8fda-25ad72df5271,2368382577,3281276154185916162 +163bd192-4274-9516-32f9-f144f37ccbbc,856438549,11013114143940515259 +58bd2c93-f84e-35d0-be60-67f8ae1da82d,2057076398,8538622891164271423 +22308932-2cba-e2d0-5758-42e39b5c21c1,401984817,17463164966459199896 +55b7955b-dbf9-408d-f583-96b505bedd51,4020992489,7819335307121915491 +9e464499-80d7-ebe2-629a-510cdd8d93a1,1270104604,7713906659295428800 +6a368ab5-0814-103c-5551-82a90b138bde,2086314513,12667969944839565034 +21c30657-bb11-a1a1-f8db-196a0a0b4023,4090464420,346555091295075514 +8e56e57d-ca1c-fe61-7dec-8dfd6939eb10,3629609864,2744065420635778813 +d9d275d7-55da-310b-0412-a0c0e10e5375,2901541198,12915684799710766868 +8c1e0680-892c-1f01-5948-3a65490b4c0b,4286368509,16417910253385495330 +182123de-98c6-621c-a43c-c1eee54c0804,3867453479,14113173817697533630 +b624bfc8-60a5-6afd-3dc0-6afe1cccb06c,829663147,4268409706304552379 +d1f401d7-99e5-be1a-fa7f-0d822e19f336,2956005646,5776991213273461395 +ce14d0cb-0d6a-a075-02c3-3fa14ed4fbc2,179472691,1950494332928133536 +8c38b355-bcc1-db71-7854-0ccfb8ec7ac8,502975054,5966856830155413332 +9337f099-efd6-fd96-64c8-5b1ecba8bdc9,3010948339,13174891068890261794 +0b03f9ae-462e-f6e8-e7b7-7ea7eee21383,2489590048,4250461178231264192 +4f63e6fc-c3dc-c25a-a8dc-0b45512f657b,1980785694,16160583630332969507 +f2e8c38c-de01-49ab-5e5e-ee6079250e34,531765430,944637152283898459 +0321b281-e228-24cb-9968-6414b6c1ba41,2446892502,5922221363832021707 +f252f271-a07a-2604-0a3e-8f89055882de,32491960,15673596569924026692 +48f11a44-5672-b3f7-7839-4c54427e8c21,1883573966,2730652851970183657 +990f3dc9-448c-53b1-761a-5ea4a8199b22,33844643,10941452030525597268 +dfc42596-93af-3146-9985-bdf37b65357a,1107823758,5798762438472465801 +7dc4fe6c-b240-5c07-4d16-48d3e10b6430,3627262043,10725258450875420981 +ea3eac99-dcec-0338-af94-24a9c8fdbb86,732580087,4877700428070589489 +ed7b61ea-a86a-d567-1fd5-3c8d6f873ab6,640620677,10410241668943597061 +fed81881-f072-0f1e-9d00-508fd03be941,358113503,946683631627302856 +0e26de7d-be51-6428-ce43-ba97c6842782,879695982,17221902682568325146 +60fd032a-999f-b2d5-cb2b-8327db4ba39a,487199974,365993903571137853 +d03a2603-5901-cb91-f14d-25ff3e218b4c,2308855488,6650477266754928191 +6943bb0d-c7b6-d9d9-4b0b-750a86120e7f,1984901117,15494156324269941918 +800095e6-694e-e80c-e6de-feec59a81b72,91765828,16909047312674667088 +44b178fd-2f3c-33b1-0749-9d90e742ddb1,527857131,11625309250986666250 +db5ed6cb-4b4d-fa15-a1a9-0bda17d6cbe8,157523174,5192415953969085303 +5928b73c-35fb-b146-f22f-949f33ba990e,2268534374,14560739439232394232 +4fd2ddb1-e726-0ed5-14b3-0018446e2010,730374345,12689355252548468980 +6e51e313-f24e-71af-08ef-1cc144944bf8,529542717,4323779605872218641 +a7588826-9702-ecf1-109e-cf00f23be1fc,2373680561,2972808440489596325 +43b9a8c3-f837-986b-41fd-c3460458e8f4,3121673605,7529094438978813366 +f7c1ee32-11b4-5f66-6df9-c441a9b59a1e,2556719246,16533044252066423899 +fcd04980-7574-ebc4-5e6f-4804e5ffd29e,2355444259,15658610855163211664 +26d702f6-5453-d641-6684-b23a2649d183,2148428378,14313682128640151183 +1c3a1feb-f29a-dce3-1af2-43746f8b5b5d,2063858552,16487246672197108603 +8edc0a50-a487-09bf-c58e-21bfe54f9254,3755951150,8961939897505868108 +437d3488-41dc-a7a0-5adf-496054c11c02,3382468144,4056566192701380255 +a19c606d-c02b-55f3-4e5d-fb6ba314aa58,1092683941,14523574739891144403 +a373dab0-b04d-93d5-48ba-9ba9ef37ece0,2919707057,13009977604870230198 +80d110af-903e-f5fa-4da8-a21d296f0797,3899275820,16747667324468828978 +9fb0faba-64e2-2c0d-7502-d12a67756c33,3490725466,6710897035481747644 +34a7d6f3-eb32-ed35-2b45-67a74391a390,890052642,12102076273626593672 +888a2b14-90cf-f74a-f277-35412f7cdaf8,3155511299,14857479144482717175 +35651bca-7f6e-5366-6499-2c846a35e86d,2807227788,16295424738605426318 +a7d4e3ba-d93a-e469-0a78-d97e1852dadc,1992856746,10340303194145211518 +d16c4046-52a3-52ec-a7e5-3b28a27faebd,3141387216,17521794828801753342 +bce247e7-ba94-c414-ec43-1bcd78f16c51,851419647,5455159217453134408 +d3bfd901-875d-2bdb-c0f3-27fde3343796,1486836166,9522551440412319920 +21367653-ff66-1d28-08da-b4f1208b443e,2948200431,7541843900963058873 +423f7b33-cefe-7f8a-2813-9d96b854ba72,2851616403,4459702195995418619 +693a9307-6172-a5ae-8d39-aa9ad71fa2ce,1217853943,877211537043809290 +6959ca14-75e1-8d87-06a6-05c8b2c087f9,3147456247,13955060182229066855 +2a4d02b4-ee2e-eb2a-12a7-7a71ca0bec90,25489033,1768856034570690724 +ab0559fb-2a7b-0d14-6222-63dd0d0aec1e,3494407740,6530420593279327462 +fb911e1f-d101-a7fb-c8c4-a22e8ae0259f,633742139,12890540183354571626 +d367b5dc-95b1-d819-be4d-ea8ec3f16549,1418266213,17832133322462779561 +d32617c9-ac1e-4d84-7590-4f05645e758c,3171226376,12576618327653340410 +8964876c-eecf-3d3b-16c0-f4ea92bd0ae0,2246724032,17637874404503558147 +6f9a0079-131e-5715-ab9b-7e332892e2c1,1608214626,15221134278216616296 +86a9ef08-dadb-548d-0961-c94753ba98ca,4283916676,11764497927937076810 +d16409e4-6885-643f-4d1b-49e4756e9914,4230232866,12273035963030737086 +c1208397-8504-ff22-d5b8-e6d71e81c41d,3664575202,11342204022681508510 +fafa0edc-3e02-f52c-796a-7dea996bc75c,769080413,16010600358873084866 +c768db02-65ab-24b4-49e4-48ec853764be,3786088631,4128979119842835135 +909a9272-72e9-a5cb-317c-bf6c9bdb30fd,253682818,17746337297791776649 +b7b1942d-5a93-c77f-510d-ae1e9b998b95,3248526080,1412315441127631335 +637689c8-75d1-bf05-06af-8ce6b659b3a5,1120900525,14178228944742021707 +f6647e4c-ccaf-321d-aa35-dcd22649d3f7,945836475,1648935656316505381 +80c88fb1-d69b-094a-d8dc-64ce1f961c04,3906844210,7751304670967604433 +db8185a1-4f17-5f15-8c3e-b2ac05477475,2553775034,4951096580533107206 +dc5544ad-926c-1855-8cb5-5fd6248b1a63,2199045680,9472966568721349855 +a34b6285-f327-1bae-ab83-8ab556024726,1611026869,2086508303287668239 +8eeea235-bd60-79d1-7a75-57f5b90caa61,1316287896,11059287492777601825 +16f84667-76fa-4ee8-9fc8-425f473f0730,1784882232,16070050884652302459 +fa397c0d-de1e-240d-fa06-e406e34d1eaf,3765162387,6769831424173793911 +5c44809c-bd0e-817f-e74e-c3e85bed034a,2500696715,13289144198673191010 +1881dfe9-af11-b70a-e961-c86d9d33f4ab,1452413292,12734473925166345114 +bc6cef40-f628-99bc-dc11-6ce4c97a4702,434800746,16327735750860741876 +fe7fb22d-98c4-37ff-c5c7-2ae789cba600,3574269502,5584968253943637482 +31c3872b-0a28-6a36-fe19-e4c891901834,843118953,18101442064896491755 +997dad76-0efd-7307-11ea-41c4e20be23e,3193194967,11378118040402348414 +55a3d0c9-4b85-8085-aa43-0173ba1f339c,1862810739,3837346098708020439 +e8a68068-cd55-3eb5-b4b4-b5a01bcf7908,2283008128,3635736879629924377 +f274c11f-3b2f-b601-c02e-08f4ff209a04,3410873074,5270345052918928555 +5bb19bf9-ac6f-5c43-e391-7ce4fa9c0242,157563567,8044427042245553841 +2d40efaa-59b6-5bf7-e001-a2e0d416db4c,379462020,13992444273662256950 +6c15d908-77ec-33ab-9219-105290c7aa7c,1527554343,7877276370267055900 +2ca62549-0e8d-b748-98b2-246247ebc3eb,728714507,7650091793439326013 +37c1b444-3d19-5ded-8557-18a3cc2fd97f,2729800016,12481582903455065180 +b0cdfe7d-d473-3afb-98b6-229766b67e76,470927667,149039178628671385 +0578a906-37ad-befc-17af-9fdaa5aa3b8c,2862893176,10198571710922574465 +de3ef95f-8e0e-65a1-59ce-b5b780d2b502,103595257,5247955359676774411 +2d7319ab-8a52-e4b8-d4d4-fa0a08f5a415,3439228850,15488082730295804379 +58533c8e-5b41-79bc-e4b2-70ce2d43ae18,2512172595,14081704220328661932 +5349a6dd-cba0-3bbd-389d-e54406ca85ca,797163897,14863257189436601866 +ba946ebd-e10b-8df7-0fd9-66b880a04cae,3702101452,10139813324377018220 +cdc0bf27-c7f0-4dfc-3dde-55832703fb78,2293319841,4169487441481585267 +c7d09073-c01a-8afd-c910-780c58cc6975,2183147982,4008190465375491904 +4b6060a0-c3c1-16ac-e55f-1cd1468641ca,2200856403,10820007333630475568 +60f22ad7-f2d9-3e6e-e47f-e61d07143960,1776992985,16794868476802909477 +c60d1b23-39f6-b976-d487-08a8037a1023,3063572118,6657575858549871674 +5dff90e6-ecba-66a9-0e52-8b5d9a4255a8,1691257277,6608897636340523836 +bf7cd390-3f9a-deef-af99-a3ff62291b5d,2657443663,13233115718268950643 +8cfd3461-d55f-cb34-0aaf-99c97ccb5f17,1707587348,17842480757410497094 +778e6789-e58c-7aee-7ca0-3ed3d42d62d8,355909679,14628491228208771009 +d5666501-b083-2c2f-dd56-22d0b0d3a783,3088488423,6278110012806646546 +dd6f8e8d-d3a7-debb-f835-1fbb95d154b4,1398748743,11219526757526721738 +6551a97f-ad95-16dd-a06d-361f7379fb51,4277210743,16265086254047935265 +c2abe62a-1d84-1168-1b61-c22ed2dbdac2,2116807189,15242742528450472618 +21df6362-265e-d8ca-9242-6219f220e2f2,1495225050,4540550769758984726 +0c8ecdac-4ccc-3540-e8a0-8eae7c70f898,4253954124,5815564460961879444 +03c0249a-e587-4fc6-385e-f280c40f4bd6,2510604512,12289082359371089310 +79203a69-3de7-a608-7485-e85ff754531f,2275103313,11127765226978401298 +1c71a7b2-614b-9f39-d49e-8d24cddc00b7,1859074737,8530338645624872292 +7cca40e2-3b0e-00bb-3617-4d964de8b937,2121512458,16987125273256244873 +fd9aca80-2f18-ad61-9853-4bbbb4e5514d,1965157103,12062893699155268604 +5f9fae67-ced9-a1bb-da28-02cfd491d4be,608739094,13314089645780679499 +61712c55-d55f-1d63-3c3c-0a19f31706e0,1598174242,10040159795570423993 +acb8e784-d647-3439-af8d-f8b057b3aaae,2664999062,17668356151836993406 +5704c967-423a-e9e7-d761-799c735b1c43,3598327798,7368594376496832076 +835f2cc4-20bc-09f5-9140-21055f7c9cc4,1385230145,8251777299358491298 +d5b06077-0f64-3fc3-cdeb-73012607a3aa,2703745384,15256173712401526629 +60730afe-f6a4-4b91-8e36-8ddb6d717161,3429391303,9184059812210538849 +78d6c2f1-56ff-bc63-f422-6c1a7bc1dbd0,2881563375,17654368789573762996 +ac853153-e264-3c34-77ad-1d547be27c33,1454166887,7039890773402574132 +ff2777b6-2c03-f4a9-57f3-fd3cabd3ad6d,1042169800,16151584288334653533 +375a4a4f-9d62-1769-9484-5b4e299f9001,1475589142,13445623916039203948 +d31b75f7-baf8-c274-4371-9cc44b77ead7,3480937103,15298442421452224312 +81b10a6e-49e7-d96f-bdb3-38ebc1478d80,1319548130,15806543194565603133 +8d2db95a-063b-0c21-8840-df9ab26c7792,2445167749,908619466686045118 +1fd31138-fcf4-40cb-b70c-54053c5d5904,2667373563,11179255574791825384 +b7426fbb-da2f-706b-489e-d18192012714,21198745,14029673649919349153 +b6db19a1-591e-f61c-ca71-3b09b00fcc16,571629739,9032464346971813806 +e5d2051a-fa04-8929-5304-45397806853d,2223071551,10837298891127495195 +f19c9163-3b74-1642-136f-59fab4e3e31b,3991754071,15166631966965357201 +a38deb3a-c114-d5d9-8293-42badb5edc86,303342925,17277990033470978550 +a4e68f1a-f05f-351e-a7e8-1bb21503bea6,3556383468,13706276968742307768 +a6e7daa9-a2b6-0c0f-7eb5-caffd41ca5d8,1141326138,3296504536164016471 +754a257b-9791-f7ef-11da-04df64f22e8b,1186766584,12701109701102924631 +cee02025-3b0a-538c-c929-8f5dc44f12f5,2094758775,18364060881751443939 +42ca49a6-8255-1737-054c-0fe5898d300b,1771373498,3920054963305283105 +4879f663-0e0f-53b8-4426-632d2bf3b23e,3070766583,13849536153789702303 +d2bd370b-8fce-9031-01d5-9804cfbdfbde,2648766713,13955872398101244425 +467f3c63-92c5-725b-9e47-1d3b2a953d82,2670845461,6834628771855197839 +820210fd-6412-af03-64e3-7b89560c48b1,1675329957,10985946494853209951 +e509f355-6f88-075c-ad7d-bcb20ec278b5,403852786,3451666310005102522 +14876f28-c623-eb1b-d9cb-ab8e4e57b56f,2283014747,5341497805349515935 +26ac052f-3e15-27e5-fdb4-2d2856fbb5e7,1156220917,13598234639601230706 +d81689d1-4add-13e5-110a-0c1acf35d258,103432445,13198416573092308069 +9c2a457b-d530-0fc0-4df9-d34ca0c36a8b,2752322856,8242298562398441176 +65305928-4846-e908-6c5c-6d611f4a9276,1289007219,3534469783748969152 +cb1b8945-3d95-3afc-d13c-a4e9ac6d068d,2873455084,5271506106538754948 +3e709dfb-120f-1978-21a7-e361f5392111,1722155089,9304458487368319475 +561a65b4-09e0-28ad-649c-7b91ac6f4eca,939212201,8344871028813037469 +9d8d6ffb-12b2-26e9-2d34-6aa11e749b9d,3731431352,3110316502475937925 +ebb5edfa-c354-7c16-5c77-da01f6558891,3316003676,6617977627464078058 +7991ce78-386a-52e2-3dfc-3bd8a5c1a4a9,1697351597,8955744597665223251 +d943978b-b582-df30-5592-427b77ab06f1,1488578907,18061482807442226255 +7a73d2f8-a4ac-d838-0e37-c290be5250c2,3078590344,14339909022322477902 +789af39f-f4b8-f438-182e-0969b5a59a50,3744341502,12271073986957311130 +394c9ab8-1c70-eddf-c23a-bf7bd6259a87,1045624941,8222642569691257299 +1eb05243-9afb-d105-3f9e-27c4674f0cdd,2759881480,9216257894282204180 +fe098ec8-ade9-858d-d841-d4deec6bf7ad,894240934,11271156459640207371 +bc593aed-01a3-9f7c-a589-40e99c27e4d0,1831845137,789454573716083659 +b1e40b62-a89f-b110-15f3-a88313a0b96e,813066856,8242089863563647466 +c5be083e-207a-949d-e219-bd8b45c7aabf,2841588777,3837639926481395819 +b8059ab9-6276-7437-c5b7-2e53d53c85f5,344126786,1914005098573518710 +a9544d51-9342-a7c4-3f90-96401990cebe,3763322028,10570444681663094300 +89c3ff86-9dbe-bbcb-8696-5728ba7b6d45,2850478947,11680355016146192153 +df9c6b67-a0cf-f676-c058-d818cbb3beca,2619707906,2831742950843509267 +e47d8a0a-0272-82d1-ddd7-f507421da676,4135871003,11022850600574037956 +f06ba7e7-c09d-b161-320f-3deff353f000,2807771089,4044964083901386404 +a1cecda5-fd2a-8e79-227c-3d2668b8a26a,724046311,4990378361166777881 +7320399a-b30e-d104-3d69-6217c782e2ff,1652102711,2524680174842240952 +3573e802-3d10-2687-d61e-a71e77669ad2,3709964644,16363011283873863712 +f11011f0-5c49-efbc-03b3-00b70d88b32d,2718416102,15689088743845650989 +5c0c77a4-e33f-1d60-b18b-79a9d4084ca8,3835427677,17480193710234419501 +0272d096-be38-47d2-5379-5e8eb35706d1,19151882,7637814291358338141 +af8fa93c-4682-b13d-f229-5ebb2ff49510,2339864586,8464980854213399753 +f71dae5e-ce34-b454-afe6-0fcdaf14a0bd,2676841801,10033294133905797959 +3b0b2208-5f3e-0deb-2a7a-9cc2e206be7f,1781340681,13666464489919092672 +3ea51bdb-2241-94c9-e3e9-a1a9dd480507,4233377168,3881900066739128624 +a6da2b49-8e1b-5c64-7924-17a1ac6a5faa,1219885971,4044571242628127701 +63ed4fd0-1ec9-9a4e-b469-8552defcae08,2721025015,4795883151058861158 +db1ce85e-6dcc-f0a8-37d6-7e2d017af126,1005502633,10131205265341356721 +f9f18f3f-9b06-7606-b854-e7f542ff7d65,965897513,4001756101970204970 +26aa6c84-a884-2005-e9e2-6f0ae3990836,1438568078,11181278417183080613 +3308af73-ce5b-2a3e-4193-8087b900939c,209758221,3364260157809732759 +da46be6d-8362-1038-239c-160ec6523d54,1415611729,5810442948935320135 +9a0537b3-3ebb-cc56-779b-40d60a845b23,813605883,12111903614705196427 +e1adaf01-71c4-6c2a-e1bb-963a78b89ab8,283687676,3150609645499400341 +797c2516-c74a-4d77-ee78-6e1c708fc89d,2203550876,7559083223518893058 +4f0bd552-0c49-bc5c-a3f4-a084d40b8957,1047986637,13890047268614723080 +7adfa722-bc26-76f2-d26e-60e8e0c441b5,1908671297,2633570272141194636 +40ac8375-2efb-6e9a-9a1d-95d72edee50c,4099456839,2617279691993730704 +d3f488a4-7306-06c3-6f12-45854332a4c1,3417086189,8963476406330929732 +c0ced953-f467-2e34-50c8-ff8d6ba282fc,2160558324,13818411729104827044 +99ad3fe5-95cd-5b93-8eea-ab9f492d43a4,1116156309,15955712020686889090 +56ed119f-1c36-6adc-b451-529c2c262757,1492864953,4094186269410392716 +5dec8556-21e1-6cf6-527e-28444a8f94a2,3182825156,9966290117899751360 +142709fa-613c-836b-3f8e-640833bc3e7b,2542398398,10634817095176967814 +a6368a01-6623-4938-c41a-7aebe6c0d3f3,238300496,1366490166666691607 +af87719a-2cc7-3747-766d-704d5b95d6e6,82696409,789047520560307896 +0b2897b1-48d1-09da-cd2f-08cd6c68d8ef,3907029371,11491659472447141209 +1a4d4504-177c-8468-64ed-b790d0eadd9d,2450513841,11727840507287700144 +21a9e1bd-1262-ced3-6eac-449e95ef6130,843507835,17298054457391677132 +741c823a-030f-de7f-a0f0-b96ad25bc125,2630336275,2223951914485031152 +93c8755b-b563-55da-4c34-c48bc504a93e,511628769,16187834362248247274 +f7d2ef93-4354-78c3-bcb2-88c23a7f51b2,1584411289,14630496559702402385 +bbb4e100-ebf1-811c-be49-cc4aca1fdb20,181311591,9995661992182545331 +f93ae6d4-851b-dafd-939d-ed7eb9c2b005,3301852069,5259033673889624108 +4c30ed81-c09c-9499-ac46-cb5f808d6393,3173023565,3897562648667230048 +dbc77442-11e1-f3b0-7e34-e0d47b65afce,1322645932,11146003656279617497 +d9df8d2f-0002-627b-cf73-f20ca2f5f4f4,959876464,14706352538742566044 +c2d1a06e-0314-fdf8-b579-be74e0b41af0,3632256514,4069160393175128017 +7cc45ba7-36cb-f019-68a1-24f838bed3ed,1304089471,15618737157494247356 +d9203ad9-63bd-24ca-32e2-94a5b171e79a,1236469886,4554790953756998105 +889762c4-3548-b233-c7ea-84e179bd7ebc,154295810,14127742580271623534 +42fc9646-b927-e42b-9f74-2233393854e5,4161955786,8178376176581763986 +d287bd80-c197-765e-9840-1bb4891c24cb,3951083882,82050965750778866 +5891579f-068f-894e-3606-8a9e1885db7b,1069520941,8361860414828825969 +77261354-2218-97b8-4ae3-32dac4362e7c,1462029506,17717648081796268032 +7bbe11c5-d1a4-baf9-66f1-7f171588e5b6,3723994083,1300420702281823775 +87c9377c-3ed8-9489-8892-6ad87d7b52b6,4111778018,5294973814255866257 +4b75a09b-dfd1-a13a-b511-b4ebbaeff78b,947453226,10436742206425899091 +07a0233b-58b3-4e4e-e3d2-462a8ec0ba49,924815743,17198673801956018277 +6e4427f7-b723-6b80-257c-708d4ea9bb58,431118995,11840860314823485264 +7d135ee2-cee7-edb2-e476-f5b234217c8b,1904623089,8864125099157606603 +63cf8c93-0767-919a-83b4-0a1d6d13a5a5,846797755,876612770489181690 +b090ec7c-122b-d407-69fd-4a5139c1835d,100829232,1199936786943701069 +21986f22-c9fa-adf5-7ca0-c8c075b1ad29,2167945362,1088800630319331049 +f846bd49-4d27-7ca0-52f1-27d990a2b62f,1634962202,11440476926426630169 +4c1c9648-a399-f76d-d4af-4021a2c34eea,497257919,4006836792061887976 +f22c0c5c-d848-b356-a9ae-dd540404b8a1,4249264042,8443102330247617324 +26064cc8-914d-c87e-b57b-e0e53f99977f,451070607,10399282191082677908 +56b4dd25-e487-6a7b-482b-89e1ba4b9afa,3227691270,13582406502942859787 +b62750b2-86b8-d79a-1f9b-c51d81bfb5fa,1778466769,14236495932189588262 +75d96b82-e808-f8d5-5028-9ef23d09f427,3962661315,10366660335017317775 +6e85d956-dfae-4b8f-c233-18cf1481dc2c,3819795736,4911029135798318337 +733c2227-a6ce-6c4b-01d4-0f7fdae83ef9,3657132747,9655421091760293063 +7b0477c0-b0e2-3580-d82f-c8b2c9b81667,665522252,7069965112679035632 +0504213a-3988-8fa7-0177-a09277a59b54,980744146,5401533627093880036 +b08d9e93-2c0b-9bda-eeeb-e14350a9fe40,2973939429,13218324674493076614 +77247d6e-ce3a-05d2-6b44-5402b7b450f8,2297509777,13027698902991741793 +2ed47bf2-46a2-26b8-d3b9-52c8f8969fef,4247661410,16397240263696504541 +3b30ca49-81e9-d158-bdc1-b882d51522c9,1603746269,10516624489785767286 +390bbc64-ec1f-bcd8-6cc0-e6cee49e0aed,3287508451,14356753781721055517 +9f7fdb7c-9dfd-37c1-b055-7cec66077dbd,1872431856,16385288025380849996 +1d8068ce-076b-3f84-24c2-225ae481fda9,2808574739,1781127494789523875 +feca704f-b620-f020-d6dd-48cf86c41542,1874347735,16474656845204663390 +e9974aa8-c2e4-0e2b-0a68-5e9ff77aa949,2317267875,9763829021493148694 +fb66fad3-62ca-50d4-e0e9-f09d56450af8,2117267421,4218134599820041201 +363002fe-2960-75a7-7464-5a7f0267567a,799623805,6614036197086215733 +9ee01ac5-65fe-e770-eaa3-8f039e573bd3,3526868861,6537412199230557651 +f319348c-0c09-7fa3-dc70-939e599c8782,3684057206,1197414170744832720 +b3d4389c-6736-2263-b380-70dc18272f81,1135210651,9352225956336729872 +302977fa-1636-3541-5bd3-176c7999a4c6,1205834246,6050684418038268048 +9f8f2214-a4b7-817d-2fab-32884742c8c3,4214489884,5816148174992244499 +1c116e2e-cd8a-9b43-803c-9d16e4215362,364922943,7743186406011328835 +dee5dc4f-2c98-3a38-2ce7-2d26ae537b32,3289652136,16297776126997995448 +b358d026-bec0-0fc9-2bfb-2024a73a2162,3831594005,1393765027577394722 +2a853108-f59c-f8d2-0ba3-f5f972617114,1702489423,14130407598420242142 +9dcfbbfc-8dc9-3bd5-3196-0f49c1835363,3425510247,6832517343839852299 +0e3f9f42-8c0a-73a8-a183-df825fbdffa4,2041937812,17584430001268222761 +cfc16e73-4264-3c8e-3871-d69a1cb5df11,2491754567,9855749419164985227 +fca809a9-00f6-37ba-16e6-6879e803df5d,3348019410,10932609666008150350 +f6ad4918-d2f1-ceaf-3746-35e8da321306,2793249674,6919050281438967742 +52cc9754-5209-f0dd-65a1-8043c72fa64e,929423510,8541706042240180909 +65458f6f-9785-bd02-b34a-cefaa15c7885,3005460942,4676256492092739990 +9bdc2185-1b10-1852-67ea-487e0c07e895,1092348917,6766238953220894166 +81b096fc-bbaa-c4c8-0e50-308a74adf4ba,1109319578,16313608061255688563 +b562caa2-5b81-63bd-1174-c8813b024f61,577699399,606568513503579674 +f454258b-7950-138e-8f68-d5452bf4b7f8,2745354122,469121490798004748 +2f869fbd-da49-d333-530f-9984e4d9edb3,2349974125,13850342595151795426 +069421cf-6f52-4ab8-6592-5656d58ee3e5,1771084189,7245592176306951277 +317b6a28-142e-eba3-a3b6-bec9157202fb,3523523360,10581095484020211454 +23353b7f-be23-e526-4c66-0bb95b86e4d2,2551606565,6806084738295130909 +5d0f5437-6e6c-6eeb-5ceb-d49d7de9bf11,2195014001,13816973142874807877 +0765a348-e2be-1c80-9e3e-3fd3d7335330,1455876871,9004654132110376826 +1e036081-aea0-4367-82f2-e0a33b7026fb,1510936193,6134668059483707111 +edd928e6-8a23-368d-45b4-a637581e3894,55181197,2284666583290973091 +761f7cfe-acc5-9dba-8a95-fd2449cac497,2411470318,1181461905372779068 +ad9f473e-5647-c558-1db9-42c6522176ad,116415088,15967126638111853748 +a0deefc7-56c4-5690-bd01-88e3a6ebaf35,839890574,16738564994480725412 +0678d722-b052-c0b1-03a4-d82a39ec5760,3344435798,5939605618437226302 +1e141572-b630-c3b9-e4be-8b5f9325dbf1,2825695057,3701768902124489000 +fe066957-065a-cd4e-f18c-638f37dce943,2660303414,13505642109613696681 +b74003fe-e3c4-15b4-5d91-c654279aedb3,3506261166,8316279284394457871 +3e3f8e32-0090-250f-f85e-a76a0555d2b7,1123107978,12928627409440359471 +ed65677c-aca5-a35f-66cf-ed1095766abf,972237251,9956801931800076716 +1f12f9a1-2793-38f9-19a2-d8cf0f57e1e1,1300625925,10267814575711254043 +aa7ba6a1-071f-5a5d-bdea-85b3a248193a,4217737436,4180413737941583962 +ed1f3ae7-f894-11a9-f475-0c091fad3dee,3207532395,9520328214612080639 +26b73f41-772c-6043-4795-ee64e1d0b9fe,2129471038,5822940843090864091 +f5fbd457-59ea-9900-d318-283e7ae2c7e2,1213201396,16028591017475154002 +0ac301d6-8985-e963-6934-f4da715e26b8,2454226522,18411310275037584108 +56506835-c350-195e-1e97-7ea3aa5339eb,934462319,18439500282864643274 +09427bb2-4057-5f5e-c2ea-06dcc5979024,3318028832,8546979992509714866 +0b9e84fd-84ad-db66-6635-bf31c5b32287,80230072,5056374490634154729 +72d84954-cb26-dbac-eea8-2eca4905842b,3947991084,7787449688883222762 +6ef22b4a-519c-9d72-1fac-2c305edbdbb8,3616439232,339195943547673812 +30cc9995-add7-0d3c-e0ae-d3732136ab69,2954998773,2560971025047044239 +461bc132-3bb4-1d36-ae50-107aaac23ece,2159766764,1994627373827680838 +4ae52599-dbee-2d80-c3e7-627a06184e69,2769404079,10350242199875694427 +478efd96-cb4a-258f-7523-72650a10e9a6,4258280765,4543944805894254844 +701bc2e8-9e60-ec55-c05b-ae3bb5630079,3204055847,5424864930298268448 +d85b58e0-9263-5747-c326-53856362650c,1883985202,5105995190632770781 +8dae398e-2210-bc98-31c3-6c694e4bef8f,2842003014,12008705018218306183 +422c53e3-92bd-53d6-0a98-da6e779f3db8,4047371964,6649290144982750980 +386620a2-a2d9-2ea7-7757-44710ecee94a,3135613369,5359285846779012179 +bdd77a4a-3871-5c9b-5319-21125a483cdd,3352343689,5168779343278608362 +7d5a4805-db73-b5ac-72c6-907fa2308f3b,2807363494,2978650875363791518 +c1869f2e-87cd-52a7-21b9-33ffd7ee2c24,4247646437,6980645364608486076 +225f43c9-586b-509a-c360-1f9b8f76e4c1,2448963607,4281531176716791118 +7e6f110c-fcc4-5165-aa34-6bc51c51d0e1,2919173837,9049814211574351591 +7425d243-aaf9-35d8-e9e3-192ffd1e1be6,3315461975,12188601156595589796 +221725b8-d83f-aaab-065b-02ccb6930fda,1760564738,12096629887155868778 +b78d3907-0a8e-8266-d0ff-b7074c399800,131272475,237628085016679016 +97840ad6-645d-653f-f268-ea0e2d99b69a,79215374,2153828977228670066 +b21bf902-3d50-2438-8c18-b906c2e274c8,301069638,5168409810733733214 +9d5ceb26-9221-78d1-54c1-7a44fc572a53,2805171156,12200204692130100589 +296ed9b4-5262-acd3-f18e-694f252432e1,4249978169,7766067352433625639 +fadad2c2-e4f0-9258-e5ff-4b9e55a8afeb,1683793333,16668173180553915748 +70240e83-cacd-f81b-9388-36416e506e52,1073382735,14914646808153794506 +96d44ae7-4c03-ce88-7adb-82aedf5f18de,953244412,14453102857473981978 +bff8eb77-b7a1-b266-0bc7-f6bc112b350c,2106027916,17837071774915675315 +81aca18e-aad7-9411-1f3f-c33dd347448d,3512720559,17666807165782477515 +0923f553-a110-2d7f-4964-cb5131bdf791,105510336,2727148570088798010 +267975ef-c07c-0c7f-dc36-7bf9365b3bf2,2886352536,10223053536005168362 +a2729a3f-d24a-97ff-357e-2d3cbf94ecce,3018548651,5389388424760704072 +8aa2ecac-5355-4879-80ac-d8579c619989,3243415374,4077587864806207596 +934e3eca-bb6e-ae3a-0ec1-036effbeec9f,2268195226,6579690459964403802 +3de53e5e-d533-25e5-6ca4-af8892b9d9d4,2245376452,8795120388647209220 +7a80f95a-1621-cc88-4bad-1053717f63ca,3573467029,5003566919419731662 +c00f7e35-2e95-d414-0c3a-025fa0758dd2,82433306,16997044690964056245 +bbb7c699-bf72-440e-73b3-e3e91c6aaec5,1768848753,16589859488234506600 +79e3e538-934f-e94d-54bf-f29c53e6ec47,2501831997,14247019659320044641 +b8577e71-c21c-90d6-71a3-b89a625ff975,1180361848,351544953510602046 +5c03d155-cf53-30d9-4fc6-c2b82eb78701,2145035309,18260807137526334400 +f7a333f3-62f1-a327-2a76-feaf9b2e9051,1575662342,6246067980741763008 +66cc10b5-9758-1b3d-272f-31c1a130990f,4142566576,9062729475150814534 +38fa5151-dd94-29fa-2494-f11c198a8fa2,4279928751,8316923438707637376 +b7aa568c-a30c-8408-1c61-192da05a78c0,4269652650,587873603018319893 +11cbf9ee-6fdb-2628-d0ee-60afa3bbb405,1458777692,12748992685523146331 +730f2f44-9727-7e26-9d31-b3201ff00b38,1051584767,18259272777879399489 +6290175e-9ceb-c975-a46a-6494d0737e68,3662283383,9253275220227849017 +641fb32a-c9c1-61c7-0db1-8eaf4eeb7d08,1398900168,10443799607328646761 +f7c88956-60e6-eebb-4655-358589ced648,1073697604,964928264303866914 +1fc090d1-9fee-568b-01b6-3edeef7277a5,3868204791,14514744167326086616 +c69af76a-704d-e6ea-08a8-df717fc08471,144864096,7522125938887299279 +4b1b4e9d-0b07-3592-3df2-3098dbff2837,1333529033,5483905156136181979 +6e10d3e6-969c-0e4b-01ce-a6a5377accba,1763589928,8966727878074496597 +4df58e74-3033-e461-2520-c1788b45d332,3754062098,4619804150614556931 +4fe3a0c3-1afa-0c8c-a05d-45ae5ce57262,3939573695,17626161258731747050 +3adb4239-63a4-9d2f-2240-7ea1e41363a8,2901512339,7398706004047712977 +c7ebd82d-b8f1-58a1-3a0e-4c48a026abf4,1182235120,14029544889607774883 +03e158d3-98a4-e643-b961-63697821e0ed,3378892497,13781933243701947564 +3a0ff8a7-7196-230c-c8be-636a2695ca35,3296233641,10615379189893247434 +ffcbbfd4-60f6-c539-d825-07781558c7eb,3325090601,14182603568818944277 +3813299a-3792-2340-cb20-162d0a03e35d,3188579465,2957061087424732475 +1318d804-5750-13bf-fd90-140dc1c6470a,2447077835,361169147017428554 +13dc7947-a9a5-eae0-9962-992822d29ad8,2101126325,13093653631422592693 +9d28d49d-dac1-b3bd-a8b5-4eba4a1acd6a,4072732094,3748442279507306988 +e4675892-76b9-49f8-cf0a-8f7d3f403d37,3408775585,11466502139416822594 +08baaf6c-d0a8-87e8-5634-160b15406a3a,3699538427,5154056110472570640 +65c04092-7a55-bd22-9173-dfec2bc98071,3422274465,396602370476164099 +9f191172-b1ce-e99e-0189-81c841504e40,2757484842,5375077820736999970 +30ebe79b-7b88-fcaa-5771-acfa5e61b237,69856742,7074424934852161186 +acdd11ff-2826-a034-ebf5-84fba19e314b,3586498671,8577367293589351339 +3f9773cf-30e9-fe87-93ff-939b1a729207,1003585779,10610860378354034943 +4cda6c80-7861-42ae-7930-5fc13631dfdd,2330049517,5401885671948665310 +a3616745-21cc-1af5-89c5-68e19058a5fc,3911701814,9586118099009771780 +d8a871c2-87af-d8a7-e159-1701968b8f78,2248232298,11648598258361479644 +403e569b-6d54-83da-6110-98c11a1619d6,2393210284,13351328899255353765 +b4bf0c56-3ef9-38e8-9b5e-9c1f64d12221,707393471,6471459309563224210 +11793ff0-8ec1-cbac-9ad8-cfcb59acb7c9,3988177599,9788814001735818382 +1efcfe0d-a550-2537-cc38-4ed8a9c0669c,3283064096,9911848482365371249 +7bfeb53c-baa2-6f58-ac4a-6622071c3764,2090395515,13155593809483398566 +030f0879-57af-802a-dd12-0a5df88be62e,4009890327,15049026718615006863 +bf3ece0d-b7d9-3450-dd15-40df97cde2aa,1023877489,329206331649932846 +7fddd37c-5c8f-14fb-bc14-170a2ae81cc7,2205809453,9661009271082812589 +93babe78-7849-4430-26e7-a86a5558ef33,392459937,11691105692790113154 +f13ca7a1-b7e3-51fe-1263-f8030fa9d350,963522151,13576278722594416105 +ac159362-9135-d217-2e89-240993eac3a6,1215928320,5413865809803322390 +5047b3a4-d6a5-b0a0-2474-e997d7e22c8d,3279454548,9457737166113217275 +604f1d23-788f-279f-75a5-167c9d23f5d6,3370861764,10975753228297610312 +2fb300d1-9b8e-05f9-8e02-65f925850683,906460590,10112505245496038928 +4ee96e78-f060-5b36-3d58-ba301651d2ec,1421506419,11621787054224287370 +af6d0ae0-da4e-2241-23b6-abade46ba992,4015744505,11479628845314972096 +90474e31-0e65-b989-d009-3f50063d0a3b,3506907593,9985705310395570493 +ebc65828-575b-e1bb-6d9e-85a0d3b4229b,2663147761,13030684558394285509 +bde1b0b5-bb6b-a921-d9d5-5daa918fb94b,2733952388,4262187406354787361 +813875ad-1a62-fc26-144e-7ffab41f0262,4202321285,4454781001926136681 +561edd0b-fc5f-4ecb-eae6-794ea4b68cdd,3916326534,13959203891465141807 +5c92e194-54b3-3348-1ed1-5c7e9bc9d7f2,4109009797,13142469268575591768 +5449158a-1940-0bbc-d9d4-415b68e49098,2646283967,121333823809027155 +d4da915c-6804-4d87-36de-972a5cc4dc6f,66958982,9774371840516216755 +153df00b-90b1-f332-8c26-7c94e06ed051,909614476,6941047653083804445 +e57d68bf-1b58-9fa5-bd24-bd241d633cdf,2862643104,12940608818704836770 +bb2c2329-526d-7509-c60b-1f15513948d2,2591775329,7796159289628021421 +fc153b50-6063-43e6-58ce-74dc46d7d923,2885788223,16346981798169518959 +9ef7fe92-53ab-2c99-89ed-bb5295c6b4d6,3892218268,1824858428032058218 +c970e457-c519-f9a9-2d9a-7d0c3ed9458c,2003486155,9493220696718839294 +c049847b-a00f-6147-547d-678a1169a363,195725084,7140696083363059145 +df85d5c8-c756-f222-533c-7973c326585a,3730858253,12642220693501992154 +fe351086-b5be-7a1c-8327-bbbfdf0491ac,1073577430,3138878851873825906 +e9d3fcc5-33d5-24d2-39e5-8cb1dbd878c6,3639329829,18222468304744875253 +aae3406c-3d7c-715f-193b-0cb01e0ddf4d,380074568,6081504230003960809 +57c42971-0ff4-abca-c941-f6150ded673d,856460848,3294424631854147629 +f4cf75f0-7ac3-4cd3-666d-4461fb5686dd,658670861,11081459653357114606 +8feb514e-9b55-c5e4-997b-a94029975d3b,265211947,17120040918032000245 +3c28c05c-33e9-9479-6dce-e917edb22c56,1672142770,9394838752418319823 +62bfd257-8882-9719-b3f0-0c3d221b8e9b,2349579973,4286007236292584559 +ded62829-9bf4-b051-fe58-2b290bd04ed6,2576617251,11265524869330342544 +3f775c63-00b7-9b35-58ed-53e5d143c6c2,1454871261,14703941120872161761 +43658d9a-5de1-85a5-341e-76192921c068,786305558,7416829407777382303 +4c9df405-7806-0de5-d61a-b749310090cd,3019217356,10158455853646644929 +57e76601-ac11-865c-1385-6d404ebd3518,2073021407,10044280580716425139 +342cab3f-e07f-7fb0-9991-0fd90870f0de,605764362,1955962026916572341 +05ce876a-b588-9fa5-a0a3-8078ab7774e8,2618257112,4313234241383923900 +6fc474c8-8db5-aef4-6fe9-6f6a1e9813c0,2034620868,5387245085303711544 +0d019abc-ec81-6c79-207f-d82ffcd3fb1d,1677400647,15021498180968562002 +c85b05bd-91d6-cccb-654d-5999477138e4,1233692773,15771977049052272302 +95af7884-24d3-9c75-6db4-ae4f2dd092a0,3718473116,1206228306099630703 +e17b01d4-cc24-b796-4e46-c91148ba4d38,2823072040,1070020169186615586 +a71869d0-a3af-a5b2-2e09-fb6b12071ad5,2848821136,1990460228226730272 +28cf4eb4-dfe9-e313-36b6-555ed1e7c9c1,3647492720,11954166564937686020 +389f790e-a4a1-0531-bb78-f2916177d590,3657662712,9482733756199899244 +d5f7a796-5fd1-e12a-1811-007565fc2b35,3881564344,16059894135952418637 +dc75eeb6-9a73-a52a-817b-9af23bd13104,1756086480,3062204564819010697 +d83f866d-236b-2c26-0503-5c8a52d9acba,1487408336,13490884169195027702 +d325b114-6469-9a71-5eb7-32c56ebb6995,187067632,8522873454615947096 +7eaeca90-36e1-1165-6cf5-3e2add10dfb8,716677072,8656046031427634002 +3e4cbfe6-b2bb-63fb-95a7-1868c87f52c8,2368030435,10857951102986827049 +0e8950d3-b972-b636-0db7-4d801cd39b4d,1616199209,3491933228374422908 +1f49c988-e66b-4d55-d73b-b7bf11cd3e9b,272605213,13588211500311334612 +221a2597-5c6d-4aca-9cfa-6f0498f80c4f,1729190579,17472552323671653021 +21c82f09-f7ed-a1f0-beb3-9301504e52c3,3089178565,515274060885885431 +f4e8a95c-6b49-08b5-c8b1-e7ac8105fe5a,1963530597,13058151951109129687 +20536604-e729-bb80-9b99-03df49fcb88f,500143105,9949220945551133537 +2e6dd7dd-ad42-987b-7003-fe49dbeb5aa5,3287429607,10572292122228065503 +1d1b2deb-56bd-5a4f-8aee-6fe338295aad,3075976250,3522897622694920659 +8189432e-82f3-9fc3-8aa5-87f421824241,3192877128,12793060789679751834 +86de9082-589d-2f25-3417-c2fd27f18385,1238609964,15469198938388645570 +2ffc7617-c1c8-8d00-75da-dec3d78f191c,2388265527,16785510456014156387 +98cc8b6b-0242-fdb7-5505-ae3f5c6b792a,2348675436,7316972692687942242 +963b2ead-3ecd-96cc-5072-72de91454087,32624930,13445197617595817665 +00820fc4-4332-fbce-28ec-28c357719671,3133131426,9023814727389640326 +752937b2-5bd3-0e8d-5dce-89790df33a18,234910796,1400347872406936057 +9ea4b50c-47fe-f2b0-dbce-f929839cf82f,566892560,1279866767368202207 +5a21cf75-b596-98a4-7fbe-183aaf0971d6,2254043500,18013510081930570181 +980f4fc3-818c-41f4-0452-f83a941be0b8,141542564,17976445370431073485 +ad7c8bab-2064-e0da-55e7-53ffcf7c9c6f,4181849949,1276927068321884944 +0d00a0ff-16b7-986d-5079-5efa8ed9b578,446438997,6451364100597684455 +b904034d-e3fd-040a-eb2f-a64e59107747,928090964,8776222548580578130 +7c4c207d-7d2d-16c4-fe06-8e98d803ac54,3784402549,5201356987765223222 +294086f3-e2de-ee99-e5ea-c008e0788f1d,3064492518,5342698474551751343 +433faa14-e1c1-6332-6023-1fe1d46c4894,2072436418,11035870410886441942 +35a789c2-d6dd-8642-e894-0af0ea12119b,4242127797,12089740345222841472 +317827ef-8a67-2eaa-b519-23002028bab9,1792873089,6515248701818971495 +b74a82ce-1c02-4488-467f-f9cd663e4977,495841149,6898624813050559992 +11a91085-676f-c45f-8a74-8fbb739cf5cc,683969218,7033969783778351996 +85114c9b-ed3f-5e9d-c9b8-ba0cce536a6d,1708042977,10848534094578539077 +f9327d7b-8827-74af-6600-bcd43aaa5541,161617912,17950310295467133343 +0bbe5581-8dcf-90f4-119b-9f3a0236dcda,1932960811,2135593775723866546 +77f130c2-14ae-a879-3315-a6e110c47a98,1498519743,9550727866249871093 +5d7fe707-4733-7b3e-fb8f-7461f1cf610a,3391487452,3765637437114162464 +43c33cd6-1171-3dae-3906-aef2ede9dece,2671820970,4641991594213230389 +6e9d6dba-f427-116b-4a4d-76d744b0451a,3764380603,11818550109350619806 +c149d072-2110-12a6-3946-fff97f31ac8d,1815325689,976861375562512126 +1b0f0259-d762-8fb3-5961-e7fe169033ab,3703863638,5752624618843596003 +d0dfff41-0bca-4df2-d589-d8a410c13d53,3043313727,10730282924699396220 +dc0e17dc-83d7-7b76-d608-cea5a0b285bb,3132332975,10716207008828296657 +59663a3c-48f8-9e75-47af-62d33af70a18,3612205940,2338696905670157345 +86313a43-6239-2f7a-5697-22eafed53ef9,1088119252,280723916563749323 +2cf2d570-7b4e-c5b5-23f2-8efc6efb0cf1,2295922606,12462752643168881909 +03405631-3daf-bb6f-79e7-3afe0821e377,642953578,4086770776086244403 +955ab4e9-6033-89de-fc6e-0c2c7be40ffb,3433339107,3620367849991679161 +8adc0fe7-d211-8131-ed69-727e4229c58f,2701673753,3335940478590585500 +aaece148-baec-29e4-360a-0004a4ab0aea,2862768373,13089335567774071544 +545f1ced-3c12-600b-7aaf-75a857c0a5ce,1408225687,15018400892789274043 +980449cf-3b33-9bda-3c7b-b8cf7cc518ce,806614010,2718757152055574356 +46b85681-56e1-397c-8cf9-2ca797fa1bb9,1164548112,12963120278944528209 +e82d2e26-538c-f30d-f2b9-3ab9680e9d34,3626415182,9650866189577270228 +0477ba08-e1ae-51ae-e13f-5887d790ef9a,3203952062,18054214771284808741 +cd3f9a28-79ed-97ce-e686-a19323b1edd9,2981313302,14152428740094106837 +6d3c25b4-5e55-1ccd-052e-fd6a185b1cc4,3410409293,5791965148495189184 +9160ffa3-a5a7-b033-b44e-480b2ab34415,321210954,80085906944033477 +1457f9a6-5c14-d303-0f96-9e8ecb5f50a1,3210403110,18193163596531585478 +961ed8e2-3f55-f510-38f9-6b5c884b5658,2785368155,13293883231704105632 +ad2323be-5ac2-a051-935c-cbdd2e7d9b26,1839478323,12343747264692723458 +39331138-4df5-3fba-8582-208f12cdbd98,655399054,12603630759159084292 +da4667ea-2147-04a8-1994-09b113d264b8,2426365591,10956153361108734379 +cecb6212-7bce-1721-9085-063e3527f6e5,620287047,7757261334348121168 +9d24fd23-9144-923f-93ff-dcd1cf490f8a,2958707791,17992499138179265349 +7489bd0c-0be7-318c-21e0-f6424b5e8f47,2279377080,3734479072392265879 +1755c697-9e8f-69f0-b7d8-3d0919d2ebe3,3379847963,16609426162114974341 +184c07ac-7018-0392-c50d-8f3d879ae5b4,924021698,14456221377900576422 +1e240ce7-0e4f-d490-9154-bdb49e0d5695,3297542641,501403012070758669 +88cf1d04-b6b5-b68a-69c2-1b1fe3f7ef6c,2713483248,7507961066653003970 +1ad1d78c-8b6e-225e-0f12-e694b2e2be89,2879957166,18157486152076401217 +7b39c222-cd70-46ef-a32e-eefb5a97f0d9,1927171241,5477727841471327985 +ae1606a2-a4d9-769b-d52d-c29f4428de77,1550914382,7410752178785142072 +6834c60a-6b09-6042-cce4-e99d834fccf2,1152147408,8620622089717204815 +71a7e5ae-6a22-9438-03e0-d6cb876e8a14,1231718288,13979265758757969426 +77a2ed71-1445-5be1-18b4-e7b91794205a,2756990848,687104652142624383 +27691544-d405-5e4e-29ef-ac4e34125be8,1696267080,11299704773331007952 +44c809dc-6636-991c-bd24-097d5429ce08,1900600067,3092888626369677577 +3d36b9e5-ff90-af10-9730-6b0e6ebc1a46,2707080193,10829146758987359641 +70de5e35-da5d-8548-8f83-c54780edacb6,862852490,8076352847662437832 +cb5dfce2-e1c4-6366-d11d-76763327fa8d,1162976035,15807615282166805185 +15f49c2d-09c3-d22b-0fe4-7740dc10e2b6,1559344741,7348534598639132878 +06d64786-9323-84b4-74d6-d392dd8d45a5,1502564208,3400962094675248555 +bf3c1149-6d2f-4c6c-20ee-007eb2ece908,4015937538,584948361715144919 +c7b06027-0dad-0985-e6fa-4ffbc70bdad4,1804828473,11269728630732431067 +43ed059b-0c69-6c74-7202-4e8415ab4854,919340182,12025388373391024817 +12d10dcc-02bc-ba3e-5e0f-ee1e201a3b40,759894431,12181851402766771139 +bc067474-ccdd-2f84-5c4d-f6ca16c60b1e,1398968712,4396911206676369195 +d06b10ac-8ecf-e776-a93c-a8ed9293e1d3,1317645603,2596001306511960652 +d80bd7cc-d340-60e2-f938-d2f3bef2d45f,3790843053,12427848364991285209 +69a3b275-b96c-2ea2-da87-3f39ca8bf06b,740687688,10525435867311336808 +f43155a9-5e2f-ae2c-7360-6117fdb63633,3706171993,8857286192575887142 +889d8ef8-2f1b-8dd2-33c1-78362762d2de,2130924691,791470032843891489 +d73d57c1-222b-ede0-9d7f-c80c2c021e54,1305667971,15866508340234197423 +59ba2e88-fc89-f0d8-0627-c5ee0fe35ce4,166768230,15278586162764603518 +bcf67daa-40bb-2943-5d3a-b5c47289bc33,2520050604,3663361409808258305 +e61379fc-9820-467f-1cc1-15cfedf87595,1403771243,15911566119453340602 +e32cb729-ce70-cd2b-1365-1846ed72f960,967377353,955303826436163081 +48c8ee55-1747-8597-da82-63d63b305500,1866378843,8001394950076421088 +a7c1f444-5dba-41c9-f4b7-1b4912bb34c7,960348910,14313833699790553787 +98d00da7-4399-69c1-a0a7-829a7fb4c98f,1106201965,17206380809660574549 +e0a101e9-7aa7-9d2e-42ce-e09030dbd9b4,2458769557,13126446433527817866 +99faa9fe-c971-cd88-0eda-9daf0821c68e,4125979928,9152632853965964866 +a27008e7-c22c-3791-22ad-96556101a499,1247770545,2783179201033716425 +4c1d4fd3-b86e-2dc8-c957-26c7ac7f8655,1835897165,7000807557551121584 +a4cfc8d9-d895-a4f8-6461-e8f23ef5e2be,1745669140,14261407393498779891 +7f26351b-aee9-e326-bf06-f5d76caad197,2448479928,8596563464355606542 +c14c4950-89de-a584-373a-86b926dd2b77,4027805115,17137609135638639423 +2e6e3ca9-2169-aa51-b255-04cf074ac88c,3693088229,14415310196209422995 +7d3ea34e-8fe7-023c-3cc2-14d88fcd84ef,3623954094,6605235624308826173 +0c6236f1-c6d2-93ff-54f6-b4d1c535b1ee,4016530959,8219980233748315393 +5843dae2-e113-9c94-fe2d-a0ff769a7d8c,87012011,15178717071801001604 +42bf6a74-8f7f-d8c9-3196-cb24cb0f2dde,4156738129,11032965440259701275 +253b1206-b943-8d09-e224-952c68d62127,1584045659,5788251226795881165 +feff5c11-a898-7f2f-13a9-8d3b1e7e5e7d,989234353,8600399166530432247 +942afd6d-29ad-5029-d34e-7a7e0829ada7,893721904,3904179669632097270 +b9bd2ebf-7988-e064-eb16-f2c65deae6a9,2649498040,7054282133929989466 +798daede-e51a-9675-fc22-bc05b0adb514,1841368022,13442257744084105790 +85dcff38-7897-5fb1-b6d9-ccf5c44a6676,1510724394,9222947825212529968 +695382bf-bc63-7550-dd20-5977faa0207e,1893088017,11080258828660827338 +6b61eaad-1974-18bd-ef27-ffba57560a66,3921567591,462203539037289254 +a96d07a6-afcc-f6a9-72c4-6d8e9e6f1772,3240688373,13978416778684225914 +bddc6da8-2165-0f40-9192-f23eded708d4,2865451396,13432270646412475138 +7c008281-0aa6-6530-923a-9805f2887bf1,2712267792,8470594172477007203 +d94eb9c1-7e60-80b8-9e9b-668397a39eef,3337790433,1312575134627920389 +4a0f4f0e-e0d2-1b25-e29d-1869c13a6aab,2281697739,9213392866757069161 +47ed0f61-2709-e6bb-e47c-f609668c3adb,2414028027,3296310680946616956 +6551e2a3-46b4-7955-edce-1afdfcc41db8,461160655,15009933306103340900 +b2105dae-4514-1be5-3ef4-bc0a7d330bee,685901512,12742452894637093621 +ad227277-6db2-daff-c9b4-c8e2a3fc4f14,306560321,5399436738263089811 +8bcb1098-6108-bcef-2bcb-58bcd074df92,1097305818,2970115726421350454 +d5ba3e1d-7b7e-7e85-65a8-b429059d3775,2270653381,15811605371355573932 +fa56d8ed-4764-b0df-5f1d-b4e411bf6126,3197272636,3907977662624430536 +25c20e25-da16-0eba-d0dd-525f74929acd,3211698021,9245776021275989497 +3bf5e6a3-99dd-bbe1-7158-9bd882353ebb,1410778639,12965074775845520483 +434fb7fc-b203-550a-a2df-eb4505e50b94,1036651666,5843654047190839884 +91a3de18-fdb4-0520-376b-8db2ef6d6032,3191874705,5330959724757032994 +465a7de0-e751-ea4f-b96a-90bc90381ae0,2645894061,435780935792582716 +d50375af-cf0e-bc9f-f49b-b1adaf3016b6,2845152512,2688320651101596784 +262fb81c-3db0-d485-efc9-595e785cb430,523169284,13926169994866720138 +3a6ad291-aec6-701e-c60e-dbfdafd7660b,1073360251,2926447966331174453 +b35c4c53-871e-da3c-ed3d-1fe3e03d81a3,1165760373,10233589379073763195 +d03c987f-30a3-4779-6c93-717af54f7eeb,4015336673,78841696316771617 +5816702d-0346-09b3-6b70-a6c36e4ec84b,3340516346,1104992083169752981 +fce8d1f8-c45a-3ee6-7f1f-417a0f8ebf15,917342161,17980174953774690300 +9eb8b271-5c45-dee5-ec43-3bd3dbd3b81b,2251907894,17777891550197883514 +e5ee18cd-54fd-838f-9fbc-9b3020bd00e7,3052933887,7617333681344224037 +6c23dfc9-2f8d-16db-0aed-a06ac883e75c,2653488960,1287460953432691945 +8ecb039f-382b-e340-3fd1-61a673aded06,2369961813,998880254561296343 +7a008a72-0c0b-ee49-fb95-48141f52c66a,489834060,2808121954320079050 +ef81dfd8-3409-3913-2c4f-95ccba7dd050,480729209,5182798988176571350 +3c098140-8bc8-6232-d68b-4b5eba7d8ce1,1591252106,6244779092962965572 +ff511abc-456f-65d2-dfa1-86921cc433b5,4219170287,7253322935061061923 +6af2950d-9285-ea0d-626c-8fd03cdf4da0,2304587949,17498813741879054540 +f90f60dd-6316-a59e-bbc6-f69a3c1c57a9,509666043,17590669723515139651 +50ee48ee-1953-4d9b-ae7c-710dfe2a5fb5,2835788868,17291089505264322831 +6a05d723-8410-67ac-5d73-0c38d736a9a9,4011025871,3335546662697139089 +b6cdff64-baf7-8b4b-4495-f6fc9ddd7b62,1571462174,8909301856731178495 +3aedb34f-1f6f-7b8c-1a12-5ab62fea2686,339389605,1540806752642200853 +4c8ea222-a224-beee-6f4b-19b4e6e3644f,660283726,17404165944314100660 +1053a6db-78c9-1391-5a87-33fa133893a0,1762698784,18390588636548223668 +13bb46e8-78c9-a844-644a-70f81fc56756,2751334870,669203345395879977 +42eb13be-0c4d-99ab-111d-7c49f3b8e37b,664631575,4203430549625984582 +8d3746c2-845b-babf-128c-0bb6844a2902,2402580453,5237215515227451306 +378c9736-0451-46bc-f581-2d178fc90a4a,1151684370,9516947076276032114 +34036d55-498e-eb4a-d47f-94c2a2892763,4281470612,10601167801468956478 +e49da451-e293-dde6-72ff-ba0d826eeddf,3654287937,11268218737012628195 +cba1acb1-d7c2-d970-ddb5-938fa217a0ee,4069403727,5780913496396682145 +84f9ff26-46b3-b23a-f4ce-80ff1a8a66b2,1986559400,12966226350574629294 +3e0230bd-fbd9-1ab7-574a-ecb0f53a2fab,142593162,4140950042168061250 +bfe1d805-8ab7-11cb-e8c1-c137f36c54c7,1414612121,10096994776538479954 +09c71019-970f-75ac-3b3d-8fcbd34d4f0d,81019490,14307067048820090786 +c2e1ade1-f154-f1c5-66c3-ab7225791216,2677381688,2062557446211395361 +001764d9-75c3-1e90-91a2-8ec2934a48a9,852783668,8898572620691947818 +396907c5-d7ea-8c3f-4a2f-94fe2bfe10f7,3193386482,10705514251871701109 +4807c921-27c5-0ccb-b2f9-432cdc9cff4d,1096553024,2796481429219030346 +b86f66ec-14af-3cb2-b920-0f9b702220ac,3696551263,2089398260887823317 +ff46e4c3-3c29-5b25-5b9e-54c391297f90,2370471289,967887986272497471 +0b99bd66-38ce-f09f-487c-198b67e96f53,2159772748,15045716872722293605 +c0abad8a-7ca1-c2c6-9666-45f0bfe0c21e,2337348791,16342189219980885947 +5532244e-9081-5888-d630-414f4f9d92a4,1812416013,4741313867375049631 +3d4aaacb-9efa-b88b-8512-e848da79eda0,4105006793,17477797535121263533 +3c0af812-d5e1-5db0-03ba-1bbe9fd442d6,3913925049,6287701971290800968 +719a7466-4398-44c8-e406-e6f762967547,3682101225,10801877599194117923 +75f9931e-e7fb-9e7e-7b60-fc7749e22a6b,2577656122,2444966316168216572 +21c0af91-922a-acc8-e38a-0ed82914b1d4,3524876347,9327401359660625347 +9be96c28-eede-debe-82df-a0a73e282ced,4091774050,9714634586579594461 +a32ac560-61b1-56aa-4e97-c389210509f8,595893039,17114871780834967868 +ef6ec5ad-1931-e249-9bd5-e8fa02f7b593,2122528458,13114162413425379370 +92b12283-e216-cc1e-ddb4-cf1456cd7d05,228693441,15048037808017963199 +b33d5ccb-70af-76a2-41e0-d4ffb63fb99a,129001216,6245568056390508193 +f8f981bc-40f3-457b-a6fa-7a9401b9bc96,3160585016,8442769168393909326 +6d7ad80b-942a-ae25-9b8e-6bc35586dbbe,1058046314,12521055757468915035 +a12e48cd-160e-107f-2001-0688a97c1238,1186847007,7982448319012715167 +f39f789e-b2e1-c0d8-9760-34056d04af6b,3077129986,4289646553068615627 +f39666b2-2c99-5e9d-0826-d4d2a8ab7b35,1142083053,17050836346723674663 +4bfd5a91-dc23-3ac1-d114-e221356e3aa3,4207488301,5358438949935508074 +14860d4e-c978-077a-e453-3d7e5530e9fc,569909059,3151476151971078217 +d4655434-4682-3500-9d78-a27c3c719cc7,2115253155,10446786097948705237 +e22642db-8eb1-337b-a9c9-6ddfa1ae6d5d,1012506995,18289277028535778468 +65507ab7-e4e3-3c66-cd13-82e55f68d49d,4251076233,6769808875711809207 +ace2350c-0e04-df8f-7f81-06e86744a8da,3954426899,5488585161342175696 +713b8a6f-36bb-e5b2-57fc-e544591e8ec4,1514818237,781346815907698620 +558cc3e5-5250-a092-655b-117ec91d141e,1603841100,17248151102222018692 +cdee1071-910c-3f79-58f3-4975e03fdb41,1023645691,6657325226702333757 +dcb74ef8-7299-b135-d582-f38537c999eb,818280213,14678630656958162557 +821c1455-50a1-5d4a-e40a-186939bac74f,2744164163,12468285055725317239 +a5aa6d58-804d-940b-b359-23950c4c2550,929081185,12955418607573037694 +c9d80bd1-8810-1be4-31af-53471db434cc,4231474193,7762681428323700083 +69f2bf6b-d21b-78b6-53e0-67972abb9520,3138972708,3364232507230282123 +14549714-5fe7-7006-d866-7ffdb6eba15e,2445686266,11828918806078401460 +0cebc116-6ab2-22ae-8bd3-5cfb800fa544,1605237439,4921913168172428036 +f033d4c9-3fa9-31db-10d0-c4e5b8b99cd8,4153499085,1492495192482068028 +a6007c10-3293-e24e-5371-2d95f1815a26,2045107860,18000151336330399368 +d94b2d20-50f9-98c7-3f80-4119d39cf2cd,3148249449,1864448502505778334 +4f23aaf2-e25c-b7ff-1527-e469d7582a1f,2204969824,8727534704700155797 +e513b313-82ec-359a-9fc5-53b6bec12de6,4015810681,14008624999020379816 +bc09b29e-4511-fc3b-a710-698553a851da,1728833406,1275790306620176811 +5a4b3308-db87-be2d-9805-65dfb70a493f,3150278854,2773184740820985451 +c1d16b97-d990-854b-c99f-7de2d7a74d32,2380693930,10681996885758493534 +66b556c4-0842-d4eb-c0c0-22c98682638f,466686742,9824972474116734457 +43a43fc5-a848-dfa6-4418-1bcc00afbdad,4149799477,3294150183392480940 +2a6f832a-4c7c-98d6-a929-4b8839ba0ac4,2001244790,7175588869560788674 +d9bbc4f4-9dd5-5e08-a5a3-67238d13bc98,1273505746,6980768961350470245 +b22c1ceb-63a8-dd5a-7c9c-14153d32af0e,4031690025,5102325387561854626 +2adc9749-c6cc-0073-bf4d-6ed182f95dc6,3161326290,8516145522355670209 +f938f7bc-1b86-37b7-a175-abb1f6d79e70,2100597483,1867343364754706281 +c17b6a3b-4fc9-c804-78f6-fe605333f906,702411384,12004094937338189490 +fa6e39c8-34f9-9806-788b-0718c4a1b4ca,1894446415,2937476504803766618 +db2c6908-f454-e595-38c4-6a22e610fb89,1560490602,13857443446352355850 +4a400951-2f9e-3309-6afa-1c7065b6c570,1664996922,228396202861701181 +52ef0d9b-dfea-6832-a756-d6657ee5344f,4014141996,6984242884387822210 +90322c66-7c61-737f-a034-3a19bca701db,1909486621,11410261750026571599 +37abd2e0-795a-7202-db2b-f687405e9c10,3532103719,10659641029217667188 +4ffc9025-2630-2b1f-d5b5-2ca0b7f63f70,3293864307,13963072407841417829 +7122dc7b-d7dc-0d11-ea9a-4744dd5e9135,227816988,7660681283455902464 +f410b5b5-4214-0920-adbd-944fe8e868e1,1057194606,5928633519340170956 +62d2cd62-250a-27ee-fa15-a41aa93343c8,1048188529,10906416238739768684 +eec8a29f-df77-3c38-92c0-c7a17c9162bc,1119604513,3555449176746929676 +cced08be-6d94-b6bd-ab79-e06b807e8ff6,1065648588,14586219975085650905 +ae57f485-e70e-d3eb-3160-3a3abb2b5bae,315574171,13474258063676967844 +67d60dce-c115-32f0-0283-0671944d0d31,1476540427,16614181491425133515 +b8c5dc86-4a85-e1ae-e511-d23631be9e3a,3629206922,17245558133573965394 +4fadbe82-cded-bc56-df2a-33d4fa7d1d1b,651427598,14421273679959382681 +b43979a9-778a-7a56-299a-84bba213065d,1086945602,445296042707428141 +630eeebe-877c-aff2-19a9-530ac3ff284c,3683231411,18435946434131366238 +5eaf95b3-6222-d37e-9808-a4c4be344752,1171633421,10604027930721569439 +e55a97e2-6b79-5ce9-88d4-8c339345ba1b,2445553364,780655032538295874 +2396764f-192e-1c15-0f4a-bbaf109dc155,930039687,1841373320649318911 +be72577f-bd65-3c46-c86a-0fb63be8cf2c,123383722,7132935644420210602 +7c01c64e-cf3d-fe01-2613-17fa63d8735a,4258784124,5376555237707454226 +261fc826-fbd2-e3d3-d8d2-532cbe6a2578,4284461076,11915585623533141357 +cc8fc888-eed4-b87f-abf0-0c66aeed89cf,1695958908,16403069083610928310 +68cdb9fc-ab56-ddbc-321e-ce7bc8e3ebf9,3578622891,17346236264910739729 +3b0686d5-9fcd-e5e2-a5a4-d239980b3be1,1319708611,7141516775620921353 +bd37274b-b43a-4b0f-7871-e024a77762aa,757350555,16952816539661620787 +4ca358b3-b995-feb4-344a-769c001e8f0e,2249142844,858306619176559642 +597f5688-ccaf-f775-f70b-34d039886b24,10813023,13369719723819762625 +0431e6aa-b165-3863-d169-667bded9e08c,1114849501,13648366233612873539 +340b8c3b-413c-bf41-15f0-78cc3bb3962c,3104494780,5948773684488497579 +dd50d1e9-ea4c-e1e4-9968-eedbfb6088ec,3603676666,10821939055805262364 +350f9ea4-e99f-67b8-526e-186cbb921cc2,1138893616,11495882814988514718 +39e10198-d669-5e4f-9d92-123b9276d445,2887285509,6076848433993151369 +e3261a9e-3ad4-cb11-2515-c508e420a237,1637426803,5802022256880919234 +8e83658b-3918-c9ff-514b-0447450d7af4,1892160843,8523522262861518344 +da555be0-e351-3415-7afc-03adc0c7ec1d,3967103917,11748877079108675706 +8f72c8fa-7b1b-ab64-de7d-04d59304ef1b,1237713057,17686609822826221176 +04f6ca64-94ee-b878-47ac-1530d6a1c361,1711025871,10479002818200582586 +78f653bb-e606-dc9e-45a1-a7e6f50eeaf5,2286026632,11533930684673597495 +e69d0539-467a-b810-78f5-4519ccfad9a4,1294459824,2208301532433215379 +f3540fbc-672c-18f7-cf7a-4485921ebaa0,11027254,14197627363285319260 +7a138a8b-2168-9df7-a583-5d444ba1433e,2561290887,2013843250332435752 +ec8c8fa1-a0d3-c54b-2170-66eef58035ae,3223814141,2840113124700542376 +e2a04467-c9dd-7ed2-16eb-d5db24a18407,3562749897,15478418984224454796 +75df2e17-054d-a72e-8937-a240bff5b44a,1999784897,15506447548692959454 +ffbdb6f7-a5d5-18be-ed1d-a9c220a5f5a2,3785823861,8987905354922137339 +bac4a812-b3f4-04e2-66d2-15c28744c7e7,1148286083,7007292934008427386 +f66bfc3b-224c-0ce2-ddb7-748a3ff281f8,1871647984,2087587187704780933 +4b3fd74c-0975-ed99-fad0-3dfa02b18437,1261812168,9858521580276575422 +75826bbd-ff5b-ce87-e3b5-93010ba18198,1821632896,17566288583022706948 +22d1c69e-c7f6-46ef-7ac7-b8d08efedc7b,4160856216,8984460817582958168 +ebeb26e5-20b0-5f48-781e-03df389ed47a,2058444582,17211455541328974014 +51d665f6-9bc5-3e0c-26fa-34ab6e73b03a,1906734213,4335405414405736455 +74677d51-9234-714c-4ecf-43044956d31a,383997123,14900044904973804983 +64c67191-b2d9-134e-43b7-13feeb80742f,1922931425,11347935138801671936 +765b4979-0555-f22b-7dec-664373a25878,1098308971,13691158380825841025 +471e4c0d-7bea-ac4e-235a-bba86c8ca824,3630459162,1656152097060130216 +e2a73f29-dc50-7650-69e9-84a1df081c98,3049804254,15358132639706466217 +0c5584ba-0294-490b-4b66-3ae378769396,2338840480,8759894572664161141 +c368da29-eb61-5ffc-75df-6ac87494b1fa,4041039438,16904896159976190742 +1eb79234-97c1-e2d2-6707-578cfafaa80a,3258528882,2190043195058874108 +19283874-9eaa-7ccf-76eb-880488a44348,1018148015,366763332126995400 +a17dfdb3-5334-57d4-e258-8c99f7ad6934,1240649368,6396221595191583387 +14c38c85-de99-b0a2-afc1-9b1c830740f8,844130382,16243426534026326664 +28c7eee7-617f-8025-c112-ffbd0ee6b8e6,1422196152,1860455850753523754 +3492a891-d0df-d8c4-5e8c-5f171f86017a,3274225707,16728912637029580247 +8e01659e-93cb-c481-9de7-b947478a14a8,1001335200,6409355840435794782 +74636bd2-d614-61dd-a880-a3b278418407,3415921838,10919874034065952344 +558bfb19-c028-0bb6-e2e7-b8a79670183c,3463086547,16864601109833740955 +b56d0837-3b00-3193-5c58-acb550773eec,2733497252,11434213844449834417 +196e9056-0473-735e-9c9d-48498efbba8c,1465997098,5385690495996360387 +6b3ff59e-75eb-5933-e46b-9b5fba2927dd,656514026,7510518488479384800 +eb452cf4-d573-d21f-4a84-387f44f2be9b,4112437355,11165893660954230572 +98657ec4-bbeb-1379-e268-f1c56153ae06,599031540,8894161594767878704 +7cdcd19b-909b-a71a-3954-e602ec7a4fbb,448018278,11515757140803363532 +fc90cf1d-7ad3-44b3-608d-aae61e3d2721,1561371055,13677814718352259815 +31600473-7961-e447-1b23-5551c03cfb8e,2773641376,8260261998893158488 +ca63433a-57f1-7cb4-2c5c-d72ae876d2e5,4120740700,14418197501115369234 +46008f7b-996f-d23d-f7e6-cf98d612c4a9,2242598104,12680881789353357156 +a5f5de52-4bb4-ff10-c377-ebcb039534f2,4185599418,16011083381852725207 +facbcee1-0e50-6e05-4c7f-61f8950a7249,719670461,15395903616198031297 +03a64c4f-5cd9-ed83-f844-4176e8e76832,74551458,1228807473426859295 +73d4e620-1c12-a062-9775-0d65e2bc19df,1940055933,13863417818511879593 +97783c59-156e-0abd-4606-f81cc8d68c96,2893529208,4086924721177821548 +e1488c88-efb6-f488-cd08-fd3fef357e20,474743555,10461838270113231471 +c97533ea-c98e-c011-a804-0d2e93da06b3,2086706842,621909463380902798 +b091629a-e1bd-d098-089e-d855440c3443,2555931905,10221352739767822653 +c482f23f-3576-beec-75c9-2d9041d3ab83,3685137682,17459984923779025143 +264ce9fe-e42c-0ecd-25c9-c34038a593e9,3262808214,18178432116023259346 +bd14ef16-dd48-6cd1-3b3c-86a3fd813108,4059506222,1718144124906875895 +9b17e070-5fb1-a9cd-e9e1-dce96c9c50d5,1455741943,2833066319222601052 +a701293b-a91c-e9d3-e0a9-016218df4f35,4181156662,2588364445767063567 +ec4ec4c6-6e0a-105c-1cf5-d59eba69db25,3815658306,7685541555486216469 +48de491e-c3ad-a7a6-fffd-fb604a2081ce,595304747,17423239564865792330 +0f9437a8-9522-bdad-9ca8-c749d853776e,2126203923,5374362840507147998 +42ced42e-a26d-d594-0858-146ec5f3fdcc,2459384746,14704785973176664245 +0e3f7440-a847-9869-ab66-a21d99c9f68c,1109158639,11290215915245937921 +97c8071e-ff37-aff0-78f5-adac26f3e403,3038154386,6454699607509018169 +a296f3b6-1cf6-bcc2-400d-779ee3e05094,3682570183,14295291843665277647 +6f49e4f0-d076-c38b-8ae1-02749cf46a28,3242363039,18302389147646033722 +34bfeff5-9ae1-eef7-9db0-e834c742d6c0,3642164724,5889236261912271066 +c26d61ec-88d8-d7a2-afcd-2c3192574338,154505107,766454946673323672 +7fcfb187-4cfd-cbe3-86b2-77e10bfb2b89,3055176861,17175149320475802206 +11a44721-bf80-9cba-0058-bde7e8011f53,1659441355,1785390546843389270 +f9b4ebad-eec4-abdd-809b-07b43d9fb105,791802017,9668577569206012073 +dc6085c1-e3af-dbae-911e-26072b20adca,1376874951,9402603887520129553 +db113261-418a-fc12-4f44-e051b269b890,91348781,12447518382805263200 +150dacab-b969-9e96-99ea-1ea120b193b2,1003698621,5917700538677130083 +9cd79f6e-073e-d6e1-451f-8189752efb21,2784552753,13842575741475870042 +39b95641-fbad-d550-c6cb-5ce05bbd8ca2,445507233,2477818658080237554 +d9bc3af9-16cc-846b-7057-ed47fd42136f,2372236521,10375462599386460193 +c95da8e2-34d1-c92d-b50c-2c29f829f4b1,4225168645,5775206234636504366 +15a88ee1-863c-8fb9-f5c3-8d713d951faa,2759564659,18222557088253912938 +be5f99bb-92dc-c03f-207c-970bc3bdb356,3806016463,9375706137615481460 +d0ed8b61-c683-0644-0515-b86616abeb5c,486729299,9628151073403871836 +4f3501a4-fcce-3682-576e-9a20c7eae2f8,1914313489,5066234261755915185 +5ba99005-6955-937e-cc84-f2dd2c00bec9,625568635,4685576988772020018 +aaba0d0f-c15b-2bca-a73f-88e8f0611be2,3721983754,12200965509119282420 +ec0aa8ae-6f55-ebf8-b350-3db9ba228e86,2341520373,11772485552675692029 +ff9891e6-64c7-0f2f-de8a-9f1eb50a20f4,622719184,17838699040964948131 +aa416188-360b-5cdb-8699-e8e504ee3cf8,1318067694,8210680897822747203 +bec600ed-632c-3496-e0bf-5ffa055325ca,4001017847,10341595665029293259 +e9f17a78-7db2-19bd-8c71-f9b10ff6f750,3251480288,17261012497208913687 +72b0ff31-ff0f-c564-c273-831cbb3c13b6,3532060550,12383623252284816342 +7a23644b-72d3-4d94-de32-a06138145e57,3949633263,15993727098792635633 +de741e7c-d8fd-3f2c-8794-af25ee7064e6,2379889465,13607533049263292921 +56d6dd48-eb5c-93c8-8ec2-77bdb60d86d6,661608797,4186886879031666587 +e2b92e6e-b263-c2d3-1b80-9c0818662f8c,745280385,11680116736811715377 +402f95dd-a607-7684-9a06-4f49f36698a7,1895384823,1899798490312090068 +b1e18838-ca94-ac0b-edaa-f8f2decad071,745298403,17286182024636032558 +d86bf927-02e2-0593-1e1f-fbe5afbe9df2,515445274,4599455761924755717 +dde3b463-7f23-1595-1d5f-e90166ae84d7,1758749590,1076105136604705021 +0b57691e-2c23-8f6a-efdb-32c453925692,3256840889,17951565521172629510 +d4ff0a40-d20a-6aa5-26f5-bd4dbe6919c4,481851502,14404864469370403724 +a2d2842e-383d-4aa0-3cd7-231567252bad,936116712,781095813566384599 +f6675873-2371-aebf-bf79-944ae192a29b,1382507889,4634895052997176333 +dd4ad259-1f3d-d66f-7beb-69c926795b89,168053980,2967649072234195414 +b1ca21d8-6d3f-9121-6015-797847ba5e9f,3596023532,16788678207235554866 +54f8968a-8e28-b43f-ad9c-b2feaad45859,3104639138,4347440775883462163 +9acfc029-c898-a0f8-91a1-28109d6309f0,932955737,16010977888453005917 +3b9f1e05-d4e6-bce5-fe08-8e981d53b5dc,3213658393,15186206064841884464 +b8507701-28b1-74a0-7d96-c159bc4973fa,2325723098,12852890449199659120 +5141c53d-92b7-3754-e343-710630184448,1968228070,3413048814640167092 +57e4bc82-2b47-fc97-e6c5-adaf0ce74b17,156598488,6665487678851722780 +7bfedabe-7b17-434a-d6f4-fddaaabea2ad,1250111628,12564767669251561825 +e56bc757-774f-c241-8373-bd7c63b9638c,2071903914,5285157281186397803 +be2df334-a6a5-fa48-d3e4-1cfa7e35a7fa,3211829815,3290585749482428256 +fb270d47-e600-6128-1a8f-6dbffde9ccdd,3193725333,11700039566922552925 +4be934ff-63ad-d140-55f8-b853bcd00078,730701770,7705853728712356103 +71607043-0208-9bc1-0d30-84241b2ba5e4,716677825,13791197901517025263 +c15969f4-fe6c-7b76-ee25-be8b8449de2b,1658767312,16520736355308408436 +d391850b-cba4-0901-6e3d-b51930a27bd4,502729506,15757686817459641532 +50dcdaab-7a54-a0e3-6274-c677e3da5389,1614156923,14870295549563779098 +6e8cbe14-50d1-1b9f-dbec-f1ae27bdad82,650929484,12757792437153956759 +caccc358-5694-2ca7-6082-53062d867849,3777401742,15128979162420875684 +cfd5b5e0-04df-391a-4f48-500a8d945043,2432965479,8033424765123059786 +f43f5f0a-ab80-9589-5678-a9be1cf2e370,3704128010,14108409665250994161 +5606acb0-2653-bce6-5eba-017576ebbade,989956051,13660755800417201910 +ec3fc417-4168-4c4b-f310-ce236ebd71d3,788296101,6555296799648261585 +576a7834-1fd5-84cd-f047-a6ef551ec8ed,2653257099,2939746776211188135 +dc71f88f-47b0-8cf9-7bd0-f149cd990546,3429794116,4672054563428322308 +acb903ac-7dd4-87c9-abde-076ace052e21,504330895,7196165543448135506 +65d5011a-b85a-7a3d-8864-6e70f5ac5a1b,190515274,2203927845037150738 +45993268-ed5e-4d0f-0876-69a70b3ae875,3208820722,8937345106794258390 +80f352c0-76cd-9922-ef87-963dae78982f,1725733715,1334021053067229452 +57a10354-3d2f-694b-c0a3-7c1b7ee9e281,1331647234,16757282962408979658 +d6f28788-9ba4-1550-ba64-252fbc1653d8,1099978582,1953763341949901403 +64312846-b8a9-aa59-5537-0d5a270135f5,2421016771,13974053400689746812 +1675c569-601b-c018-4882-bc9ff875b521,1759886678,12675654022181226214 +641124dc-8369-1b2b-4970-f4360fd341a8,832634343,3209789650770382953 +c0089f5c-4e04-ce1a-de3a-d0e8eba9ac46,2608997113,10115669576457606619 +e1ab915e-5680-f3cf-d2dc-77dfe74318c0,2450732227,8459886503971939320 +3e09969f-5a5d-053a-1c26-dc76d49d9152,3885884887,14240842627550862069 +d69175d7-7645-6c54-7d5d-7254d65adcca,2723111570,9607206295221941762 +43b44864-1018-2deb-558c-4c56155ec865,4217011991,7294049108688350812 +aa675253-fe90-58d2-a2a3-e533de6a4fc5,598688338,12669540210297318172 +376681b4-e15b-6986-253f-b41c221f16cc,2137840729,2117658901581814530 +3356145b-3af8-5ae1-096e-60aa16cee297,519874845,18290177823216012343 +a162105b-1ba6-3e8e-c57e-67eaf875c3df,857707724,16466679863680433886 +8849aa40-8680-be75-fae2-7de05b628d97,2126867380,9630819224777562771 +5c4ce0f2-33dd-db67-fd1e-b0df1412f7da,1379039985,10341214671926507085 +fa1fc0bc-95e2-72ac-070d-69f28b9118cc,709352624,12700400333594409318 +3461af33-72df-984a-f03f-a49bd9433c8e,2565675728,1051550088873030986 +43d1e33c-3ed6-78aa-38fe-55871e2d6604,1684015492,15939538983172708366 +39c888a0-cddb-86f7-b4e5-f1ae596cf355,3557389874,17803861361200663636 +69213561-f48b-da09-1ceb-6cedae362bc3,361513384,4453020379410832894 +1cbc5eb8-dbb3-b07d-42bc-6bc27aa3afb2,918619256,8450573606123893720 +5fb9fddd-dcd2-0e59-3dd9-ad9bb0ec69fd,3373620429,7246654161474570880 +1c646de0-0228-06f6-68b0-e29273857651,42287174,16837256522568770575 +575f4b16-f2a1-b2be-2489-18f409ed53f1,2037678143,7249914796532071981 +06c43b53-8215-4372-30a8-02cb5c82f92e,602470673,6269494818449052419 +4bb7ee64-25c4-35f3-7594-6b1f5c788346,2226799563,3208396073960483800 +5c1acfa3-5d34-4e99-b429-fe02188ddcc1,2151795428,6343152856724935240 +dc59099e-b3c2-36c7-e173-7e76448e30a7,2181787259,1516213099614131358 +33372158-4fc6-c1a0-c57c-408ab9477392,3718930508,5674191826173986204 +7d438b23-7b6f-730a-a748-2bd55cf1a154,1245493777,13822131997721452826 +5901f4ce-830e-f151-9440-e4d6789db705,2959051454,2736924563156034962 +27988c05-8946-2635-5083-97cbdecf9a86,215035345,15630677337885207038 +e6e695d4-b6ae-74d3-2d1e-18ab15263ba1,1823348342,2522316691581645489 +3635b698-c3b1-bd13-350a-89aca790f777,1248888312,6280895763692335424 +6e93fddd-c959-c264-2893-f0bf7b42b05b,3771259047,785991026334107282 +5c07086e-fa5b-799f-d3ad-951ec267c8ce,1689127702,11152980911823988033 +97aa0432-6525-553f-6794-82756076e6d2,2355096470,6260194729080556505 +60e2f648-ddbb-b013-3dd2-dcd029550120,2034476068,2807770520034784869 +e41550c0-764e-2a55-a7f1-2d62f1f59f33,3895219289,2305604346005793649 +4cffcf30-aa0d-7603-836c-fc8b474ca8d0,3557034470,12874449407947416426 +c35d52e8-c447-afeb-c41c-29c1259e631c,66029233,15989321382921263973 +fcb4403b-994f-8faa-c1e1-f712c4a8859e,67523944,369356558146714786 +b99ec4a4-f012-0ae2-01dd-6e15eb7f1cfb,895658341,3464737488862474759 +459fedc0-b0f8-d1b6-43a7-d827a3206d96,3126021946,3830600341550295125 +ba04b258-d0a1-9396-63bc-3a2bc64ffc4a,1982157856,3930904595629867693 +d951d85b-cc40-2eda-2ec9-cf0ef1de4fa0,661534414,2831871628661497717 +46c53726-f3bd-a144-8ae0-418b1f148699,3322037380,12644882623825892309 +232dd141-084e-86dd-ee35-436d29573fc7,1280755096,15205718649727485096 +a87a4c3c-09db-a79f-51d7-e976f4db747d,2259222135,228146686762045581 +2eec6cdd-cbde-2ff0-b1c1-1df790d9499c,3160367126,13029266464995853834 +284b2f2c-3a84-444d-d278-7f6aed2ca879,2473874875,14012748473004012060 +5244a1fc-347e-78fe-02ea-c977264fc798,834331620,3251059986775129979 +cd0efcb8-e101-21cf-0066-46ccbb696df7,1041758737,10880901376066884646 +8645f87e-362d-3675-223b-4f54386acdef,2864829231,12725136443388127184 +c36de2d7-807d-f98e-048d-29848e028982,436557618,9136360026724058136 +4e7d1f05-5ba4-864b-57dc-d17c12abac05,2451519041,5487392679099953893 +cf9ddcaa-ee6a-b367-7a64-94039d97d458,996003804,14677163778517696817 +3141f315-c1bb-27c7-f7e7-97a44354eb95,1082393575,2986276591691964926 +1d44b1ee-f8ce-2b31-24bc-94398ad93365,4085077234,12350144594580408918 +666ace67-a497-1190-4a21-297b3123fdaa,127622308,5409133233733125186 +5295aa31-e02c-0532-9601-47d267745a47,2726442800,17605592133495188625 +85515447-960a-1618-e07f-1ab5aef7f5bb,80193031,15009692588655509968 +2a35db99-b90c-b5b4-7c67-c8a005574229,3825829539,2880578748535895972 +868dba88-f1fd-4dfd-a517-bc672201c558,4056047340,1838386010681110589 +648df9b3-c29c-b8d6-027e-b0340d329e3a,1288453311,2985388868261976458 +184d6bb2-1a7e-5fa5-70e5-4ca0abfe4b5f,3471884749,3074268021514098219 +582ef254-1643-ce36-8212-0fa454b127dc,1146142974,12755420050458609411 +31170533-eb2d-66d1-8c2a-89a005200974,1130873948,9329703565593235439 +9e0c62d9-43dd-a824-cfa0-5488df0b2d58,2185428005,2134904869932293284 +c3cb9ee2-8487-e5c2-46d1-750cabd4617d,1765895138,1038507121380384677 +fbf778f3-6c43-b5d2-df91-0f1ba7fbc102,2247186963,7164370345698511954 +b646f38a-9fda-d598-23e2-dedfe24b83f9,3350315694,4179829338800365888 +895ad1a7-6229-0359-e13f-1c330d3cf7ff,3710957519,7095955175366849470 +fed98218-903b-9968-73ea-eb37f8f4a342,2175830253,903311604392252061 +c58ac953-48c3-2694-98e8-8ce485e0cb4a,2926640862,14644044085948252466 +cb8fdcdb-3bba-3e18-89ba-4f9151a13f0e,1644460852,10982188846996173598 +edf38114-cffd-1e94-eae7-54b996981d54,1716240170,9302102768244458169 +0e527602-cb86-526e-e0e9-5de9eb77406d,1668575801,9715973916146193352 +3c2f5782-1a8f-04e6-2bc4-c8f2a9e06ec6,2871499116,723433402433022091 +1d7b0f8e-7c44-bcdb-ec7c-c2a37472acf2,2105754092,6641592767218195895 +b4a7f74b-9b5a-22d3-6054-58139bfe93c1,3359145390,12834962082243212735 +51caf624-bd4b-59b2-8fd8-8b7293401d1a,2568767518,5274271904411418152 +4f70858d-fabe-1c85-c138-fa00a472998d,3939227187,12298792930901941008 +8264ebc0-c11e-116c-ca22-c4d69d8a770b,2213544505,4399439117888399726 +13dfc9f9-433b-b16d-5723-cb4ba69c687e,2589782431,6316020717673501099 +5da1681a-14e5-486b-f193-55a4383e32bb,1358884873,10759862724646985170 +ec9373ec-f971-1adb-47b0-a7ff31d5a35f,4031282422,13562313597198165935 +2ff43bd1-a07f-3ca5-391e-850c7ad09d2d,2909534809,16334433160525617817 +d399350b-2347-c8a2-4339-b9a5b72a4f29,1208012051,16719742172119033987 +3aa21883-12b3-705f-23d4-a423e1a94806,3500734653,4464026629811390525 +ff18bf2b-4843-0da2-0218-34afb4502a0e,801654406,17470242237658225493 +7b990c27-c589-4304-de9e-22ef49b3a753,2426614039,12231667486555003643 +44607e74-82ec-a4eb-b206-6ae57879386a,2537112371,11094539413400957191 +e4ab02cc-cb1f-e417-dd75-80400f2395db,1368052328,12922026364288794532 +896ad187-319e-5e8d-6a02-d02bb8a6ebf2,4139576546,17980666004375451990 +ae0a8b35-437f-92ac-9817-0108929c1294,918489996,13902877140038034038 +a48f595d-461a-9dcc-c856-717cb2b35cbb,3949644386,255591583854270294 +38c182a4-3692-eeff-b537-38b21a270230,1060720616,13573544755577543431 +1c9faffc-55e5-7464-6db6-d9d93365b03c,3594764570,1794790274481311729 +e6ffb323-3742-49db-a655-b8d58964e2d5,2938738797,15058631483691308439 +9d05cfa8-e2a1-4cef-6cb3-630b46e24677,643054678,6622242240517565488 +e85f70b1-6165-8daa-2b45-920a940cfa58,2516036761,1835226520298143071 +1b6a13b4-b29b-3897-c8f1-2459307c4a65,2520376002,11480739725907680043 +07526b1c-8660-a92d-488f-84101eb0fbd9,2924761437,11678011332172185382 +bcbfa9bf-ed48-0fdf-4eb7-441b629eddde,1030100541,13275669125129127242 +f50874e6-cd49-f10c-fda7-fdb0242cb30e,524834748,15154239729794119113 +bbd9bbfe-43dc-859a-1ae8-b552d55d991a,2334838428,17155281464320509708 +ed5cf7f2-297c-966e-575c-c5856a57424a,312114658,582454906116093611 +bc55e932-f2d8-ece4-e208-ddc58b5fc343,1722926850,2953895663839795458 +7c72b839-1d6c-52d6-d454-162d50d84f1c,207391648,2906255736597079738 +4c918143-4a29-1c25-fe31-583f8440d38a,1315123858,12741761200928074731 +ad46b7b7-9ea3-104d-18bc-3bb5bc02940d,2431143959,11575489642330505325 +ada64a89-0756-15bd-6932-b34b93416b1d,4014376391,15881428268827236628 +b2e9a5f2-c93b-b94b-6f94-0957dbc3b0cb,4174194088,14726862978041830985 +88bfbbc6-1115-9ac9-b614-27e6ea2fe2aa,3391807475,9402609789529157288 +af536720-cbf7-2bcc-a681-fd1fca002db4,2575618855,11573277485922006133 +7d2c7f71-278d-7512-eb8d-82d52db62731,3197977215,9771508277518641306 +3125ddfa-a2b9-3fd0-ec2b-fb02b0e6dd3d,292780638,1032834568697906986 +0d4cd940-d3ed-70f8-2137-a57eae4b1cfc,1122324108,17720384951598737251 +12765cc2-fc01-f6e3-801a-53ba8b978242,2344492821,1836211769315371408 +0666ab05-e748-d921-e88a-d9f008edc856,1076191041,7358194773009178463 +bc37e0c8-79ab-a24c-cdfe-21bc4827c159,1765657234,15678921601342179224 +bd1fb293-d4f7-64b6-e087-8043ecb2d159,515867770,4177511411511897284 +f430e26c-6cd3-f7af-ad26-da0af4e53eba,1682589793,11215366758354549946 +a0cc2c28-b4f2-8b7e-3da4-7ee95e9f8e73,2919650713,3193381568892478074 +b25645e4-d5b4-b5e1-0ea0-7bddd1e81de9,2751437421,14620804379032642276 +d515ebdd-73e2-e0a2-e583-bdcf479aebd0,2629447285,15850253855587631062 +2723f56d-35d1-e5be-dc28-37fb214c0bb4,1521090915,11646235184934757002 +809f0d56-d9f5-3246-47c6-d76f57ac141c,2163383453,2984189673969848456 +0e880e1e-32b9-abda-b92e-94d3c029f286,1870083462,15613458431865266814 +ced3ca56-5db0-3014-1e1d-6bc408eda6e6,4058736203,5237117889960216341 +3d69a1ba-f84d-8c69-5204-00a9731c3d2d,778489314,8069686385842331462 +82271cba-6000-2eac-a650-3abd1c0404d1,1165282549,14842535136406994180 +f780b45b-9779-e190-6d38-b8110c4d57b7,987532280,5302014267328361975 +3b38ef8c-1083-9582-1e86-38e0e3b5effb,2411762153,14055638483399223743 +339a56a2-0230-20b8-3d87-aa790613bb00,3525467539,6888741540765341573 +9b8877c9-a291-d6d2-a5ce-d7593d63d215,539553771,14148479917806336867 +993c9523-a640-80fb-9bca-9b3cd508223f,4290534129,8633108454857951372 +4dcfe7d6-3d80-2e0e-d86e-67b0ca747b7b,285714745,9941989909408507171 +c5e3d20a-2a13-fb24-ef74-8b2d2317d2ca,984855275,1243790015781328536 +3929f73a-09ae-f1de-036a-f683ab944c07,1480564422,5398809072344897550 +9441fe90-5e92-721c-37ea-9b8ee5e75fb3,2278727632,12997172108455519282 +0511ac78-64c7-de73-83c8-630c62392ce1,4255284786,1769921808199506592 +3a5512fd-b3e0-67c1-2981-8df46c03c8d3,2009578142,12861709221470609626 +06081c7f-a670-ddd0-b38e-92881b5c2259,1262125806,4028698053938046033 +30486bce-2ad8-fe3c-38d7-556ff03b41ed,1022645347,940024340244689196 +bc219a35-dda2-4f26-c984-c86b83062e9a,2865894291,18027469558452241145 +d385c0dd-ab85-bee5-bd87-2a6a477ea8f0,4289183864,12428519418113048993 +338f2301-bca8-7796-02f4-c481f72fab63,3721324473,5608081281098045780 +f56d9731-b3d0-563d-a6d8-2c399f918832,2169128636,12901092923030642134 +6d843194-ae8c-a6c0-46da-739ea63f66cf,349281259,12199012958916967067 +3d2b74ac-940d-949a-1b4f-7cc7df43874e,1701860665,14746245212649963972 +6dc1c18f-32bb-370c-e662-694defab11c6,1272120274,3818915705426917467 +65bd20f2-6033-4453-568d-143e9fa18d1a,1401117812,2691207500136644812 +d412333b-3218-9c1f-d707-b5b5f057261a,2023917077,17656479568291865542 +e2b90fed-3063-b9d7-3d80-37c4d486a76e,2539409403,17530102237168329682 +2a5f0f7a-d898-8001-ca78-460358371446,1933981859,15244709744100634220 +35521447-3170-e35b-a29f-886ab95c28ed,1473655967,238982674405550501 +d82139a3-c4ed-316d-dd82-eda407320196,1873029135,8846544104439474165 +2ebfa993-bfc8-0aa6-38aa-bebc5cf7fc3e,1086294246,10897199979459473565 +c6085c3b-4131-cb34-17b1-53e1ba82692c,2060311386,2437233825146067023 +71de2d79-38b7-93b4-d85d-15dc90cdb63a,1641351018,2274097281079411678 +91f049e8-ad16-d9d9-5183-a8b6074aa5ee,3423480126,11146416619639028826 +09fa4644-6db5-8b4a-fcde-70685c9ce6f1,1646668484,18088125110588434512 +f8976594-079d-29c0-f336-c8391791b1b0,763553110,5744275910382808898 +84b1cf9f-efde-e97b-5672-26eb1105c182,3410999361,10014655030750822917 +3beea4d7-a032-a4fc-fa1b-113ed28a12ca,1404478119,7705513453652398056 +96eaba83-4ce7-49e5-8a4c-1731195a4bd0,389371089,8774308016331427475 +d55ceccb-7e07-8497-9b80-e5d0110ee869,2399231161,17130666656886037406 +bd6c6329-7a58-49e0-b37f-1efe0449a6ce,3581204762,16179148386086836125 +1baa69f0-3bd2-36eb-5ec6-cd82ff6c4862,3250073025,13343328639422434730 +59bac1a6-6fc0-c4d0-4d42-0ce240941d76,2542706239,3734620686751611430 +31dbdd3e-a398-beb3-56cb-83116b922daa,1443535925,2516077724376448695 +a903902f-cf61-2176-a3d1-5683920e28da,2190381089,14331764599669753369 +ceb1a279-0635-940d-8956-120ae40fa78c,3052336398,3354045988197194206 +32c4d45d-e0d8-ec4c-aedb-ed94094177b1,1998780831,16940182972721593533 +0fcccb85-76e3-38e1-2279-81d319bab878,3207663826,2472844207621769707 +3f4004ac-1214-95e4-e6ce-c57387f64be9,1120283920,16162089080878438349 +449896a4-022f-d2fc-301a-c976acde2130,131435604,7123819063628759433 +ba860f66-ab29-2fe0-7670-672796d401bc,3629682,12345805551181702723 +3ea65a02-ac49-d37e-7806-95c6c10ad4cd,1471062852,10491047226696160561 +2af8b3d0-66e9-66ef-b0c2-2dcb64bc61e0,243149640,794019070661294957 +5a2f89e8-32ff-6439-644e-066d26f35dfa,560247998,15684737959508549993 +a196bde3-135b-79aa-b6bd-1b468ed98fda,63668026,6358681690007580465 +baad6976-4873-d5b9-6bd3-b5201cf4999d,1137008677,1261954653015926466 +1973ebea-155b-9323-a1a8-ee9b818dd1db,659774516,2934147480236251883 +81761deb-edf9-f43c-af60-570905b5d30c,3527039088,4559995180464540507 +50c6bf2b-278a-48a5-f1b8-3755a1e79a11,4049573309,18255479468926939231 +48a039ad-2046-ad6c-bd38-b6a76b5d1a03,2580711566,17424524451111602000 +70ea3dd5-d881-9159-80f7-c32bcb82a375,229401822,17386120206579522782 +a34a4e08-5b49-8cbf-95a4-1273a8680c5b,3763108633,8685492421920876140 +68670410-407e-2a42-2940-e710f3d72287,1789724234,5240676376047689461 +adde7530-3576-809f-fc43-2229fdefe2ad,2892224091,10846523442211997795 +a87cf232-c27a-a572-6f5f-40012f9d2775,205701746,12852850845235249021 +f044d7fc-9333-61c4-9ebf-fb97e8947296,912442632,6575517040074098866 +c67b63a5-d9f1-29e3-0f59-45ef7df58b2d,449418455,305218403594131858 +4a340427-7ccd-134d-1e4f-99cafd7fc4a0,4111400099,4135481452311556099 +ed7cfa12-f303-5f51-8d1a-31b0ebbe31af,3375532392,8328662955767437789 +37783e53-caab-c662-e451-4cf5ab43c87c,1347270154,17376614137864914011 +a9d2ec35-c24c-011a-54c3-6a36cd67b1f8,2335733303,8987295217881182979 +9e0dd55d-cab2-f1ac-b1d7-3ef3d9f9f53c,4239132844,15907579251064616060 +2a814488-fbc1-0ffa-ad9c-e521093758f2,2952444996,10195562924149403761 +72a83177-ea27-111e-1a80-2d3b43e2c1e3,3179962346,11975131312300950392 +e59cf91e-e92b-1bde-87a8-b91fcd1a8049,2263362750,17487081337330857305 +f6063dea-5c49-b84b-d0a8-177ef5a88797,454889999,9144508567958096078 +f8e808dc-4e74-2181-7fcb-a062d0309f80,4142231606,15639631104211130478 +03a501ea-1da1-14d0-78d1-aef4a18415a2,515598399,2904296157503854389 +9f591722-fdfa-23d9-23cc-49a08c6d09e1,3520736115,7981581412552992809 +ca0d2a25-7de0-0d0a-cd8a-62669e606e02,1498631520,16705147430095990998 +d57f054c-7068-130c-51ea-a688a92ac3c4,997538257,14590737835201519789 +61e4864c-9c7d-eb0d-aac3-3f5edac7ded6,4143097133,9761081369217306688 +a663159b-7bce-cf70-cc10-a1eb96884ca8,3817947289,11439188283789082808 +f229a59f-39d3-343d-d4bb-3f8368fedf8f,2488091732,228510843349878024 +c07e934d-8173-68ff-278b-4fbbe70a6008,135447767,5070929659873314070 +43cea722-485d-1168-4d16-93c6555b6d7d,228471210,6358604037896974272 +80130dfc-c251-d3a7-3c1d-1dbac368f8ce,3076350838,4494494218981634823 +cc1213ec-4cde-a99a-8582-4f7049b03a7a,1602967865,7142146055647960368 +0d68974c-f267-a627-1c3e-e6a1a10b73af,2136674640,8760765699158222420 +3dd980f4-e032-a226-3a89-d27ba761fe3b,2132262679,8383918288704614390 +3c9e218f-7c76-edd2-486c-95a9e494a3fb,4273593278,8788974500206929389 +bee46457-a711-83f2-d521-111a8bab2e9e,48172765,2469062138415159282 +675868e3-c8bd-d887-e3ef-c8a56d219f79,1475653011,8532389182930157671 +a24bfdfb-a9e7-047d-64aa-5c167db094b6,603976667,2993267924140313849 +54ff9856-eb9d-2351-ab31-c025490dc66b,580916907,13540568355489380923 +15ee7d79-7da2-9055-7b0b-a47b7f4156ac,3212946735,16244739924139572131 +defc9fc2-41bd-f212-5cb5-6c5fd1a89944,1566576861,1789645542339476209 +091f825c-cee1-c70f-3007-3defde42d994,215540817,12431155347084532732 +ada278c0-b64b-f694-3f4c-5c681a8e8239,2359769059,10131091827997599922 +f3306db0-f121-f8fd-9a07-8e5c774e78f3,2360455183,18043482688602442059 +c7d62330-7018-7941-1c35-18456d77e669,2495497983,2820762462429620371 +c8d775bd-a736-1a60-42f2-4f2701646fc1,781754906,5791619155897904440 +e878447e-4c96-f30d-21c8-8fb58e6455de,3856289479,3948619495388745392 +4c93952e-2577-8516-3339-2483138c1999,2560969597,12059130984227297450 +521855bc-4427-3da5-947a-f604db49cef0,3460718720,14524487059024256719 +db30f827-0230-721c-1493-0e01bf5162f2,2667361243,7499320966717913124 +d4fff62f-f373-6c34-4ed9-56c5d9ac0dbb,3537125289,15986568083913369143 +f8e0a34f-35ff-f408-3538-66d90a947712,2608292531,16197034821925193587 +1e0175fe-a094-14a2-33b2-54a03e9888a3,569115196,14676325738975803956 +c0127e83-b5ab-b0d3-ac4d-624581855f91,817019971,7118394934027455978 +395fdc14-34c2-f67f-18ba-97ddc0a817be,2129982839,690223695262389850 +e8558153-f444-fe32-1693-1db0cb2a3f17,3298373935,10287576968797942288 +b5a59c3a-3634-5f8a-4c3c-81fd61f9ad9e,802313099,16380535685073309019 +678f8912-6baa-7267-175b-88abd4daa38e,2762106214,5073359673765760529 +1d711716-194e-110a-3972-b4c0b4f06628,4112716861,16095470520370905415 +db4204bd-1ea3-34dc-df98-53ed17734ef1,1099202178,16420175374956172949 +ad2e57aa-208c-d733-f953-daf3cd7d1d7f,1713555960,1194058002938719948 +23d58ceb-af53-cfdd-87a7-9c15abd86ed0,2276358243,7464410620379861951 +b2ad5caf-a423-1d30-8b76-7a194367890b,609514926,16609390150993270994 +03421c37-80e2-a7ed-4ae5-e5775b08150c,1444929003,17731737388672232382 +2053d054-3710-6a88-889d-b0bba85ce4f5,4163770354,131739875816145996 +00f3a0dc-47a4-d35f-5bb5-0cfa4685fd20,98545566,10965307720029995546 +383bf024-b058-caf7-56e2-813e2916516f,2746969156,4204892842101914831 +07b37f33-220d-8baf-0475-0422f8579a0c,2531122936,2183165602093120977 +7ff2da62-3528-d6b4-bc5a-2b71a954a8ce,3843123382,6957795209198772094 +7e7a4857-e02a-6551-1e0b-23a2cd4a9e70,1001594053,16818028937635704397 +f03056bc-6103-6b7d-4761-f0a6dc90a68d,2789490403,12612461268872766402 +bd4d986c-135d-2cfc-3af9-01fc33c16a9b,3247316501,9865561689932329659 +2e96b5a7-4b13-56bf-ae79-a20b6f7fd656,613588,1798561643463495705 +435a407b-d2c8-3797-f02d-2a3836c53ebb,3986093161,17403110084149172328 +761a918a-8a6e-1da1-4a8d-9c47c492768b,2885819528,12892917964415936545 +953f6c7b-ba83-9fac-f0d9-b3de6fbced8d,2512676423,16624092879530241459 +0d29756b-54a7-c047-bcf1-1fb19d50e929,1630934561,8794851438061311482 +7faa597b-89d0-c227-9c63-54c0dd68a574,2875585681,3825465403430445139 +f01e7733-98f7-8811-a074-a23362144f0d,2538736944,12957946451367695331 +1d4d2919-d809-ea82-8a1f-deec216f54ba,1141174693,9315215340656900232 +129bdbb5-a0b8-cb18-e266-be7ee01a06a5,3365889285,16901873702812774936 +8cc57406-584a-d64a-5bb4-1a1943ed092e,2867307686,6180726834114095590 +ea333e9f-ed95-938d-4838-64007a6507cb,1068665187,15742752638960768680 +92112fab-6e0f-4dc3-fd40-ca8e2406f168,2358732163,3280246747474846677 +93ba3ed9-61ce-068c-30f0-c623d9dee931,753499908,7777625335836779989 +a61debe2-497e-fa57-5e0d-29f2890441a5,131369583,10836936511448689354 +c5651b39-46ca-2cb7-dc67-e59fcf84d47f,2336018754,5188482341061439720 +a1816555-40c3-8bfb-4941-fa4e261dbd03,1845715649,213327633266665933 +cb36b96a-e011-bec8-7135-f4589f4decf7,222271234,11542515232117846153 +77aa67d9-564a-4566-fcc1-4e1906ced110,3318203241,3318010066664698858 +ebd70556-42c7-c508-6c1b-0f81de73656f,688027576,16112587827415991682 +3f4b0a96-7a66-ce43-4434-deec31a75ccf,3967696422,10071116095819732340 +45d41741-bbde-07db-38f6-a65d91619ad1,753212771,3315246018403359600 +fa2032c6-68c2-6625-2c43-e7848474632b,2201854992,3224170313924227260 +8006d0bb-d0df-26ef-ae0e-f2a266e09ecb,1099879549,16789845010280878367 +bf3830b3-a9b8-f05a-71ec-2a9cd888e59b,3288665551,15727107053790475881 +4065c21c-d377-93cd-f354-b07bcf81f820,532184905,14667739792828925022 +0a9d102d-c049-283e-2e70-1d438c120379,2864618113,15319511259157340775 +228803ae-057b-f7b1-d52c-b1f5b6798d44,2262873521,4094681392424670075 +66b7da86-6d6e-68dc-686b-903552e27585,2570107355,9321833771339935649 +b876cb2a-b1a8-3fc8-fc39-6891372e2754,4286346106,8791721100354914557 +dd3462a7-e979-5a30-3107-156af667e559,3009266752,14675437198166744575 +1d5cacb4-b8d9-2f15-ab9d-e4a024408f0e,342677495,1734928595564663513 +55562c22-10cd-d84b-6ec8-3ca836bf2fe0,3880772524,2993093165910986902 +8a3c7b6b-aef6-e0a2-9dce-e9ac904bfb60,1048630498,4364060788831502525 +9e9d0814-861d-3ddf-8113-66628e726c5d,1211507713,12162935294198236537 +0b4c5f65-e7df-34b9-96ee-d9619098bb90,2081885286,4547500124525479762 +dcddab7e-1381-dca4-671f-1c78508bb0ee,4205345307,3965745151238998963 +ce94a42c-372b-2a48-eb7b-73c7ded87202,3984404263,10259278952699337752 +448a47df-58a0-4d58-ce7b-52576a522cf5,807519177,14941104968528581894 +f9691f94-5df8-bba7-9072-bb7f3c98a055,1457944742,16143594021932094747 +094009c1-4ddd-5cee-48d6-2c3e2d7ec3d8,728045277,2409380005181360082 +e8bc7978-8d84-1019-1394-4f3af797a34c,288575941,5049452500478778810 +d222881a-2501-bc02-78ca-bbba014953d8,3429712012,5331974907475273614 +36485aa0-5d41-ecb4-cc91-3dfbf7c629ad,2181776777,8618019329346174300 +ee6c042b-02a0-8113-1f08-2ff61edb3a51,176868940,12662799673956140042 +b7e5ffe0-3afd-acbc-41e4-32062b721f32,1792226537,14735269744922870922 +849d1b33-7002-e26b-1e12-8ef5eef32be0,4229651209,11021030440489827489 +aaa9d2b0-663f-68cb-076a-5bccdb65f083,1553348388,10789733927806709821 +f3c33e7e-723a-21ea-3873-0333016048f4,820140482,3318037629610002673 +24ba55a2-6349-c93d-5b9b-4160b6b09603,2481438219,13801009845221159521 +8668fc67-e08a-2edc-ea28-3d5f9c90070f,452513215,4282743535343137355 +99ee5599-3cda-85f0-0263-c057d5e5d240,2995555554,12368786267559934727 +f38d1721-d1ad-fe7b-7d4e-9644272da6fa,3026977471,5092140499775169760 +e76cfd33-a7f7-1b6b-cb27-c9a797a9b689,416165341,3033089453075309980 +8c30da45-5141-b140-d9f5-9e82f8e5f369,3226727714,15143563202381988546 +1dda3c6a-1694-8be1-25a1-323a3b560140,4095873471,1154323224770308499 +9a468036-ed09-d69c-a39f-39e6d6675b04,3034708846,12680235777421406047 +7805a2e4-9233-e17b-d71e-d34e6a873fdf,851585422,4261356969147596502 +de5dcf00-004e-faca-564c-3a33ea23e10b,1043441037,11757115836089987232 +c82aabfb-30d4-68d1-e99e-34c76de51bd9,1019039854,980566075527365826 +25e29b8d-78fc-f60d-9fa6-a9aaec0432fc,2111258921,9795315706144186914 +45c5d4d7-9581-85fc-3bec-ed83bc4259fb,3767171272,6749881005441319717 +70b6cf14-8e54-d7e8-2302-7f35eb087b2b,2586025908,7711090021896135329 +cdc0465a-65cd-4a65-bb00-149bc3c9a704,1402861905,8135025864685028282 +d1957e8f-4424-7dd1-39b7-2e77e58e1e9e,260108572,10557534456168936746 +63816e6f-d3cd-1b1f-f96d-00cfc65f5e20,3626065944,13837690530371626221 +03a1f43a-4ffb-4d94-6661-ab5d8bcc6852,2792056302,16276743065899357402 +773a34c6-687d-d08f-e6d5-6ce6ae9a1979,1045336908,7314310212491136751 +9885a472-f66b-16d9-2596-ae6f28584f8c,2010884444,18178202291003258083 +70b3610d-0d9f-6389-044f-3e5d494df271,3031475683,16035378778809747531 +aabfd27e-bb3c-1efa-d0e9-99ff164b0566,3714040159,9984231203495354887 +711c3fa6-27ca-b495-2c22-590bfd251a35,2412058989,4779659630619118740 +922d0112-3766-6414-202d-b6d2081968fb,2448264209,12890693625584074483 +c759ff2f-1d5c-9cb3-2015-8ef84692e7a3,661716982,9943805201422599111 +03fa7c57-9c3d-ab54-dca6-0f5d9a016e4a,4267302669,1078560440289546793 +e8503eb3-782d-e623-cd36-7a7fac808b7c,917268028,5238680438673461045 +e7e9f5ae-50d8-c748-564b-0e65c149b7dd,2628582182,15706272719787206711 +b2818889-dd79-0760-f2b2-85def3e1702b,3004533545,2367281911010400774 +983a67ce-d526-53a1-0dc4-c0dadcbb8272,1782676756,18383515091484493080 +6b2145a6-8114-e2ae-74bf-6c13f72607e2,1008856982,3329091399671583173 +90a14c87-ad72-d77b-fccd-3f897c874257,2085887299,3594476761194135063 +62fe23d7-3b17-cd3f-c762-62c4e1860b27,762999585,5768352431368583302 +1814db2d-7eea-e3f8-a971-99d954fa7541,890776099,6017211997568956091 +22c4b618-3ab5-eec1-1348-7313ce0851a2,2435731871,14674139197762786113 +042588fc-dfdd-5d43-de6b-95e09b2f0436,2714063358,16141064327985423000 +52cd23fd-b6e0-5524-93a6-3eaf9ce965df,2487060911,5305381709945790978 +0ac8b2b2-f594-6891-8595-054b219912f9,637202225,15696244242052156250 +a2d2d30f-9fbc-d089-9f3a-f2229802921f,1566364618,17287379793339928446 +9ac13dcf-f852-3b02-eb40-e1148bb4efdf,2322171194,3805623828674578672 +ef7b1b7b-c396-7bbc-0133-76a48a45e4c4,841912311,14360001796422584809 +9f18c2d2-f4cc-da51-5daf-f72e7ea5ae03,841553351,789123014046048530 +08e06676-3e67-d49c-0ac4-7dbc3936cff5,1913317762,9832215437235088865 +5dc1c74b-593e-6dfa-8fa5-a21d35154ecc,2994592196,2499909343900315188 +e3680669-c5fb-56a5-2ae3-1ebcc45cc358,3361128125,7715541641336461070 +ee4ed56d-216d-4eb0-8794-585ef9e15568,3740082738,13809264829114476605 +af8e2096-b4e9-a6cc-d471-8f0bac4812dc,384843329,14038122749893578519 +8593510c-96e8-6796-7194-4a5dbcb6b030,1218805724,7883133136995833111 +ba6e8dc0-c69d-a2e6-cfde-2e0490bc5392,1141970683,6321608356847970119 +5f2a3084-cb5d-6fea-235f-57e1e105441a,213216029,7316928160795470026 +db3e8deb-dd3b-8efb-e56b-9815739ca934,4039139495,90757144372350485 +40d812d9-6e1d-5863-7df9-a8f1067d0348,3111632144,14884062502669617045 +070d430e-d96f-dfdc-6ef9-0a8d8e4fbeb6,2157611681,8691764278755742849 +dba6f7e9-c102-d303-71db-9f9b64d89b34,416811657,14040091154559127960 +5407e699-da9d-52a1-cc8f-37275df6ffd5,4222170111,686118057164921794 +defcde1f-fee5-704d-a40d-cd9eb081a146,2984625907,7838321804833825460 +6f3c2c0c-f48f-8bc7-1437-e77b7b04dc53,23809530,17551091399266448101 +1e18d07b-dfd9-4966-aedd-cc8fc2419759,1976420149,12069329388860288423 +79407197-79f7-c5b7-4b45-fa9506f09596,3150759002,17742000901382301285 +569ca5f9-dfa3-268b-afb5-09942981160c,3908014279,3373079669788198571 +d4edcfa7-36ec-09f8-3a7c-3de050bde65f,3045491889,7611348813759364244 +a7738f22-8af5-0cdf-8725-7972c85b0238,1148120182,2336428814610249883 +b0e9cc70-4800-e928-483e-06e9b257ae7c,3089844732,17786630550839174397 +30b11894-0595-004a-1e4a-29bdfc36c9d4,343266782,8051645919000794927 +754788ad-cc20-37bb-5ce1-f65e852aa8d7,3189915970,7704122489154723454 +0450d117-678e-7e25-6de3-caec9eba3e61,3491468928,10197605431506661133 +10e3d826-17d4-4745-34e9-18edc41e8da8,315600282,10208704600211465275 +783f0548-f46f-85d4-f425-3ebe48bf5365,2132548500,7049120426092892668 +ce40fc3e-5450-cd60-a839-e333a9553dc1,3838629699,11522467431201585597 +5521234d-1911-a1ee-4498-b645e49b0799,1620920302,5505184599062392567 +c49b9b61-421a-ac46-8abf-9d71fbb089a2,45277219,17454399096648631007 +da54e822-24c1-4d89-329e-b4ef604bee78,2817878523,2437175061660427459 +14e72bba-5705-cac5-3f41-354b2a93d95f,1706620634,15572875940314285147 +4297a903-7df4-ef55-e2af-93e3c8842278,954843431,2600193723907654118 +9783dc81-47bf-018c-d37b-2855afae1e00,1244429586,5319312970865838771 +a2143bec-1856-36fd-0c0f-0015ac5daf1e,3775160829,4938657213239413271 +31e604df-018f-ad7c-b957-189db65ea1e5,2821621368,14514337959707125104 +8dbf8516-a6c5-be76-e374-07f87141afc7,227662548,13593503409339356015 +66945d71-4f75-6d14-c8ff-f2647b831dc2,1217939559,9861775926425696475 +725abc33-dec5-44eb-3dec-71fe354554e0,2650002387,14420603299584411088 +bff71092-d654-b5f9-06bb-256c3a238563,669278920,173459528424420329 +7e6937b3-055c-d712-44ea-2970ab6e9d4f,2924064935,13485005373108185378 +2fcbbc00-d9c4-c709-163b-65b7505ea10e,2753958875,11073156453178972436 +27a4d590-6000-ffcf-acdf-a9ae985b3a61,2061095690,9874957862984609071 +475afa35-871c-fc0e-f3e7-7996c362c61d,1352703454,14879734127245222409 +37365832-8486-0ff3-b4c7-a9a6745589d0,508607992,18371501460937493623 +edf29ba8-edd5-5d3c-092a-2aebdc67248a,2047365487,15737239072463380643 +b6065cf9-ed8a-32ca-df5e-33498c8768e4,3568798358,784816012098036588 +6adbeb34-31f3-2a31-b2ad-6cae221d0a5b,1952599952,10328300353979891330 +2c56404d-0357-cf6e-82c4-35488a8fefd2,370971445,337130790757487644 +3cab0788-13d5-909a-7b61-f6a86d5edce5,1912922829,10982820623229068835 +28502ee6-a15c-3f36-1078-827af25d207d,1832364037,11948530997664375528 +2488df3a-4ad2-dc38-80be-45479df8c6ba,195438167,3358913585226403208 +d64e3ed0-066f-8879-6075-9a1b10027aca,3406402445,856455943637636206 +fcb2425b-72c7-069f-a395-a997342b1d48,4194237068,12290421894110547785 +ca935074-8e44-fdc1-4cc1-8688d36031ac,914531909,5127440653348204775 +d63f5ab9-1fa6-5023-ee14-55a28ac1c4cd,1055266678,11513258472152733155 +63e268c1-3328-fef4-af79-e0f060c2c822,195699273,15079464801458723721 +b6f01af1-76e6-d641-301a-bb03f58c6a54,764760177,2277699038114705486 +231cb8d2-1c29-9e95-937a-2d192aadcb6d,821661698,43464723893545867 +92f5b5d5-5dbb-d3ad-51c8-2a8cb02cf13f,700093622,9706838906224728882 +edd7f085-1c2e-7cff-e5a6-0570d126e86c,3879203803,15046363600876662186 +474b8045-1335-a8a7-fc14-f4dc9e23d7aa,468712998,6170946745177826498 +c5137184-c04e-ef6a-34a1-b5cf5457e02f,3634806342,15854167012376569786 +01932ae3-48c8-3ba9-28b2-4c305005d1ca,2551263442,13639208471615966777 +d11332f7-f851-7557-9ad8-d37edb6742f7,4130461247,13247004547308896750 +45abc8b5-c25b-29c5-923e-f43f74e961f7,337781932,326924961933501364 +89e8fe1c-ba0b-a552-ab12-52f3c3a16ae7,3969707407,8205226541259343497 +8e8ec8d2-c46b-a0e4-bda8-9d95025e8801,561670620,11489290532045853128 +4ffd1a10-33c0-e7f2-49ce-f8a5275e0a98,3371441277,17224192110987832169 +bdbdcb1a-6fa3-638a-d29e-db9b3261ec97,1527000959,16920411262754194659 +d310aa17-6837-b208-259f-33271a2f8710,502170808,14750842353971867061 +923e7c3a-a976-9667-8ff3-154c7e0fbe16,1296740572,906684946704109938 +757a5fcf-94d5-0891-5fd6-3a6be9643738,2840270634,10653867248399340403 +e78d40ef-61e0-f71e-8d19-2d3fa571129d,852244281,791764189950590755 +e8991cca-d2cd-b454-63dc-ff648497db0a,3045985990,819648634652674135 +5a36ada9-ebde-e979-91ba-3c93e0c510b0,3002405702,399111645638398845 +2bf4e83a-3f60-cbe2-a25e-6c53cd50cf26,1822856210,943502903379081863 +4fc1de6d-1906-f1bd-74a5-9b64628a022b,1041784848,12828989952400229118 +b39e3655-9058-a0ae-333e-9291b7462ead,2325420123,2716606385905854843 +6c95c3eb-6c20-a437-14e4-6f7fdcb6689e,2508071697,11545350694682895896 +bfa3d3e0-6e8c-cec0-6967-76774b4af9d7,1622519162,15970665825418138527 +99e30bf8-f8f1-e898-3fde-bd1570419ddf,2616056669,9904639160479051132 +ebbe65ed-3f3e-0ff8-a938-ac112073c6d3,2098427835,15049525820412063107 +3e294a7d-c26f-d430-5131-f684a0786178,1071705325,12621025071139630085 +8da5e358-fa55-05dc-6c0f-d2ac5f724dd1,221398034,7330124900344784969 +95ee8818-41bf-d43f-2288-4bc22acd0ef9,2908453475,4020227126865405720 +355f6c59-9216-89a3-e530-6974e8c578fe,1600036863,12016663885168200173 +216c86ae-8779-ed06-b9ca-d1f8485b69e3,486924456,10309634595010114824 +b3a0ade8-3a87-368f-32d9-c44b401cd324,3162260795,14981933936489458367 +45574bb3-350c-3099-80c3-caf48f3fb499,4293316807,1293367749224266936 +247a0165-65cc-55d3-9dbb-269d207d5f4f,1565649939,16852981127950179261 +e021b389-21df-4519-55c0-bda9a7d24307,474819689,18131407955148896608 +a9f5fad9-ade2-e2af-5fb2-338f9a74f4e6,2320304956,14157579431048776534 +694d9184-5f81-9d30-259d-32117b2360e6,1813415509,3214880215707081913 +dba65b39-0396-464d-d64b-90de17600b62,796993521,17764723980005467963 +68664327-c8eb-cb08-fea0-8c6dc1edd464,2642212214,17522296260995309039 +5c6171df-e04a-8a3d-58a8-ce91e4f34904,2625435071,3740305809071267656 +45f01c42-fcb7-85d0-666a-678c63eabaa7,2791753447,10145077460839801241 +a2a314eb-28fa-fd79-9014-3cbc0aa7dc28,2275847310,3362483332302078970 +0bc00dbd-2b4f-e490-9557-1d5944e70a63,3342725817,7802458953259271994 +e7384733-abea-f6b0-e251-dd1e1ee75460,2358293082,11212477401957095906 +044688a3-fb18-e2c7-e4ff-91603a4bd54a,630084577,11167065905216462227 +cfa90113-7825-1ada-fe03-7e34caa9ba5a,3829661689,17254924787688109602 +3b955cd0-5167-c6cc-e813-ade32f9118cf,1880769157,14385682068817003528 +037e9e7f-84f3-51a1-86ce-76919a547501,276017197,16308863988419223553 +aafa7274-cfeb-a15a-e047-221e2b80966e,676637944,12349564895422523560 +57b7a0b9-716d-5b58-bfcf-d2d4ccc390fc,4252996604,14402944071636927833 +1099c0d9-285d-fffb-5678-6a3947dd19e4,3855441292,12288261565795243169 +cd0db79f-b129-6c43-b7e5-8c25d2d2d27e,1441172656,9108555698051986982 +dccc8906-936e-9e49-77d3-9f94e8ee6edc,2613720279,8800913220739539558 +d1d3098b-f040-8516-1383-7eecbfcb0467,2938756925,5813827631140334560 +bf6a8488-15bc-2c96-ff3d-d9b073899ee7,2331414257,496503549038977467 +c9086040-2f08-3d20-5f83-443bf08a55cf,2035685160,18382574042285519295 +bdf72320-6d89-7402-1bac-75f02782379d,2843632897,10761981210061283427 +bd19f3d0-f9ea-96d2-56a0-26216804d065,1407058059,16251625823617586357 +ae71e7f6-4a5f-d54e-19a6-19bd8772f955,40915819,451341636909178713 +54de10f5-8448-7da8-5533-3460e9f7b00b,2143710281,5702355891425914861 +0035fdbc-d3fc-5d5e-446c-e3dad4cc4ce5,1220051360,13804333695123061902 +ab4193e3-5f5b-4957-6897-d0c33e452925,3936128513,6517702191420753118 +52b414c0-fbd4-20d0-6263-ee7e7949e3cc,2809673643,14800820857775958387 +34f44b0e-5def-e9fb-2c5a-a2d4ffa393b5,1101056022,18273020515013935393 +e4c73dce-b0a1-9cb0-3d97-0873e367f390,1095847935,13002113560248953151 +f4234d85-5f6a-d328-6784-2adbdf9cb2f1,3264365421,5773961608533551735 +23f917a1-1e3a-e930-d00c-024b254406aa,1025772138,5116783962653214437 +b2034907-ef32-c689-1033-9178d6310430,83695345,11377081629099652112 +a2a902d2-c364-a653-c508-0dffebcdf46a,3519487452,14141919507118445893 +8b378c3a-e9c3-eee5-1da0-0acd8f72e8c8,3611780993,11308796609526348527 +c429109b-d6a6-b228-0046-310da3e7c50f,1585917845,7746016428111669682 +9d1facf5-bec9-cd9b-b4e2-9b7b26e80434,1704495471,3592338688739945309 +3a7d7f8c-0adf-920d-3432-f2b4c6286d94,1679391319,6326368551693626371 +cfecd2dc-596d-5292-28a9-2ec7e91a8e4d,2934827592,13467410375411080926 +6fc2b242-bc38-70d3-60bf-2b99ca88e79b,1548256453,594327928639346130 +71a23358-8585-be54-e91c-9840847eb8c6,1823659460,14252358449565958199 +d538f7bc-df16-d70d-6472-07ed42bc94c0,1545910535,12257547679556736391 +5dc8a26e-4397-e3ba-6b49-ccfbe7c9318b,3750018629,8971324210355921970 +ed648c5a-35cc-3ff0-3df7-eb5f285824fe,3349407130,6926625911671086546 +a97a1f11-6ddd-e82b-c431-d92266a7d327,4100013503,11825925003702684723 +b9e118ef-cb04-5199-8e2d-56d0acd40fbf,1342586358,7651636107771876433 +426cba2f-0fdc-77a1-fdcd-5aee33439262,2704139290,13188417851479556759 +36f246c9-7c97-146b-7e2e-870bc784c0a2,390592854,9376766600943023268 +f570bcf7-9a14-3744-9f39-ea59c7f44694,248461920,1683346409384519678 +89ffc882-e74e-21fb-44b2-76641ac1d628,3391845351,7517783214737213924 +6a7ee09c-3866-e661-b2a2-7771c1e85b45,1401451307,3964513626167640275 +0d4ef38b-0c3d-90e7-27dc-1f4f27e0b741,4148269045,598481458183195995 +79b29e62-fa54-0fb8-814d-c78786ad2a66,3806939188,561323279935067325 +1f7df6bf-755c-84c5-640f-02384ab118e6,1716185928,1264654701288452931 +53b2f73d-9e53-591d-f57c-e892ba013664,2939884173,1866578006728580473 +28aea129-7400-6126-9957-3da3ffe0738e,3562305406,15753933380451691640 +70862a61-2f24-8f91-e0fb-70f9bc34372c,803624184,11347886426516022369 +87faaf53-d52a-4639-f9fe-e48dbcaa4fda,1248067113,14872975192219562291 +cbdda8bb-07e6-c447-0d84-d3c9fb2e8607,3185417246,9502410886751841780 +8875b0af-4a3b-1e47-aef4-e1c9fe9d6fcc,1361616602,16842627238767762240 +e03dce8c-6a71-f208-5c3f-f0d95ff6d19f,2484882654,16609333021339737339 +cdf93998-5afe-df07-cef0-1be30413bd9b,1331106545,16293889202444786828 +5f375c97-5d64-f647-eb93-1d90f5f2cfca,1769025552,14707184327445549107 +563fd61f-16b6-7925-e55b-7970270c37c5,1109307850,8585884241566350813 +ca0a1183-bbe1-5058-416d-ff32cb5d52cf,2254165532,7177818244974052718 +8e02e370-ace3-f39f-b46b-5e47e4f38dd0,629414553,16408442659828209363 +2102c0bc-4d84-fa66-a71b-108d18f0706f,3305007827,17133160644978918161 +57fc68f9-5a58-87fa-f2ec-df3e0b515ee8,3615855420,5386002768128572124 +310bc72c-d116-ecac-d246-93b47b29e9d1,461157672,17367568240033667324 +94eb4140-d768-5ddc-fc40-feaa0a39bc52,870240274,14999463633949990743 +b616ccd0-3e84-f67b-1faf-74fa0e0fcd2d,1666104435,1089092096233473687 +ed202f42-0ef6-851e-a846-11044e38ccda,3067177119,16438559739696601044 +5722d82b-16f2-10ed-580e-42e69c42cb91,864068307,613677785941292586 +7611afad-8384-b26d-c70d-3a16bf381639,1581709317,864058588014691831 +cf861cd1-e772-9e86-f5ed-ba9d72c43d08,2901772702,123330897565825554 +0a3a244d-4314-9f43-486d-7563b8a38704,891958590,16157277284313403334 +d34072c2-d437-1c0b-13e3-d95f4235189e,2572156971,9566879961592993683 +6ec87dcd-fd7f-5d1a-25f1-e9e7aafb0490,604050023,9853870654331380790 +eaf9a63f-21ba-5781-77a7-b8c7a70b578e,1123262249,17671878115521221405 +573f8bec-e818-6358-3777-be9f5480dec5,2598451712,6174692210697508370 +8fe5146f-992e-551d-2528-e50f1ba51f08,14087597,3368516284542481035 +01d52280-9245-0511-4cf2-b9e32dd74025,9726157,13372498442735582538 +6f439920-caa9-b497-a636-6c06a4a8be62,325191909,12462107379417611967 +2d463267-b250-45e3-948f-6b4cfcc7774a,3437307313,18307116236535916645 +ed68c303-f3fc-cb8b-9daf-4d45efa68057,3945614823,9178966394684886840 +1ab9da0b-9a26-db1b-13f2-1a9c7201609f,1055155613,6857125535637214246 +16d87d56-f6e6-6e5c-01b4-247fecf5faf8,1792403135,17640488111120383772 +6f80c5fb-c0a7-39db-9e96-f885d2284cb9,4102380140,3377047220387582147 +020d3a1c-78d6-9c92-acd9-cf23b5da0dfd,2350963553,5819805064859905751 +cf16797d-4db9-70e9-a05f-700b85b98009,3082936179,8518788274086809937 +d6dd2898-d654-4efe-5788-e7f772d2e629,797773535,1027386444238913844 +f48e417c-6502-2630-c484-df27136231cc,3756174429,14466234854843316899 +a656dfdf-4611-9323-8ad3-926745e82c66,3169459183,16834658754041252547 +0b809d8e-9427-e286-f4e0-4451c961eb45,3394936043,4712608164475321308 +d7c29916-2dbc-1a30-ca77-90a77d73cacd,3680168601,9814545783867813922 +8646271e-32f6-f909-b8bb-8984512b1a27,653032360,12931410289279273052 +23d3edd6-caee-26d1-8db6-0b87358348a0,563106469,9218257725176487473 +aa8e4686-03dc-7f4c-934a-b1a704b1a1e9,3836749868,3536075258048401516 +6bd3763e-f2e3-b4b8-af5d-00680de7e7ba,3481323546,9991015740533001385 +29a81c43-4c9c-df09-2422-8ced0edba902,3988180706,12782696349926427167 +a1b95373-ebbd-ab7d-5427-1d0722507843,3645418482,10170981467795299040 +68d7d9e9-8de6-2033-428c-f8afa1b32e44,3618213983,8168253040883481723 +2c04820d-8156-de24-4d3d-7881f7756f83,3633895708,5439200362639061194 +e0586b02-082e-6e10-24e0-3a15ba872775,3092411365,13360939534326697308 +2201b217-342e-b6f7-ff26-b34459c365d9,3765412774,1454932167329246297 +e7f49841-df5e-9d84-282d-fd1e0e3999a9,686199518,14252147631846938751 +0d231b73-a56c-940f-526d-e2aaaca4a998,2665980255,1171612999721891992 +1c458aba-0687-bcc5-8fdf-69804fff85a3,1272928595,11490989824527209411 +9c5cbec5-93e3-48b3-673c-3b81c350eeb8,2117448856,18238900298206545950 +209d7e93-07c8-04c2-a218-5be7ca7f2c92,1013179115,4938021781979063615 +9f9fa633-879b-95eb-2756-2808ca439010,3232235587,18222122797736835986 +fee457e3-c3b0-9e96-0526-993bf7bc6cf5,701715472,1060720900334940284 +dd93fa6d-a6a5-54bc-35b6-8821f1d7a803,4263518777,17825557455655465023 +beb04b3c-de40-2086-197b-4256fbe353b8,707554886,198555019440227527 +858cd4b7-7b26-dd1f-8f8c-c0a193416e98,2758535129,1247096307193065320 +f7ad56c2-2877-60d0-d4a7-5fd555b68086,624232462,9583157847003765727 +a0053a0e-f117-b659-66a0-26812a99d46c,2374069271,4954058012138703329 +cfcc1da5-116a-ceef-7a49-d0f27e0ffea6,3747809381,8521890634641934508 +a5d5fd3c-b1c2-8371-9dc2-9bcfc38377af,17125012,14233821284939727252 +3d1158f1-bcc0-5f96-4c37-b3c8b8f1a088,4149774068,6003694229337107040 +39f8bbb4-347e-b575-a2f7-f86e9b9e8b90,630122351,17579686243098424176 +91a02123-2865-abf4-bf4b-26ac08044643,3113299600,1767342297587013998 +277f424b-83e0-b0d9-6a71-d4eace0f3631,1263862987,15890356283169733507 +4832d9c5-cb56-4177-2d25-cfc9dc417d95,1000844874,12189879811138830137 +d5130acc-56c2-bb12-291c-aa578af1abb9,2832438748,5573986065576169027 +3fd00fa0-0562-8ac2-2b35-f1678ff0d1e9,1476145546,8684770218678014199 +9cfcb48a-ec74-1824-643e-f8ff31fe99b7,1608257058,5754882453387438169 +f1395dfb-a6de-59d0-5737-1fc9a54fcf74,1374103382,10910127711369504580 +6ddd61d9-1a79-2c8b-ab90-104f351e05f4,3808050733,16622353592199268116 +c05077fb-d596-9da7-3353-238f5430095e,397314723,10465713743122207916 +a2edc92a-1f89-7e5f-72b4-ea39966143f9,3523007300,15143226963864327950 +95fcff93-4a30-f29d-3972-89240fce575c,109820837,11562079764909819655 +4e4a7672-6f66-004e-fc61-040f375f5493,3409067061,14638058431157366057 +eabcd7e5-90e1-f89f-a29d-0baf074cad3a,1153780846,17138829911121777435 +c02ed5d5-21e5-5554-3336-83f335309e29,4049413243,8082638475869104257 +ccc1de15-5212-111a-c31a-52ebd62251a9,1543909833,8359778692392138243 +a46eca7a-17bf-bc33-044a-f306cc4ea318,3740233204,10828332100578732847 +7811dde0-2e82-61f3-07df-a72edfe4f158,2344604217,12308257302548756038 +87b01089-790e-bcb8-e4a1-8a9e29267814,295243821,16362310941427377212 +a932fe92-e645-7451-c65c-18944d542457,2832653201,4351512897717818367 +14780895-e722-0b4f-0d4e-80bed75051f2,2897653161,3753598121468468120 +18e868a1-3b7c-470b-8d8b-ea9dd6e23d8e,4093657445,351232862172522441 +ba34b0b1-03a5-eb14-5bf1-49a2c3e4e38a,3453203600,12203575999969009507 +0f660b75-f052-0530-6b58-c0201e9d0e02,2712736061,7659359272820489211 +7a07f6b3-db43-80bb-98f4-4a0b11adbd47,1739249312,9950090211789639811 +d675c517-9017-1b0e-dc22-b02ba8c051b4,186733492,12576859861004506431 +186a2fe2-71fc-669c-cd3b-72ffee9dcfeb,3641618842,798075123699467696 +9e38225e-549e-4eaa-bd3e-66bfdac4a9f6,4288172529,17012707348061797455 +0e6628a7-1e94-4c87-7c38-b217547828c7,3424815663,8154717034083722825 +aa23a159-d574-7775-8af6-c41b9427beed,3888101094,17774180385151980209 +d2d8dbdf-441f-2d14-2602-58acbf683546,380689290,7453840831175116086 +4786c752-7784-dbbe-0ad8-c61ce385dedb,2053184810,15655476117094053770 +379cbf57-60d7-b48a-b91c-6a4abc11eec9,314024227,5554889977472844835 +abdfebea-1e7d-826b-3a53-2a5505807871,1694841605,1155918843361607974 +26f944d2-3d1b-b114-fa37-4ae4cb655dab,2336522820,5948564595848025179 +e14d18d1-e22b-89e2-bb86-db6e4055b1ff,2939555482,14117200290002091906 +b9a9a99d-e59b-e010-fcfb-b7b8b7eb7c10,1630331004,10569406327290565307 +5d75d76d-317b-b888-bedb-c924935045e4,1091311608,10558693941485453401 +3bdc9747-03b0-671c-dc68-35cc5d82001f,4235597030,17379835432380765212 +60fc2791-4ace-24bf-033d-8a4ccaf6fa46,2409374316,12669610668173404213 +00a8a770-a69e-a52a-6971-b7b20b575313,3548990257,11805948237611447488 +d6d8d59a-c9c4-6362-64a5-5d193788aabf,40432344,16927265615571027928 +5033ad12-794d-7f13-487f-776ee08b17a6,3522379663,14550781800826858256 +56dcf2e5-2c3b-1869-eb94-1f124648c227,1594259938,1121029841563018795 +e39122e3-e4c7-5af8-56d6-820e23be7c83,3670939886,8668706983447016121 +15ee311a-ebb6-bc59-6ec1-70191b3baf48,1226028067,7446153898364923438 +419dab1c-6935-6bd8-4078-cb49e70755bb,1726354107,15541913319689061233 +502c8e61-e434-a4f0-fd22-ddca4e9f82e9,2002797941,15472006221601434729 +11726c4c-ec85-7107-8e23-260eccfe7577,21261040,7943274320602474183 +fe03c0c3-e3af-9460-93f4-dbd35ae682fb,3239833533,4290448515264378887 +061a471e-a59d-c541-509a-19de995b8abc,2573736747,3301426548517563251 +b981cf8b-9c64-4ff2-b15c-ccca168559d3,2063169925,2159057318972886279 +4dea43e6-2280-f34b-2b6b-4da938ff3e2f,3534186121,18301222661491960701 +c3a3fd54-a654-19aa-1fb0-fd3784ac425d,262525405,2008725642500492801 +d6bc7d57-60db-d01a-b3b7-4aceb682392f,2012017125,10722020641752707101 +54df6204-ba50-cf5d-1aec-c0e640485753,634291445,10382521844854772667 +62b607b1-efb3-6ca2-44c3-2961000dacd5,2937421275,1148607792855134785 +8feaa0c9-b4d7-f4b9-1f49-958d8f521bac,3208677672,14320682459364477458 +cf969be1-05d9-de19-869c-792fe2b0bd5a,665299984,3086694623466527096 +aa952a6f-74c6-c54c-d396-aba086ac2b22,1164557180,9960557724015035133 +7f831a80-47c0-12c3-4baa-60b2745538b0,3095164773,9363732045929728242 +a32b4ead-8d27-694d-1cba-f9b46cb5b2c2,161521995,2489625591590431024 +06a25329-d5ba-b8cb-db1c-9da2c22740b2,1700557364,11337394957944718884 +c635c8f8-2483-e1ad-3117-aa6cd46277b6,2838273731,14751988848697183115 +ab5768a1-dcc8-49a2-8efa-52e5f1d8923f,3356543192,16473851939849847907 +6bde48ec-3506-0bbe-f8ab-d3379c56d311,1600719140,12400277439887592723 +8a77da2d-eca4-12d4-4169-9a798ac3888f,3190749624,8714120852658543303 +15d09b5d-da27-6b95-c31b-017043ab19d7,3905235835,5809832847318355284 +2c35570a-eb4e-6116-f510-17167259e24c,2461075488,12768019314553884773 +a160140e-b852-d3f5-a48d-c65b7eb8810b,3451434929,17761052152763764757 +4ab7faaa-82b8-28a2-fe91-51e3852d0fa3,2055317788,833384006111537848 +20c5e946-5ad4-3922-ddac-741d0183ace1,730107015,4321321086189331064 +903031ef-0f61-0c76-9be6-c0775e3d7613,458283120,8975924781448826147 +a3afefaf-79d1-6cad-99c4-337812b57b19,4179824727,2255559361551277955 +0cdd46e3-0406-a90a-1cdd-771c67e59cfc,2961996713,3466327234234407503 +d6cd1e72-735d-df0f-922b-5dec5127a807,1036532579,7174894445249157586 +8445553f-e207-891f-b19c-19e4c14702eb,4113109298,6595899421473519526 +f7c31216-c07a-051e-d4a8-ee49ae0e7242,2966539413,371090941194221892 +d8303f98-5099-6843-0b49-3f153fed3f56,1544907652,1795063565355168095 +3acaaf0f-159f-3f56-cd2f-b44033957a54,2244121509,4035398851213591890 +e082270f-a977-70ff-3d90-7565c0d40b3c,666200909,5088809693056746958 +c0afbebc-95e3-f1b5-7273-10b8041fdf8a,1896809856,14834596597695085118 +ecf3b8e4-f687-5af1-3163-db66694785a2,3635440687,1400529200328971214 +a95c1d1c-0f13-4f54-9626-10792b99f0d9,916304406,1361023166415140280 +f8bf8cf6-be75-9337-a3f0-37c862826c6d,863284459,10977178643263811665 +349b475a-1eac-4d0a-d580-167fccc4df4d,1623251692,5866113429045879752 +f2bffc84-d9fb-5512-34c2-c02d79364235,2584372910,5746752605331815766 +4cfd334a-6ed9-d39b-7330-2554e7d536a0,3341009713,13680689713706472415 +10b51f8b-fe85-262d-cdbc-79f469e3b0da,1077311185,9323248747048372167 +22adb79f-cd81-8d00-0c8d-b9c214d6a176,1311920043,13797532336434358688 +3f8b2769-772e-6518-3997-6fe76db7ddff,2730946017,10426987209261876751 +7a80c320-120f-cdbc-88e0-158f7b09fab0,394114972,4689400081027701477 +054fa2b0-f770-9597-4aa7-56d3f3292910,1108227236,7236382167774631761 +36e2bd38-5273-3e97-687c-dc1ea047c7ee,3342525584,5164600158174196315 +b5800a43-cfb9-aadd-8c24-a84403245225,830409715,10164138226418953156 +f11fd788-d733-c6b1-9616-d43a155079e6,371961314,11504432632631008819 +223742bf-6716-7bf7-b031-c53e18c89b0a,2709057021,5072091380248116203 +dbee9194-225c-c4ef-52db-b4251bae3727,1481662001,9715911446942000423 +008d33ad-7d12-a804-c356-2906b46c3dc2,429275227,11077965686771427563 +b2c88522-6257-2804-f59e-52e769654bb2,2147515094,6353627817433512695 +36868c1e-e479-8f93-60bb-e5ba553e4620,1875995175,7919428340158379837 +563758ff-a403-20c6-9c99-0e40806b6b10,1455521030,16955743178020547778 +19291523-ca0a-7376-7ead-2fa2fa824d19,1651205336,12265666119589862465 +71e9377e-11bd-3517-741c-f7873a85df30,1096641499,14006680591240016649 +f49f4dd7-963e-90e3-d71f-b6c804b4618e,1615805140,12512606476873475791 +5d5047b3-12a8-9910-2cae-6ed12670ae55,4030777832,5626463051578120295 +cd207c51-b6d7-18f4-194b-9812fcf1b94d,2175717864,10929757941580196198 +c174219d-0d41-2bdb-44ba-18a2bae7ae89,2918732924,15181244008038141786 +a5a5dae9-fa27-6c97-09c1-17af05891dc6,3634650469,12473412182931591726 +de29d963-a042-e4b1-8e45-3d41e8d89b47,3493565901,17984477817741878165 +946414b0-6220-f001-0e5c-2af3410eb01c,3351971436,17256573115025035972 +4b9eb431-6746-376b-a075-d3112c58c497,345962011,9057636870904001515 +fe0b6cba-24bb-5502-4675-2530c61af63c,3455538967,15072519823660873712 +5eafa20d-4f16-d3ec-35fe-d0a0c19bd407,3706587808,17412483279465163986 +5e31237a-7fb8-74a6-9642-e841fc64b505,2904836390,1090910248572042385 +a82a9e9c-75cb-623b-8075-d2e5e9e8154a,2089786601,15093551167145111305 +5c5f2eb1-709c-7d04-0647-174bbd2fbb4f,783424519,3578841751306193166 +722a6508-049a-f221-31bb-d207678fe566,2597893961,6981217792590753897 +d6e5ac43-5e63-b6ff-84cc-ad4a9c6d9075,2717334675,4714526559146123674 +1dd0ffcc-969d-d178-119b-ffd6203d5e0a,3762049652,2860090360373591014 +bf19304a-ed99-8860-3a7f-d828ead16050,1673065419,10733782988389049399 +09c5b740-9667-e100-64e5-0bebe32ce77a,2529026314,15847039323996832638 +250cb746-35c7-cbb0-20f7-01d4cfd10547,4290604299,14596356213843522833 +e9d74ebd-31d9-4209-4886-0fade749b43d,233516342,10418993460076328074 +fe379c76-bffc-b927-7031-80b1bcbdebcd,99127590,9089623643021911858 +b56252f9-df69-2edb-465a-a5548e341178,586809139,14729741310371523586 +8f846066-bde7-2f3f-40c6-d1f45345e7f3,1985121312,15049984159943653807 +0da63bb6-692f-3b7f-d8e6-032b453a3183,4194354197,2521716492847683830 +efced049-6d65-2f09-2b70-0b9d64a4e9cf,337931557,1705832158542891783 +b30f6374-a100-0aa7-7b93-aa93595a26d5,3278713151,1063350213738123094 +38325a06-70ad-388f-12df-b2e8c51c6671,1942982306,18431311567001550776 +1ac679aa-3ea2-9f5e-a217-8ed68614780c,4167157595,10383332985107680152 +4a3be992-4bc0-076c-1ed9-7d5a8b5ca551,4045088297,7130157703036524425 +f0a24fe7-106d-834e-b6a7-42c7898df0ba,1821301478,15533571899151069640 +22f25efe-8608-26d4-61a4-845a4416b0fd,3470338782,9675752657459323592 +e5b6a153-9776-8140-0953-55c985a6e767,3888603559,13484777773596567403 +6162c83a-78f6-8bfd-9808-55a1daaf4313,2190491106,12535691388752788539 +93acbcd5-755f-9f06-8e4f-a649521c5a22,577662347,14046354717652905192 +751460d9-833f-2378-71d6-d7979d3d2c13,1345513610,16952213559649137585 +158ec213-fda5-d1ea-79e6-91d53a1942a0,1694880854,4150788549447913260 +5f4514b5-d6e0-ddf8-3912-b10dc58553df,3600829120,4890231476497155329 +187af573-0dab-fb08-c8a4-4b1a57eaffc7,2253585345,4225889593944036666 +4d85761a-edb6-65d5-11fc-69853100c23d,2037657191,9017940963000510773 +bc4d5ad2-66e5-3529-3193-d8f575626a74,1028519569,3876915054712703171 +1fa615ec-5363-d058-f02e-b9525ac38846,3222725319,8092629336544883510 +3f3d8744-10d4-834a-dae5-032acbb7f70c,2678454185,3853286212227022333 +8ddfa2e5-d121-515a-2756-03379659593c,1030369410,16633979696154523036 +a1a2f7aa-ad08-480d-ab8c-0c350c37a4f7,2092390723,613467872252864032 +1c820a8d-ccbf-9e7d-e27a-eac32f5c2722,650227872,671811741348539481 +75058359-8cbf-5146-26b9-9b93a8d99d35,1935262649,13233506953170830124 +e0720dac-07e3-2ac0-de4d-b91316f58c27,428191556,565687415831157497 +48579f00-31ed-5f8d-3738-de13f7127f35,4167150366,12372562181102086993 +67d8a820-2b09-0c2e-83ac-ecf6011dedab,3507642120,18282184758008120447 +bc97063c-a459-724e-d970-79517391dd63,3617625084,2599953992828706027 +591a2bbe-8064-8993-ee33-255f018c77c1,2304021850,15863066019238147434 +29eacd4e-9315-b282-21ac-69051100d9c4,1261845936,16535747695039622136 +79d6333f-318a-dc5a-26ae-f764fade53a8,2856758891,5963024195775970317 +596461b2-2794-f308-fb7e-3992cb9a117b,4064006390,39194609520456286 +13591cf7-d7ac-ec7c-21cd-641227e4c6e3,591852671,12071198560654732087 +e5e79b18-429c-6d15-eab6-e5215d9b0cf2,1467928809,15110331886258623495 +ad175f57-25f4-117a-4bf0-ad651b7d9474,366041593,4564791929298039099 +ce6d54ec-890c-f9f2-8d6b-28c3aa6948b0,188101094,17339461901654988308 +c49fa032-8f79-865c-6445-55ea83bc4965,1382889821,7534177490721177023 +1f33ce5c-5b56-c78e-8bd1-4119e3fa60b3,492061952,9936843666445082058 +b56e163e-7f4a-c381-26bf-a814d5e8c321,930765025,1365725238176228030 +7ceec985-9962-8b6e-fbbe-792b1689f6c9,2544182331,16056103710726486635 +bfc9ca49-0b34-763e-e1d2-cf9bea0eec58,2414651110,944464975296312967 +12dbe397-a21d-5863-90d5-ee4b40c826c5,3725312189,2024225058124800803 +8535d1b1-7c2e-2a5c-c11b-c258b358c020,719680660,7640899612469597515 +fa089e50-7ec9-9701-c876-d87f8c5311d9,443981056,17611137152184742574 +e35dc296-d13f-99e8-b625-0c354e5c98e3,3322639453,7863514129683090295 +a4367729-ea1f-cfb6-ff95-e373352ad498,1066516523,571176994116042607 +5d466431-777e-9061-de98-68fe9943ef11,4153000209,9965403610154323557 +76307992-aa7b-76ba-b6f7-a17d72f45e93,2873607212,13589475465145842210 +6c371426-2d84-32c0-121b-a9099d780898,1019617893,8994546444070345983 +b32a2f89-55a9-b12e-70e4-6f3f2b414dbd,2139247823,1693116066589101355 +e46f570b-91a7-3bdf-8fa1-9d89b50a81f7,4252532186,16414243102804913232 +4717d86b-fdd4-c525-ee32-16255f6fc8f2,2838720602,3466803248177244582 +113fc77d-24cc-1f47-305f-b63088f4cbc4,3324382516,4531272233999546788 +362dab07-3553-7e59-5b0d-547a541337b7,1089094424,11117902927623788269 +0ef81c13-c76c-ce10-5588-fe5f01c17273,159410593,12066192624013959639 +866bdee7-cecd-faca-49ec-dde53fec9fe7,1974814154,3376804740548890207 +4a59a001-0f94-0d09-cef4-55c30154ce60,965294058,2510981621563801535 +f1d8d0ef-69f7-d43a-3292-ba0f1d459286,2502786662,8645712205697527485 +fb2433c4-9e95-15ff-4e8b-3238364d6070,3466424031,17177159953915608445 +d56d75ac-c8c8-9e24-5234-9a1bfceea834,285721847,2243154023196256602 +dd4a8892-2e44-a026-cf73-d8386e283801,3382497179,4207473876837961486 +826aea0b-750f-b0c2-e3e8-06682e485885,35349074,7028415244382842748 +d01b851f-d652-dfda-e2f3-959d8382bcd9,1236688578,3289756284199834182 +6c607fca-3092-df86-35a5-27335df2bae6,823656955,11938207692918659349 +79fae4eb-da16-da16-8322-ef3ae5c9b7f4,510259776,4937984776872426201 +25dd1ded-09eb-ed6c-a5d8-ecf58839e2e9,1340349695,14009600786432039358 +57aba3cb-b84e-369b-30d4-e42b7bed6207,56088314,9851202082986427644 +ca07fea1-8d3a-3234-f541-9e8ad5a3acda,328694133,11351261037136643753 +51809fe7-dce4-fd01-81c5-bd52e58173ec,2925902786,16146079593472441856 +4a4ef45f-a925-ea55-2347-56e63b877972,600102789,10543796526539242197 +2ecf9926-32c2-dffb-12d4-6002de11f90f,3244129376,6725311458271792370 +26eb116a-fcb4-1c6e-3e03-c444aab1f2b4,354688241,2271831174002115081 +345d216a-5dcd-d68c-d083-0cb9c374b5b9,1934640664,457354023910304247 +4160f72d-52ea-4ae9-015b-2e5e74838fdb,3883022451,7637687543046633743 +1535528a-646c-4b35-30b5-426ab2440a67,1859918921,9653734449130146576 +a22e7977-65d1-9d3e-3b0c-fae13b2bb9af,1527505915,16792141549285571915 +e2d398f7-5779-7369-0700-bf120f0067cf,183154043,10168866089555017264 +b9cd3ca3-4c24-d5bc-bcbc-43c0f6906c20,1070829020,8470117031320277746 +5eda46ac-0410-312f-3079-6daa121a7550,2041669502,1000258147585774828 +ca0e68c7-59e0-a7a1-568d-047e153647bd,2293517107,802003609688233096 +ec4c66a8-45cc-127e-e091-af2eb80ddfa4,775121666,3554609993723862040 +e7387e7e-8feb-9c64-9eaf-457d96698980,3300171374,13777882581471116739 +d0c21c28-0ca0-93a0-b30a-5c8d2bd13290,2862120279,12992629274660224830 +178a3dfc-3c67-4b54-9acb-c95575b98e5b,3792500296,4900626136441218905 +027ffec6-5d57-3b32-637e-06b0b1d333ed,1402861436,12677926296668156866 +51b87bd3-969a-fe1d-bad1-7ba88bf0918d,3548316005,2643806475567805930 +436718e8-abb9-b438-45f0-c16b866d2781,3228486663,4786884850124826862 +3f29f0e4-2bed-3d06-3bde-b2f38384e457,352849822,3918274597720301209 +fe148707-5571-f325-c1d2-76f79f2d1adb,2225232319,5296832229332217955 +aa6a7799-3864-9944-f369-ce55752ba4b6,2805918694,16701679237066489150 +759d1381-d26f-1967-192e-860e14f4feef,2927384441,183000941703607471 +db8a62d0-59c4-c9f3-b764-ee36dfde6a2d,2907933596,11256504532884602238 +6b498edb-10d5-c99f-c85b-a342bc538799,246262273,16344593691565644358 +a2e6dabe-b5a6-6911-0373-3ea3e501e97f,595249649,10221451693898073831 +003104ac-92d8-706e-ca9f-d8230d8f4215,2941689476,15611947835098123315 +d0dfc9d4-62e2-558d-5e0c-30458288ca0d,101481657,12490115048588549182 +f71b93f4-cf2f-533f-eacf-6d333c38e91a,1714159960,12793247624198357284 +cb308d50-296d-fc16-76e9-4ea1a825d557,3518595641,3199977881480499609 +f5cfaa70-ae22-382e-ac36-31ed7f26c16f,1324907015,14332732126183253594 +3cb82be0-d909-6963-71be-2cf8c5e5493d,3834518174,4303138041370944375 +88e414b2-4be7-8601-8c19-1bbbc815a381,2303712155,5710427617991528924 +f6dd552a-f211-4bba-7fce-f62cf08008d1,1573058207,12005658638806883539 +e659de70-7197-82ed-ad0e-b2264aca771e,2249372371,8222077397486285088 +091cbdd4-e84a-44e4-80a8-adbad72339d8,1897889398,7036013505168007549 +9eefdc2e-ddfe-1224-6088-264d3f4e42d1,2009055819,9835270854546018047 +d45ef96a-1aac-e3f3-9ccb-49ec8ad29b93,1023001178,3299746990097143204 +adeb26c5-12c8-b178-6164-844d92dbe72e,2009257795,1520220140763502996 +8976490c-7bcd-341d-5638-5443bc82540f,3100597533,7707943049049133133 +cf6100bf-1684-475e-db2a-d00656c9811a,53336076,8851274906742924426 +2945026a-4e68-16a4-9a14-6c8e563b982e,574340249,16356241213993036132 +c2558127-bc1a-fa00-28c5-fdec5a88c3ac,1710597616,10838727631084191064 +e9ae0343-2296-8935-232a-698c419fd138,868101478,13735012742479466263 +d9d78035-aea8-79fe-b0f7-a06784eaa455,1114204971,10312303878703810629 +7310fa5a-2cd7-4ea4-749d-5163e98ee869,1067892885,6713760222717272700 +796acc5b-4694-0b53-fc19-1da842458032,1994705785,13153676040968765359 +81b33600-9961-4ce1-2a60-8bab0db4784e,3155257722,2099892819359953548 +507dfcc6-d2ee-a136-5926-286fd8ae492b,260541677,14351192028352481232 +74e072ae-8349-abe9-c0ab-8334e80ecc82,310033699,8274812413845068475 +1b820547-47b5-de42-27bd-0c2fb1752f90,2787246736,16679983939237563289 +f505ef66-56ea-9c11-92b6-910d75dfa580,3927841441,14822879667310497860 +d46b8f7a-9bba-6645-e716-ea958c5c0eb8,701913410,18098434657243385681 +395666c3-6e3e-90ad-6b31-aaf64313360e,1755766348,13114248603907535987 +03947ad4-24b4-a2eb-31ad-4cc69e05e870,2416807817,9745732061236408136 +8f0654b2-0462-a724-075e-e0d9691aa1ee,230427668,720597571671511456 +c07edae4-ef64-fefb-ac36-2215fb822953,1199583258,878166315864538281 +14245c1b-38f7-8b85-8cc6-62196b366dad,1424421366,5704587271461541473 +271d029d-cc9d-d212-8f1b-9edae32d7179,1043599542,2637845064792666474 +6ace4e3e-8181-362f-b8a6-361138fbe883,2193188187,12676804323153240890 +2faf541c-6890-0e5b-c06d-52c6d0951b8b,3021786484,3716031894445840709 +cf25cb70-69ba-9d6a-56a0-db3b11ae389d,327158637,11230723223499097884 +ade625e7-eaa7-4ab9-0bd2-5047b577dc90,2705400406,10555563700535950681 +f8616cf3-bad2-3efb-85ef-b3c3a46d21f9,2363367614,7681773705863561576 +0c968067-9528-a031-2274-2ea28444fe5a,3773538874,10436439425318196631 +e827118f-ab9f-de38-53b1-55a849398af2,1636062697,9931141189880989373 +ee947fae-b17f-fd96-cb26-d504178f36d8,1278072961,1755812388822394620 +1d3334f1-a5fe-23fb-bfa3-afd2653e69aa,3773350894,16142896797508317116 +ef61a0fc-d802-030a-21af-f9d0a8a0fa9d,712368374,17955960341051926459 +caf1a7b0-27c1-8ae1-7bbc-aae2327c34d3,2438029646,18225684449405969981 +db8c3d3a-a122-7373-c368-8a9e89acec73,2268715148,5028273045986551723 +d32c2b13-0336-e56c-d9e4-4e8dd76e8878,3837441199,7627690489346291105 +1e708f3c-c32a-04f9-464b-9b2e540ab4d0,3949450633,6362996050197072050 +0944a899-fde3-e51a-49aa-175a0fe33ab3,282430450,15138280439278535702 +f4e38b54-84a9-5be2-8418-e8300ed91f5e,2547389289,18035380325060719252 +b8d16942-14f5-9bb7-62c6-1510659fa0b3,3084777773,11682387184039828665 +decdbd6a-2f58-9853-5d11-1489c51efaf4,1083271020,7944745183038324376 +4cad7b31-4de0-fc6c-cf3f-2f9d6d9c21f9,2774893743,18093950776522762042 +7bf6a002-cd4d-988c-d27b-b8e66c63ddf0,2057542707,9303927425681775321 +0c0a882c-350d-8b3f-265b-eed7562c6d7d,3908394377,13575425780104434116 +48143fa2-0045-6a17-5d1a-6c2123792baa,3939567024,16410095220012065789 +0c315c8f-e6ab-8054-e018-b1bd857b0603,1208674655,17552866057611006967 +d23fa70d-c69a-59b5-6609-c8f771f74e1c,3705859527,10828719965736840308 +ad92061d-de9c-82fd-cc10-2ba0f39890dc,1480033877,933997836719415589 +f8738342-7d86-c052-a3f1-c92157e53a65,2434815674,4254558342317159663 +6b68152e-ceb2-1b80-2411-a691a41a11bf,527894703,4591510583374954457 +dc4ee56a-9fc2-df50-706c-11092b64366c,1496112743,16819890346258327164 +2f1a23b1-491b-b1a3-1371-9bba8bdbfe59,4224994697,14833156595515404892 +a5913c8b-12bd-4af6-b945-436a9e481487,3740584683,15626015589622519606 +ecf87aa9-7295-367b-9724-5f6e632c15ee,757832756,4029961164447207395 +50ddc6e1-afe4-74d8-b0db-2df7a8a7ca5f,3566374057,4572429063316855169 +7a91315a-93d4-f3b2-2abd-b0aff1f0152c,2559307257,4996025529202402056 +3212f2f6-67f0-3eaf-a95b-9e9a6fb94369,1253770007,6551028238528245041 +0ae75e6e-09eb-64f8-2cc7-7bb97253c2e2,484422482,13999554099631044471 +485d6bed-bcba-78ac-92dc-6651bf26e135,1450415724,14026979253437003711 +41a304cb-4660-3480-6ff8-f8bf7a4ebf1f,4238818636,10499103186815533916 +f73a15ef-c0c3-5189-42e2-97981f8eaabe,1156577122,4098256755728168912 +0176a931-03e2-205d-d713-65b09f111395,2056351254,15979640900588607460 +ef522937-b896-5354-228a-5c324a456af4,177669533,1673877157405623975 +93ca410a-36e2-0980-bab5-bf249cb0dc92,2042111806,12080707883884570601 +8c0153e9-1d00-70a9-ca30-8438547d6c88,1952441205,17637306619318147617 +284bd817-121e-b0a1-94ff-c705b470ac23,2362373181,5121238231232282917 +06d2da34-8664-de3b-1bf0-cc59d0f4d757,1565604906,14211424518201898326 +573392d2-06d4-a9e9-72fb-e2dd0b25a3c1,1067902547,12901673392320741144 +49cbbdc0-2e7a-a7a1-c55a-bb46eeb902a8,699808680,15494127518868781001 +18690629-905e-1c5f-a744-4e3322bfe3a2,2730338544,7554645075039401535 +a660c510-2625-e5c5-14e6-191918253152,172775399,14113495119665976575 +ce529b21-0b4f-716c-4b85-86551581d829,2480445819,16542350762339786625 +6dc5d20d-e117-d96b-9c26-b8583d5bcea5,3930392117,6496401041030816099 +b65512b7-70b6-79a7-4397-6e058cda033f,3393114097,12851164390438498567 +59c336c4-eaab-d93e-1180-d546d0c966f6,2145688538,14105228100530059086 +36dc00e3-cad4-fae5-5692-a0003405880c,2257352410,13411738600457515264 +656b5d97-f214-cd35-058a-3ac82e3174a5,2105472698,2237311162365976054 +3b602304-c0e9-09ed-a842-9c407796488d,2954981913,13305566361321239972 +deeac54a-c904-a43b-64b5-e2c4337958d2,2843982225,16078967943089791680 +b3ebb363-e767-64ca-b5e0-22a0f67ff866,2875640626,6071738893125488780 +1f472d57-b767-6d1b-65a0-48b6acbdf8f8,4220999016,3926780384877629990 +506b9f1e-a88b-9978-1fed-3f52b8c37a61,2939218985,10232536975065990734 +8458e2c5-6ece-61af-afcd-0af1bdd88322,2222814787,6859421070713214464 +2c2bc890-8c26-8805-1221-01e8c0afc59c,1809163963,2505544207994461533 +9f909151-383e-6012-74cd-543513387668,6287418,4323128188800414768 +a0355980-1174-cc10-9208-646890465b8b,461134231,4230868344843890066 +6545d23f-319a-02fb-d4ab-a7a92cc5a24d,4019961529,12425558947904808842 +831b8354-9e5a-c471-715b-163b7b1965ae,1022484229,9540280126756362570 +8b0fe794-9d94-8836-6409-afdbcda80ecf,3673435455,12713336498383471425 +6699c06f-eee3-7238-94f9-9da93638b85a,3496641366,3152234823759092831 +906e5feb-135f-8610-3135-c303853e4d9d,3462220697,14251165878481243028 +2e599ffd-5da4-edf9-1e40-0cc24da00fde,2172695050,5924917924961540603 +ce9fec08-1b2b-53d1-31d7-d0b68ca5eb6a,1600198478,3900934388237998379 +60d30ea2-4a4b-a77c-eaea-45654209a980,3325936590,14349779265769144774 +9b38d067-7be2-d8ac-fe95-4acfb8fc77de,3690252707,7863234621849534574 +55a4176e-dd3c-4e45-352d-6cdebcb84368,3708098690,5045397172991552292 +ce3de73e-9aa1-f609-fb0b-875df6e1b8eb,2900082487,16834997174820855320 +48037c35-6e21-de8a-02c0-c93614552802,3205622719,5345219316307419139 +e6e46d92-a0e8-d84a-16ac-c8667303b254,357786535,13817860732088760095 +d2f7dc93-a9b5-3b38-199c-287ddfa58584,1765018776,15861423395888118469 +d549a042-11ca-961d-279b-29ff063ce0ec,1484741192,1829682330195559050 +3857a879-bc1f-08fd-8ca7-c5dcc12fb459,96560634,17743619444536168824 +f4468aba-f26b-3d5e-2931-94e17fe45aab,2233212625,3576971693623466764 +a75c7982-2fdb-7f52-6c36-76053a2f0154,36348288,11953369458704328874 +fa10e196-1c7d-41e5-0478-8492ebcb2a47,1527434043,10780180774807667276 +43f2df66-73ea-45b9-6876-96651433aea7,1440819381,15413417236816038956 +9b35b8c7-08fa-8f8c-af8a-03927a3c1dd4,3104459470,2750739450699841490 +eedf027b-cc02-901d-2310-49b7849b726a,2023469719,3446275737470214462 +e8c174ed-a6f0-122d-33b1-bfe3c1b67341,2542932654,9924636090569562793 +cbed6da9-31b3-863f-1639-8525d01a9bb0,198765850,13126132270174868018 +7a786036-2bfc-3a74-5a0d-997d51bff257,209306032,9319678662885936443 +2801e408-4015-1f16-1f1d-15b28410389d,2000021174,2929265369626222079 +f91c5201-fa9d-f76f-0178-42c0dc186757,2959436582,5813217913243571580 +1ad2c362-bb58-783a-3da5-878b635d2ba3,286015625,16871217975358041455 +6e9bfed9-7cd3-ffdf-3d7d-0062abe30711,858941259,2721438742313930700 +163c5f9f-913c-16ff-63e2-7862a949b1a0,2316663935,13842632528507827056 +2319c6d2-e0c8-4d13-4408-eb74144609ed,2563297843,3891462435154660002 +6da89432-a1ec-9ad0-5ab4-9bc1fd050314,2824771552,16823776246511067832 +4c31fae9-040e-96f6-84fb-e8274b8c33e8,3241419403,12532688633823718892 +4f34b901-748b-d409-a4ff-e28c0bdff747,1922503600,16997710151951584377 +be30fdb7-a79d-5223-2268-a3b0ed52c01f,3810687524,1687997652244092424 +ffc71acb-9666-7c42-a8f7-c550e033ce15,473068887,4337311515220657347 +7d1814ba-14ad-79b7-3b72-9cd4a7d352b5,1061848652,12504342005181460137 +c19037e4-fc7d-6b60-b09e-da1b6e2656e0,1353484796,6097238882694162609 +49b2058a-e8d8-0b00-d8b5-14592b5a075a,3495901338,10952909932788109498 +2ad379c2-7412-9193-9cb7-30973d79adfe,1472371266,2613238114304729481 +96f5c8ea-ad22-1efc-ba44-961b5e3b619a,2524789468,4384631613690247246 +24e33678-13e9-1287-de97-ce9776733599,402218536,13388340058492048405 +b377c784-3730-fe77-6de1-22b3793c4075,3482557812,7288639828990839629 +3190463c-d4aa-ac33-f882-20e6d491461d,1001298540,11490824074426862065 +490cfd67-0e5c-6d36-8c28-282c3ef2c9e9,2583258651,10132155857990342506 +7044101a-d1cf-557e-1443-5cdd74c3af81,1131789388,14106883895069951037 +0375494e-c555-6ecb-d531-85ea9d5a040b,3444696683,17360709994987117808 +56feb329-e9bc-5f0a-cb8f-0dab5f10bdbb,1745281657,5954704613510332336 +2a0038fa-3cdf-0fc0-8775-6363239be525,1752878311,16467483388108960995 +23c534a2-9dfb-84fc-b1f2-38951b227936,737651934,16643850464680667363 +eaadd346-037d-5e9f-e63f-f6c990d5d68d,872395958,12820663969096065992 +898f8285-a790-4080-7755-21b603afc92e,2969817388,1927925050925769832 +903dc417-f0b5-4c11-9f21-ed40e1af148c,248336631,11383448459230838749 +42ec764b-f48d-7f2e-b62e-57ff2340df17,832225132,12277022917744356788 +4c48449f-03a3-2f86-cbe8-66729460d38d,2642406716,5419240648089506874 +91bdb18e-51e3-69d5-bbd7-1d8231438cef,1140998305,6721512167371395038 +df347f43-181f-542b-1271-f559a8eafa86,2403820877,2264902790028070702 +9c188990-c96a-7972-6a6b-5ea0ae401ec1,1292356842,17031794565426863770 +787a63ef-5978-0bf1-c1a1-2cfdff534773,2862807491,5861966997055719555 +9a8f9412-d4b6-6f80-fa02-fafe48e8340d,646559980,15202375185368773414 +5dcdadb3-7135-9242-b62e-c2238ab0f0a7,2316431536,3752877606996447353 +60342d8d-4a55-7a59-92a6-0eb9f8c81a0f,3033489573,12598109915472572617 +a1b61e86-c841-f766-4c02-f11d477c5c7a,2150065100,16483936133404277616 +2752078d-e185-c645-99a5-f32f1c540c43,3459546264,15992255796905668498 +968ed703-e7e2-cd03-0d62-d32c1d3fdeb2,2293080746,15510612262716442280 +89777bb3-7f1b-2c1f-95c0-e8375b6fdf1c,3725735759,16416420882882913822 +ab29fa02-2412-8e8f-4373-ed2c8fe11ddd,2444506979,5240950991282647014 +e402bc01-9f51-ac62-7cb0-5b2b2ddfad22,1532708984,5273077157656099219 +50c28f27-6088-1582-ffd8-58d911514587,2481054176,9993609612686600695 +851945be-b107-c305-d20c-2a67692c72a0,2524639753,12912563177399411034 +64880079-eb20-00a9-85d6-912ca3ccc3b0,3425445360,11591672076453056492 +fde9ddba-b51f-2fd8-05ad-7e3e612e96cc,1575259242,5216304358616235437 +33134501-b5a2-b114-ac90-1dac3da31140,3093236360,10469471982679528290 +96144a0e-d7ec-4f09-a51c-4e35abca9100,2023957513,261265319820693936 +1d523641-6d02-3df3-ead8-6d0cdab66d38,1195497072,2259830004386973413 +b5e633bb-546e-7357-509a-aa555c76dc72,3335474920,16816510854584552593 +aa6c419f-451c-a320-12b3-2ea9533a65de,3955102359,8393930485214192702 +67ede717-fd48-808d-48f4-53305837785a,322898284,7362130774160288249 +0a3cf494-c966-ecb0-252a-78f72ae76421,616863969,15358003013753843674 +e5ee657e-3621-594c-faee-71002b7df7f5,97880849,9899565961411829565 +0c89f90d-9689-51aa-e45a-13f0d620e600,1811622928,12493331783555452784 +02beb57e-aedb-ce30-f91e-6bc4a586881f,3426948360,408271550182806814 +dc86956a-0f48-17ce-afe8-20c5f5fa7ab3,248655272,16443925598490310359 +3e43abff-f747-bc92-2655-96cd4f9d2610,1194610933,6382236176486524452 +a561f4ee-1f55-84d6-4c77-85c88fb43ea4,2554866749,6249709904110169965 +07b560cb-0463-57d0-8d23-0d0750830302,2830236831,13086631917243381198 +0a53bd75-db2c-6482-713c-64494fcf9ec1,2835919604,18300909283131164702 +652c7bf7-fcbe-499c-3976-f717ae8b1069,3460828569,15604584868313982761 +d16c1f07-a3af-643c-9d58-c9647ee4597d,4048150596,11005264006242080802 +96fda0b6-fa59-e99d-d151-e1b1dfddb41e,2783135191,9242682020064362550 +2ef9d1ec-e347-9bbe-22d5-19404d1d3a7f,1697625662,343044442189594408 +e4c73aa6-049e-a0fe-7f03-a2a7fcb9cc17,275231280,10891713464719950544 +c7d50691-b28e-92e1-b1b3-14065e8ef61b,111317116,14850148159515939754 +5483c795-f5da-f090-a069-9de897bce3d4,2484210053,15764988135941902119 +613e39ae-751a-1ac2-331c-2a43e78029fa,710784148,76490807272815464 +55e29787-0c25-477e-0994-392d6fa72b2f,4217340414,7376488360968467602 +6bc0e1db-0487-ecee-fc51-ef03c2c57564,1339438342,16766089032788860370 +246ede85-1c8d-c992-15b2-e922f7fdba8e,271942497,5969256490107084312 +8faf9944-6dc7-7da6-51b5-d93298b22f42,2780796732,4558398215160044834 +f232794f-2616-ae26-b5b3-3175c910a267,3694558594,16006888634886739681 +cfbea136-9872-b73a-7e8d-e2c6c3aec845,1484815591,15617190294554833455 +3e3bfe67-e03f-5614-5671-3703af76344d,3744374030,3427031809773710902 +36914c49-34a9-64bf-6707-11b5c27818c8,2344117924,6046327905133700253 +3cf862a0-97c6-78ee-c256-b5edcd17258b,3148079563,11018155825840568207 +83304ecb-7eee-62f0-61c3-13dfcccfb4ce,614156761,16752386904791287517 +a3c07e0e-cec1-c3f0-4fc2-9015f77e3b33,2828782240,14411032490053733808 +12ebf0c7-611d-ea5b-e276-d3fe6cd34748,719165505,16003040397172394690 +29f3e07e-fc01-04e1-3c69-846b54fcac9a,763838505,13669182366990244718 +1b772886-1ae3-7829-e638-a4bd0a71906a,4111137007,14722113669630231565 +8b2f2f6f-5494-c067-89d8-0ac2d1c04feb,1814945679,3975192763625517634 +c8a9c1b1-c8ec-4562-062b-f8336a5fdc2f,4277620519,4411806392570356644 +1cf24031-0c3f-1b45-f624-e8cc6d5befad,3205614155,12753102781866775463 +479851d8-f592-48c2-6e47-381465624d57,1312481521,4404464144550183725 +c30e5b27-84ee-f49c-bccf-df72956cba50,1441574044,8289595981157323809 +e4b11fb6-f05c-b6bd-695f-5ddf10eb69ea,2148876849,9784645160259881991 +1b48e200-19aa-45d3-3391-6e3edda596f3,109652863,7091452710604039351 +cf76a874-f026-5e4a-585a-4ed1edd650a9,3182814213,10250151638991418416 +01e3bc8b-6520-0902-1e71-0fef75cc61c8,3307221545,3483004818973048535 +9b63f3e5-04ef-b71e-0fa7-70643edea46d,85607028,11414056414059429812 +06ad7c4d-b12d-c488-9eb0-ea05ad440465,237159980,2951302576198421030 +b16b7cd2-6aa3-6d3e-15df-cc9847e770c0,2963977619,5657467161453307152 +47471998-2fbc-3a52-2885-885cf7ea508b,3959501074,17707750418147610762 +dc4529f7-35b3-1171-db57-e26122cd2a58,2863305478,11572772650160655046 +7ef1028b-8e09-8087-5eaf-faf650b04009,2606124170,1091511688345676135 +67831188-a42f-8d36-da83-e26c3c59714b,610979628,6443186476319623218 +a267c999-6bc8-c9c1-c574-5cd0c87d1813,4224232700,16298518619950243437 +b3d897de-92fd-fe85-f074-655db2e5211f,2796365134,12661123870241948610 +f8573e17-6060-c197-2101-595c8b3e2d03,1743467377,11645726236106046502 +db040542-bd36-c112-7993-501ce696b063,2828161268,2524847611665541354 +518536fa-3a22-1ffe-d930-2279bf47c145,1383477809,9146907425894164578 +f2e2e4d0-1bd3-dc0d-f891-ab548e509247,3223849610,38848494696350514 +c983bbe1-bed8-dc69-4d21-35a8f3f93421,1564649264,2468414691914835061 +ea8365be-cb19-d6ea-e156-7b02972123e3,252628959,15106642172838635961 +c160de57-5119-bbcf-bef5-0065c95f15ec,2832604207,9719415081026787022 +901c5031-eea5-277c-916b-e173b7d5b66a,3054209189,3186650176342119223 +20de0d80-c389-029a-a4d3-01cc828f6513,2768696818,622658363037053603 +c4530505-10d0-ca7a-64eb-f19dc48bfaf7,746642052,2207726976253941588 +1aac4485-8a41-9a96-6fc3-3b2e0f2d548e,2872818026,8277938490693728748 +0bda1256-5931-f39b-4f3e-0a02050e84f1,2628086715,4616331840820881643 +97129eeb-dda9-9bcf-7152-e40dbd5ab414,146582282,13508873933993185992 +2b55372b-d08f-8cf9-d8b8-540a42389340,2987998952,832225457203934354 +f1247cbd-a5b0-8b33-88ad-c439265f20d6,2014645019,9771664352657192848 +a225d9ae-2037-25cf-bd1f-653613f4aabb,1273237676,6341097323613776348 +5df6891b-a2a7-9144-e7fc-45496d2e3970,1240700889,4523279943234762645 +457a9a6d-01ff-b7b0-8965-cf5f58c32445,2042908725,12814321331613351368 +56bc4598-bd78-8286-6fcb-894a9ca4a994,1984714797,9920356699931220693 +7e362a1d-90ba-6aae-b79b-378ea289860b,2178750966,4234281726846872894 +b86d353d-4c07-01e9-aa55-332334ebb1c0,931776260,15638421925282872830 +f25c6877-aa86-7535-fc84-89b7e76ad01e,200503641,12112331055955808268 +8321087b-31c8-02d6-b1b9-0b5f54fd773d,4055326929,8968302270074269564 +2d0dcb4f-ed08-ae5e-ddc4-f1550c937a47,2448996681,12472232728485411194 +c08d79a0-2969-0d15-3670-4023e7029779,440372585,4974675776301344077 +2cbc67f7-3df0-ca7b-81e9-7d281c615543,3073084554,431645599245637340 +eed50dea-dac5-8640-b643-19b272926672,706286603,15653083028667052697 +3572dd71-e156-6fca-27ba-b02365ff4487,1957043419,5429602992831333792 +874a3d26-dffe-cb11-37a5-5ee5b79a7cda,2213822368,7882932249588366842 +11e05568-c53b-e446-08e6-039b61c1e6d2,2938453456,7428772942429824501 +54aa7f9f-9e93-e7e7-0ba7-49efc0e2d95d,3076081761,949147167363825400 +61b48a35-5aa5-413a-5d21-a2ebcbd5253e,1624746683,12402096158154442328 +d02dc824-fcbd-460d-1d19-85aceef1e972,597449761,10269884058017947337 +e33c88dd-91dd-0afe-68ed-eb018fd77498,154529580,10477042183700903940 +ab42b529-79e2-9d15-2195-0422a1b7a8ee,2365390126,4777715963217570662 +5e5dc111-2175-282f-d8e6-c5c419892f0c,3759634257,14038675884219763643 +ce917f8a-1614-fe18-ff63-1ce1375be725,191507001,9998541349229702736 +5038bab7-469c-3897-711d-32736549c3f2,2170806641,17050333104832326613 +e9707c00-df98-cf52-f90f-aa41a05c822b,432405975,12887485203299845536 +2acc2cf0-534f-bbad-c975-29331a003591,2592943801,8987579680000198760 +b02ff3a4-539f-776f-7165-f908d4100140,3857179786,1951750579467324317 +66e94546-7f92-3f5b-b0f0-5ed10fadfeaf,2714827554,6419407335867994346 +3c0acd4f-7442-0461-3d82-444aec18460b,2939646572,15356748659053920208 +19bdba87-66f6-20b6-d5df-b2f045609c6f,597605349,4219696535515382931 +0dc180d1-b2c3-a862-829c-fdec52bfe298,2125363889,8261623833588463677 +76b8e711-cf96-8d9b-b7f2-2c69c73c6724,3916880983,9053860729102465443 +441b999c-93d7-97f8-2e1c-d4e74a987a1f,726033079,2552240795572189619 +571da80b-7d2d-4762-1977-c1a904ab3a5a,814165306,8081854518888880787 +5766cb18-062a-2a15-dea0-c3d5cffe1508,3263342600,16340580810378177201 +1fc0aee4-0174-f0ea-be12-6e2b3355f1c4,2571408957,5975048320420677223 +84894e07-f5a2-ec58-031e-4facf563c415,2766988641,16566369372711317081 +22903828-c58e-b5fa-30d4-9c07c0c75e54,3922784397,2212635799526141208 +9bd4816a-6077-19a5-f223-a0a6d8b39218,2568872026,4000895613869544557 +f68e346f-8f98-8c4c-0393-a81bd256569d,1802865762,683341517508285243 +d263bc31-e427-a49d-3f1f-674461fab515,3830515192,2320546830644472566 +2d3212e3-0d70-9067-6244-e1b33369efa0,3294639490,11530497639676372878 +9917379a-389c-01a4-bb15-8b8e743edbdc,1361698738,16648194595296981483 +3f6db31b-a221-0f14-97b2-a1082b4946d1,2824333032,2381152160328830485 +71beb55c-7b71-db35-1daf-b6843907ed2d,326799092,17456992603341077106 +1778d6c3-d62a-cc21-96c3-f504658e8397,696040483,15534200438504106513 +72df3566-24ee-1997-a914-a5d20c7924cc,3373275316,2368880392260222777 +21b7a2f7-9c6a-b934-53dc-183cf5fbecee,3920590999,7447360535438262373 +bf99898b-d07c-9841-ffcc-ee1db026a74d,887963785,5031527696999184635 +a23da45a-7454-1737-36cf-28f20ad1f81d,23347215,7142268869596279141 +12918e22-9bd4-102b-1395-c1543df9304a,2675044086,1482112606093684779 +1d66cf12-8c7f-695c-a62b-daaa5d999f17,2863005953,3468085715104302351 +dd2bbf55-c624-a320-7918-a8e0651276c2,453245158,4378080615904962803 +bc6b4b84-2308-b38b-7625-96cf12516f5a,3254703322,1192504237963676680 +098fd628-6596-c5e7-31a9-b18fa8f3f792,199949565,15440321123645731964 +f802696c-2c76-aa4d-a715-6937ca064ba4,942836349,2598741085774515278 +be7588b7-665f-fc92-7644-9b1b2a88cd08,1320771342,13286354251915459181 +284eb58f-75ed-d6e7-a826-366b2c82e6dc,340708716,6120743255362331330 +e798ada9-2bc4-9a5e-b64b-beeafb4bdf3a,3978964888,4159210564954825924 +d46064c1-d547-0f04-9c1e-c32c9af6a661,4194680101,6627820269147034632 +1fd10620-885d-1fb0-7e75-c8ffe7e14e1b,1661143111,22815056127933215 +4e7fa6b3-5a8c-442c-aa1b-f0d084e6567c,3109192772,6128008380558590289 +61d9cc01-ebc9-e3c6-48b8-f938641dc206,1020022717,2043041351011853867 +51927ece-6cc7-24a8-abdb-5d9ba49bcf21,3113935126,892555446721760866 +a77d1c74-fa1e-53fc-930f-be0bc50a8cd7,1391860172,18155807985700541665 +039a063e-1eed-d2fb-0bb3-990a1770fcf3,132158735,4382551107948846034 +94ce9d68-b13f-0bed-e781-958665e5995e,729536226,11133031076951624665 +41b64fd9-d0fb-f0a5-c184-cb45ac6948cc,424159425,10161771803248576757 +f665eb63-80e5-bbba-4273-b1a053dd8a3e,618615355,8940550087380822618 +7741db3a-31bb-7de5-3192-a15a45f182ff,1413653343,2548739687236139769 +23695158-8d90-3eb1-c45e-e2e117e68000,2342650772,15205210372273481332 +1536093e-077d-b5d8-7c77-9147e911f982,98938165,10361279076874869971 +dfa1e542-c40d-4d2d-1218-894e09fcae0f,3684079208,13139524080355935967 +a3f27241-0502-364a-c43a-7456a1c8ab63,3990315760,17563438366335579323 +82f90b26-298e-b240-0388-58d2d8f4c3bc,1055922135,17731350469178206706 +830d2937-abc6-8ccd-74c8-2c692ce86d96,3538899086,11510777569048808373 +84f08bad-fe7f-5744-1f81-710d1d56ee36,679032387,9720569854700432408 +664d15ae-7a9a-9d36-2580-6d5fffd393b9,602002705,6058303651397104104 +cf82042e-d69a-fcc7-3bf4-aa9ddeacdb00,3944977924,17081545187278263688 +21e2c6c3-33d2-74ef-d686-ff8363f23549,268495010,12122732834152797557 +18f1a123-1b7f-80f8-2c0d-dc20bd5093b3,3459372556,10284986373087532814 +3c5cfebc-5093-8886-90f0-3417afeb15cf,635839492,14250642598596801427 +c96a80bd-6e42-8034-f3b0-c62d7bf84b72,3456584832,838471030330820266 +5f508f5b-439e-3bc6-7e2b-2e476401cf27,3813258027,4346845191190759314 +f14699a4-045b-2f7f-63d3-c46504e4bf55,3084046037,8303145163586741765 +10510544-5178-7335-2b56-0c2ac8f38102,2573475079,11737779032704993953 +5c038c52-26ee-eb2c-dec2-3e0cca1a9328,1893164438,10950381697937344966 +84e0448f-a9ca-d401-086b-123cebc57269,1252341191,8267008448907812456 +e88ed511-4286-1b91-ebf6-162741b67436,17889735,2548801757011239920 +37ddf4d6-78e6-1d25-b929-0246eb36b03b,343119101,1647101915256751969 +c67b8167-e153-0d96-908f-2539f24b94ce,4094798211,15830294923303700633 +6cf23ca1-8d0d-ae0d-87ae-1bfb2a291d6d,897000873,5824169293641865859 +d1951b31-888d-9246-7118-460b6e803aea,1065323086,3147921706954198694 +fb89a51c-be09-a7e7-6910-b07b313b5671,381636883,9329583654319522127 +6fc08c65-5415-1ea4-3fde-fb1813963c9e,1277870021,4282189229579541992 +0d617673-7244-15d5-d505-6353f16cd84b,1795543001,12386675893605303832 +fce1c3e1-db9a-802f-333c-4d192dca141b,1358111465,5018007212810505859 +c82e76e5-b165-2f35-2e7d-7ae340947f48,4123611354,16588631501918818739 +d36caa89-e301-7515-ea22-a5478de68635,1057137568,6637264399280608228 +e9e0a595-d316-0724-be9b-3e1870dd4502,356771094,5703956968802021901 +e724b5d0-7e53-c9e8-0cb8-a6c5d84fa456,3315746509,10688550686494121864 +557c6b66-0328-79aa-0ce3-018e7e895c71,3555796729,7025557397821318803 +c2e2772f-283b-cdd8-ef21-b3b13429d1bc,3159720233,18381501916229329462 +bca7e377-3feb-5309-1a72-86eb9929b3b9,49624479,16567264835151046143 +77176b72-071a-6e2f-d7ab-7202222d8c65,3963036920,14607108840909141140 +be3893e8-d2d8-1dd3-bf1d-e4890b5e7db3,2890975426,11085315097862851124 +f6185c97-b396-da58-73ab-9c7dffe857cc,2013957316,4660110559498857526 +c5a734c7-f6a1-83c7-60d4-909043e54142,3047441556,17350787658601977401 +f6b8f0a6-f6be-75f6-5c5e-19065057b0dc,3731189800,1067386733895407236 +e72b4099-ee65-2aaf-c6df-94c32d7e8b31,3745622616,17770087947806318875 +fdb940d3-528d-b29f-f058-4fa5bee9ab41,3838592757,329780298088490870 +402c10d7-6149-1358-22da-78ddc6a4289b,1099805822,15496610877780535193 +d3003f0f-09a4-0896-dafe-639443e9efd9,3417777675,13400030818967650076 +a56741f0-d7ea-8ad7-33e6-7e64fca489a0,1348311691,11696592269518092946 +fca84270-f15a-0203-0e28-90cea63d70b5,810684869,17787356603738152125 +fbc30d5d-158e-a893-ac70-12908b9e9fd8,2480965665,14968455022609884671 +072997b6-da0f-bbd1-8a19-701d578c35e6,1723883943,15257277219275168728 +ddd06ddd-152b-3123-dc51-1f5d23a738a1,141003234,16521244375438511005 +118618c4-a85e-e3bd-2846-e5441857524f,3029624368,14707779721887390918 +de091f7d-523d-94e8-4453-42cb575434f2,3336823002,9914188368670499659 +1e3f60c7-1c39-7c51-3e8d-9c3a40793435,1385627916,14859386212715172874 +362480e1-b86c-d540-b03d-4c8495a2ae84,341050269,16245151902263288426 +bba16240-e1b7-8002-a966-11158f2d07f6,1125045739,1711311863624454382 +5025e055-3e21-df36-b596-a2f2ffcee090,979746399,8265987674588897374 +69eee7e5-6d92-39e4-4496-987abf182b11,1859971780,16943108397947806472 +9da7ce15-16ef-b433-e19a-3d62c3c1ab4e,1225055435,9229878981999298295 +d8009577-edf1-cdd5-51da-54bfbd7711b2,2400509631,7839131865478604785 +bac413ad-9499-1e51-4ef7-37bcf2d39e80,1038222873,17261162847589289224 +686f0fa9-8961-0101-cb94-c651be5abaee,552625673,6935237533973999745 +0f4350b0-9df4-43a4-e61b-866fa4e355fd,3475702756,246798945844616590 +6dee1106-0f53-179a-f277-f49fc9c935ac,4136768019,4463135117552462616 +756cf6b0-0599-c550-ab24-039e5e6518db,1892268349,18390698086351352415 +596b2616-c2ec-66a3-a648-55afa755522d,861028897,7099767649141842876 +9019f2e2-f988-fc91-a61a-30dcdfe24145,914094927,2416465774276540448 +08f82183-c868-6055-2b2b-cbd4aec58357,712451988,13659862740748842451 +d820330b-02ad-07bf-cef6-bc0b17c85792,2593188358,5888801292487516606 +58836630-272a-52a4-a81b-10bdd9704751,1991769267,6546544608569611432 +4b6f5ce5-7daf-590a-4b72-be86ced9aeea,2798850144,12710129591016454328 +be62f6ee-9bd2-4e26-5c14-b853b9df2eeb,333616943,5809288483640332675 +1ada1ca8-68a2-a233-992d-0741df69d0cd,3873318225,8859908242451345062 +ecdbb91d-a4b9-aaea-836c-f9dc3dadf33e,4209962742,14077908402388641690 +e5a17d74-e809-8542-d5df-b262286ff193,2165874896,3886985543311891711 +dd269357-e863-2a0a-0bb4-500fe5b2d618,149320264,15219603065454456396 +bd08db0f-f83b-cb58-a895-6ac1a80c8608,1114077294,10606855016142212693 +af7f71e7-9ce4-6c26-fe41-62b84a8d5908,2416616661,11158644164605450619 +2b49cf71-46a3-b970-7703-4932b4621bcb,4043057640,11499824857158965240 +284930f8-16d3-5b4c-790a-06fdc902599d,1127423842,10510581712921997104 +6ccc9b66-26e1-b4af-bf07-bf45e914d463,2586246539,15931795294751736988 +c1b9b954-21b2-af90-4af0-ff825e680232,1833306886,1628586192663298856 +c01e8eef-d62b-1a82-0be1-929ae4db404c,2753101762,13176697260560133240 +f0554dd6-b7df-208f-1343-1598e840c811,2813708161,14507782639480830199 +87e18439-f26c-14b6-d76c-3fd8f368878c,1903343678,14797428677234026357 +53ba9354-7c34-5855-c1fe-54190bff64fb,1415933164,13429131603111428217 +0835929a-f862-40c8-8bfa-ca9b17920422,1038419355,5474048625499747284 +944f2019-ac77-f9be-5fd5-414b0e24a16f,3264848043,11720116826528942640 +a6dccaf0-17aa-8777-d4c7-248b3cd204fb,4013280857,14429825636526034568 +44af698c-0127-0aec-6929-f53ce18a1d13,2994253132,6871936729797659629 +88966d8d-6ded-1c45-5583-0ce965ab92f9,2615477812,11888449423945581584 +0d875ac9-9eb7-e08a-87d4-f6930bfc6acc,505491511,8055376778789923710 +2c874e91-3e3e-4bf1-3ab5-276fa89cf02d,42047543,16963749636564283749 +5c23eb9f-ec41-52f3-ea1a-8ca92b3a8942,2624553406,2043255993759113534 +1e9b089a-ba55-4455-c53b-f2ed24a7e2bb,1601710601,5324196682410433121 +844a0a00-78c3-85c7-81ed-5d6d0b0916b0,1703009570,17480759315426986633 +fb45bedd-4507-26c0-dafa-406fa07b584a,3730092457,12199806310405954075 +9492196f-2ecd-5dd2-5304-476975a35b28,138235062,7320240688920644710 +54030d23-dc33-ad18-6d57-dfafd2a9850f,697379673,7125275669439077862 +569ac00d-4110-72a8-c52e-2de7ced92040,1092230198,12466566021509949791 +b1d87e02-30bb-df1b-3adb-5bc24c2ba0cd,1678823120,1071093158771912463 +2cb5f820-268d-855f-84be-b1c824794224,2138720604,14405092195771412022 +5d26e8fb-1ad9-2320-1eab-345cada22361,1048155787,8415037062396320885 +6b05986e-274b-f3e8-229f-8255f73f0f95,712086732,12693533752972197270 +132c456a-cf26-3dc3-8aef-f46e72b4dca3,3549389458,6542589442861898535 +8a1b4a5c-725a-7d6a-2ed3-2205ea3148b9,3420311500,13913984671027397037 +26aa191c-5f36-909f-b3b0-2b2ac578370d,2368370921,17211041888213229940 +f85e258f-02b2-5d34-b821-7225f1241858,1456042610,2379000339718834952 +f6f6dc72-bb89-2777-3a8a-0e7c8058d93d,1313811693,9970082381382640856 +fbab0e80-3b9b-dfec-cc6d-744060dc54a7,2370875713,11801984767133072514 +cfa10d71-6056-8354-72a0-c8cea1a7cf08,3284744643,13259272291965488890 +fc9ad20c-713b-2c5f-7600-10ef9bf845b9,3938026525,15667437461625037083 +f38d76f2-0c2b-0864-3662-9ab84049fdea,1991865388,7627494550849620761 +0ad8cbf3-5eb5-c4f5-d670-c8789d40465a,921252193,1189146903374821812 +853e46eb-9540-5181-7e4c-b76a7180f313,1314759415,1239629262183010911 +982bf479-4852-4f97-e5f5-50f1003aeebb,1026736452,1790333156194170586 +b533dd90-97c7-7f2d-22c2-0108f192919a,3683366952,4625016734585703521 +05455480-fd69-3695-da91-3622e42d4430,4192395261,6074733539598723695 +7679cf8c-ed4b-e897-bc43-e37864b3997f,1619435953,6452989343927253701 +47fcc872-e33d-b1fd-fb44-ade1e413b9a7,2246188377,5642823944777143202 +42d111d1-48f9-f7ec-fb25-81013b7c5a24,1771825349,7359725992837518019 +55b3bdca-13a6-3d7f-23ca-bd8092b4355a,3865279746,11305891845050424804 +34d34c44-3561-d708-d9a9-16d9281fbb9b,3216307578,4834992728217093241 +8fa5137c-c2ef-8034-e377-be5031cec0cf,2225354569,1313298603763250521 +1bc3d128-a1f4-5d40-0029-c24442cbc9ab,269459829,11707609948918194922 +7fe2b9b0-4b2d-1193-01f9-3830dfe57ce7,4068098165,2718138927677243220 +609dafe2-242b-5fc6-da96-d34ceb1c863d,3984145645,5965077139760051498 +9de3cb0b-e61c-5ac1-ce5c-3ed6dbcd2d28,1340910574,6825583927213181882 +1b939c12-445f-eb9f-d9e5-532383c6fbc0,3702054893,5055601954046328897 +71405c6d-a917-c78f-71f9-106fda382e8a,4214709216,3927618758990023360 +095ecfd7-8601-4345-129a-94a5533e894f,1898544684,6882175043364725732 +1f88328c-e456-7661-e343-15683c20b307,288049854,14982458092682654049 +5cebcc82-139a-bb1b-b958-1538162ec705,893435042,10156963068163961586 +26cea074-01a1-9ca1-9668-7667b34e2fec,3643939158,1959918118129352558 +d48b3ac7-f0e2-376e-4e89-e3d9345fc66e,4152741632,15594388940363993198 +68f16213-5b11-7221-a908-377dd9a0f068,688000513,4648950393191736130 +0325a0ae-ac7e-d825-3d7e-f0e10ff86e85,2681961765,17618247199441254321 +82e6cb82-3ccd-7a87-5307-92d68cd4377d,1614485049,7830720969112708906 +f694f90f-1305-7f2d-0bd6-0209e97c8c80,1683623453,5268003761356436262 +73251424-116b-08fb-0e2d-f9535bd86bc0,3599946941,8393853763448143512 +624604d5-b2f2-e27c-9a9b-ac41f737cb9a,3208595765,11671078843282478394 +a0349c8d-7a4c-7f78-7ec4-ab6abd5267e5,2601219930,6358624553699642462 +9890aa62-cd36-8a5c-b84c-6970e7ae539d,648868150,3673242233155772106 +ae5936e6-adab-7845-73de-6edadea643c4,2911218493,7823982698212319966 +a6ee16dc-e45c-bad1-e7a0-991a8885f35a,4148687701,340994090137038037 +8e20d411-f501-551a-fd25-b5ae6e24c3d6,2783636005,781275068716856012 +051c1ae1-c224-27ab-b0c1-18fe8c03b684,3330299543,5891574103436072959 +6f0868f1-36f9-efc8-a419-255daa56b920,4223340690,18326073180332817275 +ee4b9c09-eeca-f6e7-58f2-1cc2da9f43a0,1280449545,16207605644548935080 +6e4e4a96-c8a6-ca50-6389-ce6a80794409,1020310338,458209947260756232 +a3a619d9-c280-e6fc-9414-ebd848deb063,2979811151,13501284321946815843 +5c812f64-524f-eaff-cf43-264e306d31b2,3627810454,3382008135590842813 +346c0cd0-7d7c-83e0-8a47-851ffa853641,2906768506,11153377593563442831 +3e022160-9130-4ba1-6316-95a4cf3f1f32,286139685,1143377431902693861 +b2c95739-9505-00bb-3171-522703be8a62,1189924794,13597175526889560311 +3b9c8307-801f-326c-1896-09cab3cb4349,558346707,8304616792980513540 +290776ff-d870-d532-076f-d3d1b3d8b354,517692258,14856549531848254551 +f34c9623-c3e1-914d-c0ff-be0979667f01,3773811410,14125061334911171693 +ac94c93c-8acd-05f2-2182-bd4f8543fdba,654988361,3704323448320643682 +ae0794e7-e509-8ef8-7873-f64e41405a3d,2061085068,4085022553115462428 +1dc52c2c-bd95-f817-2108-7d7d22c6f46d,62245221,2230348464883320560 +97d3b40f-2144-fc42-d3d2-c88bfb4fa9f9,533871048,13136661582389628023 +a1c1ec14-ae1e-ad62-739b-defca2726374,2469640382,13266596723133589374 +e87c3fbc-abe8-0861-3d78-35f36bfd4739,336731154,18029686593179997297 +833a1c2c-d7d1-3696-67e6-06a1e52f92f1,3629725017,12028666552651215594 +19c03051-7199-602e-e7c7-0747f9d3dbb4,3291553659,4745786660931617644 +43830732-2f00-7708-5713-26687d348c4c,1314981615,4092711058454764020 +36720d77-5c6b-ab9a-ba85-0609f011a6d4,3462800679,1819927655147044207 +5823010d-e6f7-fc28-8dcf-96f5abb42d72,4025982816,18211140543826413769 +750de76b-5e5c-d436-5225-bd17dbbbdd1c,3183684352,1855567960809811259 +ee325c29-9e9a-c43b-c9fc-d87d4a51a0f1,723007312,12296522638695696555 +9c10f849-b012-553f-594b-463cfea7a302,2157506063,14588090133179681204 +2a1f7512-f5a9-0e5f-e946-e421b3bc9b58,328563933,5182023623011484605 +8cf820a4-ef85-6762-875b-b566da294f7f,3877442037,2840095054653553721 +ec815871-8a5e-eba9-2966-5ac4df706828,378872948,1318943716568381798 +8b537f84-86fa-cb2c-d28d-a7ebf1b36567,950649796,14135340513520706530 +1ffee795-0b9a-925b-060d-9400eac215a7,1401944871,15938931826955870975 +3e71a83f-d88c-2ad8-c740-0cce470c671e,4238245925,14818243395001682635 +cdfa907c-e731-774a-1b42-88769d17e618,3640746774,15271042014120110775 +1567afd0-39ad-ac3d-a05f-db0f85afdba9,3705359272,3755904479548849347 +b31856d2-efa7-db38-fa9b-406152a40d9a,1258814594,4429603773051871628 +7b19a3b1-8fea-cad6-9ad1-819bdec1c865,3466069115,15406784878141980178 +6539206a-2b7b-8e54-ae75-a6c6ba4303b1,4189735354,5990831529687863203 +8c412cb5-b30d-b944-a1fa-998bd3b7fe60,2956671095,15312487523412238726 +10442734-11a1-1798-53a0-6551fbbbd070,2596468060,1066189952937913171 +245345ce-97fa-e01f-5cdd-f1c622004e9d,3202152796,15217492838813616274 +16861777-3e1c-30e4-19b0-97bc8ed14ec5,1155490989,17477182265035486867 +fa4b0db9-384a-d25b-14f9-d9c6703fd8aa,2061430317,11286082105396100285 +140ec904-5270-b3d6-6135-cad4250f4103,558101830,4021572313449285723 +4b0fd889-5e8d-f320-5d71-ffc6c5fed81f,3819929461,3464504945397589021 +7dcb39d0-bafb-7e61-0109-cf45767bae95,2084965463,9500616677926062309 +e7eb1920-c501-d7dd-30a7-dd10b375b343,1181679572,18135771317146183789 +ef0fbc21-e939-73af-1837-6d4bb75eb0a4,1338194057,4674559943039660451 +43067e13-3d10-98a9-0c45-92a64125c706,3796548986,4311651789733818853 +74a63034-26df-bf8c-821d-d9837a4dd8c7,1880797258,7564999985367032687 +cf6090a4-70d2-e1cd-98ea-4dfee7a671e9,3848541403,8300262782975127465 +a55a12ec-d89d-8d74-99b5-f37798521bc2,532617538,1355579226442101046 +c2c4148c-b6b7-9dc3-f397-4b45af40b39b,1464133175,16787580766393055151 +54cc8a90-d8a7-62f1-8b69-871208b47763,2425274825,14233316980595212770 +84224dc3-ffa0-3354-7a73-61d03ef02237,171317442,6465736539932178209 +3ec026bf-0c33-6953-0bc2-23ed43e9c684,3977215845,2566820501695186860 +de38f557-6526-2c96-2592-10c1d85129fd,3073641006,15634927564013655622 +2b092928-41c6-f599-5712-440a28e69846,1467105169,6723129269430594005 +a8a75dc5-47a3-10b9-7f45-56c32b09ef94,2788367024,5179893921001132255 +f087c7a5-4d12-2a87-2bfc-ad3af2d13ff5,3663135428,3839625638730504094 +3ba10ae2-3f5b-ac36-495f-ffdd578f6365,138456791,13340302967495904040 +246152b4-aa22-c1c7-b09c-0af82f79534a,2876551181,7163705922421162336 +bf11544a-2c81-125b-fd19-eda82755957f,197764010,5783869793889404141 +a55f721d-bc81-ec8d-4f02-1ffafbe71f74,4095103782,4211944951951496213 +c28ca4de-a760-206b-4276-f3fc8ace6aff,3348744858,6561827648297198456 +cebe0869-461c-d105-a774-a2a80df8e378,309549243,6730429263866435235 +8b0b7a4c-41f0-ec7c-137a-762615ecb3af,3041887364,13795699895976294727 +aa56c577-8ea8-e5a4-8668-28dd1616ec92,1543080296,13986296288724889820 +64fe6cb7-f6a3-e005-2782-c69a5aa7c487,1270293324,8096110917717550642 +7096c8f8-9e22-702e-f81d-fcf1b0875586,912051566,16495697281517197809 +f1238245-b20d-949d-0a8a-e41ecf9314d2,1642289889,14373938257921876330 +4dd04cf5-38bd-37c4-1f94-24e3ba181ca2,1741032790,11218643495787039264 +a151ca67-7a3f-6c30-ce04-aa73936e3bdc,2623966205,14083265505471510887 +d4e35929-4904-9847-f5e7-4deb36cf31ef,1202432561,9086352422919112022 +3cb5be85-a08a-1c96-b142-f1ceab30e6c8,569910874,6613402864413060238 +63445964-2182-d60f-1931-c379373837a8,1910434763,18227019356737041234 +e8dd63ca-ba5b-8af8-788c-c6dee0bdcd29,1618185208,14374996398521634779 +bb96b615-fb81-1dca-3914-504ef52dacd5,3089604964,10663923817043869170 +b837aa3f-bd63-4495-2344-717c965d1927,983315520,12877952638571575869 +dc21cfd1-4263-f931-cbc9-9549631a6e36,989724906,1856311543781061611 +5a2a6711-413b-747b-9d19-9cd95ffaebf2,2288981085,1005644440760276842 +bf394695-9b9e-6bb1-e315-4ebd6b998a35,162407569,12185583794262820827 +693fb8db-a1d2-2973-11cc-c37fb198adc4,1385276362,4631342495039090150 +3cd9a1dc-c3dc-50df-71dc-6db3f35566e0,3075917150,11757677791723463394 +225da96a-9415-2d73-25b2-7de2a46855c2,2516032219,13892388057137549634 +e3d8c049-df66-17ab-d1ca-b427857232fe,716306382,14571466078720462981 +e66f670d-2568-f533-01cf-9be5c0e15f98,2953042795,15476751340636884832 +79564b81-26b1-68d3-0ae7-cfcc16df19cd,1921528193,10780006190780267815 +787ff13d-f367-c9fa-7e71-ab513a6ddc85,2744381704,17508101443707810539 +88e51491-ea81-98c7-a4a0-7889963597af,30196210,12613743769472750676 +9805d4ce-a522-426f-ffd7-743ca7f53cbb,2549276778,1450913729778167332 +9b389e76-9d93-8f39-a80b-3b3e9a6cd630,556109022,8957711457003968934 +e311b705-148a-824e-28d6-594c35619b06,1693065366,4696393042740640012 +36439ef8-497f-cc34-244a-4c54a2086e9b,1789851875,412958396587637148 +cfa3c276-13d7-3159-a82e-0a457aa05dd6,2309823675,13722232821615033397 +11b8390b-611c-c8c8-eba6-44b76ea128de,1245795476,8998323247808798476 +6234430f-b225-2884-0592-f4db813be2ed,1865087282,15013870550330807946 +0bc99c69-186c-84cb-8165-c7989b79c0e4,4217340861,10525430975356133377 +a3104425-919c-aafb-983b-a3d867a50961,4275693104,15385668982845643182 +03b720e2-a745-a909-9d44-685e72d345a2,2307704149,9973007328872344295 +fe76cb72-b7ac-5d41-f5b5-48d72a1bac18,2490073711,7693503292849299595 +f690f926-98be-6d6a-91bf-0e1c88b9605d,2598981383,6377103854393576581 +7c7cafc4-f7b4-19ae-cc4e-9527e07ed3a5,3312047033,14666733225370270872 +488613c3-ee66-3a20-5d05-32e399237d49,2850140717,5096426048245491246 +c33efcdf-9d9f-463e-921f-e056fe2c8afa,2509240099,14788177804586494263 +f23cb5aa-2e22-e026-ff6b-5178141b142d,1283300007,15700815428623875772 +3f11dabe-8a54-0f3b-2a28-d67c1a091374,2436409774,9106136142329602488 +9a8f1e8a-2718-124e-6b29-7f0e0c26942c,557145755,1754308856398149889 +e760e20f-8408-fb8a-9b1a-51fe2a7a498d,3371053649,4825372385551832211 +621aed5d-62dc-8d59-8b3e-212f5e49f21b,2774466092,8066397839673137588 +8eee2fa0-b264-14c5-f24b-c490603410c8,2534666175,1540857083248625364 +bf9b331a-c432-a403-0c6c-90f96c858fa8,2609897046,9169827555676275648 +4bc9ca85-97b7-96a4-ceec-337779158a26,2688674375,10727130869144029781 +415a0a51-66de-e04b-1fc0-53e6a031e0b4,2888460580,1313430909043028442 +c72c36d2-ec89-f4dd-da7e-7f85b5d8b25c,366680588,10346542689377623910 +f396d4d5-f232-70c2-9551-86f4e5c1795b,751540806,3579495599669795848 +d605b5e9-b505-0dbe-bd7f-4095fddf8296,463836500,12059701148053215323 +53a7f8b4-8c7e-11ab-4695-42a18f7099a2,1410676678,4485849894549506281 +966b54fe-4b57-f85c-a295-c7bfeba31da9,753104206,17685910532508363222 +79ebdc83-58d6-bac9-c4b8-39dfa835fb06,4293452638,4844239011864102461 +d1c3b056-991c-2a3b-5b29-cc233057ade7,2508505370,475041537334361082 +cbf34f5c-e1ee-d101-f9e6-9b6d73b1a579,2546397547,3858727886214628628 +a00b4604-b380-3df5-1ddc-0406cb821d1e,3153711089,215160824104618122 +3105dc35-4beb-ba3d-db3d-2498190391cf,724638691,9679832943317875838 +e0a3bccb-0784-aaa5-14f1-168e4d67e9ec,913538158,13375551107844324769 +87670d8d-adf5-cc16-2359-429fd941fdac,1645819045,12531729486155732096 +1bbc484e-f886-9e75-6b62-db131af2439e,833279377,1592567870744731079 +629d4235-c009-2f32-4c22-d8a3781fc836,1954054960,15762015255491717320 +cce2f199-8a56-ceea-96e0-b25250d66614,2742616184,1677846208952870429 +1a4bcb1e-9643-1880-72cb-334b2624524f,4150616081,3624425722619992462 +fee0477b-3c76-849e-ebbd-91bace511720,3629335323,15557381932525420524 +8e145d27-4a45-5590-b469-d4df131e620c,1278587191,2609088454936923604 +e0079367-00af-2920-8f11-fa80cfbb11af,2979069917,12261212268868039039 +bc099852-7ab6-23ce-cd7f-a3250e94678a,4012465246,17700561555647500121 +869add5a-12d8-5dd7-fc31-8fc5cea76684,3601354204,14902911966553412883 +b7931dd8-3c6b-e8f9-078b-563e4c191025,2285933299,7809329509939352323 +0f771dce-80fe-6447-8093-66ae800d8b21,1108256169,13160083945779751466 +20632f42-fe08-94ab-f1a8-817af08db490,1693894919,1512201181877000282 +0f73da82-323b-4252-c989-42e45ef649d9,1280863244,1207365220319040196 +cc98ad54-a6ad-aa94-8580-a689d5cf57f5,1282892151,8041560870981110739 +d39a5fc0-51e0-15fc-00da-e7f515d28fd9,2366044011,8048903540739444971 +669badbb-6dd8-f4d7-0478-a09d385ec2cc,3532864978,4420613784126523018 +2bf8148c-0b36-8899-d013-c522ae0d8b93,313548835,3505453016751859721 +206c7b7f-af65-a2eb-e393-4e70c0875711,3234587984,9212423651454707089 +48700c08-de8c-86e7-8004-665ec3c33c84,510283216,4561717439523100004 +cb8d33e6-7b1b-007e-3efa-9320cf30d0c2,4115093631,5634010544841244947 +4b3ea52d-f7c7-64c8-6871-852c746f7190,3192169283,10343192774946404519 +64c862d9-c821-eec0-327f-cb6afae07f5f,806512730,9624684654713116278 +0a814a0a-7bb6-bdc9-a376-17f39d77caaa,2884072772,11837123728295914578 +584cb85d-bc51-24d2-0ab9-35ad73dcec60,4199700674,11681535469518730042 +aadc38b3-d7b0-8bf6-a390-d4e4f36022de,3828068289,14834719414157567744 +54c0bf51-8c50-256d-086c-c41aca87b66d,826765935,17388053439445730776 +906f2dea-073e-8522-e1f0-cc6b2bb05dc6,1787821543,17227609286409119231 +358c2bb1-120d-7784-240e-53ba79966647,1580658477,8038583583842064661 +342ecdb6-8924-fc52-1437-775887c5f801,1021841168,3070865679432973732 +f85c50e1-e72b-7d15-3a8f-d2f1281fadaa,3108993730,9413113025398955071 +310e0cfb-2876-7cde-5ffa-6e4887b42c92,901050251,2019322656643335837 +3780e4f4-f7e9-ee61-b329-f8c5cd650b0c,117508406,7781341410949899197 +43ce6666-5328-9d11-cc91-0cd8bc57e190,556260734,5405542428319827577 +43273d31-d004-060c-dd9c-473fea96e298,2895318164,15943309838555484120 +7f003917-35e5-14f0-9252-9f14164195cb,1526710363,15997031524883722721 +232bb564-a4c8-70c2-b50d-424029c9dad3,3736608607,14484700472872918745 +dfc69547-5bd2-ea95-0df4-aafa96b78120,1013519258,10182314547970662875 +66dc9f95-f8c8-7010-f8fa-b7aa3ab55465,1312739999,14613081893302103638 +fc517dcb-af23-1720-a16f-260d6a59163a,2023190527,10043746689306686993 +7c6dece6-7246-772e-6e07-22bec3c36922,3715561451,1161125237607745607 +4ed98519-1e88-8454-6ca0-391649faebc6,2833570757,6779331662092628227 +70996730-4e51-0e75-fe85-7297c7bb4750,2732112851,15543497768344028085 +4561c3b8-3317-12fd-35f3-56e9ad857281,1164210897,16713583709404071876 +78779fc7-bae3-5a21-60aa-bb4a41018651,2044620959,15222899095108665795 +126e9054-2156-29d5-4bb4-c57f3109d1f8,4003870514,16032842944747068161 +2bab5f30-35f6-4f30-c627-5381910c082f,1321566410,522044954534120386 +cbb792c5-0bb8-c167-da84-86999925f8cd,2798339643,510603485023200980 +ec89f956-f14d-9888-ef52-88a331723564,3112853758,12679771200410201995 +18552369-6e78-d0e4-1275-9bddbe5f3782,137912589,1969176041890670425 +70cadd52-2089-d7d1-f857-977ceb44ad51,2891363248,3201087282174774918 +da9a1c87-3179-6acc-7e19-c689c01c97aa,2618905586,7598527713632684133 +7596e66d-0654-ca4e-4b12-94ec6a44ae17,3296884174,10451050244510913822 +e39f24e7-ec19-094d-9ee8-29a18a233ca2,980414283,9827871731900419714 +73f8835f-6d42-2a13-4be3-8903e34a4107,2925937838,16258945627342961972 +bdd27ae8-6b54-22dd-1604-bd889a6747de,684690578,6974608895526619811 +1b6d5ba2-c5f5-5bf8-e1d6-d21c926b2519,1856336905,16271059109329273464 +1795528d-e31d-77f7-8619-c307a5ed2a36,3262637017,5600455642021126491 +48dd6f48-37e2-965f-6609-c244f3ac8b6e,3879491695,14061079269536970123 +af9ee2c0-e271-9123-1062-7401a838aec2,2926119047,13975915230578567418 +f248752d-025f-1b4b-6f58-cd5e3d0ea4ba,1117646542,1719813106966567722 +d26aa213-7829-4b5b-c3a3-5bc9fba5b088,1920768912,8955489524307522051 +6f497a8a-65c4-98e0-ea21-a8820d990e48,2712066982,3852104464648898351 +06270646-afb9-bbb8-0f7f-688002122a0a,1453114860,1365138860121689838 +018f1533-aec3-865c-2f35-7438e6415225,1648321252,208291872237606512 +f873e65e-930d-ad4d-5379-6ed0b75277e6,281488993,6042101401633158082 +29c58d3b-86e4-84ab-a590-0a4837304a59,626747305,13149490102441492993 +ae1b2436-76b2-8abe-1fa8-e7443345a5a3,2843272390,4939596825361400790 +bda6e1dd-ede5-084a-790d-06fac017a7aa,1500073115,728862937819754423 +a64b2eaa-e5f6-f9a6-6c4c-2c068af4c72d,327265583,1246588290582278745 +85ac6201-f24f-7f9b-6bec-093bb18d281a,2719440571,12522329184960256907 +9777723a-08e2-9edf-7ce9-3b806fac76bc,2593079028,11244108801292320493 +4d6dfa5a-ad1d-b3f9-ec56-e0cc01fe77ce,2279751470,1846800331399395096 +c5a6130d-3680-3972-f84c-086e1248180e,1050967099,13625399764612230564 +3fd54412-df02-3b4d-dd02-a80ae1a41803,2844808498,8981811504087766249 +369bfd43-5cc7-db40-4758-f35e5d7a772b,1162638585,6794452243594838016 +c19b52f1-83a2-5739-23f8-8bf67b8f31cb,2884814509,11315855357654731632 +2e12cdd9-ba20-4179-f277-2beb7fb0a55a,1515874666,17965452909469885563 +5db42ee1-0df9-aab2-db7d-77de9b0c0ae7,548470744,4218223188432428873 +b9b8cbbd-e677-fed3-9869-1c46e76c3b1f,1302773039,3789512891671707099 +468be492-b70d-c6db-34a4-8eb7463dd5d6,2444452985,14959838390905492579 +8da67862-156f-cfae-6122-26f63bc97895,417670648,17424732800818167187 +443d8bd1-e1b3-b766-7bee-c03391cabf52,3192552806,1886593660219796704 +845dba9b-2ba8-c631-b9ef-8855721ad5ac,1070132567,5363400325890486747 +31789f1e-674f-bfea-0470-65aa4f019509,2603483781,7796965899042898325 +3e09a388-44c3-a5ef-9c46-e551ff1e31aa,216523042,4059876673381567404 +ad4bcf1d-14bd-dd17-cf5f-c707a1ab3695,1139272775,8748215523433067141 +511148bb-affd-2f06-f12f-30812933a42b,886016061,17652580849762844393 +933fd401-033f-a208-9ec1-4a3a8d0d18af,4091511222,7289234495245957981 +22ef7928-3304-137e-5d95-f191b6cdc2ba,2852255963,3001027170413449106 +7460283d-d179-1ff3-612f-6a4c5ebfa7d0,3919732936,9163211259562518060 +dd5c0f7f-4395-6625-d476-06a71487389e,4144863219,8073293150133438441 +9deb4d93-eec3-dd88-e966-9cca5f0fa94a,2638480260,3534672104743390146 +019ceef8-0297-8a08-dfd2-4955727f5930,3698337077,6346986143144215441 +332ddc27-595e-e0e6-5be6-23f42dceb401,503481093,9231095850371299595 +c1fa9378-5a3a-9b1b-1153-5dc5e6a256c1,3835545725,3375672802041265090 +e347e84e-6f89-c1c7-faa3-586c1500944d,3974476110,717523154698047503 +7a0385cc-bd72-dc48-9157-91b6900db43b,1272133635,6766729898500655280 +acade086-1b76-6641-5bf4-2dc024b904e6,3693805918,3387832744792875901 +dc48e3b0-fc7c-ac14-df25-7765d7bf5032,3823868297,6364563008291380043 +017c2dcf-f6ec-dfd7-5e50-10b73172d056,1987212304,2681251192441328214 +408bd9ec-eba5-7909-ca86-fc97a310d84a,1760772463,12134417260580846538 +7e652fbf-f1b0-f430-ab18-bc4fbc67e056,2577651510,3332616022372850645 +720c64c4-3e32-fa36-e7fe-1b584e68ef81,3798267017,17238674026308964686 +98153b36-7040-7428-6c1a-1ca2e6abb158,2795041727,1976191072135164866 +9b3419fd-9d1f-ca7d-bfab-ea2875480914,2215024170,16811221779857124168 +1ce8eccb-e549-2b11-c634-fb781bb91114,238609262,15731315510587247726 +09eadc39-3680-9fbb-ba81-a5797a23909c,4154316391,14388893827503359828 +880331ec-50e5-a46c-5a69-af67f25c7183,2306065672,17567337241663467936 +4f14bf34-ca4a-c0f6-8fb1-9697576382d0,3976547312,14114295417602137274 +a449cfa7-7dca-116d-2e25-8a86fa2f7d79,2642115589,15492986642205345953 +cd39bf43-6983-8539-33a9-f6883da9af2c,791327949,14407101512154995233 +bf16c36b-872d-ce02-d3a3-860b9e776811,3772322959,6347360606268912448 +96311d97-b942-d5e8-fa88-ffbb5e07e989,4107664764,5536735990313999235 +5b401864-e747-f8c1-7e43-fd7b7fee693c,4141460204,3355453478964255719 +d76786e9-6956-6293-ba8d-a2a15d44e20c,3989724478,16129098207627004232 +190f2a7d-305f-7310-3bc0-91dfcbab890a,4126872892,13102259907843568962 +44ed0204-90fa-0643-c97f-d0040fd099fb,2654288009,2508567166672262786 +3fbd641e-6be4-9f12-f15d-8e617dfa4a9d,1661192427,211609670268511604 +caf41f95-092d-139f-1dcb-e15e0e031612,666355209,16675381050717445218 +476030a5-d063-a252-dac5-5a7512e64a4a,1043780023,5337666532466152265 +3de73a0f-aaa1-8019-6328-55e87236aa1f,1317999763,2209234046952068004 +f235b48b-7002-36ac-d95e-5cdf45747c0b,789565806,7023339036306000269 +e8472379-574c-f13a-d673-e93b21d7aca9,137309151,18159085788199189355 +78a7ec40-c968-b0f8-a181-8d94bb1dcbff,2702867996,16244920913025101106 +d4d34ab7-0d6b-7579-c1a7-9fc0f79d04a1,2785970913,3610597777779920258 +43666bd2-fbc7-5abf-f9dd-8884feadab88,1495037583,2799856830733023546 +6c51f24d-752e-af9c-49e4-2bee2f1e2c84,223221035,16466638136739591849 +6ce15da5-e90b-56cc-8a99-7cd06fe067dd,3732880450,15575511401370853239 +5f1e0c63-2acd-d2de-d8e4-5cb368b5388f,1835744980,13167180099844930924 +8430a30e-7cde-bf77-f244-44558adc50ef,117717360,11369967582411225671 +56a36576-faa4-4f13-6053-53646e3e076b,3454177227,13481942802748119874 +fde6c9b9-b777-7105-7ecd-c7abd6cf5773,1680408753,3486487062285298584 +eb53c5eb-0e65-3c5f-bdc3-f8c57f65e186,2821807670,5527073690146441771 +26cd3425-4e63-298f-c36b-285438e0694b,3187445400,12968711647324162178 +8a859773-be59-dd37-5e02-2107051bc0ff,1524532294,8757329493117480531 +752614f9-4bb4-f9a5-0b29-689ac52bc9fa,2232825993,3698782829106858131 +9ec9ac8d-5dad-f0c6-ab26-4abfe26a3747,3344113611,6264479684650348651 +e3eda39d-1d91-75e2-1065-70a0bed2fddc,573244908,15933132810752256128 +4d2196bd-3507-4f6c-d6d9-26c59666b3c0,2687934351,890633742010285933 +77615ca1-6fb9-e951-5e8b-cddf2ff0e54c,3576398348,6502975659104762914 +42e9c268-e392-6320-e326-97b691305b3f,825522842,17068601356359757038 +8263ac69-ae83-7e9e-cb6e-674ad6024a7c,261240956,6365383976774621720 +9b020e32-9c37-5ecd-d66e-0432726a204f,3460555071,10444545106675172538 +a429dad1-6bc7-26a8-9cd0-fdf206c91f25,3702145320,15515168082587663325 +7391b0c9-1e9b-5d55-2380-edaf454af42d,409444298,17361645319420347468 +dfb5f6b5-2b39-6679-aede-5278dc8a72c5,2856596538,7548128512860398498 +a2ea5dd6-bfa3-603c-87f0-e3245caa6309,2266819084,7912338706304133150 +a1d4e860-be1f-1303-84f4-8bd748a7ce8a,509006221,12388398693029686583 +a382e854-5c91-2136-0f00-680514493fc4,2284390106,10948238250549200545 +0d1d9db3-8a87-d653-c9b0-c1026a049d74,2310313865,9020053091297308211 +a4d4bad9-1c53-d52a-d747-e3b332b35d78,4138014369,7014946825176138215 +07cea1af-38b5-a627-f354-42064cbb6758,81327103,6661538402045947820 +01934a00-c4a8-dd10-3ecd-e80ddf28a0eb,3103215474,10072653254630657973 +f4668f33-8636-6cd4-40df-70c4b4b30236,606757062,10800811823893674653 +fc1467ed-5ddc-e5fa-afd1-5042af47795f,3160316156,7362848328226417339 +a2266980-228d-d36e-b304-88360d7667e3,3608010199,11238433029865358309 +900f2c90-83d4-8c13-db32-cdb66e433ea4,4053062056,9762167777580046215 +cd5cada6-16f3-c00b-d570-eb04a906c207,3654313951,18310815005129602184 +d33053f7-0b42-d968-6e95-8c4632e012d9,2263581889,9022143092178042785 +dd642519-103b-b39d-aeb1-607bf7d02f18,706752842,11996344289315814609 +62188454-3cb8-dd65-2281-214c370271e4,1799230962,8291757153978231100 +ed66807c-2527-c477-02b2-d0a6a5deb9e2,908869221,9931781375033378765 +f00e57d7-64f2-2466-a089-889c4cec7c00,3134054634,16997746702604744484 +2c753b50-e831-08e4-dd72-fe2de979ca99,238713446,7850187609404482896 +9b08ff37-355d-5469-3759-c19da651afcd,2182878559,5714643146062243379 +4456cbeb-23eb-f0cb-ddad-0ad86866f7b2,4093017101,6873058906676463383 +d7058ba5-4730-00f4-c0bf-88941dd6f6c8,1357585346,14555357828522110297 +2b11096d-c9ac-67c8-72e4-a0bee816ec92,4095849009,12715050167001155077 +b4d25544-7271-38b3-e14b-dd0885a595bc,932217346,14074319590934021256 +187766de-2758-7caf-c528-74c2e92b4604,1619597366,15764247941021205681 +b6f41b19-67ed-25a7-935a-de1a3c7b1c40,1515041433,1921503078259070527 +81a47fd2-5a4b-4c60-c09f-90316c4a6a46,4226913677,14133594206603452501 +8c93f783-34cc-418f-70bf-2348a3894d74,3109933541,1505566779788102692 +de2be234-0a43-ccab-a5f1-73d670d20f26,207408244,15140714870398116453 +00b04d5c-4cc2-4f77-a874-7fcd477991c6,1718739027,14388956631169183976 +7a847b77-85e7-378f-9477-e0dc1c7ecb21,3197330467,18177714934380401120 +f126d33b-c5bb-5a65-d91f-5a71897ab3b0,1390380700,3668384330653632207 +05a5cd5d-3f70-742e-0e06-b43ab9124c78,3142806887,5461179161826303307 +c2f36527-972b-bfe5-ed0e-d52ffa3262bf,3267416786,16029506713511296214 +4de2caad-dc55-fc85-b2fe-a5c91334ba1f,539976099,2185643907278616338 +ec470985-8023-c799-ee30-4714c28d4109,1311441664,8827940504921444720 +2c1d9e42-ccd4-a8eb-97e0-4a947bf93f68,2889284507,3304609670358973291 +cb149035-5f89-3972-0bb5-b9bdaed8a446,3004487602,13969930310026579022 +1e38ed6c-aaac-91cb-f7e3-0ff61067238a,175593947,9535734730407474774 +44c35601-a3e4-f2e1-921f-b2b040b585fc,3779150225,17651441681766377037 +f0c5d591-7915-33e1-c94d-7c5a15700d7c,341439646,17107280064636979221 +a481b3de-3be1-8d8a-8be5-2557fe5749f4,3580479330,4919609136445118591 +a998fd87-b66f-a51d-2411-ae1e919e7d8f,3170399167,12378955433350447982 +7a680ed6-af80-4799-4ab2-bd4589281814,2617377954,6898480774876701205 +8d492d7b-7b0f-fb2a-0ea6-d17fa9d5819c,342657115,5664947935796234934 +7d00e2d6-eb65-24c8-8f70-d9b7eabec169,1210315166,8327137274736195210 +88ceac63-c6af-c04b-9a33-f6bb5fd4e86f,436531561,7569563634393733718 +c78db4b3-3c9f-2946-f251-f21104d9032f,3611302985,6289218558472970903 +0b9c0b41-5176-8ac3-3aa9-4023db30fbf3,1315280586,6116820604566461385 +4281ba04-d1b4-d374-9730-84c176cbc498,4173741520,18417010670147902145 +9aff55c6-24fc-26f1-ffa3-f081e8182b13,1927311485,9014477899896563187 +8dccfd95-d9aa-fe60-ed28-e08f1217bd3f,1459362969,10997454165672817501 +14362c3d-baa4-1201-8b6f-514eaceb59f3,1976230313,8715725017126099108 +addbb3c4-8391-8892-dcd0-8b94756a06c2,4045656713,1714618865410551502 +6d67eee3-eb25-3319-24f2-9c90bfd4bdd4,3137019583,14482251762672918906 +81663252-570d-6794-bfd8-b67ec1dd895f,3713641495,3550496078051401915 +9946e606-0937-62ef-3d95-1e56483c224b,2809969454,2078011230085425478 +984b2755-e35a-90b7-b848-7743b22d41d6,1000875649,6002015000384019849 +25eec7cc-ec8f-a127-6b63-23e6f48bddd0,3367697871,668841401149061967 +6714ec4c-8e9d-2bcf-db6d-15f94ae36597,3208352422,17705127659925831444 +1b8c751d-7da6-285e-5424-a5d45e40bbfb,857239131,15812804258223431153 +6d694213-9d04-3aad-e0ee-6a777495c895,1439624400,15717708325047198650 +2410a75f-d2d2-91df-eb70-cf8c126b68a5,2663365806,1487526138061286326 +198f132c-2368-a9b0-a22e-60b43dd2bcd9,1292422175,4424299899234945001 +49d34ce7-2fa5-1c5a-4884-10bd341e3201,3530835305,9419009535339892871 +a4e47f72-299e-4ce1-c971-11805ceb5daa,2824233386,3265644464308853760 +c7281281-116e-091b-a936-5aea1966bbd6,638475635,14019054422774230347 +ca02f92f-cc78-6b88-fba3-15e47a783530,4247075801,15346358610086193839 +78155933-2eb0-57f9-7a31-1cdec18ae43e,3369085899,6741433931618477412 +338f634a-b4d1-94ca-d4be-5d168590caa6,2346216374,4578954480540493852 +38cfcb19-12e2-f677-9561-d7aff0121416,160097285,5445737863421135755 +b092f52e-18df-d618-00b2-d8bd9dfe87fc,1288255477,5901168629695458994 +73378385-c4f6-c5d5-5751-e90905bd43c7,875436243,9863335071498050383 +417af79c-d728-0902-0b4e-b4e892238f4c,1173753113,2066881959158620430 +880db50e-6fa5-16f5-7556-991186892814,2942617556,17527002514148533019 +9842e495-a2da-0e88-f7c9-797c6abb15fd,2306442635,3884737667751856064 +34cce1a7-5d0d-4edb-da2c-e7864922005b,35482644,6627536081875956594 +12de6995-4fa8-4d31-9645-929cda228b07,685024437,17229649358876501401 +680c5bbf-ed71-7c7b-a06a-baf7a99227b4,214434349,12655020296629633555 +47c3b577-4b94-ce0b-e6cd-e7f2a0a21f1f,685362074,2171395186595634207 +fa0c58b6-1d23-979b-4b91-ab8388cc5583,1239265757,15786245230530825895 +b4b13846-12e0-52e6-c5af-5ead0f2bfd68,3962284966,12613103628724382218 +28965c09-cb7a-3f62-4132-108d9b5dfee7,1185510728,7478852599318647023 +61f22023-6c4c-d32c-1024-07975f34ba78,1530938504,15308295134210129145 +05f4f366-e8a1-125f-ae0b-b126f377ff9a,2034427732,2813879305002372659 +2f925a57-74f6-0bec-d910-2e7c522c32a7,1137074070,9220965785585027467 +ae445cce-3380-20f9-6b4b-45ab1cd05aec,1711123347,4166870366533240482 +63847a17-55ef-3772-f545-cf65d1793033,3532711950,9331939466270917570 +06afb0c2-c313-1628-1700-20901f4cb8f9,2866080629,4488455626952316726 +0ee08033-d590-0666-1e2c-92607663ca3f,2391830498,17013557871696337206 +687ce50b-074d-1c96-05b1-450e6341613f,3115097713,13031996425709214584 +ce8fc3ed-06fd-6b3f-d97e-18c4a45ded42,3989291408,3866310837633078633 +f79cd2f2-07df-33ad-37fe-bfc883c28c64,4053281394,17745840291709330261 +9af665a0-0090-8c2c-0247-af32dfa9fac6,2175721499,3440464917684769995 +df1faea0-06f1-5da5-4959-5a913bb94b0c,3872133476,17065461805153976555 +b163725c-a5c9-970b-6e29-eb00a65be27d,992320941,10626185913943839840 +cd3621af-a4d1-26a7-967e-74d79c860fb8,3767140770,3396407876976900238 +49f98550-71e7-a0c4-8808-26f0eb3729f0,4196946983,16436667613982947287 +908c7839-b06b-4ae0-c1e4-e1127facde7d,322440504,2038780540823423729 +76ecae0a-f915-5d8e-9f53-1df332d882af,1773979716,1924961608317425604 +bbc619df-4e48-3365-3ebb-2eab67f62ace,2846343051,2600404595341662505 +6b995065-a669-65a4-01f0-1ec529ec6067,807829525,9932474657375510757 +21b5f18f-aab0-2aa5-dbf2-068cf33b2d34,2685252035,9091789925926517632 +b439ebbf-b03a-393f-53fa-971ea1ff4c10,1817692669,13841014523935667350 +916e3998-2577-1cd9-ec37-c671b14f2c3a,3814744188,11116635033828130533 +c6263780-b49e-bcb9-796a-118cbc0b7a3e,3259618058,8442648514533661857 +910e9cb3-637b-b84d-fd2f-e4c029d83143,309019199,17440141150469589536 +079c4f48-f986-e6d5-5d71-bae0f4633637,3244707364,17381996405934143902 +26e27449-aedc-5d0c-5021-ef45b6ffe78e,2407382332,15498760246315926308 +f8c3f652-a489-532c-a78e-cc62cfed2a0d,592535212,6011205942082945436 +930279f3-8585-12f0-079b-65689afb1906,52089778,6807491831740339492 +c33e17b5-76af-1467-8b42-ed98d64d6dd5,462805792,5325473794377896117 +ae45a901-213d-73ec-442c-e9dec3dd8c01,3525807516,8824497594256284493 +c14baec2-1a5b-4157-b63b-2c4af19b601b,3663035716,13269075636559524338 +2cd0b901-79e7-c356-fbae-7877c909a643,1396674866,3926533485811156612 +dd0f6f4a-b838-a04a-8ed9-ac032d191a3d,502578764,15202643941952295720 +3082ee18-9260-4286-b635-b703c63ad42a,3744333899,8456985014368593274 +4ee5ef1a-13d1-74ca-87ed-0dbe238e10cd,1632896971,3274254686274794995 +098aa39e-41bc-9987-0d2b-aa38ea064b0f,566531268,6298761586365903483 +71ee3ed4-7aa7-d5ee-2d1b-9875fb323fec,1164211659,527874733332077195 +bfeae4a3-b103-0ea7-6877-799af62bbe3c,3600306787,9265643208465683495 +f59606d1-6dad-7c54-64f4-243bb48978f2,1150785509,12558884826479375319 +7c2ba3bf-4408-43d0-4e23-195b375b073d,343475971,4585594246448916160 +6c6049b5-d783-3f0d-5ac3-9d8d1909163e,3655205920,9848848637389579295 +2cb36549-dcdb-3dbe-9229-f9876d50f5c8,3208911966,9525575334371614714 +6a97bd97-3f8e-cda5-f525-925a4f9ab81a,3866671998,15142600204059914519 +e4002743-593d-c394-a3f1-010ec9e34e3f,4243504783,12831186551338893289 +6626b058-4a24-4ba0-7adc-8ca7d8a1d0d2,2970387196,8225442259046612434 +eb308909-4ea5-88ed-fa17-2ee70bdea1bf,1652099165,9602931364957167868 +b673859a-b097-f84b-7a20-87c1b88479b6,2870702751,17696224428216587024 +212c9ec9-5142-8d44-5b63-0af462968190,3123669381,13907088062362137944 +6f7014fa-b05e-563e-71ba-9c34f2cff1cd,1894272551,5113494720940663138 +05492a31-a661-1821-60f0-4c9fa84e3825,3162154345,10728674908455162539 +90a6c58f-3680-1b9b-32a7-aebbf5da5e00,4042651846,810142759726376869 +f7d48651-4b75-0d6f-d80a-bab4b3e4ba8d,1422507644,4444289758788740262 +005188e1-a1d1-707d-90df-44756a01acdb,2968227173,6972996801100196328 +209358f7-e968-12db-56ba-2609da649363,2184836713,12139436370180292942 +afb87660-9479-0d8f-5e04-5eca37e66a75,4158602170,5423356714792169150 +dd995b98-0b99-843a-6650-786064e3b185,71564355,15591449647340503480 +0abe2961-23c2-0849-046a-6df99455429f,2821686732,13342912352697107893 +7d89b9d6-cfaa-8a56-e537-e11895cbcc97,342162686,7926794207743896390 +6b5d36ee-d215-4bbc-c589-46acddfb84a1,3361649813,6880345546262965940 +b5e93cfa-b67c-5256-2df6-c7db764809f7,229763953,14316605058536215889 +4ca9f171-0991-20a2-249a-e82f7d49063f,1389178931,14211632024553101640 +b60b0184-f5b7-0acd-12ab-9a0695671277,2167822856,9257712212446350479 +eae86c75-24c9-5f37-b020-cb9cd3d33dfc,2165066767,17143606971777177706 +a3c610f5-2c54-a516-92d8-f81c7355da6e,1907774778,10411805621803723370 +9c6306d1-a978-0918-fd62-e39162d44b08,2196136727,9480800268546712532 +74ccef9c-1ed8-c61c-ee35-2adf73869a5c,3810561382,18160109141091956324 +0ca08472-cda6-476d-57a5-6d6672e35710,1069373056,2871402142739623837 +b467fcb9-9e21-ca0e-da25-05fff17a34f0,4073911664,13975475910055812085 +97ecf0a1-80a0-658e-3c97-a4170988f009,1345364230,4982784354780347837 +59606e8f-4c65-8134-7df7-ac779eb95367,1109966975,6084416427332595425 +c0c1f2b1-600a-342c-e648-83414069e4cb,1908336010,14495419196942259235 +9ba6ece9-5a42-e630-e685-566e187fa892,3122244448,1848646216952192657 +47f91792-5a2f-0f79-3a7c-fb6140034fb1,72101486,7822638078548539997 +bf2fb8d2-c792-4308-448d-7a08b9e79a5d,2354449769,13982693763456437522 +3aee70db-00b6-d904-d545-7463996440ea,1672801981,10857413657025301265 +4e634c78-8bc4-277e-ca9e-261ae40de8a4,2259390796,12495105232022113324 +f55ff794-6570-5d5d-baec-5e9b69c665e6,4041015300,1014413498939230254 +6d521413-c419-f5e9-98b5-0ddf24974920,2227543615,9964097057255024558 +2c98ec3d-404d-2691-8966-e78c16cc3ec4,2656887750,6874952506881710296 +16e268e4-b6cd-9f57-00e1-4a3a2a02bab5,872652151,12633161437783858865 +993d2124-1a9c-2790-3278-54536555f274,1165786328,16303648499695824561 +58442654-12dc-8d57-937e-3cd218f52f3e,3031739070,6530410593934149689 +e99fc8b3-2654-0b63-eb6a-56b6fdab2da4,1544009222,2014909510979795970 +277c4e38-469e-d01e-f746-53b06ca750a5,3785827766,11555499574522140912 +499b3c0b-5374-1335-6a6c-0da1a1134dcd,107937575,16452775797764240905 +256c560c-35f4-7f24-de53-4c0f21da698d,809706824,12239560067070268596 +6d9ae067-6f64-f82e-72ef-546166a03e86,20310959,13425457696765853655 +577cce5f-9d68-1439-7c0c-576212e9358c,4005386402,620284251110928379 +e24e3915-e020-3bd2-a32c-7b46d56a4b54,391482449,11327196425265401120 +843cdc67-b3e4-eafb-6bb9-d6ffc666b19c,3694826678,15017143447569424730 +2a602618-da8d-7ce3-ead4-cf4b062de1e4,2221592309,2620146761746058370 +d79f52e0-afee-6382-3a9b-41325c15b3a4,129539186,4464988786533960498 +255b91a3-c223-26df-3bc4-6fe0ce5af720,3184713486,16131883340612434107 +dfa6ac7d-6baa-eaa3-c3de-88769f16cce5,2102900228,14813465524077300994 +49abb1a4-d1d2-677b-1268-59f3e290c054,4094042092,17627049264546790621 +d3627f6e-27a0-f647-3968-378af72cafec,802822522,1209217971557619544 +b04a6f09-ecbe-828b-74ad-694295f63396,816514977,6291801542859298646 +9018ec32-bb00-63f5-bcd2-f8aef819dee4,2085167954,4536826087160532795 +76b941bc-5199-2f11-3562-0021943d849f,2817746183,13660638614769632984 +5a502ec3-7bdc-30d1-e28e-6e3b7898e200,876172069,485545074326914984 +ea82e93a-087b-906e-3fb6-74d4e980abfb,4038376697,6638230193333866472 +3c366577-c079-abe2-b3d2-9ba8b4f5d82f,3117118032,9397043914641040954 +a8fa9bcb-1730-6c47-22c0-8a2d89a664a0,3011114562,6309260670441818831 +4acbb81c-7ad8-131a-5614-59d52c2cf91e,2670437786,10396809891702287664 +f4a08359-42cb-91c3-9e0d-80dd82965618,2993209539,9840766886001542688 +0245e19c-d088-cc13-e8e2-7a3d264a2e94,737600060,18323926703569556239 +e525ef29-b443-e2d0-9992-1019b8bf8e41,2919393148,11806978850103672400 +49d18986-af93-4aff-ae1e-073aee7cba83,2506567930,2646181604809590813 +6645a949-56dd-4e46-b577-42d33c8823a6,1777066746,16873592068656234593 +64a40560-bfc3-e3a7-50db-a3c39cf6c30b,2378654284,71459500169794438 +72bed3c9-d8a7-dc8c-2f08-fdb501e94894,112050909,12138460593780200271 +97c3a500-e0c4-c24a-5aa6-0e1d42505988,2185743530,5017444936110409663 +e8c62196-feea-0935-399b-2f128816d770,1252663023,9968564238497565441 +df2cb19e-73d5-d3ca-10d9-56949371276d,228829721,12668306255061467950 +beeddc65-6254-ad69-7bbf-764d15e9b841,1927153157,5824436826563733689 +32c60073-1aa9-e359-3e93-9f90c23aeba7,2299814377,13583451679414633561 +55049130-ee60-f4da-fefd-be2900605d9b,1406326403,412995768625655084 +f038e1c0-c131-928b-e82d-7ac60f623b27,913245406,17684338511237104360 +be191f31-bd29-dbcb-3f1c-9d749842800b,2562768237,18327233669670425177 +670531ff-5ce0-56ca-8a8e-834935028612,826968439,10566515936683299550 +6e0773b2-388e-66a9-94a8-516fc3b8776d,2640166771,5529255540465358919 +00685bee-8e11-9f89-5059-f4c601a8b8db,1448526705,14111005538049050804 +7f99374a-b4e0-9230-fc2c-2a40692af2f2,880947795,5305125988215390433 +59e50ccc-3b63-e9b8-fe7e-7e671ad05723,50221086,8928422397615598253 +1c8f39b2-f57c-8dbb-d935-f7cdc1206bc8,3639169686,4994858049764023741 +a0eaafb7-7b59-828c-eb69-76a7026f830c,2514338113,3815092592659507515 +f3561422-e57d-b468-0750-c8802c49cdc5,3917250325,3440134893020180617 +0f7dc9d5-634d-587a-be50-ebddb96c28df,291442512,2650783492068126477 +1aa61cf6-1a41-3e3d-88f6-fab7b94c6266,2335306427,3932579897675625 +9541904b-b53e-2763-e85c-7f2665142a7e,3608896407,338257872109636384 +abef3483-977c-5569-9363-2a181adbe6ea,1317527951,9222305216742285013 +b9109f28-84c4-a147-b04f-013124c67cdd,330696490,9415732981607461212 +f2e3fe45-e9af-ab4b-7a72-f04ef85e2f3c,1113130519,15785986300915153633 +42b6bfb4-64fa-eb59-336b-d8cffca641f5,1554066278,5339090436038317284 +b0c82365-2773-2e79-2687-ddea34332b21,507070858,18126639162463391459 +2a6527af-1edf-009a-aa7c-fb2c3b9a18ca,763316477,8576285962322375399 +ff8c3889-c519-e272-174e-6f7831cd08af,2938259577,17104605209992281863 +fce0f22e-4b04-bd13-6750-a44e0786840a,3330377828,13820885368225350589 +c23ddd38-7009-8996-6f80-f4b71c0cf179,3648011591,17781644212951765218 +b9b72d30-9784-6a42-725e-8764d0fa4f4b,1978114702,12618370121554338674 +0575b62b-2072-6b4c-8454-231a6729c5c8,1908650793,10191884418329526201 +9351879f-070b-4200-9874-427dfa8f69e0,4058781008,11305300374658059428 +69657583-54a0-ca36-ecaf-9142a7ef02a2,2710604997,18027679255515195972 +7ede2918-674e-b07e-9b59-7ec7384ca8ec,2366509757,10065433815729335225 +8cc7c615-c104-919c-5e26-dbaba25f5aac,465566108,2604080744418810032 +d3343840-cfdd-2aed-5130-440104fb5f98,3016064490,8677983247274326532 +fe5a82a1-b514-ec36-2867-fd4c72f1e9ab,395484533,2014659297119638286 +989090df-d2a1-f185-6d31-bd06a7771c84,2806481920,5526458470686816407 +5fae0b87-ced5-102f-1a80-8ad9f4fa6783,3858744514,17139528934003266148 +cd4ab346-32cd-8bbb-4621-aecfeb9be186,671183120,10441844607957248356 +d775588e-59c9-9cff-056d-bce1d95b071a,2039425535,4163400725519742424 +783705ad-c8bd-eeac-8a53-56e0c2251937,3905621770,13432095399609988137 +c10d0c3d-75ef-dc62-6983-f5b42dbf5350,315496227,7066141508980964411 +3c811855-e9ef-516e-303c-aee783cdae6b,3615108706,5817944648308515538 +cc2450af-4819-8a72-14aa-4d3d7b1569a5,1677684969,13116613702929820232 +0594b391-181d-cd89-7800-092cb20214ec,441131406,17466093575922789923 +9bdf4378-ce47-d8d0-3238-03b9a5596835,2451020882,10978373766447334998 +27d6233a-8497-e4ad-986f-6142a8fc58fe,4153912507,4438813867223617649 +eb50c61c-164d-cdd6-2a71-5e38a12ae613,45296972,8996450494148750691 +de1aee67-519c-1410-ab59-5b902b45479d,3235406096,1497593197460448720 +0aa8ba36-cfb2-3d97-e8ad-cbb5451e0a26,4281273695,8055057895754747185 +8f54ed83-f965-3064-cf0b-ce1fdb87b6f0,1208877385,9476386506616989254 +28e25822-9ee0-2cc1-3caf-691eaf3c3928,3590432554,10093556970165090239 +15f45dcb-1795-2824-f513-04cc5dd25072,3300694571,6205740015015445969 +580b120c-19dd-5e30-fe14-a72a04c2d9d6,362525362,9262248029382684790 +ea638c44-d2d5-f666-6637-1843e30d9557,1409313112,13702532528678625379 +49f9f7bb-4357-f30e-719b-1dc0abfd1010,3426396285,9287113720950438062 +6ddce07f-d161-92fc-38be-2c4919f7bdae,3709909325,5388149057073086381 +ee48254f-3635-5de7-56d7-a8063224a52f,2220555456,12262930699794613149 +2868a8d3-10ba-6ec5-597f-858e6bb74434,888382745,17005330675678884208 +cb51a019-7f18-5b92-d4be-8b764cee34b0,230902817,4234091354852517510 +56286d23-d1e3-c5aa-9d05-49b56b5de3c6,2853092757,13422300946352051514 +453c61d5-1ef1-9a14-4f79-dde67fcc88aa,3444785185,10867771232019823389 +904d44b1-988e-bab2-4483-b19147c4edaa,4155513040,14332724571069884969 +d7def96c-bbe9-5bc3-47e7-5bc1d35a7099,1333364368,16818267026494788928 +b686de84-96d1-e351-2cbd-b367a0064dc0,1007878610,16920773638792963198 +c2347ea0-e83e-ed07-8623-2d7e3181f438,69918147,13975465860855287967 +13f6ef64-1f4a-1b14-06fc-0d9c0de0b343,1528084876,2910757367131736945 +703b0371-e877-dfdc-fd52-53df9e8273be,3346719589,1852152632351716172 +8e28f9af-ca94-786e-c536-b3106f83fc60,4217755443,2105246026351715978 +97f08217-776f-a4b6-27b5-f031b70a4f2d,1043248692,1299803272403419880 +2d8bcc94-ab20-5ff6-906e-5e29cb95da87,2493795743,18179509326786104123 +78ea3941-b030-4ffb-cf51-7a8da1fb18ed,1271696759,9780749784246049210 +f374a96c-a0e3-af1d-1f65-3f7c22c31548,1750393877,9235275246668340710 +7c8d3e5d-bc9e-8b15-3448-6de19914b3ee,1363813024,9760997969939887949 +92818359-1ee5-18c0-c371-aaa2d9c564b5,1118111075,17957601197383392783 +f6b0c000-0892-2adb-aff9-4af46b66b80b,3984324163,6485932622517129684 +3e3e2db1-e97c-5266-8046-f9f07132be0a,1169985952,4892530655889170532 +47883686-fa40-1eab-2193-38b754a23b83,1910823819,12839253040549572036 +944cece6-7a21-368a-74d8-fb651c67805a,3435201945,507453859261454732 +1bf4164f-db8f-e12e-995a-38b7b54a635b,136396740,1015876883643831770 +93d5e2a8-e95b-488b-8007-0cfb7f4f10a5,471451839,14955476975742888978 +a06ea51a-3b2d-ad59-c6b4-330e65af8303,1010412381,219373823328957821 +462d7c3a-b5c3-c685-b4d2-b93ec0e9d37a,1220393695,17439176180989365670 +46b9098a-5240-1bc7-6b43-7d22e09766bc,3054922094,15896308686014428829 +f54c338e-a534-0516-8277-adf705fb2087,646278864,16885144688938350336 +06e608ed-e685-adca-3b3c-eca2dcc7198b,3183302911,12572569840653250974 +340bffe7-8ee1-29c0-8459-7b125108fd22,1748266454,13675570478217805251 +8c9d137d-b95c-b877-a377-9dec7413ae5c,3034596498,12783539958768776502 +30f0885f-9845-8609-53a8-cc0572e1a789,1112901113,14375218638103833891 +0d99ffc4-2187-1183-e34d-da6e8499bbe7,2988522175,11973795633591666728 +2d5d2e66-a9bc-6892-622c-da611ff0f081,2536410850,7985699200305506868 +4ba3003c-a97f-a370-491f-9457cc8c6004,1993613393,17597352555779026703 +172d79cc-deb8-d4b8-5dd1-879f0a580df7,3380357481,6155063304681176466 +6045c1b5-5a34-5536-cf8b-ffc725b57a5f,3527537380,7008011507583793160 +0e798f57-cebd-e5b1-1ec1-7819d09241ea,552190371,11635857732953868244 +e85b9c55-43d6-b105-63f5-5c3fe38590dd,1581765789,11523019850369382877 +a7f8a9cf-dad4-92c6-dd73-0818051f91f3,689115667,13598435220691676648 +7535e81c-1d93-79ea-5da7-626c54288e43,3171336222,13367035156453272729 +5c364dd6-5325-c89a-3a48-f5903893d60e,2807275982,3317107409391401395 +cee0f8a7-0b37-47d0-3186-319084ccfc05,904314443,13291097411285481338 +719486f8-2b0a-6912-6e02-a32b43d69406,3927506392,16472435723884082358 +b99701c4-91bd-7ccd-fe90-db749a98bdfd,351372925,11024275193001084714 +17d0fec9-9212-48ba-c623-5844c73a447b,1298150681,2920328964526209807 +f059f335-fd97-2999-aa50-68b0798b4557,335024921,3470750173940750087 +cbaba429-85d5-d2eb-64d2-a00045244446,2101132354,13734535807805241005 +08fa9ede-7312-1918-6c4e-7c673c55851a,3611194239,5525519064977418341 +22da4b55-957a-d918-d36e-27abe2524794,1216660668,8777572550604125399 +aba20801-ddba-8873-6863-2f0c4a430d37,3715795324,15054429530675590485 +2943f21b-48b9-b6b9-5055-a8f8c61fb394,3035165716,9821542549367773193 +efe6e9bb-e2ff-51cf-0610-01e93fec607d,873710397,5015451686520306540 +7a3218ff-60b3-3d68-a8c2-db04a7c51618,1921882958,9548354612934384141 +7c3c1a0f-162d-2abb-058f-f70de6885b3a,571482123,16865469961276882395 +098c0d13-2b53-96e5-0f10-f8030948c835,3451915896,1029639841142716594 +7bce9a1f-1472-cee2-5937-91da6056ada1,807689380,9675674554040486205 +2e45ca34-664d-4441-73f7-f1fb62a5096a,2379631361,9043525946375434388 +ff35b806-a61b-9f91-9b25-3ca03adba935,4165107462,13751690815886672365 +391c9170-ba91-0b2d-ec16-8db7f1e6b191,2874109978,6800117320167361051 +71fdedaf-a0f9-11c2-3126-0f8bf2fa065f,2487675387,5281577408321223564 +fc406a5d-2f26-be94-bf23-ce0c8d339f74,105353592,15007024084112360426 +cf0a1853-af21-925f-cd56-b4c0d50697c2,2029645771,5448239496081327643 +ac0fc7d3-c773-2a54-c55d-50c156ef4e32,981268628,3180040762617069860 +ff9adf96-b205-9990-5dc3-fec038b69616,3873445740,11775886672809154500 +d87e0162-fa7c-8e03-ab00-4676e06fcc65,3231758332,13886650104977485710 +14dd4c4c-a689-136b-59c7-06efadb09016,524181194,14867326225767451213 +2e02c715-c629-8b6e-f8aa-a138b85a30aa,264733244,17750233963963780614 +df42f078-7463-a5cd-2838-977b1a681d42,2941792668,10654738024618715070 +b9fddf03-4d43-7861-3dbb-a44a9903f5cb,897305967,1806866727355277815 +629112e4-4986-6382-fe5f-ec79c87c2f64,3285503818,14777528568652201736 +915c0396-11c5-c8ef-99bc-81db1fe79140,4091078239,18143227640120282098 +052ed015-22ee-fd8b-fbca-ac7ad54f910a,3929401801,7005001378582359852 +ed8089a1-78fa-781b-e47e-3f43ae10bbd8,14333266,5030497463516779518 +a235ceb3-4653-6bf7-0609-9eb259c4276b,3859969929,12079301018678641615 +e2dccacc-b4c1-edb0-d71f-b1c6520c09fd,2515086458,4732633058237338206 +0f407dbf-5879-a392-a70b-28f836018a1b,2356351800,17562633511005659056 +90e27c82-6601-57aa-3a41-00b49a7220e2,1225467902,9799588085825711171 +df35e7f6-c0ac-f497-b77f-687138cce167,2485160648,16544752407953661642 +9f094b06-2e4c-f12c-e296-53f7bbdbab4d,2042298590,6671075500519606982 +aa103b56-97e4-dfcc-40dc-ad64c5b30a8e,4069170192,2826607138816115524 +3a03fff0-70f7-e79a-c726-d7f4274a76ca,4226979590,17661209019336716265 +46ef8aa6-5085-63f0-cf9f-baae0e7788bd,212597413,5683744569707520013 +5ab25e6c-6251-70f9-7141-52579c6df978,2891693241,6851198599442350694 +b7cc4c49-c29c-2cd7-74cb-ffdde0a7929c,853146,2600150917173301712 +452f3540-2b61-1db7-2e9b-f4efaeced521,2898901353,1949127518588523595 +518f1c94-c066-4636-953f-9ffccd7eecbb,541754885,11517169129179668461 +577b243e-9555-9c3a-70d8-02e0a99cc661,2302461002,12360012632774311402 +baa49c10-f6ac-8ae0-a0e6-305dae8f8ab9,3035648770,13632893994055632912 +37c562d5-9f1f-1cc7-32fd-ad658b7a7a04,1384731886,5212583308630781669 +6479069a-31e5-f986-c8b1-ca4d195f23f6,4278966709,5627785718207240850 +1869e91f-b5e0-adce-f52e-21f154cf281a,1882076775,6966178983685336167 +62c4db2f-be3e-855a-8b91-94f20e6c6049,1637359248,14897450964164579532 +75ab2a1d-62ba-00a2-0d8b-172f5ab6160b,2420484466,7650281882495446941 +9ecd18d2-cc33-b913-8449-a964f380594c,3582120223,1235991600278404879 +885195cd-1f8a-c977-7db8-c4f86b8f2fba,2810929881,6999779426502065249 +ed2337a2-e2f1-db10-f8a1-97e8f30b290d,3851823029,15388574796283971859 +0a504578-3d47-8116-f318-acc5db3f7533,4251999729,18205005850789420360 +a752523d-5966-0e57-61a5-953b09a5714e,1137379591,12742306289039317426 +e9c1470d-d6a4-99a2-d4d0-658abd311cbb,3972620910,14455926069418560669 +35c22851-b7bc-8207-9b5d-f0e339ab8782,1909453205,15431334687956466126 +f89c794f-ef01-e8cb-03e9-e1a3ddfa7e3e,2504823047,16604312681887283733 +cfa70e2c-aa46-7842-8f75-ce7e123dbb44,546155688,17829069730455484399 +120c6df1-ccbc-dc24-e938-6bc0fc3e1bf3,1265397380,13359772750044190309 +a0de70a5-3e16-96dc-a71a-a3adf4968a35,3759324721,16196398922620658894 +43a3aba6-fbcf-63a6-f5b4-621bf359670c,1018237376,1039897970498851722 +4ceac2e4-8d72-0102-ecbf-481f4dadf8c5,1519847273,5459772321718802846 +da514642-f448-b9fa-4ff1-0e7dcd091bff,2316176040,6367074348864267064 +2aa0481c-f7cd-2a16-3d2c-7c3bd22b4706,723362392,10050227017176527668 +fae8218c-9369-11f9-4536-d26d7ae6572e,1968368602,12624065617602358933 +adb1edea-b850-e376-1775-9880734eb55c,3205551042,17614999840605266795 +dd08c8c6-19a2-cf8d-c90e-57e9fbd78eeb,1118916999,14494438296999192115 +8e266e86-e341-bf4e-6d1d-6d2a54dfb002,3209508257,5595236174657683366 +120e845e-51de-e997-afc7-07d41a2237bf,1404842331,3457643232976879941 +ee4c0e24-9b81-5caa-2558-bcda2a0452e4,1122907290,17606211489367049800 +8a568ad3-839a-b2b5-627a-8c552951fdc5,2205079939,11337931531174896947 +73def876-f752-bd7e-fee6-08cf92ea0bf9,1712104989,3511413043066970526 +02b6026c-8952-435d-1b82-5f35115d8711,384938164,14808222568672019635 +e9c09435-578b-d534-da5d-29feee985745,947587506,17801622268839362691 +ad0cc4b8-f93f-3e58-aed5-0017c6ad5625,1777745599,17489214497546075903 +f8f00049-d6d4-2083-7523-92157718f89a,104371523,2363792353822368371 +21a14ad6-0daf-afe9-788b-62d42ed7c4df,3508280099,10704130986436751193 +9599c034-0122-10a2-b579-8ec10778cdf3,3100987133,13248679016013043823 +7a5af1e5-43cc-a23f-1dfc-bac54a964627,2161562599,14860127499714926223 +537d16c0-fe73-3608-61b1-900ac390a799,1591260484,13842530203984875412 +1ef21688-efc7-984e-5dc9-e0a851c844d2,1251155960,3675156260487123287 +0ea3a7d1-f987-f4da-accc-a730212edaf8,4157351067,17178715508709798277 +4cec7909-893b-8626-cf0a-41f96ab409ea,1173240988,15314796885536588758 +e53d2bd6-7659-97b5-04fc-ce6ad23eed60,1622549987,13477023595968533971 +b987f572-f556-ad54-aa71-d7c77848e8e1,3267805809,10600720557435221599 +7502da2b-b631-cab1-341b-64290e6685ea,3613306805,12639492880706309778 +d4d84088-513d-b5d9-c0a7-dd674a287dc1,3257089013,3524054441191036426 +292e09c5-ec9b-e134-7cd4-26858e79490b,3998210386,16607139293905638957 +67c7707b-2fb4-cecc-c5a7-77c8a5980801,1742456296,10905244996672594029 +610526e9-8afd-fbe2-6784-02c48bfa0612,1331995051,8794328614409274481 +ab189b1d-8fe6-d9ce-2e0d-ec1a71828a08,1386676965,13315077167130396533 +377b0acc-c63a-d6fb-1e72-6dbd94e2fb89,2989246697,18006063127472958239 +a409f711-2e9f-274d-37a2-c20421b8227f,82245576,16367452641268370523 +2477bbd4-da78-034b-dde8-7e8974da6b42,2086958478,10719628136055447095 +5bb9fd51-1b3c-ea0e-4ce3-b60b48231aa8,1034203234,6188895882946009978 +7e78a397-1e4e-7de2-d294-567fc4a756b1,2600926916,6612693570304958794 +13a7a9fa-23c2-a1b9-d7a5-b7dbf20ed061,4085181863,81960493336484159 +1d0974fd-f8b9-35b3-282c-2f2024605a41,2920417748,747145312343632393 +50d61fc4-5ef3-cee1-b29d-59e31bd9e5fc,1210900545,12354298894882645153 +e5c55edb-6adf-5a05-375d-4c6464549af8,1407408870,15501950290346269759 +6396183d-4346-054d-b161-d946fb8e15f3,1940207291,16607021751579300258 +29d825bf-4562-fe6f-a843-757f7a166f5b,699025516,1290145143115497432 +2f17b068-536c-63da-c1a1-db018c7a9ca2,3293904181,3147045893111335233 +d90f68b4-0e5f-93bd-640c-c85ca225f9d9,556667824,6334661075620201304 +f2f1e329-5065-030c-0cf6-6f835a3f4b03,3438419640,3485829572955624060 +72ad8080-608a-476d-d80a-2f4b52e7c853,2473502153,16808278371879825056 +ea24f90c-ed05-b1b1-b897-bd0eff497a5f,3304539658,17464783977136926165 +a1a9777e-8d12-3a62-71b8-6f3eaff223cd,4186225493,6252031165800376581 +b073984b-2f3a-c93d-f9cb-4662a2f7e2b9,463056627,6805994010285790257 +73b385d1-419f-8184-6735-b07d15d559d2,2717358309,2460242560818714790 +708fa066-2431-310a-4811-bbeb4dbf0967,3374257002,10406692865684101943 +4e2fd256-af51-729d-3b19-ef76e2b15daa,1498057943,6293550953484484823 +2e58b2df-79f7-c937-9162-ca158819332b,1603202155,7058502771029880279 +564d0e1f-a6dc-fed2-4758-9a33fa77709f,1165400214,6938627602294270318 +5f4227b8-923d-f06c-2d47-e1fe83dcf49f,2170744836,6412416829423412817 +cff1aee9-1802-fdad-a4a1-8ae63a69307e,3347200996,6076244311230099295 +a90874f2-a4c7-4bc4-d681-684111d9c835,646806876,8838001239948318771 +c9b1646c-6cbc-dec9-2bdc-30ebafcd4c6b,3496271788,5247938560435379255 +0d1d6ebe-f80f-f651-563c-caec60b63859,483067536,3741792692369407052 +f1db6af2-3d86-58cc-f325-07457628b905,1621242246,6679307963450174016 +e8ff4c89-f757-95aa-11a4-217531796137,1355261208,10004077064377481799 +6d5cc4ba-c83d-16ec-3e10-17de3dca0b06,3794346344,14829617232989847163 +d0599a83-a42c-0ec2-0924-f640baee9ccc,2558326707,6830039468687304457 +efb3b442-7df1-e777-318e-17ea4981061c,2500524260,8376773997467700812 +3b9452c0-7c96-0f86-7b35-70c5546cd1ea,2933739053,5992623142782409037 +e22d88d3-b3d2-438d-b828-2c45d4526fb8,1390336993,13055414110370516627 +a68bb911-6ea0-d5d8-5606-d47451c35b98,1541326293,7259049373451775971 +e0f87dd6-d5bc-d684-5738-5864040fe79e,3903416035,9042562902628957119 +39ce00f3-ffb6-8aa4-1077-54b1486ad6a1,2188499872,18339465280545437035 +4af29cfd-3cb3-5402-1289-0e44f4ebe757,2524828738,4611358379705646831 +192e5d48-bf25-fef0-2e1a-407f5e86d073,151681883,144959780371734738 +3c22bd80-cbd9-8416-d8d9-2ee554fe8a05,1326066255,8386723836005322806 +36e7bbb6-da3c-c6f0-6ea8-0ecb265e865d,3227407245,3889811519474857841 +9df675ca-027e-09a0-2994-57dbbfb46039,4245787721,16525993244601570203 +843aeb7a-f2a6-2689-34ae-2cde5145f9a8,2981112900,806885038795987039 +511cb1a5-2eb5-f564-4bf1-d052edd61ef8,2848282054,17483354444934043732 +85564748-f1a9-afd1-f823-2b19298d3319,621313143,6516405028381492577 +421ba78f-8dc3-407c-ac7a-9c8e43e1f2f1,1256028470,10598400416968408635 +c234a022-d7d6-60b7-97d8-e02690b40f20,4201413116,3957679989206280839 +0bec0ddb-5de3-2731-0256-f0189257f768,2686555227,2301880026058083296 +d8706356-5f9c-8dd1-0b6f-299168953ef0,2395333951,11471254055441843102 +f815be50-fb10-e3f5-0f4a-8a6e26cff32a,4293190703,5402837363436633749 +9f62185e-4206-06da-eee5-a25c0b8c4b30,1226202224,13485215557563802041 +54186172-625c-102c-0a27-f908a03684ae,151668573,17801267284859802150 +9e1d5f39-12dc-d54e-6915-581979bbb8fa,1757263626,5409821660525959234 +1bb3be17-0fdd-6a9f-5b10-3faedddf48b9,374342573,17799965765300910888 +d8463e19-a1c6-1f35-b061-6e79450f8c68,3420426445,11029905119081063558 +01fc6172-128f-e208-19dd-26d0f1592449,1299662858,3574924160330210955 +cc8470a0-6068-b925-8cfd-c815da5a7525,3325519033,177212846832562825 +da7c3bb0-89e1-de97-94b6-82568422ec24,632223371,14390728500088589958 +e1476d23-7f81-8d32-edd7-3fd3fcefec71,3062200030,14456371118168913412 +be222a51-7ebb-8f6c-6230-e6facd8a7d41,4219554348,7502719822466774564 +40fc0272-8b26-7316-a4ca-81ae3db979f9,1202938900,3945788810429061765 +ed9e1c9d-19e4-1300-7014-374df56ed15f,2919314061,3568063788868806333 +df152ab1-f891-f408-2a97-b45d6cb34de5,1885271982,14964701602784157210 +9a57bfa7-23bc-b469-6d7f-5f74ad32faeb,647235243,11765238867268498109 +60c8c689-092d-d6b0-bfdf-0c37323ce898,2979421930,13790410858125863220 +bc1788a9-26a4-44dc-3053-5d7d1b4a2b58,4094677379,16958902578996035563 +034f368d-8e46-189b-cb18-984be10be4d3,738217865,9950192898041164659 +0e822d67-2e1b-ada5-fb38-ea4b171dbbe7,1515292150,10774454477101790535 +347dcd9c-0b7c-fddd-d8d2-d7ad13935a5a,2707665914,18172976785899152545 +961fd46e-3381-508f-b632-f392e1207b5c,387273707,26019596371692515 +6146c991-6381-df9d-998b-a3e387a2aedc,1886678941,11564427889081516475 +4e1038a9-e32f-9644-13fe-d427784c34ce,1735594714,10066118508728761765 +9dc94f07-1e55-6e69-4740-11b5dfec0184,2522380106,3255722108880625844 +1c2eda4f-2b53-6fbe-8c81-d67cb3b0adb2,3310262202,9837663417646724932 +0733d0ca-623c-db8c-3874-a0151aede870,807072872,2742344891529183949 +1c822267-1d6f-5265-a1fe-1ecb9e353a03,1649926686,13114864968415063406 +9d113e00-1e87-8b6b-e358-d2b510cfe8a4,3903769069,2950589578469666215 +f5d15768-5ae0-6124-5694-c31048663e5b,514648685,4559846854488709224 +7b01f905-be42-bf43-54a2-403641e38c01,11610813,12006639463539492963 +4274d4d2-105f-5400-3e7b-7c29ea1c586f,1131967581,11948800390343606933 +d8ca6454-d16b-574e-d691-51d0a540a5ab,1895763853,1954768231262129742 +1002c568-7635-fdb6-cad3-b886c015a684,3293715233,11336270757145236405 +3c1fb398-505e-c238-c596-b0430bca73ae,3868724594,6932166609189931214 +90c9f3ca-79dc-5ab1-9712-36d01d9c6212,1707386957,5576369530667255161 +53ee9f4b-b66f-a16f-a5f6-135345cec5d6,1209587149,14259239678034987690 +034754f9-dfb9-31ab-5f11-b09957e15990,244628510,10202377006602256113 +e6641d28-ca98-39f2-2418-46c7963d2a5a,1264746684,9667411479387354138 +4ae998b2-9860-48a9-9a27-ba10e072a002,3285034478,1620311719996794029 +381d0c3f-e6eb-4b34-ed46-ca4abd240812,1244709771,1772399155463665091 +9b4d9bdd-ef1a-34bb-09ee-57bc05c8482b,1958780818,14222237982771917747 +bc49d439-9c97-033b-ba3b-7441222f274e,3098818716,13763293278541678343 +5975bbd8-8b79-cc96-2437-708a945a33be,2406578733,2577680825945336129 +8e7940d0-e166-4850-4745-f12496f4f7ff,3297677540,11963859725644104649 +48c20edb-5199-1f0f-a40e-ef3a1c568b61,527038797,14980396088576012388 +92a76bf5-9187-d3cc-75cc-b587e25d7bb6,893088922,4293651532058296532 +28826002-29b0-6efc-8c64-8c19ed863af4,2865014908,10310393926765813378 +34f01f3f-3632-4314-00da-e01ca7b843d5,46789204,17556709362499141429 +ba7a7fda-412a-f6dc-a68d-a4eda6f059cc,1245740114,18423240259111323500 +67b4cb57-9486-7203-3c34-0d8bd827363d,1393561036,4176812379194869638 +9993e29a-05ab-cc68-3b5e-662ec55dff6c,3839873011,4293625373901496268 +dfadb854-3ede-3d7c-a780-74a9a60f467c,709189771,13608696878400905754 +0ee9151d-d1c8-89c5-7988-6f63091c0800,3174452833,2091244698754056268 +b75d9f2c-e50e-f6ce-a791-432ca643d779,1106724715,13404818263217289284 +b3705432-68f6-2b07-55b1-199dc76267e2,4260289614,16674196089474276865 +bc7e3a40-2ecd-6c0f-37c4-30268cfe2e15,795245284,14276906749840078090 +a80b7bc6-5223-a5c7-2471-06f417354f20,1595312634,6789592962187029062 +4e8af3ab-c363-ab13-bb93-0e6ee45ab2ec,209097507,11600871057533252890 +62b57391-072d-f331-ea3e-73dd2cdea009,323538805,1764526358144974 +37e841d4-fa78-6b0e-c3be-08641b67ea2f,2920929108,2978117888448635073 +f8a2d265-7a8c-e2c3-8de1-9c099003e890,2628049392,11568944921344968581 +1b844e6d-53a0-8d6e-31bb-ac9af3927d00,588478145,17382185429284341207 +dda25312-a5a8-2478-e870-528a7d1069fb,650900899,10701458909380379830 +46825ac1-9b5f-fc94-f23b-875c939793cb,2682006482,14493731014378109471 +8abfe306-ecce-b283-b845-ad8a93697d6e,2250356551,14602533730102743331 +579bc8b9-8847-f34a-b18d-7d26f0e25c40,1204419125,9958364144726198178 +3a582287-baed-c249-0ef4-0c148730e08e,3469855965,13297008448778590281 +679cd738-dde7-cfa5-fc1d-a7175173f594,1139027039,12036578894694757106 +038c992c-64d8-9912-bd49-236d33b2b7bb,1945383152,13407409183014719372 +20f8bd28-87b2-0371-d7e2-0984eb7ba25b,2212496170,2946042807138997803 +bf2fc870-7de6-124a-5c13-61dcfba908b2,1513779844,3474048718270085035 +97f1c21d-46b8-95ed-d78d-0397a7493356,987873179,2797800127548030072 +cd549f8c-21e8-32d0-0fce-a833e96f9af8,1880311641,18401343267843107898 +110bf55d-c048-de24-bba4-4fafb3ce36d0,4084323781,18194301982192359689 +5109db8d-af39-5d4e-2240-2d835d13ce52,3989807842,6947848726749124015 +477dd2a2-497a-f715-20ac-3eaae3b102e4,965784651,13033603291111686339 +ac4e8465-4549-95be-c786-5e91789cecd0,311957186,3695307588530009550 +8aefc12e-ff8f-11cf-7f43-bd4e6ddda51b,582655033,17875263969607924334 +ecd581e4-3206-8bab-d650-31a0b6e5211e,684327399,9233075325004814897 +103efdad-3064-9f5c-9fa2-63ec8e1a80de,1230230114,2511195435098654811 +a64868fa-a496-b5dc-2f18-fea3c9ad563e,3606344497,15129888192635725085 +ee5dcc71-2146-0493-2443-3b274ca06291,2171804279,11611885166470685308 +445c5703-4b81-9609-5f47-d7f0f94e8794,4181923362,18121942309932617915 +b77af3ec-b380-3ab5-5622-9d0582911377,1439964312,14130888676262853242 +d268e990-50cf-f84c-73bb-da8a2778593b,3438062945,2050702544470251514 +10503709-3309-d4e1-9494-a52e1628d94b,2174596120,16971107517926760171 +add896dd-bb28-1906-cb76-f5b54e5ff201,2767968584,7041372677808631295 +efe7786a-7e79-b657-fa4f-e2c5e15d17f3,125974634,14596073058866436029 +93580d58-4213-dd24-8e4f-a652260bd8e5,3695606873,11320769403694829838 +badbdd92-7f84-c083-8ef7-bebee73e4b71,382049613,17718670239310667551 +22f2f99f-6010-bdb3-0e0d-b0ed3adb9725,2879174572,2163503595880709473 +25672540-1de3-30b2-40c1-5453320eea72,1287567984,10955159910571474637 +8766496f-cb7c-7acc-db08-2cc21e7cd35d,3794245370,1577545559559967840 +75c25d53-eb76-1f7b-85b7-fdb1c4bf8317,655852952,14911642371790484233 +bfbd8490-2bf2-e710-a387-8d2c262430fa,2455656839,3623341905164307255 +4c52317a-93c2-1484-7f5f-2cab2e3ea5f9,3218898311,15307752128002215327 +90951e46-cd2d-0bf8-14ba-fecaef97292e,2115661552,12755230297582056243 +79e51edb-16df-3a9e-dd2b-46392b5548c2,3228479610,4471552603139519565 +6981daa4-c9ed-3465-8e54-9700bff105d6,4027783525,9916844204487664260 +205addad-f795-d614-3fe6-6045c024a1e1,2096831231,7475396545727048411 +7a55ccda-ca85-14ac-6949-936381539b80,273975777,7472539465251825110 +2d3d3555-6360-d3ab-6cc6-d7d5c01a5aa7,1746241584,2976492577314786372 +3e0ab801-2e8c-0ced-d1d4-252b6224142f,1309015213,3506115323411606666 +637fea71-2aad-a127-bd68-45579fb6bbe0,2325714224,15642190984823207910 +64e234e7-cc3b-afa4-fe3c-1b4e8c4e5080,4159926219,14642994282519064799 +237415b0-03c9-ec57-3bf8-18db3b300e2d,2824370943,4155165231133572009 +242edb9c-1bd2-85c1-80e9-ed52a9726258,3237176061,247712835544409186 +c03a0cb0-dcc7-0440-b4dc-382b18349e14,204195902,6817749706497327220 +9c2b88d1-0d74-f486-d528-f3688ab2d334,118108653,6932133060205036161 +a72c2dc7-963e-488b-2e1f-15ffc27579d7,2360991577,11390192838309751389 +fcf8ff04-4e1a-6ba5-f81c-d90c56845c23,2345750618,7123442890771946705 +d9ce4302-b371-d44e-74c0-5d8c49419416,2946081560,2330136029488561855 +08afac2d-7954-f4e8-50b0-46525fdeedb2,841421834,10159212834148974280 +39be6972-14b7-4e47-a796-1cbbc90d9754,1196013337,147113461399249857 +49dfd1ae-83f4-5965-74db-ed5ddc9d47bd,2547501121,9520692605697848082 +ed725a80-f596-a1ec-53a9-81c67d1d2f54,1315398363,17919772207697505479 +806e10d0-7c3f-a5c8-e21e-c9559f45864a,807043505,13715399819102948279 +53cfe53a-63c1-247c-2d75-c174325700f7,896215001,10637214173599516320 +b18adc6e-4fc3-bc42-84b3-bc51f4593239,2266021338,17975934980144805921 +3168d43c-ec04-6acc-a1b8-04c74d5f3eb7,589143381,14732696471650396232 +3f16f520-3bee-954b-9b7f-49d9fbac0e2b,864471901,2960145437855172771 +242ee4a2-3627-0d69-8bfe-93b9a3cb70a0,1433917544,2874369268938234358 +c9534e4e-068f-9979-e646-f7dfafe72449,1398907060,5448620750984917942 +60620a3b-52a1-5768-ff18-0042525d7e70,4241692817,16359353957271629535 +37c4fb02-0bae-5fcc-4260-941639726725,1512083751,14629116100190964647 +a603fd07-74ad-c612-642a-543eeb6b61d8,2012917599,9871035739066630255 +ea4cc845-7583-6783-31ca-0b5b88e99a14,1523884216,18028134309348577288 +c1f3cc65-c39d-81a2-0b3d-a64a831d1767,1175591542,3719923735507695455 +c06f61b3-478f-3752-70af-bdd42741f49f,396100938,11828400800041849297 +a76b99ee-e79f-3427-03c7-2cbb1afe3136,2458694102,15140727255582088535 +32ac6211-7ad4-32df-8f77-d96251a38c1d,2244395134,108254753865588825 +4e200a90-2d0a-71f3-00cd-358b5da76385,3310211761,2804591009387416427 +8b670b5f-57cc-4527-dc3d-aa770eaed9d8,1526289125,9818355880392596505 +1a7202c6-47b6-d0f6-7538-ec0f93f15ef6,2183711319,4942343140431755697 +30a61678-67de-0c76-e38d-a1749f691d36,616758517,14897209549883160495 +b540e954-973d-94e7-86d1-7a4db1f0ffbc,3280445267,10089945750157769755 +32ae7372-bb32-1373-3dd6-3e66c51f0d17,3771902643,17334583779702700332 +bca03fa3-300d-43c7-c656-3890a0786adf,3058945413,865108280995478589 +a2bbd5da-606b-a099-e759-bc4d28410418,3088434388,1738580098754818044 +679016a5-02ad-88b5-ed4a-4acc8db76af0,4130304862,13423061674964423877 +b9ceef68-84c1-3d9a-82a2-e20d273d6927,449283063,6336658083931552754 +9f89b69f-ae58-700a-d779-de1ec8d39dbd,3875016725,13733482014321648692 +6b3a46ba-f62c-09d2-6928-56292aa2f144,1866785127,14368680473929370055 +f1587749-6560-da98-e918-6b33e9a003a0,2307199480,16641584463426310413 +86b826e6-4673-c001-717c-6f40e1c06c6d,354491566,11631490701851414508 +4fc3fe9b-68d3-9231-8269-51e7c9584ab1,3775300916,8202429676360404632 +7bd08cdb-e3ef-b7f3-2f72-561c2de5aa37,3309991745,12937271438602345632 +919a537b-1b60-5e89-35d7-1a90951a8c55,75566845,7793107450785955554 +fbb0807e-60c7-d95a-bfee-355dd861f8de,3082257535,13969015763047895200 +5a4f4633-9196-85e9-8803-cebd665506f8,3104183261,9921822588938810677 +a16a5cfe-3b8f-3bc7-3603-b002d9d72127,2015379010,16503281620783327299 +16443eb4-f947-150b-d6f4-303f0cdaf7ab,2673635808,7468178522519870857 +00b6967b-acf4-6258-2d65-0b32d2ed6338,738057368,941432642927301400 +9ac5c4f0-7a79-913d-85f2-8bfd53c63d49,2545172580,16780950558872195436 +855b8834-3b3b-c677-1ec0-afc48a343313,3908325354,17835029126399744970 +2db9e6fc-aeb9-c653-e521-c35c202d83de,3357478652,4224319442547310083 +0e5fab58-7212-210f-b2d0-a31c3df9665e,3083277861,4403058666614723203 +4839e5de-028a-0c65-e6df-180242bb38ed,651161474,5291110531623375038 +792da6bd-1935-5d48-0ecb-62b3a38be57d,1411499289,5583267652136695669 +783ec5ef-6fa6-0444-6a58-26abfa2d5681,4012572697,6104822226219344757 +5e9a65cc-b598-0ea4-1951-69cd42254a99,387740079,202466464979965038 +8bfb6237-270c-cea0-fb35-b55324884706,3881440197,11247838072603503734 +13f962af-a504-b14a-c2fb-1ba97e41b88d,3822052726,577959065012668975 +70b936de-a93f-e324-e244-76a51260eb2c,1789209847,492605675276202178 +3f1a5321-6691-a13d-8397-0e8ce6f357b8,688074797,5184723486184448608 +caef40d0-4544-7589-6901-e1b0de68a80c,1308399137,9468732851240070916 +116ded7e-57a0-45d7-4ec3-b543e3322c92,4281041164,17723793967040519846 +bfd4cd4d-62dd-a462-fd3c-8e3386f05181,3363572572,11654205706979290123 +bc8315d2-b525-004c-7f18-1048332f6587,192949368,9226212354181564852 +fa0542f0-adad-91b5-5b9e-bdc88696516d,1680651412,867868975206224745 +253e8a18-283e-a2b8-d572-ba817d1d4995,31399630,4107416397718906762 +1e75cf9b-2016-6021-937f-97823142b34b,3390780762,11302948529988330329 +8e4ba0f6-fcbf-3b73-0782-bae56663fab3,1153056046,14246769622346938421 +889868bb-b999-bf6e-ed8f-809bbd9b8a5c,832787158,4692315962918689166 +351603fd-a988-553c-313f-944181e17400,2356264903,718238084178830865 +adfc3686-d7cb-d622-acc7-fa40c6ec9103,1545389898,15680979425010310914 +2631d802-a6b1-c369-e89d-94e6e1a8c7be,3805786951,5126728372669312218 +9889f726-5f27-be5f-1c64-08dbe6a05a26,341095677,1902647976561222909 +9a8501ae-a8c3-2c78-8e7b-bd71f5bc3f2c,2647024307,6047868772370588894 +7b22c722-d7b0-26d7-f342-42fea09ff0f8,3862060867,172839894996364442 +4f459ab8-eb94-625d-5377-5152ce30d13e,455101233,5785979561615740054 +c0042cc7-a9d9-9885-b62f-68009d4d8133,1392623322,9380222560178231267 +c05187ca-d279-5981-073d-bfcf4aa001f3,1357737443,17786442354264560476 +a2a74d74-f4ac-038a-ec6f-3c1fc7245432,2571967826,11591496187019172654 +11b80625-c76e-80c4-32ea-eae51bb1add8,4114499095,16582932870255119189 +57243193-bb3e-86b1-656f-ea7e3d4c3614,2777258648,10796948709558658400 +b81732ac-aac2-19fe-787e-ffd298cc504f,1424915126,11113403741496155237 +0b9c8d51-8eeb-2dba-68e7-ca91f95d0420,2285416455,14904149457597727249 +9bde324c-c216-d511-db1e-7209a2fbd06f,4105438806,7867784007021146994 +ef63b722-87d1-c7cf-2548-948ccb75d66a,2280756856,4034946072150900428 +4cc869ab-978c-abee-d6ef-1dd29e8ac0f7,560105071,15532298126586098909 +b4732e3f-4bc5-027f-2265-4467ab01c654,3669525377,10306855237119999078 +cc7d5204-f2b5-f88d-5164-f083592dae1c,383775120,4305172286368386131 +31d4bb96-e687-0d64-baa5-11c2f6b54a10,3635856908,4055047810231250477 +260035bc-ee9d-f046-0d04-41dd1aaf69de,3339716371,11316128304392634409 +de95d5e1-f76d-416d-1c0f-28b0397acc36,2004494460,11091367855827522231 +586ac97e-caf5-1240-aa8c-3e3128a921e5,3128996674,17035667559018221122 +7d88b37e-c2af-4f6c-f9f4-b4d0cee3eae3,1632155067,7356340303427678754 +5657f2ae-2c52-fc65-da9d-425dc37bcad9,2674978162,15762306712514795101 +4714ee58-2832-d030-db87-988f59c19be2,3824110047,3727355645944219720 +0765cbc2-bdd1-a409-e791-7aea3922619c,2020033933,18201246656393879413 +fc8c7429-f445-b343-1264-f0d6c0247102,4052513391,5908947988107456483 +e47f2030-1414-0930-d6e4-9b7c61fef1e9,4294367948,12098919801326367351 +dd818dd7-5c91-7df6-065b-306a53a12428,3387064724,7127333986729980576 +5f9814f6-8fff-fef2-ed60-9e6a3180e4f4,3020466288,793717465487048389 +ca120e8f-043f-1901-23ef-214a2c845952,134502160,8769832189134489547 +704d4a70-a78d-2b0f-6310-5570696dc1ab,1063008957,13190103223664598007 +339bc169-a011-6ae5-282b-481470b0399a,1867856682,14899352896970554672 +0f3f8d74-f125-8425-aece-ad5311db583a,3747534660,2979200398790804287 +2a5b04a3-07e3-5dc4-9123-408f1178a10d,807373796,8830596332919938394 +bfd3f2e2-b458-c074-bc81-69bc535c6c3a,1966144318,6657349165762876210 +46260ecc-db29-0bb3-1fee-03ef3d270d03,3552281496,5267344669963557686 +edf4c55f-844d-ba5c-64f6-6cf9d2f50f70,3536624962,12889236262868968962 +e235e51b-c52d-e412-32c3-2e2416f4d4b6,694865206,14131022575220572150 +236d68ea-9dc6-c67d-a514-9f27b73189ff,2904397042,165986283059980242 +5088ce14-d2bd-c42e-8c58-338619fa1af0,2958093192,8692380400596257559 +198dbd5b-3dba-8043-fcc1-8d3c29269a5e,1886650386,2031331771445024366 +6a7f96d5-aa92-ad79-2783-130ae84990ed,1548541117,15912443775781177952 +796ca30f-0f25-c29c-3aa6-0d6b35d0ffda,3503324737,7629018937489297393 +94a4f0ef-e1dd-1754-aded-2ec832d81c25,4090299481,12299579533066521305 +0b01d1c5-810a-12be-ad10-4d3adbedb76e,2842846056,4511661276547802121 +c81b685d-f386-dc7d-9328-82bdf272f659,3992943665,12878411285386471719 +737b4d47-2f5e-28ad-dae5-22ffa9e86168,242479292,14282296458648550377 +989aa12e-4832-34ce-2a6a-dd7cbee61e54,3428291976,6060496008867015048 +828d361f-49b2-8096-7afa-8422f47a8db4,1115252525,7224417684321265159 +52e18b51-ac1d-2b22-94f7-8d1c5e731b36,568177765,1830229294393085908 +a2fd5ca8-e05d-587b-6a65-ee512424b374,1658343563,18093160018384555562 +02129371-38d2-4671-d90f-2dbb13c4182b,1921216642,10642207683215127750 +e84a6d83-ebed-edc0-e676-ce58a1a631a4,640597575,2964884589189683309 +375bb201-6c35-17b5-9640-c852e8d8e8d9,265804074,15107241925013818201 +041a29bc-f061-5b7c-c5a5-a3b1d799d181,2762763280,5119166057494922197 +4993d76e-f110-69d3-1bca-0ae528bcf33e,4213558318,14449684647434717702 +24fd9f97-3012-bc20-226f-ced53e2ac921,3457924257,16046278349631564770 +f2ad874b-83e6-700f-1d1b-10e69030d647,1230940099,5454209681371957588 +7027964a-a340-ff96-2ccb-a9deb12b7229,4079336997,12390209556755555043 +7189cd85-c286-2e58-0de6-7d980ad845c9,431847626,15490076178137896333 +ad21c0b8-95c0-bcdd-62be-ea4d631e8cbe,3602569361,17758242481027502881 +8b42adaf-31c3-f8f7-d5ce-4ad5eb55f128,2107917665,6824129620781085821 +53289370-ffbc-e025-0976-eb59a4230724,1326029303,12600760965673159918 +da15ec48-cdaf-8af3-7682-07cb335e0158,2888504165,7425997644831784685 +83a264f2-9a73-90f2-fc1c-71bdea491c5c,3275298061,15916493700071366491 +8c2b6cf5-3f83-03cf-df96-a233aac1b035,3345861302,18336465491789176398 +60aef869-f8e5-434e-9fa9-68c9f1331dc5,3579351161,5070757079060796771 +c3febfaa-e7dd-f905-c1a7-247d6e2967ae,151485441,4105213592642827170 +4c7fac62-66cf-8ba7-c4f0-b20c76b08f4d,1157548631,10007252514190527414 +aa64dbb4-2d52-2b84-8482-f18c8803ff90,1157320890,5831468515475634904 +e3db60c4-e0d6-09b8-0a7a-53ea99e4cd36,2207545947,14374066239717693715 +73a12d1f-085b-9aec-d7c5-60d1391a4c24,3167990227,9644307448672174921 +b227c0c5-df90-e038-67b3-00ed9a30605e,2315426784,9391725879349597362 +353f7341-ea45-d74f-2ad9-d2e4187fe9cd,2777874714,5876464835024615345 +d87adb1d-2829-1c0b-089c-10eb33ca26e6,2576276506,9650513288798439130 +0249c91c-ba20-474b-b6b7-c3694a8699c1,2126470507,13923435549294134855 +f36e6e21-1899-b299-be3f-2ba5322fc49e,2403721422,13944081086250239693 +4a0de442-81f8-ebcd-f720-6aef71803819,694226396,10822835089562780603 +59165a35-c102-c1b0-ca0c-0aa18ff44d66,404244820,12543499727597687801 +0c95ba50-f304-6755-fc7d-4e1381b111f4,2269936988,14833913475416872650 +5edf6a05-ec3d-1da2-5277-d15d9d47047e,1000192517,14677855950179070129 +6def4740-8a94-3771-54e4-f7b7ba94e562,2877486732,3039331226456898538 +2f384179-e4d1-eb33-f74d-e074459bf94c,660686795,12384088080462820712 +f6a43e12-f0b3-985d-a538-be8dbc7a0c89,2396624990,13386055115036397076 +ecc01a4e-75c3-5921-a17c-3d119a4d3575,1612806439,11326245943813160985 +e87fe836-b65a-1cd3-1a1d-a36defb84464,234130906,4721603092341094962 +d788b533-8c9a-404e-e8bd-8ef9e1c64483,4052635666,13645780913611609687 +2cc40bd1-1a36-cd41-a61b-825c4a422165,1748810122,12665472610537889401 +a694d506-bf21-235d-503f-af529f14b3a5,1137798812,7590506675382412193 +9fa5007a-7101-505c-7df3-a61fefa1f632,2203879739,9562947020689561334 +cea59d7e-ccfe-8455-ce52-cdd5024b5d7a,2525816900,11615474510854415274 +6d5b7a2d-ebf5-32de-6346-04bb491c1813,1683032133,4076855338495377631 +725336c3-9f1b-e992-e96e-792aaa2b80b8,198557715,11764709786256573051 +56ae2781-e929-a46d-4264-f0eb65c6465d,3392421013,1736560844837919718 +8cc3d56a-a2c7-f665-b6f4-d0ba3494b086,3711256636,4955678978643611850 +e93ceb00-ef0d-2f1b-63e5-52701f982558,1044325792,429408730078226445 +0450ca2e-5fb2-dffb-badf-9c520859947e,1143071533,5575715418956560810 +322dc943-c84f-bc8c-09df-8866e24c76f9,3182111974,1251366424812277363 +556e01e5-620d-c9f3-e478-832fa5b27b33,2758529535,17523784780099838414 +0f3efa94-632c-f357-71d5-18d032a7baef,1649962958,8947865487750812541 +5f641179-5c12-2d89-f6b3-3f99b04630fc,2569129283,6402737044770722451 +850ffc12-757f-d068-401d-f86f55d7494a,3264588418,10225507271314812712 +401fdda0-b954-8939-d690-58acb3675bba,647364138,16127011812234959888 +2fe998cc-3c10-3594-e3e2-54d8ba3b0373,1998579638,870121464926362167 +8137031e-1a8e-7820-936a-7323f0299b9d,80454870,17617750543915872059 +e8a31550-86ba-03a7-abc3-3e301c875e07,3459640327,11087136753398017501 +5959898e-a285-d542-a03e-c79bd287bb26,3349664035,17775513834142700109 +663648ab-476a-e17b-2ab8-8b2fe2b99044,2056671828,9843990765155814376 +f48336f6-bd8b-1514-2e2d-22adfed25aac,4159764290,15562717349039066263 +532766c8-e5eb-474a-2a21-99e586640ed0,3643496609,16588296355245688885 +67667522-072a-3719-5238-a04a30683436,871685720,13084607152426476890 +961ea65a-04a0-6f23-d536-d3536461db9c,4020052271,14896940035997212054 +fdf5e610-b38f-db11-1870-128f707a0436,509723692,4092403659078103034 +bca0a7c8-086f-9a2c-0029-eac94993cb68,3136695194,7869348257096332627 +71ff41c6-cbf0-af78-50fd-a8c308970413,924580535,10129401502118518982 +82726c7a-6c67-3049-493e-fb83f8b82492,3654024388,14293527523463752285 +9723612f-de66-1d07-f8fe-7deb742302b3,3377478374,9769347357641793060 +3da9e705-b5c3-3677-0c11-804951631371,3449665508,15962071104638035962 +bfafc52a-6721-1a4f-1478-93889d3a3666,2299796708,11916459875372767897 +dbd7da27-dd9d-f0bd-5d56-b805bbe472e6,1706067556,16337220829555324350 +008ae6f8-fd54-c615-0ecc-d8575fc85310,119096363,8169877391331111197 +f4e98c5a-4b18-05ff-1013-c7fc5ed56b81,2884440311,1711242975638514697 +e9693d3e-c5c4-44bc-3dc7-979758b1682b,1051058143,15548930903694802687 +48610b4d-1088-8456-ee47-6f85f08d9d3a,2250324310,4930373112039367005 +9fe91577-06ac-a935-760d-d4d9e9a771b9,3795201196,12570077241584422502 +a0a7f9c8-756b-20ef-2ec8-659ef87d921e,801966540,14515457828550730380 +b1845bb0-dff3-61ba-b7eb-3bcde9f5fed2,2733576035,4847152210417150294 +0f065239-d056-02ad-8e62-9588a7c05e57,3022614822,16843312831334713317 +cbd4134d-dbe3-455e-9248-24d9ba135c77,1493114798,2410828826763800701 +f1ce5732-f5bc-243c-f6eb-98204cfabd52,23688005,9931657257798821098 +e697d8d0-1da3-e42d-ad93-77f80284df3b,3346014815,7606484448763574424 +23a1ca44-c0a0-72c6-159a-c00c1ab7b674,4268680520,10341207353631104041 +5098dfe5-8f7a-b8e1-80e0-1aa667285dff,1907684550,12232632245532218265 +6a32d4a5-e41e-6f09-b46d-2be45d6cd9cf,2062345429,17616847243358998505 +5175fb9b-7fc9-0312-94e2-3ac1a15ab0c1,2440969693,16399484082186010592 +dbc261a2-e33b-8de9-e5d8-7b9b9d65e4cc,3450787747,4302808172781058389 +43de3613-78f6-f695-6fa3-12f2c1ce72ff,2379746139,13766787887832458781 +37aed6e7-f035-62bb-81ee-9c11ee0493b8,1964531331,4958422505167714344 +db752995-ded8-cd37-23b1-7ec7d1cbe5ed,2842345336,18194485762023662086 +e6c5ac87-3bbc-f5cd-4214-c4ed6efffe92,1647283159,11611854016726153411 +5296348b-3636-c552-f182-ff6ff002ebc7,613562724,12764051255847460696 +2fade837-0727-7f70-4bb5-d034272fd54b,907660365,5160074293410538923 +2779a566-822d-f045-72ba-865305be7198,457411738,15417826907604312365 +7ad9e393-ab95-e79b-bc5e-4af4eacd7c43,1524236520,13033236109038400573 +2e0903d9-b54a-8510-82c1-941d9823018d,2835017257,11833579822491521950 +7ebb4b13-f03f-1712-ee6f-c91d8c8848e8,1602631169,3230071854599903870 +87734ffb-2ccd-0bb9-a9ce-88bbe871e498,889887350,6046683990778948650 +e426c7f3-117c-6a3d-b459-88293afba5cc,3430492455,8736237614028949056 +9d4edc18-d341-6ac4-0101-29d208d6c3d8,1181956241,8757030034059719890 +053080e2-d956-7c5e-cf30-a4354ee43152,1413894780,7007865312373997888 +31686c18-a271-57bb-76af-e9097d8fbaf4,1880763434,35296113507287199 +f1fd8561-ac57-6bf8-75cd-905ac0da2ad5,2011919275,3517350737765874948 +8b666e72-2a3e-0e0d-83d5-c653867c526a,3058571685,16268654287098947175 +031ab923-ddff-7944-e3c6-fa6d480a8c6a,4128039572,1680979768644032283 +81b1f656-d817-81da-2372-1a3db9b4ae0a,228048771,11457086497263956786 +79d31360-02ec-13c8-f443-c395a8a56679,4073196678,4870613130025685885 +2bf9e240-f399-2fd6-13da-13ee4626355d,2640616424,4570776542752381317 +0d4a5034-5585-3f25-23a3-381054340039,4208213552,18422405262582059973 +c777f591-3d87-6050-87ef-8063650694b3,1028829755,17981423194030288567 +c94216e9-8fbd-9b84-68fd-e17642f35cc8,334875183,15352637607167538475 +dd5c928f-8e80-8b9f-84b3-2d8fbae464e7,3666575673,15251980808124120385 +98ff8d28-7ccc-c1ed-2b77-bbdadf986181,1378469686,8041414009108652347 +34be9c4f-0445-ea52-4349-8d10a942f355,439981602,450802501745981091 +09be8355-63ab-97f7-da27-1c0aa9535c56,648082748,7234374251780423960 +c0ac2e9b-4bb9-054f-a600-54914fa00b08,351403632,11353129453679980224 +6c2f694c-def1-7b38-20ba-b074a4a2bdab,1782279614,7432774249408507531 +76f66ba4-322a-9fed-7a82-afbbaed956d2,796937536,13322095829478707332 +6dab85f2-7e11-462e-6da4-1ffca644b699,2758330722,1554926198540601251 +02e333f2-613f-f113-6092-e01579e682e9,929590548,9822163795117652519 +6ca7073c-0a9a-9c2e-fd27-942f36c0732a,488161006,17689553743883808454 +6233362f-d2d7-2b55-644e-ee2b2ea54656,379505417,12953371787825148541 +4b6e2d2c-933d-9238-a6ab-376b591efa63,437804816,12744621750228447271 +f7332870-a7b9-8f23-34e8-32e6cc09d228,2734924632,15364647737915264302 +49bae6d6-7ed1-6f55-33c7-d6fddf617aff,2738833155,7429726993671989848 +1632fbb6-23bd-b113-669e-9cb4f6e8eb57,7011992,7246459006421049378 +fe4f0878-dfa3-ddcf-cc00-d8ca70651676,2215241744,9624862202958136529 +e55ce452-78da-4629-4d4c-c61d691b22ef,1315977853,9336683483542849501 +b36ce1e7-688b-5593-e5f1-380f986848e0,2231965289,15541138150425171044 +5934ecfc-d96c-128b-979f-d480863c86e5,454883878,15186123987807921281 +c52861cd-df40-d42a-7fa3-73ad925a506c,2642805361,10522013186684200506 +191f1145-0f7b-1d2d-508f-f9550ec37d2e,4050972728,8181092827949106223 +2dd6f54d-40f3-4522-a5e6-0cbb6b1a37cb,2888434188,12209966606759115452 +cc95372b-47b7-ac2c-5a93-88a1ca07f686,70297432,14522679475120052766 +0d556628-cfcc-2875-af93-f734d32abf07,4260270356,15617297271906217721 +b0b0e1af-a9b5-0955-9510-35bb22ef1907,3340627610,14037261076143042506 +5890247d-752b-5e40-6a59-870f0bc828a6,3714738508,8856459563610878158 +053bda15-af18-e585-a61a-110617b936e1,3930700462,14213121668489468420 +11ea87ff-d29c-7537-dae5-5e1e7067a319,3177215869,9981094627018066755 +55bfbcd9-8161-5f21-4977-b72f391cdee7,3510309365,12305144542703813160 +0aea44d5-9b3a-2b7c-8e7d-4032cff1e76a,119772121,11343624970656532651 +f51e105f-6d1f-602e-e926-0084b11654b6,3093336515,11097507009376647643 +35c186d1-6912-9ada-bb3c-675a8ada370e,1465832308,2987225501377349107 +d767987a-dc91-4c96-bf69-4a3f8c6d769a,2649623691,9201710784276167820 +29eaa6f7-a12b-ac78-47ef-150ec2cdea41,53340064,10041419866769438756 +af90f8d9-8bf6-1b86-490b-ca92736d688b,2038935004,7055938866689090794 +d343732b-a0a9-795d-eabf-65f6817f3d9c,985621716,1461548753786672366 +4282098c-b2c4-b632-8122-655bf93f53a2,2938473259,12002393618809009165 +f439ed64-07d0-7f9f-a653-23e70aa0a584,3511227352,3329495038986086828 +f73f9546-3747-df2d-8866-ecce26f8df9c,1486541781,5451561008519753492 +cec94d7f-cee9-4648-da1f-930db32115e4,1812844265,11460779716712587551 +d7720f7e-0ba1-efdf-2dbe-f93aa269a1ca,2774549093,7601571192490830590 +90fcaf69-e2f4-c5bf-e0bd-d8c147df96b4,2672477842,10630914882990835716 +984bab00-e1b1-02d3-7486-7afc600dc3e8,1672662106,2428023914595432719 +c37e71c4-a916-26dc-24d7-4df586dedcfc,3487370311,10974515421018978793 +f9da817f-2cf2-7852-a617-7ccb6877b741,423446756,16286914567851210105 +fc89ebaa-30d8-ed2a-779e-96682b1d55e4,226930635,16201473478455598201 +91330d56-25a6-6e1c-6994-190063e89ade,1993759811,6234530194273715508 +9272401f-d2e3-8bf7-d929-d897b7ab6f0e,3021908956,13218949778662440609 +81a4782c-9c86-e1b0-1a17-4d906da5fe3d,1000411001,15755941827672527994 +45e3789c-ded5-ac41-6c34-326026618145,2739928422,9608878868036698539 +20c6cdf4-cc54-943a-8307-40a4563172ff,3780537515,40698510313629337 +b016e05a-1c94-bf16-6b4d-30a3123f26df,4283691322,17141092583146828500 +132c7af9-e3cb-a365-2215-1bbd7c2ed484,3626307260,9110265340615122811 +1ac5611b-58cc-61ff-731f-31aeecdfe197,1560647795,17440696041363965889 +d1edacca-dfdc-3631-8e07-11f05b8578c5,1842173815,7586424061462479106 +c9cae4a8-f0d2-8b8f-544e-927cba986f13,1850946212,17159753837162182540 +30906130-66c2-3aef-6027-1d0e758f4b08,1758898972,17127697097602597404 +b0cd21b5-41f3-e652-03be-705d123d4856,1631962248,13552729529200101849 +50aa87f7-f82e-5d32-f5a0-3088683b2666,4060849142,12853934127273480863 +6fb65067-8913-8015-7c28-792d28283da6,2405901013,3507972796233048204 +a6294c14-43d9-705d-4aba-45b2582ea5cf,2011088096,5422480772918822110 +5f1f4090-e4a7-29f2-b20d-4bc271ef7a4b,3690964675,508762749278821120 +f8908cdc-d914-7fc6-ed50-21a5e1a94357,3132626803,2492045195610268384 +77c02781-14e6-84ff-b6c4-3f067fa6e265,3329692675,6666744450319048738 +e1a7da8c-6bb0-925f-28c3-7e6f69949713,462840308,189671860385704733 +7f2e68fc-ed1a-26e7-9315-17377b4ac2e6,3575267996,8818329988519774647 +c5fa4645-e23c-aa3a-2daf-df548178a215,1419924369,15734474961744037783 +f4f75c15-a201-b746-e64a-37953d7a5155,2622302462,7682414992392875817 +c120e4ef-ce58-2f4b-9880-bcd24057b148,1831845873,7354724443098161068 +2ec05d52-99ac-1a57-de96-8d5affb1f382,1191527950,14116763808160150834 +c975e7ea-165c-3476-9aeb-8cf9ec2c2a16,745207253,560240094123066495 +784f0dbf-2e28-0a45-33fe-afcbe1b9d622,2470243849,9925622452635930061 +f4e962da-458d-9970-5222-cde17c73006e,2178703107,6607201205127453348 +86b17801-e9df-c738-6521-2add7b99c028,2998593267,13496849934222466711 +b2ad129a-cd3c-115f-c0e2-216560d4d429,2634819073,13561801746613330339 +ea97c86d-3958-1f4a-196a-59ea0d6116d5,3020056727,4028697636190918770 +67834403-1193-906b-d60d-af5cbd6cd738,3292563783,4723530892051785636 +6f8d4583-5fbd-222a-82c2-09fec7b291dd,2569855754,1394874975882411421 +d2551ec9-8558-df06-9fc4-ff7662c8d942,1637681135,12224466467978579600 +6051d201-79de-8d96-ef8c-f660c0494254,580218293,5564701676912129530 +25669db4-dfff-7dd2-d295-4ae72e62793b,3758439956,7631246757374140065 +39f1962b-0dc6-0dbb-b270-dd00b896df6d,1625553518,1744558618795140950 +f6a37682-8f5a-b8f3-eb6b-4c8976799e8a,4275890977,6514165271663337951 +f8979897-fcc0-ae3d-c38e-72115f670ec5,3149933538,2932168736782566165 +5e25ea95-3944-b96c-704d-bd529d6f1485,684146085,16813135846375097465 +33145879-6947-937e-3c77-9d5a68928778,714002888,15095551055220277945 +a93cb202-38a9-04d6-8925-8417b43029ac,1660215710,14111755147790378511 +e1aa10d7-48c5-5197-3485-bc7f21b97270,2548498923,9140603484314916390 +fa442623-b142-9724-db31-742724c352bb,1596810234,2912791429147121086 +edaf4150-b2c1-5131-e36d-f83a6503d8ea,2044253333,3385610499343494127 +561e818f-93a1-f669-4814-66fe49bc41a9,1607164640,9697151178279408849 +178ce306-c8c6-3663-5afe-31a7b0d6141d,2088268702,14951639325710399705 +af91e318-996d-5fa9-e0c1-cc5255bdaa89,2239229551,8586526681535101444 +43ed5451-ed34-208f-86d6-c6e8fb74ef9f,1957192777,9607397702256195791 +660123b4-22c9-ce06-ecd2-d962b08cf077,98882788,9502649901693656425 +3e802d11-95b6-7372-288c-dc6efaeca912,3449698203,3180086024204310805 +cd7468ff-b2f9-7d0c-d7e6-67c320454f3d,186257158,2094100201969701448 +3a804272-2a78-99bf-7588-800184f9b09f,1094089608,12567031328339822224 +41673fb7-60ce-86c9-6b47-d68e6c0719f4,2493959082,2391742975408579434 +788e452e-fad8-f852-df4a-feecdff4cff5,4131988463,6626647403159588852 +d20a9e1f-84dc-a5be-d95c-47832c86f6ea,3883923188,16231706084258198672 +93b5ddc6-f59d-d37a-37aa-9cb6c5265e69,2457926473,5299505708893309098 +5de4c97c-c906-0e7f-002a-11c8d036580e,3276630877,15869199412422801059 +b808aa47-772e-5ac9-6f50-fea621cfb483,1500157621,4123546108304794041 +acf3d7cf-8e3f-b172-0266-c95523c5467d,3137195586,6684372288304222497 +123e033f-c897-0e6d-a82b-6631687b3886,79390606,15290312892614702169 +56fc21b4-8b69-8042-3266-14a5fbac5859,1268338596,10753594433643182902 +04ac67f7-8fdf-0ac3-5a73-4af305206557,1529043744,4837501164847042650 +fa4137a1-a6f0-1c77-60ca-fd1b46467d5f,1827083470,5220308390636641229 +b7f437f9-8c8d-30d5-ab2d-2d16220a45fc,2654783380,18211728468331081097 +bc131060-a66c-e0d2-edf5-51c0d95fee84,3617129209,8001924991936543737 +e6170aac-67cc-3707-a5f2-8eaf9eca8aff,1602942615,11508782978038034114 +aea2aa5d-638c-6534-d7ea-8048251b8797,4004074366,14616387747321084716 +1e1de084-b2c5-12f8-cdff-949675a4dbef,1604636378,7220634861470915577 +4baed0bc-ae1a-38b0-7b9f-b8f04be6c516,112552367,11529706314356245037 +e80d7014-1636-673a-e4e1-129ff92930e8,818881390,3561886815098595331 +f66edacb-2c22-4c29-b5c3-5eeac3ad02e6,3777305507,7865890343313881417 +09dd5bdd-5dba-3f11-c34e-d713a62a45a9,2273864413,1725944510520704032 +85d744e4-7847-18ea-e490-b137ff6b9f94,1045059243,10125830122172542669 +69b484d1-a14e-5979-8916-a361f5550321,2775370997,9806265532095697272 +7fbdf9c0-7dd5-01d7-08ba-873a66336a10,3953868745,15097114934970348386 +3d3059c1-8d51-2883-afec-1312d9385383,3578685587,4211585765976843687 +d90a9159-ac90-abf8-72ef-9e5dce8dae31,2946351639,5647563700426700575 +1f39ce6b-ac34-ea04-2f4d-c4793d479843,3386895623,451145408164605795 +d46d6d61-2c86-2058-4b2e-ccd7ab8def8f,3699918982,4776133956084785051 +5f837d4e-3116-3e6b-c41c-17bd6acd1f7f,3084442626,4052470550639100783 +51d82f0a-d2ff-be5c-ef64-46788118da2b,3656513245,4863526335056844172 +48e6ccfc-a02d-0b3c-e225-04e86c4d7948,2994793812,4893378555801801462 +5c84191b-205c-a26a-0d15-85695dd6cb36,3604644217,8842531671088529237 +9d7993b6-4ac6-e639-9005-11bdfa40068b,1262956590,11348695313231875781 +739f69c9-53f7-1db7-3a21-235a7d489f68,1197670779,13651890094682996341 +3d1b8f95-0228-dfbe-fc5a-cd3cbdbd01b7,3556545285,15823411801261966947 +9e9bcf5b-6e35-1c71-fd75-25393d996861,148778962,3788907587908190477 +02075095-9503-cd89-4563-54cd6ede265b,3843932845,6804961573098157931 +4d845e90-b30b-b0c4-9176-6c869ac2a34e,3729332786,697227779157683898 +3ea86695-81a4-9ce7-99f0-f76616fbaade,2806153735,15252151401961638801 +484b3dd1-305f-9057-4c0a-6fafbe711a7a,1944550000,17080418553947775635 +a0a667dc-9018-d752-c4c2-d15279143c2f,593469279,12634900889836657765 +d8e1a281-3f88-e4d4-46d9-59bf649bdd2f,189659499,11055901171089439986 +e4755919-de9e-4e00-c0b6-e5ecd7dfe61e,335627082,11693948967085519548 +cd158fb2-fe0b-6ed1-b61d-ebb109782cfe,1409836018,2603280930387965317 +0da9473e-6527-a00f-6647-2f21d9bc8ccc,623763046,14117937447687160274 +8503e6b4-3938-89c7-361d-8ba2727d90cd,1590901356,15750059468921433594 +15876747-096b-d162-a8b8-33ae05cb9158,1278794573,17056318597002813231 +2d5cf653-ee73-e159-d5cd-f4a10993f4fe,1862881593,16734983963570029865 +d2512dca-c8c8-49f9-ba28-b1e0aea1d2c1,4044375172,12072138970786720247 +451b4193-cebf-2da2-c0bf-220f63bad125,2683695500,14242947635852076393 +0c8e36ed-202d-b405-e6ab-e20e41b926b5,769975555,18325252549190610771 +de7928bf-b90e-a8e8-3ac5-90c7d2ee6a18,2758126778,16384026004242406490 +758106ad-afab-7a89-6cdc-856359335c4c,567238848,13010088996574409417 +deceb4ca-ceec-2c82-c2eb-f756fbd68834,513193895,17125283007760931983 +0ae834ea-9f60-6d6d-e5ca-8ce5e5038bc7,460171490,1809952033866360922 +4a3dcb68-5662-2719-bf29-62078a480380,3756995948,4800699628012720221 +0837dd7f-686c-47d0-228f-fd8e3df37c3c,3374564390,646215740637473120 +36499571-1af7-cc39-c89c-321c2bcd3328,2587714575,13560770173988810041 +7f2d1c6d-cd86-5441-0588-30d2879221f2,3450869453,4535349977338850307 +a91bb9b3-906d-0dd1-1ce3-6b4213bcb769,838842717,14387188644509074572 +b43d437b-4b8a-bd76-1dec-c1620f9fd096,2896414651,14186374466611141718 +a251ddce-4b27-4ec4-9db8-0dad8a4339a1,3680900308,12735463828433425398 +ca278eec-ef84-c215-4c04-eda1d5b10eec,1205495629,4857137308634537712 +237af4a1-7ef1-41c7-9c38-5ef30d6b3cc3,983637013,8730588592527490510 +1994bd12-aa0d-1e6c-5efc-098c326c898a,3561908239,4243394259466394265 +99342aeb-8d7a-77bc-bbde-922f73e616c6,1490195182,10434217704042998962 +a9d79dab-83e4-1fd4-ff8b-22e2d4402cfb,4058915373,17091004705683692226 +32cd0626-f080-dc4d-5e99-565adee11f4b,4272383094,9285302104843452351 +f5cb6b2c-9496-e821-81dd-ef8f6e5691a3,890948521,1276638982384232121 +b164b80b-a264-af84-4dd6-33cfa4d11e9f,2323010783,5570132327955225524 +949ee294-8521-b1b9-fed5-06d435194295,479964587,13079521586306593325 +61738187-ee50-3b66-8ae0-f03e2d1b29a9,1350625563,15514059723167010652 +1f73316a-f392-5d1f-5c5a-fb470002512e,3693789973,13894715172520497466 +ad570774-beb6-2f5c-d670-46c486fddaf6,4221080552,2644492159866932474 +2f987c83-bcc9-291c-150e-a0a06adf5f5f,3710595811,5366849407832838423 +5e40936d-521c-4768-6d6a-c98befa26778,2790736868,731099953072711953 +bc211bf6-d884-e776-b992-52641fe6e4bc,3739399667,18314351697220480195 +7f6d58f0-8c43-96cf-5570-3114b5867d9b,1402729779,5728253514726760620 +6d436ecb-3ad8-03dc-d339-f0a1ce4a3238,3478074634,10330423735675728972 +a78b0ada-405e-26e0-7a1e-670de25d32e4,2670225825,12104946864909634437 +4bb3f069-4939-43ad-27dd-f3feb1116909,1423120230,6642619456913849707 +d78fd5c2-b95b-63f0-d031-fb40d59e4d5d,1857337709,10547029827763761540 +a5f2e524-fca7-a6a2-76f5-467beeff7dfd,162391873,14848744011877002375 +603ec37c-dfa9-3d11-ac1c-e74e031e3ca1,3475954472,4285043560195074747 +09ccd27a-7380-aafc-8181-4068775c19b8,44871919,1816130664424623020 +5d0862eb-084f-31e7-b756-60669199fa03,1184539784,8138086702173421823 +062adaf6-42ee-f18a-cf0d-68b5e3c0be0a,3270114860,5426214436256007469 +06ac3990-6533-3d79-a26a-0a73fe0f4aab,1288833765,8457782192123193328 +45511b5d-d0ab-3695-e692-4d02ca64a6e1,1247811210,6391305522056674254 +4e72c2ff-fa18-e0a1-3691-a98a144fa31e,1379759749,11281946837964946354 +39c2f4c6-4456-1cc5-719c-b01f2c6fe7b1,4113583232,7223205211518138091 +ffcf4197-a517-8691-4941-83784f002650,2005506583,8047171379334695957 +cced1e1a-c7c7-cf58-124f-c4fa74208822,2579735721,3472571976385475029 +0c351114-9df0-aabc-7549-db8b67097806,1260439880,15541370932397182783 +cf0d24df-285a-28b8-acb8-a4dca43ca38c,741236215,6638763449777641771 +a9f8ba66-c0ae-962b-d1c4-60d7303ff903,3138266507,4316251700455049594 +903fa3d7-8114-2b26-e0b0-120d2c009fd9,3686064128,5418889079923252944 +788fb05c-fafe-8bd7-e1e2-99d0197608bb,2709611472,16545721815143945698 +863d6d5e-1b81-1aba-db0d-c464c95f5cef,611688017,8643535846908943364 +25b24a2b-74d0-8192-f5a0-e124fa277999,2738427317,5245567572319804739 +949cfcac-7b66-304a-eff1-cd928e7840fa,1368870145,7877864472054835483 +d1bfb8f9-082f-48c6-2f73-d4e21dc2be85,2869553676,5469242732134049836 +7f03d5fe-0b31-f513-a17e-a50b07a0582d,1373941506,7668406378876477417 +92f10e44-6c14-05be-234a-0882a8b0fc39,896076548,7842329007235080979 +7309d384-d2e5-63d9-b997-d4676756eba1,4085047633,1755456728071421169 +37fd99d3-6e3d-6f14-ba39-3d1ac9b38161,4101420087,17718974940762897893 +bed00245-c849-b5a7-1af3-5f9d5376ecff,1757535430,16437328270325860213 +84036899-a746-5cbb-5f96-5796d5009feb,3171528824,3793463172641068705 +7eb4b78e-1bf3-e6da-ad85-8afae8e18d03,2860244337,7984684869628541278 +47cc0a45-13ac-6a7b-f342-39eab86c66eb,211349701,16292904221109726153 +9452d944-2efa-46b0-fa41-14c363dcc520,1805387390,10027185008598330165 +eb184cdf-bc51-609b-c510-8f2aca1d86a1,3150892644,6228434617729964153 +d2a6014e-15a2-9518-4a84-3770d29e3245,2829546727,3805935597378451934 +a6d1352e-cb08-cd1b-7af8-63cab58b80d4,224770690,7535002276536046505 +de255f87-996f-8842-465f-249dd1ff310e,2500577283,10675542567137983666 +bf9b97df-b996-cd41-7273-d5084e935587,3647498338,11484974529323776007 +16952184-9c0b-4c78-62db-1dd0ec4e4186,458719070,12726496104394845834 +ce0770ed-d643-590b-857b-a594caea070e,1891359269,18431714146706810504 +8cf78378-416a-7184-a2b1-36552b0bb3ee,1641118636,17240113294726021406 +53d5c986-b07e-3aba-976c-090a736cf9f5,987008071,2007434351989943151 +5ddf5557-c9c7-cc15-b2a2-420b515f8d57,1077700730,8577304704182793769 +e4771bca-ad17-e8a6-1ab9-c1126d321550,4100635137,10162015994751955754 +71c2973c-682e-eeaa-4502-0be78064df8e,809444015,17392880316288093189 +06cab020-84c6-07c5-91e0-acca1923ad9f,1144807910,14763847040920554997 +c54fa3d7-b705-21b1-462c-00e5a4be62f6,1840838838,10389740357419612295 +87f5c4d8-7815-d20d-2cb2-a20cfc17cae1,1191495192,8285231636397749869 +0e7d0972-ad45-03d7-c575-ab9599b6c5ec,2755687866,13317272165878336557 +601a4234-af44-38bf-d5a5-d1d5486b89b5,423550732,8559699174046571251 +78383147-15de-d6f6-a210-5c82f70f796f,3932021226,10789541556251781489 +2b3acb6e-9ea6-0539-3bf6-ba59d3d752cd,1007548483,6865616539218172308 +bab78878-38ca-fae0-7ed1-c2a64354fc73,4239643222,11389592892153216665 +e4d18cd7-285e-1d0b-b59c-e6359f33d2ec,4006909670,140128853801427719 +0d61c5bc-7896-6e31-fec7-eb996d5e8435,2541652283,16454057324170150687 +8c2eb705-5b03-8501-6701-69b2298012ad,2910891987,4734934338521386904 +33e76bc9-f100-60d5-1953-1be03218a76f,3161140750,2382159928829452964 +31eaecfa-7175-5915-3430-ce1cc48d9509,2006636944,6321300953939850289 +8d50bb95-8d56-335a-5bd1-f09ba7687405,1478381019,582332242283144062 +76731b6a-3b0f-74d2-86d4-27508e1abbc9,1119205573,996799157380305443 +72a6a1e6-3d18-4950-389f-96f470512855,2106718575,15184641045594862402 +b5623ec7-ff86-b76b-0204-b2c1b77e5a81,1168866655,13458630811193247442 +1cba45ca-217c-14ab-bc84-53daef0ec6bf,387970660,10954858047139823529 +2dd11016-a748-909e-51fa-06ccb4655f95,1512634796,5470963067536843207 +b0dcd414-2f61-3a8d-5c2d-e466ec39d37d,121987819,1507455193969063644 +12535598-d2af-ce0d-f997-66e99481ffbb,1432711990,10573232359041048517 +fab56be1-7e14-9021-434f-5c995c52c66d,3329208060,17280238154371448174 +2364d09a-7dfc-2c0a-414d-b6df8fda4da2,3073441640,7252071768116928748 +c4dba06e-17d9-7349-2027-2ec71f2e1293,4065772867,16405335275854477218 +23ffce74-c41d-d3d1-28cd-4b393cf14d1f,3991580224,2471483500453852064 +ac29513d-ea86-db47-336b-3e31536d3b54,1549727105,9942147080020263965 +761b889f-cfc5-2bf5-ac16-b2a0c0e03c91,397267882,18190210627038764788 +501fc1f0-9317-50f5-c162-910d0cba6f02,789165998,3299584981185478158 +60c3a3fc-4e0c-aaac-9285-a8b6f94a9bf7,2238985803,9249457259331190296 +9b2e3dbc-796c-f92d-4602-96f92ced2541,1102189098,952603060515363305 +e9a57b06-8201-73ad-39c4-428251779325,3343427537,6013252402670008251 +770e35de-e9bf-050f-7c09-8d1fb8ba756f,648180355,8080495487874160104 +2777898d-19ae-7fe3-fca8-171fc6ba7ed6,4086013313,5877345794681620525 +59c28516-fbe2-ced0-e9fc-3deb755fe878,120554564,12000249252248654954 +73af8ff1-2102-05cd-c78d-7e2af3def056,1755502292,15091011242055525964 +5e538447-8e24-d1d4-4ca3-e51b20795ae9,4079478279,11542213918239667594 +667a8185-f96a-a11f-95b1-e5cf3de0c5c9,4270601267,4094387671435525932 +7444518f-d30e-47e9-7e02-59299b4d791c,3044303611,4875592081558142865 +77a3c9be-9b95-6f86-1d27-d701d5d5d9d1,1508084672,13510014788624323157 +26537f38-abcb-4d4b-d635-c34b1250af43,1074375873,14046696542948629316 +09ba56ca-e592-600e-cd3d-1dce004985c9,284662360,17607508682709016554 +a1b73120-0e8e-2d3d-d03c-81238c88ca24,1436956112,11046593468047370737 +958a8851-9f63-f7ce-34d8-da42ed9abf81,658823935,16096195925658570666 +da576fb3-fa1f-5f38-51ee-c184d94568da,3896982712,1136264825654107292 +872bd0a1-1103-fee1-ab46-443524088d78,1829116623,3457836711831610546 +3bfaf287-f357-3321-6561-eda86e7519a9,2203528884,16502899399569634050 +7ef8701b-9a60-cc73-9fc7-3208db7929f5,145826331,6546526630651565407 +35175371-70d1-78be-a782-23841b7c2f95,1891674413,7799947810856142965 +9a2a5487-cf49-2773-1997-08bca8657611,3684664919,13801292646176985169 +516e756d-3c3f-ef52-b36c-7fdfa9e3b82d,346707570,2766378128100899374 +9d4156e3-6906-ce23-f228-848e915eb5c2,63746721,3709326593967190059 +78062bb1-83ac-3c84-d88e-e2f90b8ede55,2655625681,119663466776325268 +b11c5df3-b312-ea03-8ac7-d5e6b4e661b2,1191771133,13431771896434640946 +23c64d52-e53b-cae5-f937-b0c03c7927db,1734309961,10031483117405599710 +9ce1b274-143a-0d31-081a-f1da6bd6c3fa,1672229930,196312548757556111 +0dbec091-2a7c-ec02-d133-d3d2e438119a,1520146323,15756593411333793208 +4540c334-76e6-2b05-fa5e-71773437b6bd,1216959254,15301787810506279235 +3d875422-49cc-aff0-1d47-f8a0e2fe853d,1429962614,9866562298994796357 +08255190-4e41-e317-4994-00f8945fd859,559790979,8522814760025912388 +ed8e8648-3609-1bd9-de3c-6a58bd4c1095,1461322308,2509601520792062228 +446cf2d6-a81a-579c-9a1d-19ae6eea829b,83621593,7700254049072706288 +c523ab41-cc17-e5a1-ecda-95afd3db79c0,1688164900,7460749488279537972 +bd282c88-3dec-a0b7-c0e7-35532c4fb00b,3560514792,14235110977186493095 +a9ebdf0d-7de7-d7e1-7cb0-1947b4c63afb,516986678,12449899223331842872 +91091afb-1083-103e-62c6-4a22180741fc,3463155079,14771646953495185429 +014dd89a-8ab1-f6f2-476b-8c8039a457e5,4105074575,5346868331376623577 +a0e18ca0-44b4-546f-1eb5-4d438ec7db8f,3759258732,2306324708857167824 +e3a6d83f-19fd-32e6-44f7-a6ca2a7fbeba,2140157762,18422555628199709535 +8cdf023f-8421-cdfe-dea1-2010c8338f68,3549567440,18311845271912194871 +dc274d63-01d2-d6f4-b768-494f7475f2c8,711911707,11521726957670697898 +96340d78-6f5d-7821-6a13-61a545a90d22,1013654861,236431767422683553 +8975083f-78ad-85b4-173e-878114b10057,1340367075,494913795866861303 +88711a0a-d952-b1eb-2511-0db6270f04c9,3208172571,13056832012211159896 +3f15e9c8-575c-4c11-c581-104f8805f620,3504070926,8928602757309959981 +b1260cc3-3f9e-a90a-f392-2eb188b7afa0,1612087316,2962355803200632438 +e3cd851a-bc8a-444e-aa67-de6fbc0eec36,2817265434,1306433966172163890 +f841a98a-ce83-d52c-01b4-4252f122908e,437823549,15234533166164053955 +d6a9cdbe-3b0b-cadd-9609-13ecae52bde4,2790892606,2255437000684473814 +227dc0c0-0c36-87b1-525a-7a6b696f1925,1849872774,7638576899391872656 +52ebce09-5839-1940-7b95-6e14e851f893,2715662915,12043489519252313559 +9d714936-0498-2483-6da9-8c35be022f85,1904696382,3246196236201238438 +57f579a9-2011-8112-28b2-2466f59e04de,914728044,8429801697151174143 +3cb2d6bc-bffb-771e-bda8-188ceef05a3b,586597616,3072826417239885158 +093bac83-c4e0-ab12-6335-3d911e7dfa57,4135782545,5078009631611870850 +5b795e11-3b61-8206-191c-c3d4f3613112,3123125126,13441580611244451745 +c4c6f035-6fe1-8d67-6aa3-1529d6deade9,4171498682,7438178668646976437 +22ef6e4c-a662-5567-7058-18bedd51c171,1505558915,16021139695234023295 +836a83ee-2ce3-80f2-0b02-9a316df39ba7,1537648082,9139864461050315339 +9cac6c69-fa41-0b02-1b4d-5621312e1eea,4151792563,15836007187147420456 +995d26fe-f4de-668d-b4fc-cb87bc33dc18,3768315034,696053969490768655 +ede691b2-039e-9b2f-fc0b-e2a234cb96e6,838085147,12112173422929452705 +02cc775c-bd2e-d679-d419-ef769c690cb6,2678781780,10651171576503222037 +e1c2f220-d609-037d-6313-ad325b40ce83,476873662,1859794021430530058 +a5b3e565-9638-029f-bcdb-db61a35f469c,3813594671,9982158366857787329 +c84822c9-2b13-bee8-f661-f6a16ce6b06d,377222900,17888907427018388813 +f3099925-101d-e784-2f7a-c9326de824a0,3592719820,432120219642220361 +6efba57b-9dd2-79ba-335e-732277bede30,1616713774,1474179620858318508 +a8588c6b-e475-b08c-db8e-4abb0b6a7940,318218064,8443738628760323282 +eaf63e97-e70b-be37-6e74-0052b05e22dd,76862923,17206325835443655634 +ac76de60-09ce-6cd1-f408-ea22cb99c1fa,446712749,4741785209127566748 +37015be2-65ee-3221-c6a2-f3c6eddb7dca,2335164933,14326550864633135743 +cf4b47f7-f632-b102-ed12-a2955e44ae29,4102927305,13240408819577461988 +ac96755b-61cf-b3d3-30d2-d09a6646f232,2353472703,17723165139598632506 +f675d3d8-8b01-4bd8-3eb7-105df4177ea1,1879300993,13947646376663623007 +8d1037fd-adc2-2b9f-20e1-511a6bc88f9e,887266327,17231309366454049185 +5d29d581-98e7-b8f5-80e7-e8cecce68a1d,2772268602,4170224255056433992 +2a2fa14b-d789-5024-175e-bd0beb1c7c3f,2892030523,13046066357197343457 +328dd367-2a19-f7f9-7d43-d89bba768ee6,2783917914,13083581270682071772 +e80e02a6-3ff3-6962-af3f-188d0b969d46,1299227486,8996721156209321472 +98b5126f-45d0-2a71-6ffc-c215ca2de4c4,1757105350,17814809791293243691 +d8191f21-dbf7-1983-697b-b650514c4d2b,413274073,8807513227652086477 +aeb752c9-bad6-854e-edd2-a5782e1c45e9,1092797225,18125913817883022779 +862c1cce-53f4-edfe-1b31-644300322841,3106446734,18216231402033322987 +cae20ee4-a9b9-ead0-6d56-821ceb013151,1705379069,11454269953585835639 +5c172c76-d050-6cc1-ce04-293cc8c70175,513578137,16337690309366946572 +c66380e7-afd9-2053-8b93-ebba14a1cbb0,4113005115,15103356574836627718 +8460171b-cf0e-5e0c-751a-33313601c5cd,3103509298,13890505005270414944 +0103705b-e136-07a1-48c5-12f35d940b3e,3279409781,8411095234925944248 +a301c90b-1f7f-4769-99c9-7baa02bede55,525083674,14716289687204926321 +4de74fa2-9aed-018e-a31d-c1b7684ccfd8,3070019707,16695085061572361907 +f6f849c5-46b7-b251-1a63-17028897df4e,2538034949,492005983800430696 +6db5031d-cf2f-681b-cc7d-1e72c1e7a88f,1293535236,3004329206225976214 +eb8a153f-7e55-3637-18d1-93180412d204,366728513,10469562982228779071 +b841ddc9-427a-f819-b1ee-dccf2a517e22,192961901,16360265129467594570 +b3e9dd51-523b-44e5-38b3-a9edd2089b6b,2768708972,17780912489282652695 +fe992418-5396-8c76-a463-55fa9d423637,2346826295,13265063573904055204 +9517d7bf-ea36-0148-40fc-e76193ecca7c,2744204173,1303427608863252515 +c3dfc3a0-2c87-2253-ce10-b44e8477edb0,1828931337,10763003830373306258 +92e0cafc-296a-8232-3c1e-cd552a5c3e51,3360694087,6694053428738160780 +f7cbc048-9212-0541-fbe3-6964a1b8812a,929196102,106309320778521413 +7bf8d59c-4259-b887-afa5-573d608ff6a4,4229257814,14917591908878602213 +6eb4f417-f07f-6443-76f6-bb0c6f093f10,3543183152,12079465958823511172 +1bf9aa43-d692-69c7-b57b-d6b87d0965b5,3529526754,15125512804080962565 +89b68c1e-7843-9a67-9bfd-396946d4a880,2740798105,3253728135626455163 +952253fc-949f-d298-6c57-c1fcb4c4345c,2539259285,11013074984987342823 +efb9caed-d0ba-5ca9-e42f-b68f490fc0cb,1289062328,13018395087895995069 +a2ff2f8a-c71a-e2ef-1f2f-ac5d474dc0c1,3970115037,7660811963949132226 +e571dac1-f916-d591-2153-1ec0da03dd2a,1204030730,11552661074667341799 +d39cb44b-d683-a5e6-e306-98fcf472f08c,2438295119,7166220075633416857 +c96ec882-0838-2178-cfa8-a37a4349962a,1178746231,15308640637337628544 +435163e4-6927-b535-67c5-0cfd935f95a6,4068097986,11017420439285529588 +7ae5f4b7-1c17-dcb0-0e31-1af50fa9ea6c,3564582412,3461095727371548015 +fd10f973-67c9-5826-a8a7-9c7d6242ec49,592515625,6014526945941379461 +9ba2566a-30cd-852d-5a3d-432c57aed501,1187385106,4531155868745026689 +da93d377-b0b1-9a80-a4f7-d4f9d868134d,3619230833,16721204388920312565 +f9acac41-5aab-5852-4bb2-c5782c4b1040,2782418867,4779146415707791162 +de4566e7-5bff-fb86-34fc-de2178bb15c4,473322067,9121186752871435664 +65511ed9-23d1-31ce-fb85-ed549996a5d7,4158147555,13679624854148779978 +681334c7-c958-af32-4671-45bb02006c46,1882039724,5409146214550345039 +385358c0-a621-2012-fce8-d5c96a0bf748,387608613,3759229431660413264 +df5f700d-9ee2-c225-7052-7be1062ad539,2836465745,7837669792171995067 +3cbe4a1d-e4a1-b135-2e1e-d59d69b37584,305711616,11517958788744579790 +c5abf95d-3b16-422c-7fa8-905eda2598ff,1757920392,2718006317697608056 +1472806a-2542-339a-df86-95e2773ff127,1190394158,15856645267633555152 +f097796e-0eb9-65b3-e959-ca45f19e38bf,2321962856,1362982575650576768 +961d6c31-4ed3-0e4c-3725-60f824937880,3504810593,1980512543580437621 +ce55fb14-a3f3-e4be-2eaa-0140c212b1bd,1659510570,12593070101784148359 +6359d388-1b68-8cf5-4645-c78e121f1b31,3936473849,14575629019669326421 +30510abb-464d-5fae-8809-3265032a364f,3836333946,7769573617517455531 +a7b27ab7-8074-b981-3757-b9baf4bb6f21,673649959,14507294973245157662 +30a005a5-3216-af34-c981-d6999508f47c,1955573744,6462101000851618604 +8041230d-8b17-d507-bac7-f32d8fab54fb,1159640144,3557443052423413279 +04d1e44c-4757-7306-b080-251b3593098c,3712046183,16606623444504480142 +1d424ead-245c-1b1b-7962-fd6ca21bb62c,585937133,7412292593976163493 +d48c8ea4-5158-0db8-3c73-52b93aadef54,2014906903,5952709090895361494 +493740a1-fd4e-20b5-4cbe-a50872a56324,2441006142,3330305775649057533 +ba63dddb-2964-3759-69a6-082105e85963,2095734068,3187822807420639548 +24347384-37cd-ca19-aa7e-21e7326da040,1027011340,8321308512661167351 +6cdac713-5ef5-f2fc-f4c7-8a02d6d2e25f,2436637323,11509938349640747056 +e6a88ed7-1625-a6c5-058b-f064f92549e0,1385843958,10419588667572822408 +2bd2e96f-c2c5-2b90-1187-cb288c15d07b,4052538970,3031389480472524624 +13631625-6f1d-4bdf-7353-20ec89f0c136,2552857044,13099064562423436659 +5ba5c777-465b-d598-c4d6-31a109d16ce5,4276181741,17573115695785611084 +fabd0eef-a08f-e36b-11e3-831e5b461884,997159660,1660625929039097584 +75a6652f-3fa8-072f-38eb-52e04dfedae2,2706014376,4014281857229386508 +b988e901-3108-d5bd-423b-7cc0623d3ddc,3915944486,14867455528708303785 +c394097d-04c6-c0c9-1543-89451bee1f54,1369450428,16556231307799579568 +7ba18ee0-5af5-fd4e-56b7-293d59d4adc2,2053218692,6317452685218676739 +7a34e4f6-6253-ca10-c614-4ee927689782,3989400181,1814855496649948059 +ef9b7551-de3f-ba6c-ca8d-59a125ea6274,2106378717,3357432176335253094 +bb4b3cf9-6b03-f443-ff47-25893c227dc3,3486721695,11547045584979502450 +b2150c10-07e5-b64c-4c6d-3d1ed19fac7c,453382132,16704466311382194815 +f5b01403-25a3-f7ae-c4e0-29e6d810bb84,320403985,2077111685262712064 +cbef31f7-f559-72f9-274f-8388f8b1e592,740715031,9150707843251141278 +fad866ca-3577-3ca0-d68b-7a35a527fe00,3212710971,5907447231547943221 +ee209081-9c86-14f5-392b-c53dd4ee5387,3367390726,1447053056928231371 +3c4d57d3-9809-51dc-6931-ae821c17168f,2850966819,1431121418041650245 +b4c778f4-e87e-e0cf-bb22-1ab3a03e8dec,3889612233,11207649291640110391 +39a92eb9-5ba4-6fd4-7149-6443705568b4,3923487482,18178076983124213938 +bdb52234-a15b-c1b3-130b-18a5eb777f9a,4136233779,14815049915387536467 +8df9d4bb-c20a-4bc4-b17d-f29e00b14da3,3994568117,6830434500725260897 +0d4feabe-d243-5bef-8c0a-42980f2fee86,4095383657,2545736101591083119 +0449dcc8-1f05-22a0-b007-37c1cffcfbdc,1744328376,18184629081003071760 +c47f1d93-14f5-00c6-c64c-daf6d11acf98,3157358767,834057497796429776 +94428ac2-9aeb-1729-c5cc-dd32b37e37f7,1859991311,18304079616149880795 +edf95e0a-4666-cc49-cac6-464cda2a57a3,3002392512,16648434816494496174 +2c2fd482-bf76-6513-ffde-cf4f7fc0a3cc,174590554,4628702886149402181 +8b5d6538-2d03-a532-2ef7-3539cd57fdae,3832284570,10935725072275375273 +92313d02-4652-a9c3-5cd4-5a9045a08465,1622893272,10965469407606452485 +a6742aae-f528-3b96-7dcb-2c0290a21f29,3479984992,115744853610989484 +29f54d7e-0554-dba7-ad9f-b55253a50e25,265931980,13818195262337672192 +16a8c1e0-1ca4-7eb8-51c3-77bff8becbe7,1350781448,12146260722026793380 +5baa4215-1b95-52f4-95fd-cbeab073e068,2805899863,5279376161624664119 +a9b51861-de20-0f3a-68d2-015518555dae,587636724,17018964194067109267 +9c39f7a7-8201-b42b-2593-780b93a99942,160965806,8991392506430937674 +39672a7d-2276-dafd-f701-bc34796004a1,566452110,3435439428736641776 +d64e076f-04ca-e3c5-633d-e47cf92c9ca2,763692964,2653548204827097441 +1bd98454-1e28-5aed-70c5-13c923697f1c,3421007135,17707038314775596525 +135d2420-782b-47fa-d610-3158b327496e,2084015763,4182601905475541518 +d55fc9af-e290-9891-d52e-42b61025c604,3142075106,798726477600177593 +1cf9246d-5041-539c-199d-84822f552af7,3575360417,2669828664265846960 +5eaa416d-82d0-f326-95d3-c54653e161b1,1625808150,530969064603599338 +d230099d-1ae1-6159-157e-37b562dc5662,3640719877,5479572792946376097 +a041b5da-975a-44e4-2c41-c37f6c20200f,573430640,6150434954303108403 +d900227f-cd87-6af4-d30c-e4f11bce427c,3678153212,13992525768878925319 +1c89adb7-dc9b-dfcd-4e78-ed5b8c6fc370,797460672,7041441014888505573 +dc41b1d6-3d7c-9f8b-f17f-24e6a0d85e07,277376048,6206018626916067237 +3f0e82af-6c06-a5c6-bd8b-5f0a793c4b0d,2575576509,1565083443357470904 +d014d82a-7458-9aa3-ee96-1b637813bb63,1132511899,12463928444516846927 +96ae78a3-50be-8cb7-ad3d-32dea3e0c695,3862515810,18026891371203897502 +f849c1b5-8413-c04c-db1d-f17454a8e131,2930248217,3045442943291387781 +d2dd7762-ba4b-bc8c-c56a-ec860b800ac2,3997218902,13104043699639415317 +2f8c2eba-2149-3d20-9a88-abcbf1869c43,2787512750,1022572994729648673 +51679566-a4c6-468f-f0d1-b01725b67d60,352085153,5026491802902368677 +6d23b743-48cf-00c5-cf70-ff0d3cfc1e9f,1940689774,15627793543835986853 +7240717b-2b68-01fd-1306-3c55990bdb21,344641897,1099942704510726283 +d15f0952-9d1c-4499-96f7-4e6a6db63895,544874064,3714975342597984778 +3702d0b0-badb-648d-2693-f5060a15ba19,2392349818,4262978700765156082 +0841bccc-7792-ae5e-7b3d-8f28e0f8048e,3818692757,490035857625788396 +36ee24b9-5385-5ed8-ae8b-7aa8efcd002c,3684025431,15373709006242478881 +32e4140a-ad72-0faa-5c2c-29af80c70c80,1531999756,13888377873942986300 +5905574b-32b2-9b1b-0bcb-acf533fd61d3,2645901509,50293901003701975 +0babaa66-78de-a2d7-8c7f-298f555846b3,1690819595,448241137001175802 +95900a41-4c43-b0cd-acfc-e6e78f3d993c,451893659,11211236032475463252 +b62e1d79-dead-702b-ddc5-213ca441c2f4,1087578950,15625813097720782146 +2e117818-d9a6-09cf-a1ed-416ab5306bfd,3714599108,4045005021276198007 +4a487aff-68d3-2a77-b61e-24317e936a49,2848355907,6781426660816684008 +a0e60e6d-7875-13b0-dad3-a865ef8a683e,1200594350,7380382934270800493 +ad443776-7db0-7778-821a-5cacfad7ca5d,4013597107,4721459924052976397 +59c44c88-bf87-aa21-4e52-0dc4f6ace9d3,2418653482,6373538417219975768 +eea47734-a72c-0449-3bad-c4b348844f72,1406547525,3616466280108810318 +b30bc40a-4252-47b4-b88b-76143939520e,293980049,1395960052301750719 +2e04a30b-d86a-ac99-297f-ff396c20fa13,1059523607,7904717181377910913 +3cc76f5c-d13d-ccb1-ead0-5384c989a588,1105498217,17451674520600221533 +b63c61a5-d64d-c3a1-af1d-a5127e70cd5a,145748700,3416696093611268723 +daff50fc-885a-35f5-e8a8-821ce92a70f0,657805033,6592527219978994754 +6cd6bf22-fbaa-df30-4553-2ed345d2c50c,3524883382,12896602109657512523 +ee48c82a-ba0c-7277-17ed-073bd22c8311,3276462634,12852631796545921417 +b2529ff9-001d-62a4-365d-9f5ee42553d5,848771068,2264402668480791333 +6e2a887f-0235-9e00-d524-eb883f7a8293,3221622688,13841690670688837974 +c6d4529b-e452-5cd8-20a0-82aa308ef977,1311617071,15149012939246690356 +e05a38fd-81c7-1c02-e6d0-4cd77194a25e,2745801398,10885869171558129425 +44154de6-ecca-3247-e5bc-16fb65e907ad,2815140204,1954886134318592927 +c0287b95-26f1-24c9-5f13-fd1b003d8d0f,957709986,1012097184843360885 +1261a8ec-8bda-95fb-86f4-c240a126f11c,1177869844,6653569253294559101 +fbb0e16e-c120-0bef-e08c-0ee710cfab83,2090885380,5801194450646835038 +9f57a750-fa15-fa85-6ea4-7dac3b78b505,2441214588,4756919466472588703 +b9efe29a-121d-2c0b-311c-ed9f446cba4b,2642148859,10173191278060704612 +07cfd24b-4617-9dd7-86d4-a9297a5c5574,3309674815,1799444312052789683 +1e71271f-bdf0-ac38-72b6-a057060616e7,26008626,2578917755571268098 +1a9c7077-dc23-08ad-8d7a-cb3b2a2073ec,1531591466,11208490319125964845 +db87028a-5614-829c-7b7a-1b84165feeb9,601796727,4941010163211336764 +c29f14ae-c507-c797-1d8b-535481d2772c,863484124,17847624323272748128 +62345d44-ef1a-8afb-e4f4-07cf79ae72cb,1786549049,2072820438016030260 +5093251a-74eb-b332-5fad-c3e8bdf2bbfd,1832743274,15200332295985943462 +cbce5984-a291-76de-4441-c47ab43531f1,1320493061,8415520275011974043 +a3d7a4c3-9501-aef7-0f99-6c289ed680a8,547821136,12678998742667579090 +2ac22de4-88dd-ff5a-e9c2-bcd4b3fc34f3,2786443836,16463284719165936712 +ec2a7d4a-55d5-c5e5-4558-dead13ed3e49,3506914325,3269404172471875181 +85a0df29-5fb3-6c80-daf9-1063e7423d35,1995652351,12525154059858362517 +d47212d8-8df4-3d19-5347-16f2ee199864,1028596050,10144393145635955702 +dc906fde-3e4e-643c-f77f-de9bc38d783b,2775095715,16311003026803193346 +5fe4fe23-e156-7d71-3203-f909128ca93d,4189295418,10810073186401283497 +9b8f2343-89a8-9e81-879a-90832fa51b40,3118337468,3344269856753407186 +4d2b1b55-1cc1-21a4-5795-7efb873d6ed5,2848652774,8072788520600863586 +83bf2978-f59c-01a4-3c1e-468686d9a78f,2147553075,10599890877250946008 +4a9fc75d-1dbf-48a9-e1c2-828332bd4ff8,1625461931,16540921092544570357 +2fbe141c-62ba-ad86-248c-ebf6d5ecb866,1786868252,321860767599457982 +5170ae5e-9399-b77b-a4a3-707ede18179a,693638030,14327012970356403206 +f7c680b7-c67e-012e-f727-b48da109c9cb,1795765448,11356623688065728323 +5a423eb5-752c-5076-bec3-53bc3550fad8,2091142622,6053596779067461256 +f8d15bc1-95bc-557f-becc-72e4bff16013,355539042,1173000530378484179 +d7553840-5a38-28bb-107f-a1a7f3c87b93,1242638591,3474158056796944878 +54cd15c7-ec0c-36da-133e-b66d4c15cf5f,2732632732,18434648178831408549 +dd68ca16-74d8-88f8-25ea-c0a7feecc72a,951741346,1915868220654147253 +2eda86c2-ae74-8daa-ec68-bf5214501659,2981795443,843849336487849819 +441ab8e0-9e21-c531-31ef-f0eb81b3d768,1953159234,13992041500415974700 +3abbbf73-7cbf-287d-3dd5-3cce54a71e3b,4136248106,16158444938841462169 +812e0fff-60e9-1b43-ad6f-cf248134b8d5,3010104629,6384628531737170109 +a75bde76-2036-4d69-38f3-d9625182c15e,290480424,4123685898223927002 +c3a014c3-c546-8d3a-8a67-23b9dead0f84,1871361453,1746742567959140551 +9f8e59b1-bf55-2b52-0f2a-a36849758835,1976022877,12577613027857110296 +05c85090-a094-d205-f876-dc6ba369bf44,4022731888,5339977541893426345 +dcde2484-9f16-a439-67b0-7ab6414f22bc,1989447436,8077395261529726566 +9c51c9aa-8d41-19f2-dd20-0392bfb74e1f,387364384,1843843736242045636 +cf03fd3d-73a7-bd8a-da83-8dc12d341cd3,2530844140,6624076999745792401 +bbeeee52-301b-5b3d-2b62-5ad467adf4de,418710774,6664905778978261887 +2b8d4b00-fcd9-54b7-b383-9dc41046b91f,238376550,11752214353842450789 +190cb6e0-ebad-8ed1-469e-e20846e95520,1670670564,6183672461975889540 +12465a65-7953-add6-470f-38a889d82103,3977563285,17589962709398111049 +c1525a1a-3e4c-b7fb-ed4c-40aa0b8811fc,2174957207,12481300849902917423 +9339710b-5a07-d75f-3927-c0115ebd8e5c,2165225426,9041116832873252178 +689fa65e-a550-6513-603a-a6c9b9a245b7,1447513038,8945491351499385814 +548ae6c7-c823-ef71-4abe-170bfc941f6f,4083484783,14927480316327237820 +6a121636-3838-989b-d044-7762d5b222e8,1955403773,7039001369205646700 +e82ce118-5c93-caf9-9628-0d9d40cc69ad,4101317638,10222848617923057886 +50d74e51-24f6-9691-74e7-02c11b39e152,3430416844,465641853301403907 +bdca95fa-c0cb-4af3-cb4b-d362c75c355c,48266532,4484448611207632484 +3449f19d-2dc4-5b23-f50b-bffaf04eff31,1606474871,16380129783698716403 +e14ee642-9c93-d671-cdaf-60de10e9487c,2538169878,4259901011416178255 +9378a3f6-9ecb-9e14-ec8c-f7455a88bef3,4186747829,18002421809109582908 +69e95c8d-24de-80e7-d6ea-dbe4aac9fead,107756482,8201719311138974131 +aeec2371-807a-f7d5-a025-29a3c23f0a87,2146542079,7369262563939360057 +f3fd73bd-231a-a835-79a4-3edc7036ef92,4156942859,12990963866642026955 +4b7c71bd-19da-64d9-b4e3-b328b1ff9356,171857641,9674360754609867665 +5ab71bd4-0240-2f00-170c-877bae7fac60,2030122220,7352763152150985173 +f6ededfd-3638-01b0-96d0-2afe1537bb54,941542290,1031965731488980901 +a85d0beb-d16c-fc9e-b830-8d5bbdee97fd,637950734,9486495033298769581 +bfe7c8a5-d9ba-cea5-6289-9cd4f84775cd,972666063,6185481425376562707 +7642332c-bbe9-d9ea-680f-be83f5e6f13a,2924464297,9368876548657158481 +8fa660bd-9865-184d-0689-baa95a1f32a3,4070156962,12234334214212091289 +596c00b2-6692-d3b2-e958-41051b00341c,3577513964,2164119115023887648 +3c534e33-3fd7-c836-8ff9-c43e2cec6087,2821838370,2788013851099618076 +6a295a13-4c89-485f-91ea-a5dea95a3fef,2183431643,14920171453048122344 +2f89ad13-8915-8d6d-0178-3623281d4a1e,182942494,1753359870430955972 +18956d2b-359b-e75c-5434-2631d8084a45,775124771,7351606719881400487 +ef57c7a3-83d1-83f7-ea01-2932220ac45d,2990466440,1255349360668732274 +0e7bfbb8-6c41-3a1a-7a6e-0d76a554fba1,3582539109,17722974274099559410 +0f99187f-b6d6-156e-d330-fc542b0cf51e,614250933,4649132368198774615 +466cb8ac-552f-25cc-530c-5d1a3fb6d9aa,1893765103,6053714044142102992 +bc82c953-f9be-3afb-4703-8987f9e83c72,2569484303,3584403580020040489 +ab25124c-3318-9ae2-f55c-0f8265643ca8,3441960541,190797775802631484 +efcf82f2-0cb6-416c-efc6-104951407ff3,3827839371,10766569877588960951 +8a3c1167-64fb-4bb1-12ca-acf85f02ae0f,3195178872,2677061972353128442 +b258b829-7f1b-7cc7-0f4b-c9e12b15f5c9,1759559099,16984130020151896858 +0f0dfdfd-9810-9550-ae45-6038138f95b0,554734872,13559748598917091219 +1b992ba5-f970-9c4c-a1fb-3c85a5a74190,223562747,4017260406516346721 +05ad2265-22c9-aaf0-54d1-26f23683bce0,2795917685,13565364863607538154 +4ec660da-12a4-f79c-e47e-ed595b958778,547138348,7669043344891663323 +13f24b0d-dd1e-0f7c-34ed-9aacba062668,1716933194,7076439571261216560 +69abf7c4-3fba-5cd5-7b37-26df33e7c670,2153580643,16965493907328522828 +8e688861-8922-1a8a-2452-9cc2f542a449,3338669841,5893521605528382033 +0bd8eb70-41e3-2f1b-cd08-60c65fe1e277,213567150,16721561378547156884 +65b608b3-15bb-c6f0-56d6-30542c76a506,2144542775,5632658777485935398 +cea70d1e-a7fe-5201-369d-66dfa0e36c72,3386576584,9159277077064033541 +2f9ac081-ad49-2eab-aeab-38577cf909a9,870963335,4240670832942636211 +78f4aa9f-984e-7542-a149-a78352d5066e,1279053041,3275239845480347610 +bb01ff50-0962-8944-d3c1-fe79544608a5,3451054613,3903365748834231774 +1809c08c-aa47-8538-a3aa-6eb94db69b63,414583798,10596404070561399542 +604b9652-768b-9d49-6b4c-9583445871cd,366805407,13021361997404575150 +8cb0a345-e30c-81a6-865c-d3f21dd90b88,3748359094,2043762411134069342 +a6b0c740-ab37-d62b-2670-69f1cc2d9299,4209220884,3933456384854627133 +d62415a7-8a33-5142-6c1d-3a3ca168d93a,493941320,2599795777149375596 +443a2069-d0b2-ed0a-77d2-36bf2d2c049c,3974756315,7777550774679175282 +2a9869c0-71e2-9a04-1303-a9b657d8e8be,179831028,4965719848856631399 +56d78023-4267-6cf6-d33d-df546adf540e,2103326529,11468183347845541832 +5ba83697-4f88-aea0-7282-7a05d6f6c829,1943583971,5810053955676223748 +6a7170ff-f8e8-5d8c-7d62-475b2c5d001e,3314014389,10408330392665934161 +bba1e636-f76e-adcf-5d60-503bae48c137,1330214179,4429239327784321405 +6a04df92-d234-a080-976b-2a744fdf83a3,1702509010,13361073336842792718 +ad437d34-f314-ec8e-2d78-7453c9a849b2,2003790499,611027445606133936 +3114af09-c127-a819-3a88-df3f8d2df7e8,584372088,4196105011070294003 +51eb045d-5068-458f-3b1a-e00cb77e2a89,3691068154,4864344358827147997 +9a0526e6-b629-cc5e-18f3-7b4054af6455,2808789797,7331930163370767981 +3abb7506-0ef7-024a-d63f-e51ec2dddad6,1747415065,16661429384341644511 +b34e9001-330d-fb74-e530-ab8d7ef4aa54,1265484962,11129808054864896813 +2a174557-d8fd-6076-0fab-1249db4b31a1,2163019483,10850437818796910271 +c9d5e85c-d0b0-5e41-d717-17cfd21af14f,3808741085,10849790697033504271 +c34c16db-d013-2589-1aa5-433fc9bcb466,1882477977,212967417520299133 +887dc507-ac88-f2e6-bb27-7230298b7e5d,2985998964,13630374418395425291 +7537ac8d-679b-9eb3-0927-ba7f7bf59234,49376887,530216796811814979 +248f723c-d4af-a7ee-dd9c-857e1c39c8a3,2718351706,16111660361873732830 +da937980-9817-fca5-6ff9-5cf7f418b304,1341735189,16906864655474790267 +a4966814-cb07-fa36-0ed8-63cdf4542a5a,2713886460,16117751473549629449 +1c4799c9-bc78-8c65-1e82-926212b7ea11,4228049991,12675881094633072243 +2e6504fc-4fa6-d0a6-77b8-a0c9181d78ec,1746536943,17672452409594499311 +c6515b50-a0f6-a481-22c6-1c4753c8d4b9,3802099984,18199090598161494846 +658100a5-ebc5-39e4-6f04-3eda9ec95b26,1200844905,13745073237804407192 +3890fcb3-eb69-af12-8321-21804560f9ac,1909584295,5523738196012765429 +1ef6c5a9-8812-0587-c371-8d85d4fbdd9b,2169345231,17650613396745143728 +413d06cd-3370-b03d-c89d-ff70caa4a3b5,3905225741,8210530549339504345 +5bb8e833-9ff1-0f3e-f44f-93cb4fb27d24,1094393209,4220847562970267547 +5884533c-e87c-8c23-2660-334a6f9d0d13,3446943241,17235297328879506845 +17841e9e-efe1-6473-617e-372c37325dc3,1314700159,9075710323865470041 +97d586a2-b33c-9a19-3b83-01c5648c1d63,754718309,9019908390009302030 +05bec7b9-3d01-8c93-fe65-f7ec5d4ba3ca,2114805122,9024674443953901225 +883a7f52-1607-7e3b-202c-c5b8bd3b0332,1707598344,2588880421341329530 +13e52eb8-88d5-b0ed-1988-6b15630e8118,1077161682,2630130877119553937 +df873476-7736-8c2c-80a1-0d5425c9723a,547047320,7601914469048631251 +671c5fd5-4d6d-ed5d-fde5-ccbf2774cd17,3824517814,8825506109858427758 +1bb679e2-d082-72c4-f518-7cfd88e19a6d,514504566,10382608819955323013 +58232af1-ba63-3388-d597-a3b103b1f12d,4181782225,10442422334263980781 +9ef8a06b-0a48-9ef4-ec96-f105b827a9b3,167844121,12719232375808560035 +fac5c519-c2df-7ed1-3191-ee8a009dd17f,3925169218,8981360705696010847 +c0ca24e5-540d-6514-5d6e-f18ea4ddfd11,3022243961,1787136159963545198 +dc0b5581-30d6-aebb-c008-d9d13b2a3a65,2218769163,437289441612139750 +8f1fc654-c9db-742e-9088-a5d333afe119,2832196952,13190355561591026827 +f002854f-c42c-79e1-bcc8-c429f68e04f5,1262425250,17807166057623252021 +cdc5973d-98f5-887a-3b5d-ab6d0f831550,3712285876,6192253127741899097 +dce9dacd-f7aa-ba9a-4962-f11f3e84cb0b,2534747341,4861033700246293791 +fadd295d-bb5a-6d7d-b91f-480af76690fa,1264198082,16047006160105292708 +58042df3-99ba-3e69-958b-e6a0079fe7a4,589340352,17454177067197590949 +a35cc039-53b3-2490-9b4a-3a1a673ec500,2824704839,1839800612081845550 +0f1fb3b5-fbf8-7009-9ed2-b9e450dbb01d,307952227,14869098729135471894 +43215efc-b0b2-35fe-b5fd-d6b0d5e4d319,2206763641,11916407625660197026 +8aced883-2e59-7988-c15d-ae9688315246,1191526968,10311523557711844980 +5a31e522-98b5-fa2b-58bf-5abdf4b610b3,3354167950,2109861198018416573 +667c7332-40cb-1e4d-87ff-f7773f437b74,1816719869,16293287098201872737 +1ab4fbde-13ad-6aa6-4f07-d5cdb8109105,3042280090,14768493999406242104 +65e76c79-1324-2f83-8b69-745920327409,1592686390,15383031201769347875 +9aa4c046-8308-dbfe-fbd7-414c2a8d3b68,1338020579,16816471553752398168 +5f941768-b5af-bee6-1188-2d7fa02f2106,3975869997,1075923179478703044 +dfbe3af1-48c0-860c-3caa-f7562fbf4ea2,1793414539,16452643609766056841 +9fba7e19-0b39-4fcc-e6e4-777c44ab5ea1,1072746922,2375375491166426632 +18dbf112-bc75-92ff-31a7-66962b58c435,3403696549,3450475063871082557 +68ed9dfa-c8e1-b588-ff36-a083efd0027a,2074087081,15007188789005559670 +2b88647d-ad9d-6137-c9b7-1ce264a5fe76,2340867751,2868586807178801751 +9d21c58c-7252-1fdc-c569-c8ae353afdb6,808763011,16848639166202305293 +f8975299-05e1-90d0-e7d6-c893e8d65cac,3834813148,16634042673272038528 +2c6e8a60-f117-2118-a56f-9049a8905bfa,3057787229,566857477288793227 +b2320e3b-7742-7c25-1930-c3ed76cd2f26,723603226,11884993160939588824 +1bb392da-5bc6-c489-e1c8-404cb449dd9b,981765070,5371035514268540934 +069dafce-bb69-b10d-66bf-b2d7ba57f06c,2633764104,16549636626241510882 +9a1bbc65-e7e8-15a6-8594-079bd9e268b3,1261073785,2541577726616775398 +e2ceb0dd-e06d-7b53-f7b2-ba9b0eca0ba2,2618056959,7033174577492889861 +a452bd1a-d3c5-3abd-ea80-2a8ed4a680da,1113954968,3679492257421505267 +dde546d9-31e9-ff24-4051-8e1b85c27b8a,917851510,17014272118323581297 +db8e3238-6204-a079-8099-8918025c54e9,345530579,4704753439591793061 +9a338510-5990-0e59-8227-1691d9695b6d,744430681,201090654810041628 +762b978c-3031-0947-47fd-f163765b63a1,586871331,13349106697270988271 +837f3e38-23a7-d9b9-594a-0ba66109a777,2475338286,4126836852669306607 +5bf0ccbd-d6ba-34c6-04d7-caba6b89ff71,769066816,1314775384940515991 +a6d21aff-3706-11ea-eb55-66130b89254c,1189578248,13979651295566070626 +1fe19bab-cb3f-823e-0d4f-8bf59b039ab6,2588865236,11128230033381065277 +3e2261cc-3fc3-5070-061b-f47cc5f1fb56,2752099976,10914577857139065415 +26344e75-1674-022e-ad1d-4cc618a2aaef,3839013012,14372541074558642032 +186eeb75-7eb2-b22e-fa32-9107b6afa38c,2229282677,16584443969832806126 +a111d230-5cd8-e317-1b8d-c70d57afb5c6,2748461001,16824141647945028768 +728385fb-fdbd-4d1a-f22a-c7ff4d326a3a,1471125959,2028857149147418636 +a73038e9-d68d-536c-e3cd-ff2163030edd,1288563281,4735117446776548560 +335fbeee-71e0-3f46-5b46-584169de31cc,3374744805,1431341277122908860 +740574da-fcbb-91f6-94f6-8fbef4737b19,3112120873,16059721890274321250 +bc19a467-4c4a-768c-628d-bf4a16b3557b,850900057,10672841651214370305 +0f1d1e98-a0f3-45c3-2f1d-f0005569f920,4073698091,3935117856334973665 +6e35fd76-a504-a1e2-d1a0-373f533957ef,1560667697,8494877916207713738 +7dd4bfb3-401d-f3b0-3a7b-b62f82b312ed,2576878092,8627407632521410072 +506d0c4b-2fed-a64f-cbef-acce989670bd,611547627,17637654430447928704 +428ff3d2-d8e3-de32-19ab-2c033b585ba9,3607331468,16607604340907246140 +91a9873b-2912-9b82-2d2f-25645798f0a6,748575681,2076731796338232742 +b34e8aa3-f36b-f091-9e7a-427fb884adef,275637775,14007101476358530793 +efc8ae14-0f69-267d-6bed-41086d48266e,192566162,12925738181658022547 +32ba026c-9c0a-29a9-caa8-ff5fc62f8a5b,3603662924,17419345846544131520 +098c6d9c-d13e-98b6-57ad-27898802e0ed,1583886242,12834899393687326054 +974f1303-0748-60a0-32af-386894089d0b,3347579122,5715893856383352167 +2f707dae-e464-0fb5-2e5c-20e8c399c216,2170706624,143782392265055304 +b5dda923-6b57-323f-82ff-2d0cd291f201,1273851871,16683111534750353785 +10438216-a943-2b91-c91a-e96fce18cfda,3900714271,4261452314276233014 +141f2320-fae0-2614-8cd3-f57a8d655e7e,1390409232,5107106568150460311 +fd474e61-8594-5fdb-d6bd-1b7a35a38d59,2303803822,1790554001825464848 +e3580151-3d79-8ebd-7e3f-df5763afab39,1849640580,3052271401301548861 +34cba179-0b76-242f-d7a4-6e7f78d87f47,1802947856,8898823708708300610 +cd7ca694-fc94-a030-64a9-089f16e49117,1988432847,14974630856641121162 +5fcac71b-bea3-ae2c-7c5a-13e0171612cd,81520404,7697496884948583746 +750c289e-83ac-a2a1-1363-b5e43b1faad2,2144479520,13921101353716073125 +a300f33d-2cd2-cb32-1549-6d58353eadbb,1593229574,17239746681822979138 +d6978ef8-3f52-e2e3-cfd7-b036a37c8520,3972083699,777411419370873330 +e1afd279-a897-8d89-6989-79d0a337a8a3,1584893637,13005006539886288921 +c8c5161d-b867-9406-e65e-3c71d97867f1,1367471465,5227834651658960582 +0f7b257f-c5d8-089e-b6d7-8be93adcbe92,1659701442,15626518544976168560 +71477604-366d-1e63-3f24-c38c6881570d,3400950317,11604201852502313442 +a803a0f4-63f8-4483-67df-f35382caf406,3950478757,6907806068674018498 +08b60c9c-e344-babe-85c8-987e7fb76a74,3085407711,12211373260660565415 +1c11e19f-7baf-e871-ca83-1d7f7b8cb2a9,717777446,13284700351786048030 +00ac4315-05d7-8bf6-7ea0-d9673c9d0e53,662773936,6966882813440798633 +c757acf3-2f89-a364-57cd-49c83b220b37,677578341,7742437405125681554 +f7cf686b-c20b-6bbe-f29d-5157bd5165a6,2456541581,8501870991622081175 +0a5aa67a-961d-6fb2-7701-cf6ceb8cde9d,1599064497,3169179236640739016 +1d7788f1-a1c8-db60-d2a1-31079e30956e,493434407,2697928983082261658 +6b16bcb5-45c9-94f8-5967-535f9224dc4f,1434995339,11848747822711532433 +9c335204-17b7-4c70-81e5-fa44cc7768fb,1539253354,15148897179124516093 +8ad489c1-350f-a1c9-96c2-dd8a283f9b80,993629137,14120683775589475281 +183dc5b8-42a2-bbbd-7270-c6754219d206,2336653583,2658564046181862819 +d9cd80bc-71d5-df9c-0d23-d07117910d6a,1134705372,16799335698761668639 +60cce724-9dff-84b7-af53-09be3dc2b6be,2635746994,10592255423842214557 +9edbe0e0-d42d-d2fc-ddc5-6fddd2e5a350,3016347217,5813870943363065221 +70d912b9-1ba8-4a66-0626-a270e6760dd4,1734067219,13811642363568765085 +c563cdb0-3c93-a83c-c304-5f5d2db6a42a,533891756,807653224432100352 +e21e070e-9830-dc0c-2592-76208cd7750d,2513169905,18061899841254571720 +7490c9f6-b836-02a0-97a5-3783941df32d,3556751917,3364474321522565683 +b11a1040-58c8-921e-c292-cc2372e3794d,1593041836,8834138574458642948 +7aac4855-7987-8834-2ddc-74e887f25875,3575585737,15888379631172748716 +640ebedd-64c5-b42e-15c9-1acffadf939f,1281386037,11747730499362268339 +bc3ad140-ed21-8c94-af9c-11a314a26f16,3609747391,18016791061908436164 +0ba608d9-a620-26bc-b45a-84cef9c37ce6,2959154403,17400135356735034543 +c14c4eb9-5b8f-798a-6ed7-b06d0ff10e1d,234504964,12921588165617121469 +84eed63e-68e4-d59a-3999-c912acff92ff,2140158130,17837202638561707166 +06932dfb-9f88-a831-906d-b7a0d116abe8,2972465990,15690634286567221016 +f02e0c48-6e24-94fa-7a0e-36faf46bcd1d,1115589019,18173177394265645530 +cd060c5b-25d7-0c9f-41c9-6a52bef684a1,3999416803,15523415202525442606 +b1a81be3-4924-1715-34e3-29366e7ca962,3792401103,785210625949958387 +3dae30f1-c29f-5cdf-00d2-1edbd8c63ac3,956444375,1630882771160504746 +35ef4386-a422-2a29-760a-4bc9f8d37e89,1983263128,17384532455663327638 +1e0c39d1-46fb-461e-4b23-eb41bb6e20c1,2956859041,3362677932822490527 +fe34c34d-f471-bf87-31b5-fde30330411e,2968832923,11978302592659061897 +f160eff8-c953-875e-d4d1-5efea0f26d23,3068877236,3061671032600403509 +230518ba-6255-314c-57ec-0f01dcc5cd4c,2662141355,16653544069482507836 +91085d77-162c-0cd0-25b8-dd1fb0c53037,3795777922,4620527265369346606 +ecfb0df5-85b9-ba87-fa08-51f5d0e5a2a4,2514435307,14962165032394683125 +b63d6574-f44f-2c3d-ac41-47ac2993d57a,355179850,7390993801337529470 +23630346-6595-3a8b-ab8c-5fd5cc34bd3d,2581304408,4029690913857449293 +fd92af35-26f1-6b4f-330b-ef5a461e1f5d,3844434640,15238372553067134926 +4655aeeb-f358-9157-e28c-ae4e71d0a402,454230234,12478901303743213740 +625629a7-8d74-c0e6-5ec1-e9ded3ced89b,2532201606,7527377289996062858 +22b2904b-0112-ab75-5a1b-ac123c5e91c0,2847475134,13842461139070821471 +b1613352-7fb7-a29d-dcb1-fb23cf7f3d78,669781934,5547809245532580743 +3f3d4edb-3fe7-84e3-f730-600bcd5e1215,793391238,4807814312987848257 +21a280f9-8dff-47dc-1ce4-6cf7c3b1d5e9,2006932123,7586879053976939032 +0bcbbe94-bca8-3864-e45e-01a2d800cc0d,1924330192,15173001306156472485 +9cb2d0d8-940e-d307-a5ff-159cc202fd8e,3471877017,769702583321727764 +a2e42f63-f835-95d8-627e-2f6372976cd6,158334331,17116779380889605190 +3ca50540-f3fc-ff9b-ca19-3520617674c9,862360073,8335486163951974999 +44401dd2-9aee-85d6-ee87-c09f80000bab,3304823024,14510939711457081055 +851f8792-daa9-ff5b-bb08-0973e8739b05,2881447059,584855998825222307 +01f371ae-3e44-f882-0976-6d96a0e0bc1a,2150171880,750358728879877545 +784567b5-e963-f78b-3b82-b8a93a5cba80,541004861,5850046684662939769 +1e40a5ac-0efa-8ae1-fc76-b2078f9fe1a0,2504464478,17122835905056778079 +7211f2be-0dc7-9eb5-dde1-c73d031bf29f,645142600,12500509742166209723 +d5211972-40c5-7c20-696a-5bcfe92da8d6,1285754771,10080685564673362366 +5a5dbca4-ff1e-204c-c9af-5b71de7f8df9,74493374,13298545065100235880 +f635844b-a403-b624-cdc9-cea0c98d4824,2268908096,17020142442960387851 +396c5c61-e3cc-a2d0-56ae-a0acbc6d6a8f,1045248587,4242327917418344064 +0870236e-a6c6-04bd-4e49-9aa1a59bdc0d,763555314,14156638075644641003 +5a2175ba-af50-9c44-48d8-f7845ec9cdf4,464956753,575868973838987430 +38f30dcd-014f-e8e0-996d-014a32948f40,2410524097,8763985268687032827 +b98996af-b91a-5c53-c25c-6e0704be1731,2600814388,7423028080267915581 +a64f700f-eed2-daaf-51a4-8abac7f76bd7,102781851,5297616880202102215 +0d5fd96a-2285-b70e-e3e8-47c0680eb6c1,4185041704,2794634361231054936 +1fc381ac-50ae-929b-c3ab-b87c2fed9bfa,1032819986,8812555837877489157 +bf486ad1-4e5f-85d9-dc5e-ca451d061880,2469034967,12330841006031838834 +da84bbac-c2f6-5c71-91ca-43438b92cf3c,620972973,12472526443394686168 +38dcfcf6-27d6-ce4b-53a4-b2fd8e2a1c0f,3347549006,9784879548969704508 +29bebeee-2221-fb75-91a0-97e793ae42f2,1271879368,17514490929826277559 +1696dd3e-a618-37f8-ec81-0ed940e8fd9f,3495469203,3445946031747272596 +69ee7d7d-29fb-f4a7-c295-3455fac19755,1242408861,9993679831049897834 +ebde319c-8e20-7787-6ba1-3f8ec4d7585b,4256158818,16785556566312067994 +74a1c4b9-a89b-f1b8-09ca-5320048b6b10,3267310223,7505634958608623113 +e7c198b3-cc2e-eb68-b08a-af8445823bb5,2501368937,9504961238203486581 +6e4cc8cd-d819-690b-923e-9bb6cf54b587,1360478144,17105882354274947549 +4803feb6-35d0-6d79-46fd-6bd876f50949,3971836961,2161202127342767133 +abd09834-63ba-169a-f613-d371a523b0a4,845675884,1362456096345782865 +1339a221-b3f7-1663-cf87-02ec190f9c1a,2446398731,6169431766355756300 +ad523844-6431-6fdc-d39e-b1eab5adf9fb,2064967339,7101946859818427521 +4fa1424e-f63d-7662-7290-66a73c5f94a4,1441968781,10951510606371073939 +ca68ba80-73c4-549e-de38-9f846b8bc663,1664740571,9292586206394913035 +a65bb6e7-7af0-4e56-a2ff-669056bf05a6,811192785,14208264568468355470 +21ae59c5-9e09-8f54-e4c3-8885c0eb2232,1602626831,17985137047163129808 +35842363-13cf-ced9-1b0b-c699948782fd,1404455203,4326090337696670778 +fbeae81a-227b-2f45-b5f2-4f4cdacf1452,1144346247,887025118072348541 +c3042964-9101-06cd-5328-d86f80011544,949101066,2665207589057965052 +2db6dc0c-ff41-08f0-1c04-0a83bf0768aa,1044807935,3274446559307223658 +7a4d7a6a-ae75-1ae0-f854-518a498eeaba,1747244700,11240702204765229683 +2ab9b15b-203d-d5bf-7d61-3bfb7bc06162,610465462,8715327060418175066 +d99a26e5-07c3-c5d8-d4bc-f06b88898a18,1926859021,9872855821597069055 +dd7fb2e9-0145-a9f1-ca1c-e05753a2ab33,3584434077,16710509953772836374 +e3e2e05c-15ee-8cd9-c310-60667fa7009d,995747038,2303922384458388617 +59ac32a9-f65e-8a7f-9ece-b1543f90ed8e,3936548097,17658908326336064892 +9f59e9d6-909a-f314-4937-93f0603bb05a,2752614703,12485667594005690770 +66e1cd7b-074f-50a8-444b-6fab55ebaa41,1282008494,2994560561989693798 +61485fa5-5314-7f22-fd2d-b2ab5788b14e,2782853926,1312096495261716189 +91e757cf-e0c7-d2e9-f61e-fa26511cf618,2245683442,16883569438363047633 +e0169dea-0b05-2fc7-8cfb-6a056747ae0f,2114408900,9273870247917207185 +07a69fa2-de21-9a5b-bec0-03bf3cbf7178,160313412,17794883055465248923 +f6ad2eb2-436b-578a-a2d2-76cc1096b517,2814120404,868697581800562952 +754d0500-0ce4-5a13-afef-f2d8b1a53dfe,1744293555,17106560014375176571 +e83ba2d4-abec-e6f5-a5fd-9604200199d1,3664030101,2347883549735845337 +41ae511c-e23a-7f84-26f3-10d35796d4c2,3843815341,14446493081608251506 +57e2c730-0bf6-fe2c-1877-0db807ac758e,4119461380,9590116982785621107 +a9d9c9da-ce3a-1947-9f54-f89e57effeaf,573880092,3019461846105989826 +08bedf78-5d76-b3c9-9e1d-98640aa717d8,1819018873,1543684411238731168 +93d648c5-708d-b898-cc4b-43c2617533d4,3984828016,12842573310412354265 +20ee927b-05ed-8153-108e-b31e00ab6a28,3953661822,301514335926934437 +c8ddbc1d-512d-43e0-deaf-a1fc7c9fec0b,3212411126,18395282687180035843 +e7be542e-9498-33ed-6742-cb54e06ae75e,104428667,14291909541868730255 +7d09c45c-2f90-8dd7-ec86-bbb8a47470c0,1574425969,6814162139876242024 +06a199f0-a1d5-6171-d967-8b093749487e,376776600,15628477924345396327 +21084f5d-9624-fe0f-0d10-6d8bc56d2b38,3165348894,13285367119624002942 +d1f529a5-de95-c5a4-9c8a-1423fd5840af,1658195699,13513830596763615719 +f962daac-6dfc-1935-f74b-dfad2c0364fc,1270176826,12481680964004608925 +048d92b5-3893-2477-d0fc-3dc4be2abac9,3916777516,8371253848097099931 +9fc27bc9-b206-bf25-a5c0-1dd5bf8f9595,2729629615,11435672032735241773 +c433a91b-247d-c191-4e20-94067aa8bdda,791966522,11815818300561110821 +2679bd27-642c-ff31-6057-8433fde37d61,162866674,3422440426042373892 +e792e1b6-e4a8-4eb4-d901-97f555d05c6b,792790565,19317336994826135 +054d74ed-ad39-7072-0bb1-cfc6489e1e5e,1251788961,6961767406978027100 +55212110-22d3-2286-d0b7-3b2d934a4396,1374929047,12528425963347021596 +871e9082-7e4e-2be5-5468-84d70f4806f2,4252429527,15765278164502356360 +b11d05bf-ca99-f47a-6d8a-9e917d2dcb74,1757453283,7002494470340171363 +0fd7e999-63bd-e36e-a08e-13aaacda6434,1656093406,11291531060129038799 +a95e00c1-e5e3-bfe5-b924-c6b69597fb06,2295136384,17175306065813575427 +d6a1c5e9-b500-37ba-07fd-b39bdcf15c82,837031123,15555418167123465451 +1ba60585-9f4a-2bf3-2fa4-8fccbe460cd0,2080525089,9596839209090261266 +a56de306-59c6-cd06-5734-3af7aef75952,1287648379,16397487276403987969 +e8bb59c0-a090-2147-a0db-55b11503d6b8,2241620734,11578183399310527752 +b566db8e-2102-1f17-1dc9-4dabed7e97c6,1529971128,1965680709139994717 +4ba1afd6-9156-ec33-d66f-2d23637aabb5,1382278408,1509341712254885698 +5aa97e5b-3a3e-c6cc-5492-10a45e667b06,851622382,1258249684031767867 +7ec0c759-310f-cd74-4b5b-b786fb7fd3f3,762511077,16849076884968578357 +9e4ec9e9-9b84-84ad-ba62-d6e239ff44f8,1167645011,17205623002755375660 +fe16b503-f40d-bf0c-d44e-617ee4f66441,3210711059,2959626174098399603 +8be08510-ffc5-0bf4-4c8f-528c528f2ec1,3995961162,4292524598275108573 +9cf635f0-7f35-0a56-d7ff-09db501f8fe0,3884983737,4411221890811051418 +167c5980-5927-9a6b-b201-59a713ac1b41,200585539,5124942065262568103 +d1a3a68c-d9b1-1787-b898-763fddd3c620,3000754554,15189013489761295629 +7ab007bb-e1ff-5117-d266-f8165b20681d,3797014246,2364450273797435431 +cebb04d6-5ef6-614d-6f62-51c90dabaa3d,3036790761,16564356947005097064 +fe1f8854-d375-eb55-26d3-86ab0d98f3c9,2751125333,865338203778008999 +b84bae15-1e12-7efb-fb4a-dedcb8de265d,1000497223,14810022081014385093 +26e34ee5-0e9a-2836-d5e7-984686efe366,3293142471,11418293606775521666 +3eb34eb5-433a-58a0-fcc4-45148b268c0c,298285919,16116445947545370530 +2730eafe-91aa-2181-c11c-a0321841479d,842929961,17351706661833956083 +a3a5e349-a3c7-7e4d-fb37-0c8439e56546,3301976715,18068405568208046560 +e7e4291e-8d8b-cc5c-dff8-98f780b85307,3747149964,14800819828992383997 +bc4cda52-4087-2585-edf4-7c68c9f307bd,2870730955,13159880256486894667 +0f194fe9-aef8-77b0-3ecb-dbf315362763,3028028594,10207888294437366250 +efd7406c-cc41-74f3-6d9b-7516851873d2,1265472037,17466741699421491080 +9094caf6-9ff7-0917-d170-3a9b2ef4423b,2247461836,12511787690875690849 +cfec864e-d034-61f5-38e5-e2e59035942a,2235447738,11646820720098662914 +4adce007-feff-c475-dc67-1067477b8ec4,2321268908,7925040134118488643 +d988fbe3-2eb0-2a90-2491-4cf5e5935b74,1456663625,13115951101399279972 +d289fd92-b79e-2aad-7ee3-98bb31e02c33,3578896435,5177896600209473354 +665e19c7-e8e7-a182-b2c9-95fc862f436d,1902527196,12514373458109717660 +afa1c52a-14d6-3818-1bb4-d2160c174aff,2753615387,5952937356025745243 +8d101291-78fd-54fb-56fd-f5ea5eb921a0,4006045935,9994138440080809047 +7d3ede6b-4672-5274-95c3-8a05d0fdc5b9,2308357908,13842022622989285515 +816c2f36-0f3e-e867-a861-f94615a321f6,694246312,2824491822806503475 +ab16f6c2-3449-e3f7-27fa-b1baff75ac4b,899927396,6342444297120362010 +2aa127c0-4e53-2b87-f5ff-a858b1cdde9f,110333238,12800527146473051367 +fedbc8b6-6c0b-ed36-c75c-78020d72572d,1410508199,10926932290150822252 +c89416aa-8b4c-c0bd-8bdf-d3eef4a43a08,4158870177,5319272498642777357 +423a02a7-02bd-2f3b-329d-a5ce0b3c3f45,366026388,16398296654288890088 +573a818e-14e1-fdc5-c7ee-25fb8002f8c8,2853269411,15387698900658240374 +ddc377da-feaf-9e58-02b9-b5d8dccb7b27,3838616796,5355505023284597457 +ff92cf12-88fa-7c82-7ebe-da11dc6dbe83,815780781,15023358287447042804 +6abfbc23-129a-e8c9-b7f3-3df119461a2b,1160158626,4549917742774309834 +528eab77-85f7-bef6-57b8-fb794afb0535,3149636814,3028921038333849919 +242dd0d1-b3cf-ea6a-30a5-c405cba0bdc3,3156031995,10326974783738008936 +ba5b4253-88d4-4e4a-64fb-9ca4eec811c9,3498905579,1081665383866783059 +68f4ff5a-a67c-7ff7-517e-cc3469ae0f0f,4184306894,18017987388474006072 +aa02489b-2152-5f68-ae7e-e5506625bc27,704190543,6362133328953052310 +03a44306-c4d4-fd29-969b-22446d6df008,392143350,10794548663487424195 +beb77357-e4ed-826c-c69b-a8e6c8fe69d0,443963275,9230827350698653050 +a1ea5f2d-d0bd-7a60-4ee4-b4215e060f5a,1929037939,1692606125920400265 +6a02b1df-ad7e-9bfd-a0a7-c25d7a0ce5d6,3660632395,3668392881219338209 +1f558512-398a-3c25-842d-c4450b374b81,3047571271,9572405880164928973 +bca9f47a-dfb4-d847-81d0-9a12b6ec00ca,3732738513,7947197098969825657 +e92fa874-0a34-987c-5706-77e01aaa3d83,166499827,7956812068237374221 +2fbbd6a6-3a11-291c-7260-8ad66ece7e9c,3797344872,2446075633575910909 +b63891fd-1d1c-3f84-dbf8-6f8e5aa741a3,3901078554,10314989605374193640 +802757d9-4a63-49de-4b6f-d59fd091bff4,4139408207,2240008264589131023 +1a71da6b-7ef2-c808-96fd-a6fa2de265cc,2973329143,16200293976218626619 +fbd0198f-c694-b75b-464c-c3db07c0b8a2,2729341250,12019078145662328501 +b819019c-790a-234a-2882-cd65eb795412,54513900,6276529672299818526 +10b91fd1-feac-0205-5118-ae75b7c5ec01,2415316396,17188185971440788717 +c6cedf38-c85c-127c-baa4-4d7ba1359cdb,2103442407,6949401549591297225 +7e1f250a-91fe-04d8-0d08-690734436200,1958623535,3166359760139099102 +8fcdea36-10b0-903c-702a-8bd306a86085,4036263459,13346563732558267214 +79af04e3-55a8-9132-b155-5b9a7e1ad51e,4022754110,12032931811751994493 +0cabca73-76a6-a7ca-ceb8-7d7dbea30553,2526653286,13355794107797304597 +59b067db-5210-0473-dfe6-9425c010b826,2374790220,3048749213356400097 +3ee51ce5-f019-289c-308a-50e819280e8c,2812245942,859191942548110187 +1c68096e-9c9e-981d-513b-f7d031564919,1065581890,12932536581199059199 +98809c31-23b9-5d4e-7725-bb927715ec79,3646931563,1965815421581551414 +df1c5838-ca3f-1420-cb5c-cb30f2180982,2495781891,3450810696945119793 +cadbe52f-84da-1542-999c-e6f78b718c4b,3642400482,4937650656022072045 +900a341a-278a-90ad-3de0-27dd25f9b724,1629244301,4625673185069459702 +b9bff808-0183-60fa-890f-d39fd26cddc1,3516601382,9796672737761530026 +b543ae52-8393-48b1-5577-7bd27f86aeba,97932105,5527240556329805233 +8f3eeb9d-c53b-f4af-5570-33686d719c52,2309712193,14568145881290698419 +92a0011c-b78b-d007-1768-418da5083e79,134790565,5000020863991002027 +764ad874-e3ae-7795-512f-bb4e60992918,3936247053,9362004812844169734 +29eecd0d-c4d8-b8bb-2de9-853f2c870f59,1114561073,9840870808982259222 +e5ab4fb6-f13d-198d-4931-93869ba9caf1,1052943203,4391636753540242541 +1defc04d-9768-f671-8a14-61b57db86692,2661951468,4346900052772244217 +d44ec569-62bc-6b70-1505-a8dd7da2ca63,2914610161,14941689917393312155 +28a25b55-6511-b85e-b807-09692db963cd,367609937,8191180834527273058 +47ca8e35-a792-660b-803d-69efa680a7d1,1314155477,5889141576939179696 +8779266a-2f50-bfc2-2c51-abba559c8f6f,134380272,12674978118593955841 +355bb868-72bb-34b2-ac74-8f50821272b6,983699113,16933368317205816246 +cf7259c2-b913-2ebd-30c4-bd78bb3e973b,880133262,9392748397266666390 +fb3fca6c-4935-6211-88d2-2a67f08fbd2c,3223990312,1171483095173653357 +8de8ac6d-eb62-08ca-b395-c44954d47571,523121498,4292061148720004607 +18ebfc4b-8926-1e65-7be1-6504339f1990,3362807890,16967636498413679333 +fda95f54-1113-a40f-816f-f99d43ca0824,4271162382,5050669600783318192 +dcd9f8ce-2a82-6d93-148d-c69a12db2ce7,1552221200,11840160116437710 +ad464bda-3d5f-5a9f-4077-ee29c15e85af,216145414,5863283255276435753 +947d0cf5-8ba8-0f68-2e3f-4296051a18d9,2958342101,8527206541035866296 +c5fd9e5e-5591-d703-6b04-404388fe87af,653950443,14689639531249348787 +3d8d58c3-1753-f9fc-3912-6accc29667da,1641807078,14253506048285127293 +d68506a3-492a-d71f-37e8-9f93c9ef0f32,1365458993,14052716124784292450 +58324ba0-6315-c55e-16f1-384ebaa88f5a,576618388,7001675749935040070 +837336de-8b6b-4d9b-8293-4bb2624a0a83,3800947154,5695277229983548419 +71bde793-7c27-e9c4-0532-3778013b25a7,2842216435,9428802767329742253 +16c82364-4d53-e58c-c094-530580bf2a75,857341698,17673764232120367155 +ffccbd06-e38d-5eef-99bd-cfcc18b2f77b,2801479017,10870356961040198191 +b8f1e26d-f237-7aa1-bbda-60283f03aba4,3973361214,6075055912485051693 +dbd9ff40-e92a-a883-5a5f-d03886620813,2201540381,17654207605882547819 +a8cdfc73-9424-54d4-b801-f2244159af4e,3534824912,13395497584673490994 +34c433a9-85a4-6963-bfff-b4405f5e2e7c,2471114518,647932309980540316 +70b7ecfc-9837-ad05-6d20-4465803d5327,3792687187,12190066184922013575 +bbd443d2-112f-460e-7728-06be7c8f47cf,1441321667,3383807203587189767 +e5a1351c-0f2b-719c-0cd4-e82ac3b5a411,3862779073,6383407495723524406 +26b1e3fa-addd-4702-bdfc-8300531733c8,4047280261,16169737326563717198 +e283a0ab-0ebc-392c-8869-89ef0905cb52,2465804292,12180298512669899623 +b3c9a6a0-bb7c-d926-56c9-7c5daeb00465,2129594648,11447295533465771005 +17b8349b-5e5c-dc24-5a42-4059c6646719,23723862,12350324433996974074 +11339007-0db8-155a-03cf-3d8d06597669,924803264,16582210085533953338 +f6ff9e39-be8f-a3db-72c0-2a9d5dab1f7c,2888551508,15448878287961722464 +4110fdcc-82be-7cd4-e231-d93459e19682,2535265030,7563870632538878130 +503c4e09-1522-56cb-36c8-0c5c6482da4e,287239838,3557645920085338931 +6352f5c8-1d4a-de82-74fa-b44e3710acad,2233307459,7102971732631285987 +a4d64776-bc61-bfa2-74ee-651785917bf8,2405097298,17020192691170390125 +f7554252-f76e-594e-923b-94be569e9974,3259735640,17805160403595380575 +b68c674b-6f15-25f9-266d-42276c863081,3002411218,8654498028321310310 +14931162-fb4e-61e2-f24b-eb13b81c7890,3611080635,10917607697301622243 +9cddf34f-37a3-9e1b-a69e-e20c8e17f1c1,3035965965,10688576619206098812 +147b94d2-5fc4-502d-f622-c81b07415c36,1792557086,11702597920919668591 +b325f64b-2386-274f-71b0-32cf2d3f4efa,555292069,17153914361631847949 +9a7c95d7-2841-9b04-b040-d137029eada2,4213711498,15549341159797444070 +0622d9f0-42f3-6e3c-bd29-0d4b99832473,3756810818,6949123854934103519 +35fc1ffc-be96-22fe-c1b3-3afc735ba350,1725836491,8473680075096562136 +53541d55-eb37-2759-e0e7-aed15ccd3667,1460697707,2787325042851680983 +62ebb0eb-fe5e-e27a-10b1-bcae38ac0eab,2289807650,1833097691242615003 +4e6168c6-a1c0-af18-2bc6-06f2b91247be,3750906688,4296068139963602654 +b813ecad-c237-5919-36ea-5b1292b222d0,3934887016,1268043412466099940 +6feb17d1-a3c1-aafd-b954-422ca35da7ab,2520416048,15554775520608893495 +d9e3b0ac-eaf3-b3f2-28a8-9b4934b170bb,252605232,12653157007121009187 +4408f2c6-3732-7b6a-c765-275c54f10d86,1784468703,9921833803056655604 +601462a2-f56c-93da-3843-ef699288f8f5,2246542767,7210071769245601470 +0d23c19a-53e0-46ab-e4f4-409e5a349123,789923417,13998156378088509627 +1f2fdf55-740c-73fc-5cac-187ec39073c1,3846887016,7346631416995090574 +da48f883-8224-2e5f-aadb-f61d290dc3a8,3433062224,15091908610749431594 +1e92a6d9-c817-326e-f2df-47c6f0e9dabc,235655976,17602840420679634809 +b09a57f1-926c-c986-34d0-c1c655356bba,3471058332,14859992256174410455 +858d7437-6b0b-f863-b89b-2142fb5360c6,1621887128,2674802529880995306 +bf025159-d479-a290-a3b5-f59ad23f4d03,1959589800,5357381893308353969 +0485dd3e-dd14-d15a-dadd-f72c94bed366,390054199,4156811748113237938 +152b9588-aab4-411c-4504-eb06a0fb3d55,1861925873,897120055361864451 +ebc3d9f1-40b3-1a69-0c04-0e62afbb03f6,3226614407,11615467966619521637 +7129ea3c-e8e3-3770-c967-5fe30fe500ed,3586754648,3625458819317190580 +2df94c28-3e88-7c61-68ae-892fa1a6b9a4,1179477767,13283512518220115301 +ce05ef46-6571-ea26-7159-25592c9ce379,240182643,10920602645789964755 +307f08cd-94f1-b368-988f-1a5577e60367,2352290694,16972074748517092278 +8a9f42bf-f331-c4e2-a1ad-ef5de9f19781,190874937,14634705424550335717 +a92555c0-49f0-c333-c1c7-0eaca7e62a5d,224459114,9788602448379339507 +00499a56-3d2c-ec77-9cd7-fd816c3f0182,3443402297,8515956472539802538 +0a28fcbb-c777-4cd0-ffd1-9053b3399a30,627893371,7135311549493449890 +892764c1-b5a4-d645-89b0-f5e2eaed518e,56198025,3468489444716603169 +d73ac5ab-9676-b25d-06bf-ca42fd624141,2062632522,9066440079779573339 +32e223e7-f26e-09da-b54d-516c7cd9d4a4,384752432,1657153746380402977 +0a34d9b8-f0b6-b3ad-afe1-ef2d31beaac5,448548969,4346502800086844722 +f6134b36-af08-35a6-548f-63c10a220e78,2046382790,8637943349243570370 +ace63bae-2cfb-a0bc-0662-70027923182b,2060900992,15850206441718948370 +2693c8f8-26b0-d447-0f36-275e549bed90,3089589268,2804474456429620812 +71aed1b0-be75-5602-1fdc-5190b19f86ee,2616916619,2684129550447532937 +b8086e64-986a-6c29-23a0-9db2eb10c443,3753748736,11491078966667826549 +36eddb7d-f380-8531-b3dd-5770baa5424d,1307413993,4038098054330890611 +f4ed534e-255b-cc4b-5208-c5dc6cbd1f3e,3754454500,4218659466205859840 +47e91f2c-48e1-f028-68f2-69b012b422e7,4069146773,10147377378816771482 +86f6123a-1467-e675-d4c3-64f6129281d4,855131238,13415156737163959748 +baf73d5c-2917-3e3a-3362-4f52f6895937,4154586281,4440372984944071145 +419f2b57-83b5-d31a-26aa-5df72f91ce4c,2067698304,9324391960662419949 +b511e943-4f8b-5e9d-e73f-2f50d93227af,2017297098,12445030965255166320 +dbdac162-ebcc-c42c-5381-36774e263485,3395140418,14468030818798630962 +24269bcb-4ca3-a92b-eedd-f40ef2069dc4,779500811,5721892734946547973 +e3156fd5-a664-63c0-e3e2-d54522f16a9d,3793302831,9189019528858677153 +1bde2952-41a4-4cbf-1159-5b6077f1f7e2,2752481650,15376028099129349511 +2c1cbaf4-d160-a690-dc37-152fb3529e2a,3942443502,3723289297669113305 +e15bb376-075f-f0c8-5b72-22b5795fc2b5,3463154093,895962686954908636 +4ffc3c57-72ce-8953-f4b2-02474c1aabcf,846169120,4494094594370965168 +b990b99f-22de-223b-2a58-bf5b12374d7a,3852236890,13442304322380163661 +3ba391ad-8eef-d180-62bf-a7c4db4526a2,2352440586,2012032434479302443 +b40e32f0-74ec-1e0e-666f-22896e374fc7,890747033,10584410546082028913 +1d464bb7-75fc-37ae-94ca-d6ceabc6255e,2196882147,9591623630746742813 +92b65bd9-ce29-5f82-c3d9-3f1657ea28c1,1717179597,605230012429166435 +885208f0-4445-3c80-5b43-b00aeccd0ce4,881258268,13500300512902305137 +5d1d78ee-30af-7a82-3758-1ec601755a3c,2482531172,1511603078682392662 +8e495b90-49e5-2264-9e5e-11099b8411bf,1245362217,12700595246941624394 +71142367-d099-7ac1-2875-835abd5cc885,297771693,16327484088931060154 +62bdf6db-1507-e826-5853-923a6f3c7a87,894159818,16741901929772904922 +42e3a11b-7982-6b19-bb25-4061f7f6d597,2818912247,16965017306486896857 +6b474710-4b21-5051-5682-7b8dee0c49f5,94864237,5708951085648182904 +7bf440f4-1913-8c74-52d2-ebdeb9c1e8da,1896171629,3510550284153956034 +f3db1c06-35ff-ed19-fed9-a1c25b48ee0c,217380218,14848360633277128562 +18572ca8-495f-efae-0979-0a73ec39afdf,784795245,2823754444937797572 +a05e6004-e6ef-3856-ddb2-8f4aa0477858,4098491491,16929754696246808097 +1f1d1a34-31a7-d1f5-87f1-cbd467484e6f,2712493114,17375378064897624531 +0fc2843d-014e-781e-49e5-d4f3703d2560,380838334,12656228608887388950 +ca55a21e-e19f-5951-0c72-3bf6d4ebe531,1111078323,9189465501440993952 +cbb9aad6-6e84-0d46-c508-aa07291ef010,1654138132,12795284904920654339 +532405f8-dacf-1ac4-00b1-46ec744a13bf,1490124566,11672267435402604814 +abb1c630-f939-6094-db41-67216b8ea768,2294634594,17127701064646202792 +73f4fdc2-5e02-3227-88a0-01c3269b97b3,1472635548,11745709082825491232 +779a6659-9687-792c-2964-b1737ecf5d91,4206999458,4111156790311502469 +f9a17b4f-93f0-5532-ca20-7c4ff2a07553,3958424609,696583450842137428 +f22a5cc3-e3d1-a1f7-ac07-de3abb726791,444180589,16024194098494442628 +2de6b534-5253-3a80-50d5-f1b09570e670,2277489612,5112199997510803334 +e1801fe0-d130-273f-1f17-51ae91f38269,1288521125,7172423459392525383 +fcec36c1-9548-e150-d7a1-46a5677346ac,1702927385,6851147849144841230 +b23da4c2-de7c-a133-82e2-235dc235400a,2794046282,1318655045496862812 +c6a69e4c-fe95-a71a-875b-4baca0f3c73f,4087956762,3271061164284103442 +e1411de1-514d-82bd-2ca7-3d8451c73bb4,1473131981,12508933951658267041 +7a1e8150-689f-45ac-c7ea-bf5643dca5ab,690400481,8546439874213467025 +303ee89e-794d-99c7-70ce-1b96aacfb6d2,712120495,4878171985670300441 +1b2cd7cb-c9a3-cb0b-e735-f10aec76d459,2820635952,8229390526918946880 +9b435526-927e-ddbf-2657-f379f0bf9402,1165972642,12565247350472865387 +801f4e80-6ccf-2f0c-a887-a47b39da48e5,651082694,2223416289164120439 +8d13239d-228a-11c9-bdce-8d6d611a09df,3317026556,18189924042086944782 +e71bc275-e831-5c9e-d40d-f50b38a9abae,3876331908,6050557502428336151 +9fb60502-632b-7cb3-4995-ca30ae5ec205,3336463009,11042822322469751016 +6cbb5840-1f1a-8653-423e-ba49d0e83066,1148705112,17490292004677664620 +47579317-a07d-86ed-649c-61a5618d2192,2680965106,1742808653213159244 +a8c449e6-86c3-22d2-a5a1-62e20774aa93,1735056547,8958707453436306070 +9e594a0a-8eb8-023d-3c4d-9dbed2453d91,2560238740,7398839212076010207 +e05da46d-1552-cc06-4163-f64306bf9b66,1936408148,9239678418789001996 +e624c8ce-bcf0-5bd2-716a-18d81844cd24,4195394135,12567757164932091419 +cce9fc59-d4a8-5528-6a10-706c6197aab1,587947991,12053838532360114019 +1aad98e8-d5a6-152a-fb82-42e120025607,4092225311,13012623115650560527 +53a53910-3518-f866-2050-c2a07b855e34,1843306351,3657150133258919442 +51a4127a-9c1d-9d88-5796-d40bd17e77fa,3136438185,1126823757501573081 +a2149396-5c65-8dad-c5f2-f646479efd9f,2588444808,17880993548086419200 +c9d6af63-c777-ee04-d238-d1c7d3c2c7fc,699575941,5810899055582685535 +ac3d473e-524f-f8f9-e882-3db491b6770a,1449367160,15001883829676410929 +9b8cb968-9b11-28fb-80eb-2ff9e5902d2a,2442701476,9531964826605934587 +05ea8a07-a74b-032e-b335-1091e293d330,2616945424,1556076986705196197 +be7b3b2c-fabb-1b58-bbd7-0fb45430399d,2901264439,5381387601839165846 +00f96955-0078-3a2b-999f-f2c1d181e236,2159968362,7971338577859142563 +a18b6ca1-f758-664e-dc90-6d850fcde742,869646397,15159726607254919025 +fa70b9b1-fa1c-4811-939f-7ab135a6cd7e,130688264,4319035173818610785 +88691cd3-ceac-ceea-705c-e4ebb6b92621,1916915645,16636838868393692751 +024f17ab-5cdc-3b12-48ce-1c36bc63f528,1953698150,15841665304584251401 +b350f425-f5d8-cee9-b689-641fb27ba457,3616063925,14232625641841925997 +b11907a4-9e5d-7742-7ec1-1dd91bcbcead,2088483102,14742499887140383175 +017aa462-d7f9-1022-10d8-e55fcfdfe288,2885210963,18050199542131536862 +6efe9c66-a710-f5f1-5a47-d3fc1397b077,3518072463,7461132277142073908 +0ff44694-7afb-e529-f094-3088e1a9e5c5,473303695,5040657547320356398 +d49d123e-1be3-5f06-281a-5cd098bdc71b,3328040062,4759197263595686144 +45de99af-cffd-3a30-54cf-0988afae6634,3629505262,1464216259490938143 +e41f874c-1ddf-d57a-b756-2cea3ce544cf,1539854331,8430619442443468421 +dbdef0d3-2933-2c6f-0739-c79593413d34,3911801798,10157125666754539357 +fc126ea0-d853-1882-b4b0-c486996737fe,2958457241,13933780112430763998 +248d7ec4-217a-26e9-b524-44658ee27b67,562435542,15992937331893420361 +d7aec1fc-9a25-f7ab-cd11-945458f5b15e,3981681810,15779745273240820518 +b810c0c6-de69-103c-54df-3b993da924fc,1683660398,6363118720058410107 +39c327e6-6b47-0d85-f23e-4beab1a67b41,1383730043,11346385243124556518 +8ea579ac-3ba7-57f9-813e-092c22e1cab0,1061415010,1658388679393895236 +a1ab1fde-639e-8b7e-e8b7-a403d78b6a1c,1863475810,7765552519041168261 +38f64566-a283-09b7-e6a1-69633c0a0040,2515128308,8538293486270786218 +7878ba08-96e6-ddbe-5cb4-f82307962835,4193365108,9203515475995442075 +0de93020-974b-b596-a829-cbf3cb430d45,662709821,1499759623051019333 +6458a322-f314-5416-28fa-083def6116f1,2576942434,10291657073165739352 +7caf15d0-5452-87c7-70e4-9c972ac82eb3,364116562,14760669629797881278 +784d5a39-6c59-0592-3a00-4d4ffe7dd0e8,2476598424,14286572919593103715 +9f06e741-9aac-3136-dd7e-e22b561e7297,706238108,15215398062714851891 +209367f6-9d1f-d554-0ba4-01c9e4ebdbb1,683143946,4612877375559988490 +75e847d8-9e88-3ccc-11f2-5cf0f02df081,2286400585,14683378805831512301 +01ddeaaa-dc73-24fd-5e9d-824144a8f160,2842099504,12113399366237481747 +f4865e91-f7d6-4ef6-231f-45a62d441a54,738436727,7725501050974869192 +243282c0-05dd-9622-0fa0-8ba552517ee0,3965542599,12699724812070217730 +433c146d-5db8-f955-3a15-97dc5478c8c1,1866779148,10457777940401260027 +a36e8126-b11e-87b3-9de2-bb3e947a29c4,1690085317,17751657751864631223 +52e510bd-f319-5202-a04e-9240aa96bbe7,1984212011,9340355969662068803 +a6d6b7b4-6eba-ab8b-9933-f7b955f7b140,1190925601,13452213949107960686 +649d370e-f11b-ae0b-931a-9731384473dc,2514188144,13380937690923887698 +c952d814-1170-473e-809f-4a7f5e4b54ba,4264881378,12249497091229352841 +7c97a773-b259-2911-7b78-27a4eadfe4c2,3560829178,2634002642720257324 +0fdd253f-b9a6-6527-c1d6-6d5a1bfd85f3,876695111,16760496171748601481 +f2aae59c-b470-3851-a2bb-cc15d2f8b679,1054384845,3250103559849778514 +6c043396-7a26-9d51-5d1f-c186817b2172,2890579216,14947284439090987143 +bed31717-6b6e-94d9-d3a3-d24fac622fe8,1164921065,5791794642071434524 +5c716ccb-2f76-4553-33c5-d00ee135afd6,2846997614,18103152153748685551 +93dae8f4-733d-d107-72a1-6ad2d381bf49,2874922471,10574217101732150934 +2c6ef95b-471b-7915-d0f9-7b1e8ce9d9a4,3711197677,12952313234590768019 +f88334d3-6a7c-899e-2813-ab8a08dea837,3842834607,10198187340028819630 +77460bbd-d635-6c3b-0ad2-2da0f0f4efcb,2318644267,9250209962833483529 +7156bdc7-3d56-6165-7c58-2fdcd05707fe,2322484178,17761027822932455526 +dac4b44e-1001-569c-041c-495c8f80dbc6,1838626291,13903619191516655661 +ba628159-fc65-01c5-776f-0cf78d7ba10e,1467059386,2776399533509533858 +aa5079d1-04c4-c079-e177-108ba6a985fa,2659538307,9792799495555342540 +99fa0a7c-c90c-2eca-4c30-1788e94699d9,278036275,7965214007016184829 +73ccafd9-4b85-3994-919c-a945b679dff3,4214363133,1200550463794102963 +b9a76ee5-f61b-af39-9065-0d1df5d14a39,546677479,16819668201178808530 +cd1f0003-9818-55f9-4072-b570e56e233e,469302434,5853934233692148359 +2d2bb302-a9c2-39f8-6e43-1273b34d6384,706937261,13968633763862091629 +fa274559-e067-b662-4571-d2b891af21b4,2698449226,11174042147340237479 +5bc67e4e-9af6-9a95-28b0-c90b713a1525,2992451719,7560200648510642059 +88fa309b-253e-0a88-9a20-fedf80c04c77,926334792,11906257588827590434 +79a35cac-6af6-8fdd-8044-6d8ff1443066,2752093296,819608595411096717 +b92541a0-8c32-c0b5-f6a8-ccff26ab668b,2307743979,9522260091424097895 +313fb43f-cf9d-bb7d-b9b4-fbaa8f80d1a1,794593271,8970827290756949820 +360e3d40-0d05-035e-0567-02f602c8b68b,3559112298,5164994796717210957 +0976bc3d-25ea-6011-fb35-94d6f19d8a02,1840911682,5477281599182787431 +3fad61a4-3ca5-fca2-b8ea-21e9e0ace5a1,370070123,7944600577927199489 +277f3e24-4bc6-5e18-e95b-8387455cf41a,1846871503,8883213113845195168 +e70f746c-82b9-3ea6-9746-f4f261b078ab,4037803438,15054850560483227497 +b248075d-1932-e859-85d2-fd8e607ad31c,454894710,9139575422201996649 +4e830edc-c125-acdd-c7bc-0e57482c2c74,1970856080,975655765711043562 +b20ca0fc-d779-013c-c4dc-deb22e40d103,2280765181,7400096347339088291 +a1fd769c-3292-b75a-b98f-fd0cd4368a23,1249188474,8904971016247301166 +c90d4194-3342-b3b2-bc06-baee17862952,3461077242,4874221243306052543 +13d4a684-d488-eb16-9e75-7c9f7e64a21f,4034851952,8720123256691043037 +ccb089dd-b0ec-8ac5-d003-8369aae10043,1815344030,11822752476117518328 +89bdfb2c-bb22-ca3b-6afe-6d00a0fc4347,153443599,6335110927573607040 +a73b43da-3303-a4d5-ba32-eedfe745cb6a,4155563871,6181112949205094112 +d801fea8-0d7f-ffc1-3e17-a3eabf86db2f,345363529,1469192913466768506 +2d1f6361-6f23-0165-66e0-d406893cc8f5,4235243272,13553580411101707172 +27788c04-3b14-d1bc-9aa9-beffee30bea2,4068446953,9926713379390913156 +7d26501d-7e7e-7310-9b93-e36a08353336,1143852293,15055824104708143028 +791f74c1-3832-6caf-a328-e8313516d47b,2143847805,2574578305853435019 +97d653e1-d540-4fa4-28ec-6f552745e5c0,213632222,3409630376917929301 +81f7ca21-c304-2ea6-67ea-f53be7c007a8,3129589643,14913619655369438165 +e1953b4f-2c65-3429-9ca8-a49de64b2289,3152425906,9076018844228204785 +25ab9b53-f1ec-ff64-4bb6-4a52012f193a,472590657,9018602285472765594 +8ea4a259-1281-777c-e46c-5ca0b56c23e3,2573393795,11921935059633622040 +419a3550-38e1-2e1e-91d8-c00413d17134,3730966366,6730224909163886036 +42e86351-9e32-ffd8-8c5e-3fae1e3e9ddf,58837669,17108041764986891213 +d4e29e30-9d86-1cb0-a17c-e2247cbacd25,534932528,1903811656613611778 +7f0602e3-31ce-6a2d-2b25-0339a06fcb99,148449060,495880484166813297 +19363657-46b8-bf0a-13a6-38eac9df1f48,1310658685,7286324743722849118 +2c08cb69-5657-f99f-578c-176ce40ed95f,3248550653,9181737648624789696 +6c5f582f-f366-9edf-5781-50c60f3add63,2055747396,9997672543954181852 +9ac69c14-51da-bd2f-600f-9536f4881b4a,331553192,5390395143950144637 +0112d7e3-ebc8-68d4-d77d-b5132219d45c,757133418,11658059940341990426 +70068275-467c-5617-7ace-dd4c8e6ba77e,51557635,3331171986896162535 +db0a7158-d9dd-7506-6c00-3f612fd25cae,4075164235,6216295192539173268 +d04b436e-44fd-7716-1042-b198d414235f,2045809165,17108582525745267970 +df00f132-2e8f-f65e-d6d0-8f04a7aed80c,3334033317,3117875670156796268 +f3d9bf73-d74f-99be-83d5-d0ab305b897a,1619028153,5643895700339472245 +b8c9e706-acaf-a5c6-3fc1-91bc93d68472,2643852494,2827265992875762638 +221fa066-0134-91e6-dbe4-5e6d7b6df8ca,2612063215,1772352696721591569 +7ea246b3-a2eb-af2f-1f1f-838785740843,534034365,11992840964686892871 +6540bfde-e592-82a0-572b-a5c3c525f25a,918256113,4247743741730050148 +380ec1db-ee67-b327-beb3-7d720abfb76f,829232556,11117812710008201412 +e57ff00a-b0ba-10ad-1a97-026a3b276f4d,2308117729,12075423757072592672 +9074990a-2ae0-9c6c-0a80-3fb92dc63654,4020886288,7058224807736060338 +7e2eb166-b20b-1779-5db8-e90ab7680a7b,3108359360,8800470811049980904 +e2a80019-eba5-88cd-9cd0-b94f84b40333,3560785410,11285073981901164784 +9dbf4854-e19b-da20-1d29-634bfdeb04cf,1967415814,17077752077352857772 +f573c6ce-3c6b-2898-7d86-935360c783b2,3554692032,5927122509040515974 +19b75ff0-3605-18ab-a76c-e0c4a2fe2193,1026514587,4611716600408226237 +44866ff6-c2f5-e1be-43f9-a9b8ae9c44d7,2123102588,5320913895952075713 +c89fb0fe-ed6b-5a40-3ca0-446268df54f6,42941870,4196005133142446745 +df90630e-3d71-5f83-05e6-8d8ee3363178,1130058250,16696195787699053503 +d57b7fc6-b9e9-cdab-8cdf-fd9114376ec3,4196815152,11096817415248688687 +597dbee5-04f3-214f-71ce-821af26e3366,3146843681,13316390647325580649 +d6ae6583-d727-05b8-d52f-31fcb9430682,3726036363,1866822341770134471 +1769a648-af30-6cd8-e223-e864814f8aa9,372139023,5190740224653516744 +8c783c7c-c8d4-eb27-f5c1-d192c44b180b,3480604541,6516232607130152220 +27d0bc7e-a559-4a9e-1a9d-5ca2172e1833,1515853417,15920670126800451792 +30c0ea85-9297-a1fc-a84b-52487dbe0cf3,3092203807,15640320281112699281 +4e9e113e-6716-8ed0-bac5-7c726bb5736a,2252737194,15454941038093821129 +e41a323c-36cd-aa0f-f7dc-8e1877bee828,2342301369,18137501618482440572 +e8d5e82f-8638-a543-972a-9ad938763538,2076820439,5156270750763644606 +4b566765-cd0e-41c7-6b1f-422047fe82f6,539012128,11124315629736261562 +bdddbfe4-d6ac-c6a2-3694-a33ef6296fe9,3764101548,4787197122698754494 +40a7e298-b8cc-fd34-c347-a4d89aad6df2,575854094,15277122039942284188 +5942e92b-833a-2b66-16a8-9d62d7893132,1348443455,13588884985195028971 +38b83bcb-f8b6-c9ce-833b-7e9069aed814,1488126723,5345778587687022995 +fb4a84dd-76fa-d0b6-bb4c-1f6cd6993281,450998399,13787820768418875148 +c7f75cc4-f63e-8584-e7af-98b7f19f6138,612495145,5405400839072622022 +db614345-0a59-98b7-d3ee-8584e6bd13b8,2242913313,12293838389512769146 +af76b45e-1927-022e-4280-fb23c85c2544,2391954498,13350755046664100497 +c8a275c8-0055-f97c-b483-31f823345546,3585382918,11830831656613196445 +ddbce020-64b7-2a64-2198-f7e5e52891e9,502495455,8020238919611690160 +64a41be7-e4f5-ae70-0407-437c45dd4808,3720934792,7625228628779532026 +f0745667-64c2-09ae-53fd-48db2db5cddd,2288367821,15136261920876912615 +d3c97c49-def5-0f29-9704-6c7ee2cd93d8,3884088605,10263582793340421862 +21e3ea85-54dc-5406-0f50-9df0da1f08f5,3413800236,15306510433402149271 +070d36a1-1315-21f8-8ae3-363e3a1659f0,2906196549,12240161365762167469 +f2094523-655c-38e0-b6d6-20147765a8f1,2881536300,11571774143349389075 +1175a9c0-ae90-1e74-3079-988911764ab0,2630123176,11596715644997993752 +f3a82311-a984-bbfc-b6b9-9acf503c5ac6,1360554358,13289994204893850714 +f7385743-1ad0-6600-0729-60079eae7750,1733192228,12629489611509819949 +1d872072-1c06-a5f6-960d-1ae148202485,1209957176,1090665920840329796 +f55295ba-7e45-c0a4-89ae-76ee0b7b6db0,2904363351,528230510409494231 +44771ba5-4687-969e-dddb-be0968e503ba,2186351370,17963821289088221814 +3bdaddaa-4672-b3b3-1aff-b0c6c96f7348,1106012458,6315510561192235667 +a398f163-eae7-c30d-d099-4081ed5bb3e3,1800706605,12519363814463808597 +f75072b2-9450-caa2-6080-3b238ec36ae1,520219815,8406557695624130471 +1537aea8-1b25-b451-42a7-0dd6b75ceca7,4099779723,7775131688678944184 +332af8cb-7a73-80cd-7939-de5a0d40dbd9,850143195,17001506421605869706 +1b12b868-a548-d9cc-190c-b400c6368139,2376803008,8605688143992882837 +8a4b8e24-6fe7-4fb8-ea6d-a028c355b0c4,3314779745,3156822286411769735 +6f33f448-65e1-441c-5f57-fe6ef2a2daf6,2457395736,10160582041237854719 +8b0e6e5f-e05f-bb15-4673-c3a9966f220d,3858422366,7928081511894081986 +24353d08-9f14-bdf8-f612-5e0e5410609b,4059038663,6331597223820828875 +85224361-a057-f761-9566-5a19b68f63a9,1242933303,2566353979900150337 +258e6ed1-12b0-e0e0-f0e5-fdc134353a8f,124423989,16800018201884901473 +a8b628d4-d311-aee1-dbe7-d29481d7b6f7,2475920610,13379167966559798857 +547771fb-05f4-9ff5-1715-52727e94a6b0,3966467240,14283691103547629698 +7896202d-f69f-2696-0653-3032dce8a436,3455130989,16824133106805135355 +0b05edfc-d102-63ba-fdca-c944b8e2d09a,819637919,8300813209144338952 +6991cc26-6ede-39b4-95a8-f4f6b61ea902,2283899276,9351862213868816325 +4cb45ae6-1dbb-563b-d417-ba5cc54d30dd,3353290089,7356815160215645277 +3f9b16a5-93e2-a9ef-0289-18696988d495,2451390468,11561403724373011101 +6e69b051-4ebb-48cb-1db7-c4c8e8bbaf26,2233783234,4311843099213944151 +f50a0c1a-cab1-2fc2-f600-7870e2cd9b40,3993107585,10427846556304324237 +2e267e16-c007-8ba0-0eac-5ffb4c4f4b78,3002835036,5341501929015873824 +e9963d9b-d7da-d4f0-e3bf-5d6effe0f784,3457586883,1990211602030309078 +87e97936-1287-77dd-0900-d22649a6ef99,1561686738,15860105426353904086 +a599572d-2e37-ec4b-3936-164edd8cbca6,3019731202,15688410697866054027 +d87fb249-c8ec-eb62-3dbb-db2f83eb6ea5,1966390781,14179384029118328830 +a87b48e2-89f0-28d8-3598-4d41a6a6e970,2733532269,6115805343769010126 +ca16c7d7-17fa-2d1f-0509-76a59f55cf47,2765594866,17123834108598956961 +2cc80295-c7b6-5dcc-7173-f86dc4c567cf,3825965203,11272936786905149715 +a6d5c8fe-f1be-7949-c7e9-82494a9ed3bc,4053465265,7118844567282496559 +8b023e48-e8b7-6d41-e636-e59ae53e8fa1,2445108811,382422931118375937 +47b4f124-649a-2bf6-4d9b-8b85c1817d6a,1760512351,1150843787064465381 +d69d8e38-7f3e-1d84-f6f7-d53af1cf24af,3403057741,13640761809550131599 +daad85c1-d6df-7efb-7a00-537f86700f60,3340756349,2113511398923066084 +30a82799-73b0-81bd-e097-1e3f7d4117b2,1900442786,8486332784889704941 +dc0a2aba-fde0-9254-e676-d58d7409356c,2243465671,7370017137629984697 +7a055995-3969-be07-9a4b-9b9332beaa0a,1735938767,12212655608751167615 +08e5fdf7-d0c2-f0eb-e499-0c20a26b53c2,3124406556,15360948354719304943 +7470aa9f-ab90-abb5-6d4c-1351cc45aeec,2371050192,1047730807591970098 +f3e15903-4e83-f6a2-1960-ded7e4cb9444,825259891,14186072816299030022 +0120329a-aab1-0dc9-f252-e0518c189873,2260851579,17146033852023956403 +eccc10ab-525a-787c-d6ea-353c470fb41e,2841130820,2150001534882067114 +7c0a49e3-a104-32e0-1c73-1dfe3877336c,4045755884,13394190314355456009 +a61f7906-3d82-ea63-6dcb-f122594e1d95,2911385473,303193566925501914 +64daa5e5-b0ef-e1f5-0cd2-40fbc6b19266,1432149647,7339191663048905747 +89c56ea8-29df-545b-28a1-0b5e1e6b1fb3,2537117165,15955906213019007107 +e75f0cf1-8c30-c027-5fe2-a53dca02a92a,2235101520,7514568230509408844 +63941f6d-906e-39b2-c8ae-46cbfd4617d0,2623966161,3568452633401579744 +86f9df85-a53c-c8f6-685a-3d88c451bd28,2416990776,17398278096673680742 +8073ac70-15c1-a6e7-a455-8ed4d98bf964,4228381113,8349510932895507195 +bbe81461-138f-88e5-1090-faaddf056150,3051400880,1773889055799378002 +e0abd0d5-b35b-baac-3641-36a2e6092a00,3273068533,12921923959078476176 +3a9afc8d-c979-b016-3f14-b607e8b5c2a7,740525652,7771653964024254297 +3eef0e7b-0cc9-3bad-bbae-68201fdee50f,4132954074,9415378873216740280 +e886b600-0be1-76bc-3799-a9b7122c02f5,1888918921,2737649433520982264 +4ed7806d-9e66-3e00-67ee-f06ff7c62348,3932667215,14434432951640415487 +5ef1ccc2-322b-e2d6-1b7a-555bf7e4c7f9,505787860,2228082737646035678 +3a4f8185-0116-8444-0052-7da25066c8b1,1098127692,3754245322608756992 +137306cc-a866-eb86-a359-13e813aff661,3870051641,4980335037762276803 +1ea1fd2a-f456-eb1b-6f5f-89cf8b78d2b3,1362103032,5718850504246128881 +671b816c-f630-c76c-f8c9-f68aa8489d9f,1247323096,8808552728564234470 +eefd52bb-70d8-a26b-3a2d-a06c3716d081,486834359,17052652502622108993 +e81ecf5a-487a-edd9-0c0e-84f1ca079666,1865201736,4095683903379454961 +1fdfdc0d-02c1-2961-73d3-be6eea09e5f5,2790353401,7974412100132540224 +189a799f-b528-883b-22be-543aead74825,3318907253,2693626192464980454 +07ebdff3-a395-4e7e-bda4-c6553b54ea00,1958761615,14881621682119600137 +b061ee34-864c-a2d2-a667-2e11b4a78441,1682772315,4840519656575968689 +00145d0d-33ac-6509-eb27-b6ba3875fa5a,141580905,16272628841765068693 +62a4cb3a-fc47-27c6-eb74-2d22393543ea,3197199305,11332480612621315845 +09197e8a-2647-4c99-4c74-174fbc344e8f,3707355854,8353026222357455990 +f186ad5c-b254-f001-013c-0e27d2c9e2da,1972603519,16540125843091255429 +ea8ae388-f187-3a8f-33c0-f49313fda70f,2112999751,3995903972350721651 +84094b50-3736-c421-ba12-a19352e7a32a,2753231077,16977034012790219083 +3d673ae2-10b9-b39e-669f-d9ed37115367,1887260415,12098160444503217115 +193a4f3e-0c60-b433-407c-dd28d4a39a6b,1641292758,9325459538117481753 +cfa5fcda-f12e-0eba-005a-835bf18028cc,3682563853,7123764066327132264 +d2e3f376-f384-51e9-6735-36f985653e07,1977534660,2590696323926776125 +8f88d803-4cfa-2ad8-30ad-0a27fdeb4fad,2778612044,7553992646487529018 +5b705d36-37a5-f58a-2e7e-a1e99e414eec,4187106560,3916191432741650991 +76c90766-a258-e0ff-b631-c2e0869af296,1023256310,6599562768535341379 +bdf2f409-bf52-ad85-5d27-c12c495ed78f,3809045293,11795939771645497897 +e20e2396-702a-6828-a26f-1f66d05004cd,3187292835,342304369293675671 +dcb915e4-49c1-f28f-cf34-5125ad183f0b,4000758374,2954057142254130184 +ef80d2cd-fd72-f288-3044-24783a6a2c0b,3020872092,17977729187243563821 +aeb030c1-c5ed-e3e6-bdac-ebab8ef74ef7,2430090359,2542918408189772095 +a79b295c-5319-be0a-13c8-19615848de51,3711752285,10599042723864341256 +7f283b03-5d7e-7e0e-b033-ac47bb3ff4f7,1438220209,4969440367714133836 +14129e65-a8e5-93fa-2312-9a753c9ab906,2926109318,17489566395248286743 +d21dc91c-a627-23a7-232b-e6a16280710a,3193024817,17329803937066387720 +291f69f9-0ef6-2f9b-dce7-cc88db5287cf,926497746,7441001641381063936 +125c77a1-9f50-e8cd-65bf-6ca2f72d833f,1563872625,12456439796513512775 +aba306fa-e19a-a8d2-d09f-8776c2623468,600262319,17424308978696533289 +1aca201d-dc4d-ee4c-d522-f6ed0c8cacc1,2375532298,5332783181414732683 +269acf0a-451d-5b77-696c-19ef27390e6b,1119439576,5495688243707925158 +d8cfd726-b0ca-a0e0-8b33-936ee5609def,5817649,1291094591167198851 +8363b876-2cd3-09d0-8070-659573ee85ae,1440923259,5390729855199986948 +2b86ad61-a6a7-44d7-0799-c1fc18f86355,3812747203,12377328151069614627 +476d6c77-3490-5fd2-73ef-c1d06f7716df,4276951902,665729927970215741 +6581f5c3-4907-23d6-0823-5336669641a9,1927502894,7809874913163674327 +44d419cd-bd51-6f80-4bdd-2a911a6c081a,1105061805,7955222004799800803 +61681b85-195f-aaaa-a861-0baecc56cfa5,1997515422,14317662060794091447 +794c49f8-9e9b-abe2-3edb-0fa4e02bdca8,4202917203,7372016264039357486 +c8a26f5e-dcd3-9236-b69e-9d3ce33f0da2,873628676,15414902694344123968 +c763467f-0f8e-885b-cfa6-860ed3610ffd,2550722685,16527880883541157722 +1ee5160f-9c3b-a3a7-5a4a-e0d4f2753db9,513140069,8738884455300086485 +339b4a21-8956-d6a2-7479-bef5624b59fd,2194904354,11274208396762034476 +95b25489-dab9-ece5-3c34-caea0251304e,1664446787,926609895815227658 +29446994-18dd-b416-0415-453efb554550,1296909170,10371567955286248020 +87f478e6-c196-2971-fd7e-707fcc2798e4,3562647334,6506167115698873804 +52cfba09-b9ee-7b31-1e15-1dcfcf9fd84a,1376459367,15099059967583117987 +7cd1b3b7-80ee-ae9d-d9d5-488864ddfbad,557492672,6567767864431734127 +31e285a2-835f-5757-a92f-13f2e369cf7d,3437218409,12959298057114198776 +30b3f654-1668-26ea-73e2-849a9a4b54f9,2817199735,12989355578633338076 +03afd550-e7a9-4aac-bdd6-e34084b81173,1774263633,1737821196357429497 +ebaaeb6c-42c5-bf8a-4340-dc98fa5fd553,907438981,3558034592090709843 +27bf757b-394b-bf49-4dce-d9bdcd772f59,3590846913,12386517477305272705 +cea1c39d-9e98-ed78-3a77-0587fc62ca93,2460872219,2697323452875449057 +dbf9c102-1267-16ae-cfbd-0e7f66fce770,3635141203,15608825396730822017 +44c88255-3d29-804d-d957-c1bc942bce5f,1078975902,7953143362951680404 +8f91773f-7a25-9c2c-175e-8c4e840cb617,1618582738,11667493864334854774 +edd98c15-f38c-1eb6-4666-b598820d73a4,115864781,14862967663253885882 +e2b14000-fdf2-1098-57b6-6c8313ab3bdf,602837016,8926065201582170360 +71d2aa4c-c23a-93af-a364-3bd698dd8a78,3578095186,14121760951463685883 +3568a1b6-3d1f-89ba-da0b-ca79b44d74ea,4044071962,2591628661123058315 +173e04c5-200a-106b-67c0-d06b35617635,2674506817,9009836035735899657 +973a2762-b1a0-c3c2-b6e7-addbec16568e,1755091830,4526660965274611878 +40147ba7-b290-cf97-bf68-f78d740695eb,171189123,16174196333681083561 +86ae935a-7bc7-a9e9-d97a-2db70e817104,2303986065,651597335703204376 +4305a396-8191-f304-4449-9c3ba20ff049,1220786907,7977113563388368660 +796ec2c3-40f6-792f-492b-8d3d2c68826c,710787487,6693854741609554405 +fade7b9f-142a-0502-97ae-9b1b251739b1,2953589057,8048015447756948067 +fbeb53ed-2cdf-d67d-0a80-6533d048e97c,1776087767,17733915719378256968 +3943bcb3-92f9-4193-9788-e4dd6a058d63,3210416593,7932923436980022084 +b8413314-ffab-7314-5b2f-376995b05a92,4084071457,6045280741250355322 +8f9676b8-1fa9-3168-7a00-1c833d311f30,2944649450,16794289099459762022 +3448ab40-a9b8-a14d-ec7c-e09b041c7d80,2325551676,12570190801642734909 +0e6a2f22-de7b-f272-323c-fee1726edc82,989989300,10302281455372869301 +1e1fcf6e-eb71-33ec-828e-72236c558992,650983156,13074408543145346450 +74610c98-cbeb-dce9-fa9c-d3c47fed8f26,4097366283,6848170429998023445 +c6dc6115-1f7c-13cc-6e91-d91e8f9b6820,440402120,15395814858809095678 +725e3c54-0852-9dd4-3f1b-152c6057d59e,2535863964,15387448624429632458 +90497290-58f6-6edc-7f53-163b22831859,4100364046,17567653486742374407 +6c53fe99-e87a-e61c-65bb-9ece3cf54387,1470302421,11256728259571986225 +89823e0e-0f4a-9329-1df0-9f541dd055f8,3600157582,8899791554218334511 +bf52a5a9-034c-854e-d835-a3fc6a0e2526,1196570713,18118572541329409332 +e605a1f1-2b09-4be0-8b75-a7b03cb0a0d8,2636785459,13353096668234622771 +8ac57e44-9dca-1f90-e750-a92f32be0e04,3784876280,3547675168653663842 +eac46283-486a-44d9-5a7d-f9b1bf141dc7,3620241071,17918991522802307609 +7b277986-52f4-de60-35c2-0f955d9df6cb,4199636700,12479788642382809291 +a6ed47e2-eb55-acdb-b822-e82a4137972b,2236935272,11096699853512246566 +25b286e9-36d1-5067-7001-b6e1deddb076,2508486438,921228408002104894 +3ea15cd1-6fb4-89b2-9476-df0bed84b55b,2843765539,16783429817205598064 +8833846d-1316-d13a-6d25-bff9a0ea2ad2,3332396864,8155153152072232521 +227e08ee-0bcc-cbf3-c400-7b9222d032c9,2483734103,8450480455682047742 +90003dc1-a79a-4d14-4bcb-42b7533e6f0f,3869418703,10333156687449770366 +c0a667bb-fa71-f5c5-1bdb-4fd826865ecf,2298506406,5254223609422966085 +eb968816-f2e9-d4ad-5f4b-91e2bbf78a03,1124191355,11544068821240878930 +55e19fae-ac20-12af-ab15-4d905431516f,459319414,13534086621319503860 +eaf46d7b-b0de-a19e-ce66-c660be45a60f,1107782320,4264805178379701382 +cc5bf236-7b9f-d810-0558-579520687aea,1040189911,7446430042183687826 +ea74e337-9024-89e0-b06f-66b51e2d114d,2312858186,3369317377193624007 +2194ea81-bea4-dba5-ba78-9521fd622854,266601934,375744318052655572 +98ee8a91-4d76-df4f-b007-0ba56da67422,3360901352,6970132757313378604 +d894fe02-b36c-06e5-84ef-8e91d373d91c,2357694993,1141080433834787204 +fac2cc9e-9b71-e70e-db1c-0d68cdf16a2d,774071960,7102002875976478982 +bf072d44-1742-0a57-02b7-83e273d793c2,1455707452,17571929291872492519 +f1bd9d27-bba2-3cd6-3e31-362e5bfad7b9,362932062,12351588062119272555 +a4276239-d28f-4897-5c0e-d40bfe3dcc50,713663053,1937716960349748742 +885ce645-ecdf-89b1-b3f4-ff0095aad682,4130337648,4142653881325970615 +3e25d9a2-29e7-8dc1-941c-b3ea715d5ae9,827917336,16727197516211035173 +9a5cf7d3-3a4a-8c85-9f79-8ae261e3e840,4050600092,13028252510313365314 +b08ca274-9fc4-966d-3e3e-932919e4cd9b,1539489970,58057889135406883 +f0bd385e-48b6-4b15-e99c-c42a5dcc0d91,79286976,12045575868697339114 +4c56cfff-fa23-ce6b-9b7c-65ee9433596e,2027118961,7166699305615514166 +71f2f8c8-43a0-2088-6d53-0f6ade794a04,1165490011,15386713880104458283 +84163235-3830-668d-87f2-feda0ace6a44,1436712316,10780433622048329327 +33fd952a-a467-e003-221d-c950992392d8,40227735,11026896802688327292 +99f5b3d1-e301-5469-0eba-8acf15e478da,3297588490,7721864881109405339 +d233f4e5-3f9b-5172-c190-2fe37aed0579,1508983575,10050835349077403962 +f70c4d0c-4ca1-d14b-95dc-860519f402a2,3095520776,14445128840002065234 +d40a10d2-f78d-ec6e-027b-656b990e122a,500212480,6555882515805660302 +e49fad4e-e544-90de-3c24-eea3cb3081eb,3552922676,7538653153732281046 +27018b81-b0ef-1422-55df-844a5443fafb,4008349270,6889893024283826699 +30f37d69-bc47-3611-1798-dccfb8202ec5,2517264298,5352121667593000997 +2c9437f1-5d32-d04e-7c31-9577a9fa4a28,2606189020,15804774866976632496 +7807f6e6-9244-96e5-bc4f-83d61d905180,57574043,1263705000203081763 +17316a72-1299-cd7b-5429-f5a6dc9b09ed,2483105379,1412966064681004116 +2877fcbd-d373-a634-bad1-849934a5153a,1533816141,15688895212002437496 +1727df4e-326d-00f1-d177-9960dbdfa1d6,2092044605,16102943223819142161 +c8279be8-ab16-c2a4-954b-7ccd1b19611a,3546768574,13772256514506393268 +4e77bdd4-c167-2488-dc48-2b7c26ac356f,2037160459,13622967833191384172 +53cefee7-14dc-4e99-d410-1fbca18f2f7d,2291834215,11378476282446655279 +7af8fada-0bc2-aac8-13b2-ed5315c7da69,1298661518,4931948916532720830 +7149fac6-56c5-58b6-d72f-b6867fa9c504,2688952151,684542332919833432 +9ff55345-5422-d19d-5719-7415cc58a89c,2268061378,10944909416906376760 +a09ab586-3ade-177f-b76a-4dd9d7e469e1,2205001448,17201766874093525672 +7d04428b-402c-89f8-3302-92510c301f71,890551698,8049909626947077017 +c0d1552d-6c42-3f11-ba9f-311f1a132a44,529274188,16346966205436970082 +dace30dc-cb9c-835f-b5f6-610557401990,3017553379,17887106983466178582 +9b341c1b-1d9a-cb8f-d27b-ebfa4be192af,601714732,2707816592869477582 +fb171ce0-b046-2dc4-eec1-9c1352a73217,877732308,196781053167805559 +7e90c5a0-c061-0e84-1e4d-d669bb12fbe2,3403506673,6614446753619354857 +490703d2-f172-9a72-7407-15435d399e9d,3114889370,9632815671750998203 +1431bb4f-29d3-d198-fd4b-16ddbca20fea,1931573054,3829750350570397542 +01064246-215f-f5ad-e5be-64f3be5e7e51,577316760,9494487506126211948 +451ee0a7-4436-9ca8-5534-8f814a7f49ef,1223504045,10832084031656092661 +577ff03f-f813-434f-3678-358fd8b8bcda,125957332,10459462654538235777 +3ef198d1-f30e-cc2f-6737-0d5f6e58dec0,103583069,17166834431002413306 +ae40f6ef-318c-6398-eabe-5e328c37e6a3,2502883077,249721321184689541 +b59e2c01-e82f-4250-5c7b-9ad5875cae2e,1968032111,4795393011247798426 +eb174293-d4b2-4f5f-8414-b668a7fb33f4,478002472,12098150670583715764 +94f72bda-6d14-9e27-f1ff-2f6ad2f6264b,115217621,15941211114935659250 +3f050682-fbd7-f5f3-9b69-c5d500a16c5d,1590160934,10210748590691393774 +8d937b72-687a-c2e1-b53f-c34bd1c9d757,2513539277,9250582798991495898 +7f27788f-47e1-d197-81aa-ec04fb79610c,3726766730,12927968308044872816 +193dbc69-545c-1e14-b892-615d9d16d08e,1535305725,1903519723301670525 +d1b7e9f1-5865-8894-379d-38db17b61142,3301277285,5746179810258036042 +e97aa9b4-cd57-dafa-955b-09456dfc86a1,2432787133,6606543554673685128 +314480ab-a415-2f05-4ca0-57eaad67eb94,1047444791,12629047682465616399 +553082cd-78f3-a050-5de5-b941d5a8e696,37608044,899704401978337783 +1f594cfc-d5db-8c19-43b0-dc40fba2d243,1592580096,9839232327481752059 +85cc35bd-1b46-c9fe-4afb-42a78a74e82c,3516274055,2647983786793548165 +6041da99-4184-11f8-d260-4bfcc87ebc60,1176213860,10504959035706121344 +3f4bbdb3-43e9-df96-bb98-f9ddad699c3b,2350072867,10619929305779031419 +5c7ae858-69d3-b45a-a8aa-9799c176674f,350859034,13302021347011434677 +72ae6421-2081-7a5a-4665-2a29649a0089,2238899018,12355634593695214600 +6783ddce-bdde-ee34-4e30-b72f6a785e9b,553166364,15831209627622698073 +a8e41bc9-2f9e-6946-2096-82f67331e240,2043995545,16783243497663202766 +d37fc62b-2172-48bd-bc37-6c98830e3ab1,3679788508,10265211695968020487 +f7ad0fac-88cc-7861-e285-950a35031259,1521182693,8135063525351848581 +cc810e0c-2472-36b2-725a-dcada66f581d,1632525023,11547867210183855829 +a0920693-e459-7a1d-8816-96882581e0ba,1044876289,2390478184080892768 +f6b31118-84ea-0666-add0-937577523c91,416537962,17963575185267193258 +da3eb32a-49bc-08ad-361a-b576c1e97309,3136579944,16725900925129313239 +ba78f93b-998b-cbd6-a32d-328f23e92205,4082970816,10395092267283020323 +6fb1f5b0-88d9-8f60-adab-4ad21537b1a2,884057768,1848575488021520276 +ef7f158f-a63d-15c2-3f2f-191da0876f5a,57755824,1627809813220773429 +bf83f6ba-f58c-5c1f-1ee9-070486780962,3407581536,5963136129063644699 +bb89eed0-10e0-409d-e75a-ca43291af981,3705615974,11600625514960695009 +4b28c87d-f175-b5cd-a0f2-ce4760afd111,2307317161,12146136092295462384 +73b61940-a029-6709-2e41-b33ab21bbe83,433935206,411514738005326951 +72dc2c8c-8c4c-56b8-9b71-c485f5214c19,969638842,17390321930017559559 +1ca40d8a-a2f0-9508-774a-4392825a579f,1343077800,2800954887059569194 +507270e7-2c66-987d-1b0e-0b682025575c,168842644,611228894689796611 +e9d3d199-3f23-e6de-b567-3e03b2554a90,1059686459,7382742174797735995 +d300f990-c6a2-0b39-1f4b-aa9ff4702020,2149815436,15185071268677878870 +2f3bc8c9-2a62-8914-e65c-90acb72596ca,1955231029,6561870689261551470 +a7cc8569-00e0-5b84-78b7-a70f9eb02695,287349972,5432051880569660079 +90fe8294-5117-a1b8-cf65-8927dfd7466e,186343729,7688709628525734526 +f8e3a03b-4dfc-5d71-5941-3160a4add3ea,3701711849,5255090292602727848 +6fe66afb-a6ce-c5e0-370a-799ceda71290,1451116622,4937645648369409543 +88e5e365-1bbc-52ac-63c2-080e54ac3da2,4067924974,6159934244476871946 +f1a6aecc-768a-d280-9d83-7dfa70899615,2338167821,17935039025698975201 +331278d7-d821-6dba-40d5-e4352387e5b4,1285304092,14266099323560310261 +fb0fd935-c5f2-4fa2-16a5-946ff87f6e05,1854915001,13599200155831147149 +56a2eecb-2c16-b36d-83aa-cc356d33fd83,3761922772,9255198854623046741 +98d7d12b-5c17-366e-c04d-1476044d143a,1462782117,12176819573181628315 +20a3d17f-1866-f587-bc0c-ac1ae1dd85ba,65006963,14970034093343383097 +93ba7005-5543-c175-a59d-ee877eb2acc0,859786197,14763720506551762446 +6275bd4c-f541-b41f-a8b5-b1702dcef7cf,2476603504,6110678465155737985 +5454a447-b516-edbd-6705-44842204beb6,4265644404,8200404006621872165 +acbc3792-b30a-56df-541f-507136cf38eb,3407307839,12966122568509026734 +8afc796b-a657-ac45-a95b-7a5ae2af96ef,2927612530,12126229425352627413 +c0a0d9bc-dba9-8273-aa0b-97ab71a1b5e8,1668418852,3710690357679607217 +a0afdb62-c4b4-55e4-f4b7-39e5731df8e6,980476313,299032528191436065 +7f5ddbbc-86a0-3fc4-28fb-ac437739e51b,573781626,11061356139144318093 +2091250f-7b7a-0876-8378-34aa7a79fb6b,3232195379,5646809212683250822 +10a01c98-2a07-fb99-257f-15d572b738a5,1197312904,6639189231347628880 +889c141e-5a67-3257-3d4e-5a639b13bf63,2662030125,3274829901813881868 +9e0c23d7-b6a7-084e-d11b-3c76efb68689,372248204,14823465497996232928 +3d1c56f7-e1c0-ede5-30e1-5314d411e995,828953390,5846448711351730664 +b68da106-aeca-198e-833d-55aa59274402,3870371764,10287485079822233332 +9599fa80-3383-3b8a-43a9-0f3004843c44,707544370,7674995129892462527 +50ee55f7-ed26-08b3-1ef5-a477ff6becc8,1568741497,1033261433056792333 +17e3ac05-2049-4abb-72c1-d174c5dfbdd4,3613441147,11746708761564786486 +0f001607-14af-b73d-f225-3750e47bd619,3163894492,6171388972989809316 +e6b3c39f-4112-c0e0-d6c2-ddcdbdc99ad9,3832339404,13458064359150984689 +8587bb5b-af2b-dce6-b73a-10dbc3263ed1,1741664691,14187304876985923115 +b6a082c2-e6de-fec1-bc38-53c1e6675cad,3478393843,2048847945493637065 +41b3bec6-1ddf-4ed9-c83c-985b30ba2c71,3621489846,3719577826361088374 +74eaaa55-75e8-6102-003a-37831f6c1c58,2013695512,16587274199018567659 +0767aa01-dd8d-92c3-084e-05f14a944435,2926178144,6014332123069986415 +c8738673-c068-a7fb-7f77-338a5be4307b,3823121463,14647275916487602355 +45558f9d-5132-d583-5546-b04bc15d4c3f,1689339836,6625522409615056205 +130293b5-c6ef-15ae-3a1c-177e14a8118b,1651123584,662795455825280507 +379e23d4-459b-0a06-4ab8-ecba9629de53,1213797878,952668957096362870 +f869dff5-4530-f832-5bcf-610c6fd534fe,4242168477,8940851830056007830 +6d1eac32-0452-7529-d680-2398f5ece1ca,1793514227,1314658482252237802 +3a38b65d-bb84-d0f1-d28f-8fe746438fd1,2701276085,4318346751168741248 +cf3b6aa4-25a8-fae0-9fc9-65e78d72da7e,2467859519,6347886633585280057 +77fe55ed-22b0-c624-cd3e-16372691550e,3332832744,2302141231513353230 +d471bb27-1cff-63ab-f71c-e64748dba08a,1906063490,3848440904363376862 +100ad85c-30ad-4c4f-6bc6-33f1bbf1d848,1869682233,6725465995283685069 +2a828320-61c3-f677-22ab-f12351e00365,2462354161,7489919852718670935 +9ef39033-d019-ed2a-a7e7-3def78d0a558,381740032,10249099967372083088 +21029cc5-b90c-c448-6a39-ec728c93809d,276848590,5670416171902990881 +4743021c-75d9-3fcb-03f1-e09635c0cbb2,195904695,12110900027097978460 +8a9670ca-b285-3017-6ab8-f1880e9ed60c,3591401440,9214432819318922296 +310efd28-6f31-cde0-4356-395c7fbf3ce8,3166231993,2154599549548480058 +1cbe0a57-f76f-8765-b6f8-e6fcb7e26fb1,2346732609,9838676799736894130 +6aee44c2-4e6e-f379-faea-0d8f9da12f0b,938370554,1176639741787539112 +8bcabd5e-64ac-b0c2-08fe-894e999a306a,321436991,14623418078776984463 +9d3f3ce8-d56d-c548-6804-4635769ab9f4,49222568,6075050626613126574 +1a7295bf-203c-ffe1-1b18-cb8f3acd4d10,1974101997,18070731871030356508 +a41d201e-e4a0-a913-eeb7-fa8b7447565d,1668901996,5883081835358886554 +37b8a204-2666-2a10-f5eb-4348816b7f25,1503710746,11173166522370657083 +76cbd797-567f-cbea-506c-00ca226d8e5e,2920927776,3760147711320745699 +e144b338-e02a-c14d-8c07-4bb37afc5621,4111306282,15869182352067459566 +66ee31f1-2528-0c67-b891-dd22e8ffd509,3207346022,11071884461107887876 +ffec6ce8-fd92-b170-3645-89d9afd66d51,602145083,15574599384798223835 +23fb5f7c-0a35-d9be-6134-f8b176144f36,659904527,15617363428006126409 +0a5cfbc7-8e02-02c9-2248-85a5d65087c5,1667271366,2975132331295372070 +0f3c9c68-d595-884e-6d87-952e6899a1f4,3590124560,17517633291135713671 +222c2689-ad5c-9f7d-02a6-1bec0a3d1bf1,602188025,15569415383878683574 +403572fd-7aca-bfde-2407-481f048d7383,719105889,1889229751191988327 +6da8987f-6d23-0635-e316-56416f168ed4,2736211505,6851638421691986916 +472e0fa5-67d1-f780-0124-bf2a5acb19bf,3503127736,5445976873390321681 +430145e8-cd30-7f5c-5a98-6117d7364952,530674204,12132036341279420392 +1825a474-88ce-cf7f-af7e-f1e355a6b0f5,3826722727,703341594951729451 +71552a56-8de0-227e-a241-390ce87d1494,318923276,17793996872917494494 +7f4f5c11-c1a6-8943-fff9-b69b93f5b90d,1858049463,8832430649644492187 +ca4da15d-ad70-2e90-7f61-4739aa13279a,1807194796,2575703675668873489 +e026861a-6b23-40c4-1551-da4c1e1574e0,3385676345,15564416119583128074 +ec434f5e-7d68-6b39-2a37-00d528fd76d5,2875937281,8017121173540549652 +ec068373-9672-f44b-d130-81b152232451,2490376575,15545245930070732213 +bc5906e7-2130-05f2-4cdb-0c7086947441,390987927,14868147592306255261 +acca0f80-786b-638d-72b0-93bfc63199d1,669909526,8402021734212153360 +563df0da-b97e-2207-ef3d-0b13f76b08fb,2361813405,6815935344395458472 +5addd32a-31d2-4bcf-dd97-7bec45bc123e,3026450923,16496398967293756542 +4a02c224-bdaf-73ed-b2ff-f5974178533b,1923977229,11904308258530864906 +2080ff94-8aed-580e-04c6-a62d1c397bd1,2438192043,17557144433919613528 +b6a0779b-b09f-4408-c357-b050eb44d502,3413525857,4436354130443421469 +4164ca62-56ac-489b-f1f1-cc684b8cdbc3,828098603,10609400455045029005 +cc8b683f-f4c6-7a9b-6446-9e68da0a1351,3597760289,12008690087284985100 +a0d48241-8d17-9343-454f-6ecae718e720,4133900692,14798530219510149077 +3b0a0253-2236-2a6f-34a8-f8db680d9fcf,1128096946,7170384070495908259 +7565d8f0-bf24-5b0a-b28d-f832e3304418,3810245258,8784571754222603516 +929b7bfa-88bc-b530-88f9-687a4e394429,3133613848,5759951304057837941 +f76d7f6b-da73-ac95-088f-f769f781101b,1312298410,10898717896078028479 +914d9851-97af-5828-4543-dcdbbf55723d,3847579815,6877308104620290536 +0cc7541c-3724-41e3-7b4b-aef0f09950a6,1305404734,11082319813828031764 +8e5ef6d2-dca7-3483-271f-cd7cc8b42faf,3078127069,8434388065935369254 +56f696b0-08ee-5627-2c14-617e1009f5f1,1578933862,7427646094947329685 +f8f1caf8-7e72-6455-75c3-16f2b03a380b,1031757112,9126419370355753871 +0e0b1f80-5bb5-4638-b33b-66da6865d91a,2336521373,9174003172186316611 +c9a474b9-f33e-b1e2-2dd4-ef064e0391e3,2778045909,6885619544501043837 +aad80ce5-1ce4-e92b-13a4-19085c19d764,2720216523,1072408045806508209 +c4658e21-7951-1717-efde-9f5bf5ba20f5,1298734876,15726281144351087749 +ff0cd4b0-55cc-64ee-886d-e7cfc2cdd045,3963076043,6280338609818433528 +f74d44f8-0864-497b-06ee-ce763d4e6f24,3990111261,14334841441225293257 +9518cfc2-7b80-dd06-7596-45a16c3c1143,409299420,5155442761844120265 +920ad598-bb1d-f524-6dcf-0d2c90963f9b,4101003267,6438038516426764853 +4668316c-e7b1-167e-8360-dc413ce44639,400561708,290023327639119269 +3222efc6-ccfb-cc46-268b-6d609111dd24,4223677163,5226313930249306624 +bda05fa8-84d1-8a40-be26-5e3edcfc803e,2690763296,7437255116204313525 +2603e10a-a24a-862e-1a05-7aaceb414782,1563937170,12498707562364415551 +6f99c407-c0ed-1698-15b3-2aedea5d94fa,826874845,14695876149045586332 +f440077e-bf7a-6402-7c0d-99ddc3a3be5c,768716292,5714990547888971805 +90c8d703-4afd-31c2-3302-b50154cebff0,4024554389,13539780371761570691 +96823a4f-aee1-dfc8-1d8f-89179ea8926a,2319926822,13691789458887515000 +b2d0be34-6a13-28b0-7267-164ec54175ba,2820541979,12618800807664521409 +6eed23df-0027-4055-06eb-ebac082ef015,546788775,17912504980906198334 +56d233bf-ca9f-3240-ee87-69e46d11e47d,2321955584,3503591114258602409 +85454b1b-2b84-1ef7-5d46-271ad48e701c,1223223497,4226826683712750175 +4bcbbe6c-9da8-55a2-0889-4531a4847bc2,1718042486,14084191592749266858 +fa5782cb-8eae-bb03-51d0-ecc13e7f4025,124396252,12535121635226750439 +19d3b7da-9923-0da9-7174-09af5d74a54c,604178336,15577447010190316577 +60913774-c75b-fce5-d515-107783c7426f,203547021,17074231369269825921 +4b50c3ae-fd23-379a-397f-0de7f9faf697,2784672256,17836698881542240462 +c87de0c5-3f1a-31d4-a4b8-23bab7f720d3,1902112154,12921123865907346137 +a2dbc5ed-577f-8d50-fdf4-38891dd69c3f,118310429,6746387465248199967 +39024ce2-b9a7-1d7e-d7c3-891fdccdf9e7,441478759,15854568768371840110 +931ec8b5-c2dc-1e8a-8d7c-7f9103172dbc,1363918881,12308266736124489902 +4d594399-7249-ad7b-3c20-522138bf14fd,3226302188,9942940881581053871 +1bdbbd05-cb69-14f8-1d05-43cc2d10fadb,2472975161,11308734361237804699 +4e827c4c-ab35-1252-e533-c7a0a3df861d,2788131008,5617540880238894052 +006c67da-5655-b523-0d11-8aea6671f2b5,3163356879,17182075204897657583 +001e87c4-5818-f915-37f2-14d4690a333c,1322121496,7460815163094246292 +c2234288-3bef-21a4-5862-f48d845b50a8,2035336604,15016741451301585296 +2c475497-e13f-634b-31a0-3fe9ad02dee3,3145002051,17030568635869060981 +85c41e06-7690-fb60-6089-0fbc67ad5a38,3381611618,15623247809296755291 +478d15be-d72d-f59e-e0fd-00e66dca04e2,1729788316,6134716389492946597 +a01a1f2f-6f1d-7c8f-14d2-1cc496ce1614,2148910771,5968410247238216878 +6045d3a8-8b3e-a297-a9ba-424ceb6c05e7,589502750,12628968085060403823 +84f7b704-8181-66f6-c80f-a6e3152da394,1282524665,13313902707779028653 +2b7e72de-b748-d312-2be0-40fd5b1d0820,3163310698,4910995421331336627 +2cb37219-4483-e7ca-15db-724528e030af,2347639552,3869276482528506449 +9534d93c-5e49-0fb9-11c8-fd3332db559e,240274706,15342755021048766991 +e0758cdc-ecff-8e64-9c5a-401e6b8594b3,762223613,9685088496349697340 +0cb0b4d2-c01a-8136-9911-464aacf38df9,429913650,11082335047553560056 +6fa3a12f-57c5-a756-9976-f0f4718b24d9,608386844,17858225263211598366 +19aef310-c285-5d01-0aed-bf3738b40370,2234902781,1835348316426518878 +d52f183e-d9d1-2321-e385-6f04341af968,257457190,10008009035438216900 +55bcafd7-7017-6333-0bfb-9d6e8f914fc1,1547283030,14542114141891721756 +1bf151b5-d0e8-c1e6-5072-0df5c507ea10,3936650167,10812985116390830544 +0ab9285e-3bda-ed47-64c9-e0e03aa44661,3292117206,12082755114948104161 +a50718b8-4c62-4dd5-0518-71e5d31dab4f,2789247364,12594365641637159305 +fce81632-6de1-e226-00d7-4109856bce10,666729818,17314643373060011289 +142b2548-2a05-1495-d571-6027da09fbf0,3691775408,16318871290100633758 +bbfedbcf-5560-13cb-6ee6-1cd466ca77ae,4101635396,11266851288381466927 +2ee461de-2889-ca88-4c29-84cce6d9db49,2648514361,8303772131782622512 +3f41cee9-9d68-9d2d-0456-246157aa1723,1591132055,4066807692198983874 +32a1532c-5b05-884f-bb63-85b9f41b8cfb,824266910,16483403182881158954 +5f8d5176-46bf-4685-8280-5187fd850922,2420893184,4140161716236109938 +68ba7cd8-5737-ecd8-27e6-857c50423443,3829131492,11735024110741527074 +da1f459b-8870-b6e1-e434-065c316756e0,2439624249,10740896704965473383 +55182e86-3fe5-fdee-c5a2-39f90c689d2b,175768405,1808603716392226043 +d4ca085f-2b65-aaad-0e22-c4e44d55313f,3992528933,13415464681090666634 +07a7f5d2-73be-15b7-f7ce-0c156fe5ba56,667045274,17185109046568999148 +c0b276df-8cd9-6515-7a91-d08d995ac900,83353270,13693348881910680691 +165c6ef3-4a1c-abae-fc89-050dbef0da6f,2245529573,6752760424671732124 +1da0d346-45d3-7d91-c43a-e34ffa5f0f62,923328342,11096739580755650174 +5f29a109-cbe6-b503-e3ad-1e69f0a57f4b,3823156447,16362466181207661317 +65f6227d-e027-354e-964e-00fe4704b598,1941330057,7735775675237609571 +573ad961-2cc5-5e6e-6375-fc61490a9d03,3999098250,10346796583583730775 +02b7d109-a30e-58ab-3eb6-631d007fd8e1,136622987,8999379895407354101 +5647c9da-b23c-c3f5-a717-4fa84f6c67e0,521258248,983732213200170216 +fd7156cf-6c52-af77-87c3-da50980d6989,2703752230,17443408985528788701 +0b7925b2-faa4-ab92-edaf-88004c1f51b8,1783123202,12595186796179968336 +0218982d-d688-fd2a-c36c-34b00e471179,3467189016,5652130590275411708 +14d53f0e-918c-eb2e-893a-91b75e4882cf,2630309398,6836685915894116480 +a554adb6-7cc6-9c1a-f3a8-288ea1dfe18d,1294041760,1273284438588241672 +02e8b3c9-d4e6-5ca7-f415-e914ba7d8adb,1280858595,6560341070347102603 +19ec17a6-40e8-6eb5-8331-8fdd6e906c2a,431615147,9573099067453495417 +7ec955f1-acef-ed07-8f25-fa02a0e44017,3935796219,4658528989683846249 +fbaf8d80-7cee-76e9-07a5-6b23801fe213,4293639284,15020833014128753776 +cfb7c462-fa14-d702-97fb-098fb67ed80f,4281344791,3708621942729530733 +b97ab02b-d939-8144-bbbb-0354faf330f0,453113769,8569250229195559206 +c4e28ae5-e174-04d5-f9c2-62e29662fce3,3426832210,6266225940775161685 +c0f6417f-932e-f30f-8ccc-81ad030d81ed,1974125818,15836244327407413218 +6ce949fb-d4ff-19e7-46e1-99f214d4d6f7,4128540173,7105288249933333362 +6e5d91d1-1700-fddb-bd4e-5c4f33818185,339204050,10172098960570566832 +e7c6869e-7888-acb5-7b2d-68d15bd765f5,2582627463,3068465205056682401 +1ac88a6c-a06a-ebd3-a74a-caad6674f3bf,2308516853,270520757525754985 +b831290e-7a70-7ce9-2412-515d5fea6909,2282187817,11195838507804742165 +11164d2e-e63d-c797-32f1-5b5a745386de,2952612702,2121262382190865189 +2fb34537-f943-5148-3373-36a05aebf24c,1500531131,17448380373430892068 +cdcb36e9-3271-4897-cffb-b3f8cc8e7ddd,3773134768,1820244656923321037 +71408e9f-d089-f3c5-3837-e7182e781a5c,1525133635,3276875169669133491 +ff98ba27-d8cb-9954-2a68-378bd1a9e0b7,3773718946,10522341197145839110 +50893849-ee63-1861-20b6-16d7d2b9d504,1513696103,17365753869329092742 +22914a88-c684-e24f-fad8-d3a4ca3f782a,3530923228,7578702740626547837 +2b1301ea-f003-19ba-6228-baa1e98d3c3b,981800860,12241446606441315932 +ae8ef993-d610-f99f-6a17-eb4a270413ad,2229227594,8672209262290249347 +a2d157f1-85fe-4936-e906-52967bf5edec,2920691020,18007468172361006661 +f0f48a27-9f2c-a457-45d4-d3a3ae16fa35,3674777622,3203248289188990197 +e81f345e-a372-7e8f-7adf-1914c1f32c4a,2417098125,14015086615765471250 +7e549b48-685a-16a5-c2a4-4c3ac28a8fc4,788131542,14983636817372591736 +6c761f4c-a63d-569f-984f-69d849432bcc,2564212824,3632586268401177333 +11831439-867d-c919-0983-76c534d0c485,401998653,5800804689584940331 +347f2869-e1fb-8a2e-3d99-03461d1243ec,1019972928,12103649160708757846 +c85a50a1-e148-d020-eec9-407803a2edc5,2053391879,15556045974974603898 +48fa01eb-4d5a-145a-fd3e-5ee35cacb4a0,2348864495,15014301867678169251 +4f73a2b6-d37d-a7e1-b3f4-ceac13e23554,505069887,5314111046999080184 +a1a072d4-0d48-a882-593b-dbfca2d3bd27,4254180773,9171061581505685839 +bcd6155d-ac9d-aa56-8d23-50df159a9c6d,1070943357,5974801039323364077 +07b60c83-ce2f-9130-90f6-23783686b39a,1075287480,17741638830208381977 +daa2a3a6-0244-44fc-7421-cd10e6ed4cad,2663841562,5979557058127746873 +607052c4-48e6-2fd2-a241-c76d58e4b7c0,4079193625,1596365289736313782 +cd6be4eb-b478-2b23-7177-556875d4a404,2904963375,12079810307391139236 +f3ee34f6-ae0a-f503-8e59-f44dfbb439c1,100480926,12799517094224027168 +66684725-9f9b-0af7-2679-4d8cc052c773,609989797,15968405307782005739 +244509bd-d11a-f054-450d-2f1b93bd971d,3725380540,9156852334601789111 +e8f7cbeb-f3c1-6a6b-fce2-7fc485c48d98,13707637,16750924976775183851 +a496c3b9-672b-2418-62f9-b08c05716ec4,2025634080,11001298980309605793 +bf35d946-6fec-928b-3389-393b2de35fc2,2628543744,11638670265925817223 +bd9e8a1e-ec6f-2906-06d1-0c1c276b6f2e,79873979,10567276683928450999 +f008505d-4409-f691-dbd4-9d7f069a658b,1780986180,2939841204633474076 +c0943bb5-5247-40f2-6461-5a98b71b503c,1155126223,9358194787365128950 +914b819d-a1e1-e231-0cc9-fa36463bb347,1741373781,12771532961070539481 +fd727830-f8cb-9399-9a73-fac360e73872,511797769,12494554181000968766 +486b2e77-a6d7-5c15-394a-7bebeb601cad,175674056,6619709243310736002 +38493661-fc33-78a1-fccb-375677a9a0f1,4199583732,18010953548893091970 +c2b82a18-5abf-6260-4c7a-a74c447b7556,2679468992,14258196414326926428 +96c0212d-d4ae-c014-4a35-f0b4fcc70223,2594218488,2638183710664542707 +ebf01f88-7b48-ce46-f3c6-151deeb507fc,879449162,7622946365890623408 +80496009-e424-bcb6-d3e3-e33d9d12e5b5,1263989954,6206123860672921438 +7afd8fa7-aa77-6c24-a3b5-0640fb9249ee,2140346112,4278243130270461993 +c3823cb9-c09a-5f5d-1401-9c423815f44f,706775586,6398256552255408986 +4793c6ac-4754-6061-d7b7-11f7c032d25f,3456690431,18102542766986915015 +019710b1-9a4e-d8c4-17cf-9ed5e47731c5,3619296719,18072353062830257851 +3a02e398-617b-9de9-a5da-0e227f1dde04,323778885,10582812331975918682 +ab5fb7cb-af0b-5a17-4e97-01d4789387c6,248823432,17560932964076984296 +8c05b899-142f-f456-b16f-76d998d720b1,3138461062,16039911032315089755 +40b9f619-400e-f000-1a98-eee5f62fed96,2719723943,6140976584405688621 +2e0f8067-1bac-ff91-7c07-5c9e1884709e,2824132737,12004587176975526378 +05d98f26-3778-569c-7c74-ff66173711d0,3389853254,16591812322842837462 +3610a3da-709a-e593-10b2-fdf748643528,2838619028,5526036643589617124 +67e5ecd5-5e7b-f849-e3da-077de32b12d3,1071347694,12541216149987567244 +245b508a-68ef-e1c2-36b5-0774f289c8dc,2546908194,11677949969650610208 +66362270-d8e5-d7ee-53bd-014e332c5cc0,84221738,16916624194897456791 +bea86926-de88-60de-fb5f-926f4b92b98b,467516481,2060659002560325744 +b80d4c64-118c-da6b-a674-bb8048d7f19c,96563005,6035806128919336281 +b96a3275-8ffb-cbeb-74b3-17e271683e51,612355110,15309239508278881429 +d1e7252b-1e94-3a8d-7253-25bd5c04418e,2736744880,7413180055195712489 +e2d7619a-6446-7862-8f94-2fb95ae561a0,3281975737,16800520340339946867 +67c51fe4-be01-59d8-b386-06baead3ec4c,379468379,5966865769545535978 +3f54604c-f496-0966-2957-39bd0795384a,1633815697,18007893160704839367 +600da826-9126-55a4-e38a-b9cf0d5e5e66,2443251373,14587092866260028826 +5b92a459-4bed-5d0d-b2ff-5fc1f1740d9e,467759410,14675800102250992489 +1e7d4808-8955-8879-39ea-7c1771c733d5,1547423684,17430692621048698498 +8dff87e3-fd53-e79c-2161-e31dbf8d5e10,862951301,3737608867810325436 +e1c8acc1-df34-a295-5fd9-a7d60223a2aa,4189985593,17321398195973400191 +75e2b557-7ac7-d435-504a-987ae73762ab,2178693903,11020110536231760021 +0c64ca95-e79c-406e-5081-470123c306c4,164149790,1458020435044294726 +114b27c7-03f9-0367-55b8-75e78696898d,1011630831,14097314746154929632 +03560e66-c011-8217-1f19-0e7c0647b837,3204202755,9812178220620948208 +a0a2ddc3-a536-9ef3-edfd-6a48f36a4788,736957004,1730508914335421990 +0902276e-94b6-e9d7-e08f-f7d898f55cc0,1536404087,8336183590917334386 +ee750403-2bb5-f077-fc99-b6c3aded838d,3021675701,5834445520533434709 +0996243d-c692-c859-24c0-c85c27cb60a2,1130392373,14107608929792058028 +59a73359-2f9f-cbe1-3a2e-19f65fdacc77,2346815295,8567291214929898128 +efc9ca2a-e407-51fd-55bf-c996be357583,3141239482,9236266806461878502 +a2c708c0-f5af-37e3-9170-07ea23f94edc,3542939632,8352991248670592511 +a21bf78f-91b5-bfb3-91cd-6eda849b1862,1761964311,3052214385171461697 +aa4eeddd-1ece-8698-cfea-6969a05c7fcb,524890873,11446454789970977576 +58b14a77-c95d-8c09-1679-9ae069c694ea,3220258996,8951386824597475257 +e3251c89-36a5-e8ac-6379-d0226174779c,2834932068,18334668973260009937 +e07c452b-210f-21a0-bd38-7887ff60cf14,1526668913,4525184991221316828 +3a4b1238-6bbc-ce0a-17cd-4d10fcd5a0e5,2604341982,12009760487066684701 +a898b10f-8c88-c38b-9530-9b39aed993c6,1589183106,4774013588000806951 +8310c32f-4d99-ad31-ef80-7a2530e0a334,2840064731,9722475998685122256 +038eefc9-62a1-91a2-0bb2-a7f823d9fd84,364903194,7922143242472049197 +7e471828-492b-6aa2-354c-fbccd89c38e2,862741031,11319347594280264532 +46cf958f-9d45-684a-0766-2c05763e68c1,2270394834,4255197251569241951 +ced97e29-dedd-818b-83aa-4927123c5564,4051777568,3334371108092912245 +7e2f0a8f-7d7b-fe9d-c436-fb23a7201186,776837960,8268117366264285802 +e2a9f6f5-707f-04bc-ce11-1a1ba6cc613e,2460212180,7471415335717932683 +0061ed78-0c49-2e4e-9548-2937a8528e78,189143620,525632787588025272 +1e88648e-9d22-963c-8130-10fd607bcf10,1790469007,2489636997929587855 +4d240c1e-bcfb-0983-7802-15c7ff508084,3224781763,17670654688755281584 +7f955b53-2e9e-71f6-c1af-baf2fc9f865a,1179426952,17024966869806700922 +0df9d25f-48ae-4a6f-64d7-3e2012e0d9b8,2863866364,9800911513938829310 +1ad8ef21-e57f-d9f3-775c-a31c4801a44f,1364829812,2115595148639892363 +a8da75da-ec50-1e5b-2517-359e984ae8e5,1792101082,2220367467230433397 +7c5a2d26-9cbd-e1d9-65ea-6b1156b3ad6a,3447668302,12280345781654741399 +83637223-0c46-6538-b9df-d7ae3d16439c,3785629501,15382086269012093645 +8d3f9f15-423e-5b89-e5cb-a795c167b473,1517062674,13450086666664550490 +46cb0a68-fff3-e119-bc1d-7c57e78e81e7,4235527483,5334804458247915705 +5ea025ea-6046-bd28-6ff3-9e65c91af688,4234146700,3977420768360400913 +a4656f23-2506-e5dc-3c19-946c076b5179,3798229771,11420835041828953584 +2a51ee31-c082-8994-6acf-17aae839c715,2210419276,11528471591098715467 +0d313d3f-fa22-e10b-8593-ea2bc097aae5,1502004215,6445927810071515005 +94c59c8d-d915-8623-e310-5752936a246a,1126714632,11295528811437405033 +87c5751c-e299-5ab5-d2a0-9cf147e8271b,3243502306,7094552128495033064 +a08f67bb-8645-cef6-0423-6b6e6c7606a9,306368491,15445584756112381395 +1bc95834-f10d-2d28-4be0-3c5a29387f7a,693745629,15194633707097522506 +6486df96-0028-c5d4-951c-d681e7e7220c,1282760401,11176471916110803669 +2f908815-41b4-8b06-45f1-df9d6e76b184,4078047260,9931281142231372515 +06e17f5a-9c6f-6644-b356-4e459a8d0dde,1791475726,6183146728212875179 +9b01c425-fb85-4d21-b60b-f9087cd137f6,1437056929,17308345172930619321 +736b47bb-e120-df2d-3ea9-90b7b16a36e6,2137856979,17824770170256436801 +24ee0a88-d9df-c24a-7d02-027cecf97270,1072894734,3281307734879611456 +de5004f8-f003-9701-bc25-9a180fecabf7,2813265085,13602311008238392625 +5217ce66-367f-49a5-e13e-952a64ced8cd,3230195285,14265974568834241019 +bd62daec-63f8-9552-503b-7a7f928a1538,1739751267,8515503744313625278 +60ea5997-30ed-a751-5012-bd3257c65b28,4168471784,16540046368478625363 +6baa2777-b8e3-112a-093a-fef0819aed62,4135797538,12652151439464210005 +dc0e488a-4583-3620-50d8-ae39241120ac,3513110505,11468742996765362120 +e3c92a19-3b9d-7cf8-f411-868f1762ed6d,3609183535,11285666337689977349 +1844c2da-2029-26d5-56db-7438d6102d9f,1098589576,3782624750085357306 +94112b3c-4664-387c-3356-c6b7246d17f1,1269621648,13069684619675736316 +3a1c5afa-f6da-c91f-f6dc-08e037d6d461,1270452823,11390221717469596882 +254c9be1-a096-9cd2-cb61-d93f00f340e2,2367355842,7467360413096594884 +6a85d0e1-db86-33d5-a917-6d14ce1f2f14,1656226197,6679404534732388759 +8d3841f7-cf13-fccf-70fd-47af51d878be,3694836437,3123843092347190309 +2fccb8f8-a9ed-a3d9-d7a8-caf36d24397f,3982850584,5757679690857787471 +be31a38b-d6af-3c3c-80b0-d2187484baab,2969392749,4228641156146851597 +46eaa426-e6da-7f78-cf51-c02c68ab25b4,300101426,3761993009694535456 +d9739a77-aa9e-4267-96a7-096842491268,1378162294,5098034527989321205 +24e78878-27ed-924f-5ffb-f9d044d87d3b,3563492507,3907226824063345791 +f1175e46-6fd6-2214-278e-6c649c9d1e4a,2389074587,9322844137870497539 +b5e60bf9-b270-81bd-34c1-e9065c396231,2835275588,8054674398623140289 +f535a62e-9669-51bc-be5b-32dc35ca97a3,3504034599,5145699965716082452 +beb14212-198b-392c-1142-5690e26f0e15,1747217960,3071197185538743606 +9e6af016-c3f4-7b99-b5e5-6eb8dad23638,3653816781,3827662545489627342 +c40dd6e7-7d12-9ecd-8872-ac32f3a0f262,3550227204,15386585875835205929 +7db5dd10-b0cb-8b06-cb0f-cfcb25da0afe,2752417758,10704128765986033687 +2bdd2e2c-96fa-c998-b932-664877a22f59,3616330811,8772432239383195599 +05f454d0-aca0-8af5-3481-0d18f395f05e,3157285649,16369231416382854861 +1302cc15-ced4-fdc1-3f87-90f4aeefa7d3,931003856,15772787931493914959 +8eeee125-ad0b-5206-442c-47d2e7cf05a5,3105275406,10946493326963693976 +b6b23546-4001-8171-86ec-52d8b03de97c,2408201653,14112033771721132707 +cb77984e-d1be-ad32-7a39-488295aabb5f,3636216834,5681088205438034067 +ffa20b20-75ae-914d-6daa-b80d6295b185,2615753324,2293544819938648098 +32f6313e-5fe9-6aa5-4e80-b37554d1590d,807541821,8570002532022029383 +977f5a0c-e9a5-ef0b-89ce-5689529423d2,1949417810,5723951063323333727 +3008e3c7-3524-590e-63a7-7fa4b58ab033,3785538161,15773011181832282085 +f5648669-9884-006d-6d51-842c3133cdf8,2336976038,8829328254495363626 +bffabdcf-3166-9571-681b-fe181774eed4,915177034,7523971248211280615 +aa4c3b17-4797-e01c-8741-f579963f585e,827250134,3617625869548458509 +95506a9d-67d4-3142-4a60-2a49f9115471,3306452216,14492511408411707374 +ea6c6bed-34de-de4c-3f93-668806aef5cd,746640131,11746797774564173991 +0fc34af0-2903-5920-fba5-8d0cf4e38c9f,1724504446,14650061821212950253 +babfe8b4-3dc7-caea-8e8e-4e8eb0a5c91b,4228801824,13361656844728764332 +0eaf594e-ce2f-b84b-23d5-5d8668210586,2498260737,9973560251793986370 +4235e3b0-05ae-bfe7-02e1-c88a58de6f8a,3380493332,5207730314628315040 +e2bf0fcd-66e5-75d7-be79-3e6f5d6e6204,3772560061,13055676262478764328 +fefdaca0-e07c-e0f3-714f-73225daff796,917479550,3093001870530920780 +a16f3be0-d61f-0604-2b43-13a6e6d33285,2506838335,6163231318171136767 +6f720cf1-9160-b014-07bf-604297269e2e,3494090948,8771593585803313985 +70eb00fc-0785-32d0-40ed-cddda30df529,927290457,9989166740887677466 +ad4bf6a3-4e81-946e-5228-226e4175ce9b,2452844989,17770540629948304272 +3e55c6c0-c284-d1d3-cb20-01aa2cfb31b8,1867614565,7899329606978293581 +b7c54eaf-a82f-c52e-24c5-8067d70ba302,952040290,14270253113675011612 +e11ef916-df84-0e11-bc06-1f0857224521,2661784127,288816500858819916 +3a5622ae-b734-943a-90a9-6ee70f9f48a7,3968818358,7013950990811168015 +82690874-398f-994c-9882-ff60ea8326a6,2608244201,4332900202600455572 +62d22ad1-bbcc-0e15-947d-5cb39bb70c9e,1733205797,4088328146951726821 +9c1d68f5-947c-90f0-83b1-7852c1712fee,2946647382,2316748158051616169 +ef9059dd-64d2-b5f0-b551-5c6914de6912,3706628508,15277037731462693050 +337b9476-a92d-1c9c-78a3-dd096926b343,1173759180,11098669640885193257 +2c1019e3-6b03-99fb-32fb-5c166e512886,2476616553,15717162108815558975 +eda55a6f-f5a4-93e1-c128-44d7030e47d6,2562705399,14182706453560125685 +702157a9-f934-6a72-551e-b51e40564701,1917925973,6983417643692848176 +34fc2bc2-a853-2178-cf45-cb95bfdd52ab,1595520321,8403312353663004968 +547935eb-0e73-816f-8897-f542dc9a22ff,2470454120,17267733178638585613 +0fac68d2-c4f4-f8e3-16c3-d606c412eeb7,2711522139,16225647242752005508 +7dab3e0e-504a-94f2-792f-3b19c85115d1,356106376,9310344845430082359 +63f643fd-0919-0c40-c7b7-9311ebfd2476,197595957,6539202332769120782 +0b017b90-7c52-c3f3-5e4e-266c7f0afe41,192419780,12166128461165410294 +06936c12-aaa6-d17c-d1c2-7548423e4ba0,4012322861,6299466707680009207 +f21fad9a-ff7e-afc8-bcf3-5680c7fee929,3471926250,5585809422755687539 +a34468c5-5333-e9bd-e18a-a97d8b4ed2a9,981482828,16808063838844345497 +ce1ca179-e034-8ee6-bea2-a4b21dfa39ca,3349184488,8097376935061060070 +4a7fbe7f-8a07-0196-40a8-f89fd6a0c930,902619219,16609634589220254345 +c8e96bf7-1a30-1dea-92d2-898b78954bed,3553076390,501198906879215497 +ed868942-08c2-0e84-7fcf-312a894e33b8,3996079845,13492998673855274521 +2837c341-7792-ea01-c264-5c83c820fe63,1670515278,11820114289560210732 +a951d3bf-31cc-3999-f0bf-17451f406358,3990608292,4728333828632595390 +465edde5-a67b-be20-a4d7-91099da9d3dc,3335752981,11212669256733346259 +59a2b2f8-481e-a18a-ee52-f04e596c2bc7,485055589,18238214172728281629 +d4152549-958f-a231-ea5b-df26eede56b7,1263114815,13966176223591803678 +9c1cce06-aeec-3523-b69b-58078354a0ef,2665238446,18084365441677341917 +d680a4ca-906d-1729-b4d6-c086e1ee5b08,4088640635,3679919665946557136 +07e9ec59-8f37-3f07-e19d-845c578f7b45,440451955,5104452355874722287 +9a791072-b056-c33c-1ac1-602e3a4b26bb,3554296039,5783788200293725989 +e9d74ee3-266d-00e1-1f7e-5bdfb1ddd7e9,3223857680,1340105393094971638 +1ead288a-c9d0-1ee4-a919-88087ee317d2,696363156,15500741950133104143 +117520ff-533f-19f4-dbf6-933311892726,3798552359,10413443488201274718 +f38b3b6e-e3e4-77a4-0e4b-7e947137459c,1969006921,14171610303279691387 +8858e7fc-a05b-e48a-f132-dd4032c652cb,3769438332,2831075391620413355 +4a7ed396-99e9-ae1d-21df-a65c07345ade,1372759783,16596161816623981369 +b930d175-4019-a77b-b88b-2b68c5c7bfcb,937208829,2456329402770485051 +46ef049f-8622-7c41-3dde-6d82dfbe5304,3272825930,15868918125050512579 +98ad11e7-5a46-29a2-4487-f70e6bdb8003,3270702742,16912282548971170644 +8ed06096-d91b-cb98-2102-16d23a43d71a,954936260,4725401991101267704 +7edaf0f9-2d12-1383-22b9-5ce60ac9e50c,508674635,11319908035016583354 +a653745c-ea53-aba9-39c4-7b538cc3f9ee,3719704463,4251177405514379144 +d855d3cc-38f2-5397-2c8c-98d0cb8efee9,60469670,2790110733782282317 +4ca42a67-81ce-1d81-e98c-6645e4840e2f,1932567684,16548173363977854568 +7180362c-5355-d722-7aec-ac6bbb6970f8,415501031,8360665672533711192 +dfee6183-bbd8-1a34-92e8-05bfc7d4ee49,95681973,7708426992681754105 +2884c63b-3733-859b-ba29-c28c77014087,1646655566,1439615752412680018 +1d30211e-c4fa-29e2-45c3-e323cc0dad3b,758904472,7009496733046404182 +070a12ad-2746-4bf3-f487-9379c583f27e,624005725,6647219612166493610 +982f1c0c-0618-5ae1-ff5d-0ca41de9369e,917202463,441830285391234403 +3480b54c-4628-3b64-ab34-c10fb44859b6,3780254559,10326398133710466457 +53473be6-0d9f-db37-9d6f-b99deb9c5b48,1703214457,18393446980438942938 +8ef32405-7057-d3e8-6b33-8948af0f6d76,3044021988,297095244814985621 +15e49020-d609-ad35-72e6-efdeb6377f48,2656062450,1844209168761988205 +4a83b687-8029-7f15-839d-7ddf534648d8,3779631649,1410137851189879828 +f51027ed-98b6-7eaf-e799-d33c9a3170fa,1802329910,13582308146738893722 +b83ee779-9a45-921c-35a1-fb655acf0ef3,3895114903,14830387306978554953 +7965d093-386c-d5a5-2f1d-f74e85fa67da,3074134614,12072877854858221438 +cbe4349c-3b07-7457-726a-221717626581,609881108,16514774591695298232 +2fad4b14-91fe-cc76-fb41-33331395a4d0,1580940919,9498049874913931388 +7267f715-90ce-f370-1f06-261cb53c9259,2993162563,15972869508971172508 +f4c3239a-a28b-c67b-86dc-1d9ff557afb2,2806351426,9030211035216200301 +43d78287-b727-659a-3155-7d0d7035267b,2789989565,10214518830179014397 +2f49566c-49d5-f95d-9fa8-edd328bf5f19,193247264,346576716949764542 +59b55ae1-e5de-cdb4-a4cf-c3dcb5557d41,3503306352,11157030632473462168 +29c53dec-2bd4-a758-b870-d544bb19728d,3335463011,13940920581324002494 +6a3ee1ff-2f78-162c-256f-0c46b84eac76,1087651452,329074544024498241 +1cab6dc4-3f88-bd21-2b0d-6c0087b82baf,1977333681,2653651765820094834 +6c737bc3-e758-77ba-5e39-fe6575cdcc27,2203277309,2154704557673353560 +42473508-e887-7bb9-0a55-f129d4b841a1,3399633568,2207241019568141454 +e56feedf-2e1e-c723-89c5-92e377df1f55,4075216455,15631093949278064026 +99515b8e-cc15-dcce-c09b-50d6743b1a50,2525332233,920985985920146350 +bb6a3415-262c-fba8-31bf-efbd2e28c351,4009378459,11170005909413534955 +ea0220a0-7e7a-5312-77c0-5e7773d63878,967524248,3971251299401859498 +0b31fec0-4a1b-8970-8da2-20aae509a0c8,3072232620,6913347579666086192 +06517f3a-f541-5745-fcec-12f1134e5e23,3470608782,2179964334143434080 +e02528af-6d06-dde7-0742-ef774b16e514,3183042830,5888779609948714868 +35d4d253-8702-92a5-6d24-90e849131ba5,2185848817,17759511354597944523 +e54dcf81-e64e-d4a8-ebc5-e8e077bb7302,679253903,15505461311842951020 +2dcc6455-fa9a-d77c-b6ab-cca85cbef9c9,3282344495,6701556288234554915 +1c6d839c-9d07-c364-92eb-b3f71d0c8f4b,76576286,16313377438141676912 +b70e6664-b070-53fb-4b5c-7965d4228f36,889315420,16793936559227178218 +f33e52fd-d05d-950b-40a8-475f09450a08,3668259872,12190503099640099408 +c28bbfba-2571-663f-5682-34072d27ae9a,474263566,13443166111674950249 +360f72d4-a7dd-4535-29a7-e0ce4011f041,3724404309,13451218834438566015 +4ef9dc2f-3930-1fb7-6803-8ab257d0bc1f,588143111,2549811083528082612 +088c9c91-fdaa-0444-c1e6-be27f46f53cf,440039254,1372566882971526730 +fed6309e-2659-860b-d9bf-f3614f0c2c32,3269054203,13792646611151611675 +e41366a9-8f5a-a4ac-f454-4340017b6801,1755856107,345279654831109397 +81ecba60-f21f-ffa0-5de8-4bc9e966092d,3298871371,2081060943914427343 +2458a5e4-de6c-56f3-b55e-021478ee0489,129140462,2334127328488283594 +f4f4f040-1b89-0be6-c636-20aef7546ade,422187846,16629674019299927994 +bb08be4c-1c4e-0e40-2934-b1077cd8a127,2559192282,733398902966512060 +afa34c09-342d-d43a-2587-737659b6020d,2917566347,9533887367641234492 +a8a6c964-89da-e102-411c-1028eb321aba,362565401,9460662121199206749 +665a0592-bfad-9f2e-d185-ab68f5954cf2,1456553382,16250890468197352337 +de4257ef-9dea-f1ba-4509-d248dfc43f30,932368760,6149604674689163508 +9d1e9879-547b-7f15-21e4-9ec6455e2f0d,676951120,16634038420956232374 +20f051f7-214d-8e61-e1d0-184a70e85392,3169824832,10374574547447483321 +faf42cc4-b531-2642-1d6c-e0eb10cf0cd2,2233467263,10370307825280657810 +560cad06-5f39-e105-ba8a-8435937167d0,3837591235,11941769229855530771 +fbb8c487-7b15-8eec-3ea6-94854c531a19,46599283,5157216063903533150 +661dafc4-c339-5161-3d23-b826bbf8c4ef,3209243726,18068085817453262913 +96841489-4935-9e0a-6b0e-b583ae890dc9,1507680126,12224629264350377656 +c45bd545-1805-1a88-2e4c-3726e999548e,591274051,18119088685662776208 +854963bb-b79c-08e8-a945-07eb7e19e408,2553573381,11365004851028715388 +b87b4338-cb93-59ae-66c6-af17b2fd44a2,3875222652,4166001842019740284 +aaa31628-1197-be4e-a993-bf442fa6e906,1405546654,3946132383138748155 +0ded65ea-e8dc-0da8-3122-09e42908ee4a,3860846947,12882872179451171924 +c1c54243-a03d-7802-5560-c490a88030b3,3851859487,3703177235117187217 +ae343977-06c6-9c98-7193-2bdf4d72d097,3558552316,7925710307650122149 +8f4668c6-445e-c90c-d0d4-3785d8bd1f44,2305506965,3518831167622440831 +0624550c-f204-71a6-89c4-c144c119bed2,3923600149,3206234691009221671 +22119e23-39dc-0994-2418-9ac79125b1c3,3209359786,14512431156293070801 +ed4a439e-1fd6-fbec-ab9a-8daa9080ccac,2102956754,18119533780787041843 +3d70e32d-0adc-7db6-a959-47e2209be33f,2048367386,13315574113503084978 +68dea157-d81c-8066-11cb-ed822e341ff9,3524488777,447514004979315134 +f60de2d6-0dc3-b98d-a4b3-a8a331fbdd53,1048276491,11958414006968628233 +245efdde-6be5-c88e-a4db-30f13fa43cf7,2797083123,16940104879787099478 +e11451d8-6efe-e0af-adbf-1473fe99ad93,238686274,12839505417647796341 +0b6fa25b-9a0f-7cdd-b9d7-4eee48033134,317881032,15659523883042809757 +e8065404-4b5a-8bf1-98fa-90df9119acb5,46497506,651576060820383968 +5fa63960-8ec0-2f69-cd1f-8984aaf7b848,4173985830,14045782487612401547 +e6138ccf-1f80-6ec2-5f22-cce17df76d07,2991831225,650181999825516330 +ce161e47-8194-ffd3-57e7-fcc95c64f40f,2897048827,7154256244951732601 +6eb2e11f-251f-a1ae-0cdf-3ef425c7fe26,1612340097,12622371344161963898 +1534b131-7926-757f-7216-111e51e9799c,355930575,7312059631120440231 +bc6d9255-c307-b90d-8b57-fd5bff9c775d,1998033529,13238721423112498039 +e782f20e-2908-4c6b-dca9-7379cc4fa6c9,769832433,11662911892209571385 +3748d067-a065-34db-7f43-74b411849d01,1789806061,4366552006650987386 +317635d6-a719-340c-f8e2-45d0bd8fb3ed,1466116755,11468681434339409062 +08fe56b1-bea2-f2d7-b13f-faed3f655b07,1541916899,4239125852789785475 +069be756-9c40-235f-541e-b990945d98fc,3834770554,16489362111781544743 +d9f6ebc5-737c-8cf3-9c92-453533cdcf71,439437948,5289633216350958628 +1c4f04f7-71cb-67d1-e1aa-9c3828765416,1496801193,6399277298088041439 +1da08b54-744c-c916-6904-44a3d0d177f0,4160929128,13984869954037561254 +a550dc83-2881-c989-557b-3c3692ce9f2f,3137038578,2153656013313316273 +88c18511-0941-605b-e857-a47873cf3169,2138595631,16102117511504084097 +c2477273-2235-97da-2b13-cef09e590461,1636870011,11147461615308528080 +748a2a9c-f96f-ddfc-7379-502c337d923c,3697808900,13274261401119486031 +aaa94825-a3a6-8f1f-4664-bac159a02866,2666674902,10520368467797196910 +be3efeb6-8ed5-4b21-4c3c-989853439a7f,271177293,7507644236818966317 +f40a8917-77c5-b8b3-194a-6a486d4c8312,1190764391,12698255241199321579 +a6b6ad45-d610-b68f-7aba-d861159261d4,4082492032,3866550373102946170 +59d69f61-1c7a-6999-53da-5aa1513f1d94,4186287469,14041808903608756309 +dbb7aecc-e69c-fa78-a6ad-0cd5ad2be8e2,2165101353,11550588973774338115 +f7c22ec6-4d5c-1943-4937-a71e05a6f5c4,3768443231,6438370052189498858 +ef083833-8dd5-e41e-c936-b128393ae0da,2882933189,7699451373308720311 +4c8fce18-7867-7d8a-e684-677cd5633f68,1823133722,14050139339710557599 +b9bcdc77-8bd1-b073-42c8-29411bd76d28,563887233,14130877342587237375 +0c176529-918b-9a20-b143-e44d8073e4cc,391235531,15601801147933885753 +7916b6ec-ff39-1f41-26f0-9a8862833243,2911044649,7392715689097633094 +ab51f646-2e26-3fdb-f50d-3a04e0b6c882,1573593367,2560855777354549578 +c7ac1fd8-763b-a994-f47b-c58f6e113ca4,2095809144,11355105434343518741 +e02e97be-d123-8ac4-fe97-d0e55bb4bec0,162949734,14620959582640554452 +c71e78dc-6b3e-c25a-936c-ff42741ebf8c,897579126,18422777793661487706 +91c8ecca-ec1a-15b9-e88c-a74cb7729397,94054068,15740905232404840179 +d242d78d-aa8c-c2be-d9ca-0b27fea996d2,566314727,2508786417694541376 +a8164bf1-199b-5b46-bd95-c67e16c96837,3304026052,3481591161972368853 +dd41ab91-abd9-4b6e-a2a4-e6606976c6fd,698675420,13771478622408088219 +990a5784-0b2d-2f28-9883-f4bd1a0a806c,2431625824,12119861002504465607 +15642687-e8a3-a658-b86e-842303144a93,3543727839,10694389627131014334 +9a85650b-4265-51b8-0c7a-fe9deb8619d5,3080659806,3673984479063100041 +13b5ec11-b679-6711-ab98-adeaeda18e0a,473503777,18289570344881845479 +fa6f5940-7fd1-745f-1bd0-eb7f72eff7e4,2657171086,11609844024139153492 +5484a5b7-623f-1ced-c409-7ade39368f01,2835767883,5759320787506965120 +eb51c123-971f-acb6-9d5a-70d7f98dfd3a,2685191467,8870130075937657576 +3839d75a-2cba-c879-3f20-803d9d77e9a7,3814292744,16561492563344799406 +426f489c-4adb-e2d6-8607-faebe2a4fed2,316284642,15173276939461466830 +fdabeecc-0d0f-4384-e95c-5220b593068c,2865769807,8150407812676683801 +535564ff-223b-82cb-86be-79ce5c7e3068,2591930242,9573218138216232361 +6e31e57c-2e16-14d1-ffad-127e821a14f4,781799053,2831596607800698389 +803dd7cd-f987-b189-d4f6-7cecbd73f916,3492507925,5926272020704683458 +3cc8aa6e-9a32-1e31-fa94-2fdfc62e81e4,4071855650,4558286160954532754 +ab44e00d-0d08-6f6b-8ff3-a800817d9e63,2587575023,13280804970537582190 +ea7f788a-e471-547f-2617-937f265d997d,3101932756,9708534002322992491 +93582587-999e-cf3a-81d5-c2a6b50736ef,400404764,651698952575432048 +1472d08e-3e30-a6e7-7637-6b1faaa8da75,456728505,13033971794668334750 +511cbf07-aeea-3d1e-cddc-2405cbdc8e94,1064330269,15208379524657075910 +4672469a-4eec-a6e9-2799-002f3e0d4527,1396899148,8781158456550623667 +beaac90a-6d3e-c225-6079-50432c1c4355,3216370903,1275421121117350819 +c2221cd7-7ccc-3bf4-f2e9-b462d7d17ffa,2134973009,9086341643443743272 +3da3adb2-9b01-3ba8-e570-d91d65cb00ef,3592907919,2954119502129642282 +3bf9f479-d078-1daa-6695-b2d1e2bc4158,1359917456,1600696786035549526 +064f48dd-e19b-a879-08d6-533556746cec,2617957888,12733027495940881697 +ad6029a3-655c-9480-432e-54a354e180c4,3389408253,15071583223171988990 +014be490-30b0-3e47-26ff-23d27a0d1ea9,530807620,9654477152621530993 +628cc0ec-2a27-2367-4dfd-abca717656df,42287910,11652836734351027560 +f3d7fd65-7651-50e3-9693-668e83fe1b49,82631246,11498495147020255874 +69d6e21a-1225-2942-d5ab-05b7ff463044,1627983298,17029491026363290406 +280fe16f-1904-c687-2493-c292cac98e71,3943433608,15432624651686670120 +5860a65f-46d6-427b-c121-aa8e562a5a7a,1670417345,11485949231655402220 +24cde20c-a361-de44-cb60-18e747e6a56d,1692690869,5735469752366936184 +c7069763-4fad-ba25-5b35-7959d138f3af,476025547,10918162594836869125 +ac3032ab-f8f3-43f2-fbf5-00582db5a782,3635928366,14363182429065825067 +d746e773-70d4-041a-ca54-ca725034ee90,3647469821,9913496640755989098 +5115374c-5d55-a641-381f-41b230429c92,3923328709,13832612162584078969 +5fb39e4e-2937-338b-03bc-4f0bd1168feb,1703224976,1345462520357016611 +57c7cbc5-e2b1-e30c-c65b-1f652999858b,4186737561,4732871566794987168 +3d5ecd3a-daef-d245-deec-8dc2aafb14f9,3296501519,16665295178023155772 +662e360a-2ff6-d365-cff9-f876dc4f74a9,3116938527,17207416318085681040 +6efb197b-ddba-a74d-9d16-29e1fc6b1274,2097041942,4959444453457991224 +d47cd9ae-6adb-ed87-4e3f-2e2f05e56474,3385647762,5940143416233440559 +e0722f00-5e14-1d3b-5a2d-7873d64e5943,3843159099,7672363305264919984 +9638e31b-afb3-8869-39d8-3a0fb36db56c,1165095927,11229182071524277280 +d10c01f0-b80a-0e37-4c58-c60177e31a7b,1007743157,7356836015029801111 +c5283cba-2b29-ac3d-096d-80a5efc12442,1320578739,11756292715570110112 +6ee494e3-f1f4-9701-5c7d-566e6176f18e,587098792,8931575237363374044 +ddb0471c-19e6-43ea-63ee-b301c976753f,1945471,9516894265026161247 +08c021c0-4742-20ba-3a32-9e6ccc272cee,2696303980,10622711687942522393 +33c40565-646b-ca5d-4aca-3de83e689bcb,2678333168,14923977391959516727 +d3d927bf-5671-71f0-22b8-b0207a787687,3799814887,10685077523926487063 +17c9c263-8b07-034d-f40a-a1d17b677030,79308011,8733691289002548502 +d429a1f8-3fdd-b5e3-d12c-53e08849e273,3341236155,13426731260635196370 +2af2889d-a8ae-7b1c-d083-cb6b64388eac,755380767,26405658769487765 +d94f3504-cf49-900a-6057-0cba88618540,3257159926,7633673170130371775 +cc877afe-043b-1983-484d-d0b96bbd38da,4279533654,13868420641572433529 +0fdccd96-f465-688d-f184-3806bc7b517c,3350313194,12180335557734552814 +e0220667-0edf-6f7f-2326-d50e228a965f,3519981408,337250361921589973 +4a553b2f-618c-aff9-c43a-5dbe9b314cba,3182762141,17825072689466159390 +55cb182f-b094-c1c2-8404-34dec515ea2e,1366167982,8913645195125347103 +561d72f8-1abb-f442-cd66-57b53d28cbf3,511499111,17336481828453339272 +ed566c23-81ec-f2c0-395d-47df990825f8,4246777925,440250585162694643 +8614d610-b0cb-3768-c20f-fe9b4ba877ff,1282350682,291389389991965950 +8872dcc0-41e1-9ad9-7c46-3f411461bcbf,1793663081,7662351661056205020 +685971b8-fc02-0510-eeda-62e053200e84,900056892,13825777933644151483 +3e7b0e42-05f0-87c2-1705-4233233dae53,2850147563,3915646603800278537 +25c10bfa-b259-2edc-2765-69f6d4554938,1608282784,13248443265392416669 +ea24b390-3af5-0776-cfc5-3fad953113c7,2613771458,11518173224527725586 +f37e0ca4-1ed2-8a21-a615-172be306a189,2056724905,1385664972932257629 +a0371e66-cb43-d365-d5f8-816cea3f08ef,2235167099,12945294612600988316 +d73b9907-cbd4-b9ba-eebc-e59df4406def,3032965956,5438241206822914985 +03e86f70-400d-11f7-0e75-b1d962330c03,3777066594,13921215524515888076 +cadd7334-5228-e146-200c-aab82787bb36,3561420359,10532234434022253208 +096d2494-6d3b-68bd-cae3-4730eb556153,565027543,16405813630750364646 +6dff36d6-d552-15f4-61f4-967a781a6cfd,1326495692,14268393039243439189 +301c13a4-bb66-5c74-d0f9-c7b122c0af2e,1079798653,3177752801300883363 +34e932a7-d54a-792b-955e-bbaf941c6d6d,4230541693,18030147449325799958 +f1303294-544b-3665-2ded-8dd85dbebf3c,3347922054,11357465419750530265 +9ea06255-8000-81f2-9f87-9340f7a1053a,3109340865,9581467550240664547 +ea27c94f-f36d-ce85-f693-f0777b5fb6cf,265861584,10602382909899403110 +70ebc1be-0ab7-2445-4d35-54b71164be4a,837908442,3640980016181479388 +0b3f4bbe-37de-3054-267e-aef2331f9436,896929608,13083086549346688461 +0852b105-c12c-3421-e24d-b2a36b7c089d,3211519230,15201937205267861233 +ed0665bc-d94a-995e-a44e-46f4d6c8ad2f,3984664707,5213416057386546430 +37a6a618-2f4f-025a-e20f-7dfbdff9a911,2359629421,14535208851702484381 +781a49cc-b929-ef94-acbb-4d99ef1009cd,1081559360,7862462026015068081 +c3a0b017-c31d-a3b5-3fdb-5c998065cc05,3873197207,16404158989518317966 +fb420bf6-471a-6450-59dc-789cc07207d1,3643289676,6095560284311491643 +c583b51f-3791-4746-d3d4-dea382312f50,3115738151,5310918034430552475 +e25815f9-508c-2486-a176-68718d9e1bbc,2271812311,12250606139027144070 +3d1d7950-bb51-6e2e-fc45-51e78b86afe5,4230358307,13121620805472820761 +8a04d7f9-b234-286c-56fc-d4e641f5d819,301681172,8694754410056227477 +80573010-1e3d-f39e-30ba-cb84fed8e560,4179282124,15685829692495706457 +5963d554-7c5d-6618-a25d-37ec4585bf71,3898577493,9086348615723148911 +6b572378-e0af-60a2-1fa2-0a76d140460f,2936852971,7442842091894520483 +ea136de8-f360-1325-6dbd-6c5ea0030f46,2512749433,7700599607392325670 +0a62a9ec-59c1-ea38-b3b4-e55be0e020ba,2865672573,13505572601170957899 +5aabd396-0a6f-5e41-7124-222870d0e6bc,518250412,18016355825646024900 +8d44f7fc-199a-2751-f2df-317e0bf94d52,41714252,8856689919916637312 +b374ed6e-5f41-964d-71c7-1c2a4f677db9,3430211771,4602057422146373394 +ec3b24ec-b7f2-97dc-244f-6921db4d675c,3442700607,16492755950002494160 +e4a1f5b9-0a3a-c89f-1cd1-8f9e650a8175,838961623,10359918259681515556 +083f37b9-300b-ebbd-b3cf-56a1ef8f0f4f,2882566089,3190269009344708142 +6b60f9c4-0027-5f63-1e6c-3ace35a2b500,2967102791,2300426150033059070 +fbfd52db-df75-204b-8409-9128d3f4153f,4171686519,9949135067091721186 +cc5bd7bc-2aae-8f6b-4278-852db255d70c,3082952873,12026811356594283237 +6a08e199-e6a6-b463-7888-323eb51fd6a2,1923466928,17589954708642952453 +c2341fc1-5d4d-3eaf-2b4b-4c981b356fee,2976609823,264668412311942211 +8ef34bb4-f774-93e4-054d-556d18244382,2173357983,4166238258046287371 +da30d425-eaf7-586c-7cf3-a0f88ceb4fc2,133190974,1726391514596159703 +322d98c8-4484-8d08-7702-89e75ecbb4e1,3315068508,4347933555221082082 +a5fd4cfc-5ecb-5a57-69ab-6c5a658ff3ea,3086733040,5587479769638415797 +df9b37bc-42f1-0d68-d1ab-5ba1cb169c7c,2188834858,7449242642682440586 +314266ec-6925-e7d3-8865-3935c7ad750d,4122523286,7377744373470900143 +6e28cfc4-d2ba-b2b8-6437-fafa19d35f08,1617199625,10267770547443653548 +eb6e7b0c-f3af-17b6-5e73-73b4a38d903b,596287340,1884576952026073666 +eeaa94c7-48f1-bf71-fbf2-4e23d70353ba,863972359,2214270446454769821 +db53ba2e-5720-c8cb-7c0d-ac402b067126,194791352,6428448887848629998 +c6889b52-adeb-b397-1728-fd789fc3dd50,4184168918,4568697079123264586 +301307fd-8b11-ff41-ed58-9fbda61e8d91,3510264174,12694917068022890702 +769fe420-4c84-4717-c278-efdfee9fe7f6,1883713861,4975383598675738943 +01b38056-4e03-1e7a-c405-7b9d480d08a7,62101950,16457712797457900011 +0dbba043-916f-a9d2-9ffb-5eaea16d235e,1208167100,4218070446918923973 +e10feafc-e47e-593b-b489-09a0ef2bd92f,324924825,6185249562718502254 +55e353a5-ff5f-f6e8-63c2-1dd31a1f5a74,2119136191,13628509989445125752 +804079e4-1463-a176-9f18-2daed7557db8,3151453031,16866877898672163109 +4beaafe1-39cb-7b6e-a0a8-c9982bfdd73d,1640505198,5744309926858329678 +ded0f234-00a8-d2a0-8515-d8d21d893b5e,2799157467,13685972059418998518 +6952bdcd-7b62-ca2e-4c3c-cfe0eae19415,805622624,6696853287681129788 +d346e35d-d3f0-9db2-731b-dc61f8b19e69,2841569945,12624961971618524993 +9983ec53-d1fc-d0f7-8c42-a1cb37803cfd,227420717,9023168825939916420 +ced1dedc-7bc2-5f84-1e1d-2c008d81f1c6,2197657308,17855953180937034765 +e6357c9b-9f1e-20e2-f533-2988ce3b9fc6,899528691,5399169404653378618 +1b6e0b95-80dc-19ce-25a3-8c367d411863,4227988954,13846631520330695311 +b1fe5ba7-d7cb-5096-c859-067c62045d10,2274227397,2125314144570083396 +7ce2f3aa-5dbd-34aa-212e-b0b0dbf6cbbb,806269887,8064407197161626390 +11507d33-6d77-1c89-72bf-d44e57d9a2c1,3185569339,12608045285608846712 +9ff15f5c-e284-4b04-15a0-6069aa4649bc,1469582181,5687903752893159835 +33c78e6a-0054-c3c4-872a-823eb814f9e1,764405222,16836578837365415127 +dd047e73-c28c-9888-ac8d-0e5c3e9f0301,884473021,4959291474442011835 +b4b2fb45-88ed-b903-6857-8dee5aa50735,2635836155,7751973116171320409 +a622aab9-1969-cbd4-d823-76d1f25b6bfa,848017596,5565868282707450710 +3a5c326b-b4c6-2139-ab91-db6f14e614be,3050289711,7725404260001142386 +232e903c-a8a9-288e-fc67-e3a8bf4d2384,198554050,15414759746142073248 +1092df67-be7f-030b-3df5-0ebfbb38dbb4,3782245522,4962418425322967146 +766e98f2-e36e-3444-3c5b-caebd363c2e0,2258903349,2148882255200805424 +f6bfb901-432c-15b5-8b0d-2bc07c50e664,4195395381,3817312007317167689 +8e9a50c6-ef6c-5bc5-acf4-3854fe3e17ca,3720033839,7536047927030584788 +f744b950-4c48-b6fb-07dd-50b1059d2965,1645320287,15993985055776088635 +0eac0c58-96c9-7a9c-85cb-b716e1d77da6,1470811388,610938688655430826 +1b5a8a96-0f16-246f-eab0-3935a0460f97,1634472097,7491099757749367358 +60b3af08-c5a3-f5ef-2a3d-f3bd3b95e65d,599646504,15914005561865607443 +6eb7421d-b882-6369-cfa3-2ce9bb2f75de,3816525548,1703000110560894282 +6a5140d4-2180-ca2a-051c-0387a360b5e3,3918049470,16910513642549845919 +db2286e5-8495-ad08-61e0-ea7a554172c7,3838329957,17048922320863165966 +c51c2dea-3696-eb24-1839-8be95292299b,31334352,2237209101001407509 +c0a3bd17-be9a-8747-19c9-782cd59e8caf,1496316773,12223401054705037641 +0268981f-655b-bf87-6a81-6b13e871ad1b,3959575817,13467368519329174735 +6a6901d3-5b0d-605e-f7f4-2113d62690c8,3804578524,1516381534467788327 +eb61dee4-0d60-bb58-e1e6-4f448112e48d,3819591794,4679460331111309559 +36a39625-e84c-fb7e-8fe6-cdaf5f33928f,3957899387,3559087521483901796 +ed3ed3dd-1155-016d-3e0c-8833409c7b39,588068921,8998717180311620147 +664b0aca-03db-764a-15e4-f3974f6ae14b,2021797882,2090839502763675374 +5a50a04f-c4b6-947d-b06f-68cf006a42cb,1569175963,2942867243385431363 +3f81cf6a-2f13-ee3c-1d84-f31b26370ade,1845298902,5749189535841549180 +b7addd71-55bf-5216-c26e-8096cee8bc80,2736931578,2245955151210080877 +5c885202-258e-31ba-73c6-14802991ddc3,978181002,12021078624260475716 +0e37d09c-5030-4739-df93-1ae613cb7556,988435024,17400065524640726330 +591fadec-b55a-c7cf-48ec-86c0b9b8467e,3130916981,17495348207825347721 +6f2fdff3-ea47-995d-9dc5-3aceca75b719,4183166940,4143127527255230808 +c7bf3f04-db3d-7a5f-02da-89ef5c8855a0,982321756,8526483098806340806 +cc5c821d-8f45-d5d2-ffa2-ffd2e23f74ad,1869337172,8600769896562562718 +781dbce4-91e8-dc90-6b11-4593266b2a56,396430820,11387989139236649857 +55a842bb-5638-f21f-676a-cfe5db667c7c,1728152741,15912755380345885913 +e02af4cd-a4ce-de8d-f6e7-fab45a28017c,3271584143,10576304326893777620 +7d8d5801-9f0b-2b76-dfeb-69aeb0ec7232,3931352904,2294606801624380706 +19e675b5-334e-e064-5e5c-84d23dee8105,4109842730,14815356459061871945 +d4d52235-4e60-c98c-8ec3-a1e3f282e37f,241695540,17287857669954568985 +5ab970a2-f67e-29e7-5429-0b40ce45880f,1050186140,827914099093832712 +4dc8f4cb-c074-9cb1-ee78-3e991cb9d683,21276352,12695038379090579749 +67c00b31-567a-b45a-284a-28e0f713a155,4067670647,9667348756521084253 +e2f1bc88-ead5-46de-09e2-ec6becb33fe3,3557132064,3782636161797250030 +9d8e6b71-7822-cf9f-9674-27589f970a13,1687381356,877853088054847185 +6602185d-71cd-bbd7-1b70-5edda576b7bd,436447021,15882687904199128986 +aaf6f559-f1e4-1cc9-0834-e7100ce50486,1136630654,1870979047681291828 +dab7ae40-1b2a-f132-772e-11cbd6c824cd,52372492,12245221550125219989 +3cd7f614-8611-ae4e-2841-924e2867cb13,2290047441,13244549622138132541 +98ca5f7a-4742-d202-98f9-84a218382b17,3044303871,1711031836726127741 +5744811d-42ce-2173-659a-d78804d4a3c6,77450795,17489435622023331898 +4b67a321-eaec-832b-2646-3b9cbf97bfff,1324315130,988708071061329372 +3dceba64-3734-4641-917e-437a5feb91e0,3265635259,13150920591033324383 +84e0ec9c-65b5-037c-9724-3ee707392071,2538970643,13127569053318809766 +8b58d078-6104-3387-da82-419dc97f0ff1,2839439778,4740874136156353272 +1f6efced-0e7c-9ec1-c02a-d7bb5dd04bc8,839775743,2151333522630486157 +5b20638d-9c47-67cf-aecf-d1e535f7e9e5,4161404706,6483629860748266006 +fc006946-a193-f870-d92d-e7be0d5eb86f,145688589,14694442175462557461 +33113781-467a-28b3-0dd6-78b5ef806a09,523385505,13154530428628940308 +5e9d8f99-546f-b4e1-6a2f-9e98b9842732,2503800261,15834848149941080842 +3c896204-d040-4a48-4dbc-372b638ec151,158039568,16121522166629611333 +ea8f0022-12f1-6cf7-37b2-10432d5c5352,1968362918,13186263663725537175 +218c9dab-5bce-4102-983e-288a1751fb42,3640092093,11049979229018456115 +0e4f205f-205d-91b3-4450-9a068049efac,561205945,2594655132239984099 +0b56b516-9731-ae5f-9ab2-998ca6c3b40f,688696088,5064835052636133947 +314b75b7-f3e5-8ec4-330f-f6ec78b6333e,361746242,14349409895205998393 +e5329eba-d045-8fb6-3fa9-8e0a8c22bd54,524376400,4983747142020025966 +5bfc7cad-1653-8f3a-792f-0a9a0676e6b9,3360083628,3989065864990996474 +7c138155-e8d9-45d4-c82c-2232a930d7e0,1865489905,13851157127849535244 +64590790-1488-7635-e2f1-879968868eff,1949160636,6808176729900284289 +1f132357-4a27-4fed-6f42-dd9e65ac6cff,2549575459,18090889491691019093 +51cb320b-cf8a-bcc2-b14f-b4d9f528e9f0,2327280770,16708676087281951394 +61936bb0-6c25-f124-e848-44bffe97176b,4098458341,6740682791834735850 +0d545c59-263f-590e-ac07-261000e0d3c3,2102831451,3946397474377126075 +f0e6b7f6-0b43-8510-c938-58ee4bbe28dd,1947311220,16276053266002120789 +6539020e-9ff4-901b-0197-d3f802dce595,857305758,4096719841000979337 +0de2a7f3-1106-2b4b-f481-0c01fe6a4f04,3687895486,8506610480511363753 +ca2a8b03-17e7-7f00-dffa-cf15a5d072a9,3165717987,13017517901722197538 +685d2a3a-5712-5032-4d86-c8bff136ccf7,3975867729,3982513639684630167 +5a2e288a-c6cd-725c-bf8a-50595488b8cc,2140182489,13284003993712718316 +3ee6ba58-f165-f36b-3091-e4c029bc576a,3938303118,6357411081474611024 +f4c1cd19-b4f9-1ace-bede-7acfdeda2e5d,3925316098,11540617493650880449 +4c2e1bd3-d6fe-73fc-ae26-1bb73d0fca89,3163316134,16036981266558891789 +50ac3707-be74-6632-294a-a66e0b1e14f2,1266038046,2074544214082544909 +1f88c15c-dfd8-29f7-8d63-53238829a072,2606876746,1354850301630777289 +6d1bebe7-1142-08f8-c270-ceb51a64f48a,3420514640,16367987648987834357 +a0b83d99-311e-3165-00b5-406dacada0b0,1379555064,16013245340794976178 +189877cb-38fe-a920-068e-62a31c77db70,1645866015,4263958849323196966 +a4458487-daf0-1a57-047e-7c14c0ae744b,3227686789,8328919640761480195 +8e69d557-0ffc-f195-d715-564e7c942d7a,1221764661,13929660849179862857 +50fac4f8-aa22-6c5a-ee2c-356f36b0dd17,1359077677,16503428658086680184 +9716d63c-599c-f228-7ac8-90b2c7409a6e,1479189629,7577194457251727306 +9523b0f9-702d-dab4-d757-09564dd09f21,4040068325,17048071631276876479 +98248ff2-53b1-e104-3043-daabbc2a9b9e,827305445,4427711442150769429 +5f6846c0-40ed-d5ba-dbd7-de896dbabd2b,1141762181,4443253389765348902 +59edaf9c-c8b6-abd8-e122-1a5973fca1fc,306377396,10288686187314073449 +cc1caa0f-ba88-1d4c-8b2a-0d1699a223f5,1073111899,9825009482329105079 +1b9a431f-1a6b-5bae-0430-f7fa9fb5db83,3059683415,13568850272396894965 +e2f95c2f-aa86-bd66-14de-e5027568cf26,1981056106,6939577865393797855 +00d271d6-fd72-710f-ab80-a7ffe9bf2d51,606072779,15632124896290023075 +4fb8b1d4-357a-e1e5-5eb5-07b4d8465792,1719289527,894645452091305504 +ab1a7492-769f-7ca6-b5a1-f2b1fb828d33,4123035904,14383474107623845173 +bc4db8ee-a798-b76d-2817-068bae19da31,2897888123,13667828469289987268 +ee8799a1-9cd0-cfc2-9265-6a98c7811bf5,1112676407,14535399224609214005 +256fd603-0b73-f36c-b877-6bbb82b236ab,150807332,8344888492917187465 +b496a852-db19-27bd-4fd3-8a42398b1ef1,2611785539,3169138230413151713 +f1b1da92-e2c2-94e6-2138-4728df4e3647,3231738780,14773029531338634027 +4f1237b8-d851-18b7-6703-ea6723658b12,2793147749,3435994309806928517 +b3abcfd0-5194-c9f1-43d9-0d1733d44140,2093206517,9695123992743178267 +6571796e-9b09-a6ff-5993-3083e1ab9608,776141188,9172068966669528233 +20bc7dba-5e43-790a-b37d-fa1054d4aaec,3424803892,7216068322328270365 +a8798735-6d12-7823-29f1-f99a1b431bb2,686862226,7325177843914742771 +8a2d6955-db2d-f02c-9261-04445c304d22,4095740701,16551394635686374956 +18483ba5-da48-cbc9-4c12-ae8596ebeaab,1017956463,5337370381261011165 +e49b1d32-2888-0944-29e6-cefcea15fd05,928689127,9193735787633573816 +9e634dff-c865-2598-3612-202254eced11,2855689926,3908713715319673205 +4180b85d-85e0-7089-f3c1-4374cbc3d60b,668246404,8988870293971595931 +f4b20755-ce69-4aa2-b11c-21ce030cfbc6,1648870350,4676652126515320514 +90cebb43-162c-2973-c757-7ecac76498d7,993084396,16582356257367661965 +55b2d683-3f33-5989-5fac-439540d881c2,2459148047,4958449784721815483 +1a445b93-0660-0493-17ef-2b36258caf68,1189120308,3095100890240434448 +eff5553e-8d77-7173-4bc4-3b2edd3e9ef8,1227099042,15975789034368382666 +eb448e8b-5491-6322-7884-f2ea612a7401,3552208801,10341146048799364009 +63b3fff7-9543-f549-8a00-a2b5fe0a3523,1767090943,6473823895381487893 +8dbebbf2-f3fd-59c4-3a82-aa4087fb929e,2260410793,13280913210253035494 +b4cb7242-8619-bf43-a71b-b32d668f98b2,3783131263,9776701122306222574 +b9b4b331-0a3c-6203-4aba-4b85c87909dd,3229755385,445792091291314623 +7f2382ac-91cf-8aec-d757-6a99120c69c7,3391917286,7609947516773518426 +84fb8b54-f9bc-e0e0-ebe0-c452303e1030,2510518986,17190397541311915381 +cd0402ac-d9ec-9d11-623c-d4e27db80b82,272648794,3433945648840082619 +9298b38e-1403-5e03-ca18-3c3c407b26ea,3075590510,14265934992401734082 +0a360937-478a-e85e-f4ed-b0d2e451eeb3,4016628023,10540083925472509987 +bafec908-595e-baae-8a27-fb5c75bb4e9b,1319048125,4890679751409177988 +e392834a-fe80-0f9c-1cfe-8a6718757e0a,3947700708,1999732844726705596 +ecce9a01-dc99-d12f-3bd5-510484469730,1843956409,8641862315973937413 +12adc32f-3439-adbc-6aaf-ff6202469a45,2531632857,13792644558499912465 +b7c6a2d2-4f51-1c98-5c26-38d55c1fa71c,1464554870,6838215304175608687 +ca76d56a-b28d-e293-1720-30800f4b28fd,219276766,6606549425133489104 +b0fbe506-acb8-e9ed-0ec9-3ef40e2de818,3098026022,14013849488822795815 +89bf4fa9-9948-524b-3360-92782af9d99a,992691603,974087525770063339 +5768ff43-ebcb-9a9f-6073-7a28ad9a1f43,2287438620,1300059668085859031 +367583fb-ef29-1646-3c0d-30fbfc207ba8,3935102453,900405303938434377 +ca74f08f-0db7-7791-1453-2d9ddeb58da9,1788786600,15211974510666475698 +a946856e-65df-2a60-9df7-3ecaafe3aed0,1077041058,714012160477940298 +4683e444-cfed-46c1-7052-36427dd4117c,2139054450,13309872596916522365 +324c04ac-6d0d-1d3f-22ed-1a1c980538e3,903340135,7201199262694694211 +514f42dc-80bc-4734-1342-e5c8e5aecdec,3985901698,17871656171660634317 +8be4210c-2078-412f-56ff-1b95b54e3865,38142899,11071838546988130245 +99a6b153-cbb4-98e4-ceb0-d034e1ceeac0,2148868984,16837372379694086148 +2811d159-a719-130a-9326-177428f8e228,203059745,1995736065113432774 +17c48bcc-57f0-8657-5d92-ee58db80de8e,3530676906,3971247669108097582 +8251667b-08bd-59cb-dd94-2985dd18d930,1722818886,12617553321956753889 +327e68bf-186c-33fc-0ad1-741a2fb9efe4,3534921022,11173114297911733352 +1b3150c9-ab9a-d7cc-697a-e16e0eb907b4,4130888624,13660536649174545444 +365b5908-a6d9-3757-3b1f-42e7c270980e,682940869,18146324615081642610 +8e014f82-c291-abfd-b7b9-d4fd98a2b16c,3853624627,257269290716187159 +2d287013-cd3e-0e77-20ed-90b2354a127f,3384192549,1434876614230270804 +1c03deea-1367-4898-d239-f863bfec7404,3763956236,14323900581725800271 +848835c6-fcc1-0ca0-dbc7-5a698e09caa1,1485053220,2051417984717784970 +e6c94909-4dc2-72de-e6c6-b12621852819,776547748,16769088799204828894 +b2a2ee04-c2e9-3cea-9f00-733032bbb302,1272454224,4690631111837829998 +1f36d5e1-1297-5ff0-659f-c95ceca6fce6,3649949192,8502147809610691949 +e5623923-933d-7700-23b8-0e768b154f04,812330303,5716263641993822210 +1da8d21f-c58c-321a-4947-76e2de9c2e84,3477371606,16391740824235283680 +b35a9758-bff7-782f-46ea-4a0c6daa01f2,1876240576,16066340258566616439 +47148caf-4846-9d3d-8cd1-320eed04f21b,2015052246,6704979729250171083 +3e297e12-6a18-69a7-c0df-8dcc6f401a30,690123492,10723055132463521534 +b38210a6-171a-8c14-3f62-044f94707c6c,1206858320,18383549045593119232 +c879298a-2500-29e4-02b4-d63f3161416a,2366592526,14944691973692444422 +60ecad29-8593-f04c-8b46-4ce6b5527c82,1924331755,4372434608670698980 +1e531751-b3dd-9ac3-3336-3cae26b390df,1645305342,15340278018942537130 +01b02ef7-af53-a741-80e5-d9bd74928e41,2205569115,8208548908701258460 +442351f1-20e5-789a-a592-a558548f9faf,2074003154,11190916084517211664 +b82f1dcc-5b83-e6c7-0eea-fdaac2108cc4,2791361627,10835551753948538845 +f5e92d6a-5650-4644-1bb9-3e69cfb96905,3569320187,5211995410265524100 +90df7dc2-28c2-12b3-2b69-89d7ca5bab69,1886753276,4303606595707304199 +b1725d51-e4f4-b793-b10b-88519c326e94,3785639284,8224829340880172897 +0bba98c9-c828-44c8-1a92-4a0dd699a1a8,3614408762,11581958058802552938 +d8280463-5f48-c46e-b834-069887954e27,3319860150,13047767027625738512 +112ea25f-24cf-0c79-e5e7-dd031b8586b4,3950032056,15392719607380968755 +578c46b2-f3e5-91b9-2cc2-fa23b5ab7755,4260962374,17658021650837086765 +c4d0f286-ca84-8625-4276-511387052ab6,1577864057,17969025976387311515 +a7f3e9ed-d682-f4b9-5975-027fc648b370,906323353,14606308148086212495 +d38a55ea-83b0-197c-cd9a-228ef95319d9,2974173210,7684591856636538980 +21cb3dd1-5b47-22c7-a5e6-5a45eecc87be,3289580467,14619481798216818160 +b8815dc2-7e1b-3b7c-6636-85e30dabcb71,2919975838,10395565910206441060 +9283077e-a511-91fe-7194-3c01e01c618d,3198708843,13306221075635190103 +695375f3-f79b-b99f-7c5c-a558a5d6f581,3424617433,593212673041348251 +57c8b686-1bd2-7989-561d-3d07abc486ac,320222450,977820211892084190 +13fcb2ac-bfae-a181-f16d-181e8fde1bbe,957129546,4104189794929239638 +09c451c2-86a5-0222-97e9-5a40ad22d82f,610202490,10374766826533122230 +4b95d8c1-16cd-0cf1-1450-335f69083cc3,4116724915,16920359312560655125 +d7b0c374-400f-4c76-246b-076f95e296a7,2034580068,2735151393562326118 +682d272b-a59e-e796-85b9-82523047f382,710187678,11547053330868903922 +50b1d925-3dc2-0411-5ae1-e547eae271b6,660900255,12807048955726600488 +b8f4c22f-b300-1850-d6fe-4fe03a3f9d3d,38916509,12424368617041336012 +84f4d2e3-9f81-26e6-4011-dba29a629045,2593064195,8963566111195837164 +d0d62c29-c87e-4929-f647-ffc3a75a3ce8,3301340215,10178572985518002368 +7c1a6d8c-59fe-6ea3-3113-e5e5dc885141,392578905,8382982996225980584 +bf02fcd7-60ad-20b9-b83c-01395fad44e6,210209695,15230364303069364391 +29a7e6dd-fcb2-1157-3769-37ec6bb4be10,3903648974,3120891803851825841 +ad8ecc83-b08c-a9c3-93de-087d64d0a0ca,1075436534,6200412561703908870 +e0b4fbe7-8262-0f60-a38d-462f10992d01,1284506386,13323130648138268334 +2b595357-2980-5f09-a5e0-094fb93d1a68,334195243,11389288716949142205 +58840448-26b3-8e64-3749-00926b80f6f7,2634080746,2364903191371846628 +63a7bef7-9ec5-d78a-85c6-6a54113c22b4,1128088247,1177242720791227310 +f944d3b0-1140-8415-f79b-652b4d358aec,4130823837,4935415248998388042 +4ff51730-fe8f-ea4d-c5b5-c4e6430fe12a,1025925126,1981400058248159141 +32851a98-440f-b4a7-a278-ff93ee84fd96,776209488,10714431459927910546 +370c4c13-d9f7-346a-c082-9d3dc92cb28a,1961551512,7696204310570536686 +f01981ee-076e-6778-8935-4a08afd06e90,417271452,6593102558166201632 +4f775860-03f7-9133-2d3e-1064cab4df6b,1877089494,2901775435605433185 +2e8f72ef-d12a-b9b8-c6c6-fbe7f6096357,2067958194,73248027505028482 +e060ec02-e702-232f-2552-76ce75d96f59,775848189,4467302794255039536 +ccf0a7bd-3839-0739-540e-254053640511,3457741404,4337185641088993139 +71d58db6-6f19-0181-fada-d866075c321d,690026061,16885489290584549727 +020d3c2b-c905-46a7-0b57-9999e137b72f,587691426,17714054622116429713 +393941da-3fce-f1f4-04fc-fbc3915baaef,1903946957,12061570730208150184 +4624f5ec-25c5-c107-e4c9-407cbdb3cb1a,3347596296,1872998377151731476 +e548882c-22bd-23b1-843f-6bbb288fdbc1,442590929,10904455545840041804 +7d7baf0b-2205-8536-f318-70fc183074fc,1748564107,10703543212928762855 +13b1790b-bafe-d5cc-166a-ca92fa42471c,2056620007,11705325787579992797 +7fceb9c1-8e3d-0f7f-30c9-c62ac51c793d,1405893543,1896328233953776353 +689a3e2c-0a9a-60db-6c1a-535e450a9947,2288342700,4105190056769134603 +89bf2f35-8f94-53ae-e4f9-59c2993519a5,1739809868,6643095196933696523 +11322d03-0fb0-9f10-f987-3c6df43430a4,985814784,12177113845202275246 +a5968be9-3e42-85da-e284-90ad342905ac,327065065,14385131694193892828 +83ba1d6e-9fb7-9e3f-7404-3dabb0a4b592,735356081,16199610838070161134 +f10e417c-bcbb-b736-6b2f-dab830735169,1796120577,365267034471256086 +33996eb6-94b9-3edc-c3fb-564620533505,2105212266,12093306192754823030 +ce1d2b8d-249c-70aa-7555-3de5cf549408,4232683414,7085547816940544383 +18eed44f-9423-a7fa-6df8-3ab395421c17,157301919,17064691841639698553 +61f3b3ea-e41e-65e8-cb6c-c06a1fcdbd90,2177082194,5387971449259074904 +69f7e647-7743-f93d-b834-509c65deadb4,2472081491,6189993665064577689 +e1dbd67f-5268-9577-96cb-985c3100f29e,4228541756,14586905950985883988 +11986a61-c83b-1b6e-42cd-e69ce45026a1,1237020806,6488741107528089435 +18dca29f-a8e1-d02a-d1a3-6cb64c497c64,1745081144,765971334840016807 +3f0d9e25-22fe-6741-5ad3-3955a5366f79,2108633674,8070749516170021009 +98305318-04ca-e69b-aaf1-a81a8cc3734e,2588150801,12439538593593327861 +77788d90-9245-6101-b5ac-2c97a06712a4,4086066135,13803837452375375645 +e4efdfe7-71af-1e4d-f8a3-ada2cc37629c,2556077696,2562445833657799647 +517f1225-9a8d-8779-ea5a-1ac4868ccd44,1224634241,18441048977592590061 +5ad1b099-94d3-3df2-e2cd-e747d04f301e,2397344037,18220116713790971583 +81e16aa6-999c-73fb-0e6f-d9ecf6d663f6,2777393661,11420769926199295390 +c06898a6-a042-3f44-168d-0467f1b6dfac,3628393018,11518009348113261715 +6d7c9936-9738-50f0-2cc5-33517ee2e4e6,2654541842,5020391222196580699 +b771f2ff-b11e-52b5-2ded-e0854a675fae,3568774293,11416725501689032747 +8bd03939-f7a7-e06d-041a-377753446be0,1846211216,2113063460775992894 +ef7d204e-0a60-dbc2-5670-cd48b15c7ce4,2028199817,9326269845576889750 +3e1c02cb-2c85-3f54-ef3a-1ccb9b9785ef,2365544305,16262234934051505075 +4aaff930-955a-3b44-f32e-0407f2888ce3,1571868890,17082015913760124719 +a9090ba2-d9ee-9328-edcf-410025bf5336,2925290980,240657407285629902 +cdc84a62-df9d-65c7-150c-82d5a441dc06,517101877,15509802537451505859 +8cdd476b-e755-6167-b4e8-3ed78ae4ce6e,363036603,12771070146783048222 +a0977ea3-7d4c-a223-706e-a4fba8fe7ee9,1715065173,8010350824442488158 +7aef07f3-3f86-4509-e9af-43b8df1b407f,1178464785,10143908207847712901 +820f6d92-6a64-c81b-bd24-1e276cf50a79,4269740939,835024563730555726 +49fd785e-9f0f-7f67-43f3-a0c338762065,3885735205,5192402334313022287 +466659b1-58e3-4f60-94eb-9051db4c814c,75366105,9923812499701032005 +68a796b1-09f8-93d9-c96a-5a12a9732fc2,793717858,15053169081076668445 +9714e08f-b18c-00cc-32d4-7a925519cfb5,2430593852,17573648792211363742 +75ea5fd8-afd6-3531-c0fa-ebaa577e241e,2190948831,720760156405388203 +320da18e-4ca4-9011-4d00-6407ff7fe92d,1061139900,988251180903250084 +2f03dfdc-c765-0c74-b137-bc45d446c72b,605772415,11846255836793368341 +f42ec6c1-ccb1-0ad2-3180-98b1a50a3fe2,608200126,18183631463345288337 +e4cab2de-3b0b-0c99-e827-a15773ebcf23,3352652781,12909481271209613262 +f65f0913-aa5e-c9e4-d169-2ba52123b8ce,969930401,9553854565334362126 +4a488933-5c02-8d15-5724-308fde793207,1712624057,823548441782954889 +3f92aaba-e8ae-c947-cd3d-d6fafecdfcf4,1018594381,7534165187092143764 +07aec73e-328f-a316-f829-297915d2eae8,4211772659,13928101777222803550 +3568d786-eecd-4c51-9c2c-96fad30f2a6c,1709374267,9325703626986464221 +18b1b0e0-b3e9-108a-7df5-a90eaa98847d,589065529,4422852032371751645 +8f4d71b8-8580-0d4e-6e8f-0aaaa65036df,4012321672,14829061165024088926 +fb71e76f-3c14-e44e-625e-9256cfcc0c08,4145606570,5277481830067850617 +8121d8da-778e-3220-5c54-80c31cabe7a7,1495383439,18269333225996307693 +47537d1b-681e-4424-5949-afce242c9f39,496154014,17660814414125460336 +4af98e5b-a4c0-d79b-5bee-8574c9ab3f6d,1510694392,16569209489948709231 +798b56ae-59b9-963b-0199-384428d6b5d0,204993764,7378863667078407644 +97e2bf62-114f-49db-e306-34f57280fb7a,396224075,16568721632123050175 +15064f63-b2f4-421b-6522-8de5f13101a2,341691691,2262032991398715093 +955f861e-964a-a338-cd95-988ecb8c8897,3922038828,8493145193298415504 +407342c6-f2c5-f84b-b955-f03581b1c00d,3490018700,1704037834187399164 +8043a9ec-e34c-27a2-374e-c5c0f185cbbc,2318001274,909335705918864265 +0bed56c0-0fd1-2f92-d2ec-6163097418b8,1422017914,17969162494337973614 +7b7520ed-c915-d8c0-fab1-d2f1aa085b06,771659323,126694317943419381 +9979fcc3-9536-4bf0-03bc-96d658556d87,4158673910,9080911840838530898 +5e155fcf-c5cb-d060-ea69-0ac105d7099c,87052126,8647519910780286306 +cbf1be15-744c-8d0e-651a-0c4c2ed8cee4,2825407017,17199139906710054542 +999a3047-d6b5-f3a4-f841-8c8278a546b6,2418523304,4494926282804073513 +d5fcd09e-fc07-dea9-a772-4c287446df46,2097562128,17206655320266502041 +1878cb93-2563-a1e3-0e79-7685100139e5,3739364158,7012592738525893097 +f4b8b738-1412-6a13-d422-748ab096751f,1888401896,8589737770756328641 +d5502787-d1f5-8c4b-3b83-d41efa6fdd00,2759371975,2459511002525191040 +2e8da3bf-bd5e-906f-1b44-2597a9647489,1630714375,14217967612030036011 +cf9ba717-52d3-2b37-8337-062560d7f64e,1986391285,12434673831001747300 +c7463aef-34e0-af67-5ccd-80a8a549d4f4,974739463,13543816833097550250 +d9cb7a7d-6f52-7efb-3f7e-15fe62a668f2,2643423209,1120123612070036124 +e7b2bd47-dc24-80cd-36e7-4d0f427f713d,3405381437,4394778977513026398 +4ac15d2b-65e7-6676-8c31-be68bf1a2d9e,3249292113,8015319577700633523 +704a8229-972c-1e60-c96e-cf9cef730656,1091186885,9735411339978452796 +8bf2da57-e0d2-981c-9262-fc3a4d6ef98f,1632546862,4034167320678782002 +f737c336-b8a2-a238-9bce-1c059dfd5107,342185324,15133814131265474244 +9a828262-9082-b43a-f648-f4db38962a9a,3939828966,17320928241482552075 +74ab97c5-900a-bc1e-9e80-be740e36e144,3397267362,17723862450140897921 +f82d94e7-0309-9dd5-9c84-fa2e493e00af,3656602272,14072090948428356224 +181ff660-02dd-a959-f2a9-9b33aba07988,2897672016,42860807357063660 +f616f9b4-8915-deae-85de-e08525700bfe,4185519204,14110062409073624804 +a25a48f7-644f-16c3-6975-f99cafb8671f,1912873048,16641260481952716468 +73826ea1-17a2-7213-aa19-0c2ca0d45e3d,2210335756,4863480123286147756 +9622dbae-2648-fa64-d7e5-aecb850c9584,3500026362,10634833747402462405 +03512f8e-4c4f-fbb8-2fb4-cdccd7984722,1962898404,17640114180839757407 +0069171a-58c7-dd1a-76e5-09b65235e802,586941691,4071710622372131759 +cf8d7a09-a6f5-a6e9-d8fb-acf254df17c2,1484106758,17910315748447622088 +089d98bd-83b3-1a69-1532-6bda73c5c010,2445858110,15606740093337121938 +e6083fc7-3eb2-bfd9-a393-a9fad03273cb,4096450385,10886714413616133884 +53fe4d80-72af-b978-90e9-b0d7eb6926b5,908042003,5958299878598699517 +d01cb21d-909b-c7f3-9bb0-888ed8c500c3,918870631,1074301847795278755 +ec67cf46-2cb7-f319-e0c5-4b9dc824b430,1205847738,2852474728451418594 +9e8b3f79-78aa-0771-2c31-fb5cda701fa2,1577862594,17713608968901862943 +90534b83-898e-a993-b9db-d9f9a9fe04e3,996774127,1983273657672572195 +3d864301-fb82-cb3a-1163-5b0295a3b923,4001832237,2135102156937929213 +6f481085-56ca-6c33-ebc6-81ed8168bf0d,2930257881,9819096603578512294 +1321fe25-85cc-124e-3410-1d915fac9e4a,1314417898,594488595275569484 +63986a01-6fe1-6d82-312b-e58b4c0de177,3828105138,7642357454850435149 +3b24d686-caaf-cbb6-5949-98fbd136bc48,3755542763,10678686950378603242 +738c500f-7b50-e061-9317-25db2a5df6a6,1044281867,921606430863303847 +cdb16059-3bf5-b6e6-ad71-bd3d11c266a9,2638807043,11750733685768359921 +df720aba-a8d4-a2a1-e3c3-f9072b19d0cb,417941094,16323743047572613933 +f8d2ee73-11d4-baec-868c-3eef4d9b1216,2196337623,9142065862097913480 +2da03717-e2ba-4e1b-ec85-c245af115da1,195947460,6001120996815808729 +209067ed-5c31-0ae9-1bed-17fc4991d0f7,826032510,979292545737422618 +c0ddda67-1d3a-6f5b-1a13-7cbd455db79c,3789486883,16993785642244832305 +a809fa7a-489f-b7dc-3ce6-6c3c34646c57,697508967,11876648342438339194 +c087bebd-b5b1-a5f0-668e-1af9e591c0c4,2689707067,14182905961163934070 +ecd4f096-e290-0133-a030-b9c088656f22,3146346459,2477173099296765772 +a79c0b33-7d24-f752-5373-10d4e07a7d4c,1070367618,16947742868382350027 +4ff20f48-8c44-8518-3bbe-72998268197c,2869528997,16052719802129115301 +0588a9fe-9a86-60bd-cf42-a63f94a3123c,4167926034,17311247550488054726 +655b96e7-5396-c03f-0baf-075bf2d87c5b,3860737698,18438126049444514944 +a631a545-dbe5-181b-428f-10738a0a1ffb,2621792055,9653120232414180562 +1d03058c-b00f-48aa-0b09-2944c3dee1df,1666343991,3163658173934099649 +8bac7607-2a27-6073-147b-bf6da2fbbccf,2558301636,10546278945086860866 +429e5eba-5694-bedb-b5dc-f671f64a0118,833799501,13352164678133698684 +46207886-69b9-d7df-13fa-12c9f41ab15a,927869763,7408930308972012649 +8f088b5b-cc31-50f9-41ea-e35bdbbf34c2,1884635261,16287434556568905905 +f8a3b581-9acd-fbef-e13c-0e0cf13d4920,2216700002,13207439268701823715 +d0f93b89-0107-3db1-82d5-2fd64425d0a6,3809502845,1866800225580325540 +ed45792a-4838-9a7f-9a9b-fb6a7db4e74a,892210096,12122411191184785037 +e8e4c2b0-0f60-42a3-4370-3602eb56e786,4089367725,18341883307783073778 +b579d06d-7771-9fdd-6cb8-7a4f16cd3706,2955938771,4387907770482053993 +52283730-782f-914d-31d9-48dfd6f013a0,1313786647,8601152661429479979 +2a2a5dc9-87c2-e8e7-3c76-17f18bdc8a38,2332327708,11681260072750590393 +c8097914-7753-efdb-daec-d1e5bd911651,3878716809,11052601631377346904 +a7d0f11f-a5e4-2032-d427-edd03037d977,1141475843,6787906801738645691 +83f0a3bb-ef54-3cc6-2efa-1d0d123e0c6c,3716901592,629069982053091132 +c8d1b35b-d201-9d60-f517-7f236756d7f4,334998930,3157936085276243063 +9f5a7522-2fd4-dbc6-2404-6cdf21b9a1be,750321186,12917534701172620854 +4a827d6b-a729-3a5d-4dfe-ef5922d3c812,1419155840,14219201706884106567 +e2be0030-3191-52c3-1240-79f8483a1488,558969581,11532045361276037480 +751f89fc-7b57-46a2-ced2-d805b675cb81,990535272,13972673961224043263 +4f9ad848-efe0-5e0a-6acd-313f0b5dcb46,2213874636,18407143016217994505 +73f50f20-883a-93cb-f7a2-108c435e6667,894172311,13238726747857174608 +36b9f97e-60bc-5ab3-ddf4-8f90a131e778,2784527056,7142081686070768186 +710e55de-d16b-9fec-9e97-9066d9c088ae,4221883032,710987388746549092 +ee9c8c2f-35e2-fd96-33fe-18598c4df71f,1137948864,16397310632549877919 +529b85d4-02fd-1444-8e05-b93b396a2a4d,1509081077,12018629139155754898 +39dfe3cb-f5ee-bdb3-ac58-9805cfe395a9,422056332,13497195256643364784 +54ef3aa8-34b4-915a-92be-9f2934b061fc,1877034195,4518903528946119334 +ff1bb59f-9eed-9474-4262-a237cf5370f7,1287739178,8156424893359771103 +cfcaae96-a083-83ff-1cb8-4bc139e57bc3,4216436476,9927059072807249563 +f5b1fd1f-a771-b6f5-d22e-253a268e5ca5,1242241764,3077942084018571178 +5407d1f4-c264-87b8-c145-a5414cd389f6,3091038296,3880101400366782562 +d0d2743c-1257-8921-0149-01eb63dda40f,2557678155,9117357281101983326 +42cfc902-0481-853b-3788-543ab6e89c3f,1128045912,8000401814902020262 +4f565ffb-ec80-0d06-b28d-8f86bc348f1e,2834642795,11564584634133541166 +3743a8fc-7edd-8adc-0f04-aff71c7c5df7,415351860,3657301779193354854 +8b40c9af-2431-1fce-b6d0-fdadcfa60e94,3459393687,4452114123140182089 +be367031-7ed4-1192-15f6-e34c1b00b0b5,2442939689,1849677629628070352 +5b5f86be-5cca-2f98-56c5-6be47bc98cb7,1573381330,1494014562392659925 +1e686236-533b-5f66-be31-a4b561b541ed,983481686,14142040023565896071 +63fa6ad8-e012-46d1-622c-8d91a53890cb,325535077,13835558181724024751 +8f206546-56c4-139f-8435-a26159c4990f,1224409676,2369035442877831030 +fd972623-a69a-fe63-acf9-2862cacf3d52,3166439018,9060666692446784339 +74e4eb43-7f88-4fe1-72ad-9dd20b18cad5,3395609459,6904949299524507103 +e3150676-c88a-0b08-2f4e-4503e12d378a,2971551630,718586230052125368 +867eadf4-1855-3ddc-1ee5-40b5333748ad,1975708311,11129353892262549724 +cfc8780a-9b17-9654-59b7-7e04ce1338ec,1426196895,10601809980685952461 +ba6f3422-36b6-4f7d-f782-bd900c0b9139,1870399034,11568025741288009540 +b0a6c025-1e04-ea8f-92c4-d5554c64a9bb,453413790,10439968642257032494 +7c00a9cd-f233-4ec1-d224-5a86e59c6909,2701320159,15056655841844381440 +f50813eb-b13c-fd67-cab8-84b69c410338,2255127598,9345902081749719969 +0f477395-511e-2e02-39c1-6990a619ed83,819360421,6055774149569986778 +10754cf7-10c4-0ffa-5d14-90b6d22d4993,163940561,12698658272446039035 +17fcad5f-ac44-77b8-0bb3-1d2e2ebbb426,3514798429,12732444131692776500 +08daf2d4-a079-7764-e0a8-8bb8817204cd,1226140351,15241015396534278711 +7b2ef9b9-0f19-7c30-98a0-a4ddbaff2d66,372245225,171377328786965935 +899c5130-f0e4-0891-18c7-1d7dea4c3be8,1092659941,2218055960023307052 +c46020c7-ff2e-959a-a640-a35129adfe7f,3182056010,17188163344577677233 +9cc79e4d-d9ab-3955-7a28-29ab2bacf010,2896195701,17032254608752482967 +89cedc52-6a63-7c63-3bd6-43505afe89e3,441960393,7996175564853340962 +a8b5e007-4397-b138-2366-4b877073618a,2566843014,8716448685396908303 +ca6b9864-e1c6-ccb4-bf5b-aba1c9dee3ac,57306266,4582362744492129825 +94ac1650-0002-3137-bc02-02fd632809b5,3609479176,15102613052599763452 +c7e22f23-d567-fbb7-9657-1486c3a3006f,2518546319,15533550019968854877 +dd62b084-42e3-4541-b859-39bcf2a0b31c,4278743642,9896513585251145779 +7d0c8d1f-b0aa-8b9a-1233-8ceac927f7bb,174651472,2698660282634958270 +b8654e51-54bb-ba1e-9ee7-168831f0c286,1389378473,11159168916972525705 +b1537d32-ada8-32ec-6d22-84c47e0264cb,2916197516,16726277178378180162 +44785679-ee6b-d5d8-b50e-8109c4892207,3750059113,16625507686145305273 +5a12d5a5-3755-0f29-f36d-8860a7228b7c,782783970,1860702722155954467 +4e7d7b1e-6964-ae4d-172d-36c6d98b8aae,1873521263,9024205535617862772 +7774ca34-3835-3c57-4430-1d951f9ca7aa,2290828619,6329994011604973348 +94191426-d494-be6a-ba4d-97bed434aa0f,1356336990,5571735759414800229 +de3ead2e-ca03-f88a-523f-46742a3ad554,2274975736,975310911189053711 +2539a8a4-1093-c330-329b-545d4fdd277f,2199213211,16224652879070078113 +ff160d61-222b-1c20-468a-508b4b517336,2451893400,2575237486372643207 +4a67ed18-ebff-7d17-859d-bfdb5b2bf94e,2031010627,5255961419362539233 +1fdf5cc0-b562-6695-7c10-650075724115,2106347564,13234428157462826822 +8f3ea7d9-2e8e-dc94-af62-c2b6586be91d,1543161435,2978466483548850380 +5a168497-af10-fba5-fcd0-93aba27ca685,2360042794,624613120626194473 +0bbfb27d-8f12-ea2c-a597-5686b29f1738,1989652511,12323210945923774169 +d8d44874-7ae7-d8a5-2bbe-03cfda76dd44,2264273561,3307686914720604091 +5c7c282d-109b-f3cd-f27a-b4f69ced3a0e,4293619719,1159295114849602644 +78c04453-7e14-5841-211c-cfa550ee56a3,3030086301,8472261729444505745 +029b08f8-5ec4-8f8f-4ff1-085aeb53a750,2001563344,10329486083652355918 +0f6445cb-a7ed-d44c-dac3-44a767d343a0,2385124501,1782282360554830469 +0c17e421-1c8d-6760-e7f7-186d052851f9,4251585766,6129621939065287344 +1e817c67-1f51-69a0-5bd3-b863e1b81c4c,2711794458,12409675282858712128 +5b8c2e74-bc1c-a341-83f4-30ebe3202267,797961272,10960403513410349587 +953dfe81-e278-dbd1-2613-ebb25798069e,1665268163,16039354066608183185 +9c79210d-a634-a92b-c289-d60583e2625d,1930360614,5787802462607724625 +9912e32c-598a-1942-8d62-e796ba05d1a9,4127067725,10360619107782192067 +d39b7ca1-79f7-c77c-55e4-69744c9f2742,308301437,7270127215930141462 +a47e7db4-7f83-03f5-702e-b621a9cdce4e,332392019,6663957196838982727 +b2695be4-0e20-69b7-5b24-1c743566d51f,1011092149,16739440550762514065 +30660ab5-d589-a548-031b-a31504c2b31e,3861273930,3964936797351683054 +921762fa-5306-09bd-c57e-7c79e1ac7896,4205166526,1226655986366420714 +c201c8a5-a704-4629-df43-7f7434c23780,489264250,14157084771531314671 +bc5cd9bf-51b4-e6e7-2b3e-d2067f3ac0c6,1681522739,6715220500557149485 +ccc1b869-7037-4602-1d7e-f6be371a5ba1,1079735557,8196353886315964224 +8565b592-1b41-ac5e-6221-ec98b417117c,2307562968,1231407458282877839 +157cfd7b-79ed-b4e5-7f43-4b6bfee39713,1120697790,12867302223802482572 +868f0b71-5fe3-7c5c-f538-342ddde372ce,2463965245,11120792545402458430 +572387bc-adae-ddf9-89fe-c4ff0c54494f,3837739380,5243702470950579312 +6796052e-5cd1-048b-e8e0-764375dc5e66,3970409127,11090282391698284071 +b7c35f15-efe8-17b1-4a0f-e5f77a12ddbd,2372723381,16254942567610091218 +499b5054-1a65-fb84-0b5f-1c0f2316f549,634034050,13383542621928845011 +2a14e42c-1017-6a90-b906-1e0fa471181f,833287006,7018165780218668522 +c1eb1cf4-984b-bb2a-89fd-c96642e76442,3099957223,9146281215453831881 +a1fa3bc1-210e-3ee0-a705-bc6e408241e1,4074630110,6188851829727156976 +fc7be3b2-3355-b42e-2c32-3eb433b3fa87,2713766988,2810393811292725172 +2a21c219-f2ad-bdee-4d2e-4b1c11907310,4009888719,15331305988802908140 +07145a5d-379b-12df-8e3d-f9ddcb62e866,1102160905,2035790234627141918 +8361c297-1ebf-1416-dbb2-640a011569b4,3850744503,7225170828523535042 +d182e1c8-e363-9fb9-0615-c6e21e8c7d07,2092945611,11588523409051919044 +ce7ef5eb-1b98-c6e6-087f-70a723b9b68a,3295730355,1902725998658277889 +68d52114-c12c-59e4-d620-fad42ed2ad0b,4287426599,11044120543266428830 +cf6c4975-2083-0444-298b-61bea33b0052,2387364936,12365644347424610518 +260c7497-f376-2256-1d8a-21156d12e06d,2854262767,13481108316338105446 +933d763e-ee0b-def8-b652-391cbbe92301,4112805369,10312690427606842971 +36b6409d-b2a3-e89d-2279-e4589386448c,434896053,2305480675946130247 +4e2e7b64-9e3f-0cbe-804f-85ef13a094a5,1010004096,14429331466443227986 +d97535ba-67ac-9c58-c54a-c770c2788539,611486636,12449969850220897866 +9913e113-1067-0cb6-9e4b-595dd395097f,3606349889,117346384788047015 +d8a0f78e-901f-14ca-db5f-b2aa6bd92b25,1790322190,13717069469537132624 +9791b0e1-1f00-3ab7-20b2-b143fbc379be,1290545733,6617881531450174523 +791608c8-6144-146e-67c6-d93bda915b0b,1117275935,14774413871815851091 +256096e9-3b77-9bb8-b6b1-930c5eb8deaa,3408878459,8762256080915468400 +64126e1a-e80c-c603-c101-4a73e1ca5893,2409893155,1938099711295885549 +5d4e5de4-fa53-9d8c-2e29-962a56bfbf21,1904770178,15236571851736592347 +55ff8b6b-98c4-fbcb-798b-4c120fe7e471,2418983904,11356470982400722771 +3dc111c8-45a0-da0b-6b8a-830509aca812,2425140973,926110880299831459 +92e37770-bac9-f74d-caa6-c331c9619bd3,2449422602,2632818142172985450 +851d20b3-7b5a-9097-479b-e0621988aa27,2124537552,18235481699319906488 +7f0baba0-8085-7eff-42b8-fc5a5cd8644c,3646233358,10272473552845753392 +6b90550b-b2d4-c7af-ace5-1e9832920ea1,292635050,3643493640968641601 +779b7038-49ee-e47e-c2e9-18d57f79b3b5,700053641,11548013574240404535 +435d16c7-de9d-aef2-c4dd-1b213e9a986e,2204692191,17840271529224175394 +7077540a-0966-961b-36f3-f5b6add76a00,1509821912,3665883959456337468 +d8230ed5-eb18-f9a0-92d7-47b70be5640f,2167787711,18130529898545593404 +6a1d1a24-27cd-8b31-e4ff-4e77c368a173,579102729,7718999372375291181 +c9e106a1-8f23-6071-4d2d-41942f00fe00,3275311948,6817078637802680294 +3f3708fd-03a0-2eba-037a-7d8acec21ab4,3550993247,15156754999238560371 +4f82f7b7-6af5-81d3-ccb6-3a50cd18aa1c,1932325860,17162554544459315521 +868f74ac-3994-abe4-c1ce-b11594ffef73,1379981906,10126421696149912409 +cd24f67e-f5c6-46c1-b6fa-75732587a436,1854610655,10614279075567076052 +d8a7e657-bd1f-b678-b005-11504cef965c,2920383149,10618873732834932287 +be7a9099-d875-72aa-1573-6cb051e94ab9,2133508504,11253200464871308929 +a75b0700-6815-e139-b47b-ce7dcd0f9862,3830133619,15143404502787474839 +36b51c1a-74cf-e986-1ba8-710e9978921b,1915673244,13672605108331482137 +755ef426-fe40-7cff-9c21-583eedeba1d4,4012264959,4355832169635951806 +f1345afc-b287-6c9a-dbfb-19a9b93fc789,593848166,15702933995181868509 +5625f475-04b4-4f93-7baf-25931b6cae4a,3744865425,16088517524982615264 +2821018a-e638-a856-a7bd-e6022225155f,2299021262,15731406418986070093 +d5aa71b4-b03a-7104-6a9e-e581cb294651,2020031855,10615675828757957258 +695d25d9-3bf5-43e6-6904-07fe271411c6,978715645,6571771512476624157 +6d88b9ea-9ef9-b7d4-5029-5c0acfa776e7,1123074031,765144726696410338 +155b1e8f-25c1-ac42-b81d-97ddcff99eac,3664692236,10745163287650254441 +169b7957-5c6c-85dc-07ce-00acc0206a53,3227096742,107044451166926607 +8e0483e1-622c-a751-c0d8-60f4abd51de9,792430514,17045755483167345488 +44fedb31-e843-f17e-b28e-9d57c52c2911,2076828499,12610967691109875401 +b7ca62cc-314e-7e32-99b2-ed7e66931b87,1018473201,7253068115607696215 +db537cee-c714-e2de-8a3a-952d77f27c49,1472787843,9664660258291467449 +1d54f881-3abd-a4e8-408c-b94f9f17aace,15852583,10196836027531533355 +9e026c16-2022-5fee-f7c5-16bd5e498c39,3381391369,12390111119837301483 +ed393610-ca4d-b381-4246-2be1cd7eecd4,2281478392,5344379920218987447 +edb61619-5679-bae3-6e77-4c24d3dedaa8,3103374152,2006536625094998192 +9f097adb-e46f-187e-7d7d-d07c9bd40266,139540890,1660545526161594223 +8623bea1-bf9e-8a8d-b055-b6a67aae81d9,453235035,11504900155067406782 +d6c7a830-895b-bce6-fc16-145ce6079b2b,173643573,7799186770213165151 +43332686-5c54-98a7-0c13-965ef0629cac,2544705125,10048452856254910578 +78a5abc6-ca62-9d9f-54fd-6f6275fce817,3130718313,1621540396735798439 +8ee8c059-cba7-05d5-b946-682b462c92b3,1138055084,2876110955218836571 +b457e8b6-233c-322b-9c28-4e0c39b8215a,793163346,12493570388389032306 +0e009f30-916a-cf2c-2fc2-3adbfac26030,3046770189,6772994533272217807 +23cf2693-0e97-4aa5-0a9e-f1484273f5f8,1582016021,1710952347762847040 +90d3ed61-41e4-569a-ed1f-06c35b0ea50c,1149851663,12913025927112543093 +0ec41962-a50a-433b-52b8-4f2187023728,3699764353,17765199311778869458 +1117a3a7-1f66-794a-631b-196cc25f50eb,762741550,1023246260146887551 +09570ca6-3429-f3ba-fcc5-2fdb264311d0,746447503,13884162886965007831 +eafda463-d22f-8af2-1e55-8230854f39da,1430847753,3884402759793520656 +9aa26aa0-1b05-78ed-8fbe-8eb066ca5b2a,1966952583,17618945195867775366 +7f791d57-a5a4-71be-1ebb-e6725122d548,3478917132,14804186590257451418 +954e852f-ed66-d5d0-f5eb-e70ec883d3b5,2135621960,16736683370673333797 +683d6f2c-ab8d-41b7-9210-179dc8e8d547,1643972448,11117550355243986150 +7cdf6bc0-b79d-4e1f-ab2d-807de024fba2,2204852279,12166813695126066319 +cf3106e2-0387-3a11-bf83-5a53696709f8,74421968,4539510415111621388 +e0112ff4-68ed-bb6a-01aa-f706ab8034b5,1597542512,12388208463726689826 +a1981e48-9401-b878-3fad-6896d0783895,1512254140,15588784190112253191 +9899281b-57d2-d280-1cbf-050f549a2b81,787344524,6495007346463468957 +75fc0763-ed18-2d17-a754-fb7fef2da5d4,918151716,10143488124716776396 +8a0a5ff5-cc20-6975-2294-a4713d735d6d,2411664885,5997504636840658448 +e5c3babe-e0c9-f681-2b41-f9328f24c038,3231530427,3994307007018731844 +e5d73324-4a16-a4c9-1af2-eef2a31e21bd,1632268608,10932883104252593775 +3a644034-0577-6b40-49ed-d93576fa4fc8,1957956138,3306619912731061474 +26a1d93e-3657-0005-cb69-e34a08a9be67,2017906827,7212443028575160060 +cb823207-3528-90df-a947-dba24feacd86,1016433740,12119306648765369534 +1da0c5b1-f521-8d51-3e25-6d903c462f9a,2601115843,9297090807621058308 +b6395d26-e821-7f4f-53c0-42c33ca1f198,3784440908,9985220930012222826 +b96a74cb-efcf-cb52-9de3-d2c47865ad62,3917131222,15286093149036214393 +dada9b69-993b-1445-9a68-914c121ab29e,499231900,10465979094343020519 +7fa8edea-4a5b-62ce-1ec5-51c4084a96b3,3893304549,10107368591403178930 +12df80f9-22bf-6bb0-1c4b-5935a1fdc6ef,950111617,15126090545727790956 +f1df4ae3-3dec-c94f-9425-4126ae747202,78803402,11143780729216516399 +ba7dcb5d-c427-6f54-ba68-1ef5b1d0a6cb,3048723701,15276591729024264910 +c18914c3-251b-9b78-a4b6-49a4afa189e0,614042263,14751238825297763604 +632956f2-1c1d-a654-8756-27d2602a55d5,133955936,10221276221076318772 +17e2fba6-5863-95fd-7b4d-7d3d91bcf90b,435410938,4411417134863247365 +85ec233b-8680-eed4-980c-8e0f72cfaf63,1640606635,10280627328994113133 +57afd52d-b705-fea8-b854-4728a2a93d67,2360115290,11619486988750253664 +0bfe8ed7-6d33-b2f3-a753-3bf647ca1524,2203976923,7092525471937395521 +eeda14f7-735f-7082-591b-585d4a465391,2706411466,4893623614054671834 +aa7d9819-257a-d5b7-c400-7397637526a3,22547585,9737448923054774962 +580f2612-8ff9-a01e-2788-3595894cf829,2114363323,13685748409701674114 +a46adaee-4875-1530-7156-1aa307d30f2b,3139898672,11403117047917244589 +59f4a944-578e-89ac-47f9-a1c0049fdb59,2668080731,1666972434682113491 +73aa41d3-5ba5-d0e3-d8b0-f6cdbd07f429,1983851495,8135439958839555761 +517d53ef-032b-9598-e063-0f3617ef3681,981027203,10290259746489324086 +79f3f3ef-ef0e-e084-dadc-c3b6ffc27d7a,2372427040,13735060346963123578 +b68d3b7e-ce32-d5f0-a9ae-1b5fe39d7361,2351702601,9137659352007022444 +a53dd349-40a6-65e2-dccc-1719fce69434,3606011965,16157773381847607433 +8bc5b994-364c-8823-6b0a-104f56bf0722,1679383597,2326974089544333445 +f26b6ae4-e059-7670-476e-24240b25a642,893087754,15165294139979694483 +64ea3aae-1e9e-a929-c086-15e275fbc7df,3711558129,6082996562312978159 +b62ee50a-05b2-9796-3228-1c845f849e9a,951520357,10067202335047884734 +c47d3197-78d9-caad-0383-54856b105e8f,774616133,12470552923980011946 +3fa7afbe-8d1e-d527-cd4f-f634580e6c30,577031702,15670202197694717180 +15845dba-58d0-1062-ce2e-eccfb2327ee1,2089664324,11010169162956823094 +f3e50e53-d84f-bc05-dba4-8683363a18f6,133331537,14703368428318678132 +ddd4dae6-f6aa-5708-993a-dec009fabb2c,3273086416,18064339089440988257 +7ed964b5-c168-7fa6-e8d0-105dfb6f3887,1316843003,5204556157761179021 +ac885a33-ccf2-c2fd-f632-3091bfc2eee9,1104071943,3847581477126266002 +ce5f5907-3568-b842-f8b3-8c5434f51116,412719961,8804305256722979282 +713db9ab-c075-fe2c-3ac9-507e03415b8f,2963607582,9874843753097371051 +5255bd5f-9e0b-a26a-0a69-9e121396c07d,1280727529,9585411715965052008 +f61be2fa-f4ad-4a84-25b3-1f9052dc07fe,1527512085,9382833022378624092 +7002e2a8-c58f-f4ad-87f3-c3081c9102d9,3460985336,15013021183169486142 +22487bb6-9b24-b2d5-6feb-a9ceb57ec1d4,244469997,8502944395563067333 +82a72572-9a7e-7477-58f3-ac2a93d0162d,2727094510,11751446716133166560 +826019f6-66b6-5f5c-0961-2e281d15d167,524530451,14784697604908006457 +0b422938-a229-3578-8a20-6dc5feec66aa,721446903,199895434361684978 +e0c71985-2648-30f5-b9a9-eba81e776df2,875085556,16605703068022501497 +a385445b-7ca7-fb4c-3568-861243c6a754,826538117,2234264813489251222 +cede4878-54d2-858b-4106-25cfc3bb8b51,4176103373,10420480012124987541 +5eff5f2d-372f-b71b-e203-d92a79f639dd,3879257706,11895480935844003181 +69d2ae70-d4cb-6a46-024e-058cc750f09a,3486497301,15781075825600262614 +7efec1c2-6d03-6964-2d74-b1afa1071fe5,3546517019,6337598934957749141 +12f0763b-df10-c8bd-6711-7351fbf21dd2,3330230048,15945371640134224748 +36000a3f-0f73-dd9e-fcea-e1ead63937c5,2281400433,7974397493824322236 +7f3c6548-fae9-ee35-873b-767c814eebdb,2698306767,3190215202327017256 +a66eccb1-c2e9-2428-0906-c2bf3fb83727,4246720730,15043404573628881746 +78e62b0c-4231-e733-14c2-13750a82b396,1860516384,3717320434552424261 +e320f842-5aae-a68f-3a2c-6dae6517f55a,2509447828,7668132430037382920 +e73b3f2d-82af-68dd-aceb-3d753408af15,2939234610,9123090612970173731 +3b491f0c-2583-a119-50d2-32e9626b1143,3800105277,18187096800124983312 +42ea6585-3d5c-b797-a84f-531ba3537ee3,547478171,16917546028251031464 +dbe0aa43-dbca-f410-7ff0-cd2236237ab0,2385042766,5709491381889851142 +6e6702ea-f858-407a-eac6-67a0634951a9,2707276745,16745359070584359793 +3d4d6a5a-55ce-4474-66a5-1f1824487342,2937706434,2349148463053891286 +27630db3-8497-5304-9489-7f9e2274e44d,745607483,4632716251476564948 +a9f4a906-ed8c-9ca4-b0d0-c91c87db3c5f,4180415757,12959546520072625528 +1445fb4c-67c1-526c-7e28-4b24c4ee4065,1886120815,8613830488269025160 +88f52b00-bc9c-5b2d-15fc-f396dd2a2fdd,1476393618,5274810601223669257 +4bada792-22fe-42fa-5cce-ca6444539783,2885869247,13077044223544232556 +a36def8f-cc52-6fcc-2d80-39286c23b981,2971801611,4976625132049526799 +433e64eb-79c3-d772-f7a7-194c46abf513,2206199030,14647272065132493871 +6d86a57a-cc1b-1362-821e-99c2e03e8026,1014163585,11072923316265542989 +7082f4d1-f646-f365-ec19-b4f99973bdc1,2577704048,782628753288945158 +91054f65-d300-3b48-5085-7a1ab6ee5128,1011640484,12055826248036052549 +bcc167a8-3860-2734-2e62-57b73b766ed5,3866752776,5693617751817925379 +12d17539-cc7c-efdb-fdad-44e952f98ea0,3208750460,6169364511764583958 +b68bb87b-5890-dc13-18d5-292a57470624,3173885528,5371879680903082207 +bd5fdf57-5a66-79cd-48cf-d4b437eccb68,323481638,14983067207483553021 +34ee961f-ae2f-0ebe-d022-d1b7d8b3853d,4139371014,16851306912311267098 +4dc3309c-3ee3-bad0-046c-95803e6aa8aa,2393128117,16888723572640703143 +bdebf839-5a42-ce64-0b2e-2bf6302216f0,3594884622,9973163834702364243 +6cea0621-356b-6fd7-f875-6e04508a31e5,228824264,10115802825035948570 +5f888134-8fad-b2c8-6337-e7dd6482f2f4,1703643370,11066973118804090817 +666285a4-76f4-0f85-e487-05ea329ed888,3017319883,3520811434753256165 +f6a60253-0321-7190-cb7e-02f55bbbe958,2944583169,12193065669687687690 +dd4f1bc7-cb07-2c72-fcf4-8d4f124fa980,559083534,10108095518136584350 +6cc929d6-78c2-7e17-176a-2e9d30d989bb,3240537949,16869525805045493994 +0fbf2929-bf42-b70f-4bf6-5444c228644f,3951658925,11587665100537912722 +d9738523-6f4d-2cde-775c-1d1183bc71fa,539069731,11877137756389542819 +b28fef64-84da-4123-a33f-26536ebe7afe,719837594,12382908107260710816 +e430c938-d7ad-c452-1e8e-d0322ce12c77,4217174157,8499872473423656855 +ab0f4e2b-f344-0cbb-8213-a8fd3b4d8d23,3714604397,15409062405920235456 +f3daf14e-93d8-9fe0-b98c-c55ac8bcf199,1134452050,9745435449996024010 +bff60f87-4483-8d8d-2a99-e5ddbda16bd0,408506551,2509944774171645744 +71f2eb19-10c7-b13c-1363-7dd81fe49a55,2234780242,842796073187582104 +1e350560-b4d2-bc44-b326-fe5c69190a0e,1702220217,16636939473715089505 +7b92039d-0c5e-0f2a-9bd8-445aa9172698,3612382605,11026797765030563494 +57c83438-af2d-5f50-3def-9a70f1ca2eba,1776231146,12193481800224796062 +22db1b11-85af-606a-f1b1-90b59acaa41b,611859338,9300425082331652267 +b72f4806-d195-b1b6-5c1f-510b67f78cdf,3882305928,9988059841698950620 +c4e9d3c6-81cf-d52d-ec97-e60ca2a5b5df,3934191538,10531711983387058378 +d790a432-1555-ddf5-7d32-252c8e43d6b6,337337651,17724638390982292906 +b84bcd66-e992-310a-7ba1-18d8a221d2d6,4222651899,333033909535025023 +9d3005a8-45a9-ef3d-e83b-392329d971db,2572639960,9402478450673773222 +1206ce92-6397-d56d-5ccf-d77ea4ba2fc2,3951714488,13759919353784911894 +8d6bc2d7-71fa-e833-bf8b-e50d41ad3425,3238623036,1678087165341342803 +87a13564-e388-a6ab-f3c7-96811d4e2fc9,2102022686,16048636336559492770 +91fdbaec-6a76-9e4c-0027-00e348973226,3210956474,349807899185259954 +64e3ac56-0661-e692-5404-6017bfab6d28,3296418170,11532558025557196076 +a896f264-935c-4f58-c799-cc859c59cb9c,2672666815,12227235065636364697 +e4742e50-253b-62de-a8f6-29baf08a3c62,959884467,12970582461442452651 +2f607768-08f2-d503-17dd-9fae338b286b,2319793633,3193574055694579270 +6a645d4d-70a5-74fd-803a-256f9db7762f,3792005681,11601004830782003909 +f603867c-f40c-a1e2-6083-251371f22bb0,84559254,3704500354733845332 +c2a2877b-0c35-2a3b-8feb-eb0a5b938c4c,2027787644,11753780299562803484 +aac165ae-7c81-5e6e-5edb-d0daa00819b0,3594172330,2492709680709327467 +b75a3690-f553-dd99-1026-5907d6bd1cd3,2495555571,6625603264035433785 +f8277517-0f7b-f01e-282b-1c31302596aa,3428830627,7755760718603840086 +d501aa0b-2d30-93b7-7b38-8b06da014991,2869312543,10709489273141133575 +37c4874d-fdf2-185a-78d1-dc6f9b8621af,504693733,15180167181759356947 +c5285a10-7584-bd19-43e2-1463dfa3eaf3,1467750425,10034710479541463606 +792c1af8-6807-e2c0-0d9b-0117f3bb2936,227111233,7635471815320371298 +490f24d1-57be-bfa1-6b8a-ae988200a45b,1009943943,9009405052927942840 +597b8386-bfe4-fd64-579c-6818f3fc6e14,2281554202,2635696355686559983 +0da97b08-f814-994c-8d08-109df0698363,3018350882,13542735333471842190 +79d12219-e0f2-ad56-849b-6f62a0696a43,2238780079,15992027127112262768 +9daaadcd-1cd7-98b7-8a6d-aa1e4fbb2cad,4262344400,103489088420995469 +cb406e81-0fa3-6497-716c-9601fafe2090,662261196,6332210328372860889 +e2fc17e8-c3d8-1826-35e0-9c0c3ba947ee,2141486752,4361407851168516426 +fa1c8ef5-30a5-fdc3-73a0-0df90f46786c,3085786120,5477101083322340328 +974ec16a-76f9-c51a-3083-f8de769c99e8,1597115219,15279430018453599916 +0a548291-2c8a-7828-3b14-4af93b02fef0,3148099550,4426580745232336462 +9f3f8fff-967e-0ac3-1c63-ef6f33a6102e,1790455322,13101426956710032092 +f1019100-27fc-dd19-9f1d-6980bae5ccae,3219889018,3860349725961513057 +76dbd980-ffac-806f-ae18-871aae271f5d,2824430767,14592634570220363544 +cb8e1841-4619-ef4a-3dcc-479b643088f3,2805252016,16759185058777092199 +7473eee3-efd2-0a64-00b5-3a20ee784fbd,3605567894,8242072822743467901 +637cfd01-3944-5bec-f1f3-a8dcc666f329,1183958918,13989527476787458373 +0bbc73b9-289b-6216-b0b5-4f696d68eedf,4248337627,3883036896919719191 +e0f03076-bebd-a8de-09c6-5b38a71c4b53,2543487413,6371118279189710559 +7c031d27-19f6-bcfb-91d9-aa4c78ae6799,2150097710,17746267694972938259 +ada3872a-f96c-3300-7b0c-f9b6825c0f90,3667400001,10828302587908649350 +2b5f75ba-48a1-c171-797e-c11622205916,737261372,16595202234488448382 +91294591-cdd3-56c7-46a5-275da38c0e18,2742685870,12723816393927065440 +0d2c94b8-d2c8-a590-dfd7-da201e374de2,627777159,18243112116251639780 +e6e511ac-d6f7-731d-5576-df19c6824fcd,2249921850,959130383119275961 +2fcbecea-e52c-4b34-5b2b-8d322cb8a924,2869125094,5077400123312601129 +fb0a27f1-a6c9-be30-8e1e-7c6e55830c5d,1259239839,9924771791275217278 +061072b8-8068-ddb0-4194-8a5546c741c7,187696702,17146147857869230269 +8b841f2b-a233-05b5-7b8a-21f847620a5e,2713405354,10864209546312669500 +576045e6-7a09-50cb-3cc2-286076ddadc5,2539453400,18313168232000897584 +5b028a2d-ae24-bf3c-64cf-50b37559b154,1535166152,17805137041792589391 +776e0b15-79ec-688d-1cf3-060cb2761851,3905477498,13204327250285078991 +1dc34a46-1592-a3ac-f765-e951053ac84e,523375717,811531637393986476 +fd88c8b7-8b44-1c6d-ae1e-8c582790406b,3072253763,1961215464317784339 +a4fb80c1-a958-a1db-e629-3f294615d500,2440522849,4153487518037114980 +e01bd875-d196-b867-b22b-fdc8eee45c97,12697688,17219577917455875401 +2036599c-aaea-2e6d-eab8-f02036c1aa91,1773341906,1948537903576250263 +82ebdf90-8f20-aea0-631a-d84fe14cddd9,3810116841,18254761517610114932 +1d4405f6-af82-dc02-50a8-d1b41d18bfd9,1117153236,17626899589253669899 +72337599-b970-6b16-e0f3-96fed815dac4,2285042019,6303560957011361411 +d8f585f7-e7d3-70dc-f3c5-85f16b1375d4,1892610419,5170736361618199994 +dcfb4dcf-13f3-232f-83b9-b48abb245a83,318429607,17047636811838819889 +c8367777-4260-9a94-ced4-45a76cf3c1bd,2818639894,4681893234629493919 +2090bedf-c1e1-bd34-5d43-d30b7fb1e42f,2004108967,2105305920137415932 +79345ef2-f924-6d11-7b7f-f18ef789e12c,1413354866,930743516459764622 +2d0a42b7-2fd5-9e81-810e-23a7b1c005aa,3494247361,15996356150483191018 +5d737bc1-ee44-77ca-14b0-fea2c216fbf1,2229745889,16243625138800914509 +13c4980f-186f-4d5f-37b5-27871120e3e5,966090297,11104204339737730500 +a37aabdf-c168-a0c2-e8c3-e7ace336d6bc,4098659367,2555075224708050226 +522d28d8-df4f-8e42-b25c-ed235ea7c7e0,4271031513,12745000709131428925 +e9768b53-2f9e-5601-6ca2-cc1e1555a0c5,804944290,18408155936007202052 +4a4c59de-d861-25ac-3d08-4f301be68484,3977385903,1217095877089294475 +9c5bb417-eaa0-d138-81f4-bfd0a157e2bc,2734448594,11675563615913633567 +a0c1e969-4fd5-3562-53e8-2df6582b6d27,1523738764,17767839411761276016 +d471a387-2c27-73f4-b879-58de90d47f03,4230141760,17652225386222382988 +79570ebc-c2b9-fb72-89b0-555a0648fb0d,2341378959,14998038507864862785 +e7b09263-ad2a-314c-9c35-cf03abbc594b,2971787476,5079984941189379776 +adfdfc90-1df5-b183-e7f3-6a8706ef53e4,3032193233,251510141763423032 +a439084e-8fc6-13f5-30a4-9fa6de7561a2,2755634510,10582396570149953357 +632cc560-bc21-d588-9fe9-92d955ed5f16,1142769047,5514569818060385696 +5236b5fb-c158-dd79-e3e8-2a1ca4f18ff1,2459805712,3254022041756245991 +bb5870d2-deb9-7dff-0e08-5e3ef710af3b,3902641887,3421770952207927676 +90286e26-4ca4-0916-bd6d-a4f4422db52f,2158167192,3959674955330726919 +e569cdbf-1d65-2e8a-c966-3dd61d8b63d9,3092797121,11199404986769658616 +70d0ba6a-0341-0c76-6985-a64a2a0cc93d,1807755503,3445115644017476227 +59a95fee-860b-8285-7aa2-4d714829dda4,2474797301,17233227771020308121 +05914d79-41d6-8ffd-3323-7c6dc3fde581,1224712281,7963857379034353430 +ca65f77e-dc1f-96cb-0a06-aca1b392a3a5,1456690587,713847753539896939 +41091afe-2817-2cfd-e8f1-9100cf6a8884,1513268543,6224248002391718410 +e4148a99-98bb-c6a5-1bc7-f733b0612177,350267099,3945826823853607871 +6f376482-2120-c8c5-35b2-f18be45b0f10,4256229249,15134317540379907898 +fcf27581-7159-767e-b30a-f6caa73e4b69,989182948,7492790488979041910 +30e3d1ce-d114-ca8d-b89d-2704251f6e1c,3022713967,12869659531544115065 +f038f36f-3b16-41bb-db8c-c56d9be198e3,3670050285,2789021543118863675 +d4bfb4d2-1345-a385-ae3b-8d87bfc3abde,507093519,15655548601070085636 +07bc7ad7-e4ed-25ed-9273-d4512df34353,4016361836,13021350511799685598 +07fadcb9-42a3-bea2-e0b4-0c906c9008f0,3303282693,7870931461360474796 +e162cd6b-30e0-5daf-15db-a08e21a48848,2769516650,5582772955378937339 +f2133415-f8da-c13d-6a7a-0f2d8f2a87e8,3558286708,8428724705878833037 +4ab744c4-fc11-054e-82bb-b68b9ca2a812,4246400992,4546408512924554535 +d2f7dd1e-3f92-35ca-5311-4893ad137f15,2263613730,382598167442081549 +c2d990a7-da59-c2c6-d179-2e768d532a71,1717567693,970443880362410600 +92d424d1-d3fe-421e-ebda-189dd0a03d1a,3803676563,5095926865815837168 +e94f765b-f9d3-1d2e-e28f-723430ec589e,2815882153,4176977291417151340 +587d1965-1b87-42c4-0a56-dba021bf1520,3612913641,10131822835589928948 +35d16633-5f06-6513-a475-96582c59e315,1547052070,14943851060141341244 +819b82be-b9e3-ae32-5ed8-fa41d715dc65,2515724651,10691980807450805604 +ecff0d93-930a-42d2-1727-cfdf1341c2bd,1046693798,15692870152242871602 +4088b4cf-407c-4b56-4620-856947aceb63,3480313253,8573955299667134581 +c40b4066-9e9c-9cba-a326-b69431279d49,1793783074,7435231811276168516 +ef432a17-70e3-9983-332e-df9f911d4d43,203311905,14564026785675356346 +1fd56e95-336a-ac81-a5c3-5afa60db6fa9,3976739382,8556608814480507312 +a13dc6e4-0e64-5059-4585-2fd26e57dfb2,446115276,3123295273426618260 +de3be4d9-471c-c597-a5d1-1cdb9d0bc382,867099860,539468251559389504 +80efef19-f1ad-de17-38a0-7b18920b53a0,1979962517,5614003161846978034 +625ecf01-980e-4d5f-aee3-729d3ac549ed,2304390700,15585382390965372971 +30a6ed25-22dd-082c-c5bc-e77dba1f1aaf,4275438010,4489405432939483575 +046058f1-dc5d-7c21-0191-33d6d3c52776,820368413,15619635452160863731 +75d1bf42-a668-7904-8ca7-5ee8ee0a839d,64709085,8156873343765074112 +bb0d1d90-09a1-b47e-3e5c-7018ca488d81,333561029,2927605832729325923 +7c4d2e1d-65d8-4f81-c2fc-c7052b4d9003,845288895,14942348899429415613 +6bcdc239-f1c9-1987-7739-3e281962b907,2890497969,6016033106782317158 +4a868d09-2f50-4416-3dcb-f59118655f86,3144641897,770002338573588141 +b9e0e6f1-1644-fcd7-b394-1ace1af7f8a9,2399106393,7230266195771411986 +f7d28fff-c1c4-8fa4-79e9-8737d90a4c74,3978064814,14557725892833216651 +f3ca7925-c272-ddc4-0c08-e0ec530cfd81,3651314378,13472926204074388409 +b021d134-1627-d728-9691-cd21e25e18fd,3716811692,11486259457346181592 +67610a4e-8f97-1e6e-d054-f371ce4429bc,902214169,222530660929415988 +d1d38f1b-2e9a-6a46-0b90-e728fedede1e,1741786844,11458031836263753692 +29f7894e-6495-d54d-d9f9-7f428e413343,2220654114,18072976672111689544 +f1d85471-5938-f21b-84a0-4ef846c71aba,2212166239,12456368508196825314 +e2dc39ae-a977-82a6-4a6f-28dc2c09eaaa,3105751824,13336341822930610936 +c99a1306-a575-43e7-dd56-6e656ede999e,125373151,9408874414275269978 +44fc496e-3b8a-c546-7a9d-dc275b59fb93,562865591,1599773564232797284 +feeddbde-13cf-ecd6-c147-f2feacbe06f1,977378062,2340303635292700477 +c0ff17d6-0ab8-2c11-284f-4197b3ae974e,4069617587,15684965877670388234 +eaa41df3-c120-7a39-ea25-7764f8c509f8,3758540817,1211422013411393792 +9d0dc66c-18a9-6d7c-457a-6bc893f619c3,2878642821,16170169883248266811 +ac978c36-d917-b495-6f43-d691e61e3948,1444899094,16413665214233059551 +5d04bb2d-180c-3e08-e19d-25160b75a823,4197355996,1970199545283321818 +5d4a8e47-3e07-098c-4397-5bdf8d9af300,4290947357,10523122503010402598 +6b54272c-f857-1aa1-f0bd-d193340d3fdf,3896238606,8339738179085740709 +712b44e6-f694-0ac4-3520-8410bd187d12,1818410467,803614189749595638 +fa573cab-bb2c-d1f1-8b88-c6773b1762c0,3935444051,14814155843019591012 +2050537e-6a69-c5a9-f9a9-c0986f428225,2940868964,1176844902698712177 +54fc89e2-d712-a8f2-f260-b11a1021efba,2599191735,6963767442473008555 +1349091d-58da-df70-566e-02f92815ab12,2430396028,7392528513012229155 +e2be5f5c-ec76-fbfe-89c2-1d9f174e7c6f,1261471187,1889557179161374061 +ee48d9c4-2126-b7ee-d47f-fbeb420202af,1747131266,3858740201675478831 +b30c79d5-ec76-99d8-4273-a0876d888d2b,1648368477,10546673773074518432 +3525a42e-b8cb-3e31-a90a-13e3ef99c6df,607912098,5751961549904025030 +e5cd751a-61bc-2633-8d3d-19f8f8843d90,3055996986,18088094511271271134 +a731ee91-4753-55d3-e679-a74bf8f5be0d,4234679234,18256956381164950420 +861dcdc0-aa6c-312f-9016-d2da9346c71c,1673859477,14581036556910729910 +31feefc7-8839-b382-fd8e-27c7dddd5ed1,2512192148,6624427782433373821 +ec06a191-9ff8-a78b-a37d-f2f5c7bc5570,2844515131,908579988679020025 +9629416a-8d0f-70b5-e740-84ef5c275500,2911800303,10594188352627990244 +f48277ba-0cc7-e6f0-280f-790455ed865f,2707138650,17542718000310208213 +5fa8eb01-6e85-39d6-aa7b-a52f3aac99ee,1455578134,11979937446588506674 +9a1f78b6-21e6-558e-40b3-9d248ed52a54,848520761,6682722605006011897 +492ffdab-7463-1175-b4de-d7ded07a0262,1993431713,2442445952397158907 +fdf5ff88-97d8-7ab7-5c9a-440b8c13aad9,668653841,15675110374105250708 +c8d4531e-35fe-5070-7c34-0fe30bdfbdf2,4044949060,6374918795176415807 +88e5bd05-791b-692a-cd5f-4c0883cc0dc2,3200543995,10297019605692592076 +e6b7a2c7-c73b-e4aa-592f-0ef50e3e74c0,2733456231,8664027346371238061 +b495d0f5-baba-ae7e-76a6-318f4dce2c5f,1828284830,5986237375001074658 +68270dec-791c-94be-47d2-5554463c1d4f,2592177203,11896796605724572212 +046ffab2-562b-cfa7-5ee2-286f965d0e9d,2185059495,11237329329268779502 +d8da246f-67a0-0b2a-cb5c-31d3ad3abb54,1779445019,10572503008899766179 +896b6bc3-1c19-7a72-9a33-d6f646ae114c,3888114468,4882916018140618159 +52c1a564-c2a3-5d88-7761-52036c79fb85,3601107566,2247010999899724615 +67ff22bd-d5f4-2b0a-9de1-077886db74d1,3028484770,18299381621503318107 +243e93ef-d75d-f362-322e-6da0e6cc60f6,1866590893,12477036188404546395 +6f24555d-28f3-b22f-ac39-2def69e42b4b,511205768,13066417791280739183 +402c8b64-3f1f-b6d4-38d9-ddb5a12df9a7,344724378,16180044176536023633 +94dadbe8-b70b-0069-a369-58a0e66643a4,3186327292,7816167938986324110 +978726e7-ebd0-7b7d-ec32-96203bc4d3cb,2410012367,12352785926851522770 +19198553-f1aa-c330-7ae5-07c66a021f96,2104651268,8340928624501682199 +042006ec-1476-a290-7199-44f4a173fb4c,2369793982,12090203013770897508 +9e65f768-0abb-13ef-3abe-49f381aee737,4056198506,3188168333279704651 +3ed969fd-1a8b-ac83-f084-c5e8d5a3a002,2420425580,6933962411201336440 +872b11a7-ffda-7239-e17a-5a37acd24468,2338035539,14921758159271347686 +67a2de1d-4ebd-0d41-56c2-81ff9425b955,963567692,245538864796539767 +3eb7a712-ff5f-2654-cda2-e71467973525,1638119557,5327341078007699283 +c66bfd29-28d3-3203-890f-c4a70d7732cd,639412994,7595299608516275958 +50b65390-1e05-7c3a-ec1e-96c627eb4c28,1984219281,6463999981861525271 +c651518c-58cb-423e-a9ef-2b4d9bbf3b45,1664838396,9447739746315777426 +0edb78c6-365e-7fe0-f681-152cd626a394,562736276,5722935279807058060 +56ddda6d-004c-e555-7bcb-8914d0675ef1,2760269139,2834150991393211998 +7b495ab5-f77c-1435-a993-6d59f91979ea,325314074,15125312603206352031 +1b01bb71-c58a-0b41-46ec-d7b29c5788fc,2734874007,14393740994658565010 +23954a29-5012-a60e-a920-bd38fdabd281,3783426659,13689877784873393366 +b437d387-9b1d-e3b7-6970-c999d717e4fa,785778349,7531209091012617798 +d84adbc3-f559-7830-2880-9615a3c48cb7,44188668,1873544488984285923 +89fb82d0-5d77-6408-8224-f8dbd9be2e74,4217062005,5570133100876061610 +059e0de5-38a4-368b-fb9d-6fbff00f4fd9,2488457902,11844180706058373822 +eaf314e1-b576-8f54-3532-142c4a651357,2354449105,5901831444145348039 +2b1491cf-7b5d-acc2-b5cb-45d1b3d6a5fc,2835539787,1112564792408668661 +7beed271-2522-0327-886a-3b5d9b75b726,3080901642,5450997876783857199 +791df19d-37fd-9cdc-271e-b03f3d6b2dad,708837686,12555698959260463380 +469c9557-5ee5-6bd2-3e0b-c8c17c2d95e8,2370962654,14144848118359582965 +3846f2a6-47a5-92c8-8dd6-c5dddc056cda,3196552506,13421825458793599850 +dacff4db-fbdd-40e0-38d1-218414bccf4a,3009679324,15950436925784569847 +ad31215e-866e-c6ea-d099-58e2e8a80b42,2595120539,11193853920298287234 +0838452c-c26f-1ff3-be89-44418d1a12d4,3274746822,11968448306274305326 +2f922204-64aa-3158-f539-8ae506583b23,3590982143,15703812649269597619 +0728192f-b8f0-9305-805a-1164af085187,2865226604,5090244064483432193 +f5077422-a19e-5bf5-8f2e-60c45cda2b42,3981562367,11367929635543808140 +22df644c-e357-86a9-27a1-e0d006ae720c,2034405870,1118392691865850151 +c662bb07-56bc-188f-7bf9-48fcef210bd3,1286552253,17532911103035790699 +89f0164e-131e-d919-d385-fcc97a374c75,3026618179,1028996983559618330 +aca5dacf-2fc9-a7a6-8959-5c7a6d2535b6,943297245,13233606692218257245 +3afe0b22-3032-1292-74b2-1a5a7c83618d,3877657602,12741578380958842660 +69a51c17-f665-ca9a-b453-1c70fba08404,3708964575,7296590910960879557 +70169cd6-0870-488e-1379-cb1604575f3d,2048361631,2141840030117331323 +15e885d5-44d1-dd51-3710-fe2e3e1762de,2268595452,998999503232598874 +b7687904-3600-dff0-6863-99de76bf0722,3970859820,9355543869936429393 +272c1cae-609f-7226-e984-32559cc84f82,1332202658,9137607005305900541 +c2144cf2-4c4e-b0e9-2c24-a26748fe4607,225858895,7411324465640311769 +9fe36b8c-2395-ebba-4a13-7dd2fa1d3ee9,2372775323,1219528717428170843 +cec44c53-ce56-a8b1-99f6-52aa396cd1cd,2007760665,5436932538414968290 +d3e63de8-f4f4-2a66-2e07-261735d7732a,596907497,9028214352818499804 +99daffe2-fc71-4779-3592-631985716276,2773562827,17474594785290110327 +65d6ff88-21e9-0915-d109-eab1254c96f5,1937387119,12850550152520469951 +d8c7bdaf-94d7-81b9-66c9-901b83dca456,1595909048,4828038542927300988 +16eae262-fa0a-b426-8b61-e3c635d7e43d,4068576885,13910182670859835166 +e5cde3fd-ccc0-6c5c-b099-567853872a4b,3784622729,9838343960771448545 +499eda84-0654-3511-d0ea-2bd29edd7317,2803454194,5278618698380195355 +a48a7a3a-e7cc-6659-7764-feb6f65f8ec1,1036541765,14599001013931787839 +c83050fa-9281-07a2-d342-0d599c69323f,3234446633,2311735997945572154 +aa895d23-bc35-2d9a-0a93-3cdec560ecbe,139462940,10974240409828414814 +877bfd7c-7808-8639-5dce-b3876767f49b,472705024,8784656759451016662 +98bb787e-dfae-8bd4-e328-0b410c9c95eb,2499741446,1789074688996809461 +823120a6-cbfc-d655-c04b-dd87d9f9327f,368766783,3379065973632660265 +1530bde8-8ae0-ce89-af6a-6bb5d3fbd0e5,57906882,5166314301711548041 +e390dc10-7aa9-4c45-81fd-bb061916d3c8,1483152325,14674455592417475738 +07e979f4-8c0d-f408-584d-6f5897b7d414,384556035,14205588566391625000 +8bf2cba9-00c8-4417-7cf4-b0bb26dc06c7,1371131666,9733343334089071300 +189da913-8ce6-c69c-a67f-8f1a2c167f4a,3988699147,7655761845119284434 +fe2cac9b-44c4-e50e-e01e-663a6612f3ba,2363187508,17354835647820146560 +f59a84a5-b08e-1bf8-eb23-bf9bbeaa74dd,3913788801,11304430161248979100 +338ea7d5-1a81-f60f-6a02-a2830b7ce673,2353778057,8035807210971142648 +a5f51c23-2839-340e-ea6a-3c07178c7e11,490248589,8168404192717302003 +2b473c86-1a7a-5c10-47fe-47e946b0c5dd,1877888325,125086302844152523 +8ac68063-18d9-19a2-d44c-9e7723239f14,3464412890,7102297556900562537 +a2509170-2c15-4158-10f7-f65f3de4d7a6,2879355700,7789967431530740729 +513d7106-bcf5-da16-8cad-09dcc1736b67,2706001507,5846018646123190123 +3a24926c-3885-9ec7-7008-e8eabe0c24ff,2974906591,1133808368450084018 +40ecd2fe-aa80-7d10-281e-9ceac171e192,1509808547,9489714968704550848 +8c4f5bcf-14ca-5aaa-28f1-5ddadbb3187a,2012018526,14042176672422353505 +c69ae0d1-4d5b-5e36-3c72-1cedc2ebd663,1406532869,4806293843542195765 +67cb1708-ff0d-fed6-3b2b-d46c4fc0e368,2701072175,1281571611357524037 +d9c32932-67ac-a4fd-13a5-52bd61fbd44b,1263969368,14688187230107211748 +f80e0809-e1f4-b587-89a6-e8c7c4d05c94,1031241403,14607601170801820756 +ec71b9e2-3b13-434e-3977-d00d7ee7c36c,3958217675,1562671108733866503 +48f6c036-6e61-c587-d312-8ee80faf5644,659451036,17042619831726708533 +0fe13e01-2be0-0df1-1b6b-fc00f7ab4b30,4036162720,7087536895081260271 +582a6ee8-9170-b87c-ffe8-61cac8e38c23,1498815906,3183602628707126077 +c672d52c-9cd1-f8dd-5f09-a6d0d9d4d783,2382749317,738121017471062948 +9a40144f-bf82-6496-7ffd-9ab8ac84d29f,4134684912,17551361378728514574 +e31b5da1-b754-bb1b-a3f8-30b0d3e389f8,2403809966,9199431109742662648 +328dc211-3cd4-cb4f-9f8d-6587140c368c,2125667738,510800279725089289 +16b953a6-5f65-e7de-ab89-a6b692be7043,2992865769,11600931738670332784 +c0c302f8-2769-1c61-2136-7ff5f3baf352,2182355595,10088954907517814709 +1de48e90-6f4c-3266-05c2-d258d6ce7f67,1783850074,6894737714472756455 +ca51db57-168b-3a29-a0dc-03c0f57e50cf,2372266818,3777138240753943117 +b545e3c1-9508-c62e-6bc6-33b58821fcdd,4031632799,271216681662922614 +afb2efbe-a981-e923-8c78-7adfeade0222,1212739797,7095977872335051915 +c6a9bf09-6ac0-5186-3d23-d053b6341edc,1060609680,13407122405008924678 +96aa4a01-5ea0-39eb-11df-da556eb84c45,4017544507,10232643416351431622 +7793b33e-ce94-99b6-ced3-2b4cb76afe2c,1734705562,8763612787063478375 +e1f31523-40e3-ba68-72d0-ce530fffab76,1841717565,283220918549801196 +e7888461-6070-8002-d7a7-5a28e0375281,712687107,987063885184631950 +2409ffe1-0951-942e-41a8-a55ab063dca6,2821729711,6507491171218331550 +03ec21fb-bf8b-c861-423a-c41f851fbf42,1502320134,880612972263667269 +2bb25369-5e09-5304-3316-80cfc72f29ce,3867838339,3459789581236070252 +c1bab8ab-ad29-4c7d-2e00-2c4febe718c1,3524542960,9518458900357321134 +cd1accf9-6fd6-b334-f050-c966c5178013,2347415932,15174049795442722223 +535a0125-c58f-a11c-e16b-4c22e09fd38b,2567937386,10740164069226888116 +87533fed-08a4-9218-be23-9fc9349ad8b5,3678415456,18357691789256462979 +4dcec3d7-99c0-1a58-4640-764f993332a6,672856289,14678770427466124313 +8bad1025-5007-74ef-6c56-ae793692384e,1807854280,10032567152220196290 +205bd190-de16-4045-350e-262138b78b2a,1233819886,2710086290606280892 +8a96d471-6412-be88-eaeb-4b3a3740b67a,1616069586,14124893585026140023 +c34cd9a4-de74-a19e-a2d2-4707f2ae8503,2476095,5128182446259256466 +01273d4d-69a5-5447-3c58-aaa06913b690,3825591503,9093771459160945247 +a1e04289-9bf7-a9e3-3786-0203c3b20d0c,747517866,8713012190946676498 +68372250-acca-1f70-a49a-bf10f7878e0d,1815817712,3198241334754227451 +79f1661b-83b5-42e7-185e-230ddcfb5189,3324071124,16378309544766916963 +bcae92b5-a4f8-6577-e696-4f735f7831a6,1789589989,5412057645134858436 +6f10a9eb-596f-b3b3-8f1f-1437e5ccae6c,1641783343,2246622386190283536 +f20bcc68-39d7-ab1d-951c-6c816e6e1cb1,3480668053,903688538681052517 +d5ccd30d-0e0f-7cc3-80a7-200e67e222df,2332501522,4504267049628789073 +86a4a8d0-f21d-2700-ec35-b7cbd51738be,2013723167,10701465968992908770 +e6610040-2bd6-1e64-bc39-9327254afb23,3724363195,8517993670554010700 +a6e4fbca-a652-2573-21ad-871508bd3442,1101639551,9659878538400252972 +fde59457-618d-5ed7-e285-580af4061bc8,1151106717,6859632054529215006 +a90f3dea-6f1f-8031-ad26-400990916a52,1177834313,1443188066670131313 +f7e29f5e-338f-f633-35ca-459672f54aa0,3437218925,14815551811781357927 +8de7f42f-1c3b-0558-fb96-bbddcba26578,239436745,7013717764501476773 +a876ba30-f242-f04b-fc24-00693e08cd6c,2804867111,585074174919017469 +68805f2e-3ee4-4251-da26-da4239f4a02e,3109824090,1811004731253332332 +5d10ab91-0862-9996-e63e-e778fbf720ed,559779698,9630401812017776232 +26203d18-0d11-9f96-6c09-49e01ea63d3f,2977249963,3924055361517226366 +a332901c-ea92-e217-978d-24251285e03b,1463636013,15108547854071363485 +326bdc28-484a-77d3-673a-541ab88bc254,121500798,12908080837165648812 +7c9339ee-facb-e593-69a0-d141442f021f,4070513826,9344515569141106406 +1acce19f-1f46-9d6f-e70f-ad7f6f6f6110,816718168,1476891212176477910 +2a4a809c-6b50-d0f7-83a9-bce6535dc72e,345912215,11055591292292852375 +b5330077-942f-745d-c93e-dc0415156271,2800925645,8990290862195820833 +c67361ee-93d0-c7fe-a4f8-b0f548a833af,2778443213,9611432400467500166 +7760434a-60d1-8fef-478d-2f182b17da9b,1996184333,3999298910908421686 +9974fa67-d10e-9d5b-dad9-12944d42ec32,1100328377,15629102401727342469 +4f4c0d27-fcae-1813-daaa-b2d859c1534e,184032462,11754828737162671632 +d040ca39-582d-df51-0b07-28d0d345876b,4083321565,1621681529250476715 +dc304b1d-1332-364d-c3a7-a8eba6a02493,3529814777,14675781054000567340 +ad6525db-05ac-53f3-daa1-20b57514fbcc,3600693764,7254467605351784580 +3948f270-e1ed-57a1-643b-de07653940d2,2184778026,9568141779493661075 +aa91597a-6692-9ed5-4674-eb3ee10cc567,2408904008,6085805608242797779 +ab967d9f-a9c9-ab1f-05db-827d841da954,3410338903,18376045682364185653 +3bd378a9-47ba-be67-7042-1ff0030bb139,3563225527,2737082107226411346 +a8b004d6-5079-2c6e-a784-6d4862d68c30,4202381895,4967130472815185441 +9323e61f-c9f9-d4d4-5b1f-c9cd1c14183f,1789954102,11941212457352733362 +96163452-21f0-c99b-7090-e7af66a8608b,2638538171,2079245087267297750 +13c07cac-ed97-1482-72ae-ae013110011b,3651292962,191746288655408434 +61f16794-99c6-52fe-5175-ef400ef91cad,3229254549,15624452282268238893 +dc3c00cf-f285-9e1a-5c04-7ed1f084c6f2,3495748523,13045508489137250334 +fc712f6d-f634-b7cb-87d4-f8033fabd154,2294742349,14199616138738101597 +3dcc2011-e647-cbeb-c3b6-8f18d502c77d,610102167,102286605706752909 +47cbdbf6-7005-e0d4-a10e-24a63a870111,429369734,6199081335350860865 +e0bfa4ca-a1e7-f600-e707-107d0c5dee19,3394367863,15922688428372619550 +4ef81649-7f4c-db76-f8d8-94ad2c199ad6,1975909819,8889696241103927310 +0e8f9db4-9103-224a-99a3-7e869dc336e1,4241748296,2927982722217535079 +317f7dcf-c341-35fc-ddc6-e28f51b79fc6,1941986691,8424597246279970932 +6cea1018-782b-b837-a9fc-e646e1db79c7,1313462453,10533052041955470415 +77758658-f33e-e5d8-2e27-aa89093011c1,2631601255,15396996747032662030 +41102a8d-2587-2860-0a30-d641ee313bb5,1979468604,1858536480693315916 +2bfb0030-0ccb-3924-8edf-ab6738447ab2,674792343,13695694107950652136 +38433899-669e-3aa8-394a-56554ea56d64,2010174940,15953095760385620400 +9d83f82c-d2c2-3670-e236-928233ffd211,1505819743,7188305886236993694 +027bcebe-820c-c902-e678-ea1963c9dc99,1639745915,12245312066092754991 +5e9c7703-8598-3970-2a4a-5cfa68f87507,164222181,15338373270546847247 +31ffa66f-1229-ce82-3e62-7ff757caaf6e,1706511505,11932590951283015475 +b61e552d-1041-5337-6cae-bab82e1c79db,4053325525,391209514494126893 +2c746786-79eb-471f-80d5-b5efb30f1c40,1589698827,8669068451633552073 +9a540acd-7275-8700-cbf9-b4da7c3d8e1a,1432714354,8637631158717452480 +9f493f91-a9c0-d10a-8705-e0aa6239642c,1637081385,18161278560579187629 +e3eeecc5-612e-ff76-81b4-9d2cd0a4a816,726791323,167314368704772759 +a21a12d6-a172-a5df-50a9-449d57a3aaab,861578568,15904794733315260558 +d00c476a-6bb4-8742-3854-0a11496b244f,2047991209,4213378472086993434 +353c760d-f38e-d926-50cd-7fc49bf5a13e,3800629476,2027395599473290535 +810f272f-b556-374f-3d9f-ad1d9cdc2468,4153707520,10858234254972988066 +b6be6725-9945-d6db-5aed-5bfc7ab32982,728562987,16940899350181681489 +27ff9a7c-56af-c579-fe24-d8d3d5f85ea2,2551815567,3828126065098592952 +66d78709-f051-e209-94e4-b1d852693bfe,2117994024,9190717871468608162 +8dee1966-c87d-b9c3-6e82-a4a36e354db2,4017975457,4161278758034283487 +f8282adf-6ca1-967e-fdf5-630a8d43d8cc,2866994958,16490594203740167341 +5b07358d-57c1-73cf-5946-23afdfe341b7,2797845651,16958461687724764471 +87c349cf-f753-995d-81f3-ceb4c0c3b0d1,1152936683,10634033916366888701 +591ddd48-85c5-75b9-dada-30a91bf4d785,1832680158,7491448954472642076 +049bf6f6-1886-838d-3a42-8a43eba840f7,3241546248,8156292523106709804 +46da7962-0149-48ba-ef68-c3f806fa7cd0,2265088136,17504442769460568100 +d0eb3f46-a923-ea1b-1a8e-3ad89fea060e,3634194291,5887462033707187992 +035f3db7-09d5-8376-558c-9fa9c3860da6,1309121506,18332662226647668984 +dad0ad59-e87b-342c-f565-f472f3284971,3667513894,18091943567921298447 +c07b5d86-04b9-cf46-82c2-42d2b63f7ccd,745602470,5271961330513500939 +80687cda-8d7b-f0da-0cf0-32983fa476eb,2741440039,2288985938711113615 +30cd9218-da9e-447d-eddf-8372972a5e52,426017893,5991546175837860595 +eb2ff29e-f618-6d7b-29a9-5443c2dd8bb6,4252220981,1674196773723504737 +e094f011-c785-c11e-75e4-4a552f75dd49,3928675393,5174922423484694593 +e96f4c6a-5cb4-9560-2d6c-6878544f2952,1030072816,8665508803856090962 +ca3d1862-b572-3e54-fe7e-e4bb8995283e,945160855,5738295777492388532 +4532e953-06ee-8214-7eda-ce15aaedd344,3195447504,10694322029879734830 +46798107-a783-fd43-0baa-e4e96a597d71,1354914631,13134142372269353488 +e2463f84-af62-c3e2-117e-b0cfa4f22c13,3496852676,16241001377040132408 +34ce2bd7-e06b-48cb-7856-1a1ae67c385e,1607028000,12115872335023244961 +315ffa50-7bed-c093-32aa-192d638d67bc,943572157,14664547449968567422 +7a92927b-3a65-578c-8720-a5ab87625f44,2174809427,14832831655045705545 +48dc3cd4-1fcd-5f26-be64-59d5f08c7c8e,3646665511,3227662569902877617 +2e61d176-7b46-79c4-f3cb-1edff7aacf38,3034081656,8059404715103614077 +a873c790-27b1-ac9b-df28-3a525eba9709,3084750695,11591042451437949762 +2330516c-73b0-9ba2-80d2-cfb0ac2d812c,3389249167,3311930384398892118 +e9338aed-b8a7-d5c1-522b-a64a3f4bf82a,1458487306,9517380045165266323 +7923a804-ba83-1866-0c3a-dc653f1a0be4,3823079160,15773779184087006601 +9e303e2a-a4a2-3c01-08bf-c20b47c33310,2501130214,7304494232404462912 +58052c2d-0965-db19-3f49-1b169d7693dd,186197528,3491374678534036963 +847743d2-e6c9-57e1-ea9b-c42fe0ccdb87,3115323478,13243945204399756448 +1afc47d3-0c9b-4044-5818-c3e3e5111942,2731472211,16813586291383055886 +d31786de-242c-7072-f4be-9668bfbd72e4,4058966515,6324558981310684490 +1f2d7bc8-c937-f96e-7a04-76dbd06eebe2,3640125627,10833911800301038422 +f17c96aa-5c50-88f2-26bb-e7ef4a383c5c,3642091256,1640954828398410765 +d9184d45-b328-9be0-d9d2-1d5d42fa0d82,4054230475,11508981894694668184 +073ff893-4076-5917-689e-0afd0f8344e7,1156132109,9908327093191579625 +df8d9cb4-46bf-929c-42b0-e034c15677d3,857862874,15257878738583854407 +6ce9ac5a-f230-ce9b-61b0-3698571e6e62,2943619474,5776907404128028349 +7aaa0c9a-0958-4b7e-f99f-3e43500dc6da,966947020,3667994070887567454 +5f8494ea-cf17-d5d1-2e9a-98d525377cb6,2604483877,10046264658251767314 +f2eeb212-d3c6-85e0-4808-d060c58c6c98,3819621000,3553844420616404992 +99e75c94-a5c5-3ec1-514f-44ea63d7fb48,234243939,5058363557320783477 +f3ee8896-8148-ddac-4707-c4aedea3bdc4,102789649,13190584822921028899 +5d149998-b644-c412-6af7-07904920b70e,633119015,12754305411004011403 +fa041c2e-1ad1-9c8d-4ed5-cca13b81e211,2243261839,2732580916512674039 +90042ff2-0149-9882-30df-053ebceeeaed,3351535623,1426065637989631475 +ac8e9097-3c04-64f9-c148-b9d8818de0a2,3546732785,7923115935502543240 +66abb7ae-beaa-4f80-b871-5bd7e81d72a1,1059792021,4738121773370525303 +84667f88-4bf6-d585-499e-721becd25c8d,338153336,12973401162617442537 +7d83d82c-4152-c184-a430-1d4b37ec48aa,2215743443,18351149253590019420 +ad585d50-4ef6-bbc2-fbbf-15a72bc9ceb0,3252953570,14109267931253726919 +82a82c1e-37c7-a847-024b-1164d7aae51d,874634234,2216438395015763876 +f725faa2-bb08-8ec6-3d8b-1c521acee0d1,482375779,9260922082162690743 +a9ac1ad5-a14b-7ce0-8d2f-dea96ad30f67,2573454737,39937758197033138 +5a32dc3d-f09f-d74d-2cf8-98be35e38af1,3510805045,7265079034611263058 +3ff491b0-8e1e-7031-131d-302e1b256226,3028574967,18193098385929066302 +22247847-8867-f77a-08e8-29a6bb660d0c,1648878578,15388347826243890265 +20a17b63-8a2d-ff8c-4bdf-8a8d5f9ecef7,3588177661,11729397672985073737 +ac9bc1f5-00fc-a6d8-4386-4337ee70ee42,2741771906,17978471185442476788 +f57c5120-4fd3-0484-fa3f-d187f63691c1,2856990323,17046310299746158694 +90c82b53-8a0b-7743-07f4-2843d708bed3,4040330342,4705693987567164229 +be78b246-2529-aa71-9be3-5371c4f221be,894801894,16084405961813516107 +c5e39cd9-20b3-388b-ad2b-de66e33e06e0,488571039,7976816193690985574 +8600f7d5-508a-ddbb-34e8-f851c8281081,2991423890,6545262245829078315 +1ee96a44-557b-4b12-657a-218cb61e72e3,2969039715,446398656582621225 +4cd8a2f2-93e2-8e98-1008-098c8ef74bf4,909073547,15939162904228034814 +91667143-3009-f7e3-95dd-881b6a83e595,2529084259,13507411750400169777 +4013af56-88e4-4874-c114-ebf07bde34c8,3029017967,13935678036391780214 +9a0c688f-701a-d88b-0c80-3db5e7810088,1443422304,512042085136669284 +2e28c623-e726-1204-a94e-b412ae019f94,1083577820,12411031181595494083 +02e6319d-fcbf-e491-8512-2cf7c7ea032c,2185569511,5082282061918679435 +d42a47d0-9002-ac37-403d-f91be4aaf4ab,3990177635,5560571335865716352 +e57d1783-0425-eec6-687f-87a3165a7f8e,1536087527,9760645819712344781 +fc535ddf-7bc7-3161-2a54-e4e8a85d7079,1814738789,7441504958123455429 +0849fbd7-414e-1783-b717-5718b42bd1d4,1585082263,4021671690709980622 +bb7a629d-86f1-d6d6-9b87-38b13a86d66a,2621974618,14467471835460841932 +f900806f-b2dd-fa8b-c91b-efaf71750175,2928411187,3459192266120152779 +4bb8c8fe-475c-bce3-45a7-90f7301e14fd,522454951,9060364519331320045 +4b8761b2-3c9a-12be-55bc-59d43f2829aa,1141438669,16849274735426347050 +ddcfe32f-5e5a-118d-1222-cb510ce937cc,3459314748,7544030831190176511 +8d7244df-0f31-a775-361a-d23c845f5d36,2110131903,6418383780396872144 +7acb743f-6225-ba16-2116-965ba14eb49b,419786319,11273762156824529873 +fd6223bb-fc2e-3df0-60fc-894ccb5c18ac,3538702230,5823477719062328524 +abcaddfa-f654-04be-9031-7fc3eadaf93a,3441031267,12220418953260977034 +2f413fd7-0e2a-aec9-a7a7-4a3b5d796ea3,3690357173,15930078324782960982 +a8ddbe7c-cf0c-d094-12dc-84cc5d3a0ba2,526807022,8078889257507631930 +6ae31758-240b-0112-e78e-79f3ef12dfc7,903750638,18329912958884641862 +ed76f7d3-5f3d-4865-5a1b-8cff0038101e,1857237767,14644869485483882072 +adaa80e4-a176-fcbe-61f7-2ea02698ac42,4164669420,17720859667854736905 +1a9eb798-ba47-675a-9920-ad5b4a64aa19,2137813560,11334914915176973038 +4700531e-378f-be8f-ca12-1d1111bf5898,2877044639,16936203728430129684 +378f2fd0-9132-8867-f3bb-e87d998ba07d,3338558357,12723573779671256466 +5a029abd-a875-eae4-660e-c15d01f00527,4243556755,12106854212380288860 +9d07ef17-9b0c-9900-b6be-004e3c799f11,2735744658,5980113119917882609 +4ce4021c-b530-6c23-6800-3a452c7c82ed,2908002645,14438516880162100789 +2c440a8b-e3dc-16de-17ac-c3eabf8c8a3f,4112035282,13353959752400837407 +088b8d31-5381-4fb2-9ebc-e4d3b57d9647,2316163321,2498968420222747750 +b1ea29cb-bb35-7aad-93f2-42ad89f43b08,769626582,4157362151718669208 +9d6e6838-546c-8443-b419-1abe1ebd46d2,297125308,5144124516056810922 +e59def4e-500f-a729-f80f-2af07cc82648,3565529409,13521597315154119746 +da0769d2-9d88-2265-c194-17ec0e74f5dd,1347719759,14107927385007851500 +8ebed742-2fdd-79f1-6240-700fe9cc0f61,3830828535,1047528256489207810 +c2d5acba-0e21-1772-93bd-651749749a72,3455526447,9493208964467339248 +1b5dbb98-a2eb-34cd-48f5-476083b4924b,198889348,15014520338180909520 +9869b30b-6aed-176c-2a71-09a7f0eee33c,2460063544,15072717128893914699 +b396263d-c643-0fd4-dc88-99bd9e742bcf,3799909996,8239428404143648294 +e5a05d78-15b7-49cb-5e2e-c5425bd66d2d,2118811966,11041452587615822381 +4fef71dd-7ac8-96c1-a5cf-54f5595a7fdb,3198087234,7479177758576855698 +cbf0b938-dfa5-04d2-01fb-d04e62fb5ab2,518000846,12772222740333229394 +24241d37-a6d0-59a1-c6e9-40bafe3006a6,3622696131,4568515114734817725 +cd6af56c-c009-9c79-5383-77aca4ceac23,3883350665,4952773441516503606 +0a0a0be7-b986-1109-d92f-00dce9ad7904,2505988828,8210578897337130196 +2539f914-90c9-7874-1e44-45427fc3a4a0,3193881748,1155263964306982611 +bc79de3e-32c7-5404-b866-d6792fd107b7,3244593349,908283315388877223 +4895a838-fc4a-e00f-ee3d-21e3f97c277c,3554157202,3308806492689142903 +510514fb-cf5c-0c83-5eb7-4af4cc6514ae,3104973362,9802993814735329713 +d5563076-618f-24aa-674c-45e31902855f,673520358,18433793961053346375 +4fdbff13-4104-73d5-9d44-1bdf7493024e,530598035,21387802677930696 +f4b48379-1c67-45f6-4a8f-0f2e8b86a055,2510028886,16711885211871200999 +e9f238cf-8929-681e-7dc6-dd7e8fcc6cd0,107853103,16134590438557410910 +80f9c125-3655-3da2-6dca-bb0b1809c8e9,3858472296,13394131438647782093 +98556ca5-90ed-306b-728d-4c2aa0fb8ccc,3039786003,5236729746247292677 +169ba1c6-102a-e8f9-8e16-402d67c6d5f4,139930734,11046371683299654073 +7742d3fa-4465-5e3b-81d5-3a1287003af8,2441055588,5486105977709569718 +75c4e28f-67a0-1960-11fe-a36f82edeb61,2080023173,15970903315275085381 +85b76078-b9b7-56df-be17-5bf3c2483f93,1298227443,234451133159100806 +6eefebf7-fac7-1bc5-6e3e-efe3b4df6cfc,3776813560,3995767448481407376 +1222daa8-946f-15fe-940c-50c0b4000833,2213532837,17327770829681877471 +72150aa8-11ab-f02d-b50d-6a07d0591a98,3333569536,17107928505567410728 +aac81d2e-d5cf-867c-2cdb-6be4ad105558,2427359352,3037554613835830157 +bba68468-7a7c-4a0a-915b-ca8a26f44b12,288681534,13624613424594575812 +a2b8584e-37e3-f07b-b4f8-4d3ca4afbb8b,1058903756,5636309419640704346 +40550ea4-be2f-9d45-27c6-2d40fc4b4bf6,1275543864,17894789989893803198 +fb8941d3-ff11-de17-f7b0-51f61b6ffb27,4289205741,303433103333306926 +2ab57999-2564-1068-4e60-89d40c4e33b7,2367742849,3465698146452937125 +84deeee9-f303-44b7-acb7-39887efeb8b8,499424998,4845992892035290272 +d201f820-573d-cfdc-a51a-634e69fb0c7c,219078060,9409054943079415609 +3ec7f95b-4241-c7bd-4041-6eb503e8ebaa,2514916997,11290755307238904927 +f4a4d27b-9540-74f1-15cc-9d2c852de732,1820408119,681571863246306172 +22130c69-5599-f214-fe4b-1512ed4399b1,3610651415,5815407531036841595 +d486102b-82f0-f79a-b533-848f37eff1c2,117818922,3188435737840861370 +843c2f0b-31eb-96ee-022b-bf35d4ecd5a2,3930204493,14026250407851538696 +2e9598ea-de8b-0eeb-0d11-28ab463177c7,2028374269,7683569289419080478 +5a424c14-1b15-cded-8a11-9c28941bd4c1,3189338734,10770847471910265165 +e166b7d0-6a90-881c-bea3-42e674b9cc07,4242395985,14552717151963233600 +74052c3a-30e0-4d31-f0fe-a58b18f284eb,145044497,2526117631988852856 +7a8fd8ff-9f28-a405-e790-be52f05a6df8,3789814531,6990416034778148500 +acf8527e-d901-0522-155b-c1381f8d6ee2,1318911560,3073944139248608770 +5d3198ee-5f7b-b686-9042-f2fee7162751,2498781096,17479933008479630833 +7368a4d7-56e7-34ed-954b-bbd500bfb1ba,2936080705,344119205245701989 +92764a27-9ff8-4442-c110-cd2336684fdd,2251818274,11909671592087373793 +5682574e-d099-8c41-82bd-d48c26cf0ca5,4018610727,9626968597725792053 +dbb344d7-4bd9-74d1-67c8-0159f5a81e1c,4128664348,11300029531865061146 +59f3671c-4b50-5535-e513-dfe295865946,1862673606,2374469619498832523 +6cdaf6b8-22eb-4b2d-b59a-f076093085b2,3990863840,11352032481594212382 +bd4b5fc4-9bc7-ec9c-ab6a-fe2f0c954eba,2639987653,5002543004105028834 +78a375de-1bc2-9490-9ee2-e2a4e5a24a5f,854017809,5499142295064426828 +9616d99d-b3a3-da57-6542-b9388d5d07f1,2208902540,12563885229658759334 +e0f9e97b-bd3a-5393-7167-a1e6121716f2,2123459134,358345155603599420 +d340a49a-1c2d-ee7a-9b2a-47af4987f923,3070184284,9446036238264625621 +1f389220-942c-4416-c354-d79705215020,2052205863,16986724747617023162 +64f9dbce-24f9-82cb-63a3-247ffeb0adeb,127276153,6582737940915960066 +3a65d35a-e555-2c68-3202-63e5bbdfae50,2543400459,6137463970903617253 +408b0db6-6028-d639-d376-069e83435e8a,875407544,6055316960309822783 +e261d7e2-1ddc-6ad7-45f5-373a6cdc7862,2656098772,10710323422540228475 +5c8e178c-78fd-729b-e5df-cc6a96cc6640,3395755924,14024754881813984895 +979c9ee6-065c-d161-78ce-2a00f722065b,361095099,11896564567938857395 +d009be10-bb9d-400e-d1c7-0e8a52d24bb6,2496306940,14514469560775265962 +9fd7ee71-1572-96e9-31e0-a204e5f8382a,1956705545,1666042571082228628 +55c8840e-2fc1-f438-0af2-0cdda7f8a45f,1954030724,6803960933214968901 +cd53db71-0acc-8511-3d96-62266c0973f7,918993956,16063373864347369198 +0e8e618b-9a45-9922-f260-b1144635703e,2511858089,7065946929815838559 +3fc2f96b-127b-f17f-b038-d3f69bdf17bc,20633132,8104260017241404245 +a875dd3a-b28f-6928-3da9-081bafcc43ab,1969506729,14484120469376990768 +3403a150-8fec-cb4d-7a76-b1a66459927d,3274293961,17031721526814705961 +8e3ed625-0542-abd9-c3a2-d79d45ab70a0,2142926546,7786647255385094195 +c926ad08-50ce-9d16-7026-ea12cfeacd58,1953941529,14526511730526957689 +198e3351-898b-474d-4753-4444b1a6cd2a,3884253569,5968514943741716519 +153c256c-e458-49c1-9813-c26847a9781e,742561056,3292600689470428413 +c70f0bd8-0701-8489-5c80-f800b261817a,2156067627,935027214059213517 +6cbc24cf-0c25-6ac4-f00f-5f9909bd4dd8,371898940,2941855140685497360 +610ee1f9-8e97-51b5-4c57-deb1c2d54303,1205660062,6316685594480117336 +b32b74aa-b94e-68f4-e407-4dfaad66f25d,2138388736,3077255592070651598 +99b83b0e-424b-659a-a1f3-7d326815f1ba,2125052419,12121938246977252392 +76f629e0-bb3a-a952-2202-84277f97193e,846845437,7434021503853883413 +b01009b7-5634-26c7-a5b6-e05560f6e432,721696924,6087168073922287198 +78199410-eeb6-3680-06c2-61c119f9de2a,152561711,2397726949436112536 +77e78be2-bec1-d221-0910-d29c69c408e1,607172145,8414814297437677591 +c19d2442-e487-c923-81d3-8a5a8a76ddbc,3922920043,3969323578516614376 +100984f9-de52-691d-102e-d421d26bfaff,3554287677,5375336978089926314 +70e4f167-b779-2fb0-dae9-67f34c9473de,3634945484,12456536686629701895 +70d11663-79d4-882f-65ca-1f595ca5540c,2625392271,6898357219409760689 +4c39d34c-85bb-5b41-cb2e-fb705d3938b0,85175301,17473261425103451086 +da54971b-597a-5c7b-7a31-28509cd4f3c2,2277415373,1354350939816016349 +f9a35cce-9f1c-5b51-3651-47350a863d31,235518357,8030623915332045156 +332e8c82-dc9b-9d2c-475b-ca5ffe667166,1391697984,2846595066802484655 +e23ee6ff-2129-4fa9-9adc-4ea7a0a114fb,2906086727,16721043232763151134 +b2286c1d-3d04-657d-af43-b9f050527737,407927006,18431764948244661971 +5f1d2c06-6e96-5ece-5892-7b6385f40d0d,1594123456,810076932751875424 +c3b133b7-e75c-ac73-c0bf-d8350966cfc8,2149248796,10383685744523837438 +c782b4ae-e289-29cd-2201-485da22242ed,1255007533,2607237239295101810 +1f028bf1-d250-99b0-1bea-a2ac631f2e76,2677907854,2577902301997810873 +c9810e37-be8e-b540-2574-538609751058,1075022747,17081097000595052435 +a0650c5d-5f58-08b6-5673-714b5ab6d959,1485538318,17080463047996245455 +86b87856-7033-f43d-4082-7bd41d54b7d0,480225699,3198316805981740529 +b33cc40f-1e41-e273-fec8-a6b5cd657d39,2728364299,2076765569813523555 +7b8299e3-2737-c648-c5e3-24674d769377,3052935748,15352263352767151296 +3e3ee11f-7ea5-874e-ce8b-6af8e2e41040,3317099483,11534575892929271079 +dea4a416-9317-a9df-aadf-61e8c1df6e06,615556751,17631403722389407512 +e3309cba-bbcf-1834-3343-62ca826ffa6d,1637167050,89810687630887054 +44442ba7-8d87-986a-9b28-f51201d8eae3,2793040261,15760959310378697058 +9d5c0865-8884-364e-2770-05a3087cd877,744935471,14587311426865396442 +bd94b8f3-d89b-358f-33a4-2b91a808bfb4,3744728792,4442456691509591019 +ae0be405-5297-0588-2e5a-72f4cc29327d,846062753,5682163680008444609 +13ee1293-493b-22b6-b9e5-8ce81630cdae,2881206001,13307194201458749889 +1c5b6bfe-40e4-ca9b-0dda-0d2ab6a3a769,1284461056,5236415035641516790 +95a8c2a2-ed0c-616a-9b26-32cf1c1f31b6,751000805,3873730167828462438 +48c42df6-880d-4a03-d96a-a6d6d5fb995d,57443836,3375430914690652430 +22e8be9f-e283-809b-4b30-b8d402672ac3,2862569593,2577115416274673794 +8e154f9d-52bf-2beb-b1f6-03792e64dff6,2828887852,1629233493503305690 +644f5023-5776-46f8-ab45-6b3bb2ee6fd1,1739857427,7239685353896250207 +718a128f-7b94-2e29-59e5-fd8b08641c73,4237365385,10093901311838697755 +42f0a8df-e806-4d19-bbb0-1f7a1604f352,1454087315,2218939624279547933 +b95ff48b-3d32-19e2-a556-a0060f800d51,2481846304,4306793027933993602 +a494a151-17ac-1c57-674f-7b63575c1230,3344649039,18367151783247201664 +7ce36890-605d-7039-a1e9-0a7064964c14,1704840962,1457501425644146766 +732ce19e-568d-5048-08f2-d7d9643f2100,458311203,541598719633967463 +ac2b25a0-e1b9-cde3-e6d8-ab2e72e58b8a,2397413428,11319567270593727749 +a9e379d8-b4a8-4933-8803-2efcc5216ac9,2400250343,8812983474255233683 +9b7609b2-807f-0aab-c15c-4f0b06702e27,3197322901,14229207016070312598 +5981d997-f3d8-a8e3-1f45-3408a37aa855,2437409088,243431959045180943 +e941887e-2e72-e274-d7ac-010ed2f58015,33480568,7474287257661451397 +498025ec-4e8f-7649-ee56-fd36480bd65c,2917541670,9501975062663146784 +d4f3616e-a84d-9412-7066-de72e068627d,3692782149,6693556505278604554 +4719ef6f-07f3-9b3c-9e8d-5c1a94e9891d,4200787553,640022244285358406 +da73f399-de6c-907c-f804-115cb3cd6e5f,2743498782,7940053994042843227 +f319d2ba-babf-b42e-8b4d-4af7b29f7868,3146280668,12255744798505388259 +4f27d0bf-0a88-7863-147f-ab0ac39f9b88,737034585,13189303027062912440 +c302a806-cf95-f47d-5cb7-5c5c3607c61c,1038301301,16873631166616457569 +d087af24-a08e-ca5b-615c-b5538b35c837,1287723010,1060432583439441004 +b4a2ac9a-d19c-c6e2-9e9b-bedcd77f72dc,1462340718,10766215373139456347 +bc5b67fe-102c-8757-6149-c366056c79f4,2520790837,15446795227311893237 +a50400aa-c539-8cd7-f0e9-64b6fb1a9268,1017892196,7115923304307812263 +e281464e-1d20-98ec-9588-31cbcfd265b2,516419567,4343038109013567752 +42054a02-b710-c54d-5b9d-c8bc58ceea5b,219628579,2991217152784098587 +708b5150-dc20-e8fe-313b-11258b7e7cf7,261820619,17886077694279035891 +ea7a07c0-9378-f4e4-3f2f-777ba8f34831,3725432661,18013180689380792312 +143e9abf-f52d-904b-7672-f722d1b67c01,612850603,8051890373183911810 +cda4781e-dee2-0255-f8d3-2b12fbb9ef42,409939169,14541209367750528835 +6690b2b2-47e5-28cf-d5ea-4a58c9c61f46,423636313,11562227637473846178 +655b0e6b-eafc-2858-dc50-eaf51de25584,992616475,2813929612857605093 +db184f72-3756-010c-766a-644b95f0b577,11808306,5711489541450196541 +81e12403-8ac4-73ca-d4b9-67d5f0fd516f,4058142866,11697750481498665038 +821cfaa2-8275-3675-3f9f-36b6162dcc7c,2497388351,1767054518735301276 +36d00065-01ee-af44-d3bc-b5d7c43f95f3,2268477079,1415327675832720664 +b9722168-f1b7-2521-0d0b-75e6da83ac75,1274863344,15376310811639018761 +fb897530-7689-ba52-c412-3294aacb10ff,1990638910,2737886719755547662 +6929043d-2f2f-9b1b-90c7-1d2bb8bf5e80,1095525907,9654288801942198327 +3774966a-0248-3e61-c49f-0217b5ea0e52,3214268414,9468860962951657132 +c659ba6f-a277-bfe5-b513-46b71599139f,3749718526,18225343028310533364 +50bf9b5a-34f1-54d3-7616-d537d6d41760,2593370173,11043883325594572442 +80adc60e-90f5-2aa6-eb3f-72ee0befbb6c,4020178256,2204295526890577073 +48a6e18f-5911-93fb-a681-4bf10fa585e4,109256960,1937414264338298707 +e75338d2-da60-f3f6-4e43-3f07f5bbb45d,3894903238,15769645711424392866 +42b2d55e-eb14-cbc6-675a-f958fd23c5ff,3361848714,12284871821040607000 +8b007cc2-cad4-216a-c3ef-97cc9c90d59c,326200307,8189363693866407470 +bc37b932-2d66-14f8-107c-5a229251ff04,1802916953,1368641946312099016 +c2cdb86d-f00e-e937-d871-983e774acd01,2243108020,1661530804586901817 +b012f254-c9ec-2e48-4fa7-97c997584f6b,135397621,298884046591541945 +a821601f-9d94-7e85-0db6-135c784baaaf,3526530009,8892862290387689471 +d14a875b-dbab-6823-e331-d2066650a444,2164626002,17366567552366286578 +702a9dd4-c0ba-0626-d76a-ef7be94abbbc,3269141788,12176193240532751227 +03989fc7-4de6-e2c6-07f0-605419486db5,2068534867,7103146558188573369 +d4d44f2d-a266-4525-b350-c1bec30638c4,1975278013,6643241273551039101 +d5e88aad-c7d1-2890-09a2-d3e850308cdc,1243758307,12214936257507123668 +7e915440-fd2b-bd9f-c2d6-9b6cb8db91f4,469310299,9651941318128454915 +d7952ba4-848b-8f8b-7e70-dc3019698c8b,3625456867,4238844398288944063 +45152cc1-83e7-54aa-6d3f-8b9d6bdc4d7c,3147858145,12547235422574406567 +844e3ae6-4518-2cfd-4094-a64387f0e438,280524701,3320665434967616824 +97eacc26-3d3f-51b4-547f-832fb615efc5,1612337544,8564110622265771965 +11821482-3da3-1a8f-29a4-51939627cdd1,2720723857,8215103517402354155 +6517fc45-b05a-ed04-42cc-6bbc6a2d3294,2784365723,17547806906509726991 +47ee9105-6680-5296-2f35-329c0b144834,2537794090,12259551569189205948 +ef0198dd-2b04-872d-8a73-4722bce9deaa,3805406211,10983016382992841858 +6c9e9bfa-a41e-fa0f-6a7d-996251b74fb5,79000078,1895648981625910360 +751f99a8-57eb-ddc3-87ad-c0c365e456e5,3958579300,2064978441535415956 +763d2d02-973a-efd2-5431-aab84fb6dd8c,641698119,9892621655978496214 +2b41795a-3344-21ec-510a-8628fb770bd6,2498196526,7626405195696660901 +a3f8b186-9b67-ad17-666d-3f42072166df,3669012165,11430980860939020057 +0b18911a-7737-f45b-d9ad-f32a7f7d0778,3945965454,1459389038732961678 +05932ecf-e06c-6791-8e96-f3b5ee4114ee,1718752351,13788168436623875984 +4b8bfecb-4f9e-c94d-2d05-7b747f4d9c46,3596313483,6339000813724870053 +25705a54-a94e-d435-6126-43d4c3effee7,3623044679,17335698646969585823 +398dd7b6-9422-3bd3-8733-f407648a2eb2,1570266234,17656594544106283976 +15b1e8ff-7883-0c1e-d8de-309d964b903a,3098107651,12019525291625995305 +9968d5ae-1e8c-bde5-a70a-05c13298e767,1573379185,12710145086138359090 +a498cec5-42d0-4e08-c257-832c10af2323,4224160895,9667125666330743791 +c0792745-6e01-39b3-5d74-51e700bbe389,552010732,9064901638903474860 +1aefb99d-a45d-c622-eee8-6387e52a7993,3479786982,15422642143113582829 +66bae2fd-887f-2c51-218b-14b8fb6c4c2f,3303007687,15977385238182423090 +a69357d6-c15c-8eaf-69b6-de13a69fba48,1601199687,3503428098804883691 +102b1e67-8967-9f88-d29d-dc815598d230,3904980880,10647023567227767920 +8e2e0226-2c26-63e8-d030-3e3c04765ddd,2569094405,13118101082852135598 +03afabc7-c92d-d3e9-172f-b290d0006a29,2592556110,14520402423601332287 +5fb66e6e-2675-ffdc-4621-bc9addfad5b8,2505936137,3738277509241615171 +65d2a054-7a30-8ff1-466f-7838dc1bd368,2857534958,1515302834882531260 +d027d268-a95b-aed1-480b-b495246b3812,1590031008,15691614342571071367 +06f4c8cd-b4ca-76a3-4649-3c6d89c097ba,901704370,7019326316925566642 +5a3c22d3-9d43-8b58-6aac-cafaebd13920,1084749154,16847644189112604673 +8c12fe87-a2c8-c0d0-32fd-49eaf66a0754,1145616685,9146052820022583478 +b1329dd8-783a-1a96-55c8-64896fa08aaa,3245911787,4790645216481325837 +cf47f4ac-dedb-0fe5-7e73-d55d743bbe6e,4283381045,17478317662516573324 +c048be0c-74df-65b9-5412-680df700a0f1,348542651,11181460358127220444 +93224f74-bc83-56e5-e758-74b37467fe8b,4101512509,16540398726589456449 +3304b8c2-d63b-0c48-41a7-a2417871606c,433552862,3314241429182345030 +2294056c-47d3-e989-26df-f4d94b3656ab,2995289907,4621355630112983476 +93a548f6-404f-c7d9-a6db-a0fb23b60b07,2947759724,9844286343641201186 +36f91d31-cdcf-3c61-b004-359a009d1f55,3329267431,910516986564500810 +23ddd6f4-01b3-747d-5e40-5cc3a2645659,2515232511,3106855020240066160 +baa0e179-3d76-b79a-03c2-29a175457296,452443654,452671485653161553 +b3d41477-24c1-0198-7a94-fa6b207f3857,130024090,5101168328329578135 +ffb1023f-5e4e-0883-cb89-d6c4f1550ca6,146287730,4710526778724405296 +9f0130ab-76ed-5672-dde4-e7c1fbddab0e,4187486565,8628305742749236300 +f2fc2845-793f-0bff-39ec-c157b4671fc0,375547711,13621362527150629229 +0c12721f-b7d3-fa79-aecd-8fb76e6a926e,1438254053,7352077105359824377 +050e1679-96cf-a3b4-3865-fb34d2b54a10,1920290714,10055522789794612928 +d652a897-ae19-bd47-f62b-f5237dcb8a98,286002697,1515285967469162203 +4f1c6ed0-690e-9f8a-3eeb-7ad8accfd58d,2039096307,3080753655342575801 +58cd47e9-6bcc-7a15-ff17-2e116f2075f5,528831952,11766853537080232797 +5777e2c0-146b-226d-9e22-074a4d243ef6,3787489658,17094022024531842211 +0817903b-0ad7-d1e9-f694-a1026df076c8,3814356479,5545172350330245498 +bc3f8576-f3c9-fb11-173d-1b5514b92d33,129069667,13564711253065999796 +655df14b-05d6-8b0f-6251-d3b5f6f92da8,1457305262,9403746929003670644 +78defa83-cb81-513c-c1f3-c031ad42ac7a,3562868437,1784065776411232391 +61481e9a-2185-8955-88cf-cd78bb007455,3530926572,8497477870887057633 +ce38e3ef-b5bf-e6b6-d202-7bf2c1ae4184,752402241,12722299005849476050 +f5970710-ba3e-ec79-6c73-48a6900b4a5d,2702257746,11861011469976252976 +d138a47c-9706-faf7-6c71-650deaad2fc8,2406337591,13244395059708832863 +5bc7ddda-2812-2368-c9ce-634d9c601534,3057541233,2031542263020478891 +8c5ccf16-02ce-4559-4355-4cbdac74f2a8,1938977077,9690744432008632315 +af6470dd-4b40-c25e-5e26-6c3b63e21cd8,1969007003,14789319765117669293 +c1843452-6e2a-9392-bfd9-65c80cd12063,2588123866,15113592479561617127 +b0bbbc2e-a1c3-7a83-7214-4b01486fcaf8,2252069527,17499792666941058932 +36fd6ad9-90e3-b91e-d3a2-eb704ffa834d,4178605928,8191682312693775335 +4f985229-f547-46cf-fd20-a9a76f348244,3367872288,5231304917264827269 +4a309e90-542a-07e7-5c90-7fd20e02b99f,2754151235,11838969642276510854 +e09f1d0a-a03c-c73e-2d25-0dafbbc29001,2620988088,10786735583676388104 +17b99c01-d423-2911-59b7-4ff0ef22c2bf,942725429,3972902385191996065 +f73a0f87-dd35-7a49-8a66-f8300f88f0ce,3988760842,17647974180397709758 +dc180831-641e-2ec6-4221-1ff926234200,1161050636,3915500613745684257 +36b46238-f6a5-328f-ebd1-d03f16af5290,617691396,10819685176869701783 +b0cc978d-b371-1192-7937-7bae8004a2c2,4144411154,17330619605846854110 +9a51d379-792a-9a39-442c-c8bef40bb22e,2593480650,7948469726207702952 +71e2ab76-b7e9-64f1-7808-8b8799aedad5,1786140400,13701122858290559828 +e2d960f9-66f5-bff8-47a8-a1dcb2b28be0,819195609,15978674517500951939 +c3a4bf26-99c2-3c5b-432b-238946c12368,2519367547,1271651773962997178 +89b0bfd2-ff69-4c8b-72f3-a276fb46bd6d,3758725289,6943271650874888336 +c116422c-d029-3914-3178-be7a4a975bba,521507472,11737912729912065503 +e926ad2d-a002-5d0d-3668-bea3d1fc9a0b,171353156,11271317037325845036 +83d2c5c3-afd3-b50a-2ef0-1a4e165b19ea,2548535734,2244793820108382775 +e1b4138e-75cf-ce25-f1ab-9ca713cdba64,2870550018,16849812966901490263 +bdd2be54-34fc-1d27-f6fa-fc50e45a9d91,2660328910,89099183174926551 +6dec85f2-6f88-f35e-2f48-f76594b55f0e,2748343983,4439577448010290898 +49773ac9-3765-1f3d-2775-03e475179ce9,1865851921,3782116724637133967 +4a4d8687-02dc-4127-db5c-11e59da1511a,1418932223,3328098958631974646 +dc5c66d1-25b9-0b0f-dd00-8d53efa57372,118754345,13093108421174691184 +43d0aed5-a972-780d-b460-4622ad06b3b6,393865600,18310531209786207930 +c1de30d7-69d1-35a3-b4dd-004842207610,917126391,17832353346524391624 +ea1227b9-a175-84f2-648d-72d8ddfac16c,269108323,17992083232496388328 +f275b7cb-c2d9-1e31-eacd-8db4558d2c3e,2044859131,11476723808616971489 +034cd1e7-b7fc-a2b3-b661-be0885975834,925921403,13149740789567518262 +3b184d70-4d41-4d9e-32e1-d092e14dacda,351166358,13572811141497024626 +1f25d2f8-7324-b2bd-7d7f-3ead391fd63f,818668656,5678815895009690296 +74049bd7-f17f-90a2-3596-d2b674c14712,2655231206,10801399592782601970 +be113ca6-9874-57bc-1e95-d27d006297e1,3941218363,3587841103038312241 +8e30fa78-0903-ee2b-4c75-727764e15d53,1534191646,9869417519430314082 +4803151f-b15a-c236-8802-b75b0aba0dcc,3293296529,8097765117151332088 +f9578f7b-b97b-ca90-a1b7-3b540c3c918f,2440920812,8682508414807587449 +aeeb3826-aa59-835b-c260-986902811e3f,1391033813,2550096658378500160 +41411aba-a004-5efd-0adc-e959ad9e3ad5,3354660599,12072958607931327786 +4ae7a537-77bd-84ee-fa1f-ff21491057bc,1241288965,5758092501567433907 +a6406419-5f3f-5e1d-dd09-c41270753403,2593529969,15676094658606134290 +70979cb9-64fa-5fe0-ab50-756d91eb1a9f,1705841910,8141541339081848439 +38527f60-9709-aae2-bbde-0fa016089c49,991917812,2393477860647785782 +d29bd033-c3bf-2a73-c025-42ea372b4e66,3181195927,16196295975524943800 +c7eb20f2-5e50-1d3e-c7f3-3421f53fa5d2,3596163743,14778374942991949129 +f83ce79a-a0df-7ad7-e5ba-4c185e835bba,1357885056,154451555369725136 +8be50b58-5746-0395-2bac-c5d136068a58,842584957,6341091995592555391 +e6b88948-a03b-fc9b-594a-68e7e1c24d00,1140158234,16532426286148972273 +cc65ec6b-5115-d755-89b9-7643613ea6fe,1243061531,5359209281983140393 +d011774e-e181-5515-588a-811bca6a3419,1340657229,738013679497643861 +42754b01-3a99-775b-a984-f3b4d3291561,1646793082,12391749519801922385 +8030767e-a226-a5dc-f16e-de87e0557cb0,213210192,15735529924244587046 +a4ff30c6-bf1b-0cde-f1ec-569cdd489585,560807862,10112753510697837291 +49160a24-b220-27c6-c827-1aa319e9c220,1616181510,15947491393909126346 +85c6d8e3-c5a7-6589-19d8-2ba16e88c04e,4252583326,5308810816455127229 +8561cc84-2b74-245f-5626-293d34fc02b9,3705048171,9833487817493814474 +58787d92-7d28-0683-18fb-45e4ff619bc5,2235714292,7600829596020930507 +89a62f96-2fd1-066c-c5ac-7f0f6ddd3cb6,158263423,13694480627855538491 +065c5f2d-bde6-73f0-9c14-1d6e9ff950c1,881190259,3299335958520533017 +526ac6dd-2d44-f1c7-2c93-f8e387c2d822,2496113570,9699197843769366778 +39470697-d452-4bdb-6f28-b7fe463aa4db,3522508047,364946882243838119 +7dca39be-4503-a56d-e25a-1fef617352d4,1558748128,4629051836567415040 +97b00ccf-7fb6-528e-70ca-36efe4fcf920,65969955,8282102570069646027 +c6022cb3-952c-88a6-2e40-8d8bae381b10,3109142590,16942772687261096932 +81cb278c-5d1c-1ae1-55e2-3ead3140eadc,1962984016,14830856425631905380 +28ea4f60-3f1c-a433-3804-03dc5be06f57,3171515187,1889646871428705886 +30aa74fa-a45a-5af5-ec56-98d2e8391d0e,3276974401,17087888423455894647 +a573c857-2503-0766-e060-06d67e2452c3,1339463021,13800309235145981034 +a373418d-3b0c-91e1-0202-1a6c209c3999,3614665407,9448452327249184684 +d66d7dd4-0078-fd26-103c-2b1fed661bc4,698746971,10766128431680279699 +f3f9fd07-edce-ea30-709e-6d42de7292f2,1265524745,12738329023962770334 +0c2a68db-3219-8849-0861-a13795c2f9e2,1614170183,1396326218503596437 +ac384709-77b5-bbf3-3855-a87bb6f19450,1296973687,15473490744113234627 +3823f678-75d3-d3e1-d775-989b56c58cc4,1884109752,396606291400874593 +7e0051f1-ae2f-5bc6-fb0f-6a867030389a,641959967,15745756214997471824 +5e73edd7-4603-7d26-55fc-1a8c7f5e1d13,905565077,5815352035475260250 +56434ef0-99a8-b905-b3df-fb445125a6ba,2102328526,4439982914807017974 +8fd086ab-cdd9-bf77-cbc1-689e7dce3157,1789414120,7727335505127684143 +8099ef74-06b4-2c5f-913a-329579c55125,2870473071,18275832997964810716 +e1a56c85-1af6-bb47-b820-336d9512d09b,400872315,18249673368283884288 +f509a460-a786-de59-c438-b92dbcf43564,4254595429,4832318907367542342 +4fc802e0-d732-cb32-9f04-01e1c8f17a69,544842034,6291142971082734163 +8690daa1-e4fe-9711-6160-411b599b4ed5,1318514142,13751928124804490988 +4bda2961-ded0-fb69-6ba3-a89065c45eab,386528474,3300402579494035829 +9c2949ad-d356-06e0-126a-7fc18184cd0d,4053598488,597814042918456667 +e4eb6a75-66cb-3c84-a6e5-44d5945dcb96,53516570,11263087716169549001 +f979bc2c-635a-c8cf-0eef-f9e9494e858f,2746620290,9374919319112327471 +ce9592fa-9c2d-b157-f6ac-a227aa946dac,1819809728,1045764137565344179 +6cf1bb85-fa2c-e96d-28ad-dd3ed64ecdd3,1231114388,13626478013001532548 +99ca99f7-d124-0c7b-9aee-68f3385735ce,1439044757,3422443730477518254 +7f4ebabe-bf1b-a9eb-6ac6-eee9aa71f40a,3574814140,11288953912758461728 +4d486a8b-3b4b-1d54-b2f3-312512003c92,2774639604,10264985919960853911 +390cf7a2-239d-dd93-a7bc-692f9fea2bfc,1128890493,3730674122298261233 +d7f6467c-2a82-18c5-b99e-87178ac51d97,1206703198,18238599525408725501 +bbe45340-95d5-6c27-e2b1-5d367de8e1bf,64916038,15617797235598805732 +f245823d-33d4-337f-bd64-a0913d7c8df3,3447210362,3043909131542066226 +5baad6ad-8ba4-5344-6896-b765ba9379bc,1220626370,7352983546095086872 +a6443c7e-7226-0c19-adc6-1bfad1e1bee1,494136100,8793468634783367562 +49cca39a-1070-a46c-c78b-4e0285bf9c79,2037742427,16302066590309680607 +d87cc803-576b-ef8a-ac17-b2b176f7181d,508251857,17855126756512369621 +84dbf35d-aa86-d047-034a-065ca1fcf51d,3515438706,12886369148857071937 +8c81a0b5-40a9-b182-aba3-f4b9597a29a4,331842550,12246755622052586285 +e067b8bc-5c53-2985-b053-b77db37b832f,3219040013,4163147556846746209 +9f3fbc8d-47a6-2764-88d5-f6b62019913d,2430871682,17542607912999316807 +4a06d38e-ca02-6a92-0bdb-a8bf3f02c5d2,4113098003,7433374380419256338 +3dc79c15-e210-a65f-f4d8-c35750422dcc,1578170847,9093645060917070330 +a8eaeb7b-eb62-01e1-7747-c4a734910283,3594647869,9826868460046794074 +c7bf6ac4-ae4c-31f9-c8d4-106f4525215e,2933339988,386323351976856217 +0fcf1e30-6ae7-593b-970e-526d277f2dd3,3210014096,8322524433105480359 +df6a3894-e315-770c-3314-6c3a86b54712,728408705,5461523288931091450 +6aa126fe-dc57-147c-8b34-af710a7a2a86,3000514921,16298244916433728871 +33d8938b-e04a-bf6f-b3c5-41161ab5312d,735205755,4923804847265489673 +19864806-213c-3ea9-315a-6782a322abd3,2061736172,11515129067339631693 +2d6bf6f7-42a0-042d-a5ba-82cd9dd1fed1,4244254612,7491741891752365276 +f356e278-bac1-9ece-501c-325209dd3b51,1067866483,6165636427836171481 +bd3fbe13-19d5-123d-bb81-334a155ca87c,3177252941,16222143633986082864 +62ba80f7-71ff-1bfd-e5fb-72931ef7c762,27441506,13881232090356074947 +9e559b4a-aca3-6107-49d2-67a46ab7ca65,3121359764,8345402789582051224 +3f48e062-e769-9e9f-5929-08cdf2b3f97c,1733698358,9248598515100737590 +6249ff52-0ed4-ed40-689a-01d829e40171,2181895776,5603846889635604852 +b040963f-9c74-ead1-4fbc-952dad4e1fdf,897980928,2802966150764556485 +3b96fcfe-9fa4-68c6-9661-1e4a1fd2e3a2,3625709633,4675908909884244621 +c533b06b-6268-40cc-301b-b002aea97302,2793642224,9392237270706894873 +266786e5-1b22-1b03-3a2d-79cd7cdc0e41,2323445553,17328775306822626161 +74632dab-d2a7-0774-6967-c5a3e37f2050,2096041037,3416687175270558898 +d78148f5-0dae-d497-52aa-f0971f6ec159,1938741502,15912257512613589936 +74af1c1e-5eb3-cd44-4148-8686452500f8,2329027054,11720618529412484611 +a9959786-8c2a-3724-40b4-1b9e3fd82b71,4294032564,90909060003164688 +bcde5f2a-c8a8-1bd2-dde4-0f47c8eb0015,2518374064,8385352443910747122 +fb68d411-b2d0-2092-637f-381c085b1369,1475068732,3666502904034920722 +afaf0751-d4e3-0fb6-d0fd-48b3d993ca61,2078932007,16841047714881610782 +5ba185e3-d52a-bb12-5601-58bf1ee5cdaa,740581830,6217185404790570807 +3b4f1022-2b91-4466-0965-c27024b0ae21,791787870,17518805785674808274 +822bbae5-7817-dfdb-640c-9780e8d122c9,3336713488,16775308023972249359 +19542d78-dd3e-d172-4892-a745a8075a1c,3939682808,18100802694180164136 +ddec2b09-0e2f-7fe9-2652-d69ec9285de4,2810614436,12410272956999391763 +39bae92a-a7fb-9023-9df1-ed676667d916,348085268,11435919627148493061 +6a20f8b9-0baa-7409-9cd3-5d377e8c4b0d,3012447225,12557690726436256508 +9e54bfc2-f3ff-9352-e908-932a72a3c648,1702143135,14577295809012441016 +a42bbbb7-d1ee-03e8-9823-6a56e826494c,981659936,7669607401508484154 +6d57e8f6-a884-e3b2-d862-2f8053ce6158,788306782,17821418496761766004 +a88a46e5-f7b8-313b-b9a5-e183cec1b700,442177709,10473235438521642194 +e2736d4c-a425-dda5-a101-77bbb783c660,2661631871,695511342834569751 +e04a04ca-9bc9-8cc5-5694-967a7476dc87,2009088284,7437734272644989069 +c30f1c52-eed4-48a2-018e-4088bfdf1ab2,2378076598,16466851541520948070 +a87a21cb-8c09-b4fb-dd07-f5eb1bf87811,4260514101,6768858332962955345 +b4c78f75-cb1a-ad4d-3964-efdab237c65c,3723738716,4220211680728279925 +32f43e6c-8df1-4e23-35d4-5841ee703d9e,4258572643,12108880556887640264 +33e536bb-7892-7e70-5f03-d86ceb14122f,1168098460,9876773911205462844 +7dcfc0c7-22de-6bd9-99af-917464c850e8,3099566972,14827017111095831733 +a6904e05-78a3-dc00-2f7d-2659ad1efe21,907114266,16727094054131797115 +f4a51dfb-96e7-2a0e-161c-6111a26006c5,4250831119,1681429259259682364 +4bdc37e1-f7e3-2f76-86e7-42d7bb43129d,4007635529,11599981764100337384 +fc56b276-8e33-f2cf-6fa4-fc64ebdf0f37,4162969719,12186437077132714867 +a6c4ec52-e970-49ba-38bc-3c9be767f39b,2814279376,1681686809907096599 +a3f57b7b-f71c-8d31-339f-48dd394158b7,496600202,4309505222556099967 +83b600a0-7498-4a2d-3338-841dbe085dc0,2360927719,1110585161115234958 +dd146696-f39b-e945-5496-a8526325f1eb,1953910796,7938330202839522729 +206beeea-abc0-beed-d36b-ea52c8dddadc,2156635535,5389112796555606984 +ceb6c4b5-84c5-85dd-e8c2-57b706afa629,1022852044,12014038387709998457 +b4c6ea0b-da12-3f76-8b91-218dfff98869,2900317745,14508250541625327964 +eb602830-21d2-849b-8b3f-d7e90385adce,2485892025,5072067343886222473 +9875049e-2623-554a-11fa-da0d1fb126b1,2751309091,4734015429983797507 +616c7742-4dc1-cfd4-df03-5646e9f6def6,3831627764,8039983172081337694 +e80cc3e2-2543-1397-9b6b-31bc6a9b67cc,2116642828,16037241963640593015 +d933b064-fce5-03d7-2a6d-36b51559b57e,639357943,18086812611371068577 +3ab74d68-1f34-d918-16f5-c0404922a964,831517367,13842673981368544592 +84cd97db-7606-3e6a-8ff2-d4cdb0258bd7,2505711886,1381594624749005346 +2a2ee481-cae2-03e5-aac0-775299c78bd9,753672896,6640302152645843106 +5b0c8d09-1720-2c18-cd5f-ac203ba95e98,281958400,15628208079545495465 +a90aca18-0f0b-11f5-c077-89a8e0756ec1,835029271,16272489700038825212 +437c43a8-4769-25ae-4281-fa21fbf8dc37,3057718943,1128132999198122593 +c0553bd9-7f91-d6ae-8ff2-a56c757153e4,1995273501,7865026754329126869 +14060ade-3f46-ca96-2712-5bf20fba0eba,2945026912,14472012925208475993 +f9a5e3f5-d043-6630-d318-51b0872e161f,3993482953,2357915871825385270 +990f7751-4689-d623-6b89-ca3e176f7b2b,3175650613,176926220376103505 +3fb8db52-56aa-5386-657e-d21b73c89950,765471683,9463430872617354407 +e983f671-affa-799f-5823-0402d6970163,1143101531,7601632639489949652 +9a3a82ac-e133-cd8a-d690-234221a1aa01,2186613804,14637199917816906612 +e5fcca60-85aa-3ca7-9b94-94ffbea11ce1,2537697256,2609198146710960487 +f6c8c019-fa66-090f-9478-5c0c6d0ddc6c,4101877258,3622583367967497747 +1b990f9d-3975-fa94-5e89-24ef16a21294,951492395,1170524597983690330 +ba52f002-1dc7-9da9-2233-bd9192aedd8d,1310899595,4734582135654099812 +782028aa-e0ae-9157-f792-6bcaf1d830ea,130529091,7791365974489011568 +f3ae7c07-e287-9e1d-2772-3131e92bb767,19752584,10755448164588366216 +80e0a51f-9c3c-e509-05da-5912ab8864c7,760566054,5468892163715362 +c533fcca-c9d8-cddd-b145-8f0324170594,1911096103,9952399371668269618 +f28e9131-fae4-928b-2550-440001aa3cc2,1746066284,7349627842903883735 +3bb7e831-49dc-e47f-7ff8-b6df0ae2450f,2484479624,766127978316165435 +f7a310ee-4ccf-d8ac-fd55-4c45a81a4971,3917345387,11174766629394852507 +c90f7490-6eab-bf57-766a-bc498c586149,3323755380,8960472388279468949 +48930168-c945-fc3a-59da-2aa21a7e0a57,2902786066,1578780090609935585 +a691a407-7231-65b5-0864-6afec348e348,615916722,16051095365150967839 +a2233d6a-9a85-0335-d718-4c0d235f5e77,3733848237,9173930902378934502 +a5798d6d-5803-5f85-4ff7-d880936e44a7,1314252550,2882314844379664282 +e0d36e2d-35af-ac2b-1aea-babcf5493263,3239231189,17409428984139090950 +2a89d1a9-72f5-a54b-87fc-437e3028594f,959070827,6103241388654213489 +ca583b9f-97bb-1b6d-9e17-61702b6b5217,806816486,13537206377016078263 +42664d2d-aa27-779c-68cf-b86f4bdd75ec,719946335,10840959530143093722 +400c42f4-4303-28e1-fdd3-326fbbf4880b,1385753169,1589879976249603427 +67e814be-ac75-97b4-1849-26d09d8d26c8,2956158611,16004772894325976914 +63256fda-2060-18c6-745a-ff931540d25a,2760379210,4991607861474672492 +d114781d-ba71-9030-b31e-e62402523936,825686387,11432877302720779583 +04603d28-819d-739f-03e0-0aa5ecd31972,2403843673,3315231562974462536 +5045ff1f-7144-f2af-c84f-7cf457de8723,1874744022,9722254845532089573 +e820cf01-f6a4-679e-2f84-a2412c805023,2313650161,12945122597167513585 +ccf661f2-3c7f-ef94-5610-634ef2cebab9,519900054,9966087496526892145 +8a22a8f9-7049-df8b-3d91-3a85fe8cd966,2052123084,10712935516522612595 +d3ec941b-d9f9-1026-988a-5e5d08fc2f42,475886219,11404582177552156633 +c5479d99-7e0c-6a89-ef4f-b9cbffdc88b1,2666300506,12898475423351291373 +51f35656-815d-e876-1ce2-8b10e3007529,313910932,8869947828443431063 +700280dc-d282-55bc-00de-a90528b6a2d1,4148206425,15580186123206239406 +d326181b-14fe-e632-634f-fffd00b25dfc,83552687,1834196777068441887 +e74a04a2-9094-96ad-441e-d89d2943e348,3142301657,819821420982409159 +7146ba62-0389-53f5-24d3-e60ebc989e22,371210365,4474501080918373948 +29dac9a4-0f83-9f5a-acd9-1a6277c8ef76,4020843267,4816367834421386824 +36f818ae-4941-0589-4b75-41354d823215,2497282635,2100332995657681901 +faeea99f-b843-ae42-4de1-6107e75698af,119417050,17146038091781977439 +e4d3d52b-ffb0-888d-0fd0-2d0657f3827a,841612042,11862216144443287118 +ecfa9b4a-4320-24f4-63e2-041e2f657793,1845163537,2635256301330115210 +f9ea7487-90dd-6373-f36b-3d3b9c4ce7fa,2384862086,14121430985611972123 +38bb6b79-ff02-10cb-518e-d4980f34b1fe,3394514946,16679949764077280818 +e3112a6f-879b-573f-8ad8-8b9283d95e81,3333643512,4146417761804223599 +f258f156-ee75-3e5e-8da7-0b5244b63738,2865841352,9584295919177811639 +fda8c8fa-55f0-1b64-c350-5e4133fb413c,836596324,12793535732452371549 +8b6065f3-625a-ee35-3e3d-a8b4bd36b07f,3952700495,9330272143336662536 +58f22af4-61ee-1c61-c172-f300c5276a77,2038934578,15489746612224335181 +90f69a45-175f-8f18-cb0a-ff1649368dc9,3723163469,7104329846434330463 +e196dfc5-ee7b-2939-cfeb-cb1d59273148,1485544376,15813800981988392858 +4d173c0a-d625-8222-95ca-74354c565da9,438787685,14919593583867460663 +59fe02ab-f72d-7ac2-2a93-7b2a7385a8b6,1188326771,16940496920579576510 +f07b16bc-cae1-fefa-dd54-89bb84c46930,1988162013,4231846693661416223 +542c7614-37c9-669c-e20d-ca9d4a383115,3670863924,16027268809997350286 +e3c8cbb1-ed4b-e67f-e33e-c9c118e09dbd,2862948211,2642373169135376801 +7f02d8a6-6111-b089-3484-217e38f98140,1472903765,12968667377296532884 +4f12dea6-7eba-3eef-9f69-ec4c5e8a2b86,1496800171,13917505738680328030 +2ca7eb53-6c6e-1080-0c35-b81f174cdc7b,442949449,15207927254775606171 +09eb4a73-ff32-5545-9659-cae5b5e7856e,2135678511,11325371192444445296 +59df0ffd-b886-0b6a-11d8-99fe02033d71,4037762060,2761141899920630539 +24435ead-bd9d-6123-3589-2c02dbb416ad,1184117652,8228431336126813511 +e4649a80-08e7-00b0-1b16-6baa45db5e84,5112510,15812696446667726603 +59aefef5-710f-0a88-96af-b880e5f300cf,1228970465,16413252657512676245 +14511665-3abe-3484-5bc2-e44b9ff68644,724293242,5373557443406725483 +124cebb6-020e-0afe-4edd-296920c06462,2256393910,8484197378209151173 +a3ebd881-b46e-6edd-8f44-f9a2ca104dcd,3595040777,1676121585520305256 +6114a5f4-9bc9-fac3-095a-df11614f44cf,2462944063,883773341437477095 +a6e3d762-184c-38ae-25ed-19c1303f9cd0,890895018,14527561310084984064 +629b5dd5-08ab-ca33-ba94-0d1f68b65064,2790087323,539892126203449167 +a93d3a19-051f-e0b2-b873-bf848b2bbf04,2758448366,11010047657271940627 +5c0280b8-12bb-de49-586a-56858e9c720e,3227994138,10972791850838204097 +b79bcb35-47b0-48b1-8a52-d509afcf3418,122550075,18405104011391758209 +f88c95ca-50c3-5921-3914-ed7d5687b190,3006997639,3928857658416796484 +ac534583-2708-de61-21d9-2af424355313,1331541592,5354596743623205287 +561429ee-ddf5-7265-e586-aee7bd0b20ed,2103741189,13170395554400524932 +b69dc5f5-a057-1258-f0b6-4c6b0aa6665b,4268406133,14346793526539894109 +e835e5ea-df09-9f11-7b13-65cb3a9e51b8,2556631699,9548115679338327136 +73c8ba3d-9153-84e6-601d-dadef952453f,1539194885,10528408513163407150 +2f10c7b4-20fe-4bbb-ee85-bdb7aed9b599,2004985076,13537635421473266777 +2cf0cc81-b550-3b40-b2a6-ebc6b7b741ad,4094451943,8478076562671964313 +75fb1e3d-ec73-9985-6dbc-de3347a03aae,268282742,9014006767685494491 +9eb6f5e7-dd08-cc30-598e-fb936e3b49a6,1166920538,13190217964204037034 +cab62dd1-4de5-2414-8007-5f0611b8bb35,2607281569,7880659711665588503 +e3744cc9-efd7-3e55-e68a-6e691c802ea7,2409846198,13253285962944833553 +738980a5-8a78-e6f6-24f7-7fdf8270e5f2,3346771248,10578504312986598716 +05c0ec9c-0ac2-7cca-4133-d3bc1be91ea1,3617816013,11741077845332350577 +84e0a49f-56e8-be28-7e4a-02a4850fee99,808611266,4530087048055884951 +e90f8616-071d-db69-4277-f81df4cb3d8f,1239905393,17882896080333456627 +071200f5-9f51-509d-570b-26fc1bdb6877,3332785131,2397300449165641563 +96d86283-93b1-bf93-ac9b-7fc13e9e2bb9,110112992,4146297884968693577 +046fb44c-967a-93e2-a1e6-f6ea4ee89818,1148535776,1668255164187741848 +a29788f8-3905-17b1-ffef-d531e3e87777,4048563734,3081076916175341135 +3d4e42e6-1060-3f18-e12c-197ff94cfcea,335721437,10954698032670841890 +0b2fecde-8ac7-92d8-8e4e-3e2aebe25be3,1115539892,15515446966879644567 +6caac761-bc70-44f5-5be1-22267547966c,658035281,12797734855116947395 +2fa58944-57ef-8440-5d1a-38237e97a6a5,3509027420,5746651555517587524 +d597d643-4dcb-fd9c-99f3-7f93f8ff4572,735885845,2378938517500301882 +dfe81285-31b8-e0da-c195-aaea1ed6d1aa,410883327,9777192624843599476 +2cfec755-9d79-fa17-e04c-4b4b6556d499,3748891677,2803613835689445226 +3bf4f88b-79e5-a8c0-86a1-b66996bb47ab,3658804106,7690990796026123033 +c8541ed8-747f-7829-7532-a99d7698029d,2762535844,12118581558137864351 +f6da4477-9a22-7eac-49e9-013834d704be,3397586364,14579784555662029599 +3571a9c8-0d1a-e727-a4f3-4b3182200a55,1429555560,10341183026958480208 +28f0b774-6661-b434-7ff5-d4b2be6278ad,459508155,13714009126863750065 +b3de6923-3f8f-b852-9294-60c8a359b919,1544033076,309553099841654210 +32a45f3d-befd-338a-0257-a8d55c648df7,1859764901,13018682543691080270 +f35e9c9a-04bb-4d0a-fa93-4d5090f98ff9,754838925,812222411909639685 +567d329c-8e78-3f67-3ddd-1cc8d12b51d0,826292334,3358585541417514192 +316dc042-0558-fe96-285d-fbcacf82e481,4181820785,13182926984526578966 +4d7cc8c7-78ee-190c-e872-f31cc2adb54a,3331889490,16752351486211051980 +fcc14a5d-2d68-5de9-ac3a-a589cb18eb36,3837596564,13458461084962600294 +01e17702-d0f4-ca59-9323-588cecb691a3,1440216412,16703961634897100231 +ad508ab0-3351-ce98-b6f4-a71603d9b2c3,443851586,17162152063047458241 +5b5bf73e-3c8b-3616-990a-366d328cc54f,1082485365,8182712492922906674 +f363a099-a37b-589d-4d48-e738cedc19df,4134345060,14273311860960770609 +bd869542-b1b7-c27c-ea8e-b9afbca7fc48,2404752401,7028258351218425044 +821dd42a-363f-8892-8b95-3c22a16dbf4a,83863687,6539791897596949788 +1c2ddc01-bcea-dfc2-715d-1ef03eddaa1e,2340793571,6571090625147304543 +fc0c7d2b-106e-ee62-99df-0067a56e3083,2387878361,11063523462416597203 +2ce75923-957d-3969-55b5-78aa4d6922b5,2984778596,13445501355515935078 +aa4eec60-deec-f0b4-a3b8-c1aba9816445,4109874583,17780655549424440658 +52d7ee5b-9b6a-2acb-f090-4cc04e84024a,3986403350,13341085611648965121 +3f72f373-54f7-3cfb-d322-63b0dfdce346,68643749,4514110038621212360 +13785fd0-1364-fc99-6657-4639b9b3c542,3746708860,10935236206747151639 +d9b34775-550e-4f64-d092-6c13a8c96db7,4191476506,9818382895804353655 +81a0f8fd-ad46-3452-125c-d8df5c7bed39,1922085563,4722272889315933704 +4f735f56-eccf-2973-257e-7cbb468ace3b,438722728,4557177859341470982 +822aa22a-8b6c-d409-48bc-fb24ae18d427,282763662,2182415984663557407 +a96b9a5f-59e8-6920-02d1-a73623b181c2,187102171,14367859710850521503 +03dc15ac-7b2b-d1d9-b544-2e0f536c8e45,1507853089,9605495761360869427 +dff65824-96f6-90c0-7ab4-8be7b5eda02b,3073149694,2570600073560666086 +00230cc4-51f9-c7d5-30ad-d781f8e36334,3389719448,6086737706434543243 +04e25367-cda1-95df-c3c4-fe2bf4908547,3427864734,7812242993191952479 +49f97b79-0e36-83d4-80d1-4a6e5ff6f24f,1905734633,8219704157768598837 +8ad7c580-9c57-4a61-3cca-bb1856c7e45f,921067967,1786996235712705069 +afc1a071-f3d3-38b6-560c-b553779d1c1e,3146669066,6127499046974591866 +9af914fd-7c25-42a4-e5d9-1bdf401b4dc0,482583512,13932694605039961962 +629b7e23-81ad-9504-c65a-f2ea9af4e9ff,2160867184,6357039640348621988 +3833903d-f980-2d88-d73f-8429a221751c,2430661468,516053590466948844 +a825dacf-a53a-6583-fb81-86fd8748fdd4,2979533968,11961411827356072369 +102029d2-3fde-804d-5449-15b099f8550f,1434011725,6349017709027774665 +1053b8b5-331a-f72f-a099-eb83e3df70d6,1732870001,1138329632120497912 +18d82fec-a51b-2b6e-4849-33f751a84f10,301446369,4623007077911438562 +0b1183cb-5e37-1738-f640-1301948bfb63,4115018196,5370854598650333658 +e5e875d3-5943-09ff-5dbd-f78be19d2c68,141427428,13801419370574167265 +05c8fc10-4082-05d8-9c35-e526fe264a0a,1971183194,10044360898882696308 +83ef9af8-72d8-c94f-7986-d4e2ea4d0f93,3024763256,1486269575837971972 +aea1889e-ffd3-9538-5ac7-256eb3e5f633,2599731312,6072034272702081293 +ba6929c5-0be2-cc9b-c93f-337884d66674,1669419638,10333080463822820570 +b2116277-c8a7-aba2-a0a3-231d430cfa1a,2824549933,3501817223332957278 +cf0df513-ae01-5a34-c438-adf948c293a7,3594322085,12695311603434859275 +f9565f3a-f056-ebc2-620a-dcd773817354,205581477,3318447217095366144 +80243a14-a8cb-9eb1-4407-68fea68f2a96,302047696,9350137612241942022 +e2dbb67e-c494-a15e-e08c-d1c021ab161d,3565905706,196219681836302729 +dcb707ce-448a-daac-3fad-c2755f709bfc,1903431223,12990805802995521648 +a9c9da0e-c99a-1949-4c98-d5d81d35e371,3235665063,1611410610984804432 +2af929b5-9d59-409b-a67c-cd337aaf1e4d,1359485080,5588082288799741568 +57552db9-6508-22e1-b644-1bda726d9c87,188751296,6251327540066801499 +bf18bae4-0ebb-fef8-fbe6-479116582749,3645249203,15505257735988138760 +d904f45a-e51f-1aae-0970-207f1776b426,1502989205,10078866796042500824 +32490f20-0c6c-62ff-0027-3d181b8422f7,7203696,13330338362243508839 +e10ecdc6-7ce3-f0fc-b03f-01996aec00f6,1502034707,7069547428642746495 +e1250d68-3be8-f11c-71b0-2803a5aa5a65,1754465710,8671048316053678367 +f412cc58-8a78-87f5-fb7c-192621ead9cc,129325586,4613057845133678024 +027b64f5-2a5b-b4c8-a3f8-3a13262c7eee,2526192967,11759111639503129091 +37172bdb-1ea1-268e-176a-a145674b1f4b,2341241358,857407870712923221 +452ecef7-0047-9dfe-9201-f4a5332a75f5,2704673730,16980215895148496048 +e70d8602-45d2-842c-5305-55f676f22ce2,2333110110,6125128055674851106 +7ea1e4ca-24e8-86b3-9083-89983c4cc608,2376119921,13687641152476505160 +d246dc6d-c0b8-60e7-8d6f-7c12f2fcc6bf,3450381028,6283050424859102061 +ac19a620-7976-8733-7e77-e99bb5045d46,1955485694,9488842354091815579 +3d42c8e1-4339-20ae-bd9b-a8dbe02d0377,2876360774,5056152892172272506 +8ee63469-b409-f702-cdfd-557ed6f0fdba,525175382,9374248060755568873 +877b9bda-81bd-2eea-67c4-83eacc095d9a,1689521799,9243131868433116277 +262ca251-e0ce-2688-4356-d3dbbb98471a,2817822237,16303849554836295741 +0d949521-f300-ce4c-e052-e69453050a41,528843380,6920018946656700516 +1b845b36-4a01-524a-64d1-44b83e375e11,2516054521,13312657960422846916 +e7a80441-a859-1f46-24c8-622201e6bc6e,2506218151,14635083781748969604 +17bba10d-58c1-1a6c-8869-f7bea1b0f175,1445915120,3468867168800337589 +6c361f21-e276-9800-d39d-d0d6d6b1ad8e,1959498458,17027321185773800431 +fe5ac3ad-816b-36cf-1abf-1a9a53c912ff,272576173,8333615831323247907 +386c4395-d393-0a0c-e680-eeeef4db4658,3415745708,3437165813268337984 +c88693f1-013f-27a4-20aa-5de8e225c4ff,280732386,3300808437231739071 +17336d85-7250-6c21-0afc-4255f5c1a13b,826154302,9404489818069107115 +c16f019e-73b4-cbcf-0ac6-01c4e734086f,774098703,18218103210807346244 +0a24a1ac-3cf1-8cb1-8baa-eb4f35f8b984,3234127315,10381475819408678002 +c10c6fbe-da4e-6fb5-6deb-35d777b8c793,2457037549,3052384122440065804 +fefa2756-fd94-15c3-3bcc-881bb3ee4b59,99059407,14226021013083053196 +ec892c11-9aec-b76a-d777-30247e106875,689370278,4065190929593588447 +0e47fe76-a900-a0fb-8a6c-01d00a27ce67,910152134,6905635718014189337 +970e9f09-f9ee-ca61-3314-d850a40fcab0,4096585477,16036174847495391790 +497e43de-1488-5b11-6b85-ce92771f4e75,302398790,10582935017164571176 +637feb9e-75b3-dc70-5ab3-120f0ec32183,4165568954,16883508305758280774 +e4e145de-ad2f-92ee-ea0b-188f9fab76f0,1462650861,5861158835576539195 +785d4794-32b7-8971-ce81-1b131eb86075,3165801583,17639006044243976235 +b7750ffa-f731-0654-9ad8-3b93623d86c6,2713375973,9765444584878933249 +1f349cc6-2733-e0c6-1dbe-ac65b7c2013f,129827271,3271857323644219184 +f07a02be-765a-7b91-84a4-f981d4dd46d3,2268908332,14840242830230252653 +b56dde01-36c2-f78e-27a1-df21e8f93966,2175734027,7137046498916688013 +77555d90-5b62-e793-b11c-a3fa792a002c,1840715875,17128976116454646163 +046ddbd3-baa7-6d7e-2fbc-71b739dbbedd,1927645133,2628060200861631602 +dcd2eace-168a-c5ee-f0ac-cf5525e240fb,4280352087,12878473231948965452 +febaba11-2a8c-9756-b6b0-b873e4fb37e6,1583917508,264892763459666171 +8745a337-d475-2a4c-0d81-0c7021f66f72,2840838037,17867931771661460799 +61510543-7071-fb84-e9c7-e3c7aba29411,471110014,9773053685308628243 +6b716235-55bf-ab4b-7a2f-17ecd6cd0cb9,2899976377,4702767879390822297 +1b1f7c7a-e932-5515-9524-a2dd2a2d1ce4,3053893173,16600797643356253402 +dd1116eb-f462-330e-bcfe-6a18a2ad6f2b,585948557,3041676816586247464 +a3c599a6-ea20-c98a-b635-3d34a057e1db,1759499265,4360490877301209076 +ad7fe0a3-133a-eb6e-831a-01624f75af59,2788737069,312547031839072798 +47576845-2c58-3a04-df56-83f7851824d6,1226316364,15968673578705484659 +d4c2a947-ceb2-202e-fda8-0184fc7c5c5c,791586662,10552869288100346436 +9f5cd4e0-7790-8987-b173-17bcde1b9fc3,2233331494,13681107479094179463 +94ec72c2-37a0-5dd6-8457-56c798c9e5b1,3084197205,1338788569558559210 +cd980308-b393-90a0-00ea-b6a9cbbe7cbd,4192880835,15622214520593498021 +ece7d60a-8a1d-3640-7301-ac859ead0863,1088743620,14000195307544822755 +15138113-4a8f-c52b-3901-a34e5d9b19e6,2137012118,3462970605662525546 +a257d7cd-fcec-e35c-9273-cc2486b1b815,1425937113,11099512680279379239 +fa6aabdb-e97d-4e46-88ab-713c828e7d44,2239277245,15122612297826467969 +0516aff0-18ad-9f11-47da-d1f6c37490c6,147476489,4753477244681129493 +a04b306d-32b8-953e-17e5-0dc0eeb096d8,3748037097,14380722049264543373 +4447cb6c-48f2-6a2a-daa4-05e9762ca8b6,2072747417,2656338783545138948 +041558a6-4fd5-64d7-8a3c-372517a0266b,620856107,10916612162072466965 +49a0a447-b096-a6ff-69a7-c44b643c845f,2217891090,1785188541115603481 +0a6c1305-b97e-38f6-683f-cf8301f5fe78,83065955,14480749048282088528 +5d00f86b-ef8f-65c9-07c8-36ba2b1a2b3a,568034937,1426185078802013 +4aae6634-e941-157e-66ef-d73d463ecd63,2184821438,7508710348668725789 +45609a1b-4d8a-3d75-1a39-6c9412a96ddb,3393997478,1497612731575737732 +a19307eb-06ce-4754-168a-1a0327bd5edc,2296383417,6514854260681287928 +d767450d-a0f9-0218-fb67-35907ac11683,2268751492,17037389575714377443 +ae5fae4d-478f-968d-a9a5-f6770118d856,446908990,17903514990270309810 +ce25caf2-68d6-15ec-6b12-5154ef788ade,3915046037,8713963623829692554 +ff10f909-7069-905e-7ce2-6049006cf750,1835017727,2463473693898461942 +49abc3d0-824d-8f38-2374-7aeba1fae855,282565499,6224290438272231703 +9ecf4b10-6ee0-1552-dee6-b71703c9108e,1146893873,1500754613932309860 +b8bbbb22-bd58-a0c1-23ab-422845a60544,2149363686,10350648104447540159 +0817b02f-f68d-598a-b2c4-07bab8ac60a7,12480476,6910227135631551247 +beaf4d52-74f8-a829-c7a9-f59f6d96226a,2929880587,12226941560457407183 +5813139f-6e74-6b65-01ee-72ef37e10f02,575051492,6268780402569429436 +b9981498-bae0-54a1-ecfb-35fe7332ea64,2789353652,16413286033989428703 +88163e9b-0cb9-a7dc-7468-b23ee6aa4ec2,1822554972,11489223089681130104 +19fae37d-b7e4-ed72-828f-11a274f18ece,206157595,7085884781519887042 +c0b61b8d-0286-6918-e0e5-caf39b82ad46,1084427695,15316601429435252795 +9fbcc8c5-ef35-062f-675a-040c9f542c85,46701866,7634855689599319069 +0a991a0e-7dcc-3a32-0267-372e2ed876ad,3569920841,1257481670354630521 +eb0fc069-94c2-2659-9539-29612e49c754,2224977689,1280785425795976850 +063af257-4e74-d4da-3cb0-68a3bfd285b0,1232734055,6939194193223815178 +d5dc6faf-8eb1-6cd5-4801-8f492b33425e,2210148053,16327353617707983703 +a862dcf0-a872-0b1a-e722-ae69c7bfd86f,3577402838,12613252618514165982 +21177e57-5ac7-76a7-59d8-b75a68859ee9,1533118377,3707916630961735886 +0f7fadba-2edb-4368-5ff5-283d0d1f8a84,1458577723,3285544301934283874 +d1e1a431-f829-fd00-a846-73cfdb44c455,2355838058,5985765567372272960 +e41706aa-5060-1f73-05e5-2b13f2f9ca14,1196539740,17331510362093004216 +79c5b08f-213f-238f-e702-e9ffe513300f,800978561,17895812459124249504 +358768ef-c807-bc8a-6b5a-8ddd4731ac74,3884687561,6087375654430287437 +a8cad8db-1e7f-6b61-a832-e71337ed9225,76392388,7684061442251623830 +dad88a1a-48aa-4b48-e29b-435825b723a6,2463337546,14311371130201722807 +881330c2-b6a5-cb30-150f-717fbaf1a1b7,579484700,4001414482586089647 +34b615a6-6fa3-7095-e4d2-08b104cac39a,2767061965,158450464134687597 +2de07028-29c3-e81c-92f1-4718fe18d7dd,3151091126,2151105968249498611 +785eb196-35e6-21f0-6ade-13754c58239f,1547546524,9395460216352989264 +2406707d-2b00-d3a7-33de-32f31a6da2d4,3561014301,17462003861110426019 +a648991c-8abe-d84e-1b4f-d407d4ec84a5,3854381306,1043893708833814430 +c330922e-efc9-29d6-eace-e787d729dbcc,118046950,12632089643756258308 +b8ade7d9-4d88-07cd-026a-8dc2e23c7228,2555286692,16766695682618928407 +bf41de73-dcbb-2491-32e5-df041279bd3a,3750773765,13803652794378025802 +92e7dc69-2fb9-9bbe-5ecc-23dbe6241a47,971587828,10971536436264014740 +122248d7-77c0-82de-28ca-0868a00d836a,533522935,17850789286029920623 +d0fff8b5-3feb-6ed6-a561-a3c6a1eb69e2,1621625111,6422087293373800883 +c30e5f40-c32a-5eb4-dcd2-f1469c223e40,1640702469,11484401204122517532 +59438c37-97fd-b225-5d49-d76ff5f52da0,1900971560,2691402190985754806 +86a2e06a-2a52-9a83-bf28-8df021802d10,4025199009,13527924376519985448 +84039776-0377-5bcb-7094-1fc443265567,1706126469,14110791412060020065 +e5cd1f93-cdc3-453b-6e17-cb37208fcd2d,929157889,16633575641564064926 +98245d14-818f-a8df-a43a-cbd46bc6598f,88005627,14462065618188174208 +a86ba28a-3085-5cb0-c17b-458a1af39a29,2123304248,17798750657209093107 +7b148fd1-eaa5-a1e9-6503-f5933456ac6f,1674493459,8967995294314688328 +7b00de14-b78f-483e-5bdd-a3a60adaf3bc,2423717274,2512969379994270276 +69cc20c8-9e6b-7f0a-2802-d7eabed969f1,513090335,389438055014196156 +ce4c46db-ddd7-d0da-ade1-e52746104d73,359841073,2566842150364632733 +8a26001c-3b0d-85ee-7305-7212bc845bee,2537665890,9159620834724626359 +5c48c233-56d1-f2c7-d9da-e061ff8b1e17,2475552964,10333589397221921993 +e4e829ce-dabb-401e-9156-6e737c02d6d3,4203102020,5716839087821359829 +d447020e-0908-4482-12e3-c3e531fbcdbe,3536010800,16146596337671419111 +698fa012-0ffd-1b13-0094-9c0e63466023,732111783,17854176149588719609 +217400f1-e500-f2c9-ba68-6b65bcd3300e,3344973723,2603753196894358115 +c348a341-fbe6-ba69-04da-4bd854f64252,58795245,13006543261949065597 +a9992f36-f3dd-d4aa-3590-a15254af276a,1969411802,6954682699794322461 +977953d3-be07-44d9-11a0-281efe51dc4b,1552432992,2269283922368443990 +299093b9-0d2d-5279-5b83-130ef45a3fa4,3877472859,5706828474000345964 +589cff89-fed2-43a0-6d68-e6178985d76f,1290432548,11691135678208525655 +f5a519ec-f2ac-0e9a-9d1e-d7d7751599f8,530584209,362267762497320171 +c2fb893a-7524-fcc7-d5a7-5a06022ac1f8,1274226979,13047571849822022918 +20639beb-4990-746a-c639-841b3ef617a2,718358725,15635397296955693581 +16ca29aa-8c9d-c021-8e2e-b32d0d2dff6c,3759948046,14908062734089370725 +b55ec45d-7243-87d3-920b-4bda6122d929,3988361132,5950230053485967030 +c6a9b840-4ad3-d671-9eb6-1801eff124ea,3059533676,17579455724912495350 +f9639461-1248-f648-3c27-1a96f7128749,993848062,15770698551350515952 +c3415f0d-e5b2-0bcf-59df-7104fd60b7da,113405195,1498705155140328945 +0acf5b8c-de0d-bb22-c8ee-56c35fa17e8a,3665622585,12494432218009257506 +d47329f0-0666-a5e9-1b65-23816fac1268,2661089231,6128086147967734536 +c51ea757-1423-a303-d369-94c53a0c4c5b,923203513,14040774167126455479 +32e15ab1-034c-ffc7-326c-4a0927cc6834,3500266732,6062824875629512039 +4440b64a-161d-7436-03f7-298d4228b6e9,2724803395,10368340774459703952 +d3de3f35-e56b-b612-54a3-746f6da3bd4b,180462172,8284788873688396161 +630a9743-5a8d-f58c-2863-141136157eb8,2845711324,10717259504500886553 +72a7e1b5-e267-2a7f-e9c1-f48fcad28763,300008274,11691430727736832951 +e94b60ef-f42f-fa30-da5c-62d3c73430e1,359061564,1791480261935150207 +95ef15c5-091d-e0f2-ea55-4312a37dde38,2835518835,6995014055414377647 +31efc555-e566-ed31-5f45-43d2026b5019,3773722345,16592979689944188071 +f611d4af-be82-70f2-f28a-e1aca97f2bcb,16963352,17892281284851084615 +f1f74004-0029-ddda-7892-5c34f7914b69,2972926582,16464132574959685369 +e2d9b4b9-c73a-3947-af15-9f030bbcffad,2132844753,18186112207922174773 +3d8cccb6-5e06-00aa-2620-6a1479fd5f2f,2208922296,1507080275835209989 +10030645-08d9-d78f-99c6-d576c8d126ce,2178854797,15047518074556291708 +249c442a-69fe-911a-caa0-8f0ad69108d2,242921204,8355935905734279811 +2e8c9357-38c2-0543-d4c2-ef2291ed1c29,1201630801,61367399288514602 +87902f44-f67c-e72f-8fdd-7ef3f87526ae,1651551862,12089782392868283292 +0ad45540-a3dc-a8d0-be2e-1ef25bffa832,897920800,4678750092740670633 +9a93c60d-88cd-d33a-da4d-dadf7fa14472,3074840026,10788223689599478971 +a3902fe8-e51b-5be3-dd49-0eca0c845890,2114282729,17331027284306548800 +26237b14-eafa-d8db-40ee-5399375a5a23,2429361040,16070880542694322613 +5b63e735-a68b-b941-ee3f-9b95636ec0b5,3526132614,2927156788978463163 +a567a142-fc4c-8ebe-db14-34fa363b9534,1307340268,10111469647044855095 +5d6ba0d1-4f45-6977-0e54-73a36a201a7c,3251521985,7501141904292319154 +493e5273-8175-d91c-b666-c89dca2b9566,3238437149,1553505268628358921 +003c8103-2dd9-25f9-8117-955bb9e72ba4,1714100778,10419358777202316723 +268f3c12-b7eb-8639-6bd0-29bdab31f669,688252195,1585984784168558494 +875e5a70-2771-916f-6099-c2bf3ff8dce9,2937388839,3657144758442884815 +aafa2187-a7a0-d0cf-1da4-36d997e5558e,1909633704,11126668267875668566 +aa6d6977-1374-7a47-6da8-2c11dc12e4bd,725434967,3696847395053937264 +0bedbd12-d145-6104-d7f3-5935e5dc6376,3494147486,7962550204713169543 +8bcddda5-5d9c-bb9a-3205-dba69eceac68,3635277460,12424562856465642893 +f1bd671c-89d1-6448-c9e9-69f0173f7ff9,2405298742,15144491784725257588 +f2404a84-6633-5b5d-0279-f1bb5cd83a3d,3618684757,6886875444327291395 +f2321d1a-5ff4-6b44-ab47-10ce3d30496f,2188737045,9096959876050426096 +ea4c4ac0-4602-9c58-3f74-c4a5ee2a72b3,2703905891,18132268402620908419 +50fef38b-c47d-faf1-14a5-54995754dbbd,1991976111,9358314976386794448 +047fb583-75d5-4861-944c-a07bcdeed0e9,2436851654,12060208851083859944 +2fddda66-32ec-0c46-d59c-01ba0747aa3d,865205600,6923982127476098905 +dd7bed3a-3baf-f527-879f-2bb42463499a,3332691934,1611411916093733753 +cc2f3943-7bdf-fc0d-6f04-c0a65e4f60ec,2565023286,11111792932874262176 +709cc093-6f53-dfb8-a331-29bdc606cc13,510090688,16502517724473617888 +a1926e64-6a8b-a41f-a992-422aeba16820,3075585112,5340192929330352814 +13e70afa-61ae-7cc9-7e89-a6f2018685fa,3230324343,11209371842481668014 +cdb37435-0be3-f6a1-c458-dfb05c1819e6,1842530089,3145268406786080522 +565a396b-12c8-cfef-4bb0-a7b80a433972,3996910777,10318380345111288126 +c4740176-5fb0-150b-b841-c52660c43474,1210377212,943656710152526723 +b7f8adc1-2a8c-3baf-6a4c-ab0d8461b7ae,2423583567,10673294498576574421 +a3b1f0a8-707e-9f0f-2183-67bb3b020e32,748026559,4957953328362442128 +5a8745ae-a54d-c9f1-f3cb-cd26e0af52ae,1363673635,17186454663352926591 +e506a591-a534-5e00-0f08-06f2620a4724,3239223947,5510138751588571385 +9a5e92f1-a065-fbb6-e136-661fb9fbaac6,2418579171,8541608579281466555 +23543006-e23e-14d3-5d47-996cb8e2e86b,4017536931,2968225264110740459 +bae5e14d-834f-f0d9-5e54-d2390eaa2c84,2851019087,15012668414562126477 +cff64ca2-9459-6307-9ad0-b67c065aed7f,1333164297,8883329139211594441 +526220aa-8443-03ff-658e-c58affe8c154,2881224642,5880688652616655676 +76d8e641-d3eb-3c01-b6ea-496dd9b6cdd3,2953318773,5353961194994050727 +0d63531d-c42e-e616-905e-a68e3eb27b37,759426047,16898244388043183275 +b4793a04-c066-0cb3-30d9-bdde5174bbbd,4190234794,13261406605239040843 +095b3afd-9f70-cb89-c3a5-2d3321434955,2586478858,2468303497175885992 +fba3fd40-7707-3fb2-865b-b9bce33c2f08,2015470438,5187066080853586166 +07e9fd97-cbc8-d231-31ab-c792c4bdff3a,619931061,13720470298649807719 +e7bb0c49-dffd-b18f-1032-c39e29e4e9cd,1588133677,1685220978895562270 +e0a1cb16-365e-703c-d519-4352d1c94227,3074497577,13173002613430960072 +073b1eff-4f62-8509-7895-04f9d55ec4d8,3025690433,12754043418580745738 +7d583dc5-f401-1e69-c54f-173e2f191bf4,2256862851,14912416631159864866 +19ff3735-b499-aaf9-3a41-62dc361bede4,1108689677,10047672387725738310 +1c1caabc-c11c-636b-284c-36935d116422,2489620530,10069525320980040098 +a6d0fe19-4c42-e217-b875-5a681d073f0d,184598848,11363169523610559317 +f6d1bba6-1e43-77ef-c439-8221a26b89da,4112778926,3020568441805229598 +28dfb72e-cc4c-1b36-ec05-5e1f1cf0a3d7,1668836760,4802270042028699932 +2215f673-3b9e-a4a1-8b25-9491da2f7cf1,2123497507,11549021451499807101 +8ff75ad7-c2e4-d155-9155-dc8cf9619e5f,474417821,4988226063642626082 +b55f621d-6075-dd7e-b684-21043e12e45a,3744439486,15742792355871453355 +2bd4ad1e-7613-cecd-cbae-ec1415c1fe7b,1781794988,3869404338004511047 +79bf10ab-19a8-7599-128a-7e9cebe3f1ae,2237894069,2729912252403950234 +6654a350-7496-9152-1576-391fd3efb6ee,3965784469,7687466220256665220 +63bd4c52-fa26-6163-c2c1-e02b25a5edda,2894366328,16862078404844242383 +a6735833-762c-1ea5-2687-7db5441f2439,3556689665,1667111283259459970 +64f6155c-ebf0-ee2f-5bd1-dc885c438ed6,2444948549,17366018675746631629 +ea76bce3-ff78-a3cd-b2b0-22b7ee93ab6c,577881007,13834579944939966666 +7821ed1d-a3c8-73d9-152f-61b2060d014a,3294516897,12703642315598823709 +1e744485-0828-971d-3706-7e308919699b,4010867980,8161355005827611055 +835390de-4164-96be-f2f2-8d89a7814188,3988851705,14816691547453048035 +de271b20-dd37-8c88-1dd7-e5da4c049abb,375149452,1376181912238318648 +cea4b34d-8a7e-c729-2fa9-9fc674f812ea,713255802,9611152538354955086 +089bae35-83b2-4ed2-ac36-8bf455e6e312,843016096,12979347268752908120 +f6a54544-0eba-8f27-3d38-f5e440ce358c,2493047236,5459646168697720460 +6f7f7b05-ec91-1427-76ad-b75f840301af,2743459974,5776924768378945206 +3d8f6910-bd8d-d94d-2366-e8acbfa8a1d6,2256120203,12658295051274755100 +5ba00901-fa8e-672a-5926-8c85d641ec50,2004153297,1439025835018422552 +0152bca3-48ba-7ade-5610-2e07ab6e2478,3222006771,862059780202836791 +e0a2192a-40ba-3372-7071-452e616d8cb9,3990837037,9967290298896638480 +8521ff31-6f68-ff99-c517-17d54eaeeced,158974789,17327080194593380107 +60fe4d25-7937-311f-5cdb-9ab29ecdc432,847203228,9023569611644809472 +12206694-f8e6-24b7-b4b8-a6d70dc24db9,3462327591,2890918797678142697 +1e31baa5-cbf8-a313-7051-c6e6718f8dfb,2686841141,7266176433452091522 +7d624f38-84dc-384e-4d45-2bee97a30243,1609593403,2599099036661909315 +317c91df-404c-1abc-0543-ff3a7e825833,2124249505,716771314889291996 +b561d795-8de5-fe36-d237-35fb66c1f28d,3031694774,3878453565145982768 +9110901b-f98f-aedc-0122-1868efc4e426,551687055,9392119324438780543 +094a47b7-f785-186c-d029-63ba40486f74,4194090986,11568075665849854024 +d440a444-68dd-9935-7d77-d1c86eadfe75,2764439127,9949830173691462480 +74767cea-75dc-5f77-0598-4982c7cb053e,908561496,8487992061026713682 +e75e75d4-5070-40aa-8363-32315fee192c,2466975741,3498602671236392024 +0b71c3dc-0997-6405-68ab-391472a71dd4,3191773018,1784454353624691213 +f1dea62a-2ec7-9730-7175-8237d0b28475,4155499382,7104450628875899148 +f8068224-f702-aee8-be76-7c0c29f26aec,3075183983,15168106851175171718 +c10e4963-64de-1f36-a9d2-8b23deb7e881,3555418556,6498865381434129150 +f9106ef9-e703-d806-b522-7cca25c81828,3192517058,10465331095780942892 +8f91c055-6b8b-dc3e-43f4-e2a8a3f9a887,2584687299,12691644596326764997 +9c527418-b313-357c-b307-aaf55fede4c8,1082611897,964420230494665534 +99e2c90b-66e0-1c24-5f78-f1cf986c1892,2593277538,7550886057054107962 +7463f12c-d748-d5ad-8da3-9cfb91da8d37,2621289617,4714959209222257397 +f98c52ee-b96f-cdea-962e-d9d941c133c9,1557269578,6573493581374948226 +a6e96439-c246-1379-444b-b150f61e519d,143958886,7389169186175578219 +cda55fef-2445-8784-0cf9-aa9646cffa03,1999720827,15849674240839869962 +ff9342c1-19b5-d3ec-3098-fa238f287d73,2204194357,18380923503067505748 +07a61378-c21e-3f4d-f860-b87b9e3cd748,2863803453,5104116852097621288 +7eef1faa-75d5-e1d2-85ef-45b30382c6eb,3766948095,1056232514853765359 +2ebdc646-6ff3-4883-c992-2bcf336c9439,4150232770,4414627474872470658 +6faf81f6-c56f-85a2-6a1a-ca46d1d61fe7,2443383340,10758643478392360701 +1dac049a-5e78-71a7-ca5e-e6ebc56eb52e,1788911630,4115636219151899238 +68db9e6d-a82f-51dd-0a80-63bb5fef0bfa,1007055859,3092519059961815710 +97022347-a51a-14de-0e37-006d62787ab3,3178116542,18000002083474674261 +337e0801-37c2-4d0f-c6db-abe53ae6f86c,2510166240,8875163071390969238 +4b1a2634-e1f8-736c-2cba-9f565208bf7c,2156484712,8828258946378839947 +90201e0f-c4b0-0e3e-2f2e-ca539cf4411d,3410128550,217298918731735209 +709b2a20-0e72-47aa-3d4a-f116f2ed0a93,64114199,16973082720020268641 +edab75c1-f72a-9d27-18bc-81d4288bd6dc,4278766713,7504601026521795340 +e81904f1-6e21-7d53-c7ae-2f12adf8618f,1636536697,2097225226140944272 +466fa2e2-fac6-f96e-7bad-64fb60dfb101,4112314927,3909710835143224553 +cba5d54b-72bb-77da-5ce6-f23487d71cf8,3347903223,2203480499011007263 +d01a38e5-917d-8d09-7275-28a80debc2dc,3840314293,1080112532102562124 +791068db-249f-36d3-ffe2-45c719045c35,1985535586,12207251722981029301 +399d7990-40b8-d417-4f0b-89362aa6bb3a,95771147,14519993097352290291 +3f52f39d-db7c-6883-5114-17fbe5ef3859,583614516,3717030991943354554 +09c9ea15-d2e4-6986-4a6d-3f76d4f3cc16,3517606534,3655363704712745029 +62d116f1-6428-fade-4e18-14ecc678dd2e,1685435193,3742221459747113843 +ca9e0759-edb2-82d8-cf22-7997481f55bc,1819221761,11097103318562836039 +9eaa01f7-fee4-6ef5-f4dc-d58a23983c4b,3311567671,1549458141190815373 +e821ec75-d75a-d4b7-e7cd-3f9d809f129a,2370669425,15308231924419430940 +9ed411f2-30d7-fa3f-c63c-2a175eeb9ccb,370449596,659584561764918447 +71094712-b074-d428-3901-9a236b9b11ce,3443662831,16560622513058446462 +e284c83d-56b2-ab7a-3217-673ca91826a3,980666592,2945617826540914462 +0acf670d-2e08-775a-71fe-7078abef7c81,61003142,17149451028352684338 +60aa5b49-73fa-a56c-2f8b-1ee284c05092,344835243,15424460145249850194 +b7496c6a-c969-bcb5-1739-afbf14782e12,703060003,17505291004624901880 +fe4f7990-9caa-e756-2abe-e2e15da28e85,1912864858,1369096618103869125 +73505862-648d-ad0f-a3d2-27dc5c9afb96,644765962,4307971107218034991 +d236cdb7-c7be-4159-0ddf-d4268af182bf,2864135906,10297700418152351655 +0ad25301-94d3-19d1-d361-11428e3247c5,802199655,11984236121660213820 +ad64b819-d9ed-54c5-2f39-3e46ef9f83c1,4007200666,6640137951829131343 +ae739947-54f3-f98f-3cbf-df75c5319144,1064535766,13827687589060240626 +00128e40-1999-ae85-b6cc-3f7c4bd5e766,429580465,15711565379283032072 +d9f95503-a4d6-5492-93eb-8dab3f144728,3297048268,11783237279692975335 +f67cc326-7245-29da-099e-a0b083a233f5,3523624488,4705641748059777542 +2fa73977-beec-057f-ec21-2543ee88465b,1705744808,8807717696709118900 +b06047cb-eb3c-c6eb-1ebd-c242177b80a9,3371176460,3182473970426153624 +03844202-9ec1-b153-effb-e61b5a4ad498,43170936,2799429086768291886 +c3221882-b3f1-189e-6ce0-a15ef38366c0,2489353177,11616359555048498255 +421d9ee9-b1a9-f570-90bc-290b74c3239d,3640494442,941266362484402172 +84f67dea-cc13-b35d-9855-4a461b752e5d,3402092941,3428207355091419200 +4105862f-8d9d-82df-6df0-46a3ef77b975,1229846202,18333332763611039263 +71f60cb7-8cd1-a24d-ad6a-94f7d5395fbc,1178819972,1597150117237407714 +e7e741af-1014-db36-86b9-32ed0e9f73d3,879426389,14170324737476031779 +7afb4016-ea04-a0c2-7170-4ac67cd682e9,2575837242,13566498398073036144 +ac412dbe-306d-a64e-6755-b20673d41e0f,1620532140,16356123158319079341 +449bfe63-1173-7f3b-e59a-387e28bec827,1456439985,193026073313765080 +5a33bdbc-14c1-dfe8-a3a8-6143eb716393,2166205195,10707954834362819684 +f6b7c491-e0ec-44db-cf5c-07afc4e44d78,2425626654,11820464814106015841 +5a33ad26-3d8b-fd3e-61bb-fe4d3739b970,1449914829,13462978930976812380 +8b243c1d-8830-0216-ac77-c229ad67495b,144860493,3641257944501598496 +7d76e03f-cca5-cfc3-9e6d-75eb53f95aea,1138965246,3079948719625405857 +7edf3c2c-db72-55e9-ca72-126f862fcd6e,2969530790,5524854851385715688 +9da2762e-b123-ecc3-f091-d734108c3a75,3127472011,17677463381981632048 +89cc52ae-b383-6b6e-1df8-f69352e47e78,2689729171,1626119558766528696 +b0c68d31-0e23-10e0-3d9c-fab0d24559cd,2307841886,14460991243215187375 +627269cc-07c8-bb96-0231-288848984142,1629357385,14324816840145381022 +98a59d37-9c67-4af2-cf7b-9f770579f3a1,4170634948,3507346625547610737 +36011608-a7f1-3965-2880-a4032f41a4a2,3329614769,4622432961792904757 +24695cf8-cef7-7c79-07c1-2c460b2a148a,261749767,3144736898934427605 +408646e8-e2cd-9831-b58f-5799fac7e24e,3516201815,7741558980700556674 +8862fe5e-a421-f980-2c45-a074c7c443fc,992452612,5990745087535943202 +a233a4cb-5670-7919-6d42-55e21882c551,2254867818,14946731137974277957 +881091c9-c6d7-de7c-9fc0-c4d138989df3,771352139,16689713935869930345 +f8978662-4aef-054b-3a54-f74ed552e270,3507585666,3422355849700624353 +b8f7f8be-27ae-5079-7f8b-43097b82f1d1,3260483628,7126993604729735175 +d4aae69e-d435-6e62-1b6e-effe97a94bb1,1935672043,13403568660277801964 +bc090596-5189-9722-5785-d8a7e0e03466,1384596130,8105325427509438478 +c8e47ddd-c0a2-6c07-7b0e-179c3dd1b666,3336198738,562329169694166400 +a67b2ef8-0e08-a3e8-71dd-421bab0a0191,3833486075,3637616209598905362 +dce2238e-1ab4-b19b-0852-13532abb4357,3813238197,10727985609704243515 +1e1bf847-0811-1aec-d91a-ff8d7160e7ae,1565999696,16460024825476143553 +b81a1d84-541a-ed63-e480-74dc3dc48ca1,3326333273,3810020892707009859 +85d1d51e-6ed7-a2e5-88a0-01d5399aa992,504016735,1108060325826747237 +9791cd5e-6c34-228e-34fb-20d95ff86229,3659128627,8988022275834972705 +009ee582-d618-fdf3-9c16-b573c553aec7,1086450741,3780870476970939947 +8f8419b1-bd38-6582-7fee-408bba36399b,3469021863,11591012114874938156 +8b9df9c7-25e1-dd82-7984-0efcda32fcf1,1472469214,15201900727766499718 +9101e0f4-2695-a46d-aaf9-d981c420bbd2,3106517258,13249399934879594223 +49d6a077-5550-45a3-cd84-d4ff1b8a35f1,1526082005,17151082470220394231 +e824c2ff-6f1f-7f97-7c95-410f0dd8101e,158949161,8565123733905425454 +1bad501b-dcae-99a6-aeda-44e6547556eb,53941132,2910959929606964364 +1ff0d842-3c1e-0dbb-0a29-5ad127645feb,3152241619,8911059649043312203 +a86f43e9-8004-af76-198a-eac5615bc2cc,1007420585,17635214340364324652 +255f02af-d77f-69e9-462a-8a528f003046,3424167274,6261165982669475993 +c1057f82-a5c4-8725-cb2d-4c72da236c3a,873238149,1914009869498223225 +e5df2e81-f040-5dd1-520e-1e4a075dad23,3487247887,14863182726763888111 +cec4a2f6-e393-7c5a-a98d-d8448b736ad9,3487005497,5213164118942716758 +59916883-526e-9ce3-d4a6-f1197ef29a1f,2626638656,14403469155649573423 +09fa30c1-ef5e-1f24-ca67-4f4a20a856ef,4184424646,2826499667849142030 +66a75c81-2d24-511c-852e-4df95af71854,2425530362,55132027562305420 +755d81b2-ec2d-9b95-81ae-b03d235bc6df,1644868929,3238830687373132275 +f0df873d-326b-8175-7052-74996eab508b,1411453877,5049118996526127097 +e760b0e7-9ad8-106b-78d8-37e5ab71d62c,2632183850,890232667454844790 +bab4229b-0794-2bc6-9986-b0a6d9747a45,3125993518,13776442063182155161 +040047d8-5e7a-197f-6986-6645b7d7c3f7,3102907944,2653440687812910194 +f4466d06-f2f5-d344-0d5d-630871bf3707,3210805460,14380423803169391972 +ca5df9aa-baf9-9ff7-51b0-bff0dda2b76b,1599044919,2356644685510862888 +1fdd075e-a737-f500-a0d4-e12b6d2e9641,1918458217,6749314431782950328 +b18e774a-6a17-8efc-e365-7896af6dc545,3284364304,15353053736071182603 +fa4a234d-a8f3-8c5f-fa73-987616506a93,2498915953,10543060131449964682 +536e4a09-c27b-6452-9d85-9ef465f39076,3494056636,18155647440179106666 +6321bde2-b4fe-2486-11e1-44dcf5a80845,3582202986,10929899634431728121 +40fd4d08-be03-cf11-7763-ff1c35d39cf3,3025403131,793770325850707213 +3e648111-a330-e05c-dfe3-e1d0d3add39d,3060244643,6069655314122879666 +129c66c5-195f-52f2-9e59-4d9de79f54f8,3744839163,1234804558881417728 +2280f361-a433-157c-4477-2ccc3eb6aabb,1826115330,1101455873462969190 +e913c0e8-42bc-dea8-ccbb-ef43bd40f334,473081743,11752008793613624604 +a2077ea1-9533-f2d7-65fa-d990231da279,2616653657,7205976432711009709 +ae41dce9-170f-c74e-bdeb-e397e151c3e1,397698150,10145862011890265677 +c2c3057f-b8c6-9d6a-627b-2e70f4ab559a,2700122201,13510174420647205363 +c1d20d59-0b91-53ca-9583-ad2d7c9b08cf,224968462,15329270839151647835 +677e7f2b-8aa0-23ad-9ad6-74d93823fc82,3103861670,184538232608587042 +a879186c-2eee-4da2-8cff-281c53538692,4183465754,2635936208863001055 +a4cfdd1f-67e5-f95a-95dd-9e3e7124ef20,2781160522,4726564156711827238 +bf417f60-5724-294b-9e25-37161b023b23,1545719333,2687722065610460484 +7aa0423e-7a2d-df07-5ea4-18d9868790f9,4120819848,17434162811485113237 +dc774f31-6275-ecb0-54b0-28ec65c8cc90,885212348,16386089035015301802 +4df12cc9-feb6-6ada-ed4d-1bf1af5f0b36,2689301938,7781170555577441725 +46689602-be97-7a45-ab53-33c6b94b0cac,4164395278,17024222499145599694 +35b498c5-fd07-2581-d2ea-477b62ef86c7,1455659610,12151074270451127422 +c6f44e26-f153-cf6f-b567-9351435673b2,3273520193,13649823910146622207 +0ce07f18-2393-f029-6aca-1c94204b0866,3658957104,8343155080245521948 +c7424846-8d48-5efd-84a8-fe119d230ce0,3155461545,8956821812317511430 +e864fa98-c459-47d4-e1cd-3fe5c743ad7d,4292375077,4687793495698748724 +340ad911-2345-5dcd-cb9f-5cbc3cb5a3de,1482373833,8898865296319992394 +bd85c509-8d29-305a-1829-0111e46dba6a,2853878530,9799546528396423473 +5c7d6e1b-330f-ffd6-ede4-5b30987643e6,2434479615,2851206976834601997 +e212b436-9636-8fca-f98e-8091790ac7d4,3674309184,2806467082826107988 +030c1300-643d-e28a-5fc7-686f863f03d2,1515727402,10922962103203590400 +30ff3b08-1ce9-0eef-3308-b08e4a18719e,3649814611,14425340555417123607 +5a5b63e6-c176-6989-60da-03b46d37447c,1950602984,15641831923978449651 +03a98a1a-d456-ad5f-080c-188cc2aa00fb,209521430,814795053825946877 +18506814-9c76-2b20-57fd-1e6b4a15c0fa,2469939339,8396624057633540533 +cad76ba4-e52d-885f-4798-d2c432ad5dc5,464621347,12065446847335453099 +4f1727e1-58cd-50f4-fe11-0b5ad38b6bb6,2396105313,18220588195611477703 +89ae9bee-12f0-937e-2a87-6b8239cc267e,280741789,13524112486399747668 +9573eb18-024e-f645-7d8a-7633da2022fc,1773819657,11420565302713276065 +aceb0f1e-f4a0-5caf-923d-73152dc433ec,1436044498,15032999593972496127 +da9fb1af-18ff-2fd5-dd9d-efb37e441578,3450778648,3524653648009903631 +10b0d2be-4202-8242-64fe-653ec2e57955,1830927628,13713996915760028431 +bc643f87-c709-f733-fab1-7cd748b446bd,730184124,1347413792212389333 +76f3cf26-1b95-2090-2153-530b75101926,3382323934,12256598008290836475 +21cbe4ac-39fb-af61-0f03-4cd0caf1007e,3817741278,11336782605944941377 +4315dfc5-6499-6ee5-a0fb-0c91e4046662,1990416026,12793654624343103232 +e3e5acb5-1cc8-ef0b-e39b-c06322ad93d2,2221763135,7375231567385402240 +350db7de-6d6b-22b1-4fe6-515dc6f0c77c,1979341311,4441643597509921631 +cb5c4b5f-37d2-177b-d9a2-52f81b9a67b1,3265328492,9927620649708357351 +dc46706d-244c-5be2-6b8f-50e6863f89a8,1833712575,428185464103213599 +25bfad17-26bc-c5ff-c2ce-3674e9590766,2850160706,12731250065961268925 +d0e68d16-e1aa-254f-77d3-9f51e12d51a1,1151927103,10932978211569862064 +ce194b6e-34e6-bb8a-ba5e-62459e12baf2,1395571504,15261426652389059236 +5548956c-8c9e-f927-a394-a22f05fd8a93,2153141591,17104102983962313480 +371fe22b-4a91-7c48-8b06-3817c017208f,3845544299,10940269024989707884 +6ff1ab48-b125-5e9f-846d-4c07018dd640,2707083180,11623220289507585174 +95fdc695-bd36-c7d5-e527-1073498f6eb1,1037455247,3944829015958298751 +134af459-e9f4-b645-18df-be98bfc24f0c,2850635240,8819443328483070922 +ef9c4e2a-4767-b464-320b-569f3400ff41,375371959,12120823009489514308 +be8e5b5e-7bed-ce55-6ba2-5b3a8b08f47c,1888760830,1303132941346309814 +3af5383e-bc60-d725-a6f8-69bdd914b6a9,361251563,10356885399580792764 +00e0fb42-d6af-d533-31de-abe6bb3e6e7a,3512159229,933803314595144417 +969ae40c-3e08-b83c-5b97-5273db0aec14,1313648161,11389258490632533073 +e4b32002-ada6-dc41-af6a-59a9765799ab,1805234138,2432519129972351049 +16a74b89-c60f-a0ba-c591-1e0237c1c12f,3253634273,7557934846192163225 +4d79576f-cea2-594e-83a6-bd8b228fce35,2912893876,6717565859466252608 +2d99dbfd-b652-76a0-8bb7-695e5b413115,1778945600,2866332013672683627 +3db4bba4-61c4-3ac9-5fdf-3b22615c13c5,2599058192,15229360321804657339 +5b4a9246-8b1b-a124-2814-03174d7fc041,145866626,6880596433441283691 +cc0e50f3-4146-1bd4-9233-ec36ee5b1d34,1119057367,15462534050356757214 +e9e72d5d-4bf6-3b0e-700e-6ba212b1cd49,993767433,15541592533330439464 +ae4d3cba-b1f9-fa30-83bb-abee4c026178,3891569941,6553176116934044189 +3248203a-aa68-45e0-c2f9-7fa3492a74db,3724622262,1395743230420506626 +f3e8b497-5ac4-0cdf-69f7-8517e9ed3b70,730747469,3890671848059371303 +03f21172-43f7-a592-1020-fac57be87392,821645839,7380110057886477041 +a9105b88-87db-f784-1f92-1f92cb1ec4ee,1033738678,1424519326209516513 +a278e43c-2afd-a5b3-cfab-2dcfbd13ea3b,174940669,16870422721434556582 +998f4d4b-944d-0d91-a915-9c3c33ef8b51,1483599662,8459943866482797572 +4a11d38a-648d-3bc6-4d3a-0433d25bfe1e,4279550697,1463788456140201965 +63913eaf-50c1-c939-7b40-ed06c873d978,2638844500,958001614613833278 +e40288ba-420f-3eca-ea65-747e283c5805,2723465340,17431971594695024061 +2ffb745c-a246-a885-0abb-3fe9c9bb44d7,3009028382,8458639321225032604 +d84c5055-51a0-5fc3-1882-0ec82e330bd1,1109248636,10094794096948450091 +01f2ac70-ad09-c6ce-7b53-2b3203a2ad0d,1852814102,17239809207646262261 +15b87474-787a-903c-cb96-976353bfd3a5,106158967,7849436950795647673 +b8ecb84f-b45a-dd9e-a6e8-2dfc33a81a4a,2817474282,9268325811079695860 +5d9d33bb-9b0f-a8ba-3da6-14a11323aee4,3738997524,16444581571118829640 +3f015fa8-7a42-d8d3-cab2-c92aa51c90a1,690276500,4185692562891025811 +bca75c11-680d-9273-62d4-c7be092da743,1682013639,11110732240860110162 +6bd5e12e-8079-a06e-ff8f-852c1f114e02,3753039167,14491553518912800413 +a358504a-e4af-a5c2-1a96-f9993fe86280,3204647840,10278693056139699233 +14376f07-a36a-565f-a8d1-4431df321b5d,3847240466,10125972825174633258 +664f6cf0-6553-028e-765e-0d76165c6daf,3819097917,6940193890925812504 +012b5ddb-57b1-a727-ec6a-3f0ee94fc304,1429234806,11399571889044843553 +4aa79fa3-7204-487d-4fff-ebf24f73ecc4,3776935181,3517415554842478545 +3ab0a68f-9a61-d496-e708-dede0bdb1555,4125535701,6548163573781624518 +8b4cb7bd-7913-3e5f-6fee-53d3d285f9a0,3396142561,17435599089737460822 +617d9bbc-7db3-9a76-9799-38fdb6946f4f,3136519114,3096151183532879179 +7b54cd44-790c-688c-f53b-bc0c27d9cc1f,2057355420,16142800722880256418 +4248da47-5f75-6719-37c6-98ebd8e19635,4059589177,11562071919755625116 +f952ac62-fb66-ecd5-209f-8f530f9e9bad,1932309537,6032918044720650924 +f31337d2-c882-afb8-d143-366a2e9da34d,575953812,14397462403033143056 +26d1edc6-136b-915e-56cb-f9ec4777ca25,3812041996,10918129730523250590 +b532e3d3-7508-010c-63e3-19000638ed14,912126139,12233641943375076383 +976299ec-59d8-6527-fc14-059d46a8859f,3942371531,15978867716684747494 +b3f7410a-d648-4536-928f-851c69f9ac02,678817640,14156897124741510308 +28a4fbd8-1025-5646-5ac8-74b74f98446c,1602411147,5145018501280881874 +aa9af9b1-5a70-ee3d-9d5b-99b81dc931bd,944161675,12025922404250227161 +75fc93cc-a111-6fbd-ceff-fcac1335e0fa,1060818265,725064747705708150 +56ea2719-6509-0aa4-d402-c149a793cdef,2269996401,7736033015185792291 +0bc3a642-027c-d551-b1cf-a9cc72dbcedd,3961230970,11754359792284216450 +7d99e36d-c007-94d3-dbf5-2301fe7d2bb5,3941972233,4427798406053023009 +38121c35-6fd2-e77e-95de-293195d04605,741493959,8488763986428687488 +78ac5fbf-80bc-603f-64d6-39558592585d,3131828210,15540351012893019670 +92f56728-3f8f-60fb-2fe8-fe8f1ba17d6f,947653937,11412939054294790519 +0c89418e-2179-7d9f-41f5-4da615e7491d,38742437,8820541958584726050 +97151af3-18dd-7633-c07d-6f8b20e2dc85,1068865241,15268189237978315215 +17e6d07f-6956-3092-9253-ace70047770c,3681396101,17879684305897669179 +223d4f18-ec5e-ce31-8abb-83c3c5357bbd,1701248044,11459347313979827718 +d5f00bc1-eb8d-8555-f2dc-7872d65d3099,286102102,11787346730395216651 +f918abdc-97a9-4230-6b83-82d079d3896a,436437449,17513862906311128790 +52255c63-6823-a7a9-43f3-072836fd3910,859808030,11745734543815932627 +0a70ece1-843c-f3da-169d-fde15e4c485f,3666882070,5433290514821774600 +1f614e98-38b2-1733-ba66-d22368229dc0,2131024278,6691487331719563922 +6c016c28-1330-d986-bda6-93736894a251,1303404265,18384854309424412773 +f5ee4946-0be6-a5d6-2cca-deb494d83bba,1947799319,13169053594402647824 +1b021fa2-8b05-c8fa-0227-97e040dac17c,4188366084,9096706431042362583 +d88e896a-a248-1eeb-e9b0-4ac0b75c04bc,15158010,4200921020133347889 +dc25b74e-5129-0aec-31a9-74c1e063e060,1867045419,3493506843761944118 +6aa8bd2f-39cf-96e1-2fa2-f826eec6c89f,4246899456,14368691828846865750 +296ec5d5-6de3-3f94-eda6-025a51055a06,950772279,8885131243269469997 +3dd3b640-7874-8bd3-5bf0-b53b2b8762f7,38803789,14419294447373381352 +f405f2c4-d9e8-b1e8-f707-81ffdf20f9ad,800404424,7014104904229693745 +eef645d8-a036-a337-b753-7746b9150e63,894636517,132347617555533793 +e51d9120-3f56-ab6f-848b-9e042ba9be24,92255131,15981518861233389918 +ac92c86e-d0c0-b9f4-e1f5-1f8a3a6f091b,274031172,9000137286661223182 +53ee0e0e-fd8f-300d-4f22-6129f2507c7b,3510153229,14833945511369458982 +f21da7d6-9897-e382-9dab-312a74bca830,611101556,920136717987279975 +b481ccd9-6738-38af-c340-e1b94c5a5eae,2714483081,18422121301976733849 +95bcaad9-1268-36d1-f65c-d024a8cce9ec,3817953966,759722222533656468 +232f5bfd-ae2f-b1c3-df1d-3f6f1df5527b,3359877268,5090776812297702926 +13e0f38a-b7f7-9b4e-81bf-ee1c9624d6eb,1056895358,8472003850873447661 +b9897a5f-638e-eee2-0139-cd20269a810f,1564829365,308657606487147506 +f277cf74-5679-b6ac-233b-ddaea24292b3,218911832,11330572785563292467 +4c139998-ee46-6def-6fd9-d6b2f5bc6015,2034083153,17947788755292007853 +932d9e09-9f1d-61da-9997-b2d608d5bb94,1435515982,12886600847008110712 +594ba070-0a3f-871d-307f-fc644e3af3fe,2346635948,17388576265406960898 +0891ce45-b768-4a82-0526-ddae4361a1bc,1408598279,9229391376289070507 +1e6f84eb-2a7d-1a83-1a57-407f755d6c10,3874578679,16936456230531673551 +398ad8b9-6f62-db84-07bd-2a59e0b513bd,668843454,15120772138357662278 +c2ad0769-5837-80d4-018d-40eb03867c91,1584683505,17546836052033448746 +f8a84a3d-2e11-509a-f466-15238b8d19d4,1305293335,8325770835790717679 +c28cba4d-33f8-0bcf-bc63-0915220c5919,3409231045,8526365213056442856 +41cc53be-174b-71d2-f955-6c939586577e,1609289580,5775025551999642547 +e6b9b661-9627-e589-bd36-30fb2e056174,2714487191,2769421942089448022 +02749222-b285-f476-b86a-1ded35b31fd3,2213931564,931980824644077218 +2e250893-8a09-ec47-f128-fd846959816f,1336756706,3461839371317658176 +75197028-bab0-c1a7-b4f9-13b5ac228a82,1992543679,11276663472016814547 +0b2e4be2-0de9-a1f8-da21-a45ec70e4033,773522010,930711159473643301 +2f1e4fdb-91a9-08ff-eb7e-31a126e75b63,1146341746,9537179397913427404 +ec7c66f8-28d2-f674-8b99-7200ede9500d,3730409339,3325305319743798916 +e5a31a0f-57a5-552e-b5bc-7ffcd2cf71e4,2667539477,5024139610660894066 +fedf0979-8073-b761-689e-f7c61319f56b,1083984018,10538776132616351225 +f44fc9eb-b59b-1843-6d4d-51def24ed899,120536282,7359687351025634596 +27878c06-7266-15d6-0bd6-990615b944e3,4039144032,6805817469292964724 +4fa1ed4e-9508-b80e-e53a-0d2a7e7072f8,3425035803,6726729613534373188 +dd8594a7-bd2b-a39e-9564-968e12d347fa,765676370,1853042178817230240 +18759e92-d588-a1c2-9421-86c60382cfe9,2856630798,10435789719960946106 +6e5a5d38-1789-0c97-b6da-f2c4d4f54161,2347730357,8742467445199548285 +85334b84-ca65-2b6f-00aa-05ea3559c0c7,223037550,7221890505705111945 +abd0b5cc-77eb-811b-f38b-83a358fed08d,168385834,6979229511104190833 +aaadae23-1778-96f3-37a2-b9c397436299,439527029,3573428836896273279 +c7a6a2d4-e3b4-ca6e-cf90-7142e9e7077a,4156099507,12465836350074912513 +ea7114a8-2407-ceb9-265a-688ba31a3d84,855494088,10751992702676826451 +8771b8f4-63f6-edb2-f587-0fc04e9efcef,2824213166,12075679629798155371 +dac2f385-4c8c-642c-eb90-3efc900f4f80,742074246,18221303695194972760 +2b2de270-c693-d4a1-2763-5a4ab23a9f39,2710391103,13778257781847993257 +f2c20073-c1a0-190d-d9b9-be94b2c58b63,434232861,2812143274527742575 +382d8626-ca85-b9d5-5647-d08562952096,3101629803,3574302650244215242 +edfecd5a-6abb-5679-c3e5-23861f8800be,3998665839,8714975094337796269 +9e870e95-bfa7-abd4-2be0-06c4722f4293,3697098630,14727511847843646541 +be7f12c1-7e42-b181-98ad-038ed0034b4a,1674079702,12331857989474974273 +2b27626e-70d0-e13a-5542-8d78cc10cfa3,1736445835,16504785692306696984 +a9911e12-920b-d536-2074-89e469a88448,1886277304,6332893869055281631 +8f0d23dc-d69f-a32d-e39a-9de7e322dd7e,1085600410,11698332991227836115 +5fe78725-0ea3-10b3-a515-b6f6d118572c,2140510340,6738533017807652436 +aa9b54e3-cf5a-b7b9-60b1-5338e4ad5e39,3288863219,11423858663081848932 +116c7c76-0529-7202-e2a4-1f7cc3528150,1406282731,14783364500943691336 +87ec317e-e57a-08e8-2566-2061e35b4560,1337289336,9206717088051620960 +3338d20f-c425-1917-0144-4817c6e66a9a,3720612328,3864495478518003073 +d28f925e-2ed6-f74f-88d4-c7eacd4514a9,1303764152,12581804834983670625 +6ae28899-ed64-aa99-bc79-5c834f15a86f,4015104409,16796322089633974416 +82fcf455-7ade-1a58-6d8f-981741c39478,2661411760,5024599740489834490 +727adfe9-8f41-b1c2-6ddd-719174f9d0fd,3496238258,5855930566063655345 +90ce8c3e-87fd-be3f-474b-7edf427b659c,2114440406,10897491375023507395 +456c3e36-3598-f8c1-678c-554cdae9284a,1914459469,5396414357812739769 +ba386409-6d62-3683-70b6-790747de00c8,3825179115,2058297591586459083 +ac9c7733-9609-7a77-e3e8-400703412365,1736133990,691812420592433581 +7e06a39b-8bc0-1df6-3df8-cf79a458fe4a,3672297030,17699661904931054611 +4186b127-36f2-3e5a-321e-93ac7b848770,3627056145,16219763813131688557 +609b20c0-7e21-e672-10c3-cedef83431dd,3636687085,15019491963191053307 +9d3581ab-add7-b44b-c224-63faed299b5d,893297573,15285194029757722489 +f1667757-90fb-f197-1747-5f2ffb181f3b,3146259287,14166697143623523557 +111880c2-1e77-d393-cba6-09f00ba33585,2295101013,188129558239327689 +ee17aa26-cf19-f6ea-3da1-2a36876466b0,1140307474,1577784275784011123 +e88b77f2-4a7f-ef4a-cc91-7b7bc7f44bbe,2700548745,9924403691966059722 +0844152c-0a47-4406-1475-f7671a85f7e0,957297153,8551232005250675088 +86459034-81dc-9ac9-0d94-85028b4c0b8e,1746455992,7589580214792598249 +90d95c7b-f803-3f6e-330e-19fa7b688f10,3645307111,15774441038869487472 +dcfa3e22-9c86-43f4-8f42-0a6c880f0b46,1405590711,1894232030217030917 +be402a56-4e7b-2d1e-6bf6-24d0fa51b687,764741579,3806384122355149101 +78f34978-e938-ca7f-ca0a-c19154aa9039,204729122,7421108332092029040 +0156c724-e065-23ef-fc36-542d6271ec20,807570208,15696098764744238796 +6eb4870d-ee4b-9a7c-a89c-ef24da7fda37,2241484859,5453931635431119217 +34de1fd6-b124-9d60-6576-05562ec96c1e,2196131840,14931766493503802513 +d4d3a791-d545-ef1f-37b6-a2d36d7579aa,2209019700,9373393965209121655 +2080bc77-dfad-e387-7c1e-58c52818f236,500767155,16012864425157306928 +3d835334-e175-19dc-0394-08e210d65f0d,3968536570,15060954935064613497 +e93ab39b-c5c6-7dd7-8da9-c9e3ec79b4f9,3142642103,11344716804023041511 +b210ef66-2040-51b8-f288-016c63107d43,161405562,11146674318132447217 +1707795b-90a7-c768-7e57-187631def87a,379503145,1283050992554604134 +3491b8cd-967a-26e6-a52c-5ceaf88c0654,2851870839,12752157977207921340 +986c98f9-b62b-beb5-564e-fd8da0e043ea,3985954956,11023580560190042661 +20564150-7476-2fa8-194a-faa7820b69e8,3389012758,5815275338871851796 +d79833b7-3e91-f916-d89f-b70df6c93a2d,1853722226,11789712496936340609 +e018ecb5-79cd-afac-fdc3-a87e1422b8af,2725090328,16363409315690472957 +232c352e-b680-3e08-cac1-e7ae6dc4b7e6,379729144,9873043167208829163 +73045216-d82d-eae3-9dc1-b450581c5820,1234125242,2770687925515306281 +61d82fbb-03f8-258b-ad55-ccd69a8e643d,3468294714,13647241353060342608 +6af93d66-9f61-7858-2753-dd877a995136,1776014368,6904938807707805033 +0061e0bd-249d-3bb8-d39d-e051a88255cf,1579914694,6056550262750088658 +a9ce86f8-f51f-23af-e2e2-7d280850abb6,2824940656,2972941913340014780 +3b151284-dea3-288a-a1be-8783f88a4425,3475842710,10732832076719751598 +06acd548-54ad-833c-6660-1c2e524396b1,870415615,18306101414301946661 +fa16fd9e-4757-544f-d3af-bdd21b03de40,845443283,13705926744482952751 +c75e7980-b974-7817-eb47-20b90cdc9098,4201257145,13027228100503345195 +c9acda30-386c-59a3-5bd1-83fca085e7fd,3693457726,644221217494037086 +d960a62f-3367-2f44-f2d1-f74c08c7763f,3739250832,11885537686094933400 +4d82d738-228d-30b0-4e4c-0d8d9ae0689e,3415506986,10528154486635510151 +ae07b038-d21b-ac53-4f0b-df185df1eed1,365796366,7301359801562431434 +87e836cd-bba3-4809-747c-dd103320a7e0,392620382,2025385338492103706 +55aebbfc-0699-75c8-d53e-5118fa353eb0,456276037,17574422680240349658 +86015482-db44-a9c2-d2ef-940bd766df38,3364645918,14573013657410832506 +a4f2709a-950d-3659-b1dd-2af03562b481,483426356,5420081283077793850 +5a6a788e-f337-bae4-df95-4fa6e764e830,385349526,4214551394913009528 +d31ab75a-723e-0a14-9971-ea4512d75f73,51766107,17988551999872932882 +68b942e1-b8a2-15d6-2d60-26d9b50ee8a0,2569525672,3028733955748948525 +d8015d2e-7a12-9e45-e39b-ffdd9d4e222b,1656853339,14668616929565325700 +a72aef5f-11ab-b176-a99e-41c02437a153,299901285,6187614847178968085 +abad9a17-5638-c0b1-3d27-ca8eb7c35be4,2722978516,9715378083584794205 +b0b7118f-e4c2-9186-bc14-68bafc6c0756,2267188483,4856617486755762714 +127060db-2771-a006-f2f1-ce7ba47f6614,3856378065,13950922339705332492 +a3e95698-de91-0fee-6370-9c6316ba4b97,167020262,870935579042230355 +b153a5c7-2a39-2b63-9316-3c29d21b72af,2111068489,8064898457532835967 +02a7de2b-d03c-7931-01a2-1a50f1d337df,4059331619,5847101610638165384 +e6f6a92a-2562-361f-ab1c-cab96d5db05b,1690548571,9888734499062577982 +31b11311-f0c9-bed0-8f66-adfc74006e08,957161712,9374800910324760694 +b461ac2d-4bc7-4e85-d111-874395314367,1583435102,14736949406118669229 +140ae276-e684-a212-1cae-923fc7127767,659027254,3624348005702920622 +73588242-1d43-60a7-3952-3c137b31f38e,3369584419,1570931921778732927 +d3c0c356-43bb-081e-6813-091eb10fd4d8,2712415452,4480279891650075621 +5c8449a3-fcef-6f91-3258-d5c71520d37e,3657104927,7339562039217857585 +0e36e595-9386-60ab-3c68-e39d2bd85005,3678130520,13531542845950217173 +ddb173e8-6ec0-0c95-208f-522aff353db1,3125692093,1098427452298468220 +2a65ab9e-7223-160d-0926-f1d3a909e173,818125161,4024614496838609828 +4bb99aa9-2640-247c-1cb5-043d579b8f7c,2745461079,13704478207812862410 +ba0cee22-080c-c3da-3f71-e8b5ebf0e33f,4112874287,9949813229055806299 +caee0f0f-acd7-5b6e-4d67-29f38dd8bc17,2227924736,13624020932465769823 +a88fb0e0-dc21-fa6f-80a3-a0b0a0ae5a88,1422343473,12348553753772177617 +b29972d6-bbc5-9dd5-efa2-650d242954e7,4156848872,334215187267111234 +03c2cb4f-b97b-8d37-f00d-30d56911bcfe,3739546364,7922519345903615828 +084320b7-c62f-e68a-848b-8f0c5e337614,2749449506,10472799866978375801 +a1490b72-bca5-409f-d9bd-3601190588ff,3266103912,8094795926430947565 +76ee53af-c0ff-ef2f-4bc7-8fcec2af7b11,1108154729,17514630686061304207 +67fe21b3-1be5-b2fd-dcc2-6bdba56e1cc6,4222423165,9780352863318645733 +e8c7fd53-6e8b-57b3-e136-d824d710ce52,476670031,15872004695726123448 +ed05616f-e706-4854-dda2-24ceb3a53255,267759139,1921579470330492157 +ffbe723e-896f-5ca3-3c02-81f3db4e3ff1,1382308493,8039438505694973400 +3a2bb960-7d81-2fb2-3415-b2f851ef235a,1756500620,5566437370509890868 +d6ca3786-1078-2da7-0c7f-d6bef691f6a6,3977766051,18039067934697588693 +e90bae57-7252-3cef-a9a5-492c7efa0581,2416133995,14829100285354020250 +82d6b483-71cc-f7cc-5197-365e78c6e7ab,2768702102,13364079932388710706 +323ac3a7-000b-cf92-2bc1-86b8f0e970c4,3425535740,7633045036844695373 +5b1e5d60-f472-e787-2995-d0a8865a862f,3175639449,12260377664437052566 +a4383333-f51e-1186-cd8c-cf8de1163dfc,2154328556,6839161782790987018 +b3b3f5c0-5f98-9061-b98a-92ea3de7d651,3776588203,4969051260656149745 +e57f4272-1d92-6af5-6170-16a0b1ba4742,4261281202,9743163314343660434 +70774805-fd6d-1713-12e1-e43d6cab308b,2881556482,12382393072678014443 +0527bf57-7831-e54f-d46a-b746519911bf,2806388479,3016905067439994053 +42628eab-1de4-e581-fc68-c85f37c18b0b,2205691939,17336177853995018536 +a0676b04-464f-def8-d55d-c9e5cecbfb64,3477131063,8650046348123730203 +9c1bf4d1-1d66-9721-73f7-7cdfd8fc0611,113650412,6357036629533752245 +489c1721-680d-a5b0-4c9f-0b7d48d7152d,1774885155,7135469457593732095 +b3935d0a-36bd-e30d-2282-bc4e03f0343c,3433266336,391812421596007411 +4c26b7bb-ae02-4eaa-6c46-827088fea47c,2220122072,462147139740005741 +f3169c6d-d155-1fe1-8772-6e6dd97c7051,1261676747,14938871935395447814 +6df5ef5a-aa15-a35a-7ebf-642f0d900197,4023417802,10234213386866311446 +118a18cb-5928-ba44-409b-3be63659d316,1382732681,7080073099444400373 +f7ab3b74-3527-ba84-fe94-52222e4cf945,2380131244,13252502334582721169 +e3cd1ef6-5840-9877-d0d8-1e87e9fc857f,252278434,6803187350263872672 +2046f380-aa25-0844-246b-419f3fdc0c91,383817809,8698033087457151099 +8d3a2944-d134-a366-a121-5060d9825deb,442653443,6653806389056870323 +2f9c8a13-6419-ae54-eb83-4d18ac695b71,3781790513,12038904346539997932 +baf4e9e5-b138-4dbf-a1ac-a1729e6b5992,2851512277,7974754882517933130 +809cdc63-83eb-16dc-1b35-c89847ee6e41,2149192073,13296904944613810914 +438db9cd-3d8a-b51c-9a13-70c9a79ddb9c,1810772228,1872391856267407075 +a00b2aef-541b-e327-c2c3-413eb17a0384,3621868574,13334273904481383486 +12144b5f-4ed2-a95b-67b5-b3ed1811eecf,1255557198,4328628153905407752 +6fd0667f-5ef9-70e2-4b48-ff90efa25818,2849607869,9659706499751308155 +975763db-7157-6c33-cada-5f76aefec41f,1441016749,14933665568586372485 +c2664d67-00b6-a97c-35de-ce3f8e64874b,144559624,4002418836008844745 +c58848fb-29a0-e759-f3ab-3b8f9300506a,3347479713,11619878019189727901 +2d3a4ebd-b9c7-fa02-7d31-101ac037e21a,3902714262,1447136482313539936 +1af8e34d-57bb-145b-3e3f-d529efc5c48f,4107750315,16186876339803131118 +2a0363e0-2bd1-3936-ec32-b77f15225ff4,4193737124,6439362271627938389 +eff9b0ae-b329-7fc1-6018-89dd239538d5,1639484630,16355991047142669078 +4f32d8d9-050c-fe7b-3da1-1fd275d3034c,266160079,11373090793005416059 +358f475d-0ee2-c1c3-fea3-001eba1b9ea1,1982441347,1730698880426609400 +211d5b9e-5033-bb12-8596-1d32f0e04775,2094601242,3694745007456927382 +9354ab19-e53a-4405-e676-fc356f88c041,319290853,275784940022434239 +ce85ddb8-feef-64d1-0acc-b57c51847a78,1029996292,11244692298842349178 +18eb8b5c-bae4-4e2b-7be1-9f133ef62ba5,920718878,4103942603730879386 +7b240d3d-d418-c8de-65d8-1ca992288b32,1332349839,1133974504561707846 +8265fb5e-b941-da20-1957-4da85f4d17fb,52034351,5401801592043490298 +d605412a-c023-437b-d469-d9136306a774,1940461029,3394608342004163685 +69bd5805-d0c5-29da-a8ca-5c27f0e2f94b,2595986031,1801460603231132290 +e4c68341-f016-0b26-1572-c7590948f2e3,668997237,13749307046152286845 +5f905e48-e6f7-9dd2-0a21-ded05abd8ed0,4096405508,13279813812022454358 +5cd7a095-6ebf-b73a-da3b-fa683075bac6,2105210595,526080090740035553 +9961f17b-07d8-4009-3977-50156ae43df3,2999458877,11579877051951439341 +5d63aefd-a4c0-c7ea-6d02-3c6cfd607b4e,2628814659,5834862844001997897 +6b57c1cc-7bb8-d0f0-2de3-d912c2003179,3201658715,17193224522725331028 +2b574264-c62e-3da8-c659-b4f27a1a1e18,901803126,12580295249774550945 +aeb2a1b9-6365-373c-72d9-d954e5ba6bf5,4000675839,5366088685709620157 +2037b857-9877-5e98-76a4-fc738efb6dea,2104424776,16481453698383873915 +b59695e0-819b-97d9-d476-ab274cbada1e,681140010,12353715187318451198 +555e1d02-b0ef-c8cc-4966-eedcbf6721eb,52085234,3144061184987881087 +e542425d-672d-b3c9-ac8e-9ea18044c9e7,2656697539,15500475674272365037 +acb805bb-7b5f-fdb3-3793-bd07f927b957,165604693,12203046280955016945 +9f6a5b8c-4869-fd31-e4ad-46cbf63f7af1,2179122494,10890414033788910168 +0e0e245a-6ee4-1858-4bb2-b44d9f12ccbd,3460161182,1332384691771230381 +632019a6-dbe1-2bed-b5a5-d2a3e65c4210,3225514410,11901323458138938098 +a1849c50-7fc7-85f6-7189-06e70c8f58b4,1015486313,16620295138577735516 +a9634793-da6b-c6d8-3226-fe690325446d,1535331173,722237665754911367 +e289e375-ae64-49ad-e77a-0833025da3a1,838539427,10697933257962309996 +137115be-a626-9e91-6dad-8f12dab951b8,1336901307,2671340924497647533 +f4a70973-b6ae-4533-842a-d611811d01d8,71506733,12010879987821193805 +abb6105a-cc9a-e0d4-4d31-c4f633968764,1831503922,16036442847406278745 +6b7a5b62-8f4c-7037-acb6-7f35fd77018f,3042043029,3266170341306124168 +fa2dcb9d-d47b-af92-afe0-be0fa347afe8,3737895739,1861371723074016701 +44c5e3f6-8f92-f675-fd49-4cd6403c0d83,2494584391,896096604193810523 +806f12a4-363a-ac88-b148-eb0aaabb2f08,660764395,16985045547848902156 +3af39677-7a3a-68c6-6b01-ea92a1872a25,1909610952,1324317922222569951 +92790221-19e1-6557-2d1e-9d492a6d25da,3239287407,10056431828088990889 +4910acd6-fda0-ed43-d090-dec448893bbf,941831404,1466338339360880404 +b5781e38-af72-9a4b-ebbe-a182f7b432ff,891295249,315626317278570704 +13ab40f2-f79b-21af-d8e8-70af355efb0b,3701395631,2456972414968524283 +efeeae83-bae8-335d-0da4-e64203c41b71,1548733497,9210570712128503685 +0aee4a88-31bd-2a3b-c8b4-92a8fd864dd5,4074657083,3090637287880582856 +54db9fca-8e86-138e-7363-d544b613c064,3369782154,11208821903573638058 +2a209ee8-319f-1a97-dd05-f9501a951e83,4130876947,15413452378218496234 +eefe862f-24dd-32f7-d8ae-5feb2a206eae,3569296583,18208224028783915118 +42f1a79e-4661-79b4-d2cd-acdc0b52145c,903179567,4023254594214721876 +33fd3ce1-5633-20ea-6b96-49e4f54b98ae,2982398158,13580064043766071167 +378ff402-94ce-8343-6069-53b19569f987,1126714807,6907777395174710719 +355a6488-c462-59ef-672c-cfc505ea4a05,4059997957,3659472531547891981 +32c097e0-9dce-3809-0584-573bce828b16,2872605978,14871179772688910291 +c176da10-4fe0-259f-9160-f31fa3a3db2a,3070013146,2255471456028878755 +932a532d-a014-0af3-ba53-68e221652705,2321292635,13923400909985262691 +b979e79e-f3cb-74a0-ab1b-fbd3413b22de,2075342052,4717577168769724982 +33f8b3f7-7a0c-346e-5089-98e65b8a6548,2629738334,9217036001829094120 +2ebbf99e-e9f8-f619-d9d7-3f0ee1d6a928,1352110316,2776059506171211113 +e7ecf0f5-e5a6-1534-a7b3-d49e4693efff,3269262687,11293919452714602674 +38d45130-c10e-a339-0ceb-b4bdea6d22e8,2810894188,7968190863951708798 +7ecf64d3-2f04-5b95-5979-e6b1587c710f,1083171788,7553281965868905945 +5692df97-7d24-8691-b66f-955970832c2d,21849877,15604088746673013743 +034a8652-096f-3ad0-d428-6f533d91646a,3434523097,9133215392001374424 +cbf0cc0f-74b4-4abc-aeff-2e3b75607bb4,2471940108,3950674594256718642 +e2c81079-7144-5601-39b5-ab37f2006fae,942262696,16246689503925161880 +6be171a3-5237-795f-74b6-e7ce5867d892,3713787000,17124377098466663045 +02284406-774e-ceb5-9776-d15bb919d9f9,2563774517,11841047845553376730 +3951d6f9-7db2-f670-84d6-d14385140ab9,2435211446,15047764364941311658 +58858766-5ca5-674d-7296-96d9c5573b5f,1502364519,9217281433207163862 +3bec0fb6-9bad-0b1d-bf0a-d5d4b71e14d9,2688808266,10172798970106028247 +31729aa1-bc80-cb8b-2956-2c60c34f7f1b,3969173992,13017318368232108068 +c1ab19db-c24d-89bd-8e8f-45696b90278d,1792649346,6566091084025890041 +17ae7f02-a991-8a83-2218-23d6286898ca,2273968526,3840686320607407380 +60827e0a-df8c-8334-3ba1-1311e8848b38,86461659,14876729407641033578 +04906082-5deb-dd7e-6fea-e22499348569,3921666946,17467141431947529576 +50120a3c-ae0e-6525-7887-80fb6efa7367,3895367974,17541973409151781611 +a11d6b3b-678b-d481-896f-59b98e27e6f4,1251120794,13250437347762154140 +b3abe6e0-ede2-311b-997b-6706efabbc35,3924926083,14977333789348888773 +c764a20d-f69f-e7c0-36fc-eb8df5a765a1,2519931883,721015383498752575 +701b34d9-a833-73b8-ba19-9c3849a9e111,1290445505,5028298712027692817 +f266029f-f067-3f83-0f6a-0bfe3eb1bed9,4146026313,9567983901306501137 +65625685-01f6-daf9-2365-1f9d840e99ab,3403826024,12141807750813595079 +4b1760f7-2cac-a8d6-e264-b7de5922bef6,1723366272,1752427847081867564 +6f50e496-1887-c5b8-46c7-8e34455f2dc4,3353948020,12111330008699465829 +5bd799b6-9daa-2691-050d-5a231379f7a7,2434177461,11057061372174117868 +ad6d58a7-cdc5-4e8a-99fa-26d176ceba24,1668810847,8415972880402296722 +ec619082-75f9-4721-520d-a971c5575d1d,1046625663,15135386720889647801 +a2bcbea8-51da-a13c-6e40-e5d6b0e5e4b5,4056956805,3303063292971468445 +550b5d94-39bc-200c-3dcd-c520b4f4297a,3515720807,12922360206786475582 +4b093ac9-aa3d-8ea7-e7bd-c4d02a9327b0,264224974,11554527423683669980 +0d2bf380-7159-2238-c7fd-6478363c28c5,3040144879,5760798221653993682 +99ebce54-d398-0c7b-d338-b5042bae2d15,1167746616,7767748475808663005 +05ea0375-311e-8066-f461-01861651997e,2977169726,9013478161164644110 +2be6d614-7c67-b29f-c229-da6fcd5a4a86,123739138,14006891992244435668 +3e951b71-a128-43f2-c90a-e94722514c04,3906367966,10609531538109842561 +7da30dd9-153e-f317-c521-c07b2b9a1f3a,2883140915,12664426923689950672 +97e849b2-48a7-0a1c-10fd-a40b69f828ae,3964810771,16273161734316159210 +d32ac014-24e6-a83a-7f43-36e86163ed93,405975585,5396156363540265074 +a62d49bf-a753-8a61-68fe-1befeefb7be6,2581749937,12968533410951189776 +719445dc-86cb-a129-175a-2ce7bd0c0ee2,4293387345,16319649287873247154 +ff9d0e64-ab67-dbd8-28d1-1e813977ff7d,1865791707,9505484353587239042 +016b3125-315e-0f6b-75fa-2ca0fde9a8bf,805089427,16907193905288956081 +7149d15a-a95e-575f-f984-9559632cbfcb,259470200,16127663187276903073 +c179b321-752b-fecb-d200-42b81eaeee28,2738284298,7928962708190419582 +9a429077-f861-ff4a-c804-559f37f1a4b7,1433560575,4898412124854149231 +ae2c0219-5a5e-2258-7c65-91461c7e86e2,3869639661,8278764490786428359 +2bab3935-262d-c6db-acb8-9fc11fc14745,3353135448,6062911793678804590 +67519d35-beb9-5601-b19a-5559fe5525a4,3275870114,8477526922373589210 +efc82133-3350-325c-7ad1-dba034e622fb,2199875553,2906575561431178500 +f0493278-dabd-98fe-9016-9e3e7b7336ee,3097979716,17704405734003857982 +1cad38f2-62a2-3488-2810-4af811e637d3,562887088,17010539096694566361 +3534ac57-69ee-203b-8289-277407f78977,1205575672,16647994406481456089 +3ff882d9-e845-053a-f633-ea2271b2f76e,1197021473,16262917214652799994 +e1bdce13-24cf-4680-b155-75922278b844,1343755123,5617354549894273990 +ae42531a-9d4e-0ba2-4dca-248a1feff81d,1180398527,9943098151868405493 +f648643d-c6fc-a463-81ec-fb482b7a07c3,2784087650,12589423579904943946 +18fc1eeb-45d6-f380-a3fe-af9551ae633c,2427040930,16746953500607429982 +c6a06e33-f8e0-e6c3-2604-6d5e8c65a4b6,369761707,8227893667187214939 +2d977e94-5a57-097c-ecb5-e194e6c7c335,1378349257,17424990643176208614 +0f81fb7e-450c-df21-2c25-f4d75e107662,1020358686,7355140934943684534 +2112f14d-fb24-9946-5065-fa4b564f0c42,1407012826,4906804691207812653 +8a538fc9-236f-9cc9-6200-ab3bedaf76e1,3551461354,58776466940477847 +3956933c-0574-4e83-e23a-af1fc9d9d1a6,1508979386,10528360708753347337 +8c45f8a0-767e-7498-bfe6-4fdeb07ef55e,3370551113,8393775921647418553 +9c638c52-1da9-ea5a-20db-62d857ff1580,2094472763,7489840643233894158 +1d3dd09d-089e-79b7-a313-c2aabd253ad6,1709928369,14237657399944585337 +b51e9d4f-f962-3b3f-4e86-47f513ff0bed,2618099634,12736344537032652694 +aee9a45f-b81e-2afc-2bb8-3549c9282c01,1347328371,7134059685551454451 +d750578b-01bc-dbb3-361a-d1623a5a638e,2737436661,14519469556699406868 +9d29cbc2-b397-b41d-829c-3d86601258f0,2114358801,8122629931934517627 +4d5e082f-8734-9d81-e172-5b6fa6a5f0aa,2648340148,8269046407228477085 +b0002bcb-f431-46eb-1f0c-dbb7849a286f,1274827688,2594243812447437881 +bfa9a335-0088-0522-ec1e-cdbb19a63d8d,1762549419,8668627237889561609 +92cf50a1-88af-4501-31b0-aac9aedad040,1495600844,13742528641328639202 +73970847-02e9-759e-6eff-8429489d185f,2917897650,1761310319286514764 +f8ce8df3-2aba-eb8a-1182-24132b1fc7dc,628080697,8959918925672930537 +257713ef-7d9d-6137-668f-3207f2c90e36,924358083,6630089946775463630 +0e2f2f6d-1fc3-e023-5287-5a278428c1c3,2156808733,7446184452509705716 +28668788-7f6e-048b-34c4-9751e4c2172a,900308499,8578102163857031720 +4c5e4ca4-1c47-9c5e-bca2-172dfff8a80c,109062743,7023601023602487006 +e5e3b7af-4076-4317-e632-6f7a1fa43618,2223918050,17611134928321079152 +8bf5270f-f73c-49d0-8f94-3f9ae4971781,1444653030,176469504608623446 +88c622c9-3033-f7fb-21cf-d437283b8f71,1324253260,11073935626003438102 +434c0316-8106-fd3d-778f-f7d3af8b2248,1080424017,6818099810142337258 +e129da96-85e7-ffae-6ebd-6fa663a25bfb,3196507792,8208421376161829211 +779edc46-172e-5845-a52f-7c8b29fd3a82,1225935231,9632589310180820665 +d7009ef4-51fc-8bd4-fc4a-2db3e4d84dcd,1894788269,7304829854771003408 +35f3e28c-6729-92fc-e5a2-a9c5396d3809,1362313757,12098639102376506771 +ffdb0a07-89aa-e846-9432-5e8d3a327a3b,64839473,953158252143803645 +96ac16c6-0f35-2513-79bc-c6b5cfd2c5a9,98618757,9406735278724124466 +63523b52-f7c8-a06d-20aa-79bb663a41d4,728732751,8007597226774818507 +60da5b87-9a6a-28d8-4908-bb58b76a31c6,1188243158,3056384792346573928 +c74c6163-d464-c104-9eac-4447b6f3f565,85468370,455562036221366097 +7b206f5d-7544-2024-1615-20a709eec78a,422779298,217871030773846645 +b9995ac9-d38a-aec4-b9b4-c388417df6d8,2061947465,248222250887494672 +460fb1d7-9862-cd8c-1b48-7e2fd51fc8a5,2390359600,3791180880581803802 +c3897ed5-765b-e8e4-b903-35e7455c2f0d,2000563166,12907491100788470068 +60b46c24-08eb-924b-b5a5-d7c9a2fb74f0,3296108426,17838656273788063529 +e832a3df-b453-8fd6-f9a3-33815a4a864a,3946983060,2052580787176578893 +3821d2a2-e122-12d7-3578-c4d9eca51426,1858308881,16072236246096653668 +c9f0879e-45ba-be10-94a5-653be8859e1a,2379692677,12912944750199774169 +56741171-2810-e3b1-8ed3-03be4ed820d4,1981049469,9851199797478365665 +de5edfaa-f36b-7de9-22cc-05d81fe2a8b0,3472553935,6852966562876110568 +6acb640b-b4ee-a9e9-a338-da0ab217d959,2560491472,3581830591717666914 +3754f396-d08a-807c-91c1-ba79015268db,3439851598,7863477191493437064 +1109e64b-87de-28d8-aac9-cc1b0ba0505f,4117563709,4877164943835653385 +ac55768d-046f-1d93-52a7-80abf9fc0461,4203361237,3022824157048853908 +a9e3b526-ea41-d43c-ff74-e826a9b9bad1,1651328445,11256246407209786776 +95bc5d3c-2556-0e08-7018-c49d97d28ef4,708583793,9583793174806845343 +0eb421b6-2d77-e96f-a882-7172cf7736cd,952906829,15436596820424821965 +c26982c2-e075-e0d4-2703-49b5c74e5c7a,4138289274,9623240881340616856 +ec13a8ea-b853-0e5c-7a8e-86e3aca368c1,4182281324,11351733437592790876 +f71302bd-6d98-91bf-a9e1-799a6b1023f3,4274429602,804279574539080346 +fef24cb6-ac87-6d8f-bf9d-dbfa1035ebd9,3197250498,4987769578373073842 +b9be9179-b388-fafb-5d8a-83a091f99aa4,2479021166,9756832846367439469 +6b0de737-4e3d-816f-2675-b4767e125791,2542179815,3027807876721574183 +ae137c6b-dcde-0dee-a298-b22e812198d8,3757722744,4658056808604031025 +423f2ecd-dfa2-29a7-7834-d2a2837bc9d8,2015940587,18118044226309351935 +7e95a5c5-e0ac-4f16-51a6-8e7cfd378c4b,1387370171,14867971348173710359 +5c5b21c6-b993-247e-d046-1337d1013085,2992418580,8614093999941786147 +c879f42f-7fb4-acd7-9bad-90df78e09b7c,787994978,10644773544310010249 +b0d2c2bf-7e13-3952-7729-d6d521fb2f54,4098810568,12770785394662734531 +3a37cd35-f5ee-96b4-7f4e-8eb9dc7c6942,3379846049,10926393280217153222 +01332ecf-1362-c004-22cd-dd2ea3367f6e,3981462830,5586326246676453512 +aa61f596-4796-5576-4533-b855613db0ad,971390006,3492474373669876878 +ce575798-f77b-a673-b864-750be72d61bc,769233930,13613432546449433009 +307fee5d-51e5-478c-0aa0-c2d692ddccf2,3354909433,9115427512131173762 +02d097df-d0ec-851e-cc91-d04f5a767db2,2538799656,7765443436148752545 +ef3770dd-03a0-630a-ceea-b32ec805d128,3111951189,16946616385435003069 +8efedb9d-a4db-e784-b24f-b694ea2fb58d,2821103060,3399099985256014710 +a80fcfd6-ebc0-3a39-065e-c4ffe6a5dab9,2407451036,1775540002752673843 +11ede758-99b1-9d0b-daee-803654858a10,3413767924,5503388123586374543 +728c49f1-ed05-9e52-c754-b825fbf19bb4,781488843,11275152981850282856 +b1927956-8032-22f9-4e8c-ce3fd04219c2,218815793,109055839303954459 +6610d3b8-3745-d27b-3ac6-f8c1aca615a7,1515161062,1337528712062641411 +b6609b53-2a79-cd1a-94cb-ab8d20a8f362,3706208782,5336005727778009737 +2acadfdf-dc1e-602c-ce35-e4478ba7774f,745086849,13489480730193150101 +46d37b48-1a92-91e3-5694-e5b2bb350b92,2486572458,1694390320371201021 +63a96409-04de-ea47-ed4d-c6683f962075,1485994929,11021981220150164121 +723d6d51-836b-435f-3627-03f663e33088,4000095953,5815855344762285505 +9b154d65-283d-7e67-871c-1abca83828a5,1904553692,12806890925457315808 +3b69d211-14f8-fbe5-8568-9d56717838d9,1462025144,4267315464261147038 +a72772e7-b44e-76f5-07b6-862440130562,2053477753,8797803326978920628 +f45e30e8-14ab-1562-2827-ecd6d72be3c1,1215223703,3842557552009489809 +698ba624-9629-3da4-bea6-ad50127c406d,2005232059,4344596044044519877 +538e924a-2ce2-1dd9-691a-303090f33e3c,3449837018,11687492260017386991 +0b423518-ed4b-34dc-419f-9e9f6e52811f,1198933660,9390631037744382325 +b9e1b677-617f-53d0-19f6-956509a50d89,1903680435,15709051115584403379 +539ad353-9959-377a-1453-386c333b3699,3338094897,12456032457863500718 +708de8eb-fcdc-e15f-ca40-7953af7f3fa4,3512834176,11943918861764124149 +e2e817b9-b160-f005-dbc6-a82dcf4ef2e8,2159125859,7167435298199602433 +fc70d8b5-0990-d026-07f7-e3b46df18ba6,1288663937,14476152772238360993 +72e8167d-fe6b-c1d8-bc15-bc5438aeb74a,4288882469,17136592751915094133 +e3bac37b-3b9b-6ed7-1f92-ad7e564e29ad,4103606990,9087854392961527723 +7fad0257-1cd5-519b-e97a-44b6d485d7c6,3053020341,717952062112590802 +8f3bd927-e36c-506c-3797-3545222b45c8,2023812857,5010553209567155300 +59e8c4e6-24b6-214b-be2e-ecc1d017d27c,3965450065,9097571281044686750 +49abe813-7422-7878-620d-296ecbbc8f57,4103353897,12932943711558458377 +916537ab-4aa9-d234-46e5-fe2b11c6d428,2140390619,6451267175740343778 +40598c97-b8fb-c16b-3d74-5f1ab1a32ebd,4136397363,17603123411659882660 +85aa9aef-c2fb-0dff-1db6-63609d82d204,817810087,8895055745783084594 +10e25d60-13ce-a8a6-1627-953c741e3433,548646103,13652166547703999063 +b685a6d1-ca03-902b-535b-ae8108e4068d,2828809939,3284815292366954364 +6a1c0b8e-f3c2-77e6-79f9-65422f6ab39a,1204922839,4058717006083771814 +b4d41a47-210f-0ac9-a23a-4395c7ec34ca,1931824717,323277156965564224 +093fa737-ec2e-0fc0-8dd2-1560e91358d5,645221824,14433317043749452177 +a0330ac1-e157-922d-0953-933076a8ab8b,2427196153,6793711969291471722 +9d0875bc-e3bf-9977-2901-6f867c8d99ae,59243597,2081814338603424417 +37f47645-4357-d7d0-8d5b-76ebeaf7c685,623611464,14381221669269006547 +72b011b3-76da-e728-3076-7125c3d0092e,1780068288,8620649820378197963 +c1ea9373-e55e-e6c4-07f4-872e5465e12f,2417293969,8935355608776141002 +7c0a887b-73a8-ca05-592f-2d16acabb43f,1933426256,10369074623332874305 +ee42a073-ad13-a908-7bc1-0389d738b7ab,1413061138,1582476258511668558 +33c7b28d-bae5-46f3-d572-66639f18ff5c,3731395682,13903804180361930716 +f75081c8-e686-58b0-9288-c1869f2069ff,4182818178,3806341046405205132 +8e8d7633-3ec1-ec36-5d6f-c3c767ab48b4,2967890029,10018722961041868351 +6850f6b0-f3b2-f86c-129b-0b3901158d4e,2658487240,13303406125124696773 +e9abd14b-8ca4-c2bf-10f7-f9ac9fd85041,2767090812,6134944157133900529 +71f80f18-1a47-40d6-ae02-54df99dfc270,1874377725,4857833811272927113 +68543cd6-0dd1-c3da-bb42-69c80473d99c,2268913307,14042083525615236214 +96333a07-9f31-f207-a299-59c41cd88993,929793599,17768162037555302611 +688edd60-64b4-26e6-2224-8846b7cb4aba,3463584904,13589918029367522057 +bfbd97b8-4206-ca15-e77d-321e163a69fc,2125739205,16645552329333220778 +fa1b7884-2b00-e232-3d69-0076ca5da229,3747096324,1157866952828406719 +c683561f-229b-1900-5a0c-ab4ec8a2f261,4195896530,7327756810027637178 +25cc305b-8ff9-6f55-abc2-66667a3b07c6,1172173505,10082632735801687632 +ec02875f-3d74-cf2b-1fba-07f91dcedc07,2165397521,14572368382524747414 +ed2be026-f681-3a76-5757-9b8cdeb7388f,2304348029,18262581545578585435 +f2f4489d-aadb-97c6-d820-6305747c2bcc,3938513260,15559147601072634027 +9e1ca3e6-8a64-0715-4417-4227e2b5c59c,856206679,8028945156134803863 +b6451fd7-0ebb-474b-144e-451ed2c5dbf8,4069114893,14765710111962199574 +bf1dc46c-c789-0cbb-a9cf-849aefaceeff,171416553,994195618051158401 +ce73c27e-349a-07c7-8cf9-4cacad657372,4168479350,4524808483244166629 +e46a4f7f-90c4-02f1-e79e-b1ad20065d79,394382509,16016149908493739245 +ce22050d-db02-412c-b945-1589d88f2bf9,4076926850,29693038812818674 +980489f3-672d-2f51-6330-406e8bae75b6,3363889819,9908888344514138172 +73d25e46-6a19-b7a7-b991-1208ca88753d,495351823,12795919097827121763 +eb61129a-bddf-6afe-efe3-1174a1d004b3,2675596820,15675565922074735082 +7f9df22a-d4fa-02e9-bb77-c00010e97ea8,1901016524,9943241994145429889 +c8e1faf5-ec42-a3f2-d2a5-c58578fabebc,1068250529,2283021478788537282 +756f887f-4bf0-a9d4-8e2d-f6a3d71c876c,651679294,1053265337349201189 +48b054fa-6902-deae-b7f6-453a1fe8ca6a,1932137188,6682754856997046300 +39885fe3-2d4f-f5b2-928a-80a3dfaf42cb,2891332312,5687108756558896259 +5faa9e8e-9f61-7bb7-9d49-6a36cc99333c,616700358,9539421197333300538 +fe7456ae-add9-31ce-3b6a-550fbf01502c,2369400160,5703324600292644530 +baa66749-92e6-c27b-73cc-e92499085c5c,2364761618,7252994988416468421 +e78a3058-a58b-9afe-4daa-794c4a41d956,3282996134,13012800995213230277 +09c0f114-da44-8769-57a1-4e3c2afd5d61,4163222529,16694203677393368262 +a2b23b3a-ce69-de98-ad65-df90c17def4d,1227848634,4822344326598171338 +27d18011-c64f-15b8-bb54-799ddcf54491,2594306423,16937237619166914939 +1ef75e1f-edf1-516f-8c66-a3cd5447684e,266897094,6050342557109532034 +3f79162a-1e01-81c6-8e19-ae6824d8f14e,2946248789,584370940749581409 +152cd77e-40f9-fb5c-1846-40ce0667cd20,1558968499,9172671376230615308 +47025876-57b0-e653-cb28-9414409978db,319863651,9127061434704216770 +babf5d49-1d19-994c-2f97-7f3c50a4bc01,2569203795,14177990140016824759 +cc00d6ac-b75d-a60c-4181-819f08137502,3585841477,13442541984377302980 +133434f3-5540-c4a6-362b-1af66bdc9566,2210242398,15612518426375722213 +13dd0624-0c35-909a-0c76-2ef39229b51d,437717980,11780678974797989472 +e50abee5-f9da-a9da-c174-b590973d9005,1995262154,15777729888550085891 +633672c5-315a-7223-d169-247e4990ae3a,4105905755,3021326152093226123 +98481fbf-d743-9d27-04b3-678f0425faf1,3403547715,11484780862486009403 +67fd6bb8-86b0-6f0e-6546-aa1c49b2bffa,992138934,1227586519335205233 +0f592359-2615-039d-213e-250a0b9dbf84,4165834615,13215139541928406884 +ebedd116-1e70-981c-bfcc-8e7903380163,1907595263,1030554903263218341 +e89e88e6-5665-6eea-64e4-db534c45d8a5,3374502843,6822174493051414890 +961377e7-f972-0164-bb20-ff0548165833,1390348711,14044145922394261137 +8de786fb-5eb7-a648-e89f-88c06c881b1e,2944518420,26962571673901703 +5ee57217-e6fc-f3b5-153d-6dc448b0d28a,2597292733,14858734844252954795 +c11cb288-f249-9d4d-5ef1-111ec83b49f3,3901868085,14422889673380428137 +29102ab7-1c98-885e-cf4c-8e1424fa154c,3450705168,6760073870547895247 +dc9f10c2-294b-bf00-c6e7-c5eca1b2aa49,3098738117,4115246034717257362 +2f7eae68-af43-74f7-fa41-633950f64e08,1196337344,8587271014032595425 +735fc85e-61f0-fbf2-64c7-f09d0aeb87b5,277147833,4929339041800124202 +ea67b245-0a40-3148-78bb-feee9593a366,152115914,10990307563490132966 +8db6246c-81e2-6517-0c49-990c62b5388f,1436545537,6298910967850206027 +6ffe7934-ed02-4981-0c8f-d201335772cd,2988983521,5436721558717744070 +322fddd3-7794-84ee-f07a-a95b31999c86,1943520256,9761435722798406247 +2c93b809-049f-977b-12af-1d279c1e1caa,3149876504,17536852414661240138 +38c08f77-3e56-1b53-276a-56fc100fa243,3068951202,5671285118205906083 +2b09bf01-8409-f2b2-8904-d413e2505e63,2136581047,3110729371086974861 +d4e1a650-4a11-90cc-7b65-c30ce20e29f2,2273251860,10140874725592227683 +55790c31-7a9b-29aa-6758-af0ab2790a65,447667298,5409845352256595766 +650b0d8f-a9c3-40dd-c7b4-a273d2e8397e,3480623151,8429147931375215669 +7766da0f-0a27-e6b4-8c9e-50065c098fcb,3081309993,3810722771719214889 +c5086a02-5170-ae30-c2f3-27c7af7f8ef3,349227848,11076458714133957450 +ccb300c3-da67-8da5-9faa-724fb95f7eac,3887061684,13511731742288874594 +ddaed765-5140-9551-0ced-cc6a315b18e4,2685155503,7286953995759544891 +48936a28-63e4-7645-02fe-9f203f3392a7,415557555,14854477429318951038 +b86a1b85-46b9-25ca-e027-77807dedb47c,1457818511,15179846020486331355 +3c58f3a1-0162-4575-28c3-4e9863f39514,1183065751,18417401904527792071 +05e99c22-1f2f-e4d9-141b-38f8bbc48506,1270105684,6077805107120672677 +365c3a17-52c6-4c4e-f336-49d0e7a294ce,3893653339,4419561178287110365 +420cf154-d95c-579e-1a54-29b272ccffd1,864036821,3668040571678766437 +8f7750bc-b28a-2de7-f5f4-40fd3d95390c,2673225108,6260224182194759760 +f61a61e2-8167-860f-0359-4be66224372e,3330263528,280374323012190817 +0b5b3a0b-490f-11f2-3162-4088a15988c1,545157140,13066609753329579439 +3f47bac0-f796-88ea-d331-207ceb004b5d,2372548855,4009499103544463854 +640ff9c7-a31b-233b-ac92-8031d7104273,4261186266,4963442806063666696 +7acfeea9-1305-612a-15ca-24d0397a2140,4124905723,16455000579368780441 +e565b078-aaf0-3a87-28fb-64bc52a6923c,2631835833,14172866943784738770 +b8018319-dac6-c09b-23e2-13402b45d009,900445396,3191415947544822789 +6f5718d7-c955-08f0-cabf-7ba34e53e2e9,2684737794,12134456939627248440 +8f6677b5-e50b-671c-0b55-d867008ea108,1494842517,2113685420954817274 +910dd72d-7577-1af1-effa-24753274e7a0,1791023472,11426048898105191611 +62c19e86-9211-1b03-47eb-b657d4cc7bc4,2316151368,4000308454203948386 +498a1a8c-1894-b6d7-8d2c-8a50e771c1c9,1101420587,4152713806598574481 +e5f1a677-6b99-4c84-3897-5cfc164229b2,4117713450,3458171168930510736 +0e2cab1d-7fde-ad84-3d2f-9dd57e2c8dfa,2492157934,12425186696751098621 +6329d5de-ba1a-9992-b57b-3cf34dfd81ff,3184865566,3731840637813994096 +c16d240f-431f-fbba-466d-267d00192807,2672384309,2749134206961574376 +33c4995d-624b-1c98-b843-bdc3f34c859f,1818752900,3852116591928332540 +dca70097-eaaa-3304-0aad-765a75a55fa6,1051445243,6567431649852149773 +9110b2cc-77b4-58f4-6ab3-10a58792b79e,85167994,8005301258431518845 +abaaa497-8aca-e2e1-58ae-0b5826a895bd,2441452702,14655558418478888304 +f3b2dee4-7c84-9ded-ecfb-f69a6beea872,291390517,8562341190146906258 +463236dc-7247-19e0-17af-f52d0dc6f87d,1621534976,286429468190818848 +63f0b3b8-5bda-cf1b-61c6-be4fb4726442,4269273342,6126026575821057948 +7dde12eb-567d-d30c-840a-a98b7ef6b09d,973056194,13397857934287510593 +896989fc-b050-e465-fd3f-942c6f1e129f,861348167,2594216014595839869 +802cde2d-f4d5-f059-8d40-8a2c7cb05eab,1979924172,5977466994797053724 +145eaade-ed48-024d-cff7-0f270353bbe8,1199067830,4570351272937397287 +3f77c3f2-005d-13c7-ac07-6c4db015e4d9,2197214547,7563598610190777216 +32dadf9f-ca96-22e3-e4a0-6959db5f8ba2,2241314642,4109486234729848633 +f870422f-3e68-89b1-dadd-fc705271671b,251412181,8276676222836245426 +abae28b3-68a2-b909-fb34-b56faa49ff83,1812512996,6574801936362622813 +5ceaff1f-9d12-bf20-c995-1f4e6bf4d45a,2201977967,2583925201199764142 +2761722a-0948-1cb2-9d25-74af92a61ee4,1815489515,7025505870372424697 +a4db2ac5-1968-5f5a-d904-6685ca043697,3643839798,11570372040379206905 +fccbbc8b-8f5b-31d2-2e43-b17b9148545b,3549698346,3852490623437366585 +fe8b89cd-2f1c-fcc4-efaa-03e72efd7b3b,2998792548,13322824595089207675 +bc87e085-814c-1482-8bf7-0dffd0553ab3,455057994,9238395347188920627 +67fa7fff-4f02-9163-aa2b-bdf96526cf76,1558798225,18427433096371040935 +bc07e8b2-0a1a-d318-bef1-c2f29ff9dcf5,718532738,4388217020689332412 +12a6729a-86e6-7215-cb76-425083509cb8,513394306,10479372218125792053 +a2ba48a6-290b-6a7a-f653-5c919ef76cbb,3277347803,8962382517285974113 +6fbfc719-e2c8-5d6f-3189-76d9b42712c5,1548208192,6649815404315131451 +87bc9ef1-ba0e-788f-792d-81bbbe30a78b,3479428937,16885731705113336113 +59217e7a-3358-b8dd-7be4-de3f8472ab2c,3261094858,2857985078182690216 +e7c5952d-3790-9f8a-7892-8abe81af54aa,1065321220,11160801689078597273 +94992cfc-700a-6c9a-25c2-fb041e641766,4148500784,16593256259746709288 +ef6b17b7-edac-c861-4fea-4d29ce3f96da,3959384475,14464901050551254740 +e7e1e048-59f5-d38b-c37a-239b4af8dfad,4236040450,11442477037594940055 +5d54ef5f-9583-d8da-a729-0cd38944ac4a,148923375,11020916628500083452 +74041b6e-b475-3b12-63f7-f2edefe5b8b1,2794181305,9019762722120709744 +7a633779-011b-2102-349e-0265289f5b01,2196911354,14623969531607109438 +bf88c24a-1069-335e-ec0b-7ce0969d1b1b,2145927818,7706996373887256838 +5c1c949a-32c6-2da1-9e7f-c9a3d5950a34,3620584077,5189955383033623791 +86b3f3f9-0e7c-1c8a-8da3-8befe3e88895,3831721560,13000513321795083189 +87fc5ce2-1f42-c137-6da9-af2a79ffa6a9,2930065226,16633380941811302927 +8f5f6973-3e55-b0bd-b16e-8d20b37fa4e5,1830693979,12191736168791193775 +a6fb9f01-7c57-6b9c-5d74-1aaee531f53e,673220222,5852327605625462233 +cbf9634e-aeb9-d721-0797-adf703916505,1167509889,5329891367018771254 +c038908e-fd4a-0c6d-91f8-bce08304743f,1377527338,6469917094948554761 +7ccccee1-1c08-8195-9e83-196cb15ce3a6,1716385359,1070812019276392051 +e2a6e82d-3b8b-ae80-ed58-f19fbd68d585,3281973498,15837278178184115451 +2c82a86b-4b28-0784-bc0c-d044e5e6b055,3938725019,9953688639343418729 +488887ac-cef9-d299-5b54-be02b722b442,783247663,1752789280399552048 +24b33772-5fe4-6b3b-c71c-cf7e5ea0dd7c,506850615,2171756962813033546 +3921cae8-1a39-eb27-e832-0633e1fdf406,3397263644,15885471777378255947 +bad439fa-4b0b-0400-181f-fce5e5c6fe93,3481726402,1459710949895493179 +dde3ec29-f7df-e55d-13c2-5fcb160c54d2,511332068,6101306906300561549 +3daf01a2-111b-354f-2cb2-4ee48acbc108,1977746431,11860210770300395674 +92df4d3c-64bd-e652-7e25-60fbc8afe047,3384129330,15083545531572978883 +c284471c-5966-6853-4ccf-39d71cb4cf3d,2290207146,13660105245649088016 +b0cad6ce-7037-1683-8d41-81d9d81659e3,2072970891,1485681390596397842 +e8611b6a-f835-8420-51c7-c47583a53c4f,1869236074,14670479064383654544 +63fee61d-0d39-b7de-7d38-0a261b814e27,1871301428,14286521040502755401 +a450da7c-eb4c-a0ff-1a2f-0c3737e5bfe0,3393194832,11890946904361589322 +ae4a5246-0b58-39a9-3b65-d91842ab1d46,2109518354,7232734512615667624 +4c71bf20-8af1-5522-25bc-256ff61b831f,1590062774,16759663660613570539 +aacd0734-6ab6-e3f5-11a9-7f2966348c08,1883478069,11877558420451354680 +ae190e32-87d6-9cad-2bd0-f31f894ea13c,3656156656,4762812740585646205 +868463dd-7130-a075-38ac-75caf8402d9e,816703769,15654324567962989274 +440782a8-ce08-0ade-61c4-18d409ffed3a,3481692290,14427662325301473060 +cb88dac7-454e-5db4-b92f-fd1baa0b3e5d,1863616317,5729653175301309080 +864db60e-f355-9b81-4540-6d895eec33d7,3654821150,12662787143728104140 +4f248e35-4894-66f5-309b-379c1b39ca0d,3891906627,15815939178141640502 +499771e9-9b5a-e0da-c560-84d76e7bfb2c,195624715,11016336810658532982 +47ee7999-8083-e833-8dff-7a1ec27e9ae9,523599146,9999836536315904569 +49e7fe0b-0bc4-08dd-f751-e4c5bb12c133,1129499674,7090710817677993671 +a113eaad-7985-9bde-9094-5048d5dc71c4,1800021156,3587422417911325958 +95e13a8a-7ca5-2d79-628d-b920c610793e,4211882458,2241247252946156487 +3c78653a-0670-7adc-6986-ca60143ab4a4,3460492489,6143916557644888045 +3a2edc82-582a-47c3-9fe6-11e1c2e382ad,724118463,17341798161794486396 +57332472-9e5a-d47b-c2e3-163b51739caa,1751948184,3975087380471020864 +dea2a700-a9d8-174e-6dd7-95387090df38,3136562410,647231668556377192 +850dc0ee-238f-d980-39b2-c6cb73092a72,3529097527,688795322261496230 +4052354f-1d45-6922-32e5-0f76f6670d5f,1758920896,842125571964540319 +e70c8cd5-17e2-81d8-e5f6-dc4d147cf48b,494870105,2999856128797829735 +53162f6b-5fa7-e7e5-da6b-4d444b28fe4c,4152028020,5741462610174408510 +37b4c64b-93d0-f9e2-3b83-19d46246b3c9,2921974391,17280899526856410455 +2712e61d-a7a2-f5a0-489e-2efb3420b1cb,1943595534,7537094107508427704 +d569e832-2f90-db20-3974-c3c49f4c9105,3191386481,3834690253398430986 +b0496710-46fe-8506-025c-7045ed07e7d7,3862710319,11876647423556687223 +a5ed4d44-3650-b8e0-0ca0-e895eec71c75,860845957,777185019677358501 +b6c4d06d-fb12-57b4-4085-926daaf014bc,2087220395,14319193836249960889 +a239e7da-2e98-8c3a-9bb2-241dcb7dbc7e,2521135073,14148677409657077874 +953af078-c2b4-bf5c-1dee-b857e7512579,3435002596,9721087550252165038 +09e21463-5760-6c57-525f-b6bc50dadc48,927461056,1274193349055342287 +50aea60f-cfcf-0bf4-5ca9-dc7dc6940d96,708690767,16496867831498629572 +aebdea61-a613-ee5c-dfd0-017d683b9567,2217555056,3736569697352726741 +6910b84a-18a4-da42-996c-e9a300596544,3231059404,1492904260587424442 +09daef25-0aac-9bc5-0afb-5f21f9f8f900,892271031,17713697639322968209 +b2f05ed6-53b5-b500-8a09-56cbac563bd3,2723215170,16751862059226683647 +4f6ec3cc-0fb2-025c-4c27-87768fcfa695,1530894244,1978832669351703012 +29c306fc-9e10-89c9-5fea-e60e41171a91,744462029,11648711882032120915 +911ea3c0-ef5d-78f2-1314-9e8b0f5769ed,3013322268,6977934630497971609 +c2c6f344-5231-716c-f116-6bb1b388c125,548290282,2079049550494025437 +b7479624-5e81-b1c2-2b22-343ef41a63d5,3122735642,9962499371967506701 +34b75f4f-f28c-a716-3fe5-3864ba3cb56e,1934002675,8203743449681783273 +13ef9ae8-9ae4-f421-9e87-45887c6647ea,4037555401,15393725950834323037 +bbf6d1b2-e06f-10cb-64b1-45640e933987,1288600979,6700512781798970074 +c218aed1-6846-2a98-cd47-563db77eeec5,1642873397,9779324317419798919 +0a42fcb3-37a1-722d-80d7-4979255ebc1b,1073745512,10418007531506438953 +c96a9436-71a2-b629-2bd1-11ac75532742,1964329750,3298748434864056267 +ffd733f6-adf4-e612-e4cd-6280070918fd,251898263,15037577617175628387 +98a3b860-37d5-5117-fd9d-3154e8d3b2a2,2193299010,15397908680072179454 +3f6a4701-e3c2-b710-65b0-2dc43de22b2d,1388495752,13823603633493145050 +8a72ac2e-6400-28b1-f573-70f2386f6851,2143530258,4065675186322103706 +0a5ef500-298a-41b8-cc0a-11feea784339,2863555045,11122622863264646388 +964b93dc-43d3-7887-0e5d-31ad77c281da,2298220785,9373279289810028292 +e6e23a39-7c20-abd2-8382-ff63791e02ed,3975400988,8834539903197506755 +0e2c8c31-cae8-ed32-2966-30941b2fbe6e,1165530281,9106941470728169962 +b1335164-9c19-a903-750b-cc03a585013e,882077830,8950214127188347265 +0f829dd3-adbe-b33c-d195-191ec8237982,254102960,2676357090020882100 +70190121-546c-fd4c-d732-316f037f9970,3094184180,14503130236342100714 +fed54420-76a6-c22f-fef8-b781dd42af8c,3153207075,8586305843660301454 +bed7922c-6b4b-45ce-13fe-ba94c07bfec4,3970902501,15226093048443872679 +e382d888-9938-8b2e-ed2c-5b46d2dd383d,636136826,801164559133166736 +018a1bad-7c70-5fac-0ec5-1a73fe06b0d1,973333046,6440808337004652228 +574124c8-7145-481f-5b74-1a6d47289f86,2370615035,643358004676128490 +710988b7-c33b-9bf5-8ab9-d0ec015bebf3,2305550722,3255165349264385876 +41588741-9e19-7d90-2e42-c9fbd220c8fb,4273307825,7118095485101116935 +cd7626c0-9875-36e6-03a2-70950b155fe4,495730900,4859693178440329721 +21e3bae3-0975-266e-4810-f0b41ebd3b79,2435841559,6325626008599444242 +59c2d2ed-2bdc-4734-7522-e7cbca4b92ce,2695214009,5555918485297116772 +d06a6e72-b0d8-17c5-b9d0-4a92665f1131,389488932,14520236010273926372 +08649cde-a1ec-9aab-be64-82c2ce46453e,4174910751,13382287289056555985 +6f481537-7604-2c6f-4ca0-ca55d0bf8f71,2523820647,4507080672524851200 +2e28e90f-6871-e2a0-6812-639191e751ab,2499465342,5363818403224066540 +0f08b2bc-bceb-d4a5-64bd-aa40b60fab65,1517059652,10171474156888381322 +71f53788-4e63-bfed-860a-ec885416c305,1070235508,5730737969017652739 +818f7b89-4111-a92d-9961-e402c65f2117,560718620,10416744877379493228 +4fcd69a4-fe17-41dd-bcf9-e3e5f5e2db99,3048841680,16698197345778490252 +a6bb3abb-a395-6168-6d78-13f7f8de5fcf,3662902249,2903348135862577233 +aa440c8a-696f-0e6a-7a7a-f2e2fe4712e3,254679766,17586089752026244087 +07c5aa76-4734-b822-67a4-bdd94278e647,3401798259,6207399620734431820 +a4ef7e03-1626-c0ca-9787-95060d82f893,2067558209,11680209966722488742 +e3d758e9-ad78-a8ba-0827-6299d4d3c840,3913972589,9234372960934936831 +772df8bf-6341-3e9b-6611-1367b0497c20,134220513,3531972531242664373 +efa4a4f1-4e3b-0ffc-91a8-01d05cfbcb1d,2365698826,11407541934802985076 +2b14577c-3dc4-a7d6-5e63-e5654f926ddc,2041645276,6258963600747495034 +cb98a971-593d-699b-e2e8-dbeeaa383f56,1019326986,9153146389812305920 +98a08c33-a1f6-3b98-0af4-d23ffb12b6ec,2846150539,12377694792641266147 +9e186046-e5ee-8189-c624-159e6415a75c,432904088,18168881349013678123 +b374ae3a-5713-8cc1-0dab-30cdcceb7338,2971826090,7513964665208342030 +a8f6ed9e-8901-40f0-19d4-ca26602242f7,1060428891,835233186904536747 +42adabfe-f64f-5d43-9778-3d15db360eb8,1119358734,7548503269888657012 +a6ee1289-3258-e299-9540-72f8fcfb3337,1300582648,2050522369337051100 +580289d3-dea8-e8eb-40e0-32c72fd47db1,2203129959,6255071965161471269 +2cd6ccd2-93d9-c531-8ab0-0da0be399862,3299463804,18380694226051476116 +8a407e37-db9e-a1da-2d0b-b7cdb38fa0eb,1882186001,5705965947824220809 +1eada94b-c485-1fb2-d4e5-31d44a6a728b,3898305778,15393643326533219686 +a2c07e31-8cb3-8951-c890-8dd911ace0fd,146597181,5926602824038868632 +4b4b5304-e869-af87-453c-830dd6f40583,1956799085,11780315159481850039 +c755f402-a0c2-1374-1d96-6a5a61651486,2510209495,6716713485004592228 +6aab8f87-904e-f91c-bf2d-9894be94100e,3981592670,2518534684018066853 +148b6fa5-8a00-7af1-3c60-d0838904932c,3974976528,12497305765170272274 +da8f8571-104a-3b47-a624-1b414fff7511,2437601250,9466691894288444495 +2dcf55c3-7be0-6fb6-478c-3ec321e7de05,257494148,16573948551992927798 +333df2c9-575b-10b9-5691-aa4b84b70d50,2886108776,9402851689883580864 +08643643-cba0-9f95-233a-a50057fc1454,3269598481,2021011741448701735 +ab9ba674-7c3d-6147-ad94-05e78665c10a,4031452482,7295039509239033480 +cbdaefbf-e2d5-d689-e8e3-e7b2085aecb8,2710099499,10427846853255503009 +1a7a8058-1a7e-3426-4373-0afb6df3df4b,3159476540,2947564834247340700 +55416ff9-b56c-7189-e7ac-cc060fff242e,227042182,9687052826747661825 +185fe3dc-e223-3401-676d-aa9b83b6f0c3,1122552790,7997165498895010030 +27a833bd-3b39-e643-292e-988a62189d4d,1559358124,9866232842581201473 +63cd4843-0c9f-a261-b3e1-99a7707d94c3,54726525,10907269926430570755 +34a5c4fb-47b0-c216-df03-8a9325f310c9,3767976143,14109318629254844936 +7b051484-e7f9-4074-53f2-1e04991c00c7,3318038902,1331851596992128819 +4718c539-03c3-1c9c-89b9-cade5fb3f1a1,4166745041,4761652387865975138 +f0ab8a6e-e147-eebc-1c71-1d9399df7b9f,1027283626,8582294052581992880 +282db209-f236-6091-02fc-ddd093b323e1,697333978,5920849487145826657 +196a89fe-22fb-1766-a4ac-9ba2813fa090,3371355931,5454547155145894221 +47a65677-31fe-e72c-48ef-ffc4023b8f4d,4125735806,16033962192616835941 +4297c2aa-ab14-d8d4-0e62-c2c0dda4bcdc,2822216143,6944256568732982740 +825d35a5-51ed-0b4b-beb5-f9e8b0f160e6,2098236140,11852566640747139669 +a5cc4913-5464-e984-28bf-d12459a30ecc,2778755812,17715255718779812917 +dcca481c-c0a7-78f6-b4a7-47501c8e21a8,2696746647,9257859967955117974 +d384cf0d-8d1c-b7ea-08d5-5723b5cc815a,2271885603,5963850683184323331 +aa5fd9de-0a8c-411f-4853-8b842e5eb433,3018997405,4630590395609651549 +e0fb4168-7cf0-2b78-bdfb-f9352338393d,1599759973,14230664893301564021 +86e4e1c6-4ebe-65ad-f3b6-62138c2813f0,4220278707,14694083235297362573 +91d0f3c4-b844-066e-e155-8f8c2ff79485,3671866944,13522470735637702432 +b0a002af-158f-9b56-93a9-6c1ff5293175,1072680753,10787055889221296697 +ab273038-6d8e-7c5b-90e2-ed830e58ef52,4231081706,17777455479170595514 +a1153a51-f2ae-395a-a0fd-ba83199d70ce,3179115163,7095913076647609681 +52722a73-94ee-b385-d165-661b82b1a30f,893901926,1513881173354771804 +a2ceda8f-5f97-00f6-5397-b19e5b28cabd,943116751,2997659983031304731 +1c051e32-16e8-b720-c931-8f3dd303bb40,3012473761,17132642150012398576 +6a33a6ec-70f6-f049-ee7b-42cb435856d2,271842010,15313305985599140136 +5b42ce8f-68f9-5e1f-e687-2689a312b879,2682186879,7356584208930223505 +1f549199-3d60-04bf-9026-371d7ed71640,4192349784,366404172746130578 +1f776aa6-122c-7017-5372-914915182196,293871275,17275331283448410122 +f444fbac-a03b-9c21-f328-1f2aafbf916f,3128958830,14515199508350046398 +2990996f-6ded-9dfb-3776-3a5ee09e1857,3228510738,4991209602095545114 +f3edaaa2-b945-b80f-dc7f-c185380e9543,2838999606,4923365682559201556 +1a1d06b5-91ba-29dc-5f79-4a743bd9c724,341789721,16725940927831768593 +0a9fc252-6626-a692-4de7-7b8a7d585e28,2680751050,996093813953049983 +d77e6a73-d60a-b127-338d-c22a4dc4288b,1005169484,795030009395204568 +d4408cc5-52d2-d378-eed9-b07a1dac8b3d,2395762891,18245147582103514954 +8be56655-f2fb-2890-750c-b639e543c13b,1508512102,445006646555616558 +6563fda8-ad32-eca8-6040-5bebaa4994b4,85398755,4927647241074596026 +64b2ab00-88d1-d4db-e837-8a5ff2fac503,1872470303,15340240752477214360 +c4c1b46c-54be-3811-29d1-54bf86a9e92c,1828907283,6173896290623007506 +d94a7e2e-1758-3847-ce60-26da4c2a3c47,3714533402,13537668731085036478 +3dd4673f-c0c5-76b7-98d7-d5d9c5854acf,1203880324,12804686156679975278 +dbe4780a-feef-6833-3154-911a3b46cba5,802920026,12025201042785846089 +868534e6-fd40-3cae-4b0a-7f3dc2df0c3c,522343854,17823667154356226083 +dc13e4a8-af3c-597e-f99d-ea3d1827f901,2957120262,4381297306347073808 +fa208626-2e36-834b-2c85-96126641c903,2372166386,5740669408101299905 +a1e8882a-9036-528e-a3f4-cf60baca110c,1857305831,16544339845329502061 +e77e0ce6-31f2-d286-6258-f4bd26183fc6,3172479139,3058055452435125983 +a06cd472-5ac3-df27-5130-f8005c153b4a,2656407018,7927133599190808780 +01a1adf9-fa8c-e801-123c-3db819659241,2378151545,9473790112964726679 +7891ca06-41e8-2468-12db-3559ec28898e,3476431632,15847405810632114857 +97ddfd8c-65a1-3634-bc4f-0b677c2de6be,3515686818,10942702140722774750 +5920e62a-8f4a-db9b-cf10-b0021ecaace3,1714344754,12699136976130874794 +ec2ab9f0-bf30-e5a2-fa40-2d8997964894,1245636748,14537733416910282068 +e9685784-3202-c52c-7459-9836c62dfccd,2309732016,7216548422824290615 +2e4c6ab7-ba82-358a-a0c5-719e1df76697,4223238087,7265787842125385125 +29607e05-efc9-2953-e3e5-36bf07d6a3e7,2243651704,10154379818493842722 +15fb2f1f-b765-63b8-4239-0c2cff8108ce,3033874076,13752510526306888373 +b1a86e02-15c2-0e27-1877-2eb98613ca9f,3812648343,9397570646889765691 +e1113465-b449-1a31-b853-d1942c123d32,3507814962,6569553449566976010 +1225a684-3e53-9e18-479f-207dc831f7ec,3441625765,12631868130222972340 +40a005c2-3ce4-06f4-713f-99882e337328,404052511,9105604583279574617 +7eed4bbc-6948-7353-2a6f-0c13be106693,2260862891,3796050397350590027 +79bbcab8-248b-75bc-a4a0-7f683674a632,1438846424,6903398713652341382 +60f45444-1863-3aa2-656f-11b23e237e29,813241560,4293687325049354098 +011f08e4-3ce9-f5c1-40fa-18b2a24e4908,3955416614,822990825760256046 +ef1911dc-aa01-af14-6112-abe1098f513f,3383272002,2379023198591492126 +47fddf00-0953-2602-f414-084cb9ea1329,3608672793,2774787401328034137 +f9edeb60-91b8-6e34-278c-f3f13f44a3cc,1222928301,1190095281624297546 +8dd25eed-0ddc-93e5-acf8-7e15d6848392,1510723759,17132257134400683793 +def5e4e5-c4d4-0639-e428-81e1cd3ed90b,4213616256,1511297844014493272 +79957da3-7f0c-add9-3242-74365ae686d1,3923809036,4753323734649602466 +add78d02-911d-3ed5-b14b-20513dbc81af,2116160534,331596539888831817 +3eae0384-363a-bfc4-6ee2-219bbc9810c1,2545247242,1621848032735191052 +615ae8c0-5f59-1255-92eb-fa4e73704333,3921909242,10269793803443573761 +9448a19d-d4b1-9ff8-fd2d-e39e30efa04d,2124799217,17397521323586926708 +4974dd0f-1329-f64b-8f48-93d8ca985b30,4052920134,6191583628948265360 +fff4f6c3-e56b-dbbb-bfb3-2e6ede047671,3525285231,10058857747120359386 +90a6357a-4f57-8791-f925-34f42b78cf62,1822052295,12944440674621261541 +49aed2e6-2a95-1347-d460-9e9a02e42ec0,1965595420,12063809230935011342 +f5c30e0b-ecab-404f-51c5-b2130912eebc,3607104754,1363617857805960353 +4c032d15-6878-7a3a-084d-003ae0bba450,2552502929,13985867444685177283 +48322ac7-1592-3c0b-4d6c-ab2f8386f09c,2086287457,1489706322293457796 +b21e0b0e-60ed-4939-d1f1-681c31d48164,2537732745,2994846130100619094 +264517d9-a835-b1f4-805d-6ad1d506dd2c,2178077418,18056714896362423072 +2998f77e-e334-fdfc-b6a1-e07047c4c28f,2688808981,5610372411106914279 +dd609398-c7c6-a672-8fa1-cee855c31f03,126967906,15433895577159433075 +9bf81166-8984-6a5e-44a5-eab04faedba9,2584202887,13342645404224178051 +9e33dce7-07c3-b463-053e-5af57ff5066a,269711925,13227159149315397922 +36951276-f90b-8482-af16-5cb5d63c3ddf,4105330224,12799651952296520583 +426f610f-7a3a-e31d-7f3e-71a61b024005,490782320,2014959581465334101 +06984942-2608-48cd-e842-69ebc7b82579,2043674636,12564147936133197026 +832e2c00-4c46-c28d-23a9-b57c8f08c6b9,3662295352,11972721053766493614 +621bf36a-7cd4-bddf-c688-82441c7d7a87,315848524,14915243874774003032 +50339d7a-672a-3e0e-34c6-e04632bd00cc,2744424716,16530541793447932674 +78243170-a46a-a85f-e6f8-963115a2296f,3651734775,5887535568651952128 +10817d15-49d8-cda9-9bdc-c7b103a8d0e5,3226582722,7669040167811637956 +19a84b97-fcd5-211a-5edf-70852596d031,580463283,740910569117677575 +471620a5-9ed9-435a-7fa1-4072b920b00a,3943644588,1850860452592736634 +a34f84d1-3c48-e93e-2841-8eb3b6a6cf4a,3338401820,6575133059961371042 +fb382210-a01c-53eb-6f07-ea6289a994b9,2079766259,3247526723005471179 +7f9e9a16-c1ef-01c3-f1c9-eb303a2d9d58,2761414360,10817572029029402319 +756ce2ce-3a46-c161-4e95-2685c85326bb,2296597416,12024993261827233208 +1b0ee098-085e-c57d-7e62-3794de27622e,903963355,10199262123155366164 +b70bd993-f53c-d3c3-5cc7-0f61e8451e82,1154922453,8551469857737738860 +63c39e2c-4a70-9319-a763-347627b2b9e2,2477495608,9036521242022277024 +670b941a-038b-7f4c-15a8-9c48157cd1b1,1629509446,10944721119627989413 +ee68b0dd-d3de-2d40-c11f-e1eb1dc7a264,475069189,2317050228358352007 +0e2feda0-65dd-ef80-b4b4-53ba301974e6,3831797766,14285531051405606605 +3322be41-6d2a-24f4-1df9-037d2642a0df,255322359,9113897513090088645 +f37e092d-dc6d-de01-62eb-591443edcb6f,1774411042,2155499389486234127 +4d3c8f07-b29e-5ae7-d3b7-350348755f64,3614835111,7880525311243819308 +578cdf2b-ba01-2449-99db-3852a67c51e8,746234585,129372648859799615 +ec430b83-3316-5a4d-8dbd-bad41a7825b1,2478359454,14955193885860389915 +267aad50-d615-60b4-e052-078d53e39498,1394791948,1620894722061988571 +7c8ecedd-be17-d0d8-cbc7-9bf95e65cd01,748461845,7152720073723526992 +1d097454-378f-e453-f37f-140d1de47123,2307206270,1945412601619676954 +55a1f197-4bdb-c840-a2e2-bf020933e791,165266340,3596881322044976755 +1c097c5b-3625-f4db-d2d9-12b0242a51fa,3245005981,17575155693655514452 +08c54da9-8a79-60ed-5b9f-e2bd43b1ab61,1042647888,7261179356890557630 +5db2603f-19aa-d5fa-5c21-e93ab5f39a18,470606670,5541808738129772760 +09182624-96f0-7452-e231-3abafb59f5bc,2742699736,3813853871806094463 +cc87261d-88c9-bd4d-40d0-3c61667a42c2,1013386,15972757954044279747 +cc7ea211-ca7e-6692-f9c2-47764de00bdf,3669073985,16310142826600201726 +5e7c3b35-d9ef-6a9a-2d7b-5a2e15d13f52,3497080122,4257601795161330807 +30df10fd-d374-07d0-aa4a-c4593089468d,1833211699,4419736224613214516 +0b068893-0fb0-3b06-1c69-18bfc6c35d87,2976402040,2897208232986192 +bd3f66af-b9a1-0b84-b43a-bfda9f5220e9,420879873,991894041866522290 +cb7a7ede-3e4f-39ef-9121-4d32630b064d,1375059012,15539309183790030225 +dab1880a-4754-d768-52cd-51659b532aa9,1830134185,1223083819496581404 +6bc0cd8d-6a0c-7b38-e93c-c5f43936fb50,3309267279,11762547318998816331 +de59c2d2-a53d-ead8-0ffd-8376107d4a6f,3121389713,8458927347267866582 +bce11899-75c4-792e-3d88-caee05431704,4211959944,16682794538570103062 +20d2d8c3-ac01-d38f-1cd5-2c16224d2a46,2989198526,7678394067371535444 +116afa80-962b-77cf-5d94-1c6dda8608ec,4169481813,11117741341135022478 +e9d9b7ac-49c3-5bdb-8964-0e7026d2e0bd,2956930006,5844631709839401919 +f6621fca-ea20-ce5f-e9eb-25badc29846d,3350118330,3947764027495036525 +34dc68d7-caf8-358c-b0cd-07a0380872c8,2740109682,6891005848745568464 +48fb11c9-dcba-0537-27f3-a07877a8af25,1854953025,13762069858029106169 +84da13cf-619a-d853-aeaa-6ebc1abf9ed1,3189312280,6369971483581259427 +2cda3361-95bb-bdc4-68f7-34a9789f410f,3501239986,2693211966282406687 +174fe635-ceb0-fd62-7209-5cb302f3e560,2179414095,16608506242747016683 +9ca2426c-3d12-60e7-993a-78f540ff490e,3300348909,12345805466614174620 +bd18921e-25c9-8352-b6f6-a7ca01dabfc7,761347698,11677143362366447909 +4d507d37-4ef5-2ccf-a82e-bd5c544b1b37,3727673616,5372676260443567749 +c341caf5-39b0-f724-a056-b792f5795981,2023065195,4549191946041810299 +952110cd-c1ab-c01e-1f45-405e2b7944e2,1613246030,1553105035395441651 +9259e906-fca5-3b1e-9e4f-479bfc5e3efe,2880745767,4670717067736572819 +f13c2a03-fefb-cf13-8bca-a1391d99fb4f,1285498357,9377862316658877422 +3531b608-a64a-8a47-c19c-48392c1d0100,2085326262,18187904753467212065 +30461031-df08-7b26-40a0-875835fcdf16,4118339845,17900595568370633642 +83f59ca4-c92f-d823-530b-80322964a9c4,454930014,3228412022051921708 +fbdd025a-fe0b-4582-17dd-933643d9dd64,2313050362,5921505349938093995 +5601a765-97a0-961a-e787-b8a9f6bd534c,3295375987,13048916246221282662 +053a13e1-653e-75b4-6d76-ea6954dd7e04,1083050583,14691485873912501870 +ec969004-74e1-ad61-230a-8fdfd53e785c,2149240322,13021463511639646258 +178f2cc7-18c0-6da4-d537-d9c112083db2,2596836241,7975280202610258016 +adfead2d-7b15-4c2d-a2e0-25f7b92c7970,1916219103,5783562915285263228 +49e40d8a-baa4-cbdb-b716-d69d273a186e,747881871,4791285508538089260 +ca586a3c-501f-c78c-9b06-541226de492c,1780546234,8434159410945122090 +1c39632d-0bb1-e3f1-3191-b047f2c7721d,3085566197,8401314294463496273 +6dadd14b-a6bf-9f40-35bd-b936776e73d7,3747562856,6670908483890222565 +f9cb7f33-5640-af60-e871-469045115456,2073005626,1006649829471660770 +47a94e54-b419-9bc9-a072-f3eb8a5b04e4,548437580,13342100202176974474 +2e91c4ec-e234-351f-cf7a-d8e8805ceb45,2218672172,8409368678122592659 +65decb64-3583-0b52-dcca-1bc62eada3a1,2288152786,7886674456159160048 +1909ae42-200a-2607-96f4-0d62191bfd50,2478558880,13599025851019905153 +e673eb23-0255-d131-3b14-548c48b9c105,3856480803,17771810441075790151 +7edb528f-66ee-a60e-6d40-a56417a5756f,1193174849,4447018581253340972 +76871d00-a1ab-64f0-0b9d-b52eb1f0d2af,4083530820,13049710725323205076 +1905c122-4587-7905-7793-eac5acaae327,126043603,10355173651272142624 +6ae045a2-973d-8a5b-9e01-bb907479dfd4,2369721222,17244684345483976358 +00d8ecf0-5f1c-bb94-8fb0-c8611b2a93ba,1538692220,14628589762342761605 +d7a44b9d-9928-738c-c899-af3e42436451,2738585999,10838395338387417465 +c7dffd01-e1af-300c-5026-cd2663622292,744828498,7783949008092602522 +1aa48f7c-3718-60a5-52f1-eb56511051f3,1804270860,14976881531990006350 +db8699b7-cf87-2a56-119b-7451a5a68d75,4023313346,1001011473045324323 +981ea1a7-d188-e9bb-9ef5-be2d9497c743,668872255,9859531677797774908 +7b5fcc50-fc4d-1d6c-3c24-442cb29e4acb,3523203659,9832728834416583557 +8c714186-89c6-a198-eab6-25b3bfefe04b,1828929149,13564439311041624666 +7532872d-b037-b6b9-4894-cd5ea6d28d6e,1417521877,6403065888849016856 +05c98546-5067-1735-77e1-9ef2d979cf64,3047268342,5196936573480315957 +5cd61c5f-4572-8898-50a4-0891b38ec7d8,3937118411,15785394899360081748 +2bc8771a-6763-ecae-0a58-143ba00aa73e,2225251514,8371436840734151202 +63416f6e-ea7b-8810-cad3-c08bf813ea26,3504557449,3211694863462127737 +1520ba61-543e-5fe2-29d1-5a9827689484,4247520213,17847977884770960815 +3cd41dde-d784-f957-0ceb-29b82b821537,2111949322,17728205083962428305 +8ec31427-6d21-4874-5550-3ad4f0e113b9,585072721,1289029504497910554 +3f847d2c-43d2-1385-9f30-cead39a79340,2840257943,9017899100065314588 +3bd59985-a611-5f5d-5893-1a267c33fa50,2429114671,14573735793792005443 +2ce2f75b-2e9b-824a-280e-2f96ad5a36e2,1541023664,15755243348707601165 +526c4220-7f47-c9bc-2e27-82d732ef465a,913924949,17948342154050979064 +be611e63-4519-17f7-f693-46cc38cfccba,1874380777,4912452417305178643 +28ea01a4-469b-c291-fad8-238bdda6797b,1296705416,4027042255602052840 +43e293ac-13c2-2499-7d6c-a7976bf6beb5,599359689,2170906296829851873 +c4976e19-ce65-84c9-acef-627d89bc1e93,3861499772,18320247268702027726 +d3871d72-ea7c-22c4-3f9c-d245bac9ec83,1864558247,13159473387334574253 +8bec9335-88d3-1676-7d40-33674bfd48f3,3745316572,4167059829612213071 +8ffe2c71-545a-783a-0cd1-861403300058,3762306327,6069249990544495048 +af6d22a3-ccd4-dc2c-0adc-f2e0f7d9987b,2877423352,12274990933431424840 +ab027373-c294-b31a-cf48-e9f4b05fa7d0,3332817312,8006111219579804874 +c475cda3-2d09-96e1-8d88-3d33efafe0cd,2795374210,15242838729151237310 +e6c52402-a2d4-17ee-9861-d96ea59aa297,2765836085,16440476230842771841 +548dccff-59a1-81b7-a676-ca67d3edc686,2525329335,17178389328318871368 +946d1f09-8042-b43d-5b95-6d9619e79713,3420500934,1041985937153497200 +067ffbd7-97fd-ac74-db3b-1dbad0d93fee,2013455568,9995403304469314383 +0e208a4e-e48a-c523-10e0-ae88d4478539,1563127611,6521767020831593817 +cf40ccd7-3237-5871-c259-f25c7b5dfecf,2698303774,446750653535855690 +9153f7b6-fd05-1293-61bd-9837834f8142,2694923570,7970003588652063727 +f1951bed-6fcd-f48a-4e7e-86adeddc15cb,1195557344,8391204129041061648 +9796564f-a4c7-7b12-1381-fc263663335d,2909342351,16942930284090514092 +896eb464-8c94-60fd-04a5-0ecc2a7ba1f8,2074638602,16068709752369482688 +c37e4e12-9829-8866-b489-89271b4c9d5f,3419679711,5992133618134041630 +b94b8cc9-63af-5317-202c-643e630d1f41,2290774768,1913102590064673609 +4c0ba471-4c8b-6259-8eb9-977ac574d7b1,898026083,4868256868460019396 +7278db88-f5b2-12a3-021f-3af02757b80b,3778040064,6151918477099412832 +781b409a-c53a-1569-82dd-1464e99d5f4c,905690960,7516331065961505926 +99796598-2473-7e2c-a421-82438e5d2755,2590608510,2111821344797504812 +ba7071c9-cee5-10ce-32ec-4120aa12b9a9,523629610,6364839464559953026 +59d823fe-3380-0504-154f-20ebd7ecbadd,1118405972,11273633606695085075 +1d2bf8ac-93ed-837a-09ca-40c7de3655f0,852928848,12189882926240024158 +eb4497ac-ea0d-49ba-af40-5d033f4eb9fa,3235794869,11167143388924757782 +32d5786a-c18b-2f13-c0da-8f826e18e0e3,4276030559,3270438348923981233 +a05561ef-1991-1af9-c9a9-11ec087a4421,2065085921,12152371944883308454 +c3a4a84c-9581-40c7-dc41-3fa829a47477,3583255249,191274882363603237 +07b7f79f-c371-5130-fa64-210edf3840a9,3320680097,15474414069841846111 +c223b650-05f5-bccd-5af4-7cf78aea1720,2471786472,10201212484590933462 +8aa1b261-b7f7-57c0-de3b-b2455bc80e46,3268870353,15974736586764921403 +af6f9837-05fb-e231-80c7-ef7b7c064b54,2657119121,11887130902274703243 +6ef3d7da-16a3-aed0-a6f8-76494caaf1a1,55521506,17937060597150824086 +96db9c78-ee9e-5d47-0efe-d74241ef05fd,544238531,17486260357540824095 +26e8326c-79c4-839c-04dc-dbe6abd2e11b,3359061141,2082710214508898932 +f0ef3287-9137-341f-6ad6-6644eaaae5e6,873706789,16619016487902940366 +30ef32c6-ad01-6c48-a0c4-24db89355721,469422495,2362982996640343548 +b1278fc4-725b-8271-a8ca-954138a597d8,2297905301,7128787530083133538 +5fb367a4-e1dc-d4d7-69a8-8a4e9847f357,419400701,15591411176256357588 +36d9aafb-ae8e-ab49-af7b-bb8c18f35b17,4153832629,4991254392764555097 +8cae0ff5-dfef-2975-060a-3f13764b2180,1006800160,16807802810167161498 +98c1423f-34bf-de6d-03b6-d99db4a341ab,617920594,12962342422136052825 +4aacc21c-3e95-e036-780f-4cd9422034dd,4192244554,14762268346752682792 +922aed50-3404-5f95-e05f-746dcc973929,1227868603,15289261772909278853 +2d7267c4-a1a2-5ec8-996a-7a30a5816827,791582918,16654884109599676419 +2a878d0b-5869-8833-398e-2f348952b670,356589445,7027593889249066802 +ec788fe5-ce2f-d48c-8520-e6f7c526ea9a,924392906,7936607001706232212 +a0fac6a0-cb92-4fed-24d5-608354e6b4b8,1693156093,16854449191026286632 +f864be45-6a4e-86a6-4239-4b5845f57146,1008590771,8780640172190379348 +f38bb600-df1c-ae2a-4185-3d187680e788,2308311795,9859115866129102130 +fa389fd3-3ea0-c25c-b854-a4cd461fdf0d,448434809,15594604558349279417 +d0bf8024-53e2-76e0-e553-1d92d11d0302,882320615,11422671215485796924 +6ba52f4f-bff3-c8c3-efd8-6b45ee365db7,1674115602,13517454417305307417 +53a2324d-dfd9-39b2-5d72-616057613e4e,3927835476,10410007205600270133 +0a715055-c248-ce45-a626-21975fa43e4d,3041771394,10099902380725611909 +6902284e-3fd7-c206-8ddd-6890cdde6294,3582865344,5705560271052990775 +73915b84-0132-f74f-ab9c-fc9ccfe619b5,1026525133,9571328743583686189 +1da1660b-9c42-0dc4-828d-848f5782db2d,4054991503,12564437286086955494 +29fd0379-eed9-8494-3d14-9211836b6f08,303084840,4449958216761978290 +ea74f011-6472-e973-0f46-becc8f1c8b2b,1365891233,2138406035308163135 +35a927ab-025c-2d59-3dfa-d23774a73375,1199016293,10419220605114314489 +29549822-6e26-ec65-193e-c94e96e8ee25,1464010493,17618449724920230906 +3a77d1e2-2412-d3e6-4eb1-932030dce1d4,878460063,157447625523801565 +458341fd-0a25-dcb4-7eac-9b823d08c08d,3167202894,14626472144422738830 +4e113e8f-acef-5ce2-9b1d-7792998b633b,3770051330,11253777969159368539 +f642f975-e63f-4f9f-9137-2a0dd062f85f,1678527073,303136115949769063 +f67d9cb2-41ce-d831-de57-0d87f017d854,2815802993,1686098111992898424 +d62ce96e-cb82-742f-7c7e-6a049b471ccb,2094846784,5446267986041652252 +4d322866-cd26-08ce-2bd1-e2c5171bbf26,3596076386,9536640660515333808 +51affba2-313f-5671-41fd-d40f75710dcb,3290918332,2253446225576056026 +4bba18fb-bbc2-1e38-d459-29b5d2ab3e7c,75384091,17156718415112141859 +81fd795e-921f-e5c1-c1bc-954735709665,2223255834,2552643116144878420 +7f38b600-1a1a-075f-551a-cb782e81c7ad,3161004696,6939370429672612399 +ebe52b07-bd51-9b8b-b0ef-04283a58f327,2622065309,10485039724840098952 +e12aaeb7-370a-167b-3fa0-382581c5637c,2983901114,7948274383025775330 +b4e3dc1a-8db1-ddeb-9992-f9b153282bd6,369123702,8231141666962906572 +60023db4-ba6b-7b37-87f7-e4c4b349ba91,917138879,5409585884070464503 +8557302d-8439-1f62-7b20-ef0188e259ed,1124083897,7244747029465664579 +ca3cc0f8-49ce-e683-4902-dc0d6425f6ca,771896142,975966677409713304 +c8cb23e0-67bf-a861-0f6b-ca351f031510,1815694901,7437115403084881833 +08407c89-1394-9f1c-6b9c-40428a245dae,427498229,631249069072722137 +1b6aa21d-af0f-03fb-bac9-c0d0452bb523,169396900,10794614006584942620 +09a8b5fb-e771-7ed9-4dc9-89db8670e298,3730906116,3860821121702322104 +1e86beff-071a-060e-1e0a-2d08ef77276a,2409561655,11600833306259915723 +40445b0c-2c1a-9eef-eeae-4899ea0add0d,1779369713,3801582601712261088 +bc12d3e1-bd44-f695-a1dc-e8e228872bbd,631831440,4099892287047474061 +7b39ad47-2ac4-e27a-b1a1-c9779b200391,544295141,12030379335838607505 +18a2e411-8e54-b645-0793-a20441c0d762,1844965534,11408524497545933793 +0b440747-fa0b-606e-dda5-5fb3d8199294,2014615539,11804785214989830672 +01777ef2-313e-408c-fcbe-81fd62e117cf,1004685798,2895059686668556779 +eaf96f5f-e89b-3a2b-76a9-a2f1d1d2141f,3982302077,8035814730384670760 +5a00d7ab-31d2-27a9-ea1a-d9bcd27635eb,2074330213,2916685088193867935 +85f38d4a-bf8e-fed8-c39b-205bd751ccb3,3337854313,16604265752219050151 +fdf9fb5b-2eeb-177b-1dfd-36be33af06b8,2324819224,15146583379379205591 +3e2575b3-8b55-b685-1f43-8be979e30c4b,2598336883,1566116281061926972 +9fe757cb-feaf-9318-4cc8-daba2fad12d4,2291898961,4048393036300703621 +8bce947a-7477-2ef7-9a6b-abdced74fb47,1483234184,9448928890699185075 +549e5241-5bca-20dd-dac6-e797057a433d,688984875,2329612230321926206 +8eb8a7e9-196a-60d0-d059-ad7340cf2e8a,2731970827,2025659771262070908 +2be58c97-e9ec-d83d-2249-f25633bb983f,2441470066,5414306568029234073 +5a64d947-7f96-12c6-4a54-1ae0f5f8a5a2,2890449883,17986387861951029955 +a23ecd22-068d-9090-7962-25aafd1d2003,3553082988,2364999809266111904 +7578b628-d61b-5747-c7cf-7ec76dd8b9b9,115913680,15062542760774678150 +8e6c065e-a7f6-19a1-833f-fcb05112fdff,4051062250,4291497030419032208 +1e428648-c76e-8f93-ca42-567dbb2cef26,647119720,10423845068576144862 +de80b8d0-aceb-6bab-85d4-67f0ae5da87f,576168996,9037234023286176463 +c2495558-d52e-7e94-1ade-fe2424f8bb79,2448699089,12229427965916351773 +0d03646f-0b56-f23e-2cc4-a7fa94b55120,1924966156,7773947371994185108 +f4036b9b-7dab-07cc-ce50-a9f058e96fae,82431214,7870866931639009089 +753766bc-63ee-54e4-a57b-20b473730b30,2451491126,1089719117728266043 +7e57bef4-86d7-e8eb-809b-76b47a3b2dd6,3321238064,17409292651136331286 +72db6066-7a73-f83f-cac5-cf949bd4f6a3,3408554480,13635787108022838963 +66423f61-87ea-e71f-90c5-633f5e19b37e,2554920211,5096793107627319575 +b115aed2-f9f7-1168-5c94-9a53aedecbbd,3907892526,13507834148336217008 +7c48cb8a-0cd6-301e-1811-52b58a29b666,3109965767,961736385684220577 +8892d8e1-87c4-ce26-17c1-d528395bdae4,1471989001,2127182582631633493 +3cac3bac-1f81-63ff-6922-5e2e4fb55ae7,1993461991,14566098004610603415 +7f39d7ff-a36a-e284-c365-87c36e16c01d,3365030457,2796454016066279434 +2d6de24a-4f68-0300-4c00-16cd711bc313,3261141713,17042940019398230164 +694af77a-0a82-2531-955a-def8ce69a7ce,839172988,11990670677225024251 +7b2e9f40-ac0c-fe8d-5133-0912b7819407,2120029161,16803096649162885377 +bf111ea6-173c-e004-6363-cacd3ba1b0e4,2287095260,17639450565810076856 +e428fb92-cc68-fd44-703c-78e14601d8eb,2503637378,3883315212081204680 +756b9974-afbb-8b48-8eaf-538954cabaa5,3491569184,1690080893003714025 +4d6140da-7b4d-119b-c034-07339987a052,1455837894,4986977136168522775 +aae1200b-4dbf-9114-8987-af2d1e1fb400,4206366612,9895377078688151560 +8939ba75-deb5-bf1d-ba88-f339df786339,632423052,13022761571142111409 +cdf97b82-cc96-1baa-2f0b-f3d417a33f03,171014153,7595053806651722613 +4bb4d140-b9a6-0137-1392-1db7038ece60,1298645875,16945465670205413597 +1bb07c09-2740-aa2f-ef17-c6b4fa29c09f,2114508966,9193330734935618056 +de666636-7918-9375-8565-fbefedec18f7,2399587547,16775547124527487320 +d83558f1-604c-028c-c2b1-5bfa774f9ef6,757331889,9149507608089890907 +51ff293b-3de9-3c09-c510-8f2c5be60d65,1466993342,17065613850258209441 +b09d1d0d-686d-04fd-62ac-7ece290160d9,1477327185,681953690015762124 +a870073a-a599-976c-76ab-00286831375d,3218374302,14079718650768240960 +d99bffc0-074c-a7f8-8197-76ce65ba52da,895677447,15812010688916877709 +466c2d42-b02a-7087-a08a-d32b30779517,3837108764,2873576582010543164 +686a20a7-8138-fd15-5e77-d6f262527435,1640039790,7594614905707734507 +a66840cf-be2f-6683-b186-4ea44db54323,3987035001,16545858645601795602 +f15b2c84-79ca-e55c-e313-6f5ff50a9cbf,2280140723,14693644427002977840 +bd6570eb-cd19-b83a-6f70-8b989c5493c6,3522263849,6618468050593456135 +6a6c24ff-886e-d280-bef3-86e1ef1b542a,1568698315,8792406380317446816 +19ac12c9-c98d-8ccd-80c0-420f1eeb17de,1102329430,8315768133941241920 +f0dc7da0-3caf-7618-2185-4d36c2e9cd1b,1895045170,7336088335853921392 +0cfb217f-f4b9-fa3b-5398-8d5b96377cb0,2697280285,15134014102864984869 +5f652a07-69a8-0168-0403-304914713b82,243181996,11995421062714107174 +d994928b-2e4f-ebb2-0d9b-0b87180d5c77,441106914,17373011931327149774 +9990ab10-2733-6cf3-45cd-aa6c2c572f09,2799462091,6806248026839654309 +a63cc503-0c7e-9a52-f9db-942f998d6415,1984277854,13125006299556999184 +a4fc2fd4-2f60-1884-001c-892b2a4981b7,3780233172,12731234365948868818 +03bc9fcb-3777-de6b-8cda-06cd12c61a10,4162296180,3442709374581805853 +9263516d-faf8-c648-a8f6-00366ce88b0c,1720509514,7209027179673936454 +54b3bd09-6396-2745-f0f5-cdfbacbb975a,3548990231,138680712776837301 +a87d15f3-0591-9b70-a200-7b135908d063,138930829,17752078915442129673 +106685a8-12ea-a78b-6d10-47d9c1afdaad,296518364,15221366344968332013 +9914de1d-f60b-7b6a-0704-29126161f1b2,3575406553,10965143144054199137 +9bd0cb2f-cffb-9e53-ccca-3f118f0d03b9,3286666969,11824867314185791145 +acfb4110-13bc-a34f-7510-562f36c21ef4,4231702863,6549013836517174228 +99336443-f165-b03f-1b6e-6105d714989b,1179394446,10069832914627389822 +d10db139-9595-c85a-867d-782485d2cf6c,2292198212,7917481824086973844 +fc7f6e07-7155-90e1-bb8c-2f07bdcda36d,2587369483,9090150002976597723 +d4da9801-d497-59a6-88c9-1c16bb5eca6a,395695149,425009235129772218 +d6d69e75-af92-8e79-5023-eb2b55804e78,2087714853,18300666636656623188 +7f0479d4-a819-af22-a66d-3f6e76dd9ee7,1227080633,6064848972681008421 +3dfbb226-6176-4fc9-fc0e-adff42b6a7ab,53539550,15910381044367165620 +4ef8ff33-c378-5fc7-2b2c-4268b315c275,982227106,1196679896700423514 +521e7845-f512-3752-01eb-8109d1a543b0,2808364484,12948025203608228292 +b5537136-568a-5dde-624e-ecbec644a2a4,2850379475,7874931389050476504 +ebf48d61-2136-bfbe-2341-5a6e316bb2ae,4196208438,3441462985420826835 +7efdc56a-6e5f-d2e9-9d3d-0491b698caf8,832839296,3776286137506846861 +626ab25d-7ea6-9a09-abba-1612e0f363a7,2162141783,13511279961772167750 +0af5783f-27a4-8690-ba6b-250435364952,291006638,4171025844137197025 +c66f4418-2957-02aa-bd32-2cc5c64e9123,3994217854,8711575249818805512 +ea5548cc-b8ab-5567-0943-98c435bcb232,2553363786,7445454274117105470 +6a19b45f-dbc1-f582-0eeb-fbdae15240b9,953002143,15500221479748401564 +61035dcb-2046-73a6-a5a9-11be5f3f2007,2085382841,18131441035127778957 +7707d1de-ba51-a778-8912-d293fe3c7c89,3903724255,3539838951129854161 +3da783e7-d031-1b96-0ee9-3518ae5f7f43,736931243,14285395223699032423 +bef37849-aae6-493d-4895-903c7f0c0fbb,534765527,5388707143324063758 +f88259e6-95ba-f749-bd2e-fcfd67d212ad,2050974558,530690936517820058 +e1883958-74a7-d6f8-3231-dd27dbfc28d9,1227514363,12673940762642876149 +59d3e434-6285-f015-d8ea-98bef33f6fbd,1741246126,17942364292652040455 +19184a43-4b3e-7260-a2ee-ae90196e2d1f,4165377616,2498020163358831619 +14a41b43-c7a2-eaa9-d604-7dd5b314d82a,1936401747,9364704187233596553 +43d6adf4-ec26-6860-f2ee-9a115d000521,2613481976,12122673562479605614 +ddfdf076-15ff-5396-69cf-4dac97b00af0,3505432312,6848087996446108183 +c8f6e178-6953-caf4-2942-9a5e4b561b59,3041498493,2441873521533196409 +60e9a706-7ba5-a209-7b06-37f19db0bc77,3026588295,5629504402000127754 +db37daab-fdc5-bca6-de42-4db403234c29,804461661,2543645305959953687 +7bcecdcb-a4e9-0202-ce8c-dde950f637ed,4035675501,8789908641771079931 +8c783393-bd0e-e92e-1f48-118ffa2d3d75,1828316054,4362821144507529950 +435a5f92-2864-15a3-a9ec-fbad698aad05,2539311144,2069690793910178535 +8df52c58-ea29-c57c-fbf5-bd1dbc4c3241,3616404770,1792145188188229282 +2409d8ea-d917-f6dc-5806-b40610ee694b,2611541047,15047560989521097029 +4edb3c1a-e31b-1615-2a03-7bf00bc3191b,3724805114,14043901610288331075 +6230df59-1ce4-9497-f0f1-025614901c5f,2419609513,4518153236369433166 +6b9cb40a-1bac-d10a-0a3c-60a9eb85fd52,2373550051,11182131475115008519 +76444845-71b1-872c-35d8-033d75961076,257190821,15484882431647402562 +190ee926-f2ea-60f0-03c6-309ad7cb4a1c,3203321810,4306098938063255662 +77d96b0d-aa06-4f12-ade7-457bacf70e3f,3190099724,3683858845988348664 +b01988ba-e121-5f97-c194-a0c52d7d929d,2403841538,13818738898637437991 +a6ece93a-4633-0127-e67b-5d473cedeb79,3222696317,12067871058796892086 +af671bd7-cf79-24ce-48ea-e153abaaa694,681440426,12727734571712712082 +6992483c-8f0b-0007-c03c-9065c172128b,3624019251,2371513886804967677 +ae8ea9bc-d093-97da-a708-d9ea93c2d53a,2735470124,16717062171305286958 +c39d915c-4b72-9b12-7d2f-f73000d2c1a3,1059542637,7064957943080931179 +353a6a6f-c895-a32e-88c6-cfbc5b87e227,4107698952,16030860826772920748 +56095cdb-07de-1909-db79-33f5d10686c4,2527223401,14239127694606794222 +6eb965ef-4dc0-3bf3-5b30-2e3a4ad0566e,963258289,4437115097619496766 +117cedf2-19a0-5ec7-f01f-18168ad3da6f,1799567448,2237869767938126249 +8456e939-6622-6208-4e2a-82c7776642ec,579221684,13701718073649236576 +c28a451c-7bd9-4aa1-3bf8-c37ca6179e8e,1481453316,6575417742071118545 +3f8eeaa2-e65f-feda-b528-178a868319bc,466829390,8213336260982899575 +75a49e70-93ca-e69a-1fde-61b2e746c3c3,140171137,2115806116379109822 +e316d938-d58b-540e-0664-7c3911f4de10,1024642305,12189943319357451303 +a44d4be2-7d30-8807-dc76-d239e308f56a,2586402799,1166251746584905561 +8236bdcc-ea19-e032-989b-2302a84ffe74,604971283,17967102167483535641 +3f421431-9576-584f-91eb-1c5a1f057c26,3070921946,1910898221454510146 +8c8c597b-61a9-3dd3-c111-8c4d2936859a,3730892045,2410295358681391198 +140365e1-d5e0-10cc-32b6-52e884130012,3911379638,9778999228719043881 +f773ad65-2099-28b0-d7dc-fe3ac86ea7f7,1934370890,9455433919042388144 +d252931d-8a20-caf8-5eac-4769ae99c6b6,443481414,7284931976953807505 +4bd3b5e6-37a5-4dc6-3148-a18ca3c5deea,1828525394,3854789052168772831 +15fdc0ff-ad5c-e5b2-0932-a7c42a7c5994,2324360570,10325651231271066295 +ab6ee2d9-6beb-cbeb-adb1-a4f0eb9e6b2e,3145477296,16432480176543556865 +9ca5b3dc-ef6d-1d15-34e6-af6f520caa71,2864597065,5715781195717647754 +486c8010-3ac6-e167-ddc0-ce1bc117093b,4271399084,16025108817616395401 +07815150-8268-96b9-0de0-82939005ad39,190330259,15196168059165507766 +0e96c488-e7ba-4afc-6f15-f85b784e65ca,2357272327,17979805648095756931 +e7d2c27b-7aa0-d018-03d4-52fab7a616b8,1719738411,15010615553380529291 +7053edbc-6fdf-7e02-5dc8-928f93f41d7d,1131189606,3028267587168232826 +f4816c87-8f6f-093e-c1d6-6c17099ee25d,2274709979,15934712721652888278 +ede65b27-acbe-657b-c02a-64e0c7d40aae,1080077664,12189459487121025075 +68b65948-7516-e307-f084-c327f86baefe,2686684166,13384579644370006038 +99cb29a2-54a9-a356-5741-2661c54f4fe2,3004626382,12030337297768880892 +b7db2cc5-2d02-f04f-0d07-0efd008e1272,2396577253,8420506614005437667 +59f49660-518f-ab57-3c81-3b054d79fe7d,3582834772,10841043798286925378 +c19697cf-d0a6-4b57-f941-ccdb4cf7b887,1386311613,14405498235931505338 +4a59f458-c9a4-86e8-babd-33909c36fe64,1753882765,6640534698265419425 +9deae0a2-dbdf-5c6a-b80b-0a9fd69122fe,2051984607,14475112317885771603 +85e2c1eb-ab7e-6209-7fe5-8768abf82c08,817115630,8376217056803447699 +b7e71441-f99e-84b4-18dd-d7d958f0d5fa,680427446,12149755568896490017 +91d2724e-61f8-6612-deff-81d8d14bb2dc,2636359495,12677548758714935589 +957db568-75d7-9225-33ce-cf62a3e18b4a,820087538,17868364267019579260 +60582922-3798-24a7-9431-b078ad3bd659,1011099521,2592122366169910277 +e4f47686-7314-851f-6e53-aab60491155f,1576707719,14658725989628289530 +7717fde3-25c8-a14a-9644-d372f28d0329,232219465,690994796242993047 +3f4041d4-361f-2271-27d3-736ecb8d9e30,3243625421,16945545151750410975 +1f17bc77-1264-ae49-4e6a-f0e8337851a7,1196301703,10495754226119180961 +56feccc9-ff25-4458-dc12-37098f57b146,3970103888,13495401858443394751 +17065cd9-0f70-10d7-c5a6-0c5448538916,2004615612,14604711185639375338 +b971fbb3-b08d-672a-821a-2297f062a26d,2545556422,8675944270215523068 +a0c98f28-b81c-0ed3-9710-8fbbc424cb9c,3022770566,3585992044546222660 +c97a80bc-17cf-3849-44ea-e78bbf4f4bec,4150179840,12622778051267315393 +a8c73cd1-4557-61ab-3947-e18030cba798,2096678566,10790459263406151673 +1cbc0186-8d00-f76e-f4bd-a81f67452f7b,1901562414,17227454738994112469 +9c84dcae-eecb-614a-a079-bd59dfd8d784,535034230,11705432572612873167 +50f67042-510f-ebb1-7509-bb835e26cabb,1642216864,14049947881152373049 +6696b858-2701-8634-8458-8841a659b8db,3455601075,17704065262092875651 +b8d007e8-e406-bdeb-37cc-bd319ba95302,681184857,10152211422964171239 +26bc4a77-d339-1d28-bd82-bb707c406dc9,638749210,6849116073641552260 +1033a80b-f060-09b0-76c6-6570cf0c7b41,4257207490,7796518453622074055 +ff7e9a49-cf6e-ca17-1c13-f1b08e9330ac,4022450041,5154185640595269225 +74c1b638-253d-5135-1866-3038ba9f7011,2076792594,574879069129720480 +888811a8-65b4-03b4-32a8-70fae886c616,3694301825,9485440389507924982 +fbca3390-10da-f050-03ae-f104be6ace5c,1658985152,7345873728366147126 +86c88cdd-224a-c093-c1b2-1eebe2f409c6,3282238250,14054991300485281946 +da7a16ca-7cd7-9b22-0dfa-8f21f9ac8025,3418594667,1932628717549317601 +ce453ab8-da6a-62ac-aadf-7d3a3d967340,3477838496,4169697602643500141 +98f39d59-40d1-d8db-94ff-1d4606a9d880,4207979718,4225225658746920220 +814d2fe5-f186-94b3-dc00-9a8ba484f68f,3219831085,16973329252422089482 +de124b2d-880f-5fba-1427-f15d75030035,2878504436,2754993923712528533 +cfdc9c9b-a91a-3dc2-4a36-18f85d76d58e,1967154662,7399177061768321070 +df586865-69d8-6ade-a5d8-b129e4e46327,1039110537,5916809055171645993 +b59551f9-2824-cbe3-bcfa-b68cd2a2168b,1511963298,3930283414455322613 +e205dfb7-93de-71f3-7264-1e0c0ba7702f,3854010911,9932655349910819856 +bfd90d52-09af-aedb-dfab-fb7fe7faba92,1052635748,4388161182256326687 +bb89f6de-5ea0-82f2-3667-606b8816a0d9,1150372432,1616944799619117778 +48d6f5d1-fb90-4662-08db-34664b7e1a41,2799101605,13256926717047093174 +94b37dce-f549-0aa1-1407-29e8faa82c98,3520543925,374270036683540212 +b4ed5b51-1e3d-38d5-2f45-4ee1ccf202b5,2398767848,863162687761146630 +8162cfb2-e317-2870-e189-3b9abcaded1b,1500691670,2081483388595298638 +819993ba-ef56-ce1b-1133-f668614d2df0,1347567467,5297690524204147025 +0ee588ae-784b-24ea-fbac-bd56b76b6d3e,4272651353,3089826234619985444 +bbb38cf1-7eb2-93a0-9caf-4531520ad68a,1850200438,5681424668702121690 +6147c032-def2-385b-2af5-9cca2aa00e30,2201121492,3878847315838349199 +a901b9fd-a3ba-e2a1-8444-be8c78a41cb4,3519122893,13880919150093076831 +070a8a2a-7767-69e7-f536-07eba7af3549,2768968818,12380523631322903841 +ca94716a-eea5-f564-50e8-a8ed1ef9236f,359808159,7267713808151647105 +740b5781-cb97-838d-c27b-68af22400dd2,3457069022,1659021869771241933 +e9dabaca-8e53-d3bf-7316-7dca64cd63ba,2512954573,6927782260724078627 +0f9671de-be39-85bc-0184-f7e6c8f50ac4,2183113732,5082727165245061816 +0195c03e-b4e0-b4aa-5800-5d77d7c5b51d,3098002651,1825351553079544719 +de85083f-5787-e902-57bd-037f99b20674,1417930154,16856641000298658060 +47591d41-8845-1a8f-65e3-a9037b256492,57602053,3224038039534546250 +485175ea-7e47-6d2b-0dcc-ee69012a5a02,852311542,8511046771075616140 +35e1e388-eeae-5f58-fd70-9d270f2dda23,488709196,8320869748888761050 +e147be86-94d9-db7f-a15c-72c701192e92,118489456,5681725699651214945 +0ea4dcb9-fe6d-de43-d9cf-059ec082e55f,1194846514,2348392721699218064 +b0247b44-d16d-1f13-6e4c-5a57d3268f84,1609938029,2956029385667631998 +54691bb6-1299-331d-9b51-494a3d4295d3,3175132728,5859855844120100609 +c09ed204-fe25-ced3-84c7-59edf9fc95c5,2602722842,1838661458159211811 +9b5bf18e-be8b-e010-5080-b83aede15bcb,934368011,3255097848521824494 +6f46c338-e363-32a4-89f7-9581da4c1d6a,3795280933,18076216163391129731 +e8ef550d-860f-4bab-bbd7-09b87a71d95c,4065598654,4426586675733731596 +41cd7d3b-729d-b7cc-be4e-b90b4aed5fc8,388299207,1808896481471083738 +f95ba67a-e61a-0d9a-a0d4-2041f3f65136,391755252,18083420704031757118 +d6be9415-7cd5-dd2c-0c08-007694baf1cd,1194574144,16216288721036828146 +2edd542d-dc3e-a37e-3ca9-f6ec45ab8815,1002668481,17030094338445354014 +52199bc3-115e-064f-22f0-789eddaa2f27,485679676,14780033244741931847 +ec4f6db9-72d5-22fe-4bd4-fbf1cbd399e8,2458709478,11598853455418148112 +4947d9af-d2c9-b6df-ed06-ce7ce8c72825,1703779423,17482957184559412267 +fa376811-bce5-4c60-6174-d61e7cf2ef93,1423549040,9336837604874264930 +06c82d9e-3955-6eca-58e0-d2be43d354d4,4202908265,8195696778116686811 +9c2c2081-6c37-00b0-5a65-6c2a7d69d3b6,1584706761,17000056025347283001 +08b8f220-8036-66b3-2f0b-8f47ebf4b625,907571566,4457888013555301197 +b36d1630-3b86-b167-d095-d9b4893351a0,89011874,16650084409419160877 +a5d9129d-6fa3-bfb8-886d-8a2f824c459c,2209458290,5166422292822024138 +eee65100-a4ca-7a6c-da9b-3e5e32a6c8ea,4275601027,16562270468471561156 +875e5b63-5ab2-140c-1305-16b5aa5084cf,1676566072,17089605393567924991 +9ddfef9f-e9a8-da56-f579-37dbbf741a37,2657360351,12123227938259213497 +6608b9d8-c5a8-4228-8caf-62eb289bcfba,3378902938,1656296166578500469 +fae46119-8166-ebab-3c75-ac93012e69e0,491437828,7816061792010390619 +7fba4890-8a66-28aa-b950-5f5e45a5b1d7,2308724258,11294559838923796877 +07befa49-0777-d4ad-c6a7-464f285c390a,3152898526,8043983596187879508 +d848ea64-29f2-c8e5-ea12-0247ed03faec,4173898001,7113363079860384794 +3fc0d271-c4e8-73a3-364c-9ea77110405b,2317404786,2401593520256823539 +d8788c49-1e8c-1148-2498-f1ed2f7dc334,2788345335,13210833452313447696 +5b80d039-2628-0b54-c8a7-25356fea620f,975978754,10885734085594137693 +d7ed0592-e2cf-079c-3f65-2df1c46bfba5,369580483,18288879639795562093 +d8c0b6c8-bacc-c4a7-b2ca-fe507957401d,3163382190,17565204783337410964 +03ff3fb2-732e-e845-f248-6af6a5b92762,241265058,10504406340801642064 +144a6101-07c6-f9b5-e7dc-07c47514baf2,2719018264,11099943518613906832 +aff7bd2c-6f35-8383-6774-f07cadb810a3,2936809996,8602128627446739390 +6f103df7-4ed6-3421-4479-7be0cfa483d8,2323621704,11597818227972803516 +1988fc45-e263-bc51-7836-af7cd96064cd,1564774979,18044115204548538897 +93d7d174-97d5-2c29-1cce-5f8c82c0f5fc,3437398754,18440558502821519824 +b02a74aa-61c5-dc7b-12fe-5951dd49c295,783930996,876840078906914339 +9fc2bd67-cd42-b88a-8af7-db8bb478e37c,2144888929,3870303328387187050 +3229d04d-4f24-c5a8-e25d-1e9bde4b762c,870009582,2847801716282754384 +e197a767-0343-bad7-5d22-0ba6dfa79bd0,4083506423,3252334974581103927 +911fdfae-4102-4b26-dc6d-8f95262f3cf5,934451267,14294520983400116206 +123e0632-319f-b89d-866c-d9bad949e4b0,2170962679,2341129675616529736 +8fed2811-11eb-ba10-2ff0-f62d3d86ded9,914700440,14225316374185470926 +219aa60a-b4a9-d4c6-d436-de647c13c93a,2947081249,17878789851739801321 +dee8ccf8-38c9-7d18-400a-f2e22a37baee,2616425330,7667963297696318832 +31ad1a98-8625-36f1-25cf-187d65fbcc9c,4040954094,3046948537060079903 +3c305ca8-5a6a-2260-675d-9300ae936c79,406208614,14337694819872076171 +e84d37d6-e6f1-47e4-b4e7-48c390c0ac68,1475780569,1250693179030894672 +2ca9a6c5-6686-112d-b238-656f9e35245b,286071471,1586745283892910816 +e6671cf0-75ae-eee3-684b-d8ace87bedeb,4042171145,12747756609357383749 +0db59205-3c3e-341c-cb1c-a87f1fc528d5,3299406046,2676512952193381781 +96d7983c-92ae-694c-3e72-86426171ce45,3882308473,7907077085485154358 +b5ceadee-dc68-a2ca-2cc1-879f2830510d,3150158720,11347116104985306621 +6a5cc629-f6f9-0c4e-c2be-a811c0e72aa2,918245639,6477903076380710307 +153f44f6-0510-7ed6-1d15-d57b3e883989,2599011752,17028341601741487551 +1d9bdbe8-5b01-1d36-1e27-936232e46c26,2529583513,18134936280516530221 +27b41423-2840-7ab1-866f-a3bd0ea38d35,1150123425,18133778821007256769 +5a475be8-e9ce-260f-35e7-e6f844680cdd,4276975162,12493612164616885951 +a76ecf53-1f9a-993e-d213-25a32314adb7,952431572,15499007756361729693 +6288cc58-8c93-c92d-1330-a6ccfe9d3d8c,20870559,14056560535166858081 +af217563-66e2-bd55-2c22-6ef3ccb36030,961154095,818948847555832436 +472a9122-7a13-08fa-6ea9-617c39bbf241,2728149184,17430190196940579305 +4a8f4ff0-1a6f-c145-289a-c318c147384a,2914778215,7806965865287312377 +10ff3068-bf16-5fc3-a6a6-ae133e15e3c1,1341325406,8761786621629230351 +bfb9646b-1f63-20ef-68e8-ac1f205c661d,2438166317,2308842746109759849 +d2ebe7e0-650d-c3ab-567d-b650bb99987a,908545845,13593642029043197366 +31111294-1918-74d3-d94d-d3eda6e4b956,3653180968,73961352331998445 +fa881486-b2c7-d713-fce7-b667d72949a3,1985520820,6691036271337001983 +3f02e798-02bf-3c92-ccff-cf0d85f2d36d,1663231761,8894539814418835419 +eee5f047-70c3-0abc-7ce4-850aeaf65f9a,3156025784,7553748095328253259 +2aebb3b9-08fb-4118-4ad7-2563e92c4b7c,3942291044,10484734625939192454 +f183b286-5ae9-e0d6-c7e1-dd5ff57aaea7,553602908,1637381444732825849 +f84a5d62-8ebd-7656-4b8e-4faec6af6f3a,3528304346,7720563044347159725 +7dd8b8d8-a778-db17-4b30-966aa7eaf132,1819709270,15917320153633905490 +43516538-d897-3224-fe4c-adab728723d0,4136484641,18276379265569517262 +34722c3c-9ce9-93db-d64a-a08c4e159a35,4182132178,6084663270304157267 +fcab0d84-78db-cc0e-2e15-522b885ab81a,264370268,8084052167922937952 +3e04c213-43da-6b82-f582-ea94a202aef2,467147780,15619134123602313063 +6c4b2efd-c332-92cd-6f4f-c363f4284029,150769186,6031769454432033319 +ab127a81-e334-8501-786e-1978c88a8957,1945760569,6917364268022994560 +1269d2bb-f7d9-3460-922b-bb76c9684b87,3874422712,3299661392399938561 +8350eff4-15fa-5a30-1167-930bfe3766a4,2368627327,15031961168268488029 +11afe6ab-6b50-7cb1-2cf1-b777809e3517,1951022204,957745298275504867 +f55253e7-bf3b-b173-59f2-7f9aae2cbc5c,709504496,14477560571414678422 +4f3fb7dd-e685-712c-ff40-72c5ff98955a,3479589101,144095994971228459 +802e4b1b-c131-a2d7-528c-8baabb802a89,212028519,15153652632826046582 +7350da9e-d77c-f82d-b25a-ab8fec97b9bb,76323198,14629340084423162006 +eba4ecd8-3f43-8474-ced6-5c1338ab947b,744185866,9292544847729256842 +f939cc34-518d-6afb-51e4-8f6b76016b65,4085919167,2386228949142055350 +51a90ac9-466b-9702-f3ea-8f2de7df7926,1558823140,8954438317115210367 +4a91cc46-eb0d-a830-6ec7-e612c104e0f7,3832414116,12194489608338592187 +b94e0d08-0f51-28c2-7eeb-75324c1ac0b8,4125140473,5585988105757663215 +50f373e4-59dd-95f5-0863-c86e543a089d,2108211609,6036203033812209679 +a3ab6ae6-4db9-fe24-be80-5fe1fed647e0,456506482,13977309138442026434 +ebd21e5a-2cb5-3a48-fa1d-b7c5e549db2d,2733980124,15468018553303393758 +1ae3d8ff-90ed-3c00-2d96-f21c63d4f8a9,150379123,3885716454310042612 +a3438f42-5037-1463-14e4-7a566a22780e,1263128589,3210776728620349209 +a47272cb-b34c-22e5-37c2-47268def6f34,3294626883,6704224085171359567 +762bd4db-936e-653f-6e02-d768984a3b10,3032195503,14840388990327719018 +6e0542a3-33de-5611-d786-b2339787f004,3952704710,15423844807571517486 +31d6b432-ffa4-86f0-2523-30fab426a5f3,3299835500,571480209020519339 +f6ec45ee-8a5f-ceb1-05a9-1a3a5921e95f,831695139,547302177820570385 +92150270-53d2-6e0b-4c3d-d9e5ba8359c5,3179047772,7591662849969594342 +baa1626f-dd5f-df7e-a608-fde8963329be,560096884,11568616418680527660 +687a28eb-fcd6-cdee-1805-b5ca4df32937,592140535,17592475791078250562 +c4d43b8a-4a2d-7b64-f7d0-57e7f871f4b1,693952288,14678884018192427984 +47699fa4-9f58-34e5-367e-0e8416a64551,1106629354,7890291715238023369 +48951719-d9a3-d664-7094-bed6116080ec,2044793771,4413492905610389514 +0d464ad5-d729-9c57-4193-09fa38ab05bd,228540638,14400949181270234211 +5a8d96c8-bd7f-f7f4-1c0c-653850e25152,3780561807,17479532353012671095 +05e7180f-de1b-33bc-6033-208bdef6823c,4177404728,9378080100660562351 +ff9c7e73-9ef4-6110-be2a-b7c867fe2e25,3166489905,12318650358289501866 +ca64c35c-b40e-1839-ec43-c5738c7d4880,4005614700,16280695556787247102 +72d005c7-e72f-8307-f265-ed40114000fe,2096484588,13412325492576968145 +9e16c790-af27-8d59-6c80-95d3f5d6863b,1301785507,15577606092825402634 +533d55c5-4218-0b4c-fc95-3c9c7244103b,2956371356,5369962221260579373 +5569a151-0d6e-f38a-a8b1-c775b7ac913d,1405588618,16178911124643162481 +866e5a2f-4354-e450-37f9-15d03bf23ec4,1063755446,2754284402685399524 +a94ee800-76e6-9d7d-dd85-1d4683ad4986,1592932488,714535952981896724 +d8006f03-dccb-7563-b982-22f0585d89c1,209264559,7166711541592416862 +c87efe45-7c77-78f2-8c33-a2cb0124fee8,3564910016,8987784579327171922 +b46f4bba-83a1-451d-3cb4-e3553b7bf76b,150961895,6720478471336345089 +d1ea3509-4aa9-ca62-bca4-e20aecee07a1,214164423,12149702832527656760 +0f851a20-317f-2b07-545c-b832b43d3181,619285506,16205883800885042653 +11cc87d6-56e8-c148-8836-245269c4da98,3533347294,6701932998991202852 +a2abf696-45df-9d28-0733-6850c6994bc2,4185207149,3781031075853341129 +dec0ded5-2fec-c9f4-05d6-482d68f23c39,3969943956,209887130447469771 +f6cdf885-0ef8-f334-b287-2a6b318a0056,2023069547,1460598720489473953 +f9ecff13-c807-9dee-99da-44924102152f,3146888958,11658025441465910931 +b3f4bc38-5e7c-737d-2e0d-cd0669061e8f,2338154933,15828082918115964077 +6325493a-4791-1067-7793-860eb80576ca,3443373206,7688503130481645333 +75478a8d-cc2a-9f07-1ab7-85e8bfea565d,1843031136,14930148610314560315 +a6466c71-af75-6ca2-a1fb-e5fec892c613,2361797269,3429542495763004807 +af52e54b-1d66-7c3c-8121-f7d2eab33eb5,2927278124,9306114784565748526 +2419f62f-34d9-37b8-3249-fe57364faa82,1168748889,609079878641278438 +0b3f247e-3ac5-de25-84bb-961824074e16,220280075,376239536740140566 +7a6758e7-2a54-afd9-ccb7-e38def302d43,3287338618,5048788019870328569 +21acfb68-1c40-ffaa-797b-d775b1bb3000,2478557085,14693596443047194526 +318f0795-63eb-6e7b-3050-a8d880feecb9,864136389,794756159134217412 +da35c6b4-d24b-80c1-0db6-e12c0c1726e3,1213594164,3935617906374091490 +31d3257d-d49b-926e-7ca5-9777e00ead53,1356704941,1790909392448673420 +e684a8dc-8ac5-43af-db39-522ca7db1234,585384076,16775865145443808995 +54dbf682-f161-8002-464b-75c595c2f330,1092424174,11835808376388141918 +abd9cf23-98f7-b2a0-24d7-4c4c5e3d0cf1,3384925184,14297000047307379482 +918f0c62-c788-664f-832c-c86409fd88fa,3742752120,33500082919542003 +d9b022f9-7c2d-11e7-77a5-576e2f2466a2,32393635,3200348674603879736 +1520be7e-f033-8f41-2bec-f1f7afda7320,280116747,12480451676335428441 +ec8d4a72-4485-c85f-ce44-c9a5f3875078,890867479,15424418440578086132 +7cbd43b4-7582-7cb8-93d9-8e88e8b70a10,2295637054,10589984314971232189 +3ed1dc84-ff81-9b3d-8604-cbf670e067f4,2989522364,13879385549494944047 +0faf99b5-7f17-0d25-b18b-ee42d152bee4,775700733,1547551162664573120 +2c9eb9a4-db23-9633-de06-8e9f34a4258a,1656235871,11327200942077833986 +17b60913-c609-f1bb-8fcf-f700e8822d86,3684250741,13266115202879907682 +8c58f6c1-1e95-dc4e-0753-32312d396403,2075650865,17107861961479427629 +66fbf6d9-e70f-0a8f-0ab8-83219554a0e3,1596656496,2726330179350492764 +f5fae419-d76c-5c44-b439-a6f82f31f626,56426702,9649392195041366426 +048af170-8631-f37a-e8ed-346fb0b87d52,2003572876,4483080746572408063 +4d6bc127-2e1c-a3bc-68ba-739b6ec4fa79,1083487759,834332577099071267 +758ec3e2-7036-2e44-09a7-f76111fb3b04,1373156711,316432407411174008 +4b2479bc-55d9-49eb-2437-a03a41b7a4a2,1795295950,6657254228410927134 +6e7eb974-b2e6-4606-540a-76c98d788073,4254892802,15696093140688994737 +d85a5e89-d41a-e68c-a53b-c5b08823dae4,495870287,2872871781435093936 +cfaa78b5-8165-2617-8c0c-107fc3bf12ee,3014075158,14327955441849321823 +07beccdd-d069-42d8-a9c8-0e365bdfb895,1057600163,10359505044999825314 +43af0f2b-bd42-fb34-b35f-1b963fb496fc,3064058529,14408797678466453421 +088c1c90-3b6f-8030-123d-25f98d4d4760,4207515092,3321950927087562382 +22ed1a3c-40c4-66ba-2f03-bc4cd0c048f6,1147866379,10025179654525832826 +33852e5b-c83a-9c38-b73b-ca8726ef14d4,2972347288,14650936146943648909 +38983383-79c8-fff8-92a0-9b3ce3de60e9,3287318237,12298665284314482648 +484761ff-2a69-020d-dc79-b3c4d1ae35d1,204128883,1775026179686694098 +3ac67ffb-e4c5-fb31-1eb9-2d738ddf9a1f,2132780375,15842398745205315339 +27abb472-7b0d-36aa-55d5-953686bb2fda,4022236714,3349469576122934290 +f141bf63-bb7e-0b9c-25bd-97184f331262,1150322012,12604260049406674951 +0c2b4d5b-741c-abc2-66d7-1f1a49dbca2b,1942138406,18006771957379392100 +9ef550fc-3471-01be-69b0-becb46ac89df,1342833791,16125210316085547808 +ac7bc6b3-360d-19f3-be94-34ee9294e63e,134092387,11140548735045303878 +bdf866d3-a3ce-15ae-7eab-5e8be0f1e0fd,261332126,4275485236798369895 +015c64b4-94e4-4652-a81d-0b4aaaaa974b,2896096633,4406022740601717505 +51cd5584-fab5-3643-9df8-478981a4deac,3835221234,17235690107930528236 +2d6b9fb6-96d8-3f49-ed31-9019786f1f3d,1400863741,9692444144592951733 +c087f99d-b1fc-e93b-ca61-fb1644537868,4089505291,3079176321063297865 +7fee5996-6713-fc97-1b6e-fd37284e153a,3792754107,7025805870576132181 +bf4df1e0-22b2-80d9-0c4c-2da21de9e8b1,1276872863,976368878321971257 +eac2981a-77fc-263b-40a7-4bf769aa4741,1843343917,3425709292968605477 +6e3bd150-a946-ab82-e5b7-e5eb7850bae2,1608853938,10039417248502713998 +fc16c405-5536-e603-fdfe-f4245d4fc819,4021704323,14091864268485755447 +d84e409e-ebaf-562b-554b-adbf3413631c,3996870964,1121663768754230377 +c076df4a-911c-b8d4-eb2e-f477d0c2beff,4018001027,2976621816582404359 +6d00833d-5879-f06b-344c-74ba61007b0c,2032976615,9611041534804292673 +70a808c5-d486-5470-5a77-e1c5bd1a37c8,3947919029,5611377447996477387 +ad758535-f15d-a891-5d6d-2136f725a3b9,1436709014,8201183654451810818 +e70214d7-4be6-12ba-0e25-429df6ad5f3d,337918740,8877312395108018057 +088c13b5-5e1e-3e62-dc96-02c0edff7ce9,933206203,15529478547878752255 +3efad7b4-a747-7c93-b1fb-376f587f0424,3726719156,17276865231148666439 +153bf202-5c35-6aa3-303b-65662b48f65d,186323232,6428064161438093002 +b913c28e-fe1e-6d97-777b-4f4d4758f63b,1334637992,8877481243127198759 +21e07bbf-9650-facd-59d6-a0bdc70746ed,2050103054,12076732075967246767 +6ad35838-f642-ddf1-8773-9b14eedae483,3182294437,7351528307567094723 +e99a72ba-3902-5500-b9e3-a2c8dd1ac373,4059279883,16656100031064568473 +eff8e346-e4b2-02d7-fb4c-57720674b835,97980176,1726682665094769221 +51c40bd1-1c54-dc12-5e4c-8b506a512953,2370593929,4765471985069230096 +794c7936-2ef8-625f-36a2-e9746fa7090e,3421516787,3830590780043136929 +a9658f47-ee1a-4cb5-affd-dc000e93faa1,947540131,13596463512669887655 +c5620726-d249-33c8-3ec7-4f3e130ef741,1830311184,4449028363783480033 +1795319c-a842-5eca-7a85-5e453d3fe34f,3687239475,12172819887755837859 +d8d8ffbd-b04a-fccf-16b5-92ddf3774e9c,2245916844,16434083860439416903 +f43ebe6d-b8e5-02ac-8212-7ed8c8a5e7e2,3085220945,4262929070615828321 +4ec3a2a6-627e-5245-441b-931d12c884a4,973147445,16159740109796805830 +a2f7cffa-2ab1-8fee-b931-b6a562c85bc0,3773812310,3236979321557961838 +de206838-7fd6-4879-8b83-c122f625d2d6,2308287071,5589682046474775751 +5b8d3393-f301-7a5d-9279-afbb623bfc85,2623638845,13367964418326297480 +24676d13-b094-0221-a30a-52b575f35fde,3387890531,4244740476897137385 +73441d27-60fd-3815-04e0-16726e2f9d7d,2730346682,7542935074468713719 +8e50b662-e5b5-8245-e324-775fa77b3e59,3906815728,10446178309826660095 +eac9330c-b8fa-8610-614d-c88d2843573a,3091821349,2080649993858503943 +c93714bc-72d6-e01a-dd44-f015da92dff8,1339442194,2338138610701258712 +4506330b-484e-fb08-3f31-1a543132d4d0,811394131,3962200618286839100 +4a83a105-2c3b-e366-c92c-7a4a3a9bb6b4,882174971,15340299380738413607 +d505677f-0d24-7d09-c8f3-d57f45ea15ec,1329638634,13566332401213695350 +6283ba6a-2b0e-b31b-1010-da6d48ce504d,68074206,2518562265367808236 +1e611ac0-f242-3805-64f2-7258395ed1e7,2134925113,13975787925557860362 +2474473d-2ff6-3ac1-e6aa-80f60aa0b6e4,4095079685,14535511339393742276 +eb691dc8-8dea-33c9-928c-cd27e8801708,1941913578,4721911964830495647 +cbb01950-a57e-2665-aaba-782d594c6a4e,3956016049,4022177763818436762 +72725514-98f8-791b-4d46-ae903bb285d2,4269822400,13712315199893484618 +90de486e-b082-344a-8228-5268e29948ed,3218533151,12948191072910032084 +561900b7-d358-c69a-7017-012e71a8f9f3,1130171886,2775764602662848242 +d1d0d105-ec21-0483-bd34-026926e4a918,469732068,10645279082017687657 +8b79b367-cb4f-8c38-dfa3-44bbd59af9e6,185379409,9786435745176830684 +9ef0d7b5-302c-91c9-02dd-e4cfb109445e,3261270344,14872447780018059708 +6410823f-c90f-0f47-dc93-be7829921fc4,1401199843,2317939934319003272 +696b32a4-7edf-1026-af4b-0b336ac78dc6,2470715398,5514811105777917212 +ffa2bcf8-5fc0-84e8-bc28-33826367b7c0,2558063963,13450110707540366957 +09ea5e04-51ec-99f4-5a1a-cd956a72372c,246367120,3589333554561259581 +762d34c5-4498-3fa3-0bfb-baac6be0ff62,1821136000,16630074183753168780 +9bf9d8da-8d11-9dc0-34ea-6103e5946439,3864169945,13079669723458155466 +506d15b0-93fb-2bc7-7ab2-958be4968d92,1562529394,8190564061487185623 +3c8e5ba1-2dc0-df2a-b6ec-dc82dce6f78f,2977043444,5897732515117517682 +2a87a328-e768-f68e-e5cb-93a2d9e66d01,3319401731,4846135622968666947 +8444b1b7-1f9e-20c6-b7dd-3e3e5ac351ee,1332844248,11070823575071421489 +7414b075-4e68-898b-e097-a7db2ea02126,1314902150,15276714395986297106 +5ec0be94-dd4d-4c4b-b5d4-09a2d745f6ab,170153600,8926640198643584214 +c44f14af-3ebe-c6d7-e198-a7d035691e9f,456854977,6428122798356150777 +9864d4c4-4131-7b95-fa0c-b9ad79488d65,735025095,3296191288614732907 +86a7169e-d006-b97b-07ea-e342b78f95e0,725053128,3592101304085356133 +f690ac4d-9648-c62d-5fe6-70aff1dbd17b,1470453323,14110972740010878716 +328f6d11-53d9-52a0-92aa-f0429e219f10,1269198324,15610478976665759112 +e2e094c9-8004-708d-39f9-05a708bbbacf,2770552279,16295358261522317050 +be65a93e-68b0-3ad4-a89f-c40c8e7a56aa,2705726642,13453581816917988578 +0a10e381-48ac-4362-4ac4-7a621005426b,1321506009,2048484270539550347 +03b6e723-0a8b-b053-e645-704786631e54,3991323773,15317327114744315750 +abea0cbf-1659-8558-ff79-4b4c895af30d,1713365518,12792009997762748573 +d8a4bf1d-b20f-3350-b67b-829ba461ebb2,3239397389,10800164626989541291 +afd74afb-0ad5-6a17-b4f8-ea8ec1a0688a,858391517,10555482168175147442 +d9fa0d85-6da0-b934-9c05-e87818e25517,4185041154,16512070285935252617 +0b2f2385-e036-ada6-858a-64a3803b2c4d,2714841059,2170975731635573142 +045571d9-03c5-ecb9-2b92-8d29017b1cfa,1062996105,17905151032069681521 +2c3567c0-97e5-aab0-fa81-f256f62c5506,2116168114,560692755616890660 +47344e4c-7af2-1daa-fc90-2293cc694ac3,3526096826,12650176867710444691 +45f5fc4d-c12b-fee6-2387-87110fb25f95,2695595727,6149257971793220180 +777f249a-1078-9da0-bd4d-0193dbd31269,3491469348,11512807106120548517 +abfa3ba4-9f13-eda2-ae45-43161704444d,842351657,5511101539331650271 +d218718b-0eea-e0fb-f1e0-19872dd99d1b,2385704475,9789466181871407405 +7586dc52-4435-aedd-614a-59966d203d11,3406357959,14574122608747056621 +b0e92150-672a-3e81-8b96-d616bbaa1c95,1104572376,4044301184522775889 +fbd47685-c5eb-38dd-1d51-5cffe4a3319c,3843803726,1211906891762888858 +cceba919-8793-5a11-a4fc-7f5e18167bb6,951253533,12379186776137100755 +49e3b8b8-9c52-7eb2-857c-7297b75c9a2d,3383385193,17716129523800369127 +d6fdb711-be52-7897-fc72-5f6daada3a3f,2926116678,6005170627322262036 +342c07e7-968e-04f4-df7e-dd628c2693f8,4134838487,16273403213587900570 +2e2dea2f-87b2-d1f5-6e0b-dee54bd0849b,4276527763,4121317951778366789 +1597f924-dd0c-d9e3-4cb2-71e332d5600f,2681241229,11222959369285409523 +228af789-7a8e-2d8a-7aa8-17234de75a0d,3443150832,6383857818992880133 +46fea6ed-640b-4301-0a31-dbe8666eb64c,1934747328,12610953437141635217 +5c4b02f1-f37e-7b9c-ec34-6d281de3d389,206557406,11730848975780178253 +20a7cee9-28fc-3295-1fed-231d6e1bd94f,1576965396,7314258657651973171 +4165783a-bec1-9c93-5037-d797466dd724,3308842125,13938802429062622415 +da919cdc-1eee-a670-9e86-1554c83b33bf,2676656075,16789804784239059792 +78464061-7e57-c391-d3d4-519e62854da5,762504104,14570375607141041501 +6d4c3c28-6667-fe8c-9d36-bde00c31676e,3503675364,9390441701255256778 +2b86fee3-d0c2-7d5e-5fc2-c9bcc230ee4d,769728212,17692017900964278843 +01fe53d0-4f89-82e9-5973-bc2dba4e4860,2459572406,15164865961271057986 +a202cfe6-489c-2646-ba53-c238df781ef7,3985559754,3487499492154928085 +f63bae78-6663-45c3-7445-58351844f938,3710063328,1924919039884217442 +fd3ec9f2-cee3-a32a-f59f-bbb1be1f0588,195526297,3533521240458867217 +2eacf6c5-e190-c37c-6713-094692d51dae,1622958534,9315065227243558650 +c6a70918-043c-0ddf-2744-33a856e506f0,501756234,11968952582001311719 +9e04834d-8133-1dbc-8962-e416073e4f99,204088844,1986946100095698147 +2a4ae3f6-52f9-4615-3a17-50565c3655f9,3292907586,5719489375642916400 +3d6a465d-f07e-daee-66ac-df8c348dbee0,985651369,11984407859887306970 +5ae22651-6dd8-89ef-b039-3a638c94953d,2241438165,1043619913065620052 +d90b11a5-bc31-f8d4-2b64-cf67681980db,964160163,1241099970604618580 +5e3db0f0-3a36-d117-631c-9c89d42cb31a,2074785512,15751889971575361260 +44890263-9a84-50e4-822a-f0af4caed0a3,1320704943,13165631398825800796 +c1b14163-8120-9377-2554-9e9ad274852e,436499291,15540417810408935938 +8a4c594b-9bfa-2c03-f5ba-70c56b9ac28d,794714445,12495438368501827101 +c019916c-c5ad-0f57-e3c0-332b5e30ebdc,3439556028,11647968418882135481 +07037a57-48d2-98ee-8d43-5f71f4c2e10e,1140507949,660372254181418925 +4c179a09-79c8-9bc7-812c-a46fbb208e60,2401583174,18410369850766387739 +b6d3362b-445e-159f-803d-e723ef6ceb16,2348912814,9923331583023444947 +cb666238-5e69-cf88-43d7-ded0d46db7e4,240142303,9232139742800266843 +e2b3a61b-6fb5-1f5a-58ac-e0cf62166274,1625707386,10575265962325597113 +6259bc17-a835-03ba-f3fa-645e8c11d086,1999610239,4234864029405450296 +ef0278da-3249-3c84-43ee-442f835f902f,4011823466,15998632365704089942 +81d047d0-2d1c-d235-c3c2-41e0eb0be571,1314614721,16707308783553212880 +29b854fd-667e-d39a-3c39-5856b6ddc7fd,2310863255,15270894357972955427 +8f01bce5-9c23-be60-b260-cc1f0c5eab6b,3916845056,9549744864894256889 +e03c7fb9-155a-a70e-e991-9e61c93cbc8e,1379742170,6218277080580461837 +49c43d84-a0be-d222-aca8-78a7ad5c21e4,2125430880,15232275626959555789 +82f4738e-33dc-a209-630a-1eb766276e30,3580558694,13488213406691571211 +bf53480b-fc42-99dc-4709-5902b9d4a0fe,4118514861,1620976098837228212 +c7cfd5d9-64b0-d7cf-3cb2-02af9cfb329b,1954074888,9167810982443025451 +0b15fe6b-aae2-f212-aebb-479878bf209a,1610362498,8537751612014216200 +5b4ce476-1aa5-e703-9b61-d72871c5a553,1213252407,11488496009248520537 +bfe252e8-07b7-6c70-41c8-854de3759d3d,3825124915,13823997681947296034 +237adea1-5a9d-3760-bbb1-c63ca130b870,1325283547,8592877174862524151 +5eb989c8-a75f-8075-2e3a-ca76f417bffe,3203392005,1135844844971779914 +b1de8a05-96c0-3684-8797-f6c853e40e9b,2427074862,10799304912401962476 +a843bab5-e7dd-19d5-b673-e83d160e7996,1657600997,2366461951952235060 +6b126c1a-ebaf-7a06-5c8f-692298dbd17d,687967934,2948274850822584579 +890d206b-b943-75d2-dd61-06db1f874033,416639622,3632763199743339000 +24144507-6963-f14e-abeb-fd185c929123,2428096332,14173249756583688373 +8cc5f8ce-b63b-3bd1-0465-67473fd200d1,4162717704,9974228007837265711 +30cd7f89-6c6a-7723-95b1-37a2a3978cd1,849072660,12633513708533293315 +29671f64-9ca4-27d5-01eb-3fcd14aa6af9,3957514418,15418354948846593482 +f614f86b-e51d-9f5f-59ee-8e0d08d788ab,3048169651,15305584914530601860 +43190588-4210-6081-bfdf-21a2ebd08be7,1439163294,1035936020888168032 +b6281c7f-60b5-d34c-3340-3afcb3dc8056,4049265063,15343473629787508677 +1da84f2c-f10b-33de-143f-a8b3653a03c4,338315778,4626529876601605697 +4265cf54-2e4b-7c62-6b30-b53fed751b8a,1482412574,7634694972064594158 +9e15bd65-b6c7-9c98-cad2-972ec144db77,768424579,396308993114075477 +8d54b5bd-6115-1cd0-fc84-df8588c18fb1,394458429,13600820769903217502 +90b323df-efed-9f7c-db6d-83282ab82648,2649978018,2309269758359064132 +72b44e00-4e93-5cc6-7cda-ebb555a9ba95,3216110057,17092910818420599897 +b2b63415-cbfb-2043-b7be-84c7ace4aeca,1673502344,10854044383800993336 +757d17be-396d-55c4-6568-388a8fd8ddb1,4251960615,14356520408782853067 +31890cad-cef8-5d6e-bec3-1502e13d51fc,815761502,17373024123573337167 +44844925-f356-3123-b5bd-30a78e202a61,121158312,5750295064068700143 +d6f4d1d0-89c3-d043-4f42-102691389013,763297496,12015411370161842831 +18e91033-a3a9-d6e8-7096-d37faedfea6f,3605095320,16125919664789550583 +8a56c5ec-9a83-1baf-e356-2b3fcd34db88,2387048976,1500675933126457794 +82117bf7-8e16-ad72-b786-e310fc36647d,3102811092,2827922744027370792 +3494354d-f3ba-8712-f939-33238b9dc0c8,384544902,13423647082930181446 +26c9dcc8-c081-3bec-bd1f-f21f5be8b53a,374311224,10507914771273881495 +b125c1a6-1f2f-ef28-fa97-5afb355ba08b,2526620609,4300100411012294617 +4ef86046-673b-790d-905b-64af4e78f5d7,556428713,6353331941523782883 +a9ebee2b-f347-f393-1295-7f95ff5bcf03,1085320561,12542151708189299725 +560091e0-4264-b6b3-e15f-fb37c8ccb370,1420945847,10861885441718980300 +cc93aff1-b19b-54a3-69dd-9d6c53bcb33a,1604128992,10080346746652350461 +a33a8b34-8f0c-28aa-de40-357eba748c12,1555434607,1345321603063312113 +f4c73d42-cd69-2968-b3f6-bfe447d7199f,1700061774,5756910159554195815 +5ff65a7a-38ab-db5b-8c80-4060ff9e9731,3623119304,16311830278922086345 +a09cdc7d-68d4-c858-655f-512e07b37de8,2260827257,5815988089335444764 +a300f272-5324-2d20-725a-679f94bb7fd3,2942781864,10963172915709068984 +364095a3-5982-2a6d-95b5-f3d84846989b,3577840285,14775563359646201772 +3dc1b287-ae43-535d-2fb1-240e9e5dca45,1387988191,12308260804922428020 +b697950c-4fed-aabb-6308-cc528c97d840,2282554349,11139829723646929174 +222bbc58-2a88-4d1b-19fe-9df397b483e3,541660645,14472504403486078767 +13d54351-5381-1894-b6e9-a1e32c18c530,3921063889,15069430163555436980 +995d464c-3459-5878-b0cc-6786ab9addd8,1149065318,4429575435240496151 +4e046065-7cae-5471-cbfd-fea22ea624c1,90544653,12124300526551597006 +144388c1-934b-fb45-7e3f-5d92dc0f9cb0,850417299,6432390457469029600 +20132528-2996-7779-914c-830204df1be7,328457355,13630505096896510961 +289769e0-f2cd-bf14-34c3-b66f4756b3d9,3474881172,3973500434086902703 +1e8d0c38-49bc-3367-666c-549f60e8d9a0,1663878659,7964304034738220848 +f6d941ed-d798-86b1-e459-d5ba336848e2,1392025721,2142864776744620862 +b8cf128e-e941-cc8f-91e9-8586c0736371,2402214155,12785573363457168554 +cfd6ed5b-fa90-7b4a-a0c2-d5295fd55669,238430499,11576573690365166590 +beeb2bcb-345c-a8b9-f526-34759153a9b2,2982357573,16112099961510748240 +c1119465-523f-7576-5022-c68870119257,880349629,578766049375106056 +93635903-3bc8-1007-19ac-5c0d3f56d031,2403248946,13952378896322111183 +f733c912-cd6d-713d-0367-985b631930a0,2827679711,1057218020722495920 +9109742b-74dd-0a46-ba22-c5165ee5ee54,546869687,8641713947542393860 +a0684365-e1f6-0844-17b8-4f086c62b9be,2137015427,316980558388294035 +8c0098c8-b473-775f-3ada-5e7d0fb95e8c,2123618205,9237384317544181317 +9c7dc142-a58c-d316-7a2f-be123a8a10e7,84936676,153296650314355056 +a680c9bc-b770-37d8-f364-61cfed5d6b24,3318229932,9052188663950534630 +c5dbd1ae-ffd8-9b20-4ff0-8e4474341267,793879817,17595131705114912980 +9a521c19-cd21-de74-f64b-e0c6305c300a,2782949450,5478036614989199260 +968650a3-f9cd-f299-c6bc-30397d28a55c,3207877534,12186868013723543415 +3df16891-a2a1-b268-83a2-d63a8b05966c,54789323,4887020405404379025 +48142156-801f-0389-03cd-b660142924fe,2287130832,14623124515382498207 +186d99c6-902b-cee0-71c6-aeee1447fddd,2678374723,3177494053127293949 +6049be86-61db-704e-778b-168a6e3efc8f,3136674077,1334940361529438271 +1cbfc47b-3622-a67b-fc44-ef14a7c79771,2930962004,1053345259090950070 +4689e652-3a93-8b20-729b-91f530c9179d,2257180221,9735734940983752665 +89e26022-d0ae-1498-42dd-a6f33ee8370a,388539162,10000863913539381147 +0445f665-f7ff-955c-4572-d1ed353ac147,22054949,8628625890525774612 +56a6948f-04f7-74f3-dcb8-e18ca7956576,4097916098,3867605573091987690 +1e9dcf3f-dd94-87e7-9a68-3938b29c54e2,1138992095,287999700678192909 +15ba060e-edc7-aa0d-325d-a0e8af4092a7,2683579572,15550706895286083741 +5425a1a3-ad38-bfe4-7aaf-a03b55932935,3255306552,17199030675059589110 +383a6182-23ed-66a6-f5d8-21cfafe21336,2996245698,15776240770517389852 +c97467f1-ae02-d74b-3d68-f311e246fc5a,2667732319,18146060584303216647 +cc9caa19-18cc-1ebf-9cbd-88018284709a,945221566,6409535489693803159 +070ef2b2-fbd6-57bc-23c9-12e4edfa51d7,1375444060,17773468614046460246 +56c0803e-1a0b-b09b-b365-37a2b2dfa667,474483234,8332705747913843979 +f47cdee8-e9e6-f26e-a518-173a62e447a0,2407353251,10921647625892057608 +082a12e1-453b-70b9-9aae-d675418d6b33,2819702273,7731916191177597813 +8c3bced9-11ac-135f-f865-2db79dcca2d1,1419503500,7670872649829343164 +bde4faae-c596-18d0-4435-a6b824a59758,1781175718,6313280678079843050 +5da4cc96-9666-6a3d-e7e6-0dfd84301580,1204914195,7769443666166848375 +833c2b61-d1f4-e75c-ed51-41bcbe3f0642,3941964947,4107822900901205281 +50945871-dc6b-babf-6a04-a23a90f770a0,1397657045,6327084510132160635 +f6c79b9c-732f-14fd-c04e-e818156e85aa,749830489,6585995412342049816 +f9435ced-afd5-f8ec-39bd-69e474e55e12,660057385,10850815090194363870 +eeea8484-0d12-b701-50ad-274287eef5b9,3176880072,2701157620814774666 +1b7c05a2-f6fa-0ceb-2beb-a8013a7ca0f4,3152237683,8145269232172110760 +ce92276a-2b42-6b26-31bf-bd3185ca0eba,1053397130,4207538119580107965 +42c16714-063d-f8e1-4db0-6fa359381a4b,1265251676,16861518737119370362 +5935884a-577b-4387-c6fc-39afd2476013,609377746,11346294264568200052 +64b2535a-dff2-62dc-1f0f-607d5d75cca3,2389927617,6956728350157984540 +23d123be-e14c-e3c7-887f-8099e6b752e4,220146562,14826700000580846431 +069d9407-5946-ffaf-0934-0adecfd0f919,1856383717,14542725624096574131 +6a5f8824-8219-53c0-ba29-f0825c9e7ce8,1995133228,11823169476526604557 +c1e5520d-0857-231e-94d0-d531d20da42f,1050335315,16236895969685073425 +e30d5310-e266-00ee-c1e1-7b56477d8b95,1472602863,8841895852795284547 +22e9296d-de71-fe1c-1273-53432f3486a6,3339050274,9430287607158716367 +02331937-efc4-bfe9-0d17-a63d79e89888,382127012,953149667179678634 +41a485b6-ce77-a75b-87fa-9fbe446b7ee8,2639585914,18347059260318115736 +b87628e4-8cf3-7852-c397-816a270dc836,1660615450,6846921331202342490 +f2278bbc-1349-1691-5d87-0a9309103031,673133712,16170316764804071883 +e8dc1614-3da2-ce89-63eb-86d3db973fe0,2102702819,5071328445786881828 +187579eb-7d32-b126-dea4-78b35b75c65e,2307409980,5114731442560772851 +9b7ab1f1-7067-525e-cd74-044c284f1fcd,84197213,2347685103758036178 +d5ba7540-9bd1-abe6-5382-fb4b5da57f1e,3573364264,3978292652991892084 +db9ff797-90c1-ead4-be95-c201488cfbb8,3665632101,12026397028003802075 +c46310e1-987c-98b4-08c6-5b9fd3476807,389524867,17604034491110235444 +c7827968-c9cc-5d0c-5e45-3760962f072c,1981848391,2190207524038729014 +6b941db7-b1d2-5865-1cb9-81ad1a900dd6,1470433018,3881735392693141880 +3dd38b15-7355-dfb4-1228-b79a4cbed816,1968730606,16529302313426813049 +8e44e05f-3ba3-d24a-1367-5ead28adaadd,654212117,18267351463475089489 +d611a126-be2a-56f9-7685-d27383b8880e,1939357006,9608532345802667401 +1ed0a30d-ce9f-f131-4459-9f3c9b8640ac,1022522992,420259526426235931 +f80c7ace-e988-2727-39e5-2c3a9db8fb75,3425457330,3908178519244892172 +6f86097b-76d9-db3d-8d14-7804ff91d24f,3944718653,1314755402150300547 +f2f633f6-708f-3268-ad9c-100064eb1182,496198004,942016199868043763 +5e5a2f30-62c5-0860-de8d-4aac1918b65d,534626728,10430617165774597528 +4767b4ed-f3df-a1d5-0a25-a1fbcfa20745,3519733827,5676160276786389330 +aebe9a6f-65ca-2e5d-b277-20a353621f45,2891402380,5761639423131277075 +950e5f7e-5fe5-3dcf-fcf6-323b5ea3f8f0,2503906623,13534654422314710626 +d4674011-905c-099d-3688-19033b33a753,2758705409,10620242396272212362 +e4fea9c6-dd47-1e9c-82de-7b089cb1af8e,844262179,11296773262444451557 +f2ad811a-4165-f789-283b-6b73219edc39,1891443875,8813622879994528020 +56a8a295-76e0-f816-e656-d7103a43b9c7,2183620347,13106260335337258730 +31392358-ee70-dabd-8e1c-58779b400ab0,3831899554,12837326112045685978 +5adfa074-80dd-e5b8-3279-1ca0d8f3b24b,713097218,2178327098575155074 +cd825d62-4059-de51-8662-924ca312c54f,2082817665,10246421095253552246 +60d08b89-6ade-e444-7a63-227e1fe35340,4066726290,7616129597366075619 +01d3383d-55a3-bc10-135b-913a3ecd39e7,2277436606,3291456208568747435 +1b7f71a1-0639-79d0-400b-d7b4bba117b5,3451304392,11519292232346074177 +09dfdfea-8acd-49d3-a380-06d760c8cd56,181406302,11435930547053273209 +08cd1f49-82b1-27cf-1494-531c5e4ac6da,3430233235,17847869946986141524 +b8b71274-f481-8afe-5804-17ac734541d7,1233352935,1636559146289071388 +a1beafd7-425f-d017-383a-1cbecf01a763,1387584013,6895239346574283197 +bb75c20a-f433-e22a-81cc-de8179e6fad6,112938743,9888717401885249667 +434b753e-f5f4-f7f4-9f57-4bf570d339a0,2685543104,1753598292356322683 +7af74c98-f1c8-c33b-f016-c318a6a7069e,4147897340,11375454624921625676 +7977025d-442a-1828-c0d4-ccffd8ebbf21,3330943190,6545268157414587657 +38c24618-8e99-9dfa-180a-a491b07b53e1,3724542302,10784832123182095287 +8d20f38c-5690-de18-62de-c6f79b97e3d1,1122321461,14835743736508423130 +4d0ff2ad-0866-0898-acb0-329fcb9cd1dc,381735005,10743964522040793533 +7e14d4cb-ebb6-efa1-3bda-a8cf816eaf1b,25498220,16440423844598641174 +5687823b-c17b-714e-785e-739cbeb88010,146317594,5862180931531758871 +f638cc10-0482-c256-62fe-ca548c3a6321,319945286,5143717715673941783 +58b993fa-7ced-f6fd-d2eb-975a9c419425,2260778647,15479708225091861949 +09122642-97ad-688d-1d61-2801ae6e4ae7,2805607741,1350458971538524029 +940c34d4-3766-530a-128e-b58fb8360aa8,3726305834,9316888124131919743 +6341ed74-0d68-f634-9b44-549ec6ea9440,2245589127,7728703717237553723 +71bc16cd-9c7a-2dd0-4432-f0781fba4731,1929889327,18039709588753494540 +dd288ea9-1349-8d8b-390c-e43301157e3a,3958138623,9736159016380187786 +6b8e3420-a092-6165-db05-da37580c199d,2647437992,11667248752183853042 +4ecb0c0e-c3bf-8357-6a1a-8b716b9e4b8b,1552828253,12260363200689871586 +deff9a6e-779c-1d06-ef0a-bd356e859398,876551328,7959013948825320053 +a9be5d48-7310-3221-2b24-22e9dd1c0af2,2761636215,3928682102906076829 +9bae9f9d-abac-07ff-fd55-6324defa9c84,1401604035,14818334302777051779 +f3c6ec79-ed01-5b0f-a93f-adbf36080a04,2280040630,1411177574487035349 +5297d5ec-631c-7def-27a5-9211075c9ea4,2121974662,4225518224292141423 +d1cfac59-d0cc-cb49-172f-daeba38d63fc,1097100256,17618198558726172872 +c1533895-7183-3983-a6b8-4c71b633d607,3696594416,14972371291105234870 +e4e90406-86d0-ceb0-a2fe-b62b69a76524,315452515,2248630477196245623 +cbda3ae3-4ddc-e73e-aef0-191bca53b8fb,1521358240,13997315980138570411 +e9d8922a-f90c-e7e2-d0db-f5c02f7f5f39,2337879201,14834117596792194720 +46b28d22-a5cd-396b-3f4f-a9e0fd4d5d9b,3979979678,17051734827437330085 +8c30f563-de4e-bcce-96cd-5880f890f242,1962850178,5570773595409196372 +4f71ff3b-54e4-176c-8df4-2f9ff98a56f3,317625169,6268671175512660484 +421573bf-66a7-431b-d6be-c100bb2458b0,2396096396,13058491152843462352 +3337eeeb-3437-b075-2961-a6d84bbf74c9,4239563456,12477243740996385067 +9007d330-c6de-546f-5509-39e0026d7bb0,851452412,5955538106081874921 +42ee79cf-5783-7a92-5104-477bc8eead88,2385402293,14629940620781248760 +9b47461e-a5e0-4d58-c7f6-9831538a5d7c,452930150,18120220266540257666 +b601d74c-d685-2f90-608d-1628313ab6ac,3566319140,11270226182286847851 +7e4862a4-bacf-985d-8444-671857b3b533,3582603774,16848077578185961463 +f2397cc9-dcf0-dcab-802d-d51cdb63c923,1530309902,15180166622974222791 +409386a2-9116-f37f-6c19-d7e6418152ac,1750242102,6275508759703188285 +ae2e865f-5a47-3d44-cf7a-583980c7562e,3030695665,1905979234056547092 +541845d5-67d0-7a5d-a1b4-fe09db29bbae,4043855466,5740363932113179562 +9a0a432c-7e62-5c79-db04-d57bdd399f7a,2373737092,5716386740859043303 +6a544499-ba44-f8ef-b423-ab7f2521e901,372857711,5585003483200438326 +0d703565-1c22-38a2-8c14-36c89a848616,16247217,3770757908277627813 +80a00f6c-4ba3-2304-cd0b-1368e56d4491,4016422781,10258184371342895696 +38216e40-09af-c3f1-85cd-7909275a3767,832696899,2777274673040965447 +2beaaccf-762b-9ab7-5e62-7e8b1c3f5d7f,2555348873,15435909768673506583 +57c59b01-de46-5399-692a-01e4667ba740,271375469,7541905111609478503 +5904cc3b-e640-3ca8-31c6-1598d81c846b,11983193,5346239133301459488 +81e35eea-4374-0403-c875-6edbd34ceb23,3144721900,10135494381232026156 +ac4ea577-a94b-612b-0000-029c1e7791b1,1251151748,9387313803200194691 +0bfaea96-b7ed-47fe-765c-59ef6a0f17e5,2042401352,6559205354270619638 +649575f4-c8a3-8f71-53d2-b98797320a35,3898060529,5140642534064537436 +6085771d-9144-b9ac-2be8-9c216a7f1903,1913951781,17743648335035781396 +b49d9873-63b0-1911-1a78-8083f391561d,3915460642,8755592041702837322 +8e8786e6-1de3-cd66-8bf3-42f7a0d55334,2146932105,12079157304796232189 +f7dbc930-11e3-697b-b087-4f57b8765699,1829349848,5509471908660321062 +c5b83f03-4e23-1c1a-7bce-5d21a5507466,573182935,2743832367496207858 +74b51530-1bfc-9de4-580e-07dd78699a2f,3075063221,2804329395568501343 +c25c1dbc-d423-3a4c-370c-badce94eea8b,771462251,16876024799769572262 +30caf6ea-6f5b-c630-1b38-6c3c3b76a98d,2089112534,9142636221334704136 +5ddcef2c-ac7c-ca06-1eca-9e27f7e37d69,413688156,9990630683252621655 +9caec7a5-181d-17b9-8f6a-f2c12c44d1ef,1930314130,615320072676085176 +d568b7bb-dd95-f26d-363f-b50e1fed28e1,420114256,18205588782028309525 +c9d70c58-6c12-3083-d0c3-f288bd68ec98,2774659521,3917888100256231696 +44ff1d81-0e12-624e-e1dd-7fb2c63f409e,53415269,18124066958862198192 +ca336895-c6cf-90e0-c9d0-87064a11f882,1007181978,5191363040640001305 +280d2f0c-8204-586b-d3c7-fdcf279eb22f,352893768,3625295992774960027 +681bc639-1b4c-1ad0-6a33-bb159f75367a,3958171622,4449391356213167240 +fb2e8770-31f6-86c5-ae83-6c498f62c271,1279914271,11039952063183893164 +401cb57f-fe80-e150-fcab-b1dce9ca8e0e,1327773148,9185779378284318336 +5c8cad83-11c2-0604-d649-9f555395dae6,392629503,15421694556878403523 +7d76aeea-2611-15cd-0c6e-540ddb381361,3372098675,12743586714357424141 +fe4093af-1cc9-efe1-4585-e3d31eb78359,3585122771,1700538386311768497 +15e02f89-ea16-d421-750b-29c109f57cf8,2782980818,17749484437647285134 +c213fc43-d33f-b07f-f4ab-b664ee3e3ea1,1400412668,11152756947724418835 +8fc33cb9-5d43-9ed6-5b7f-39fd8e733c63,465487104,6065838825091847120 +33889184-3abc-1789-b85a-0092ee6caa35,1408385448,2235604240873536872 +d857b787-0951-435d-c9d7-03ee5db090f0,4033409753,11616726338646892462 +2ea7244b-94fd-257e-cf73-31824cfe954b,559789608,18385317110422273937 +f516f799-9f86-a3cb-3e42-3b87f17b83f9,3935342432,5100457965665690530 +e4e0e43f-a10a-2a4c-e081-b1a92e568ad6,730569116,11059699774962900737 +dfb7928e-c2c3-0295-ec43-f11ba694fcba,1329890051,11005205464049301546 +e37a79ef-ce9c-8ba0-fb15-a5454cb67505,2948322915,13789513051544569835 +f74066b9-7823-d8f1-b8d9-f70988e96a09,4136718486,4220502851848506875 +7bc0b011-3028-1a3f-d8c7-d59fa0f0f1b4,62315921,16920247833208278894 +24b91caa-9cab-cfc3-351d-6c3beb84a0d6,2100809152,17410126682163059545 +259d42fd-35b2-31de-83c8-40d017e46e1a,4255442378,14639720301784779929 +64fed1f2-2ab7-1942-0e22-4b95239cbb72,190151303,6356706626613964696 +025b6211-2ab6-decf-47e6-979135ab6eef,2911507319,16310535090767465312 +4c5ee2c2-bfd3-27de-c477-dacb130ac3b6,1061341670,15489476381954045848 +8ab6a73f-1a50-8dd0-99b2-360392813891,3963980397,7009710097312933532 +a68a7b2b-7902-1439-7914-967c9d0d96aa,441984657,11320608088661648836 +744d47b4-c03d-500c-8e9f-f3f50d629809,21327009,3607295298307774447 +e6b0c7aa-da4f-031f-f6a2-039da2b6e5eb,93625713,11499756748642832359 +580c4284-8532-f1be-1b7e-5e492cae35c7,918815041,5424769536814456002 +68dfd9bf-cc83-3b42-4f03-a681bcbf8f1c,95180102,7941759509383320169 +1fa5c22a-8ae9-9638-dfac-a2ac452fea69,1918229186,121248472953568468 +416ccf46-121e-35c8-33d4-27812c7ead0e,4099241533,5048528925622026549 +710167ec-7049-2469-a395-00bcf5ba9938,42881579,16087847715341555275 +f850e196-887d-c575-cafb-5a25d88c64df,413499610,6981976805889109850 +0f95e981-1581-77dd-878b-e9655a6755f7,607713954,5833567573695700961 +fcc975a8-def9-52c4-35fb-ad4e6df6a33b,3353740034,12604813104414248270 +6dfbbb6a-631b-646b-edd2-a7cc7a9245de,244009678,6346629518904929669 +552a0aa9-1ff5-475f-2325-030e313e9655,4235359295,16205362518792344550 +84c70ae0-5347-cea7-08fc-018aece7b927,2676675360,5379993932079626152 +09989da7-f987-5ba8-edad-4e5dcfdd4c12,2661180299,803045837946037319 +67c25fab-d2d4-62ee-cd7a-8af5c7cfc672,2840877563,11642040171115282017 +f7aa9629-463b-ca74-f1eb-47815af3cc9b,1148907962,8787717178257902386 +4bd035f5-def0-8bfa-186a-3b8a12560803,2679910375,5936171715221639820 +8fa556f0-1ce6-2191-3d29-8551a9e3d8c0,4141329990,10202390621058477747 +d523b65f-aaf5-6e1b-76fa-6b581d0d3214,108645678,9447657324850693693 +e0e88074-45c6-95c3-1759-c0af69ae7033,752702324,12694356590727428103 +457088a6-2a69-005c-3584-dacf0dc16026,338627262,17068458443728313103 +e1792422-4f6b-3deb-a292-debd540689e3,2906039603,17033114598471686162 +9f7b1959-4df8-c099-5679-aa58d3767e86,2153257689,9348463565226347658 +d879f815-83da-2fa2-0394-3699cbeedb49,3793718247,12644508090280196257 +d1727713-6d4d-1ab5-b373-7ab80848c484,1620784365,3776381520118527951 +3ced616e-8945-690f-0f2a-e2a270f9dd0c,1029849982,450759552977987246 +3d5c0dcf-c170-4ee0-046b-5ecd8653a983,1123167007,378293191870071813 +f3a70bd8-7568-efe6-9066-a16af11a1a82,3394951660,18389503973953640537 +acc02e60-77bd-4ee4-1bb6-71fc827a7e8a,1299301771,13937656057436940166 +3b2a4b9d-06d2-7338-c773-93e5c2926b2d,775595925,10089796802151061704 +cdd6dcbb-2f17-3b7a-9996-02399f086957,3748666162,11531840052172203238 +4cf0f8d1-7476-865c-c303-8014549e3ddc,1480978711,4023126361940901178 +5bce0f82-cf92-5521-f549-9e4985c3d4c7,3433155147,6009927074548173401 +7c48f5b5-c76c-bb53-275c-7a98f08e8e1d,1255939016,11386363594183581977 +5ea9e501-d0ee-bb67-ed06-81b837b01152,698080161,6331422324565874478 +10a6f05a-d21a-7e60-7f43-14ecbd696e7b,3129892553,17183667603568923413 +be772495-e036-d88c-18cb-0dc2d96197ce,3494214644,7815193288770633266 +df3c496c-ee34-5f4c-a879-4c99f4fc82fd,366622248,705829098848537112 +2675f8ce-2efe-a0d8-efd9-833556cf178b,535242815,16682910346474866321 +04ca90b1-4de0-f153-d9a3-c1662cc410de,891627947,14292739520026621934 +a7a757b3-9b78-dc84-e887-3965bddc4b88,1964439959,3568147238276545990 +5a441b56-b934-2c00-9bc3-a45210c67c1b,297718721,17794744187610986180 +ca9b4d03-388c-1bfc-5cc7-3c05c7ec2caa,3214991021,4315439531222432006 +f33112f6-bdb6-93fd-4927-6273b84b4b76,1122792774,6546543230198469282 +96faafe0-6b82-18f2-b0e6-def3d1fb3091,3299623735,7057670875834819068 +b3ce6250-5a37-aff5-9ba2-ac2f352baba3,1083791824,10283054268432565291 +77ae25bc-1c3b-4410-bfd3-c2363ed8243f,1924832903,5802579879452329699 +94508436-4c30-5604-4236-fd33f795b1d0,2255471780,12376877803832492541 +7e2dde8c-1a86-7aa2-ca58-1881ff164f36,1236464239,3079710732405379019 +f57fa5a7-41ca-37d0-1836-14452a7241d3,1230440207,14919039548698261847 +9a110644-d9fe-3b5c-3ecd-a3b55f97fa47,743968529,18389209488173754821 +32840f8c-7dc9-471c-eb5b-0976af91a04b,3786330213,4424289723054799386 +8ef93bb3-b3cc-9f94-0d5d-5f74ff75f25b,723244913,8067413782501512004 +767d413d-82c8-599d-56b0-9993e75a3c87,110266354,14337092671978812792 +fdd696ac-fc97-24f4-8542-d87138946fa6,1119459824,1284623128202603480 +556f1965-d1e4-14ec-712d-80b6a485943d,623361087,7386988775159634801 +013c2d76-5a02-1b50-aaff-54277b504a19,3083368844,15863347249554558480 +aac91ec2-b251-2f6b-b7f3-6d81dad7c879,3403724169,7156080445182683511 +532a8a1d-d39b-866b-b157-572bd2ab2131,2495482386,5886985496189934070 +1af33813-9493-e5a2-4f6e-5af2f262779d,2678825578,5237924920075973404 +ad557168-a02f-6bdf-1f19-7f7f6732e64a,2897080442,1201045901508575556 +1436f5d4-1815-06d1-ee69-1a4a299f2857,148322982,17491581168174916458 +67517bf1-0de7-cf36-3274-a1bfb059f73c,1518339429,3933328580260943868 +ea484566-dc5d-60ff-a9af-72283469268e,374148259,17912728504088334099 +75b61078-fe71-33bd-4137-b03b40c1a77d,902308262,6292014585853436208 +adbd8284-e781-ac5d-1d23-c19eab8d35d3,372224886,14450607909370228137 +84b68de5-a82f-bf74-f75c-cc23d0583ffd,1502227634,7055277349007972343 +3206fefb-abba-2da3-cdfa-665fa4509fb1,1281369487,12223872308701068154 +de9f1820-d5d2-b142-c8d7-f8d59766e4f4,2431504690,2518552193916630891 +7a518653-fa8e-a526-5b80-21029625b8a4,1626866817,3587797272527493612 +e0ca4205-0070-dfc8-3ca1-7282179f3164,4185219031,12297216966200645330 +78a6954e-51d8-4257-e9d8-520b326c5fce,1298244696,11261353541197794230 +3675ceac-e5d8-3293-ea2b-9720c5cb646f,4090984184,12220444472479005563 +51e7e546-465a-bb76-e53a-db348c61d331,3679999081,14291288790628007324 +3b297504-8c17-273d-ed0d-4f9f2786b849,2789123759,1073904439173401183 +71a1b370-af23-4281-588c-a7b87cb2fbc7,3438714603,16714078222664098487 +9bd6946d-ff51-a055-6004-3911185ea5f7,1089630502,4775136394297757016 +24c28172-620a-cc81-a012-28525fb1b6f3,1844023943,6264823123589319730 +e0d6d4d8-fbba-3b93-0b78-6983959a5822,505641792,3894366589265047972 +28c19cce-d8a2-aad9-1f8f-cfd87d22c4e7,2343065615,16799285165589118608 +ceaea335-d4c0-53f8-1f1a-d2e2fed21d28,2493607632,16410423129122934972 +17230d76-d2d8-6b64-c10f-a75bcdbf7232,4048817643,4871991116533831868 +5edfc95d-ec0b-ea36-0802-3fe7d5a6b41d,2265700981,3801365321080369590 +3e6ced92-0bdb-4c6f-65fc-ff60328267a1,2602022533,16396685920363614398 +9e01950a-39d7-d6c2-6f4e-98ff4f45f4b0,3484405169,3748552435464507549 +35d0d964-2145-fbfe-09df-5ac0d62e6259,2293436411,11401543604359368588 +4d52db47-d866-afd5-7633-72b831adff44,1802996990,11880029633575460772 +c2ed673f-c4d6-9922-dcf1-1ff552183bc6,2378705508,13487575594619432926 +738c3d9f-c942-37d3-147d-1b1d6a1f0923,1880288632,13706569859816532782 +cce456db-1aa3-d1ab-7b6a-19d71d7260cd,859159417,14006779392132879171 +c0e084be-689f-69ef-4bf0-2fd0ec1ad052,1007995375,13807076772557707863 +6941f9c3-df30-4acf-e3d9-8f9f987810ef,1517512925,10838462978003356315 +1d8bf81b-8b2f-e7fc-689b-8e803881e6c0,4211298402,12985797220885010198 +6b4d7c28-d609-e650-a520-8f5a0817d7ba,3578743462,2847043226473020460 +f0a96465-7cf6-ddce-41c6-2b9a15efa11c,3044624199,5542397395063337613 +9366acd7-2997-5867-4606-11c8f11107df,1527828991,14719000210445272776 +ad374b04-1ae6-120c-35d5-1dbe67d192b3,3627288375,7572994368238914390 +64112cb0-7178-e5b6-5d82-4c79b1d25b0a,946978538,2900197881684247291 +4f69678c-8672-366c-fdbb-dcbc125f843c,3737887662,4863594361099761220 +37c8cf20-2ff2-6eb5-4460-224d1cca1757,4107944883,12841037426352018718 +90be8d88-8818-a269-0b6a-67576e07ead6,77843720,12052707864243472457 +04bd7e35-bcb0-b957-dc12-d436d6b36d64,1361073050,17484151809258338546 +edfa50ed-aab5-fcee-774e-88baa97d7c02,3882164814,8225186369091883418 +86434056-7772-74a8-f99f-87a2ab60e356,3458537391,4790008474350703103 +0cb3bfe6-368c-086c-eab1-32986f3526bb,86204726,1663374340009910149 +5b28b7c6-5052-b5af-c2db-65199546d910,3296066805,659291874932832001 +abbc8fb0-1bf4-adb2-e338-737417b6081b,1172010367,15558011303420429781 +f6fb60fb-1727-e247-e528-77db2478b3cd,3967708050,6791603836014395353 +c6eac5ce-f582-74f3-1ae2-4f78484edace,3422742053,2246435208389284239 +f08bb226-f429-6534-e153-ed5c904b5040,1633586369,1542546043877202658 +52cd120c-a64d-2079-ba78-c73af0a3f249,4278019464,17157321754411640193 +b5736af7-4ff7-3e7b-399d-028e5af4f4e9,2058212056,1969109020295523640 +edea66c7-480b-e735-ed05-0955e597b227,1732109156,2766625260784812264 +bfcc15da-f5d8-e01d-9bb6-b7eca0971d40,323581720,2203174208611362281 +3f9accbe-245a-a150-be5c-a8684e3588d3,282298805,5090494557899525138 +3be4ae7d-ea14-3b8f-c40b-4b53184effde,2386186521,2405029183158448228 +961c2cde-47f0-9142-d292-ea09f5b03a2c,1744721085,14718681138114487835 +1b06399b-35bd-e49a-d5c7-9ebb68beac24,3991205983,12378514687890016116 +0a2a47d0-e291-3f2a-233b-88f0898df7d5,255567264,15802634167718585790 +a6f141ec-4177-28cb-b489-28150f8f09f4,1253129429,7685386873748893555 +8e836faf-472e-4dba-363f-ba9b40eb2635,3537630084,8684289068276865226 +e3357cac-6824-6937-70c7-51776e5391b2,4049681931,1387128101255636142 +64e9914c-8f1d-c6b0-163d-c2bbd943ef23,3704449201,7613733090294519457 +9bbde6e4-8ecc-9aa1-c91d-bdc87f781003,1269786578,10339000422230737034 +55a97d23-a17d-561e-c8f9-a99928469fe3,23722666,9552321896411393217 +2ebba34c-537d-1211-0f95-e57072c4d191,2731485029,2070074356298131803 +4e4c2f17-65f3-d4c8-ebba-c01fc588e130,479867961,5240801851241315439 +22f92e31-0bf5-68cd-dee1-52d6e37e2669,1662097420,2653760832873324030 +27f7ed89-06c2-eb8d-b7d3-3b1b9136ea31,3083875158,13749416859674278685 +343790f0-0def-4d73-92e5-b967bbe9afc0,4123069910,6028907805608558147 +b7341418-4d61-f92c-3880-8a9685bd5f47,1473916590,18202618482709360823 +cbebba0a-c817-7c11-3355-eef0f75726cd,2327761852,12067583967931502103 +a7b8edc9-a692-af9d-92dc-d639bf2712fa,2191637012,9941845415880719663 +06cd47aa-17b7-31a2-2a41-ec21f4c41eef,247451758,17611188176865967397 +bbcaed67-eb69-69ed-8bdf-e6070e63d3c6,3889913445,15721510418023183015 +16f95bcd-ec2d-bdcc-5bf9-ee3b3ba9eefc,670346562,13111944265683191920 +730df4ce-b927-0384-bf6c-f55c0a7cbd0e,37566371,16776293157413731151 +fdaf1b8b-45fe-60cb-392d-ebb893ac4a37,1511258570,14554956458952348213 +35cc6aed-a097-294a-f6b2-15b119d014cc,1171129613,16477287277262375907 +5ef55b86-89ee-a4d8-da8d-1f0be8f672e5,1746307058,1806408554700825467 +776a64c6-5467-9a92-fd96-72351053cdfc,2108610361,15582336791454147584 +08dc5b55-d65e-bbf4-5996-f3e8e6dcbe4b,1309099013,6671597471063174568 +9134c861-9bfb-4f3e-99ae-8394170668e4,2556870516,4073933831402419866 +1189d4ab-c195-ccb6-c1b4-211dc2119afe,3020067508,14545701465463855636 +94d15096-f686-edb2-915e-9a276f24be8a,3442397993,7529683992052710562 +976aa577-9e95-49c4-036f-0e8d12eaac57,3550573215,548636338138447625 +1335eda1-9bdd-b32d-264c-16a28ddcec62,3410970353,4388615592063466214 +e145c586-a9fa-b6a3-3d0e-6c4e8fbc2a6f,3308634063,1932531810519517446 +c4bfc631-aebb-a0ac-8699-5e0cd939070b,370599843,12691927408069551470 +3ed94fa7-cc26-17f9-0ed8-db4b017b26e7,1918085227,15943477193801765963 +b5d0aea6-7333-657d-3c51-a088ab47d97d,3532402164,9713096021212152736 +a4e4d09e-6775-6e63-2865-7aca6fd06e8b,3083873413,13354131618156757942 +2916e71f-0736-3718-2724-94bd75cee2bb,2344492988,12382508110029920490 +c01350bc-eb62-ed71-8d0b-5ed4210807a2,1874462719,4611664751427257384 +826d852d-d488-0562-9721-95ec349239f1,2087059770,482410516492690652 +052926fe-ba1a-5b53-8132-f8e7a2562ac9,2261065027,11388113835731576920 +9bbc065f-b250-126d-4cc9-55c867f9ce30,495233597,10298064077252853412 +0dcd4c74-0451-ff07-db63-9cd29611f1ec,2094616054,9950910879131375584 +25424285-5ddf-6b7d-efed-d12894941f3b,4212697754,13303311281790728650 +1bad1eb2-fb9b-80fb-3c4b-62c1a97d3725,3156092873,9131185838819983180 +993ffe1d-056e-2555-b7e7-7b8a83389a8c,3624233639,17279428357113340204 +8432eda6-5ddc-f192-22be-2ad7a7a6040d,1751354627,16528347363202889027 +8dba9772-1f0d-bb7a-ad0c-3d097ca1eb7b,1720467955,5201810084487966698 +5a0fb643-f923-c402-f9ef-0043b91e7f92,2862938539,8039041701086748204 +0cdb8eb6-bde9-3df3-bffd-a4943d93697f,726580673,9074782919874768118 +40b51d38-f9dd-8f91-57e9-e024987f8d95,1229206735,13294666502306102104 +dea733c3-a7ba-2ff2-e2c6-0459c71a87c6,689217524,13685336074513389342 +9a336cf1-b8b9-b616-1bfd-9890474e82a4,1687607912,19473735009198589 +23931346-a5e9-d67f-c984-eece7356bd21,1956666666,9982745695134143323 +ede66f7a-20d7-0b1e-efc6-a5397496e454,984066322,8522899017646219360 +d2a5ec7f-4353-158e-36e7-fa42b075ed84,2771044761,14969322338906593531 +601ebba1-9bb6-555d-cd7b-5d45615ca1b4,3753714885,14284204260667030329 +f5ada59a-d406-af35-6fd5-983f8704740b,2619915399,11066093766903609005 +c48a781f-8cd0-06d5-d689-3cfc0b223041,958807249,12978025556322420472 +ed4fbe80-6a81-6db0-68bb-1bd40b91a9d1,3347361584,414947662351788171 +dc5da8d1-4229-c185-93b3-13472e093260,4006954378,5693647417708167081 +724e998f-e7fe-be73-4a78-99d63f0d35d6,2332320418,377407375379524801 +ca373437-ca53-6300-2d13-b1c1a776f97f,2048566061,2119200496760611651 +e90a7cc8-262e-305b-3dc2-d97cef54f144,1425659632,18338221310929880616 +001795e0-9777-9c10-995a-87c5a035ce50,56187791,3084088978665572949 +4815dda1-c649-aa4f-7beb-741febbdd7a9,4104447810,10266692640357101535 +17c9b110-86ae-b663-3a62-abd1591901ec,747513429,15081875705018573854 +a08cc533-9325-b1b0-5174-fd2e01b03b14,3651552166,9222904541659426869 +de0fb00c-7296-f356-7ecc-fabc37605a0a,3643521964,6982833818300663383 +f6f8670c-f90d-e749-7ec4-43d81ffe5e5f,3406647068,1149207882190552974 +e87c7e4b-3308-90f6-8a4a-362c7883b254,161668790,16322362847878075172 +32061ac2-e6d4-5043-6440-b709f9fefa53,2811618773,8110903463274528695 +d9451f35-d83e-692d-0a41-0405ae6d1c96,775598319,16735788747137180500 +a33da8e8-c2b9-8c2e-ccb1-105c4a9fd81a,3493908020,11840059698807290339 +5c6937c0-6edd-c069-bade-ebd5ff80de49,3078376152,17948052714388737247 +bbf15227-6a6e-85ae-9e48-108c7f38c523,1450013069,12409891302426173040 +3e98a2bf-7fd9-f578-7b8f-27ec38211148,1705002451,3311137434932433226 +296c2ed4-87a4-8235-fa91-326a0ae6d971,1550199037,17250527299182422481 +1712bda7-5332-52be-d3b9-d7c11dff2b78,1240118504,4330361716699240817 +31abb4e3-08c2-2d81-483e-9828431b769a,2207683491,8185015215168310036 +d6b6c151-b73e-2dc5-0c5a-33475ff2b943,4014225465,4347779997515374132 +cee0fce6-19e9-4c8c-6cfa-52f1fd550ed1,818967351,10387390611379796870 +7d577eab-88df-9712-4ca6-2c8b71445329,7135271,1573012009895416957 +d7531b45-0786-e044-02ac-32ca402d9c17,962886919,2568241723374213969 +e1fd260c-4d57-5597-03d8-9145a08f60e8,2488693704,18230471577799645841 +d58fcbc7-41f4-e6a5-1830-5d759a07fb15,3989172021,14351320574193875923 +e5e8c8df-434b-d7b2-1159-d9addf49cb6b,3771506197,7726186778948982405 +7ed50a84-3a46-3411-46eb-0b9f1e989a4d,3970097968,13399760627389440235 +5666dace-efc1-a1c1-dbfa-28f409428ad2,276668662,224710258032863281 +57f8638c-9fb2-6a0e-df80-4f551983f663,4192025423,449321327060813073 +0ddec4b7-0ed4-0904-8883-b9a438e4c4e5,1269438220,10409136222538537299 +c3ca0e01-6bad-b72b-c318-9a4b6f57132d,2189215704,3172321609552189651 +406c6265-6aa9-a25a-ca30-702f95fa1328,985111055,3623307821041370609 +15613dcd-39e8-3b02-a1ff-eaf6c5f1b1c9,1209166976,4137810008586018615 +e68e7a5e-9e1e-4d4f-682e-3f8c4e7dd635,128367686,6048457201350349195 +742f6cef-6ec4-ce89-9bd2-56c4b7a5e01b,1361937467,11375099311999855247 +81e9bd40-55df-fb3d-4ddd-1a2aa95a10f6,902479020,4241805773854839582 +8a5f39e6-0230-e5b1-4dde-92d8ee43fdf4,573249217,5331848876000638083 +08a85dcb-aa84-d3e7-44af-bbd63f381a7f,728291115,16756309803950694549 +c780a6fe-07af-c7aa-dba2-73e0dfe0d0cb,692009296,2686148350851593014 +63b301ee-4daf-248c-f5df-c30027a67924,1301730885,3461589241293203024 +c1910303-62a1-6071-c606-2d71656a42ae,1684676089,10833735231700372530 +12dc0eaf-cd6a-d2a1-e0fc-047b5b52374f,2804807884,9360475475717212445 +8dc03c1a-0f15-ffd5-ef53-e7b2dc7cb52b,1952914329,15160808293278463092 +682ab6f3-5719-2b7d-89c2-ccf8209eb535,2091127593,10918773622517821152 +1ff65993-e013-7508-c8bd-d70cdf9a71f2,5355993,1681560000710903287 +0688dbfd-4493-b21d-6aad-9208e2849dd3,1980588613,11904832752675381191 +251b0fd2-2755-cb49-1fad-2ffafbef0fbb,263878352,11131388999223795694 +4ceb45f9-6e5b-4ebc-ff01-f4322b45310e,2069677221,17059044069128895357 +ef045808-a4c6-611c-09df-8b967570855f,1392334658,4424011924062698092 +af7e0e14-7007-4e64-0f10-6708169ff563,3638343350,3450532313587171546 +0833f795-0062-88b7-6b47-31efc4c86f6f,651118292,10884699703737825389 +426b2521-5708-4491-ea4f-aac04bd13e35,1465677215,15344371242800048279 +e5a4cd3d-e1c4-448d-4ca4-aa1e49528d7d,3734685017,617716002533225537 +aa9513ae-cbcd-3ead-83d2-c5de4af143dd,3284400325,1806401196123344752 +858a8b8b-b3ee-6ed2-41de-64055f3a4009,2658072111,1197654999873422877 +aab90caf-bdef-9983-4df2-4d4e3dc94909,1721409755,13624677813225697909 +f972487c-97a0-c846-b472-b394b1944b08,544852907,1492779243932587810 +f7336af2-ba0f-d944-5ede-b118bf37548c,4163154267,2943717620429978939 +f0734550-d43e-85c6-f455-d0e2eb903a43,774213828,10886729809427949991 +08021669-ed75-e7fb-d676-3d2a16e131e6,2292685827,4896783400995902516 +6306f787-f852-06b8-f200-b963411c7e4b,1014191179,11131837673575752600 +471c1fdf-25be-8350-5454-1251eea9f0d1,3103224388,18064809593270245664 +1c5c677c-d480-1c0f-4d95-d9675b121418,706470157,15070690153033291672 +59a549d2-5d6f-9003-bb4a-0e75d3f37d56,995297647,13749570842440331837 +4afbcb45-e473-07eb-a04b-52368a7b3292,2900347359,11136670879372185123 +7d2257f8-5f22-e279-54c2-4a222c22b979,835450066,12707247230975875667 +26aecb06-abc9-3efe-c110-bc067894c4f6,916169814,7540875326851460621 +04f9faab-0eec-ffea-1ea7-0657adb79554,648023767,7999178332642554632 +9d1fe657-eaca-5a2f-7b1d-059e0a4417bc,249966975,5151000455218854923 +0aeb7d1c-6734-f446-174f-decc585b93fc,1913854420,17800745045088671551 +5d6e2a82-d00d-5b9a-41d5-49c50578a833,1452574280,2271538896946929197 +76d33605-8bdc-1381-62f4-174a84686280,2310549351,10746580647783950381 +6f9183be-d7b8-110e-43b2-ce033b83734d,2349339035,1846945431590818084 +26c27482-7632-6e78-c14d-6f75553d20e9,552415484,8562383636253067962 +68baec53-5f5f-b110-4d18-fae30d7a5ecc,2336863129,8472711748690374248 +7ee6ebfc-8589-2ace-b73e-8f5dead51082,4118646917,3177240985261171544 +184c94eb-7781-1286-6cba-53185f3dc3b7,2087626653,6826961717913350900 +fb892663-763e-4f90-d47d-15608e56433f,379572683,13230827205788902083 +bab0286e-4513-53ed-808f-e50455352daf,1591785778,2746940953128949003 +f183fb7e-f4fd-8581-a815-91eed30b323a,555138219,17903262622272916871 +50209c7e-2cf0-4c50-77e0-c51ac5de1e31,663672430,10849025845828068504 +407bc21b-deca-b8de-5336-c74f23377760,2167585836,10112140032671037988 +cd5f9b50-2020-0b15-9218-4a6ad9d35b47,596978669,12712639103447393161 +1d5a79f8-fd73-b6cb-64b6-d92de4640017,3590966825,7244008903689349233 +1c091054-be29-71c1-289d-879282b3c80d,1101108583,759771486190255840 +26cd0d6a-7f7c-313e-d67d-b5453cffc9b2,1826770619,7819098431512334723 +95e6621c-1026-1297-d036-afd61ee6970d,2761651645,16161602224444204473 +d195fffb-870e-a459-904e-c1e68921f513,3385170225,11497182853324215267 +7dad54b1-68c0-48a2-e061-bd0bc94f7ebe,4171457460,5784851947377192939 +23d592a1-d9b0-6a33-0312-655f3511bd08,1211140007,10969950349614800616 +939c04bc-270f-94ef-83b0-3d09e510ef08,1712458672,17541657681304082947 +a85a8f34-4247-f587-fffb-c32c2bf11203,3917637329,17585125042211887920 +f8a2e594-4342-20ce-8d17-ac04b939db63,2323817426,14516086266176378972 +2255e1e3-9869-e0bb-472f-9eef57f4fad0,176605679,6922170166269050543 +2e6fc77e-fd27-de14-cdf1-b4b705f7a73a,13582291,9915118524717149905 +0cc6d4f4-e927-68d2-d63f-2c2a05d12ac1,2709179699,11847284907685196912 +378cea1a-f0c9-b6c3-f61c-c8fbd1b3e445,297915082,2340050854759641321 +22c50ff0-4939-0aa5-4081-53a6067e1b10,1732260374,6702671232758620106 +eb339922-3061-937d-fede-043c957bed5e,109228589,14358054547091128053 +c5392891-fca9-c330-5c65-e019e329da83,2340659350,949982718972516173 +e271bc3d-1401-07b5-f42d-7be7ab68f2dc,1819636374,99430926496317538 +9ed1efcb-4619-c3ad-6d03-347b080b5516,3154222505,5818928591294642483 +028cad1a-7089-d9a4-e63a-f687fb23f068,3215572135,15127142134609204040 +81ffd2a8-97d7-14b2-c096-b1e2337b96a6,3199311193,14734249376909855411 +0b0018b7-23f6-4ba5-5398-8608c3a1968a,1692376356,5063791479210171913 +bbd88ed6-f33a-9ba7-ef0a-155efa602c5f,905812250,16296902313551846941 +3438a4b4-902b-f89e-56db-89e06ad0468a,740764047,2042757931307212428 +5ff5e502-8813-d2d5-2f92-b3c24f245e06,2037245994,10961056926382908668 +e818ca55-07e0-ab3b-9236-d062d9aaa21b,2829617621,9305464151704205684 +82ee4d4d-14ae-18ab-b66b-5be42706c3ca,443054335,12510607845436994536 +4705aa17-8e64-02f2-7d09-484d5ff004be,1275129959,7571276205610077715 +39642033-e080-5084-8be2-340a9be046a3,3891670948,5034991376004046340 +8943a91b-0ae9-6d9b-807c-c0074155f26f,649820063,4292906489068969046 +b31f89e8-e42d-5017-8673-887531736a70,3021764734,16286550578455981749 +1d760008-91dd-0a56-1525-b83050089383,3400988747,10774179850134297114 +820671a5-f77d-b883-4a75-00a9efee320f,356587737,11005148146564176838 +f25fba0a-a586-5986-29af-c10ef1b36ea9,305093604,4790146617123804124 +4dd5c57c-a8d8-c213-55c2-669397073cc0,2913976520,13818032066416930771 +dd11a88a-86c9-d212-8f9a-5c69d8014d78,350267357,4075259408672124000 +ecf0aa4b-25f3-8055-0200-e2ed179c8e79,3061371213,5767107044299654097 +ab01da73-a265-6326-d64b-9b334415a39f,2828681227,3411914797686888646 +626f0164-ab2e-8910-4101-a8fc71f5a41e,905667090,11084825918578579893 +8d14b321-4365-2d75-b89e-f8cb2ae76d2b,1986344440,5462726278157003581 +25a24fde-6d1b-077f-8ba4-9000c84c9a9e,1107034187,7572442872756704880 +baf65c1e-5805-1011-e463-e10e8b579f29,3426757899,11722737448548879530 +9a24e1cc-ac81-5e73-bf77-63c9b03aa6e2,221179228,2607163431951900754 +15d1d86c-dc29-1bfc-675b-65cfaabc3aef,2676359298,5250468145227516543 +4254ffd4-4006-c04e-26e6-75a28a530051,3766675164,3980564112403759824 +8f655d6f-aac6-7f26-ac32-266a56d83f33,3744927845,11407936281893779601 +b2659c5e-6b26-4f0a-ac59-ff7262e8db0d,2495413070,4291352343131410920 +f9f62584-6a35-9d28-55ac-7db7b942ecf8,1935205715,6444718887818881270 +68869870-9923-c4d1-2039-5b3b67de51a3,1630305460,14919670489062929228 +7e4095c2-124a-1253-b017-afebc000e711,491874489,9900142262676715803 +d8b9b138-5ff0-bbe7-ee7d-823793317779,3531165455,16093672180989298983 +a3304e30-f16f-8bf8-f2f1-a549566bad59,1605387438,7196636061774460888 +04fd64d2-d4a8-cc0b-8d81-c00b8de070a4,2992102506,7753986195261487107 +a15f0ef0-839e-c8ea-3375-60ea2198033f,623395369,2743822358698824465 +a06098d2-288c-a859-2b43-cdbe88061fd9,1360110029,4166535100693357509 +bf44c482-8ee1-e166-0034-52e9862e746a,1101684874,6268470677370976752 +9c008ce3-0c6d-3107-37ef-6141b42fedf6,289557191,634489412844545617 +b169c73a-55f0-4bde-e230-bc20484700b0,1318259169,5708137278760801211 +c9bff708-47dd-c9f1-963c-d43a8964a024,2448603235,6042655540801336554 +aba09e5d-888d-7667-4873-2eb45d402b56,1316256389,4826061489939284341 +3146e1fe-a58a-1074-cde4-a4fff7ac790e,1927766981,8174175550417216877 +16d86ccc-3bd2-ae0b-a6ae-b9f200508903,3571096299,4844892706660845751 +3a5f87c6-6da7-8103-d2a1-a51e64ef15e0,1184146412,9504631046610309078 +2c24222c-8dc8-f7e3-0e80-83bf1724851e,2467885093,12558018248309248888 +ef65e1f8-a512-3cdd-5964-c5ca00783f80,1351531975,11266615042674972357 +c67030c3-57a8-43a1-cf1f-198c1fa8cc1f,554746817,8838191175543963649 +5d1e1369-9259-01dc-750d-583008458122,3715550120,14173131708892224471 +63fefc87-9bb6-776c-9dc6-5f5a9c6801b0,266218795,8664453070405798211 +3ded6d19-678f-9abe-f51f-ddda55e7bb13,3214874259,12337744500014320200 +5040f33f-ccea-399c-d2ab-882b1f2f4c27,4017731872,12535772431248188581 +3806d815-420e-8d86-d304-71de69664390,2129896069,15840157030573061072 +6b64b6f0-201a-2601-e783-960590bdb593,266160915,4857692812988289078 +0fd386f9-5002-8b4a-746b-26185679541e,2041756714,933858166955598864 +69b598c7-b117-b204-b74a-ea8753d0cd68,2597424058,12975304190008731540 +4e2f8b60-5dd2-0c51-47d5-c79423268f05,996189427,15773345481512578385 +b186fc94-1b85-5a65-b16a-fef39a21d743,287006995,15111670251435782818 +9e608683-ccbd-90ca-58a3-610e6c236d1e,4288323276,13931107885929661242 +e8a65d46-c1eb-b677-809c-800ecdbc730a,1719981983,17286366238580549479 +038d30a8-82a3-2ff1-3ab7-3099a9ed6838,3062731853,17477805259960348269 +a69fefdf-b7d4-ebeb-ba62-720804d4d1af,874844150,12559838828690523839 +d67555f8-5507-5236-f60b-9a6c02c6c2d2,1581805714,1065910951195598877 +702c4491-1c2f-8de7-c313-5a571cbb54dd,1796402189,284075440892792208 +82e1115c-8f1b-a418-d486-caafd286582f,2010990470,5983941795046198513 +c97b9cac-2a28-3427-a210-e1f2ce1fb9ff,2736329311,9188582447047306997 +61f8f668-3735-78d3-1e33-bfb6b5065d9c,631000866,145476087751844878 +8928aebd-6ef6-d95a-4c33-6461b147cbff,2939917153,2268673458093622559 +95612503-8580-d1ed-8ef7-21561e892175,2813612686,543328678658452312 +e28f91c2-1120-2e86-0733-94ba3f754811,1288640275,10043488410647746685 +a02965d2-0828-b5dd-e413-03e85c4483b0,3960305652,13222431697166634636 +8ea8f605-a091-93cc-0a98-38063da97a4c,835359217,9647455789975692937 +9c65ffd7-fbf3-9da5-5833-92eb7f08231c,2492719474,15468964456523074197 +86a3d609-fda1-690d-66a6-a53a0fb7d6f1,1336168647,13680456649337414699 +b2076d92-1ac9-7f52-25c6-b520b9d06412,1924618581,704692911184888624 +884a8c5b-cdf0-a267-a51a-2cd3e41a55bb,3158682260,605611028786259467 +b6e9544c-6d8c-49ed-96ec-2971989971ee,4007260985,9099394830507892710 +c5756264-fc19-644a-93b4-d76aefdba0eb,3446993698,6792515823608644028 +bb57e23f-e0d6-2b9a-5a31-01a739bcd4c4,3544305243,15982769508533679558 +56f3c4c8-19c5-c1ff-d9dd-2d19b6af0ab8,2307782825,14742128910363287922 +8481e9e5-4fa4-8d7d-e9e7-a92601fe5162,976013404,10294231277422089452 +b1bf4b24-d8e5-b612-f824-afc76c564099,199555110,3707124369718464060 +73d4ccb5-a71c-4978-a170-29467c6c4bd4,3371539703,14248600021510796168 +1a3620e7-e7ba-ec78-83c8-d62c352d5e1c,2621988773,5040720709367840808 +b5eeb1b0-24ee-4a4d-5c00-f65c4af2a0a2,1376062214,6288957812666035302 +5a4276a4-9a4c-59e2-a433-cc350a8f683b,1960534020,4548140189888192722 +fa594eed-1e93-e930-5159-a395fc927a9c,3430005786,366513627655065035 +afadb1a1-b6b7-9748-b44f-96925943b1b5,682477770,12407361966144930454 +863a9ed3-d246-deac-f70d-67f816441597,890964568,2545692170620664732 +9bfe0ada-23ca-dfe5-eb7e-3cc23aebc83a,1756324899,12264498506078290283 +1ad42799-1d00-d54e-ecb3-e0f7aaa1dc64,1479802306,4875596086702704087 +a9439a3c-87f7-ef03-1b27-2124bd9e0dea,1028821630,15725505610154069930 +e2904eeb-d6bc-191c-eb86-608b046414ce,1507103721,12566709449765285976 +1e58def5-c596-ff9b-8637-0540a4445f57,2560215139,6510907842728959936 +ad77c8ef-2b32-d49d-2274-ed082298ebda,3224285618,17887187248831070392 +7295bf31-1d0f-6341-3d45-f11dcbb60bc5,2338971574,11376297933254568064 +21d575bb-c955-bd86-b7ed-7cd5f7f6bc39,778302342,5761481438252532298 +c7dc9247-aeec-ac03-3b54-c05bec98995d,3824013256,9486479625704327925 +173c5805-2a5b-8bc9-a8e6-9a8eee52a65e,2217303623,11304603913618560198 +4fe8462c-6691-ff69-b7a3-55c044970769,2862820048,776268941950842774 +f3354d7a-c213-0d1c-9ef8-a28ba8315246,1010099818,10749717330049307356 +8babfb4c-7eb6-7f16-b43d-68a50fa710e1,3726694981,8692842207129678688 +e508aff3-e4b8-e93e-1e89-2e36f922b675,30060823,2893584069472498262 +127edcc6-97b2-f4ad-80a4-b51273d834e2,3965857866,3303407906573452504 +a9e6ba6c-2f0f-f27f-1c22-64636a0ed2c1,712350190,11091652010142857417 +9d930974-d193-2b4f-26e9-36126e570cad,2743665693,7996493039011664261 +57a30535-f911-6568-8fb0-b4a5d23c3e7d,2952356951,13912746008324326332 +f000bc34-165c-47d2-e064-a46cd6f559c6,1722278421,13681153333018939504 +bdad1bf2-ed21-e9df-9a65-8fca2770e1b4,1453109919,16141705393050606563 +031cf112-0f06-73d4-8f33-68de4c9d4dcb,2146175916,1522203534658638910 +1d7d2db5-b6fb-da19-02ea-640c3b94fd42,698116376,2020579787443434948 +bd907008-2561-891c-5fb0-1adc1241d274,215959483,11698641466155536293 +35d7d66e-59b9-f319-20d5-fe33bcd86a86,3293573545,14429181985396851127 +ee352215-18c4-3a7f-e167-b077c653d9ca,1581477006,1158611192890202794 +4abedb36-79e8-7236-f02f-74adaba30c35,1780586641,3473842205235062539 +33b10f1f-7832-2e25-14ce-32c4e9b0380e,1911887352,3505513971820536187 +b8607604-a686-49ca-7795-8637ed7b1e09,3958364140,3563736961821766251 +9d762e77-4ae9-d850-a0ba-70722c09bfe4,3066411837,18256064374050970053 +977db6ce-26f6-a2b3-8e28-d3e1237924f2,1846919556,10563916700975740779 +e9dc2aa7-67b6-8fef-550c-43d9ed3b6bf0,336841953,1449100298818957430 +82a41f7f-aa06-e811-4882-71ee41404acc,3410445559,11239957036040988222 +5e1a0c99-e3de-51a4-9414-bf8a49704402,1305842920,1528660020574647564 +2c4a30e2-6ae4-0383-69d3-dcdb7f31e699,3967550878,2897085361830377172 +dbe0a691-029e-8149-310f-4f0c1fe1603c,1353131565,1103005315519594773 +6f0cf28c-3547-7897-f3dd-17dfd83c43e8,3883361625,10968216249125028337 +51dd6a5f-0ccc-a518-b3a2-0d525d26135e,796376985,16457042553719302842 +939e3edc-7a6a-511c-3832-e975ed42a495,714260257,3810099941218859357 +9836cb14-da53-848e-73bf-5e57d67516dd,1538103023,7509897936482511213 +cd85db6d-d2d1-b31a-6465-838e6cd7bd9a,3513632407,12258432749541286498 +cebdf8dc-f8d8-1033-d127-a814f143d9d7,4247936921,16154715062122779365 +264c04dd-3ffe-219e-1d64-8b47bdcb87c9,3577749920,13604893658158753971 +f1b79ec5-6929-5d42-4d0c-9508277ee71f,2541576408,4485417603491762263 +d795d325-8f46-2dcc-18aa-c60f630391f1,3891383526,10313868429981247554 +c752f158-99f7-d913-733f-781801a2a10b,3304215314,6331518380991223765 +ad3a9450-eb0c-9e57-4871-de81d2d3b5a3,3586180632,5717975580842709053 +13716cdf-76b4-3d54-a628-d7fc57636395,2627715854,1271122351039361163 +9e0acb28-18e2-09d6-e7b2-492bf9333d63,1693561689,8115129786729929703 +93e64116-e762-4039-6d7e-efd900648636,3864224291,5846732751453673442 +320a8a36-c911-5cef-512c-997d53522100,3513106303,1708717293129574394 +50d3a837-d0c7-4c0b-7936-14918061730f,104811598,1918809163759727642 +6e845ba2-16b1-addc-6d36-1b17e535b81d,3994232222,11487415410845278365 +e056be78-ad91-83ea-35c2-dabc42d2d45c,2833606475,8153089149128914220 +84aebc84-981f-f931-3cfd-0354d3d3aab3,2510674754,16002588499654689165 +c3a6fedb-fc4b-0130-0a25-e7c8ccdd1865,636973361,5398450804722398284 +55356324-bf7d-d093-1c97-bc1dfcc5b017,3532607270,12557018929704705861 +16518b13-90f0-fb2e-5d08-b9aaaebed87a,964357088,6351029100523077013 +1b36510d-226d-b07e-ef36-d08a65641a82,2448158478,12331715113814990169 +aad1831a-f0c2-4dc8-d659-d7b576bb559a,1981276588,5819254760140330449 +082bd394-45d1-f97b-bf20-ba9f40b4a6da,655591672,4849394986707086400 +75f4d805-a1d8-9e9c-a65c-9af10857deb1,124026739,15238672304858989591 +66b198ee-1d45-117b-db09-07ced20ada4b,934355506,7007997711145860746 +4b2d727a-a34e-849c-2ea1-a9ccff0f6fc1,248132491,12716914288299656293 +c2d701be-7450-1cba-8d89-39bb0a06196a,502169357,3333581825933563534 +2024ef8c-5e84-992d-96ec-6d756c1d2e04,3101858739,16635981273308100738 +0e606674-1f05-efee-6921-681eab07d9de,2926083836,15637728152057517136 +b8077117-a764-151a-2b0b-65208f8445fb,3822524246,1590777611486629988 +ec4b3495-a385-ad68-26d6-362a6e8fd6f4,924751151,3235248477685793806 +c1eef4bb-8416-c119-2cf7-2070d1245aee,316347302,14591092852491906548 +4b57b989-6b50-4257-7d6d-2708ff6fc9a0,4259639854,12120229635357722789 +020cbd74-ea6d-f9df-f8ae-8a9fcf66f13d,239877381,4539061929005576491 +6fc727c1-6361-2a2d-0e43-df5e0d931f38,1895790374,4532762157339733507 +42b7281a-2d54-5912-37dd-d30ccc2f7509,2924824433,3180367774390315309 +9217c014-0b9d-77b1-5693-efe4e5408425,2505917289,18002980046013170924 +532c4648-ca12-f549-9ce0-c1b91e0eeebb,850831035,5268411552414283925 +808e1378-233f-e449-b6c7-05a293a970cb,1334213158,17787314938512117794 +bb843443-7396-6a0b-e445-364a63416ae9,1234813747,13334645511988070524 +bd62bedc-2525-0c44-811f-6301acae9213,3548730759,2137447094526693508 +8c161932-13b0-71ac-5dba-b3ba38293a1d,4273528473,981022990979728496 +227b3c10-0059-e8f3-3982-67cc011fca7e,3763676823,1954611682039014419 +4ac499fb-2652-b4db-dc83-87622de9d91e,1951833773,11142775676704188481 +8ab452e9-1830-86d2-8833-fa19717abf76,724612649,4509572489633907675 +9e0d7d85-d5c5-7c8b-2877-f9d5d08b51d0,1735025769,10559667658506324052 +86355b68-62a1-e2bc-0cf5-7914161a4858,3857230461,14126234269306939758 +51b06fc1-2371-fbd0-553a-a8c4ef77150d,1435143665,9206472187098323985 +5333cc50-2df6-7f54-af9a-0d09bc67ae2e,4065283694,17425042472532816650 +0f07e97d-2c3a-390b-f2f5-fa07f1c763de,542621673,6663228719587920992 +4ba35aa8-369f-47cb-4751-d24fb1c31fc3,539350851,17808094542001672848 +b105ccb4-5240-e611-616e-4c0a0f9391ad,1535159514,6396847103677979684 +7e6b575b-8ec7-aee8-617c-aecf9a6ea3b8,944919279,4704324521629611192 +2da449ca-55b5-7a2f-5fae-22ad30f71346,2340588668,7574350189375369578 +2be51966-3af5-8a5f-664a-02aadbc4d312,698779305,17926904602556698763 +6c52df25-36e2-3c6d-f802-3f97ea533078,22632160,8023236906104396408 +7d15f7a7-3f36-5c30-8cbb-aaef73b71a7d,2185387833,16639628983225384131 +53f87932-cf9d-6d08-bce5-c05945320de0,2559496965,37085232371956196 +54327979-dd9a-ca08-1c8e-05a466d463d6,112700925,10716240658318128134 +cf39144f-c42f-34c4-8a6c-c40da02f8d53,2977984274,14031084184999026743 +66c7282f-e2ed-036b-b7dd-9cf705ec7642,2386033099,16129456340005144615 +e326ef6e-bc51-9cc5-1aa8-26e647c62910,4001028436,7532937277342912280 +25ee700e-2fe1-9dfc-6976-d86aac473e16,1611812599,4149638964875036439 +336b86a3-c20d-d544-7f21-84fd46063425,4081179494,7564342882197617593 +7806018a-d4f5-a8e2-b0e8-b4b633c3cc7c,3919152402,18187653996512438650 +3a406bd5-ecc4-91b6-a414-084b9c55f8fb,3496632350,5359194697341522930 +28239dce-92a4-d26a-f2e7-efa0b30378ab,1045085343,3428374290126759919 +fed42651-de0b-a1a9-8cee-b09fccdcdaf8,3105408370,12143913979253477078 +efc5b9a2-d67b-cf1e-8db9-1c6a45575f6f,4058149159,14545950582312341631 +29e84b85-0450-95f2-8cfc-6cc864fd486c,1761221833,15016040339677793120 +5cf4c2b4-fc04-8574-5592-a9f05623eec8,3816964254,7467178300239653720 +f2e08e15-b1b4-46b1-1f71-fe4b0ed6a799,3361038694,12583755778303130134 +70beb142-14cc-c0e2-e39f-a75573a6735e,606772441,12009620598347378779 +e92a8385-91d6-2352-45ac-3ba1f210d16d,813569776,8036709251925149863 +fc3213ec-903b-66bd-7a64-db4488ae9f43,3641216778,13563336444723940279 +e9261b96-9e15-6e88-c3a2-df21aad97456,2690416909,3059301478858072032 +9418daae-7d43-4513-9bc5-3faccb7c51f7,1753150460,13878205338360553847 +d6998bd6-37a8-94b5-337d-d72767a07cb5,774886426,14658936529946056931 +c9516019-bf89-5c0e-39f0-e78b24eaa57a,283112054,13297668414140010451 +97fc3038-eb20-b4b6-6718-f07200b3ef65,2233506401,11992312023396398769 +d6fcbc4f-a941-a0d9-c7b6-60406961abb8,2627203632,15576781298725859715 +06624701-8e27-f5fe-d45f-b5904cdeeab3,2765105982,11242403206202804273 +6b26ec09-41d2-b176-5c1e-bc2aba76d7d3,744860863,1416029971348574371 +d79528ed-141c-1b5d-8e06-2658c71d0b5a,2419683650,17761391170417428767 +ce40df3a-2939-0417-5682-bc4a9ab7dc0b,1383712991,6776383371205444294 +f962b326-c9b2-29bf-0670-79ea0c63502c,476490557,8408568751595796319 +0144c2fb-5091-d36d-4287-f201ddb4b8f7,1837594739,14493714240678770005 +01ceb334-41c1-d999-4847-36031f0bed7b,2006972002,16462397162037390909 +985043f0-770f-9d73-94b4-2cea7305d266,779506969,14579920604933005303 +4d4b634b-6e22-ea14-cb5e-0a62aa509948,2276286424,8468721787061419597 +62060744-840a-2cc0-b679-c6c6f0ea03f0,3278231923,14504958982255651156 +a708e3a2-e62a-be69-b886-1675c0782971,125960025,3204206268012943549 +884e9095-0d97-3613-a4df-96647071fee3,1695218137,18371303945553255426 +c73e0586-0ccc-e9cf-7f81-5782997e2a08,2716885512,12955774384624090306 +ade25deb-5b76-07ca-7bdf-ebfeddbdd6a3,2589606474,3343782538934576587 +f5a6c792-5692-43bb-85cd-bfb79e383340,2001708641,3883866854866413176 +c209ba94-fb68-f837-2956-1c6178dce016,903912689,14126288368351921637 +466b6994-4daf-7908-36ac-687ef13b2a9e,2091819507,9786378647549441586 +8f8a01df-0955-df37-b258-634167fa5552,1288007764,5975083168745411788 +fac2e01d-be8a-fa62-e3f9-619b048a4d81,3047719142,3647517507155771498 +ddf4ade6-5709-dbd6-49c5-aba065a498ec,1787883213,18202916652944481680 +a1e8ac60-1699-4059-7400-062e3da71ef2,2675418434,16075067874760211340 +12c14114-88a2-ca59-6272-c8d6ee866f3a,1802729047,17410426337654410752 +b9797a96-b6c4-500a-a2ed-28cd3163aa7a,2854635130,15959945064322538730 +8ce4fe62-949c-538b-157c-6ce3cda67210,4089788353,1076147889953991479 +9fba6717-3ec4-4f28-d3a1-47a1dc3b8283,1014786306,6168840644410785247 +accef49c-91c2-0363-6045-ffb943d0f979,2391767172,6281534313073039856 +daebf4bf-ec6f-c07a-6690-843d2c225db9,3128026737,7384460424563586961 +6e041699-6366-d490-dc50-7b42ce92fdd7,2432639019,13169971689110264960 +04d79572-659a-225b-addb-6f34b16c2b7d,2234394608,10897412864879047213 +4cff267c-f857-1a25-38b0-09f77644d67c,3227606985,8224452207787443242 +56e270df-ac37-7884-c45f-ac525abda21f,853141812,16311515096384120594 +62d344c6-bedf-2cc4-d47b-57d9e2173535,2555601334,11164982087020247804 +0bc31591-67f3-9ec4-e130-f1abf7a2ae33,665707652,9515490903604369565 +48029146-4888-1989-ca03-4c9c39f4aad3,3462768187,17170728828288960783 +997b43fd-7e8b-9d25-91bc-c6b334c2f780,1952033007,4559844032131399243 +dc8b275d-ffc1-2271-f17f-1a3b116fe7fe,3356931327,3980527080019802706 +d5fe0cbc-6adb-24ba-0c96-9ad352b4344e,2343057767,738297616513678267 +5a15bde8-74ef-dca1-642a-c71012121bd1,4124539206,10296573416445938226 +881bc4b6-6b00-6660-017e-7cb6ed2c602b,818290922,9793833476889164886 +8736b03f-bba4-a236-5ead-8f660fd18b4c,3437445009,14305509169500794936 +4bf77e1e-8997-bcfd-d59e-f50729fde874,3974459951,6271441678140341636 +76b10a6e-58ee-aa51-82f9-5c19a03934f3,2299080898,13098630710590733803 +f5187a75-e98f-4210-f128-52b21c71c661,716983109,15468462449682698138 +92230cd1-2571-4a42-72df-8e8e90ab56d8,3293859516,4942669004457168055 +b33daed2-412e-7a81-a57a-f642a6d76747,1495704312,5122921257206022130 +723577da-1164-b0ed-ed5d-9298af6543d6,1020721836,6995737665962667043 +d2cd9bb1-8af2-46f3-eeef-4bb195e199e1,3737276821,12475816969651597283 +f2fb1b8e-ec94-684e-c4c1-7792c959c193,2042707852,12806334944574239290 +be1e8e97-1dcd-e11f-f8e6-c1a8988c9414,3677708679,9508253525797427253 +70c8289b-5934-6af0-5a6f-bee002d2d23a,1821485577,15127205086789275732 +3b2cdeee-37c5-7eba-7bfa-1cb66a9cfe8c,1203777786,17332968574617884339 +2f2c9884-edb8-960e-6e5a-703c1fe52986,2429076763,1084116428456990855 +aa03af93-6ed9-bd87-7f54-01ea483e7367,3265771215,6698179830991644105 +7f0f26df-6ada-cb32-0fb9-d90bd709fbe4,2156672817,10784826599966585691 +e7ec02e8-a624-60a1-9932-35ec6d86a9db,2599090588,4989970721235576222 +67a2dc23-e06f-b0e2-0a02-a947ac3601d2,2017655082,10827725013398746235 +2cf35e70-4338-ea3a-b056-90c23f75979c,3412960176,10375500911534489223 +4d4c7a18-d332-714b-1ba7-2f1c3fda4076,3089818889,9890220434706375410 +f4bbfd36-f530-58d2-efd2-bb1ba9c9b693,3392121122,3074381759250006720 +72454e9c-b93f-90ae-b94e-4bc68b62fe3a,3018925962,10720682330538248840 +7a3e9f83-9663-ab48-8949-ade4e20b20fa,3650799417,12074631217778032848 +1f8285b7-b60c-099c-ae87-366e2191a214,3017311430,2658628648916806388 +ca0cb282-67e0-54c3-f76e-17cb680c89c8,2717402078,14344551304665320413 +c4a0224a-fc8e-9325-99d6-c75b6d8fd3a1,3267985314,10303353948826610647 +e17c4dc5-4529-6c04-5a31-ef1e6ffd4a94,4263128468,15739070454583871000 +35813756-c517-d785-275b-e8a35d208e6e,3559546358,10711579670735178745 +3ece27f9-8a43-051e-a17b-e9cf567e1186,2722390963,9947968816075741061 +e739a6db-2c1e-7098-21a7-dc96a678c4b5,3582117754,11148089583147633310 +248e13d1-b63c-5bc6-f5a4-1fca8fdc0f69,1871356910,8994984287153637435 +be3e4c59-b668-0061-e9eb-89e2eb43d041,2111161905,2439230810160737822 +e8765038-0859-1006-1500-e1772cf770a4,2350632281,1084636107338386647 +3b4cd52d-d444-b5e9-7041-fc6a14b29bb0,2539454927,2717997358583360032 +d08c816e-14a0-13c6-7f44-707f0ab4d4bd,616289789,17992816679070595633 +175add23-597c-f2a8-c1f4-e7b722403b68,2235291968,15892675119228201482 +6a9137f4-3722-d5f0-bd5b-cf4503a2c396,2219305803,6324015831320110264 +3a918109-ecb6-c9d5-d8c4-74101ea481f2,4183103443,9129444753260888288 +c7056c77-582e-c62c-51be-cc4d1766fa0b,224935463,13126639800379678558 +0639f08a-72d5-eb01-7130-499bbbc46baa,3517385631,10875045179371601037 +30bde76d-0c4b-77a8-786b-a6ff274e99ba,3879757307,15702723306740421306 +56d127ba-7cfa-ef9a-a8d7-037e5f236be0,2184386616,9688524248826220679 +9ee5708d-d24f-22e1-ef59-c5ddd6248adc,1073830284,2067104529503803140 +9bee324c-f5b8-8161-5c83-ff684d3c25fc,2105293321,15000283575250191651 +aab11165-76c0-a5a6-6bc9-81d9767d7d85,2874621227,6815850552781354643 +ccf774f7-0b6d-a0f7-379d-db5bb7d6edb0,3179461831,16093004135819972184 +cda45a21-245b-d187-8f41-1f8ef51ff87d,4274848947,11363707484997361536 +28022011-24b1-c483-347e-4eb75a7a65ae,3114607449,12383581765525280342 +f48ea874-6843-5d90-5f2d-a0ea3c07a556,2682858021,3519772489181019453 +061349ea-3aa8-7574-447d-172e05deddf0,784535434,1530230925259926151 +6a6e8cd6-1728-7f16-d3b2-83d90c60f281,1059204261,11448204614717958604 +2c70d118-88e3-703a-fcf9-769e036ecf4c,2022159494,15933148735977939416 +91b03d46-13c5-24b1-bc0e-7c3521a4f429,274729388,14782259943119777689 +b2ee1974-7f94-f65d-6ea6-24efa9e860e5,1166954334,8830646660159619314 +b881510e-bc37-4711-6716-09cf8d50b7d9,350914667,9084889811140510827 +6bb12646-7927-af87-78ac-cecd4bb8f6a4,1645350176,7628245432648436030 +e7bb7aed-ec38-b83e-ce1e-3e7021703647,680144842,2797389187275945584 +7a98658c-9d34-be6e-6b70-7a6c00d3d844,644581955,9270015707359491726 +79cc238e-0312-6daf-2030-89e0bb9e652f,1148329404,6886484418841844862 +a29fdd06-fa21-7c94-4fb2-a43870b2f5c8,2393328868,3901279577320298119 +3192d5d3-91c5-c1de-70e0-d151614edf8f,3827148489,1540726778688349631 +9a4f852d-49af-ce1b-fd7e-fa39886e70c2,963139314,777992985651313300 +e009f1fb-895e-cca2-def3-f34400bdb185,1601394593,12062803266588501742 +2074b77d-9511-2253-ca8b-316921d02938,944179273,10251380740720954995 +874bdbaf-0bca-be0b-aba8-341616ad2d63,738841958,1486099851940458369 +ca1b2b67-d63f-471a-9035-926e104d7f7b,4027021584,7425252084363772208 +c016c7a2-6325-593f-a248-9366503a70f2,1889362205,12244490078637944537 +7bb64328-d227-c1a3-9768-d08df70f0be4,1058400807,15742601716963836627 +6c2bd690-8d50-54f5-6aeb-428917c2aefc,2636049162,18439893653971696281 +06c45ba4-67e1-0ed5-0595-791b18279fde,933741894,9457973365751210017 +e2d00e2e-4a1e-0f4c-6b5d-048d93b062ee,472938716,2564252054714309440 +30b7fb71-19ec-454c-7d6b-805a9fbc16c3,1708667176,17528335108417047455 +0bf389ea-9985-7010-9e6c-7d98089bc94d,252673321,615424884354843975 +196f6e33-7162-6113-c5ea-9872d1219f93,770708953,13846392994622500743 +21ffc9e2-d903-fce1-a863-e2eae0d3775c,3093326244,5579929906717137638 +f1f7359b-15cd-1f4b-95ce-12452fa2d27a,375840143,4755303172402994922 +c6883654-0bd3-f250-4224-6b9fc0de8bbc,3870847027,13842359387041756714 +f711cc39-73c5-2915-60f5-ffda48572c63,1297131983,5413576743108418592 +75585cf0-5529-0eab-95a5-59f46a898e9c,2790494775,16742151740087170504 +a20dc470-777e-be35-7108-014c0b779800,3741300996,15723284745150877280 +1d945207-fadb-e630-956d-41f079b86e9e,4071617874,5329755014149525492 +8c783aaf-336a-9034-0a20-b36dcfd87ef3,4139538104,9747287738986699761 +24c13429-2dce-f062-5a23-c3f97e684a95,3576289414,993310855788411418 +cf5fc72b-bdbd-f791-5e27-e62e79ad2e31,636868136,9982674194143109027 +c9e0d559-84cd-d377-7095-5bfdf7acac2c,4084049483,842585440731674585 +96c6659d-f2c2-bc4f-1975-7a96043b264b,3819187656,15817708173869191943 +f043cfa2-9ee6-0fcf-d21f-a703b0b49419,1701103699,6648674761204277693 +7b51af6a-d113-081a-185c-46b8e906f950,2266321473,2448117403494498856 +80db6476-a357-7b62-8191-127b5d0a35ff,651999881,12065482993704083385 +7fadaa39-943a-b8d0-8827-c383d1e41a6d,4172002985,14322181466456878945 +14370aea-1d1a-8c6a-f973-64645e6af06a,463486812,12182639765577960118 +3b1e02ca-6f4e-c259-2480-419341847d77,3320292813,15704602139585948851 +6822b43c-1303-f5ab-3d3a-26f475eee768,1276062185,8159711004817238994 +0080591b-9a36-e4be-a5c4-b672d4acafb7,1984735495,9454838365639927813 +4e76e25e-e456-5982-2a28-9aae022497a8,2815974096,9715401631568169086 +69107627-ac31-ff1e-6802-3381349f37eb,3381111064,4907348930521345207 +614237b1-03af-e0d3-ea9b-9f9b0124fc8e,2916885699,15569059264950236771 +044ccfff-7ed0-1593-8c03-013004e75915,785571226,14010114435070049721 +9a89abaa-ed9d-88b2-18ee-737e3b31c22b,1469610683,5850027495884508011 +f3204a19-9ce9-daa3-11dc-5ac42647b518,2360335029,10264333636407996487 +a1be69ce-ad78-beec-d98d-2d6cdeb7afbe,991690080,7986167140094496768 +8d1ea0c9-652d-7be9-a4ae-855ea0901944,2126935052,629272982724036533 +14068c92-03a2-01a5-1ff5-0b1ca3889845,2193725078,126009354263885536 +8314d3d5-ddba-1545-b894-e9d43088f224,2334134,9042891819209151766 +b8f54681-b6ba-9e20-8cca-340679d94b64,2566604428,8853613598203450565 +a5d0e175-98f5-9aeb-6f48-359ac19d01fc,2475544605,13612769365419403789 +395efa74-1cd4-2427-9529-fbfa9cb23b66,4292508656,10201912669050181692 +b22ab636-fc9c-7ba0-8845-b269786c1fce,232461120,10220095186715790753 +365a711e-a4ca-2434-7dca-184fbe2369d7,1048478457,9971996529711137806 +6bf966d4-2b59-25d1-0599-76e313c75de9,1739998644,16636957318038088782 +617249b0-b064-4e0f-14b1-8f12a4fc59ab,2700433787,335530846059132651 +e7d3ee80-bc27-523c-e6f0-fe384de94bf3,1829412233,3551650817535438747 +2c285e0d-ed3c-686a-310e-5d9ae8239b32,880974,8509688931257151967 +39defcf7-a73e-2c1f-cb4d-3c435de724cf,3125869857,10216008095289768839 +595ffb52-84a1-e6f2-d0b9-a2e811d0323e,2934993522,18013510281129621393 +5595d308-fc6c-2952-3b21-7bbeb407b19e,1038380832,6717966821827441726 +85c6c9ab-ad6e-1f11-d253-f62ef2959bcb,4047330296,12545928142420340283 +caba4a6e-c080-d83b-1c21-de2056c07adc,3377614995,14069469015421488310 +acd3568b-061c-950c-a2f6-dec22f752743,1406997137,15863128614460253878 +fb3b2aa7-afb6-33e4-782e-3c475a6b2be1,1399337108,7095186875253019371 +a014b3ee-86fe-2cbf-4913-536c815683c7,2200857928,9349093538758023643 +ada013e3-27f5-a5d9-34e2-c8bf4347f9f6,4151694086,2492706430505939534 +7f516a7d-4ed5-2143-4962-ce61b2c2cc1b,878670581,6141828318815426102 +3e5326ae-dca0-4335-d63a-15a2cbb1719f,3839287036,5005965360802615010 +a7e9017a-5c1b-e201-f93c-15c6852728b6,1105108985,4243146656378005509 +3c8ff244-d066-eb0e-c293-9eddcef713bb,891406796,4329257715161424622 +209b2b1f-4baa-ab70-a27a-f36eb179b2aa,3552376828,3262698835911147365 +14d80660-27f2-d86f-78b3-c297420fce70,1569371653,16760862595663606627 +348d57ff-f7fb-86f0-f566-08d48ddfd31a,3186255788,17015585907449719817 +ddcc7b91-c167-5259-1059-43b505b60dc2,3210102609,14792287226365902989 +b98e7e13-87c8-788a-c89d-235d3d4e8231,373694529,18120240371665619446 +876149d8-3a15-2d7c-6ff2-2792978f09d2,3548675550,18184006412186500180 +f9bd9a52-d8b6-b1a8-cf9a-b5133c66540b,4106592053,3343412138853842926 +a489d90c-4ba8-808f-e56b-32d622273307,3277582381,12379815772571092429 +10374211-f43e-0e9e-840e-07d38cc94381,3094262208,8303251583916038001 +b5a41c6c-4cd5-dc84-c23c-1fc836d9ffe9,2810915269,6769984576435654991 +a8ba46c7-44e8-367e-f774-728519918cc0,3635886840,12751868982062489646 +fbdb1b4b-6a8c-4b6b-5af5-a1d5740678eb,3905260164,15872787286282777100 +b8a473fd-20ed-d567-7631-24934feca6ee,3320390884,3330674374834724687 +5ec2e1d2-4bae-3856-78fa-4959fe6132c6,1729274572,13891194348088750832 +793da42f-17ab-ec6a-b05d-21b61491b928,1115766243,987730652366770939 +f476dbcd-6bd2-6b7f-785d-bd9dc426e97a,1314790471,7298299925007835965 +e7451d22-f500-31d0-bfe3-e959c2bd111e,3978328463,12848987777254899857 +fa2e1148-9a50-1ad7-7ae4-3cf2a9bcc1c0,3466198488,16518370341181139107 +74bb4f56-336d-8795-22b9-d7f5d54ac880,3768997639,4103115817127505030 +e65b1c04-3259-88bb-ffc7-71176cbcd6da,1858089179,3157877795226076575 +2d142b3f-2470-ef45-5ca8-a983cf9cc14e,2335220351,10699700520544641160 +85118c55-d897-9df0-d705-161f214b2267,4126672204,12103803096954905851 +3e749bdc-34a0-901e-1555-7ee7ce9fba89,3961252566,16247276214707640926 +76f4658c-bf8b-673a-b9ce-56e0d6d10895,482234233,8220442989470862527 +b96c47ed-2cdc-352b-d493-cefb88a44d30,3758847578,16118446647972406760 +90fe8e3e-06ad-788b-4df1-47dad41697f2,1763468790,17079996962672367164 +601b47cf-ba35-b5ec-88a7-215b7ee14a93,1278436280,15060273520144697558 +d6fafdbb-6063-c068-2ca9-8a8189b1c4d8,1974195847,6120346604011854919 +d5241ef6-6102-b4e1-0d6b-a14974484009,3659032147,14515976548943068344 +3cf55a31-788a-4728-4ec5-707796c4da2d,1936744076,18079430317039268056 +56ea5acd-9f2f-8c7b-4c44-8c42fb0e4001,499611274,17385657600823680441 +b0d9a9a2-80b7-f169-bebb-8b81231850e4,2998867058,10118168048896507541 +7eea90b8-1331-bfa3-1934-65b7834041c9,1299292874,14447611595277188504 +07728fe4-07a1-12e2-5023-08faa384dde2,2144200252,15350269030537814981 +b603b54e-2b21-afd4-2c55-43bb52cb5109,940683333,17449570312818517696 +f7116901-8e4a-33bd-64d8-489beea38d77,2700691956,3372688301104017843 +c690ed4e-db91-e2d5-258b-34ef88cd3ef8,3388377037,15027212647658382773 +988ef68e-1f53-4e9f-6f3e-8c5a5d8a370c,2217667961,6770447100450212039 +17e8199b-e348-5ffe-6556-0cb92601503c,28596841,1870006961411727820 +fb7d6d15-f156-c0a8-bd19-1163d0fa6992,931330117,7043939858994242101 +a5c0d568-90ef-345a-c288-33d869289312,817666752,14808040926764325968 +98fe559b-705b-dfa1-4f2e-56637d6d3ba6,3970529875,1715824210714730230 +e2e5ce19-07d6-5227-1f81-39af25ca16fe,867929685,14715786986142247244 +80af3b36-f6c5-159f-9c5d-44991645b2fd,4045215010,16480073200930346277 +f7c440aa-3d18-e4be-5725-ae9616c31591,249488826,5768010562868445838 +5c8b4851-7121-f360-9ae1-1848641622a2,2428198386,12280765021781666548 +d271f76d-4cd2-872b-6d80-88429b60ed29,3202506778,4187901301328674792 +c950e8ec-41ff-02de-5b67-df199f37630e,2615048735,15181156579559740236 +227ff481-ec4e-52ca-a409-98308bd5c2e5,1820414882,410090942649767883 +b33a60ef-48a8-c776-1869-6ac1794435fb,2034219663,11869243037976111356 +a0e2b142-4941-ce41-a179-87830446ee12,2666999589,4750868503658157558 +d50a1dc1-abfa-03f5-6812-16db0af284c4,3692243512,11855589696440993438 +2507c6bd-77d9-2460-a55e-6b05d11dad69,3376418354,17573794480351706509 +9be13da6-143e-e4db-952a-fceb4e8d6a77,2736003450,16026210933360912674 +09d60c98-83db-2170-00e2-668bb640f534,923076746,10789467866923175879 +85d1e882-16e4-05ff-6cad-788238fd14ca,462148659,12106134642559129080 +ff92ef0c-139d-f5a4-8c12-28b0e52b2e48,2879114165,10279517488304018464 +c6d02b94-3efd-452b-a0ae-0a0fec16b869,1401421537,10980300573058539465 +3b426c7d-d1c4-8d16-9209-4c4f33a4ceba,403443415,9181380260439684750 +12b7c11f-5b44-65fe-8fdb-458b118708fc,918202423,1764600184576359082 +beb886cc-8f07-c854-e382-db8e9629c8b8,328513993,7192893763586643611 +8097cd28-8e49-b2a5-8893-0a2e1a601f27,660038238,7859023191735182735 +703e6e30-334c-01d8-080e-c244fe67653c,1698274406,4936370077472915993 +f2a5d606-139c-21db-8b86-17650d6728bb,3682955234,9144007866246471937 +be001730-1ade-8cbe-dab4-39ab54983ea1,1665702936,11767442386745305050 +3c4de722-79b2-6556-bdca-7f76279cc7eb,628780731,16103238694043484205 +08835f48-240a-6a5d-a897-780bd895eb62,2601204975,11592286269088812614 +fb3fa9f0-da3b-b0c4-8ed3-2e7d9f940291,4077118238,9171791095214677756 +867c4a87-f146-07bf-c64c-823c24dd3079,3094456126,4971549481928415588 +516627fc-4e5e-fcba-7dc1-834d1d727035,1441460550,4370197136600931237 +e457d6b4-df5f-6dba-8ae1-f6ace8a9128a,2295299468,271234660468641933 diff --git a/src/utils/murmur3/__tests__/murmur3.spec.ts b/src/utils/murmur3/__tests__/murmur3.spec.ts index 1200cb9e..0f3c8eb6 100644 --- a/src/utils/murmur3/__tests__/murmur3.spec.ts +++ b/src/utils/murmur3/__tests__/murmur3.spec.ts @@ -1,7 +1,8 @@ import fs from 'fs'; import rl from 'readline'; import * as murmur3 from '../murmur3'; -import { hash128 } from '../murmur3_128'; +import { hash128 as hash128x64 } from '../murmur3_128'; +import { hash128 as hash128x86 } from '../murmur3_128_x86'; import { hash64 } from '../murmur3_64'; [ @@ -57,8 +58,9 @@ function dec2hex(str: any) { } [ - 'murmur3_64_uuids.csv', -].forEach(filename => { + { filename: 'murmur3_86_uuids.csv', hash128: hash128x86 }, + { filename: 'murmur3_64_uuids.csv', hash128: hash128x64 } +].forEach(({ filename, hash128 }) => { test('MURMUR3 128 / validate hashing behavior using sample data', (done) => { const parser = rl.createInterface({ diff --git a/src/utils/murmur3/murmur3_128.ts b/src/utils/murmur3/murmur3_128.ts index 1a719c82..a9929324 100644 --- a/src/utils/murmur3/murmur3_128.ts +++ b/src/utils/murmur3/murmur3_128.ts @@ -265,7 +265,7 @@ function hash128x64(key?: string, seed?: number) { /** * x64 version of Murmur3 for 128bits. - * Used in hashImpression128. + * * @param {string} str */ export function hash128(str: string, seed?: number): string { diff --git a/src/utils/murmur3/murmur3_128_x86.ts b/src/utils/murmur3/murmur3_128_x86.ts index cda04228..a05ad3dc 100644 --- a/src/utils/murmur3/murmur3_128_x86.ts +++ b/src/utils/murmur3/murmur3_128_x86.ts @@ -179,10 +179,9 @@ function hash128x86(key?: string, seed?: number): string { /** * x86 version of Murmur3 for 128bits. - * @param {string} str + * Used by hashImpression128 because in JS it is more efficient than the x64 version, no matter the underlying OS/CPU arch. * - * @TODO Test and use in hashImpression128 instead of the x64 version, - * because it is more time efficient in JS, no matter the underlying CPU arch. + * @param {string} str */ export function hash128(str: string, seed?: number): string { return hash128x86(UTF16ToUTF8(str), (seed as number) >>> 0); From 43be8e20a09bc6e0ecebe6e34682944e75898d29 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Fri, 1 Oct 2021 15:53:54 -0300 Subject: [PATCH 124/144] renamed a few vars for consistency with js sdk --- src/evaluator/matchers/matcherTypes.ts | 4 ++-- src/evaluator/matchersTransform/index.ts | 24 ++++++++++++------------ src/evaluator/value/sanitize.ts | 14 +++++++------- 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/evaluator/matchers/matcherTypes.ts b/src/evaluator/matchers/matcherTypes.ts index 76ed1192..786db547 100644 --- a/src/evaluator/matchers/matcherTypes.ts +++ b/src/evaluator/matchers/matcherTypes.ts @@ -19,7 +19,7 @@ export const matcherTypes: Record = { MATCHES_STRING: 17 }; -export const dataTypes = { +export const matcherDataTypes = { BOOLEAN: 'BOOLEAN', STRING: 'STRING', NUMBER: 'NUMBER', @@ -28,7 +28,7 @@ export const dataTypes = { NOT_SPECIFIED: 'NOT_SPECIFIED' }; -export function mapper(matcherType: string) { +export function matcherTypesMapper(matcherType: string) { const type = matcherTypes[matcherType]; if (type) return type; else return matcherTypes.UNDEFINED; diff --git a/src/evaluator/matchersTransform/index.ts b/src/evaluator/matchersTransform/index.ts index 7a4f6241..abb1c8b0 100644 --- a/src/evaluator/matchersTransform/index.ts +++ b/src/evaluator/matchersTransform/index.ts @@ -1,5 +1,5 @@ import { findIndex } from '../../utils/lang'; -import { matcherTypes, mapper, dataTypes } from '../matchers/matcherTypes'; +import { matcherTypes, matcherTypesMapper, matcherDataTypes } from '../matchers/matcherTypes'; import segmentTransform from './segment'; import whitelistTransform from './whitelist'; import setTransform from './set'; @@ -28,9 +28,9 @@ export default function matchersTransform(matchers: ISplitMatcher[]): IMatcherDt } = matcher; let attribute = keySelector && keySelector.attribute; - let type = mapper(matcherType); + let type = matcherTypesMapper(matcherType); // As default input data type we use string (even for ALL_KEYS) - let dataType = dataTypes.STRING; + let dataType = matcherDataTypes.STRING; let value = undefined; if (type === matcherTypes.IN_SEGMENT) { @@ -39,29 +39,29 @@ export default function matchersTransform(matchers: ISplitMatcher[]): IMatcherDt value = whitelistTransform(whitelistObject as IWhitelistMatcherData); } else if (type === matcherTypes.EQUAL_TO) { value = numericTransform(unaryNumericObject as IUnaryNumericMatcherData); - dataType = dataTypes.NUMBER; + dataType = matcherDataTypes.NUMBER; if ((unaryNumericObject as IUnaryNumericMatcherData).dataType === 'DATETIME') { value = zeroSinceHH(value); - dataType = dataTypes.DATETIME; + dataType = matcherDataTypes.DATETIME; } } else if (type === matcherTypes.GREATER_THAN_OR_EQUAL_TO || type === matcherTypes.LESS_THAN_OR_EQUAL_TO) { value = numericTransform(unaryNumericObject as IUnaryNumericMatcherData); - dataType = dataTypes.NUMBER; + dataType = matcherDataTypes.NUMBER; if ((unaryNumericObject as IUnaryNumericMatcherData).dataType === 'DATETIME') { value = zeroSinceSS(value); - dataType = dataTypes.DATETIME; + dataType = matcherDataTypes.DATETIME; } } else if (type === matcherTypes.BETWEEN) { value = betweenObject as IBetweenMatcherData; - dataType = dataTypes.NUMBER; + dataType = matcherDataTypes.NUMBER; if (value.dataType === 'DATETIME') { value.start = zeroSinceSS(value.start); value.end = zeroSinceSS(value.end); - dataType = dataTypes.DATETIME; + dataType = matcherDataTypes.DATETIME; } } else if ( type === matcherTypes.EQUAL_TO_SET || @@ -70,7 +70,7 @@ export default function matchersTransform(matchers: ISplitMatcher[]): IMatcherDt type === matcherTypes.PART_OF_SET ) { value = setTransform(whitelistObject as IWhitelistMatcherData); - dataType = dataTypes.SET; + dataType = matcherDataTypes.SET; } else if ( type === matcherTypes.STARTS_WITH || type === matcherTypes.ENDS_WITH || @@ -79,9 +79,9 @@ export default function matchersTransform(matchers: ISplitMatcher[]): IMatcherDt value = setTransform(whitelistObject as IWhitelistMatcherData); } else if (type === matcherTypes.IN_SPLIT_TREATMENT) { value = dependencyObject; - dataType = dataTypes.NOT_SPECIFIED; + dataType = matcherDataTypes.NOT_SPECIFIED; } else if (type === matcherTypes.EQUAL_TO_BOOLEAN) { - dataType = dataTypes.BOOLEAN; + dataType = matcherDataTypes.BOOLEAN; value = booleanMatcherData; } else if (type === matcherTypes.MATCHES_STRING) { value = stringMatcherData; diff --git a/src/evaluator/value/sanitize.ts b/src/evaluator/value/sanitize.ts index 991ed8e1..7ca7b5cd 100644 --- a/src/evaluator/value/sanitize.ts +++ b/src/evaluator/value/sanitize.ts @@ -3,7 +3,7 @@ import { IDependencyMatcherValue } from '../types'; import { ILogger } from '../../logger/types'; import { isObject, uniq, toString, toNumber } from '../../utils/lang'; import { zeroSinceHH, zeroSinceSS } from '../convertions'; -import { matcherTypes, dataTypes } from '../matchers/matcherTypes'; +import { matcherTypes, matcherDataTypes } from '../matchers/matcherTypes'; import { ENGINE_SANITIZE } from '../../logger/constants'; function sanitizeNumber(val: any): number | undefined { @@ -74,20 +74,20 @@ export default function sanitize(log: ILogger, matcherTypeID: number, value: str let sanitizedValue: string | number | boolean | Array | IDependencyMatcherValue | undefined; switch (dataType) { - case dataTypes.NUMBER: - case dataTypes.DATETIME: + case matcherDataTypes.NUMBER: + case matcherDataTypes.DATETIME: sanitizedValue = sanitizeNumber(value); break; - case dataTypes.STRING: + case matcherDataTypes.STRING: sanitizedValue = sanitizeString(value); break; - case dataTypes.SET: + case matcherDataTypes.SET: sanitizedValue = sanitizeArray(value); break; - case dataTypes.BOOLEAN: + case matcherDataTypes.BOOLEAN: sanitizedValue = sanitizeBoolean(value); break; - case dataTypes.NOT_SPECIFIED: + case matcherDataTypes.NOT_SPECIFIED: sanitizedValue = value; break; default: From fb47f20d4fb1286de7a3dfce7f4ddd69945fe5a9 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 6 Oct 2021 15:28:42 -0300 Subject: [PATCH 125/144] comment polishing --- .eslintrc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.eslintrc b/.eslintrc index 9a4d4f70..94782cad 100644 --- a/.eslintrc +++ b/.eslintrc @@ -73,11 +73,11 @@ }, { "selector": "ForInStatement", - "message": "Don't use for-in syntax in libraries, because it might iterate over properties polluted by external code" + "message": "Don't use for-in syntax in libraries, because it iterates over members inherited from the prototype chain" }, { "selector": "TSEnumDeclaration[const=true]", - "message": "Don't declare const enums, because they are not supported by Babel wich is used for building RN SDK" + "message": "Don't declare const enum, because it is not supported by Babel used for building RN SDK" } ], "compat/compat": ["error", "defaults, not ie < 10, not node < 6"], From 3a92d9682a854027a03500400c05d8d16a0ebfd7 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 6 Oct 2021 16:19:49 -0300 Subject: [PATCH 126/144] update rc version --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8e9ed3bb..c7c96541 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "0.1.1-canary.17", + "version": "0.1.1-canary.18", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 6e44d0cb..c25e6cb1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "0.1.1-canary.17", + "version": "0.1.1-canary.18", "description": "Split Javascript SDK common components", "main": "cjs/index.js", "module": "esm/index.js", From 8056192878b1bb1b7427add8267b4212c33557a4 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 7 Oct 2021 00:54:29 -0300 Subject: [PATCH 127/144] localhost mode with localstorage --- src/sdkFactory/types.ts | 3 +- src/services/splitApi.ts | 3 +- src/storages/inLocalStorage/index.ts | 12 +++++--- src/storages/inMemory/InMemoryStorage.ts | 3 ++ src/storages/inMemory/InMemoryStorageCS.ts | 3 ++ src/storages/inRedis/index.ts | 12 +++++--- src/storages/pluggable/index.ts | 12 +++++--- src/storages/types.ts | 12 ++++++++ .../offline/syncTasks/fromObjectSyncTask.ts | 1 - src/types.ts | 9 ++++-- src/utils/constants/index.ts | 7 ++++- src/utils/settingsValidation/index.ts | 5 ++-- .../storage/__tests__/storageCS.spec.ts | 22 ++++++++++++-- .../settingsValidation/storage/storageCS.ts | 29 ++++++++++++++----- src/utils/settingsValidation/types.ts | 11 ------- 15 files changed, 100 insertions(+), 44 deletions(-) diff --git a/src/sdkFactory/types.ts b/src/sdkFactory/types.ts index eb834930..ce5981ce 100644 --- a/src/sdkFactory/types.ts +++ b/src/sdkFactory/types.ts @@ -8,7 +8,6 @@ import { IStorageAsync, IStorageSync, ISplitsCacheSync, ISplitsCacheAsync, IStor import { ISyncManager, ISyncManagerFactoryParams } from '../sync/types'; import { IImpressionObserver } from '../trackers/impressionObserver/types'; import { SplitIO, ISettings, IEventEmitter } from '../types'; -import { ISettingsInternal } from '../utils/settingsValidation/types'; /** * Environment related dependencies. @@ -27,7 +26,7 @@ export interface IPlatform { export interface ISdkFactoryParams { // The settings must be already validated - settings: ISettingsInternal, + settings: ISettings, // Platform dependencies platform: IPlatform, diff --git a/src/services/splitApi.ts b/src/services/splitApi.ts index e2d6e3b6..a080be33 100644 --- a/src/services/splitApi.ts +++ b/src/services/splitApi.ts @@ -2,7 +2,6 @@ import { IPlatform } from '../sdkFactory/types'; import { ISettings } from '../types'; import { splitHttpClientFactory } from './splitHttpClient'; import { ISplitApi } from './types'; -import { ISettingsInternal } from '../utils/settingsValidation/types'; import objectAssign from 'object-assign'; const noCacheHeaderOptions = { headers: { 'Cache-Control': 'no-cache' } }; @@ -20,7 +19,7 @@ function userKeyToQueryParam(userKey: string) { export function splitApiFactory(settings: ISettings, platform: Pick): ISplitApi { const urls = settings.urls; - const filterQueryString = (settings as ISettingsInternal).sync.__splitFiltersValidation && (settings as ISettingsInternal).sync.__splitFiltersValidation.queryString; + const filterQueryString = settings.sync.__splitFiltersValidation && settings.sync.__splitFiltersValidation.queryString; const SplitSDKImpressionsMode = settings.sync.impressionsMode; const splitHttpClient = splitHttpClientFactory(settings, platform.getFetch, platform.getOptions); diff --git a/src/storages/inLocalStorage/index.ts b/src/storages/inLocalStorage/index.ts index 4cefbf38..6587208b 100644 --- a/src/storages/inLocalStorage/index.ts +++ b/src/storages/inLocalStorage/index.ts @@ -1,7 +1,7 @@ import ImpressionsCacheInMemory from '../inMemory/ImpressionsCacheInMemory'; import ImpressionCountsCacheInMemory from '../inMemory/ImpressionCountsCacheInMemory'; import EventsCacheInMemory from '../inMemory/EventsCacheInMemory'; -import { IStorageFactoryParams, IStorageSyncCS } from '../types'; +import { IStorageFactoryParams, IStorageSyncCS, IStorageSyncFactory } from '../types'; import { validatePrefix } from '../KeyBuilder'; import KeyBuilderCS from '../KeyBuilderCS'; import { isLocalStorageAvailable } from '../../utils/env/isLocalStorageAvailable'; @@ -12,6 +12,7 @@ import SplitsCacheInMemory from '../inMemory/SplitsCacheInMemory'; import { DEFAULT_CACHE_EXPIRATION_IN_MILLIS } from '../../utils/constants/browser'; import { InMemoryStorageCSFactory } from '../inMemory/InMemoryStorageCS'; import { LOG_PREFIX } from './constants'; +import { STORAGE_LOCALSTORAGE } from '../../utils/constants'; export interface InLocalStorageOptions { prefix?: string @@ -20,11 +21,11 @@ export interface InLocalStorageOptions { /** * InLocal storage factory for standalone client-side SplitFactory */ -export function InLocalStorage(options: InLocalStorageOptions = {}) { +export function InLocalStorage(options: InLocalStorageOptions = {}): IStorageSyncFactory { const prefix = validatePrefix(options.prefix); - return function InLocalStorageCSFactory(params: IStorageFactoryParams): IStorageSyncCS { + function InLocalStorageCSFactory(params: IStorageFactoryParams): IStorageSyncCS { // Fallback to InMemoryStorage if LocalStorage API is not available if (!isLocalStorageAvailable()) { @@ -69,5 +70,8 @@ export function InLocalStorage(options: InLocalStorageOptions = {}) { }; }, }; - }; + } + + InLocalStorageCSFactory.type = STORAGE_LOCALSTORAGE; + return InLocalStorageCSFactory; } diff --git a/src/storages/inMemory/InMemoryStorage.ts b/src/storages/inMemory/InMemoryStorage.ts index a4f29d8e..349ac6e1 100644 --- a/src/storages/inMemory/InMemoryStorage.ts +++ b/src/storages/inMemory/InMemoryStorage.ts @@ -4,6 +4,7 @@ import ImpressionsCacheInMemory from './ImpressionsCacheInMemory'; import EventsCacheInMemory from './EventsCacheInMemory'; import { IStorageFactoryParams, IStorageSync } from '../types'; import ImpressionCountsCacheInMemory from './ImpressionCountsCacheInMemory'; +import { STORAGE_MEMORY } from '../../utils/constants'; /** * InMemory storage factory for standalone server-side SplitFactory @@ -32,3 +33,5 @@ export function InMemoryStorageFactory(params: IStorageFactoryParams): IStorageS } }; } + +InMemoryStorageFactory.type = STORAGE_MEMORY; diff --git a/src/storages/inMemory/InMemoryStorageCS.ts b/src/storages/inMemory/InMemoryStorageCS.ts index 1ef25283..d59e4c04 100644 --- a/src/storages/inMemory/InMemoryStorageCS.ts +++ b/src/storages/inMemory/InMemoryStorageCS.ts @@ -4,6 +4,7 @@ import ImpressionsCacheInMemory from './ImpressionsCacheInMemory'; import EventsCacheInMemory from './EventsCacheInMemory'; import { IStorageSyncCS, IStorageFactoryParams } from '../types'; import ImpressionCountsCacheInMemory from './ImpressionCountsCacheInMemory'; +import { STORAGE_MEMORY } from '../../utils/constants'; /** * InMemory storage factory for standalone client-side SplitFactory @@ -46,3 +47,5 @@ export function InMemoryStorageCSFactory(params: IStorageFactoryParams): IStorag }, }; } + +InMemoryStorageCSFactory.type = STORAGE_MEMORY; diff --git a/src/storages/inRedis/index.ts b/src/storages/inRedis/index.ts index e12e537c..b13413ee 100644 --- a/src/storages/inRedis/index.ts +++ b/src/storages/inRedis/index.ts @@ -1,5 +1,5 @@ import RedisAdapter from './RedisAdapter'; -import { IStorageAsync, IStorageFactoryParams } from '../types'; +import { IStorageAsync, IStorageAsyncFactory, IStorageFactoryParams } from '../types'; import { validatePrefix } from '../KeyBuilder'; import KeyBuilderSS from '../KeyBuilderSS'; import SplitsCacheInRedis from './SplitsCacheInRedis'; @@ -8,6 +8,7 @@ import ImpressionsCacheInRedis from './ImpressionsCacheInRedis'; import EventsCacheInRedis from './EventsCacheInRedis'; import LatenciesCacheInRedis from './LatenciesCacheInRedis'; import CountsCacheInRedis from './CountsCacheInRedis'; +import { STORAGE_REDIS } from '../../utils/constants'; export interface InRedisStorageOptions { prefix?: string @@ -18,11 +19,11 @@ export interface InRedisStorageOptions { * InRedis storage factory for consumer server-side SplitFactory, that uses `Ioredis` Redis client for Node. * @see {@link https://www.npmjs.com/package/ioredis} */ -export function InRedisStorage(options: InRedisStorageOptions = {}) { +export function InRedisStorage(options: InRedisStorageOptions = {}): IStorageAsyncFactory { const prefix = validatePrefix(options.prefix); - return function InRedisStorageFactory({ log, metadata, onReadyCb }: IStorageFactoryParams): IStorageAsync { + function InRedisStorageFactory({ log, metadata, onReadyCb }: IStorageFactoryParams): IStorageAsync { const keys = new KeyBuilderSS(prefix, metadata); const redisClient = new RedisAdapter(log, options.options || {}); @@ -47,5 +48,8 @@ export function InRedisStorage(options: InRedisStorageOptions = {}) { // @TODO check that caches works as expected when redisClient is disconnected } }; - }; + } + + InRedisStorageFactory.type = STORAGE_REDIS; + return InRedisStorageFactory; } diff --git a/src/storages/pluggable/index.ts b/src/storages/pluggable/index.ts index 6689f0f5..2afb1bf4 100644 --- a/src/storages/pluggable/index.ts +++ b/src/storages/pluggable/index.ts @@ -1,4 +1,4 @@ -import { ICustomStorageWrapper, IStorageAsync, IStorageFactoryParams } from '../types'; +import { ICustomStorageWrapper, IStorageAsync, IStorageAsyncFactory, IStorageFactoryParams } from '../types'; import KeyBuilderSS from '../KeyBuilderSS'; import { SplitsCachePluggable } from './SplitsCachePluggable'; @@ -8,6 +8,7 @@ import { EventsCachePluggable } from './EventsCachePluggable'; import { wrapperAdapter, METHODS_TO_PROMISE_WRAP } from './wrapperAdapter'; import { isObject } from '../../utils/lang'; import { validatePrefix } from '../KeyBuilder'; +import { STORAGE_CUSTOM } from '../../utils/constants'; const NO_VALID_WRAPPER = 'Expecting custom storage `wrapper` in options, but no valid wrapper instance was provided.'; const NO_VALID_WRAPPER_INTERFACE = 'The provided wrapper instance doesn’t follow the expected interface. Check our docs.'; @@ -34,13 +35,13 @@ function validatePluggableStorageOptions(options: any) { /** * Pluggable storage factory for consumer server-side & client-side SplitFactory. */ -export function PluggableStorage(options: PluggableStorageOptions) { +export function PluggableStorage(options: PluggableStorageOptions): IStorageAsyncFactory { validatePluggableStorageOptions(options); const prefix = validatePrefix(options.prefix); - return function PluggableStorageFactory({ log, metadata, onReadyCb }: IStorageFactoryParams): IStorageAsync { + function PluggableStorageFactory({ log, metadata, onReadyCb }: IStorageFactoryParams): IStorageAsync { const keys = new KeyBuilderSS(prefix, metadata); const wrapper = wrapperAdapter(log, options.wrapper); @@ -63,5 +64,8 @@ export function PluggableStorage(options: PluggableStorageOptions) { return wrapper.close(); } }; - }; + } + + PluggableStorageFactory.type = STORAGE_CUSTOM; + return PluggableStorageFactory; } diff --git a/src/storages/types.ts b/src/storages/types.ts index 5a1a8113..288e1fea 100644 --- a/src/storages/types.ts +++ b/src/storages/types.ts @@ -439,3 +439,15 @@ export interface IStorageFactoryParams { onReadyCb?: (error?: any) => void, metadata: IMetadata, } + +export type StorageType = 'MEMORY' | 'LOCALSTORAGE' | 'REDIS' | 'CUSTOM'; + +export type IStorageSyncFactory = { + type: StorageType, + (params: IStorageFactoryParams): IStorageSync +} + +export type IStorageAsyncFactory = { + type: StorageType, + (params: IStorageFactoryParams): IStorageAsync +} diff --git a/src/sync/offline/syncTasks/fromObjectSyncTask.ts b/src/sync/offline/syncTasks/fromObjectSyncTask.ts index 0f1b3597..2eccc27c 100644 --- a/src/sync/offline/syncTasks/fromObjectSyncTask.ts +++ b/src/sync/offline/syncTasks/fromObjectSyncTask.ts @@ -55,7 +55,6 @@ export function fromObjectUpdaterFactory( return Promise.all([ splitsCache.clear(), // required to sync removed splits from mock - splitsCache.setChangeNumber(Date.now()), splitsCache.addSplits(splits) ]).then(() => { readiness.splits.emit(SDK_SPLITS_ARRIVED); diff --git a/src/types.ts b/src/types.ts index 02def5dc..9d20e84f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,8 +1,9 @@ +import { ISplitFiltersValidation } from './dtos/types'; import { IIntegration, IIntegrationFactoryParams } from './integrations/types'; import { ILogger } from './logger/types'; /* eslint-disable no-use-before-define */ -import { IStorageFactoryParams, IStorageSyncCS, IStorageSync, IStorageAsync } from './storages/types'; +import { IStorageFactoryParams, IStorageSyncCS, IStorageSync, IStorageAsync, IStorageSyncFactory } from './storages/types'; /** * EventEmitter interface with the minimal methods used by the SDK @@ -55,6 +56,8 @@ export type SDKMode = 'standalone' | 'consumer' | 'localhost'; * Settings interface. This is a representation of the settings the SDK expose, that's why * most of it's props are readonly. Only features should be rewritten when localhost mode is active. * @interface ISettings + * + * NOTE: same ISettings interface from public type declarations extended with private properties. */ export interface ISettings { readonly core: { @@ -81,7 +84,7 @@ export interface ISettings { retriesOnFailureBeforeReady: number, eventsFirstPushWindow: number }, - readonly storage: (params: IStorageFactoryParams) => IStorageSyncCS, + readonly storage: IStorageSyncFactory, readonly integrations?: Array<(params: IIntegrationFactoryParams) => IIntegration | void>, readonly urls: { events: string, @@ -96,12 +99,14 @@ export interface ISettings { readonly sync: { splitFilters: SplitIO.SplitFilter[], impressionsMode: SplitIO.ImpressionsMode, + __splitFiltersValidation: ISplitFiltersValidation }, readonly runtime: { ip: string | false hostname: string | false }, readonly log: ILogger + readonly impressionListener?: unknown } /** * Log levels. diff --git a/src/utils/constants/index.ts b/src/utils/constants/index.ts index cfcd245b..51bcafca 100644 --- a/src/utils/constants/index.ts +++ b/src/utils/constants/index.ts @@ -1,3 +1,5 @@ +import { StorageType } from '../../storages/types'; + // Special treatments export const CONTROL = 'control'; export const CONTROL_WITH_CONFIG = { @@ -24,4 +26,7 @@ export const PRODUCER_MODE = 'producer'; export const CONSUMER_MODE = 'consumer'; // Storage types -export const STORAGE_MEMORY = 'MEMORY'; +export const STORAGE_MEMORY: StorageType = 'MEMORY'; +export const STORAGE_LOCALSTORAGE: StorageType = 'LOCALSTORAGE'; +export const STORAGE_REDIS: StorageType = 'REDIS'; +export const STORAGE_CUSTOM: StorageType = 'CUSTOM'; diff --git a/src/utils/settingsValidation/index.ts b/src/utils/settingsValidation/index.ts index 0d6b1882..96f22227 100644 --- a/src/utils/settingsValidation/index.ts +++ b/src/utils/settingsValidation/index.ts @@ -3,7 +3,8 @@ import mode from './mode'; import { validateSplitFilters } from './splitFilters'; import { STANDALONE_MODE, OPTIMIZED, LOCALHOST_MODE } from '../constants'; import validImpressionsMode from './impressionsMode'; -import { ISettingsInternal, ISettingsValidationParams } from './types'; +import { ISettingsValidationParams } from './types'; +import { ISettings } from '../../types'; const base = { // Define which kind of object you want to retrieve from SplitFactory @@ -101,7 +102,7 @@ export function settingsValidation(config: unknown, validationParams: ISettingsV const { defaults, runtime, storage, integrations, logger } = validationParams; // creates a settings object merging base, defaults and config objects. - const withDefaults = merge({}, base, defaults, config) as ISettingsInternal; + const withDefaults = merge({}, base, defaults, config) as ISettings; // ensure a valid logger. // First thing to validate, since other validators might use the logger. diff --git a/src/utils/settingsValidation/storage/__tests__/storageCS.spec.ts b/src/utils/settingsValidation/storage/__tests__/storageCS.spec.ts index 8e1f1484..72456b1f 100644 --- a/src/utils/settingsValidation/storage/__tests__/storageCS.spec.ts +++ b/src/utils/settingsValidation/storage/__tests__/storageCS.spec.ts @@ -1,21 +1,37 @@ -import { validateStorageCS } from '../storageCS'; +import { validateStorageCS, __InLocalStorageMockFactory } from '../storageCS'; import { InMemoryStorageCSFactory } from '../../../../storages/inMemory/InMemoryStorageCS'; import { loggerMock as log } from '../../../../logger/__tests__/sdkLogger.mock'; +const mockInLocalStorageFactory = () => { }; +mockInLocalStorageFactory.type = 'LOCALSTORAGE'; describe('storage validator for pluggable storage (client-side)', () => { + afterEach(() => { + log.warn.mockClear(); + }); + // Check different types, since `storage` param is defined by the user test('fallbacks to default InMemory storage if the storage is invalid or not provided', () => { expect(validateStorageCS({ log })).toBe(InMemoryStorageCSFactory); expect(validateStorageCS({ log, storage: undefined })).toBe(InMemoryStorageCSFactory); + expect(log.warn).not.toBeCalled(); + expect(validateStorageCS({ log, storage: {} })).toBe(InMemoryStorageCSFactory); expect(validateStorageCS({ log, storage: 'invalid storage' })).toBe(InMemoryStorageCSFactory); + expect(validateStorageCS({ log, storage: () => { } })).toBe(InMemoryStorageCSFactory); + expect(validateStorageCS({ log, storage: () => { }, mode: 'localhost' })).toBe(InMemoryStorageCSFactory); + expect(log.warn).toBeCalledTimes(4); }); test('returns the provided storage factory if it is valid', () => { - const mockStorageFactory = () => { }; - expect(validateStorageCS({ log, storage: mockStorageFactory })).toBe(mockStorageFactory); + expect(validateStorageCS({ log, storage: mockInLocalStorageFactory })).toBe(mockInLocalStorageFactory); + expect(log.warn).not.toBeCalled(); + }); + + test('fallbacks to mock InLocalStorage storage if the storage is InLocalStorage and the mode localhost', () => { + expect(validateStorageCS({ log, storage: mockInLocalStorageFactory, mode: 'localhost' })).toBe(__InLocalStorageMockFactory); + expect(log.warn).not.toBeCalled(); }); }); diff --git a/src/utils/settingsValidation/storage/storageCS.ts b/src/utils/settingsValidation/storage/storageCS.ts index d8d1406c..9cb762e2 100644 --- a/src/utils/settingsValidation/storage/storageCS.ts +++ b/src/utils/settingsValidation/storage/storageCS.ts @@ -1,7 +1,16 @@ import { InMemoryStorageCSFactory } from '../../../storages/inMemory/InMemoryStorageCS'; -import { ISettings } from '../../../types'; +import { ISettings, SDKMode } from '../../../types'; import { ILogger } from '../../../logger/types'; import { WARN_STORAGE_INVALID } from '../../../logger/constants'; +import { LOCALHOST_MODE, STORAGE_LOCALSTORAGE, STORAGE_MEMORY } from '../../../utils/constants'; +import { IStorageFactoryParams, IStorageSyncCS } from '../../../storages/types'; + +export function __InLocalStorageMockFactory(params: IStorageFactoryParams): IStorageSyncCS { + const result = InMemoryStorageCSFactory(params); + result.splits.checkCache = () => true; // to emit SDK_READY_FROM_CACHE + return result; +} +__InLocalStorageMockFactory.type = STORAGE_MEMORY; /** * This function validates `settings.storage` object @@ -10,16 +19,20 @@ import { WARN_STORAGE_INVALID } from '../../../logger/constants'; * * @returns {Object} valid storage factory. It might be the default `InMemoryStorageCSFactory` if the provided storage is invalid. */ -export function validateStorageCS(settings: { log: ILogger, storage?: any }): ISettings['storage'] { - const { storage, log } = settings; +export function validateStorageCS(settings: { log: ILogger, storage?: any, mode?: SDKMode }): ISettings['storage'] { + let { storage = InMemoryStorageCSFactory, log, mode } = settings; - // validate storage - // @TODO validate its API (Splits cache, MySegments cache, etc) when supporting custom storages - if (storage) { - if (typeof storage === 'function') return storage; + // If an invalid storage is provided, fallback into MEMORY + if (typeof storage !== 'function' || storage.type !== STORAGE_MEMORY && storage.type !== STORAGE_LOCALSTORAGE) { + storage = InMemoryStorageCSFactory; log.warn(WARN_STORAGE_INVALID); } + // In localhost mode with InLocalStorage, fallback to a mock InLocalStorage to emit SDK_READY_FROM_CACHE + if (mode === LOCALHOST_MODE && storage.type === STORAGE_LOCALSTORAGE) { + return __InLocalStorageMockFactory; + } + // return default InMemory storage if provided one is not valid - return InMemoryStorageCSFactory; + return storage; } diff --git a/src/utils/settingsValidation/types.ts b/src/utils/settingsValidation/types.ts index 77759c88..a3ab3761 100644 --- a/src/utils/settingsValidation/types.ts +++ b/src/utils/settingsValidation/types.ts @@ -1,4 +1,3 @@ -import { ISplitFiltersValidation } from '../../dtos/types'; import { ISettings } from '../../types'; /** @@ -20,13 +19,3 @@ export interface ISettingsValidationParams { /** Logger validator */ logger: (settings: ISettings) => ISettings['log'], } - -/** - * Settings interface extended with private properties used for internal purposes. - */ -export interface ISettingsInternal extends ISettings { - readonly sync: ISettings['sync'] & { - __splitFiltersValidation: ISplitFiltersValidation - } - readonly impressionListener?: unknown -} From 45398e72be1b259479ffa4b33ab0ebb10ea1bd3f Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 7 Oct 2021 13:06:59 -0300 Subject: [PATCH 128/144] added localhost module validator, used in the slim entry point of JS Browser and RN --- src/logger/constants.ts | 1 + src/logger/messages/error.ts | 1 + .../splitsParser/splitsParserFromFile.ts | 9 +++++---- src/sync/syncManagerFromFile.ts | 13 +++++++++++++ src/sync/syncManagerFromObject.ts | 12 ++++++++++++ src/types.ts | 11 ++++++++++- src/utils/settingsValidation/index.ts | 7 +++++-- .../settingsValidation/localhost/index.ts | 19 +++++++++++++++++++ src/utils/settingsValidation/types.ts | 2 ++ 9 files changed, 68 insertions(+), 7 deletions(-) create mode 100644 src/sync/syncManagerFromFile.ts create mode 100644 src/sync/syncManagerFromObject.ts create mode 100644 src/utils/settingsValidation/localhost/index.ts diff --git a/src/logger/constants.ts b/src/logger/constants.ts index 79f629c2..5ca7317c 100644 --- a/src/logger/constants.ts +++ b/src/logger/constants.ts @@ -118,6 +118,7 @@ export const ERROR_EMPTY = 319; export const ERROR_EMPTY_ARRAY = 320; export const ERROR_INVALID_IMPRESSIONS_MODE = 321; export const ERROR_HTTP = 322; +export const ERROR_LOCALHOST_MODULE_REQUIRED = 323; // Log prefixes (a.k.a. tags or categories) export const LOG_PREFIX_SETTINGS = 'settings'; diff --git a/src/logger/messages/error.ts b/src/logger/messages/error.ts index 9a6de5c3..5d4d13dd 100644 --- a/src/logger/messages/error.ts +++ b/src/logger/messages/error.ts @@ -30,4 +30,5 @@ export const codesError: [number, string][] = [ [c.ERROR_EMPTY_ARRAY, '%s: %s must be a non-empty array.'], // initialization / settings validation [c.ERROR_INVALID_IMPRESSIONS_MODE, c.LOG_PREFIX_SETTINGS + ': you passed an invalid "impressionsMode". It should be one of the following values: %s. Defaulting to "%s" mode.'], + [c.ERROR_LOCALHOST_MODULE_REQUIRED, c.LOG_PREFIX_SETTINGS + ': you didn\'t pass a valid "sync.localhostMode". It should be defined for using the SDK in localhost mode'] ]; diff --git a/src/sync/offline/splitsParser/splitsParserFromFile.ts b/src/sync/offline/splitsParser/splitsParserFromFile.ts index 62de09f4..95ea8d14 100644 --- a/src/sync/offline/splitsParser/splitsParserFromFile.ts +++ b/src/sync/offline/splitsParser/splitsParserFromFile.ts @@ -7,8 +7,9 @@ import yaml from 'js-yaml'; import { isString, endsWith, find, forOwn, uniq, } from '../../../utils/lang'; import parseCondition, { IMockSplitEntry } from './parseCondition'; import { ISplitPartial } from '../../../dtos/types'; -import { SplitIO } from '../../../types'; +import { ISettings, SplitIO } from '../../../types'; import { ILogger } from '../../../logger/types'; +import { ISplitsParser } from './types'; const logPrefix = 'sync:offline:splits-fetcher: '; @@ -68,7 +69,7 @@ function arrangeConditions(mocksData: Record & { }); } -export function splitsParserFromFileFactory() { +export function splitsParserFromFileFactory(): ISplitsParser { let previousMock = 'NO_MOCK_LOADED'; @@ -163,8 +164,8 @@ export function splitsParserFromFileFactory() { } // Load the content of a configuration file into an Object - return function splitsParserFromFile({ features, log }: { features?: SplitIO.MockedFeaturesFilePath, log: ILogger }): false | Record { - const filePath = configFilesPath(features); + return function splitsParserFromFile({ features, log }: ISettings): false | Record { + const filePath = configFilesPath(features as string); let mockData: false | Record; // If we have a filePath, it means the extension is correct, choose the parser. diff --git a/src/sync/syncManagerFromFile.ts b/src/sync/syncManagerFromFile.ts new file mode 100644 index 00000000..f1a27fce --- /dev/null +++ b/src/sync/syncManagerFromFile.ts @@ -0,0 +1,13 @@ +import { splitsParserFromFileFactory } from './offline/splitsParser/splitsParserFromFile'; +import { syncManagerOfflineFactory } from './syncManagerOffline'; +import { SplitIO } from '../types'; +import { LOCALHOST_MODE } from '../utils/constants'; + +// Factory of Localhost SyncManager based on yaml file. +// Requires Node 'fs' and 'path' APIs. +export function LocalhostFromFile(): SplitIO.LocalhostFactory { + const localhost = syncManagerOfflineFactory(splitsParserFromFileFactory); + // @ts-ignore + localhost.type = LOCALHOST_MODE; + return localhost; +} diff --git a/src/sync/syncManagerFromObject.ts b/src/sync/syncManagerFromObject.ts new file mode 100644 index 00000000..24b4f0f3 --- /dev/null +++ b/src/sync/syncManagerFromObject.ts @@ -0,0 +1,12 @@ +import { splitsParserFromSettingsFactory } from './offline/splitsParser/splitsParserFromSettings'; +import { syncManagerOfflineFactory } from './syncManagerOffline'; +import { SplitIO } from '../types'; +import { LOCALHOST_MODE } from '../utils/constants'; + +// Factory of Localhost SyncManager based on JS object. +export function LocalhostFromObject(): SplitIO.LocalhostFactory { + const localhost = syncManagerOfflineFactory(splitsParserFromSettingsFactory); + // @ts-ignore + localhost.type = LOCALHOST_MODE; + return localhost; +} diff --git a/src/types.ts b/src/types.ts index 9d20e84f..3b960424 100644 --- a/src/types.ts +++ b/src/types.ts @@ -4,6 +4,7 @@ import { ILogger } from './logger/types'; /* eslint-disable no-use-before-define */ import { IStorageFactoryParams, IStorageSyncCS, IStorageSync, IStorageAsync, IStorageSyncFactory } from './storages/types'; +import { ISyncManagerFactoryParams, ISyncManagerCS } from './sync/types'; /** * EventEmitter interface with the minimal methods used by the SDK @@ -99,7 +100,8 @@ export interface ISettings { readonly sync: { splitFilters: SplitIO.SplitFilter[], impressionsMode: SplitIO.ImpressionsMode, - __splitFiltersValidation: ISplitFiltersValidation + __splitFiltersValidation: ISplitFiltersValidation, + localhostMode: SplitIO.LocalhostFactory }, readonly runtime: { ip: string | false @@ -581,6 +583,13 @@ export namespace SplitIO { * @typedef {Promise} SplitNamesAsync */ export type SplitNamesAsync = Promise; + /** + * Localhost mode factory. + */ + export type LocalhostFactory = { + type?: 'localhost' + (params: ISyncManagerFactoryParams): ISyncManagerCS + } /** * Impression listener interface. This is the interface that needs to be implemented * by the element you provide to the SDK as impression listener. diff --git a/src/utils/settingsValidation/index.ts b/src/utils/settingsValidation/index.ts index 96f22227..2a211f90 100644 --- a/src/utils/settingsValidation/index.ts +++ b/src/utils/settingsValidation/index.ts @@ -74,7 +74,8 @@ const base = { sync: { splitFilters: undefined, // impressions collection mode - impressionsMode: OPTIMIZED + impressionsMode: OPTIMIZED, + localhostMode: undefined }, runtime: { @@ -99,7 +100,7 @@ function fromSecondsToMillis(n: number) { */ export function settingsValidation(config: unknown, validationParams: ISettingsValidationParams) { - const { defaults, runtime, storage, integrations, logger } = validationParams; + const { defaults, runtime, storage, integrations, logger, localhost } = validationParams; // creates a settings object merging base, defaults and config objects. const withDefaults = merge({}, base, defaults, config) as ISettings; @@ -144,6 +145,8 @@ export function settingsValidation(config: unknown, validationParams: ISettingsV // @ts-ignore, modify readonly prop if (integrations) withDefaults.integrations = integrations(withDefaults); + if (localhost) withDefaults.sync.localhostMode = localhost(withDefaults); + // validate push options if (withDefaults.streamingEnabled !== false) { // @ts-ignore, modify readonly prop withDefaults.streamingEnabled = true; diff --git a/src/utils/settingsValidation/localhost/index.ts b/src/utils/settingsValidation/localhost/index.ts new file mode 100644 index 00000000..0f6aad9d --- /dev/null +++ b/src/utils/settingsValidation/localhost/index.ts @@ -0,0 +1,19 @@ +import { ERROR_LOCALHOST_MODULE_REQUIRED } from '../../../logger/constants'; +import { ISettings, } from '../../../types'; +import { LOCALHOST_MODE } from '../../constants'; + +/** + * This function validates `settings.storage` object + * + * @param {any} settings config object provided by the user to initialize the sdk + * + * @returns {Object} valid storage factory. It might be the default `InMemoryStorageCSFactory` if the provided storage is invalid. + */ +export function validateLocalhost(settings: ISettings) { + const localhostMode = settings.sync.localhostMode; + + if (settings.mode === LOCALHOST_MODE && (typeof localhostMode !== 'function' || localhostMode.type !== LOCALHOST_MODE)) { + settings.log.error(ERROR_LOCALHOST_MODULE_REQUIRED); + } + return localhostMode; +} diff --git a/src/utils/settingsValidation/types.ts b/src/utils/settingsValidation/types.ts index a3ab3761..a9438a30 100644 --- a/src/utils/settingsValidation/types.ts +++ b/src/utils/settingsValidation/types.ts @@ -18,4 +18,6 @@ export interface ISettingsValidationParams { integrations?: (settings: ISettings) => ISettings['integrations'], /** Logger validator */ logger: (settings: ISettings) => ISettings['log'], + /** Localhost mode validator */ + localhost?: (settings: ISettings) => ISettings['sync']['localhostMode'], } From 35163205c0210b0336a53beebb0ee7c491ecd07a Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 7 Oct 2021 14:45:11 -0300 Subject: [PATCH 129/144] updated test mocks --- .../settingsValidation/__tests__/settings.mocks.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/utils/settingsValidation/__tests__/settings.mocks.ts b/src/utils/settingsValidation/__tests__/settings.mocks.ts index 4b8a10e4..a08da99d 100644 --- a/src/utils/settingsValidation/__tests__/settings.mocks.ts +++ b/src/utils/settingsValidation/__tests__/settings.mocks.ts @@ -1,6 +1,7 @@ import { InMemoryStorageCSFactory } from '../../../storages/inMemory/InMemoryStorageCS'; import { ISettings } from '../../../types'; import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; +import { LocalhostFromObject } from '../../../sync/syncManagerFromObject'; export const settingsWithKey = { core: { @@ -61,13 +62,19 @@ export const fullSettings: ISettings = { }, features: 'path/to/file', storage: InMemoryStorageCSFactory, - integrations: [()=>{}], // A no-op integration + integrations: [() => { }], // A no-op integration mode: 'standalone', debug: false, streamingEnabled: false, sync: { splitFilters: [], - impressionsMode: 'OPTIMIZED' + impressionsMode: 'OPTIMIZED', + localhostMode: LocalhostFromObject(), + __splitFiltersValidation: { + validFilters: [], + queryString: null, + groupedFilters: { byName: [], byPrefix: [] } + } }, version: 'jest', runtime: { From 5a526807fe75a53dda6915eab477acfae5952491 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 7 Oct 2021 16:23:27 -0300 Subject: [PATCH 130/144] vulnerability fixes --- package-lock.json | 8263 ++++++++++++++------------------------------- package.json | 10 +- 2 files changed, 2607 insertions(+), 5666 deletions(-) diff --git a/package-lock.json b/package-lock.json index c7c96541..17c71d80 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,39 +1,58 @@ { "name": "@splitsoftware/splitio-commons", - "version": "0.1.1-canary.18", + "version": "0.1.1-rc.18", "lockfileVersion": 1, "requires": true, "dependencies": { "@babel/code-frame": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", - "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", + "version": "7.15.8", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.15.8.tgz", + "integrity": "sha512-2IAnmn8zbvC/jKYhq5Ki9I+DwjlrtMPUCH/CpHvqI4dNnlwHwsxoIhlc8WcYY5LSYknXQtAlFYuHfqAFCvQ4Wg==", "dev": true, "requires": { - "@babel/highlight": "^7.10.4" + "@babel/highlight": "^7.14.5" + }, + "dependencies": { + "@babel/helper-validator-identifier": { + "version": "7.15.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz", + "integrity": "sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w==", + "dev": true + }, + "@babel/highlight": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.5.tgz", + "integrity": "sha512-qf9u2WFWVV0MppaL877j2dBtQIDgmidgjGk5VIMw3OadXvYaXn66U1BFlH2t4+t3i+8PhedppRv+i40ABzd+gg==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.14.5", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + } } }, "@babel/compat-data": { - "version": "7.13.12", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.13.12.tgz", - "integrity": "sha512-3eJJ841uKxeV8dcN/2yGEUy+RfgQspPEgQat85umsE1rotuquQ2AbIub4S6j7c50a2d+4myc+zSlnXeIHrOnhQ==", + "version": "7.15.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.15.0.tgz", + "integrity": "sha512-0NqAC1IJE0S0+lL1SWFMxMkz1pKCNCjI4tr2Zx4LJSXxCLAdr6KyArnY+sno5m3yH9g737ygOyPABDsnXkpxiA==", "dev": true }, "@babel/core": { - "version": "7.13.14", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.13.14.tgz", - "integrity": "sha512-wZso/vyF4ki0l0znlgM4inxbdrUvCb+cVz8grxDq+6C9k6qbqoIJteQOKicaKjCipU3ISV+XedCqpL2RJJVehA==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.12.13", - "@babel/generator": "^7.13.9", - "@babel/helper-compilation-targets": "^7.13.13", - "@babel/helper-module-transforms": "^7.13.14", - "@babel/helpers": "^7.13.10", - "@babel/parser": "^7.13.13", - "@babel/template": "^7.12.13", - "@babel/traverse": "^7.13.13", - "@babel/types": "^7.13.14", + "version": "7.15.8", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.15.8.tgz", + "integrity": "sha512-3UG9dsxvYBMYwRv+gS41WKHno4K60/9GPy1CJaH6xy3Elq8CTtvtjT5R5jmNhXfCYLX2mTw+7/aq5ak/gOE0og==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.15.8", + "@babel/generator": "^7.15.8", + "@babel/helper-compilation-targets": "^7.15.4", + "@babel/helper-module-transforms": "^7.15.8", + "@babel/helpers": "^7.15.4", + "@babel/parser": "^7.15.8", + "@babel/template": "^7.15.4", + "@babel/traverse": "^7.15.4", + "@babel/types": "^7.15.6", "convert-source-map": "^1.7.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -42,32 +61,6 @@ "source-map": "^0.5.0" }, "dependencies": { - "@babel/code-frame": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.13.tgz", - "integrity": "sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==", - "dev": true, - "requires": { - "@babel/highlight": "^7.12.13" - } - }, - "@babel/helper-validator-identifier": { - "version": "7.12.11", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz", - "integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==", - "dev": true - }, - "@babel/highlight": { - "version": "7.13.10", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.13.10.tgz", - "integrity": "sha512-5aPpe5XQPzflQrFwL1/QoeHkP2MsA4JCntcXHRhEsdsfPVkvPi2w7Qix4iV7t5S/oC9OodGrggd8aco1g3SZFg==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.12.11", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - } - }, "source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", @@ -77,12 +70,12 @@ } }, "@babel/generator": { - "version": "7.13.9", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.13.9.tgz", - "integrity": "sha512-mHOOmY0Axl/JCTkxTU6Lf5sWOg/v8nUa+Xkt4zMTftX0wqmb6Sh7J8gvcehBw7q0AhrhAR+FDacKjCZ2X8K+Sw==", + "version": "7.15.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.15.8.tgz", + "integrity": "sha512-ECmAKstXbp1cvpTTZciZCgfOt6iN64lR0d+euv3UZisU5awfRawOvg07Utn/qBGuH4bRIEZKrA/4LzZyXhZr8g==", "dev": true, "requires": { - "@babel/types": "^7.13.0", + "@babel/types": "^7.15.6", "jsesc": "^2.5.1", "source-map": "^0.5.0" }, @@ -96,137 +89,131 @@ } }, "@babel/helper-compilation-targets": { - "version": "7.13.13", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.13.13.tgz", - "integrity": "sha512-q1kcdHNZehBwD9jYPh3WyXcsFERi39X4I59I3NadciWtNDyZ6x+GboOxncFK0kXlKIv6BJm5acncehXWUjWQMQ==", + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.15.4.tgz", + "integrity": "sha512-rMWPCirulnPSe4d+gwdWXLfAXTTBj8M3guAf5xFQJ0nvFY7tfNAFnWdqaHegHlgDZOCT4qvhF3BYlSJag8yhqQ==", "dev": true, "requires": { - "@babel/compat-data": "^7.13.12", - "@babel/helper-validator-option": "^7.12.17", - "browserslist": "^4.14.5", + "@babel/compat-data": "^7.15.0", + "@babel/helper-validator-option": "^7.14.5", + "browserslist": "^4.16.6", "semver": "^6.3.0" - }, - "dependencies": { - "browserslist": { - "version": "4.16.6", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.6.tgz", - "integrity": "sha512-Wspk/PqO+4W9qp5iUTJsa1B/QrYn1keNCcEP5OvP7WBwT4KaDly0uONYmC6Xa3Z5IqnUgS0KcgLYu1l74x0ZXQ==", - "dev": true, - "requires": { - "caniuse-lite": "^1.0.30001219", - "colorette": "^1.2.2", - "electron-to-chromium": "^1.3.723", - "escalade": "^3.1.1", - "node-releases": "^1.1.71" - } - } } }, "@babel/helper-function-name": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.12.13.tgz", - "integrity": "sha512-TZvmPn0UOqmvi5G4vvw0qZTpVptGkB1GL61R6lKvrSdIxGm5Pky7Q3fpKiIkQCAtRCBUwB0PaThlx9vebCDSwA==", + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.15.4.tgz", + "integrity": "sha512-Z91cOMM4DseLIGOnog+Z8OI6YseR9bua+HpvLAQ2XayUGU+neTtX+97caALaLdyu53I/fjhbeCnWnRH1O3jFOw==", "dev": true, "requires": { - "@babel/helper-get-function-arity": "^7.12.13", - "@babel/template": "^7.12.13", - "@babel/types": "^7.12.13" + "@babel/helper-get-function-arity": "^7.15.4", + "@babel/template": "^7.15.4", + "@babel/types": "^7.15.4" } }, "@babel/helper-get-function-arity": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.12.13.tgz", - "integrity": "sha512-DjEVzQNz5LICkzN0REdpD5prGoidvbdYk1BVgRUOINaWJP2t6avB27X1guXK1kXNrX0WMfsrm1A/ZBthYuIMQg==", + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.15.4.tgz", + "integrity": "sha512-1/AlxSF92CmGZzHnC515hm4SirTxtpDnLEJ0UyEMgTMZN+6bxXKg04dKhiRx5Enel+SUA1G1t5Ed/yQia0efrA==", "dev": true, "requires": { - "@babel/types": "^7.12.13" + "@babel/types": "^7.15.4" + } + }, + "@babel/helper-hoist-variables": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.15.4.tgz", + "integrity": "sha512-VTy085egb3jUGVK9ycIxQiPbquesq0HUQ+tPO0uv5mPEBZipk+5FkRKiWq5apuyTE9FUrjENB0rCf8y+n+UuhA==", + "dev": true, + "requires": { + "@babel/types": "^7.15.4" } }, "@babel/helper-member-expression-to-functions": { - "version": "7.13.12", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.13.12.tgz", - "integrity": "sha512-48ql1CLL59aKbU94Y88Xgb2VFy7a95ykGRbJJaaVv+LX5U8wFpLfiGXJJGUozsmA1oEh/o5Bp60Voq7ACyA/Sw==", + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.15.4.tgz", + "integrity": "sha512-cokOMkxC/BTyNP1AlY25HuBWM32iCEsLPI4BHDpJCHHm1FU2E7dKWWIXJgQgSFiu4lp8q3bL1BIKwqkSUviqtA==", "dev": true, "requires": { - "@babel/types": "^7.13.12" + "@babel/types": "^7.15.4" } }, "@babel/helper-module-imports": { - "version": "7.13.12", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.13.12.tgz", - "integrity": "sha512-4cVvR2/1B693IuOvSI20xqqa/+bl7lqAMR59R4iu39R9aOX8/JoYY1sFaNvUMyMBGnHdwvJgUrzNLoUZxXypxA==", + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.15.4.tgz", + "integrity": "sha512-jeAHZbzUwdW/xHgHQ3QmWR4Jg6j15q4w/gCfwZvtqOxoo5DKtLHk8Bsf4c5RZRC7NmLEs+ohkdq8jFefuvIxAA==", "dev": true, "requires": { - "@babel/types": "^7.13.12" + "@babel/types": "^7.15.4" } }, "@babel/helper-module-transforms": { - "version": "7.13.14", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.13.14.tgz", - "integrity": "sha512-QuU/OJ0iAOSIatyVZmfqB0lbkVP0kDRiKj34xy+QNsnVZi/PA6BoSoreeqnxxa9EHFAIL0R9XOaAR/G9WlIy5g==", - "dev": true, - "requires": { - "@babel/helper-module-imports": "^7.13.12", - "@babel/helper-replace-supers": "^7.13.12", - "@babel/helper-simple-access": "^7.13.12", - "@babel/helper-split-export-declaration": "^7.12.13", - "@babel/helper-validator-identifier": "^7.12.11", - "@babel/template": "^7.12.13", - "@babel/traverse": "^7.13.13", - "@babel/types": "^7.13.14" + "version": "7.15.8", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.15.8.tgz", + "integrity": "sha512-DfAfA6PfpG8t4S6npwzLvTUpp0sS7JrcuaMiy1Y5645laRJIp/LiLGIBbQKaXSInK8tiGNI7FL7L8UvB8gdUZg==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.15.4", + "@babel/helper-replace-supers": "^7.15.4", + "@babel/helper-simple-access": "^7.15.4", + "@babel/helper-split-export-declaration": "^7.15.4", + "@babel/helper-validator-identifier": "^7.15.7", + "@babel/template": "^7.15.4", + "@babel/traverse": "^7.15.4", + "@babel/types": "^7.15.6" }, "dependencies": { "@babel/helper-validator-identifier": { - "version": "7.12.11", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz", - "integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==", + "version": "7.15.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz", + "integrity": "sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w==", "dev": true } } }, "@babel/helper-optimise-call-expression": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.12.13.tgz", - "integrity": "sha512-BdWQhoVJkp6nVjB7nkFWcn43dkprYauqtk++Py2eaf/GRDFm5BxRqEIZCiHlZUGAVmtwKcsVL1dC68WmzeFmiA==", + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.15.4.tgz", + "integrity": "sha512-E/z9rfbAOt1vDW1DR7k4SzhzotVV5+qMciWV6LaG1g4jeFrkDlJedjtV4h0i4Q/ITnUu+Pk08M7fczsB9GXBDw==", "dev": true, "requires": { - "@babel/types": "^7.12.13" + "@babel/types": "^7.15.4" } }, "@babel/helper-plugin-utils": { - "version": "7.13.0", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.13.0.tgz", - "integrity": "sha512-ZPafIPSwzUlAoWT8DKs1W2VyF2gOWthGd5NGFMsBcMMol+ZhK+EQY/e6V96poa6PA/Bh+C9plWN0hXO1uB8AfQ==", + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.14.5.tgz", + "integrity": "sha512-/37qQCE3K0vvZKwoK4XU/irIJQdIfCJuhU5eKnNxpFDsOkgFaUAwbv+RYw6eYgsC0E4hS7r5KqGULUogqui0fQ==", "dev": true }, "@babel/helper-replace-supers": { - "version": "7.13.12", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.13.12.tgz", - "integrity": "sha512-Gz1eiX+4yDO8mT+heB94aLVNCL+rbuT2xy4YfyNqu8F+OI6vMvJK891qGBTqL9Uc8wxEvRW92Id6G7sDen3fFw==", + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.15.4.tgz", + "integrity": "sha512-/ztT6khaXF37MS47fufrKvIsiQkx1LBRvSJNzRqmbyeZnTwU9qBxXYLaaT/6KaxfKhjs2Wy8kG8ZdsFUuWBjzw==", "dev": true, "requires": { - "@babel/helper-member-expression-to-functions": "^7.13.12", - "@babel/helper-optimise-call-expression": "^7.12.13", - "@babel/traverse": "^7.13.0", - "@babel/types": "^7.13.12" + "@babel/helper-member-expression-to-functions": "^7.15.4", + "@babel/helper-optimise-call-expression": "^7.15.4", + "@babel/traverse": "^7.15.4", + "@babel/types": "^7.15.4" } }, "@babel/helper-simple-access": { - "version": "7.13.12", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.13.12.tgz", - "integrity": "sha512-7FEjbrx5SL9cWvXioDbnlYTppcZGuCY6ow3/D5vMggb2Ywgu4dMrpTJX0JdQAIcRRUElOIxF3yEooa9gUb9ZbA==", + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.15.4.tgz", + "integrity": "sha512-UzazrDoIVOZZcTeHHEPYrr1MvTR/K+wgLg6MY6e1CJyaRhbibftF6fR2KU2sFRtI/nERUZR9fBd6aKgBlIBaPg==", "dev": true, "requires": { - "@babel/types": "^7.13.12" + "@babel/types": "^7.15.4" } }, "@babel/helper-split-export-declaration": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.12.13.tgz", - "integrity": "sha512-tCJDltF83htUtXx5NLcaDqRmknv652ZWCHyoTETf1CXYJdPC7nohZohjUgieXhv0hTJdRf2FjDueFehdNucpzg==", + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.15.4.tgz", + "integrity": "sha512-HsFqhLDZ08DxCpBdEVtKmywj6PQbwnF6HHybur0MAnkAKnlS6uHkwnmRIkElB2Owpfb4xL4NwDmDLFubueDXsw==", "dev": true, "requires": { - "@babel/types": "^7.12.13" + "@babel/types": "^7.15.4" } }, "@babel/helper-validator-identifier": { @@ -236,20 +223,20 @@ "dev": true }, "@babel/helper-validator-option": { - "version": "7.12.17", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.12.17.tgz", - "integrity": "sha512-TopkMDmLzq8ngChwRlyjR6raKD6gMSae4JdYDB8bByKreQgG0RBTuKe9LRxW3wFtUnjxOPRKBDwEH6Mg5KeDfw==", + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.14.5.tgz", + "integrity": "sha512-OX8D5eeX4XwcroVW45NMvoYaIuFI+GQpA2a8Gi+X/U/cDUIRsV37qQfF905F0htTRCREQIB4KqPeaveRJUl3Ow==", "dev": true }, "@babel/helpers": { - "version": "7.13.10", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.13.10.tgz", - "integrity": "sha512-4VO883+MWPDUVRF3PhiLBUFHoX/bsLTGFpFK/HqvvfBZz2D57u9XzPVNFVBTc0PW/CWR9BXTOKt8NF4DInUHcQ==", + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.15.4.tgz", + "integrity": "sha512-V45u6dqEJ3w2rlryYYXf6i9rQ5YMNu4FLS6ngs8ikblhu2VdR1AqAd6aJjBzmf2Qzh6KOLqKHxEN9+TFbAkAVQ==", "dev": true, "requires": { - "@babel/template": "^7.12.13", - "@babel/traverse": "^7.13.0", - "@babel/types": "^7.13.0" + "@babel/template": "^7.15.4", + "@babel/traverse": "^7.15.4", + "@babel/types": "^7.15.4" } }, "@babel/highlight": { @@ -264,9 +251,9 @@ } }, "@babel/parser": { - "version": "7.13.13", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.13.13.tgz", - "integrity": "sha512-OhsyMrqygfk5v8HmWwOzlYjJrtLaFhF34MrfG/Z73DgYCI6ojNUTUp2TYbtnjo8PegeJp12eamsNettCQjKjVw==", + "version": "7.15.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.15.8.tgz", + "integrity": "sha512-BRYa3wcQnjS/nqI8Ac94pYYpJfojHVvVXJ97+IDCImX4Jc8W8Xv1+47enbruk+q1etOpsQNwnfFcNGw+gtPGxA==", "dev": true }, "@babel/plugin-syntax-async-generators": { @@ -369,112 +356,65 @@ } }, "@babel/plugin-syntax-top-level-await": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.12.13.tgz", - "integrity": "sha512-A81F9pDwyS7yM//KwbCSDqy3Uj4NMIurtplxphWxoYtNPov7cJsDkAFNNyVlIZ3jwGycVsurZ+LtOA8gZ376iQ==", + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.12.13" + "@babel/helper-plugin-utils": "^7.14.5" } }, - "@babel/template": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.12.13.tgz", - "integrity": "sha512-/7xxiGA57xMo/P2GVvdEumr8ONhFOhfgq2ihK3h1e6THqzTAkHbkXgB0xI9yeTfIUoH3+oAeHhqm/I43OTbbjA==", + "@babel/plugin-syntax-typescript": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.14.5.tgz", + "integrity": "sha512-u6OXzDaIXjEstBRRoBCQ/uKQKlbuaeE5in0RvWdA4pN6AhqxTIwUsnHPU1CFZA/amYObMsuWhYfRl3Ch90HD0Q==", "dev": true, "requires": { - "@babel/code-frame": "^7.12.13", - "@babel/parser": "^7.12.13", - "@babel/types": "^7.12.13" - }, - "dependencies": { - "@babel/code-frame": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.13.tgz", - "integrity": "sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==", - "dev": true, - "requires": { - "@babel/highlight": "^7.12.13" - } - }, - "@babel/helper-validator-identifier": { - "version": "7.12.11", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz", - "integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==", - "dev": true - }, - "@babel/highlight": { - "version": "7.13.10", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.13.10.tgz", - "integrity": "sha512-5aPpe5XQPzflQrFwL1/QoeHkP2MsA4JCntcXHRhEsdsfPVkvPi2w7Qix4iV7t5S/oC9OodGrggd8aco1g3SZFg==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.12.11", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - } - } + "@babel/helper-plugin-utils": "^7.14.5" } }, - "@babel/traverse": { - "version": "7.13.13", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.13.13.tgz", - "integrity": "sha512-CblEcwmXKR6eP43oQGG++0QMTtCjAsa3frUuzHoiIJWpaIIi8dwMyEFUJoXRLxagGqCK+jALRwIO+o3R9p/uUg==", + "@babel/template": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.15.4.tgz", + "integrity": "sha512-UgBAfEa1oGuYgDIPM2G+aHa4Nlo9Lh6mGD2bDBGMTbYnc38vulXPuC1MGjYILIEmlwl6Rd+BPR9ee3gm20CBtg==", "dev": true, "requires": { - "@babel/code-frame": "^7.12.13", - "@babel/generator": "^7.13.9", - "@babel/helper-function-name": "^7.12.13", - "@babel/helper-split-export-declaration": "^7.12.13", - "@babel/parser": "^7.13.13", - "@babel/types": "^7.13.13", + "@babel/code-frame": "^7.14.5", + "@babel/parser": "^7.15.4", + "@babel/types": "^7.15.4" + } + }, + "@babel/traverse": { + "version": "7.15.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.15.4.tgz", + "integrity": "sha512-W6lQD8l4rUbQR/vYgSuCAE75ADyyQvOpFVsvPPdkhf6lATXAsQIG9YdtOcu8BB1dZ0LKu+Zo3c1wEcbKeuhdlA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.14.5", + "@babel/generator": "^7.15.4", + "@babel/helper-function-name": "^7.15.4", + "@babel/helper-hoist-variables": "^7.15.4", + "@babel/helper-split-export-declaration": "^7.15.4", + "@babel/parser": "^7.15.4", + "@babel/types": "^7.15.4", "debug": "^4.1.0", "globals": "^11.1.0" - }, - "dependencies": { - "@babel/code-frame": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.13.tgz", - "integrity": "sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==", - "dev": true, - "requires": { - "@babel/highlight": "^7.12.13" - } - }, - "@babel/helper-validator-identifier": { - "version": "7.12.11", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz", - "integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==", - "dev": true - }, - "@babel/highlight": { - "version": "7.13.10", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.13.10.tgz", - "integrity": "sha512-5aPpe5XQPzflQrFwL1/QoeHkP2MsA4JCntcXHRhEsdsfPVkvPi2w7Qix4iV7t5S/oC9OodGrggd8aco1g3SZFg==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.12.11", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - } - } } }, "@babel/types": { - "version": "7.13.14", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.13.14.tgz", - "integrity": "sha512-A2aa3QTkWoyqsZZFl56MLUsfmh7O0gN41IPvXAE/++8ojpbz12SszD7JEGYVdn4f9Kt4amIei07swF1h4AqmmQ==", + "version": "7.15.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.15.6.tgz", + "integrity": "sha512-BPU+7QhqNjmWyDO0/vitH/CuhpV8ZmK1wpKva8nuyNF5MJfuRNWMc+hc14+u9xT93kvykMdncrJT19h74uB1Ig==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.12.11", - "lodash": "^4.17.19", + "@babel/helper-validator-identifier": "^7.14.9", "to-fast-properties": "^2.0.0" }, "dependencies": { "@babel/helper-validator-identifier": { - "version": "7.12.11", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz", - "integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==", + "version": "7.15.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz", + "integrity": "sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w==", "dev": true } } @@ -485,50 +425,30 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, - "@cnakazawa/watch": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@cnakazawa/watch/-/watch-1.0.4.tgz", - "integrity": "sha512-v9kIhKwjeZThiWrLmj0y17CWoyddASLj9O2yvbZkbvw/N3rWOYy9zkV66ursAoVr0mV15bL8g0c4QZUE6cdDoQ==", - "dev": true, - "requires": { - "exec-sh": "^0.3.2", - "minimist": "^1.2.0" - } - }, "@eslint/eslintrc": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.1.3.tgz", - "integrity": "sha512-4YVwPkANLeNtRjMekzux1ci8hIaH5eGKktGqR0d3LWsKNn5B2X/1Z6Trxy7jQXl9EBGE6Yj02O+t09FMeRllaA==", + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.3.tgz", + "integrity": "sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw==", "dev": true, "requires": { "ajv": "^6.12.4", "debug": "^4.1.1", "espree": "^7.3.0", - "globals": "^12.1.0", + "globals": "^13.9.0", "ignore": "^4.0.6", "import-fresh": "^3.2.1", "js-yaml": "^3.13.1", - "lodash": "^4.17.19", "minimatch": "^3.0.4", "strip-json-comments": "^3.1.1" }, "dependencies": { - "debug": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz", - "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, "globals": { - "version": "12.4.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", - "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", + "version": "13.11.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.11.0.tgz", + "integrity": "sha512-08/xrJ7wQjK9kkkRoI3OFUBbLx4f+6x3SGwcPvQ0QH6goFDrOU2oyAWrmh3dJezu65buo+HBMzAMQy6rovVC3g==", "dev": true, "requires": { - "type-fest": "^0.8.1" + "type-fest": "^0.20.2" } }, "ignore": { @@ -537,14 +457,31 @@ "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", "dev": true }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "dev": true } } }, + "@humanwhocodes/config-array": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz", + "integrity": "sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg==", + "dev": true, + "requires": { + "@humanwhocodes/object-schema": "^1.2.0", + "debug": "^4.1.1", + "minimatch": "^3.0.4" + } + }, + "@humanwhocodes/object-schema": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.0.tgz", + "integrity": "sha512-wdppn25U8z/2yiaT6YGquE6X8sSv7hNMWSXYSSU1jGv/yd6XqjXgTDJ8KP4NgjTXfJ3GbRjeeb8RTV7a/VpM+w==", + "dev": true + }, "@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -565,32 +502,19 @@ "dev": true }, "@jest/console": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-26.6.2.tgz", - "integrity": "sha512-IY1R2i2aLsLr7Id3S6p2BA82GNWryt4oSvEXLAKc+L2zdi89dSkE8xC1C+0kpATG4JhBJREnQOH7/zmccM2B0g==", + "version": "27.2.4", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-27.2.4.tgz", + "integrity": "sha512-94znCKynPZpDpYHQ6esRJSc11AmONrVkBOBZiD7S+bSubHhrUfbS95EY5HIOxhm4PQO7cnvZkL3oJcY0oMA+Wg==", "dev": true, "requires": { - "@jest/types": "^26.6.2", + "@jest/types": "^27.2.4", "@types/node": "*", "chalk": "^4.0.0", - "jest-message-util": "^26.6.2", - "jest-util": "^26.6.2", + "jest-message-util": "^27.2.4", + "jest-util": "^27.2.4", "slash": "^3.0.0" }, "dependencies": { - "@jest/types": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", - "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^15.0.0", - "chalk": "^4.0.0" - } - }, "ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -601,9 +525,9 @@ } }, "chalk": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", - "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "requires": { "ansi-styles": "^4.1.0", @@ -631,20 +555,6 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, - "jest-util": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-26.6.2.tgz", - "integrity": "sha512-MDW0fKfsn0OI7MS7Euz6h8HNDXVQ0gaM9uW6RjfDmd1DAFcaxX9OqIakHIqhbnmF08Cf2DLDG+ulq8YQQ0Lp0Q==", - "dev": true, - "requires": { - "@jest/types": "^26.6.2", - "@types/node": "*", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.4", - "is-ci": "^2.0.0", - "micromatch": "^4.0.2" - } - }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -657,54 +567,41 @@ } }, "@jest/core": { - "version": "26.6.3", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-26.6.3.tgz", - "integrity": "sha512-xvV1kKbhfUqFVuZ8Cyo+JPpipAHHAV3kcDBftiduK8EICXmTFddryy3P7NfZt8Pv37rA9nEJBKCCkglCPt/Xjw==", + "version": "27.2.4", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-27.2.4.tgz", + "integrity": "sha512-UNQLyy+rXoojNm2MGlapgzWhZD1CT1zcHZQYeiD0xE7MtJfC19Q6J5D/Lm2l7i4V97T30usKDoEtjI8vKwWcLg==", "dev": true, "requires": { - "@jest/console": "^26.6.2", - "@jest/reporters": "^26.6.2", - "@jest/test-result": "^26.6.2", - "@jest/transform": "^26.6.2", - "@jest/types": "^26.6.2", + "@jest/console": "^27.2.4", + "@jest/reporters": "^27.2.4", + "@jest/test-result": "^27.2.4", + "@jest/transform": "^27.2.4", + "@jest/types": "^27.2.4", "@types/node": "*", "ansi-escapes": "^4.2.1", "chalk": "^4.0.0", + "emittery": "^0.8.1", "exit": "^0.1.2", "graceful-fs": "^4.2.4", - "jest-changed-files": "^26.6.2", - "jest-config": "^26.6.3", - "jest-haste-map": "^26.6.2", - "jest-message-util": "^26.6.2", - "jest-regex-util": "^26.0.0", - "jest-resolve": "^26.6.2", - "jest-resolve-dependencies": "^26.6.3", - "jest-runner": "^26.6.3", - "jest-runtime": "^26.6.3", - "jest-snapshot": "^26.6.2", - "jest-util": "^26.6.2", - "jest-validate": "^26.6.2", - "jest-watcher": "^26.6.2", - "micromatch": "^4.0.2", - "p-each-series": "^2.1.0", + "jest-changed-files": "^27.2.4", + "jest-config": "^27.2.4", + "jest-haste-map": "^27.2.4", + "jest-message-util": "^27.2.4", + "jest-regex-util": "^27.0.6", + "jest-resolve": "^27.2.4", + "jest-resolve-dependencies": "^27.2.4", + "jest-runner": "^27.2.4", + "jest-runtime": "^27.2.4", + "jest-snapshot": "^27.2.4", + "jest-util": "^27.2.4", + "jest-validate": "^27.2.4", + "jest-watcher": "^27.2.4", + "micromatch": "^4.0.4", "rimraf": "^3.0.0", "slash": "^3.0.0", "strip-ansi": "^6.0.0" }, "dependencies": { - "@jest/types": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", - "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^15.0.0", - "chalk": "^4.0.0" - } - }, "ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -715,9 +612,9 @@ } }, "chalk": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", - "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "requires": { "ansi-styles": "^4.1.0", @@ -745,20 +642,6 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, - "jest-util": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-26.6.2.tgz", - "integrity": "sha512-MDW0fKfsn0OI7MS7Euz6h8HNDXVQ0gaM9uW6RjfDmd1DAFcaxX9OqIakHIqhbnmF08Cf2DLDG+ulq8YQQ0Lp0Q==", - "dev": true, - "requires": { - "@jest/types": "^26.6.2", - "@types/node": "*", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.4", - "is-ci": "^2.0.0", - "micromatch": "^4.0.2" - } - }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -771,30 +654,74 @@ } }, "@jest/environment": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-26.6.2.tgz", - "integrity": "sha512-nFy+fHl28zUrRsCeMB61VDThV1pVTtlEokBRgqPrcT1JNq4yRNIyTHfyht6PqtUvY9IsuLGTrbG8kPXjSZIZwA==", + "version": "27.2.4", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-27.2.4.tgz", + "integrity": "sha512-wkuui5yr3SSQW0XD0Qm3TATUbL/WE3LDEM3ulC+RCQhMf2yxhci8x7svGkZ4ivJ6Pc94oOzpZ6cdHBAMSYd1ew==", + "dev": true, + "requires": { + "@jest/fake-timers": "^27.2.4", + "@jest/types": "^27.2.4", + "@types/node": "*", + "jest-mock": "^27.2.4" + } + }, + "@jest/fake-timers": { + "version": "27.2.4", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-27.2.4.tgz", + "integrity": "sha512-cs/TzvwWUM7kAA6Qm/890SK6JJ2pD5RfDNM3SSEom6BmdyV6OiWP1qf/pqo6ts6xwpcM36oN0wSEzcZWc6/B6w==", "dev": true, "requires": { - "@jest/fake-timers": "^26.6.2", - "@jest/types": "^26.6.2", + "@jest/types": "^27.2.4", + "@sinonjs/fake-timers": "^8.0.1", "@types/node": "*", - "jest-mock": "^26.6.2" + "jest-message-util": "^27.2.4", + "jest-mock": "^27.2.4", + "jest-util": "^27.2.4" + } + }, + "@jest/globals": { + "version": "27.2.4", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-27.2.4.tgz", + "integrity": "sha512-DRsRs5dh0i+fA9mGHylTU19+8fhzNJoEzrgsu+zgJoZth3x8/0juCQ8nVVdW1er4Cqifb/ET7/hACYVPD0dBEA==", + "dev": true, + "requires": { + "@jest/environment": "^27.2.4", + "@jest/types": "^27.2.4", + "expect": "^27.2.4" + } + }, + "@jest/reporters": { + "version": "27.2.4", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-27.2.4.tgz", + "integrity": "sha512-LHeSdDnDZkDnJ8kvnjcqV8P1Yv/32yL4d4XfR5gBiy3xGO0onwll1QEbvtW96fIwhx2nejug0GTaEdNDoyr3fQ==", + "dev": true, + "requires": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^27.2.4", + "@jest/test-result": "^27.2.4", + "@jest/transform": "^27.2.4", + "@jest/types": "^27.2.4", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.2", + "graceful-fs": "^4.2.4", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^4.0.3", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.0.2", + "jest-haste-map": "^27.2.4", + "jest-resolve": "^27.2.4", + "jest-util": "^27.2.4", + "jest-worker": "^27.2.4", + "slash": "^3.0.0", + "source-map": "^0.6.0", + "string-length": "^4.0.1", + "terminal-link": "^2.0.0", + "v8-to-istanbul": "^8.1.0" }, "dependencies": { - "@jest/types": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", - "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^15.0.0", - "chalk": "^4.0.0" - } - }, "ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -805,9 +732,9 @@ } }, "chalk": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", - "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "requires": { "ansi-styles": "^4.1.0", @@ -846,33 +773,64 @@ } } }, - "@jest/fake-timers": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-26.6.2.tgz", - "integrity": "sha512-14Uleatt7jdzefLPYM3KLcnUl1ZNikaKq34enpb5XG9i81JpppDb5muZvonvKyrl7ftEHkKS5L5/eB/kxJ+bvA==", + "@jest/source-map": { + "version": "27.0.6", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-27.0.6.tgz", + "integrity": "sha512-Fek4mi5KQrqmlY07T23JRi0e7Z9bXTOOD86V/uS0EIW4PClvPDqZOyFlLpNJheS6QI0FNX1CgmPjtJ4EA/2M+g==", "dev": true, "requires": { - "@jest/types": "^26.6.2", - "@sinonjs/fake-timers": "^6.0.1", - "@types/node": "*", - "jest-message-util": "^26.6.2", - "jest-mock": "^26.6.2", - "jest-util": "^26.6.2" + "callsites": "^3.0.0", + "graceful-fs": "^4.2.4", + "source-map": "^0.6.0" + } + }, + "@jest/test-result": { + "version": "27.2.4", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-27.2.4.tgz", + "integrity": "sha512-eU+PRo0+lIS01b0dTmMdVZ0TtcRSxEaYquZTRFMQz6CvsehGhx9bRzi9Zdw6VROviJyv7rstU+qAMX5pNBmnfQ==", + "dev": true, + "requires": { + "@jest/console": "^27.2.4", + "@jest/types": "^27.2.4", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + } + }, + "@jest/test-sequencer": { + "version": "27.2.4", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-27.2.4.tgz", + "integrity": "sha512-fpk5eknU3/DXE2QCCG1wv/a468+cfPo3Asu6d6yUtM9LOPh709ubZqrhuUOYfM8hXMrIpIdrv1CdCrWWabX0rQ==", + "dev": true, + "requires": { + "@jest/test-result": "^27.2.4", + "graceful-fs": "^4.2.4", + "jest-haste-map": "^27.2.4", + "jest-runtime": "^27.2.4" + } + }, + "@jest/transform": { + "version": "27.2.4", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-27.2.4.tgz", + "integrity": "sha512-n5FlX2TH0oQGwyVDKPxdJ5nI2sO7TJBFe3u3KaAtt7TOiV4yL+Y+rSFDl+Ic5MpbiA/eqXmLAQxjnBmWgS2rEA==", + "dev": true, + "requires": { + "@babel/core": "^7.1.0", + "@jest/types": "^27.2.4", + "babel-plugin-istanbul": "^6.0.0", + "chalk": "^4.0.0", + "convert-source-map": "^1.4.0", + "fast-json-stable-stringify": "^2.0.0", + "graceful-fs": "^4.2.4", + "jest-haste-map": "^27.2.4", + "jest-regex-util": "^27.0.6", + "jest-util": "^27.2.4", + "micromatch": "^4.0.4", + "pirates": "^4.0.1", + "slash": "^3.0.0", + "source-map": "^0.6.1", + "write-file-atomic": "^3.0.0" }, "dependencies": { - "@jest/types": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", - "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^15.0.0", - "chalk": "^4.0.0" - } - }, "ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -883,9 +841,9 @@ } }, "chalk": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", - "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "requires": { "ansi-styles": "^4.1.0", @@ -913,20 +871,6 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, - "jest-util": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-26.6.2.tgz", - "integrity": "sha512-MDW0fKfsn0OI7MS7Euz6h8HNDXVQ0gaM9uW6RjfDmd1DAFcaxX9OqIakHIqhbnmF08Cf2DLDG+ulq8YQQ0Lp0Q==", - "dev": true, - "requires": { - "@jest/types": "^26.6.2", - "@types/node": "*", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.4", - "is-ci": "^2.0.0", - "micromatch": "^4.0.2" - } - }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -938,30 +882,19 @@ } } }, - "@jest/globals": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-26.6.2.tgz", - "integrity": "sha512-85Ltnm7HlB/KesBUuALwQ68YTU72w9H2xW9FjZ1eL1U3lhtefjjl5c2MiUbpXt/i6LaPRvoOFJ22yCBSfQ0JIA==", + "@jest/types": { + "version": "27.2.4", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.2.4.tgz", + "integrity": "sha512-IDO2ezTxeMvQAHxzG/ZvEyA47q0aVfzT95rGFl7bZs/Go0aIucvfDbS2rmnoEdXxlLQhcolmoG/wvL/uKx4tKA==", "dev": true, "requires": { - "@jest/environment": "^26.6.2", - "@jest/types": "^26.6.2", - "expect": "^26.6.2" + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^16.0.0", + "chalk": "^4.0.0" }, "dependencies": { - "@jest/types": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", - "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^15.0.0", - "chalk": "^4.0.0" - } - }, "ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -972,9 +905,9 @@ } }, "chalk": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", - "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "requires": { "ansi-styles": "^4.1.0", @@ -1013,568 +946,600 @@ } } }, - "@jest/reporters": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-26.6.2.tgz", - "integrity": "sha512-h2bW53APG4HvkOnVMo8q3QXa6pcaNt1HkwVsOPMBV6LD/q9oSpxNSYZQYkAnjdMjrJ86UuYeLo+aEZClV6opnw==", + "@nodelib/fs.scandir": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz", + "integrity": "sha512-eGmwYQn3gxo4r7jdQnkrrN6bY478C3P+a/y72IJukF8LjB6ZHeB3c+Ehacj3sYeSmUXGlnA67/PmbM9CVwL7Dw==", "dev": true, "requires": { - "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "^26.6.2", - "@jest/test-result": "^26.6.2", - "@jest/transform": "^26.6.2", - "@jest/types": "^26.6.2", - "chalk": "^4.0.0", - "collect-v8-coverage": "^1.0.0", - "exit": "^0.1.2", - "glob": "^7.1.2", - "graceful-fs": "^4.2.4", - "istanbul-lib-coverage": "^3.0.0", - "istanbul-lib-instrument": "^4.0.3", - "istanbul-lib-report": "^3.0.0", - "istanbul-lib-source-maps": "^4.0.0", - "istanbul-reports": "^3.0.2", - "jest-haste-map": "^26.6.2", - "jest-resolve": "^26.6.2", - "jest-util": "^26.6.2", - "jest-worker": "^26.6.2", - "node-notifier": "^8.0.0", - "slash": "^3.0.0", - "source-map": "^0.6.0", - "string-length": "^4.0.1", - "terminal-link": "^2.0.0", - "v8-to-istanbul": "^7.0.0" - }, - "dependencies": { - "@jest/types": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", - "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^15.0.0", - "chalk": "^4.0.0" - } - }, - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", - "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "jest-util": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-26.6.2.tgz", - "integrity": "sha512-MDW0fKfsn0OI7MS7Euz6h8HNDXVQ0gaM9uW6RjfDmd1DAFcaxX9OqIakHIqhbnmF08Cf2DLDG+ulq8YQQ0Lp0Q==", - "dev": true, - "requires": { - "@jest/types": "^26.6.2", - "@types/node": "*", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.4", - "is-ci": "^2.0.0", - "micromatch": "^4.0.2" - } - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } + "@nodelib/fs.stat": "2.0.3", + "run-parallel": "^1.1.9" } }, - "@jest/source-map": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-26.6.2.tgz", - "integrity": "sha512-YwYcCwAnNmOVsZ8mr3GfnzdXDAl4LaenZP5z+G0c8bzC9/dugL8zRmxZzdoTl4IaS3CryS1uWnROLPFmb6lVvA==", + "@nodelib/fs.stat": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.3.tgz", + "integrity": "sha512-bQBFruR2TAwoevBEd/NWMoAAtNGzTRgdrqnYCc7dhzfoNvqPzLyqlEQnzZ3kVnNrSp25iyxE00/3h2fqGAGArA==", + "dev": true + }, + "@nodelib/fs.walk": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.4.tgz", + "integrity": "sha512-1V9XOY4rDW0rehzbrcqAmHnz8e7SKvX27gh8Gt2WgB0+pdzdiLV83p72kZPU+jvMbS1qU5mauP2iOvO8rhmurQ==", "dev": true, "requires": { - "callsites": "^3.0.0", - "graceful-fs": "^4.2.4", - "source-map": "^0.6.0" + "@nodelib/fs.scandir": "2.1.3", + "fastq": "^1.6.0" } }, - "@jest/test-result": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-26.6.2.tgz", - "integrity": "sha512-5O7H5c/7YlojphYNrK02LlDIV2GNPYisKwHm2QTKjNZeEzezCbwYs9swJySv2UfPMyZ0VdsmMv7jIlD/IKYQpQ==", + "@sinonjs/commons": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.3.tgz", + "integrity": "sha512-xkNcLAn/wZaX14RPlwizcKicDk9G3F8m2nU3L7Ukm5zBgTwiT0wsoFAHx9Jq56fJA1z/7uKGtCRu16sOUCLIHQ==", "dev": true, "requires": { - "@jest/console": "^26.6.2", - "@jest/types": "^26.6.2", - "@types/istanbul-lib-coverage": "^2.0.0", - "collect-v8-coverage": "^1.0.0" + "type-detect": "4.0.8" + } + }, + "@sinonjs/fake-timers": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-8.0.1.tgz", + "integrity": "sha512-AU7kwFxreVd6OAXcAFlKSmZquiRUU0FvYm44k1Y1QbK7Co4m0aqfGMhjykIeQp/H6rcl+nFmj0zfdUcGVs9Dew==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.7.0" + } + }, + "@tootallnate/once": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", + "dev": true + }, + "@types/babel__core": { + "version": "7.1.16", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.16.tgz", + "integrity": "sha512-EAEHtisTMM+KaKwfWdC3oyllIqswlznXCIVCt7/oRNrh+DhgT4UEBNC/jlADNjvw7UnfbcdkGQcPVZ1xYiLcrQ==", + "dev": true, + "requires": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "@types/babel__generator": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.3.tgz", + "integrity": "sha512-/GWCmzJWqV7diQW54smJZzWbSFf4QYtF71WCKhcx6Ru/tFyQIY2eiiITcCAeuPbNSvT9YCGkVMqqvSk2Z0mXiA==", + "dev": true, + "requires": { + "@babel/types": "^7.0.0" + } + }, + "@types/babel__template": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.1.tgz", + "integrity": "sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g==", + "dev": true, + "requires": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "@types/babel__traverse": { + "version": "7.14.2", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.14.2.tgz", + "integrity": "sha512-K2waXdXBi2302XUdcHcR1jCeU0LL4TD9HRs/gk0N2Xvrht+G/BfJa4QObBQZfhMdxiCpV3COl5Nfq4uKTeTnJA==", + "dev": true, + "requires": { + "@babel/types": "^7.3.0" + } + }, + "@types/google.analytics": { + "version": "0.0.40", + "resolved": "https://registry.npmjs.org/@types/google.analytics/-/google.analytics-0.0.40.tgz", + "integrity": "sha512-R3HpnLkqmKxhUAf8kIVvDVGJqPtaaZlW4yowNwjOZUTmYUQEgHh8Nh5wkSXKMroNAuQM8gbXJHmNbbgA8tdb7Q==", + "dev": true + }, + "@types/graceful-fs": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz", + "integrity": "sha512-anKkLmZZ+xm4p8JWBf4hElkM4XR+EZeA2M9BAkkTldmcyDY4mbdIJnRghDJH3Ov5ooY7/UAoENtmdMSkaAd7Cw==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/ioredis": { + "version": "4.17.4", + "resolved": "https://registry.npmjs.org/@types/ioredis/-/ioredis-4.17.4.tgz", + "integrity": "sha512-kb5+thmQJ7HHyOAnCOeqRJlF2fyvadHghnLLLKZzCNyShStJeIQtNGGDjA30gWqj6UFSDAWBfGEMKrFDrGfvzQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/istanbul-lib-coverage": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz", + "integrity": "sha512-sz7iLqvVUg1gIedBOvlkxPlc8/uVzyS5OwGz1cKjXzkl3FpL3al0crU8YGU1WoHkxn0Wxbw5tyi6hvzJKNzFsw==", + "dev": true + }, + "@types/istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "*" + } + }, + "@types/istanbul-reports": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz", + "integrity": "sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==", + "dev": true, + "requires": { + "@types/istanbul-lib-report": "*" + } + }, + "@types/jest": { + "version": "27.0.2", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-27.0.2.tgz", + "integrity": "sha512-4dRxkS/AFX0c5XW6IPMNOydLn2tEhNhJV7DnYK+0bjoJZ+QTmfucBlihX7aoEsh/ocYtkLC73UbnBXBXIxsULA==", + "dev": true, + "requires": { + "jest-diff": "^27.0.0", + "pretty-format": "^27.0.0" + } + }, + "@types/json-schema": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.6.tgz", + "integrity": "sha512-3c+yGKvVP5Y9TYBEibGNR+kLtijnj7mYrXRg+WpFb2X9xm04g/DXYkfg4hmzJQosc9snFNUPkbYIhu+KAm6jJw==", + "dev": true + }, + "@types/lodash": { + "version": "4.14.162", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.162.tgz", + "integrity": "sha512-alvcho1kRUnnD1Gcl4J+hK0eencvzq9rmzvFPRmP5rPHx9VVsJj6bKLTATPVf9ktgv4ujzh7T+XWKp+jhuODig==", + "dev": true + }, + "@types/node": { + "version": "14.14.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.7.tgz", + "integrity": "sha512-Zw1vhUSQZYw+7u5dAwNbIA9TuTotpzY/OF7sJM9FqPOF3SPjKnxrjoTktXDZgUjybf4cWVBP7O8wvKdSaGHweg==", + "dev": true + }, + "@types/object-assign": { + "version": "4.0.30", + "resolved": "https://registry.npmjs.org/@types/object-assign/-/object-assign-4.0.30.tgz", + "integrity": "sha1-iUk3HVqZ9Dge4PHfCpt6GH4H5lI=", + "dev": true + }, + "@types/prettier": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.4.1.tgz", + "integrity": "sha512-Fo79ojj3vdEZOHg3wR9ksAMRz4P3S5fDB5e/YWZiFnyFQI1WY2Vftu9XoXVVtJfxB7Bpce/QTqWSSntkz2Znrw==", + "dev": true + }, + "@types/stack-utils": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", + "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==", + "dev": true + }, + "@types/yargs": { + "version": "16.0.4", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz", + "integrity": "sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==", + "dev": true, + "requires": { + "@types/yargs-parser": "*" + } + }, + "@types/yargs-parser": { + "version": "20.2.1", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-20.2.1.tgz", + "integrity": "sha512-7tFImggNeNBVMsn0vLrpn1H1uPrUBdnARPTpZoitY37ZrdJREzf7I16tMrlK3hen349gr1NYh8CmZQa7CTG6Aw==", + "dev": true + }, + "@typescript-eslint/eslint-plugin": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.2.0.tgz", + "integrity": "sha512-zBNRkzvLSwo6y5TG0DVcmshZIYBHKtmzD4N+LYnfTFpzc4bc79o8jNRSb728WV7A4Cegbs+MV5IRAj8BKBgOVQ==", + "dev": true, + "requires": { + "@typescript-eslint/experimental-utils": "4.2.0", + "@typescript-eslint/scope-manager": "4.2.0", + "debug": "^4.1.1", + "functional-red-black-tree": "^1.0.1", + "regexpp": "^3.0.0", + "semver": "^7.3.2", + "tsutils": "^3.17.1" }, "dependencies": { - "@jest/types": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", - "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", + "@typescript-eslint/scope-manager": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.2.0.tgz", + "integrity": "sha512-Tb402cxxObSxWIVT+PnBp5ruT2V/36yj6gG4C9AjkgRlZpxrLAzWDk3neen6ToMBGeGdxtnfFLoJRUecGz9mYQ==", "dev": true, "requires": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^15.0.0", - "chalk": "^4.0.0" + "@typescript-eslint/types": "4.2.0", + "@typescript-eslint/visitor-keys": "4.2.0" } }, - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "@typescript-eslint/types": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.2.0.tgz", + "integrity": "sha512-xkv5nIsxfI/Di9eVwN+G9reWl7Me9R5jpzmZUch58uQ7g0/hHVuGUbbn4NcxcM5y/R4wuJIIEPKPDb5l4Fdmwg==", + "dev": true + }, + "@typescript-eslint/visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.2.0.tgz", + "integrity": "sha512-WIf4BNOlFOH2W+YqGWa6YKLcK/EB3gEj2apCrqLw6mme1RzBy0jtJ9ewJgnrZDB640zfnv8L+/gwGH5sYp/rGw==", "dev": true, "requires": { - "color-convert": "^2.0.1" + "@typescript-eslint/types": "4.2.0", + "eslint-visitor-keys": "^2.0.0" } }, - "chalk": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", - "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "debug": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz", + "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==", "dev": true, "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "ms": "2.1.2" } }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "semver": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", + "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==", "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } } } }, - "@jest/test-sequencer": { - "version": "26.6.3", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-26.6.3.tgz", - "integrity": "sha512-YHlVIjP5nfEyjlrSr8t/YdNfU/1XEt7c5b4OxcXCjyRhjzLYu/rO69/WHPuYcbCWkz8kAeZVZp2N2+IOLLEPGw==", - "dev": true, - "requires": { - "@jest/test-result": "^26.6.2", - "graceful-fs": "^4.2.4", - "jest-haste-map": "^26.6.2", - "jest-runner": "^26.6.3", - "jest-runtime": "^26.6.3" - } - }, - "@jest/transform": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-26.6.2.tgz", - "integrity": "sha512-E9JjhUgNzvuQ+vVAL21vlyfy12gP0GhazGgJC4h6qUt1jSdUXGWJ1wfu/X7Sd8etSgxV4ovT1pb9v5D6QW4XgA==", + "@typescript-eslint/experimental-utils": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.2.0.tgz", + "integrity": "sha512-5BBj6BjgHEndBaQQpUVzRIPERz03LBc0MCQkHwUaH044FJFL08SwWv/sQftk7gf0ShZ2xZysz0LTwCwNt4Xu3w==", "dev": true, "requires": { - "@babel/core": "^7.1.0", - "@jest/types": "^26.6.2", - "babel-plugin-istanbul": "^6.0.0", - "chalk": "^4.0.0", - "convert-source-map": "^1.4.0", - "fast-json-stable-stringify": "^2.0.0", - "graceful-fs": "^4.2.4", - "jest-haste-map": "^26.6.2", - "jest-regex-util": "^26.0.0", - "jest-util": "^26.6.2", - "micromatch": "^4.0.2", - "pirates": "^4.0.1", - "slash": "^3.0.0", - "source-map": "^0.6.1", - "write-file-atomic": "^3.0.0" + "@types/json-schema": "^7.0.3", + "@typescript-eslint/scope-manager": "4.2.0", + "@typescript-eslint/types": "4.2.0", + "@typescript-eslint/typescript-estree": "4.2.0", + "eslint-scope": "^5.0.0", + "eslint-utils": "^2.0.0" }, "dependencies": { - "@jest/types": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", - "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", + "@typescript-eslint/scope-manager": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.2.0.tgz", + "integrity": "sha512-Tb402cxxObSxWIVT+PnBp5ruT2V/36yj6gG4C9AjkgRlZpxrLAzWDk3neen6ToMBGeGdxtnfFLoJRUecGz9mYQ==", "dev": true, "requires": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^15.0.0", - "chalk": "^4.0.0" + "@typescript-eslint/types": "4.2.0", + "@typescript-eslint/visitor-keys": "4.2.0" } }, - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "@typescript-eslint/types": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.2.0.tgz", + "integrity": "sha512-xkv5nIsxfI/Di9eVwN+G9reWl7Me9R5jpzmZUch58uQ7g0/hHVuGUbbn4NcxcM5y/R4wuJIIEPKPDb5l4Fdmwg==", + "dev": true + }, + "@typescript-eslint/typescript-estree": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.2.0.tgz", + "integrity": "sha512-iWDLCB7z4MGkLipduF6EOotdHNtgxuNKnYD54nMS/oitFnsk4S3S/TE/UYXQTra550lHtlv9eGmp+dvN9pUDtA==", "dev": true, "requires": { - "color-convert": "^2.0.1" + "@typescript-eslint/types": "4.2.0", + "@typescript-eslint/visitor-keys": "4.2.0", + "debug": "^4.1.1", + "globby": "^11.0.1", + "is-glob": "^4.0.1", + "lodash": "^4.17.15", + "semver": "^7.3.2", + "tsutils": "^3.17.1" } }, - "chalk": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", - "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "@typescript-eslint/visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.2.0.tgz", + "integrity": "sha512-WIf4BNOlFOH2W+YqGWa6YKLcK/EB3gEj2apCrqLw6mme1RzBy0jtJ9ewJgnrZDB640zfnv8L+/gwGH5sYp/rGw==", "dev": true, "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "@typescript-eslint/types": "4.2.0", + "eslint-visitor-keys": "^2.0.0" } }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "debug": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz", + "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==", "dev": true, "requires": { - "color-name": "~1.1.4" + "ms": "2.1.2" } }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "semver": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", + "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==", "dev": true - }, - "jest-util": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-26.6.2.tgz", - "integrity": "sha512-MDW0fKfsn0OI7MS7Euz6h8HNDXVQ0gaM9uW6RjfDmd1DAFcaxX9OqIakHIqhbnmF08Cf2DLDG+ulq8YQQ0Lp0Q==", - "dev": true, - "requires": { - "@jest/types": "^26.6.2", - "@types/node": "*", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.4", - "is-ci": "^2.0.0", - "micromatch": "^4.0.2" - } - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } } } }, - "@jest/types": { - "version": "26.3.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.3.0.tgz", - "integrity": "sha512-BDPG23U0qDeAvU4f99haztXwdAg3hz4El95LkAM+tHAqqhiVzRpEGHHU8EDxT/AnxOrA65YjLBwDahdJ9pTLJQ==", + "@typescript-eslint/parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.2.0.tgz", + "integrity": "sha512-54jJ6MwkOtowpE48C0QJF9iTz2/NZxfKVJzv1ha5imigzHbNSLN9yvbxFFH1KdlRPQrlR8qxqyOvLHHxd397VA==", "dev": true, "requires": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^15.0.0", - "chalk": "^4.0.0" + "@typescript-eslint/scope-manager": "4.2.0", + "@typescript-eslint/types": "4.2.0", + "@typescript-eslint/typescript-estree": "4.2.0", + "debug": "^4.1.1" }, "dependencies": { - "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", - "dev": true, - "requires": { - "@types/color-name": "^1.1.1", - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", - "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "debug": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz", + "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==", "dev": true, "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "ms": "2.1.2" } }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "@typescript-eslint/scope-manager": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.2.0.tgz", + "integrity": "sha512-Tb402cxxObSxWIVT+PnBp5ruT2V/36yj6gG4C9AjkgRlZpxrLAzWDk3neen6ToMBGeGdxtnfFLoJRUecGz9mYQ==", + "dev": true, + "requires": { + "@typescript-eslint/types": "4.2.0", + "@typescript-eslint/visitor-keys": "4.2.0" + } + }, + "@typescript-eslint/types": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.2.0.tgz", + "integrity": "sha512-xkv5nIsxfI/Di9eVwN+G9reWl7Me9R5jpzmZUch58uQ7g0/hHVuGUbbn4NcxcM5y/R4wuJIIEPKPDb5l4Fdmwg==", + "dev": true + }, + "@typescript-eslint/typescript-estree": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.2.0.tgz", + "integrity": "sha512-iWDLCB7z4MGkLipduF6EOotdHNtgxuNKnYD54nMS/oitFnsk4S3S/TE/UYXQTra550lHtlv9eGmp+dvN9pUDtA==", + "dev": true, + "requires": { + "@typescript-eslint/types": "4.2.0", + "@typescript-eslint/visitor-keys": "4.2.0", + "debug": "^4.1.1", + "globby": "^11.0.1", + "is-glob": "^4.0.1", + "lodash": "^4.17.15", + "semver": "^7.3.2", + "tsutils": "^3.17.1" + }, + "dependencies": { + "debug": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz", + "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==", "dev": true, "requires": { - "color-name": "~1.1.4" + "ms": "2.1.2" } }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "semver": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", + "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==", "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } } } }, - "@nodelib/fs.scandir": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz", - "integrity": "sha512-eGmwYQn3gxo4r7jdQnkrrN6bY478C3P+a/y72IJukF8LjB6ZHeB3c+Ehacj3sYeSmUXGlnA67/PmbM9CVwL7Dw==", + "@typescript-eslint/visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.2.0.tgz", + "integrity": "sha512-WIf4BNOlFOH2W+YqGWa6YKLcK/EB3gEj2apCrqLw6mme1RzBy0jtJ9ewJgnrZDB640zfnv8L+/gwGH5sYp/rGw==", "dev": true, "requires": { - "@nodelib/fs.stat": "2.0.3", - "run-parallel": "^1.1.9" + "@typescript-eslint/types": "4.2.0", + "eslint-visitor-keys": "^2.0.0" } }, - "@nodelib/fs.stat": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.3.tgz", - "integrity": "sha512-bQBFruR2TAwoevBEd/NWMoAAtNGzTRgdrqnYCc7dhzfoNvqPzLyqlEQnzZ3kVnNrSp25iyxE00/3h2fqGAGArA==", + "abab": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.5.tgz", + "integrity": "sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q==", "dev": true }, - "@nodelib/fs.walk": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.4.tgz", - "integrity": "sha512-1V9XOY4rDW0rehzbrcqAmHnz8e7SKvX27gh8Gt2WgB0+pdzdiLV83p72kZPU+jvMbS1qU5mauP2iOvO8rhmurQ==", + "acorn": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.0.tgz", + "integrity": "sha512-+G7P8jJmCHr+S+cLfQxygbWhXy+8YTVGzAkpEbcLo2mLoL7tij/VG41QSHACSf5QgYRhMZYHuNc6drJaO0Da+w==", + "dev": true + }, + "acorn-globals": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-6.0.0.tgz", + "integrity": "sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg==", "dev": true, "requires": { - "@nodelib/fs.scandir": "2.1.3", - "fastq": "^1.6.0" + "acorn": "^7.1.1", + "acorn-walk": "^7.1.1" } }, - "@sinonjs/commons": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.2.tgz", - "integrity": "sha512-sruwd86RJHdsVf/AtBoijDmUqJp3B6hF/DGC23C+JaegnDHaZyewCjoVGTdg3J0uz3Zs7NnIT05OBOmML72lQw==", + "acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true + }, + "acorn-walk": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", + "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==", + "dev": true + }, + "agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", "dev": true, "requires": { - "type-detect": "4.0.8" + "debug": "4" } }, - "@sinonjs/fake-timers": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-6.0.1.tgz", - "integrity": "sha512-MZPUxrmFubI36XS1DI3qmI0YdN1gks62JtFZvxR67ljjSNCeK6U08Zx4msEWOXuofgqUt6zPHSi1H9fbjR/NRA==", + "ajv": { + "version": "6.12.4", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.4.tgz", + "integrity": "sha512-eienB2c9qVQs2KWexhkrdMLVDoIQCz5KSeLxwg9Lzk4DOfBtIK9PQwwufcsn1jjGuf9WZmqPMbGxOzfcuphJCQ==", "dev": true, "requires": { - "@sinonjs/commons": "^1.7.0" + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" } }, - "@types/babel__core": { - "version": "7.1.14", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.14.tgz", - "integrity": "sha512-zGZJzzBUVDo/eV6KgbE0f0ZI7dInEYvo12Rb70uNQDshC3SkRMb67ja0GgRHZgAX3Za6rhaWlvbDO8rrGyAb1g==", + "ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "dev": true + }, + "ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", "dev": true, "requires": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" + "type-fest": "^0.21.3" } }, - "@types/babel__generator": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.2.tgz", - "integrity": "sha512-MdSJnBjl+bdwkLskZ3NGFp9YcXGx5ggLpQQPqtgakVhsWK0hTtNYhjpZLlWQTviGTvF8at+Bvli3jV7faPdgeQ==", + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "dev": true, "requires": { - "@babel/types": "^7.0.0" + "color-convert": "^1.9.0" } }, - "@types/babel__template": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.0.tgz", - "integrity": "sha512-NTPErx4/FiPCGScH7foPyr+/1Dkzkni+rHiYHHoTjvwou7AQzJkNeD60A9CXRy+ZEN2B1bggmkTMCDb+Mv5k+A==", + "anymatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", "dev": true, "requires": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" } }, - "@types/babel__traverse": { - "version": "7.11.1", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.11.1.tgz", - "integrity": "sha512-Vs0hm0vPahPMYi9tDjtP66llufgO3ST16WXaSTtDGEl9cewAl3AibmxWw6TINOqHPT9z0uABKAYjT9jNSg4npw==", + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", "dev": true, "requires": { - "@babel/types": "^7.3.0" + "sprintf-js": "~1.0.2" } }, - "@types/color-name": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", - "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", + "array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", "dev": true }, - "@types/google.analytics": { - "version": "0.0.40", - "resolved": "https://registry.npmjs.org/@types/google.analytics/-/google.analytics-0.0.40.tgz", - "integrity": "sha512-R3HpnLkqmKxhUAf8kIVvDVGJqPtaaZlW4yowNwjOZUTmYUQEgHh8Nh5wkSXKMroNAuQM8gbXJHmNbbgA8tdb7Q==", + "ast-metadata-inferer": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/ast-metadata-inferer/-/ast-metadata-inferer-0.2.0.tgz", + "integrity": "sha512-6yPph2NeCHNxoI/ZmjklYaLOSZDAx+0L0+wsXnF56FxmjxvUlYZSWcj1KXtXO8IufruQTzVFOjg1+IzdDazSPg==", "dev": true }, - "@types/graceful-fs": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz", - "integrity": "sha512-anKkLmZZ+xm4p8JWBf4hElkM4XR+EZeA2M9BAkkTldmcyDY4mbdIJnRghDJH3Ov5ooY7/UAoENtmdMSkaAd7Cw==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/ioredis": { - "version": "4.17.4", - "resolved": "https://registry.npmjs.org/@types/ioredis/-/ioredis-4.17.4.tgz", - "integrity": "sha512-kb5+thmQJ7HHyOAnCOeqRJlF2fyvadHghnLLLKZzCNyShStJeIQtNGGDjA30gWqj6UFSDAWBfGEMKrFDrGfvzQ==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/istanbul-lib-coverage": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz", - "integrity": "sha512-sz7iLqvVUg1gIedBOvlkxPlc8/uVzyS5OwGz1cKjXzkl3FpL3al0crU8YGU1WoHkxn0Wxbw5tyi6hvzJKNzFsw==", + "astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", "dev": true }, - "@types/istanbul-lib-report": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", - "integrity": "sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "*" - } - }, - "@types/istanbul-reports": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz", - "integrity": "sha512-nwKNbvnwJ2/mndE9ItP/zc2TCzw6uuodnF4EHYWD+gCQDVBuRQL5UzbZD0/ezy1iKsFU2ZQiDqg4M9dN4+wZgA==", - "dev": true, - "requires": { - "@types/istanbul-lib-report": "*" - } + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "dev": true }, - "@types/jest": { - "version": "26.0.10", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-26.0.10.tgz", - "integrity": "sha512-i2m0oyh8w/Lum7wWK/YOZJakYF8Mx08UaKA1CtbmFeDquVhAEdA7znacsVSf2hJ1OQ/OfVMGN90pw/AtzF8s/Q==", + "babel-jest": { + "version": "27.2.4", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-27.2.4.tgz", + "integrity": "sha512-f24OmxyWymk5jfgLdlCMu4fTs4ldxFBIdn5sJdhvGC1m08rSkJ5hYbWkNmfBSvE/DjhCVNSHXepxsI6THGfGsg==", "dev": true, "requires": { - "jest-diff": "^25.2.1", - "pretty-format": "^25.2.1" + "@jest/transform": "^27.2.4", + "@jest/types": "^27.2.4", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.0.0", + "babel-preset-jest": "^27.2.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.4", + "slash": "^3.0.0" }, "dependencies": { - "@jest/types": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-25.5.0.tgz", - "integrity": "sha512-OXD0RgQ86Tu3MazKo8bnrkDRaDXXMGUqd+kTtLtK1Zb7CRzQcaSRPPPV37SvYTdevXEBVxe0HXylEjs8ibkmCw==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^1.1.1", - "@types/yargs": "^15.0.0", - "chalk": "^3.0.0" - } - }, - "@types/istanbul-reports": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.2.tgz", - "integrity": "sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "*", - "@types/istanbul-lib-report": "*" - } - }, "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "requires": { - "@types/color-name": "^1.1.1", "color-convert": "^2.0.1" } }, "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "requires": { "ansi-styles": "^4.1.0", @@ -1596,48 +1561,12 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, - "diff-sequences": { - "version": "25.2.6", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-25.2.6.tgz", - "integrity": "sha512-Hq8o7+6GaZeoFjtpgvRBUknSXNeJiCx7V9Fr94ZMljNiCr9n9L8H8aJqgWOQiDDGdyn29fRNcDdRVJ5fdyihfg==", - "dev": true - }, "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, - "jest-diff": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-25.5.0.tgz", - "integrity": "sha512-z1kygetuPiREYdNIumRpAHY6RXiGmp70YHptjdaxTWGmA085W3iCnXNx0DhflK3vwrKmrRWyY1wUpkPMVxMK7A==", - "dev": true, - "requires": { - "chalk": "^3.0.0", - "diff-sequences": "^25.2.6", - "jest-get-type": "^25.2.6", - "pretty-format": "^25.5.0" - } - }, - "jest-get-type": { - "version": "25.2.6", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-25.2.6.tgz", - "integrity": "sha512-DxjtyzOHjObRM+sM1knti6or+eOgcGU4xVSb2HNP1TqO4ahsT+rqZg+nyqHWJSvWgKC5cG3QjGFBqxLghiF/Ig==", - "dev": true - }, - "pretty-format": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-25.5.0.tgz", - "integrity": "sha512-kbo/kq2LQ/A/is0PQwsEHM7Ca6//bGPPvU6UnsdDRSKTWxT/ru/xb88v4BJf6a69H+uTytOEsTusT9ksd/1iWQ==", - "dev": true, - "requires": { - "@jest/types": "^25.5.0", - "ansi-regex": "^5.0.0", - "ansi-styles": "^4.0.0", - "react-is": "^16.12.0" - } - }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -1649,517 +1578,562 @@ } } }, - "@types/json-schema": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.6.tgz", - "integrity": "sha512-3c+yGKvVP5Y9TYBEibGNR+kLtijnj7mYrXRg+WpFb2X9xm04g/DXYkfg4hmzJQosc9snFNUPkbYIhu+KAm6jJw==", - "dev": true - }, - "@types/lodash": { - "version": "4.14.162", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.162.tgz", - "integrity": "sha512-alvcho1kRUnnD1Gcl4J+hK0eencvzq9rmzvFPRmP5rPHx9VVsJj6bKLTATPVf9ktgv4ujzh7T+XWKp+jhuODig==", - "dev": true - }, - "@types/node": { - "version": "14.14.7", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.7.tgz", - "integrity": "sha512-Zw1vhUSQZYw+7u5dAwNbIA9TuTotpzY/OF7sJM9FqPOF3SPjKnxrjoTktXDZgUjybf4cWVBP7O8wvKdSaGHweg==", - "dev": true - }, - "@types/normalize-package-data": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz", - "integrity": "sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA==", - "dev": true - }, - "@types/object-assign": { - "version": "4.0.30", - "resolved": "https://registry.npmjs.org/@types/object-assign/-/object-assign-4.0.30.tgz", - "integrity": "sha1-iUk3HVqZ9Dge4PHfCpt6GH4H5lI=", - "dev": true - }, - "@types/prettier": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.2.3.tgz", - "integrity": "sha512-PijRCG/K3s3w1We6ynUKdxEc5AcuuH3NBmMDP8uvKVp6X43UY7NQlTzczakXP3DJR0F4dfNQIGjU2cUeRYs2AA==", - "dev": true - }, - "@types/stack-utils": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.0.tgz", - "integrity": "sha512-RJJrrySY7A8havqpGObOB4W92QXKJo63/jFLLgpvOtsGUqbQZ9Sbgl35KMm1DjC6j7AvmmU2bIno+3IyEaemaw==", - "dev": true + "babel-plugin-istanbul": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.0.0.tgz", + "integrity": "sha512-AF55rZXpe7trmEylbaE1Gv54wn6rwU03aptvRoVIGP8YykoSxqdVLV1TfwflBCE/QtHmqtP8SWlTENqbK8GCSQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^4.0.0", + "test-exclude": "^6.0.0" + } }, - "@types/yargs": { - "version": "15.0.5", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.5.tgz", - "integrity": "sha512-Dk/IDOPtOgubt/IaevIUbTgV7doaKkoorvOyYM2CMwuDyP89bekI7H4xLIwunNYiK9jhCkmc6pUrJk3cj2AB9w==", + "babel-plugin-jest-hoist": { + "version": "27.2.0", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-27.2.0.tgz", + "integrity": "sha512-TOux9khNKdi64mW+0OIhcmbAn75tTlzKhxmiNXevQaPbrBYK7YKjP1jl6NHTJ6XR5UgUrJbCnWlKVnJn29dfjw==", "dev": true, "requires": { - "@types/yargs-parser": "*" + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.0.0", + "@types/babel__traverse": "^7.0.6" } }, - "@types/yargs-parser": { - "version": "15.0.0", - "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-15.0.0.tgz", - "integrity": "sha512-FA/BWv8t8ZWJ+gEOnLLd8ygxH/2UFbAvgEonyfN6yWGLKc7zVjbpl2Y4CTjid9h2RfgPP6SEt6uHwEOply00yw==", - "dev": true + "babel-preset-current-node-syntax": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz", + "integrity": "sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==", + "dev": true, + "requires": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.8.3", + "@babel/plugin-syntax-import-meta": "^7.8.3", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.8.3", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-top-level-await": "^7.8.3" + } }, - "@typescript-eslint/eslint-plugin": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.2.0.tgz", - "integrity": "sha512-zBNRkzvLSwo6y5TG0DVcmshZIYBHKtmzD4N+LYnfTFpzc4bc79o8jNRSb728WV7A4Cegbs+MV5IRAj8BKBgOVQ==", + "babel-preset-jest": { + "version": "27.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-27.2.0.tgz", + "integrity": "sha512-z7MgQ3peBwN5L5aCqBKnF6iqdlvZvFUQynEhu0J+X9nHLU72jO3iY331lcYrg+AssJ8q7xsv5/3AICzVmJ/wvg==", "dev": true, "requires": { - "@typescript-eslint/experimental-utils": "4.2.0", - "@typescript-eslint/scope-manager": "4.2.0", - "debug": "^4.1.1", - "functional-red-black-tree": "^1.0.1", - "regexpp": "^3.0.0", - "semver": "^7.3.2", - "tsutils": "^3.17.1" - }, - "dependencies": { - "@typescript-eslint/scope-manager": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.2.0.tgz", - "integrity": "sha512-Tb402cxxObSxWIVT+PnBp5ruT2V/36yj6gG4C9AjkgRlZpxrLAzWDk3neen6ToMBGeGdxtnfFLoJRUecGz9mYQ==", - "dev": true, - "requires": { - "@typescript-eslint/types": "4.2.0", - "@typescript-eslint/visitor-keys": "4.2.0" - } - }, - "@typescript-eslint/types": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.2.0.tgz", - "integrity": "sha512-xkv5nIsxfI/Di9eVwN+G9reWl7Me9R5jpzmZUch58uQ7g0/hHVuGUbbn4NcxcM5y/R4wuJIIEPKPDb5l4Fdmwg==", - "dev": true - }, - "@typescript-eslint/visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.2.0.tgz", - "integrity": "sha512-WIf4BNOlFOH2W+YqGWa6YKLcK/EB3gEj2apCrqLw6mme1RzBy0jtJ9ewJgnrZDB640zfnv8L+/gwGH5sYp/rGw==", - "dev": true, - "requires": { - "@typescript-eslint/types": "4.2.0", - "eslint-visitor-keys": "^2.0.0" - } - }, - "debug": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz", - "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "semver": { - "version": "7.3.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", - "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==", - "dev": true - } + "babel-plugin-jest-hoist": "^27.2.0", + "babel-preset-current-node-syntax": "^1.0.0" } }, - "@typescript-eslint/experimental-utils": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.2.0.tgz", - "integrity": "sha512-5BBj6BjgHEndBaQQpUVzRIPERz03LBc0MCQkHwUaH044FJFL08SwWv/sQftk7gf0ShZ2xZysz0LTwCwNt4Xu3w==", + "babel-runtime": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", + "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", "dev": true, "requires": { - "@types/json-schema": "^7.0.3", - "@typescript-eslint/scope-manager": "4.2.0", - "@typescript-eslint/types": "4.2.0", - "@typescript-eslint/typescript-estree": "4.2.0", - "eslint-scope": "^5.0.0", - "eslint-utils": "^2.0.0" + "core-js": "^2.4.0", + "regenerator-runtime": "^0.11.0" }, "dependencies": { - "@typescript-eslint/scope-manager": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.2.0.tgz", - "integrity": "sha512-Tb402cxxObSxWIVT+PnBp5ruT2V/36yj6gG4C9AjkgRlZpxrLAzWDk3neen6ToMBGeGdxtnfFLoJRUecGz9mYQ==", - "dev": true, - "requires": { - "@typescript-eslint/types": "4.2.0", - "@typescript-eslint/visitor-keys": "4.2.0" - } - }, - "@typescript-eslint/types": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.2.0.tgz", - "integrity": "sha512-xkv5nIsxfI/Di9eVwN+G9reWl7Me9R5jpzmZUch58uQ7g0/hHVuGUbbn4NcxcM5y/R4wuJIIEPKPDb5l4Fdmwg==", - "dev": true - }, - "@typescript-eslint/typescript-estree": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.2.0.tgz", - "integrity": "sha512-iWDLCB7z4MGkLipduF6EOotdHNtgxuNKnYD54nMS/oitFnsk4S3S/TE/UYXQTra550lHtlv9eGmp+dvN9pUDtA==", - "dev": true, - "requires": { - "@typescript-eslint/types": "4.2.0", - "@typescript-eslint/visitor-keys": "4.2.0", - "debug": "^4.1.1", - "globby": "^11.0.1", - "is-glob": "^4.0.1", - "lodash": "^4.17.15", - "semver": "^7.3.2", - "tsutils": "^3.17.1" - } - }, - "@typescript-eslint/visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.2.0.tgz", - "integrity": "sha512-WIf4BNOlFOH2W+YqGWa6YKLcK/EB3gEj2apCrqLw6mme1RzBy0jtJ9ewJgnrZDB640zfnv8L+/gwGH5sYp/rGw==", - "dev": true, - "requires": { - "@typescript-eslint/types": "4.2.0", - "eslint-visitor-keys": "^2.0.0" - } - }, - "debug": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz", - "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "core-js": { + "version": "2.6.11", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.11.tgz", + "integrity": "sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg==", "dev": true }, - "semver": { - "version": "7.3.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", - "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==", + "regenerator-runtime": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", + "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==", "dev": true } } }, - "@typescript-eslint/parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.2.0.tgz", - "integrity": "sha512-54jJ6MwkOtowpE48C0QJF9iTz2/NZxfKVJzv1ha5imigzHbNSLN9yvbxFFH1KdlRPQrlR8qxqyOvLHHxd397VA==", + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, "requires": { - "@typescript-eslint/scope-manager": "4.2.0", - "@typescript-eslint/types": "4.2.0", - "@typescript-eslint/typescript-estree": "4.2.0", - "debug": "^4.1.1" - }, - "dependencies": { - "debug": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz", - "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" } }, - "@typescript-eslint/scope-manager": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.2.0.tgz", - "integrity": "sha512-Tb402cxxObSxWIVT+PnBp5ruT2V/36yj6gG4C9AjkgRlZpxrLAzWDk3neen6ToMBGeGdxtnfFLoJRUecGz9mYQ==", + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", "dev": true, "requires": { - "@typescript-eslint/types": "4.2.0", - "@typescript-eslint/visitor-keys": "4.2.0" + "fill-range": "^7.0.1" } }, - "@typescript-eslint/types": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.2.0.tgz", - "integrity": "sha512-xkv5nIsxfI/Di9eVwN+G9reWl7Me9R5jpzmZUch58uQ7g0/hHVuGUbbn4NcxcM5y/R4wuJIIEPKPDb5l4Fdmwg==", + "browser-process-hrtime": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz", + "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==", "dev": true }, - "@typescript-eslint/typescript-estree": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.2.0.tgz", - "integrity": "sha512-iWDLCB7z4MGkLipduF6EOotdHNtgxuNKnYD54nMS/oitFnsk4S3S/TE/UYXQTra550lHtlv9eGmp+dvN9pUDtA==", + "browserslist": { + "version": "4.16.6", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.6.tgz", + "integrity": "sha512-Wspk/PqO+4W9qp5iUTJsa1B/QrYn1keNCcEP5OvP7WBwT4KaDly0uONYmC6Xa3Z5IqnUgS0KcgLYu1l74x0ZXQ==", "dev": true, "requires": { - "@typescript-eslint/types": "4.2.0", - "@typescript-eslint/visitor-keys": "4.2.0", - "debug": "^4.1.1", - "globby": "^11.0.1", - "is-glob": "^4.0.1", - "lodash": "^4.17.15", - "semver": "^7.3.2", - "tsutils": "^3.17.1" - }, - "dependencies": { - "debug": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz", - "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "semver": { - "version": "7.3.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", - "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==", - "dev": true - } + "caniuse-lite": "^1.0.30001219", + "colorette": "^1.2.2", + "electron-to-chromium": "^1.3.723", + "escalade": "^3.1.1", + "node-releases": "^1.1.71" } }, - "@typescript-eslint/visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.2.0.tgz", - "integrity": "sha512-WIf4BNOlFOH2W+YqGWa6YKLcK/EB3gEj2apCrqLw6mme1RzBy0jtJ9ewJgnrZDB640zfnv8L+/gwGH5sYp/rGw==", + "bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", "dev": true, "requires": { - "@typescript-eslint/types": "4.2.0", - "eslint-visitor-keys": "^2.0.0" + "fast-json-stable-stringify": "2.x" } }, - "abab": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.5.tgz", - "integrity": "sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q==", - "dev": true - }, - "acorn": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.0.tgz", - "integrity": "sha512-+G7P8jJmCHr+S+cLfQxygbWhXy+8YTVGzAkpEbcLo2mLoL7tij/VG41QSHACSf5QgYRhMZYHuNc6drJaO0Da+w==", - "dev": true - }, - "acorn-globals": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-6.0.0.tgz", - "integrity": "sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg==", + "bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", "dev": true, "requires": { - "acorn": "^7.1.1", - "acorn-walk": "^7.1.1" + "node-int64": "^0.4.0" } }, - "acorn-jsx": { + "buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + }, + "camelcase": { "version": "5.3.1", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.1.tgz", - "integrity": "sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng==", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", "dev": true }, - "acorn-walk": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", - "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==", + "caniuse-db": { + "version": "1.0.30001120", + "resolved": "https://registry.npmjs.org/caniuse-db/-/caniuse-db-1.0.30001120.tgz", + "integrity": "sha512-DvFSRBZYuPj2976yroUk/vqu45nvbf70+qJLH7q+q3S9IOXY4Wl/q7nWOJX8mq6SjWQzCWbz0J0Q6UjsuYwd2A==", "dev": true }, - "ajv": { - "version": "6.12.4", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.4.tgz", - "integrity": "sha512-eienB2c9qVQs2KWexhkrdMLVDoIQCz5KSeLxwg9Lzk4DOfBtIK9PQwwufcsn1jjGuf9WZmqPMbGxOzfcuphJCQ==", + "caniuse-lite": { + "version": "1.0.30001247", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001247.tgz", + "integrity": "sha512-4rS7co+7+AoOSPRPOPUt5/GdaqZc0EsUpWk66ofE3HJTAajUK2Ss2VwoNzVN69ghg8lYYlh0an0Iy4LIHHo9UQ==", + "dev": true + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "dev": true, "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" } }, - "ansi-colors": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", - "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", "dev": true }, - "ansi-escapes": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", - "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "ci-info": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.2.0.tgz", + "integrity": "sha512-dVqRX7fLUm8J6FgHJ418XuIgDLZDkYcDFTeL6TA2gt5WlIZUQrrH6EZrNClwT/H0FateUsZkGIOPRrLbP+PR9A==", + "dev": true + }, + "cjs-module-lexer": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz", + "integrity": "sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA==", + "dev": true + }, + "cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", "dev": true, "requires": { - "type-fest": "^0.21.3" - }, - "dependencies": { - "type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", - "dev": true - } + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" } }, - "ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "cluster-key-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.0.tgz", + "integrity": "sha512-2Nii8p3RwAPiFwsnZvukotvow2rIHM+yQ6ZcBXGHdniadkYGZYiGmkHJIbZPIV9nfv7m/U1IPMVVcAhoWFeklw==", "dev": true }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", + "dev": true + }, + "collect-v8-coverage": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz", + "integrity": "sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg==", + "dev": true + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", "dev": true, "requires": { - "color-convert": "^1.9.0" + "color-name": "1.1.3" } }, - "anymatch": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", - "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "colorette": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.2.tgz", + "integrity": "sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w==", + "dev": true + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", "dev": true, "requires": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" + "delayed-stream": "~1.0.0" } }, - "argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "convert-source-map": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.8.0.tgz", + "integrity": "sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==", "dev": true, "requires": { - "sprintf-js": "~1.0.2" + "safe-buffer": "~5.1.1" } }, - "arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", + "core-js": { + "version": "3.6.5", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.6.5.tgz", + "integrity": "sha512-vZVEEwZoIsI+vPEuoF9Iqf5H7/M3eeQqWlQnYa8FSKKePuYTf5MWnxb5SDAzCa60b3JBRS5g9b+Dq7b1y/RCrA==", "dev": true }, - "arr-flatten": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", - "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", "dev": true }, - "arr-union": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", - "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", - "dev": true + "cross-env": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.2.tgz", + "integrity": "sha512-KZP/bMEOJEDCkDQAyRhu3RL2ZO/SUVrxQVI0G3YEQ+OLbRA3c6zgixe8Mq8a/z7+HKlNEjo8oiLUs8iRijY2Rw==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.1" + } }, - "array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } }, - "array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", + "cssom": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz", + "integrity": "sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw==", "dev": true }, - "asn1": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", - "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "cssstyle": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", + "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", "dev": true, "requires": { - "safer-buffer": "~2.1.0" + "cssom": "~0.3.6" + }, + "dependencies": { + "cssom": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", + "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", + "dev": true + } } }, - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", - "dev": true - }, - "assign-symbols": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", - "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", - "dev": true + "csv-streamify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/csv-streamify/-/csv-streamify-4.0.0.tgz", + "integrity": "sha512-d+QZf8WaKwn/8/dJDyN0TFkmSH0fHBFJSyz+ujC2a5sGvXgxEj3zBwqx/mLoPMJ+YbI9H7n3h5YL4uAHUj731w==", + "dev": true, + "requires": { + "through2": "2.0.3" + } }, - "ast-metadata-inferer": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/ast-metadata-inferer/-/ast-metadata-inferer-0.2.0.tgz", - "integrity": "sha512-6yPph2NeCHNxoI/ZmjklYaLOSZDAx+0L0+wsXnF56FxmjxvUlYZSWcj1KXtXO8IufruQTzVFOjg1+IzdDazSPg==", - "dev": true + "data-urls": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-2.0.0.tgz", + "integrity": "sha512-X5eWTSXO/BJmpdIKCRuKUgSCgAN0OwliVK3yPKbwIWU1Tdw5BRajxlzMidvh+gwko9AfQ9zIj52pzF91Q3YAvQ==", + "dev": true, + "requires": { + "abab": "^2.0.3", + "whatwg-mimetype": "^2.3.0", + "whatwg-url": "^8.0.0" + } }, - "astral-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", - "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", - "dev": true + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "decimal.js": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.3.1.tgz", + "integrity": "sha512-V0pfhfr8suzyPGOx3nmq4aHqabehUZn6Ch9kyFpV79TGDTWFmHqUqXdabR7QHqxzrYolF4+tVmJhUG4OURg5dQ==", "dev": true }, - "atob": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", - "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", + "dedent": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", + "integrity": "sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw=", "dev": true }, - "aws-sign2": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", + "deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", "dev": true }, - "aws4": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz", - "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==", + "deepmerge": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", + "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==", "dev": true }, - "babel-jest": { - "version": "26.6.3", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-26.6.3.tgz", - "integrity": "sha512-pl4Q+GAVOHwvjrck6jKjvmGhnO3jHX/xuB9d27f+EJZ/6k+6nMuPjorrYp7s++bKKdANwzElBWnLWaObvTnaZA==", + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "dev": true + }, + "denque": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-1.5.0.tgz", + "integrity": "sha512-CYiCSgIF1p6EUByQPlGkKnP1M9g0ZV3qMIrqMqZqdwazygIA/YP2vrbcyl1h/WppKJTdl1F85cXIle+394iDAQ==", + "dev": true + }, + "detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true + }, + "diff-sequences": { + "version": "27.0.6", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-27.0.6.tgz", + "integrity": "sha512-ag6wfpBFyNXZ0p8pcuIDS//D8H062ZQJ3fzYxjpmeKjnz8W4pekL3AI8VohmyZmsWW2PWaHgjsmqR6L13101VQ==", + "dev": true + }, + "dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", "dev": true, "requires": { - "@jest/transform": "^26.6.2", - "@jest/types": "^26.6.2", - "@types/babel__core": "^7.1.7", - "babel-plugin-istanbul": "^6.0.0", - "babel-preset-jest": "^26.6.2", + "path-type": "^4.0.0" + } + }, + "doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "domexception": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-2.0.1.tgz", + "integrity": "sha512-yxJ2mFy/sibVQlu5qHjOkf9J3K6zgmCxgJ94u2EdvDOV09H+32LtRswEcUsmUWN72pVLOEnTSRaIVVzVQgS0dg==", + "dev": true, + "requires": { + "webidl-conversions": "^5.0.0" + }, + "dependencies": { + "webidl-conversions": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz", + "integrity": "sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==", + "dev": true + } + } + }, + "electron-to-chromium": { + "version": "1.3.788", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.788.tgz", + "integrity": "sha512-dbMIpX4E4/Gk4gzOh1GYS7ls1vGsByWKpIqLviJi1mSmSt5BvrWLLtSqpFE5BaC7Ef4NnI0GMaiddNX2Brw6zA==", + "dev": true + }, + "emittery": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.8.1.tgz", + "integrity": "sha512-uDfvUjVrfGJJhymx/kz6prltenw1u7WrCg1oa94zYY8xxVpLLUu045LAT0dhDZdXG58/EpPL/5kA180fQ/qudg==", + "dev": true + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "enquirer": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", + "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", + "dev": true, + "requires": { + "ansi-colors": "^4.1.1" + } + }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "escodegen": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.0.0.tgz", + "integrity": "sha512-mmHKys/C8BFUGI+MAWNcSYoORYLMdPzjrknd2Vc+bUsjN5bXcr8EhrNB+UTqfL1y3I9c4fw2ihgtMPQLBRiQxw==", + "dev": true, + "requires": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1", + "source-map": "~0.6.1" + }, + "dependencies": { + "estraverse": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", + "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", + "dev": true + } + } + }, + "eslint": { + "version": "7.32.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.32.0.tgz", + "integrity": "sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA==", + "dev": true, + "requires": { + "@babel/code-frame": "7.12.11", + "@eslint/eslintrc": "^0.4.3", + "@humanwhocodes/config-array": "^0.5.0", + "ajv": "^6.10.0", "chalk": "^4.0.0", - "graceful-fs": "^4.2.4", - "slash": "^3.0.0" + "cross-spawn": "^7.0.2", + "debug": "^4.0.1", + "doctrine": "^3.0.0", + "enquirer": "^2.3.5", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^5.1.1", + "eslint-utils": "^2.1.0", + "eslint-visitor-keys": "^2.0.0", + "espree": "^7.3.1", + "esquery": "^1.4.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^5.1.2", + "globals": "^13.6.0", + "ignore": "^4.0.6", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "js-yaml": "^3.13.1", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.0.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.1", + "progress": "^2.0.0", + "regexpp": "^3.1.0", + "semver": "^7.2.1", + "strip-ansi": "^6.0.0", + "strip-json-comments": "^3.1.0", + "table": "^6.0.9", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" }, "dependencies": { - "@jest/types": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", - "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", + "@babel/code-frame": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz", + "integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==", "dev": true, "requires": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^15.0.0", - "chalk": "^4.0.0" + "@babel/highlight": "^7.10.4" } }, "ansi-styles": { @@ -2172,9 +2146,9 @@ } }, "chalk": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", - "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "requires": { "ansi-styles": "^4.1.0", @@ -2196,12 +2170,72 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true + }, + "globals": { + "version": "13.11.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.11.0.tgz", + "integrity": "sha512-08/xrJ7wQjK9kkkRoI3OFUBbLx4f+6x3SGwcPvQ0QH6goFDrOU2oyAWrmh3dJezu65buo+HBMzAMQy6rovVC3g==", + "dev": true, + "requires": { + "type-fest": "^0.20.2" + } + }, "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, + "ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true + }, + "levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + } + }, + "optionator": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", + "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "dev": true, + "requires": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.3" + } + }, + "prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true + }, + "semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -2210,2588 +2244,789 @@ "requires": { "has-flag": "^4.0.0" } + }, + "type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1" + } + }, + "type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true } } }, - "babel-plugin-istanbul": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.0.0.tgz", - "integrity": "sha512-AF55rZXpe7trmEylbaE1Gv54wn6rwU03aptvRoVIGP8YykoSxqdVLV1TfwflBCE/QtHmqtP8SWlTENqbK8GCSQ==", + "eslint-plugin-compat": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-compat/-/eslint-plugin-compat-3.7.0.tgz", + "integrity": "sha512-A3uzSYqUjNj6rMyaBuU3l8wSCadZjeZRZ7WF3eU9vUT0JItiqRysjmYELkHHCpH8l7wRprUu4MZPr37lFCw7iA==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@istanbuljs/load-nyc-config": "^1.0.0", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-instrument": "^4.0.0", - "test-exclude": "^6.0.0" + "ast-metadata-inferer": "^0.2.0-0", + "browserslist": "^4.12.0", + "caniuse-db": "^1.0.30001059", + "core-js": "^3.6.5", + "lodash.memoize": "4.1.2", + "mdn-browser-compat-data": "^1.0.21", + "semver": "7.3.2" + }, + "dependencies": { + "semver": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", + "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==", + "dev": true + } } }, - "babel-plugin-jest-hoist": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-26.6.2.tgz", - "integrity": "sha512-PO9t0697lNTmcEHH69mdtYiOIkkOlj9fySqfO3K1eCcdISevLAE0xY59VLLUj0SoiPiTX/JU2CYFpILydUa5Lw==", + "eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", "dev": true, "requires": { - "@babel/template": "^7.3.3", - "@babel/types": "^7.3.3", - "@types/babel__core": "^7.0.0", - "@types/babel__traverse": "^7.0.6" + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" } }, - "babel-preset-current-node-syntax": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz", - "integrity": "sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==", - "dev": true, + "eslint-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", + "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "dev": true, "requires": { - "@babel/plugin-syntax-async-generators": "^7.8.4", - "@babel/plugin-syntax-bigint": "^7.8.3", - "@babel/plugin-syntax-class-properties": "^7.8.3", - "@babel/plugin-syntax-import-meta": "^7.8.3", - "@babel/plugin-syntax-json-strings": "^7.8.3", - "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-numeric-separator": "^7.8.3", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", - "@babel/plugin-syntax-optional-chaining": "^7.8.3", - "@babel/plugin-syntax-top-level-await": "^7.8.3" + "eslint-visitor-keys": "^1.1.0" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true + } } }, - "babel-preset-jest": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-26.6.2.tgz", - "integrity": "sha512-YvdtlVm9t3k777c5NPQIv6cxFFFapys25HiUmuSgHwIZhfifweR5c5Sf5nwE3MAbfu327CYSvps8Yx6ANLyleQ==", + "eslint-visitor-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz", + "integrity": "sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ==", + "dev": true + }, + "espree": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-7.3.1.tgz", + "integrity": "sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g==", "dev": true, "requires": { - "babel-plugin-jest-hoist": "^26.6.2", - "babel-preset-current-node-syntax": "^1.0.0" + "acorn": "^7.4.0", + "acorn-jsx": "^5.3.1", + "eslint-visitor-keys": "^1.3.0" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true + } } }, - "babel-runtime": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", - "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + }, + "esquery": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", + "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", "dev": true, "requires": { - "core-js": "^2.4.0", - "regenerator-runtime": "^0.11.0" + "estraverse": "^5.1.0" }, "dependencies": { - "core-js": { - "version": "2.6.11", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.11.tgz", - "integrity": "sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg==", + "estraverse": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", + "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", "dev": true - }, - "regenerator-runtime": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", - "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==", + } + } + }, + "esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "requires": { + "estraverse": "^5.2.0" + }, + "dependencies": { + "estraverse": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", + "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", "dev": true } } }, - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true + }, + "execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + } + }, + "exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=", "dev": true }, - "base": { - "version": "0.11.2", - "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", - "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", + "expect": { + "version": "27.2.4", + "resolved": "https://registry.npmjs.org/expect/-/expect-27.2.4.tgz", + "integrity": "sha512-gOtuonQ8TCnbNNCSw2fhVzRf8EFYDII4nB5NmG4IEV0rbUnW1I5zXvoTntU4iicB/Uh0oZr20NGlOLdJiwsOZA==", "dev": true, "requires": { - "cache-base": "^1.0.1", - "class-utils": "^0.3.5", - "component-emitter": "^1.2.1", - "define-property": "^1.0.0", - "isobject": "^3.0.1", - "mixin-deep": "^1.2.0", - "pascalcase": "^0.1.1" + "@jest/types": "^27.2.4", + "ansi-styles": "^5.0.0", + "jest-get-type": "^27.0.6", + "jest-matcher-utils": "^27.2.4", + "jest-message-util": "^27.2.4", + "jest-regex-util": "^27.0.6" }, "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + } + } + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "fast-glob": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.4.tgz", + "integrity": "sha512-kr/Oo6PX51265qeuCYsyGypiO5uJFgBS0jksyG7FUeCyQzNwYnzrNIMR1NXfkZXsMYXYLRAHgISHBz8gQcxKHQ==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.0", + "merge2": "^1.3.0", + "micromatch": "^4.0.2", + "picomatch": "^2.2.1" + }, + "dependencies": { + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", "dev": true, "requires": { - "is-descriptor": "^1.0.0" + "fill-range": "^7.0.1" } }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", "dev": true, "requires": { - "kind-of": "^6.0.0" + "to-regex-range": "^5.0.1" } }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "micromatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", + "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", "dev": true, "requires": { - "kind-of": "^6.0.0" + "braces": "^3.0.1", + "picomatch": "^2.0.5" } }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" + "is-number": "^7.0.0" } } } }, - "bcrypt-pbkdf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", - "dev": true, - "requires": { - "tweetnacl": "^0.14.3" - } - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true }, - "browser-process-hrtime": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz", - "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==", + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", "dev": true }, - "browserslist": { - "version": "4.16.6", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.6.tgz", - "integrity": "sha512-Wspk/PqO+4W9qp5iUTJsa1B/QrYn1keNCcEP5OvP7WBwT4KaDly0uONYmC6Xa3Z5IqnUgS0KcgLYu1l74x0ZXQ==", + "fastq": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.8.0.tgz", + "integrity": "sha512-SMIZoZdLh/fgofivvIkmknUXyPnvxRE3DhtZ5Me3Mrsk5gyPL42F0xr51TdRXskBxHfMp+07bcYzfsYEsSQA9Q==", "dev": true, "requires": { - "caniuse-lite": "^1.0.30001219", - "colorette": "^1.2.2", - "electron-to-chromium": "^1.3.723", - "escalade": "^3.1.1", - "node-releases": "^1.1.71" + "reusify": "^1.0.4" } }, - "bs-logger": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", - "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "fb-watchman": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.1.tgz", + "integrity": "sha512-DkPJKQeY6kKwmuMretBhr7G6Vodr7bFwDYTXIkfG1gjvNpaxBTQV3PbXg6bR1c1UP4jPOX0jHUbbHANL9vRjVg==", "dev": true, "requires": { - "fast-json-stable-stringify": "2.x" + "bser": "2.1.1" } }, - "bser": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", - "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "fetch-mock": { + "version": "9.10.7", + "resolved": "https://registry.npmjs.org/fetch-mock/-/fetch-mock-9.10.7.tgz", + "integrity": "sha512-YkiMHSL8CQ0vlWYpqGvlaZjViFk0Kar9jonPjSvaWoztkeHH6DENqUzBIsffzjVKhwchPI74SZRLRpIsEyNcZQ==", "dev": true, "requires": { - "node-int64": "^0.4.0" + "babel-runtime": "^6.26.0", + "core-js": "^3.0.0", + "debug": "^4.1.1", + "glob-to-regexp": "^0.4.0", + "is-subset": "^0.1.1", + "lodash.isequal": "^4.5.0", + "path-to-regexp": "^2.2.1", + "querystring": "^0.2.0", + "whatwg-url": "^6.5.0" + }, + "dependencies": { + "debug": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz", + "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "tr46": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", + "integrity": "sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk=", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, + "webidl-conversions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", + "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", + "dev": true + }, + "whatwg-url": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-6.5.0.tgz", + "integrity": "sha512-rhRZRqx/TLJQWUpQ6bmrt2UV4f0HCQ463yQuONJqC6fO2VoEb1pTYddbe59SkYq87aoM5A3bdhMZiUiVws+fzQ==", + "dev": true, + "requires": { + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" + } + } } }, - "buffer-from": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", - "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", + "file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "requires": { + "flat-cache": "^3.0.4" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "flat-cache": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "dev": true, + "requires": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + } + }, + "flatted": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.2.tgz", + "integrity": "sha512-JaTY/wtrcSyvXJl4IMFHPKyFur1sE9AUqc0QnhOaJ0CxHtAoIV8pYDzeEfAaNEtGkOfq4gr3LBFmdXW5mOQFnA==", "dev": true }, - "cache-base": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", - "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", + "form-data": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", + "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", "dev": true, "requires": { - "collection-visit": "^1.0.0", - "component-emitter": "^1.2.1", - "get-value": "^2.0.6", - "has-value": "^1.0.0", - "isobject": "^3.0.1", - "set-value": "^2.0.0", - "to-object-path": "^0.3.0", - "union-value": "^1.0.0", - "unset-value": "^1.0.0" + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" } }, - "callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", "dev": true }, - "camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "optional": true + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", "dev": true }, - "caniuse-db": { - "version": "1.0.30001120", - "resolved": "https://registry.npmjs.org/caniuse-db/-/caniuse-db-1.0.30001120.tgz", - "integrity": "sha512-DvFSRBZYuPj2976yroUk/vqu45nvbf70+qJLH7q+q3S9IOXY4Wl/q7nWOJX8mq6SjWQzCWbz0J0Q6UjsuYwd2A==", + "functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", "dev": true }, - "caniuse-lite": { - "version": "1.0.30001247", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001247.tgz", - "integrity": "sha512-4rS7co+7+AoOSPRPOPUt5/GdaqZc0EsUpWk66ofE3HJTAajUK2Ss2VwoNzVN69ghg8lYYlh0an0Iy4LIHHo9UQ==", + "gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", "dev": true }, - "capture-exit": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/capture-exit/-/capture-exit-2.0.0.tgz", - "integrity": "sha512-PiT/hQmTonHhl/HFGN+Lx3JJUznrVYJ3+AQsnthneZbvW7x+f08Tk7yLJTLEOUvBTbduLeeBkxEaYXUOUrRq6g==", - "dev": true, - "requires": { - "rsvp": "^4.8.4" - } + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true + }, + "get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true }, - "caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", + "get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", "dev": true }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", "dev": true, "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" } }, - "char-regex": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", - "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", - "dev": true + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } }, - "ci-info": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", - "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", + "glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", "dev": true }, - "cjs-module-lexer": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-0.6.0.tgz", - "integrity": "sha512-uc2Vix1frTfnuzxxu1Hp4ktSvM3QaI4oXl4ZUqL1wjTu/BGki9TrCWoqLTg/drR1KwAEarXuRFCG2Svr1GxPFw==", + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", "dev": true }, - "class-utils": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", - "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", + "globby": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.1.tgz", + "integrity": "sha512-iH9RmgwCmUJHi2z5o2l3eTtGBtXek1OYlHrbcxOYugyHLmAsZrPj43OtHThd62Buh/Vv6VyCBD2bdyWcGNQqoQ==", "dev": true, "requires": { - "arr-union": "^3.1.0", - "define-property": "^0.2.5", - "isobject": "^3.0.0", - "static-extend": "^0.1.1" + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.1.1", + "ignore": "^5.1.4", + "merge2": "^1.3.0", + "slash": "^3.0.0" }, "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true } } }, - "cliui": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", - "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "graceful-fs": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.8.tgz", + "integrity": "sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==", + "dev": true + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", "dev": true, "requires": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^6.2.0" + "function-bind": "^1.1.1" } }, - "cluster-key-slot": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.0.tgz", - "integrity": "sha512-2Nii8p3RwAPiFwsnZvukotvow2rIHM+yQ6ZcBXGHdniadkYGZYiGmkHJIbZPIV9nfv7m/U1IPMVVcAhoWFeklw==", + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", "dev": true }, - "co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", - "dev": true + "html-encoding-sniffer": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz", + "integrity": "sha512-D5JbOMBIR/TVZkubHT+OyT2705QvogUW4IBn6nHd756OwieSF9aDYFj4dv6HHEVGYbHaLETa3WggZYWWMyy3ZQ==", + "dev": true, + "requires": { + "whatwg-encoding": "^1.0.5" + } }, - "collect-v8-coverage": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz", - "integrity": "sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg==", + "html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true }, - "collection-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", - "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", + "http-proxy-agent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", "dev": true, "requires": { - "map-visit": "^1.0.0", - "object-visit": "^1.0.0" + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" } }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "https-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", + "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", "dev": true, "requires": { - "color-name": "1.1.3" + "agent-base": "6", + "debug": "4" } }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", - "dev": true - }, - "colorette": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.2.tgz", - "integrity": "sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w==", - "dev": true - }, - "combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, - "requires": { - "delayed-stream": "~1.0.0" - } - }, - "component-emitter": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", - "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", - "dev": true - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", "dev": true }, - "convert-source-map": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", - "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==", + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "dev": true, "requires": { - "safe-buffer": "~5.1.1" + "safer-buffer": ">= 2.1.2 < 3" } }, - "copy-descriptor": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", - "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", - "dev": true - }, - "core-js": { - "version": "3.6.5", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.6.5.tgz", - "integrity": "sha512-vZVEEwZoIsI+vPEuoF9Iqf5H7/M3eeQqWlQnYa8FSKKePuYTf5MWnxb5SDAzCa60b3JBRS5g9b+Dq7b1y/RCrA==", - "dev": true - }, - "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "ignore": { + "version": "5.1.8", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz", + "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==", "dev": true }, - "cross-env": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.2.tgz", - "integrity": "sha512-KZP/bMEOJEDCkDQAyRhu3RL2ZO/SUVrxQVI0G3YEQ+OLbRA3c6zgixe8Mq8a/z7+HKlNEjo8oiLUs8iRijY2Rw==", + "import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", "dev": true, "requires": { - "cross-spawn": "^7.0.1" + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "dependencies": { + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + } } }, - "cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "import-local": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.0.3.tgz", + "integrity": "sha512-bE9iaUY3CXH8Cwfan/abDKAxe1KGT9kyGsBPqf6DMK/z0a2OzAsrukeYNgIH6cH5Xr452jb1TUL8rSfCLjZ9uA==", "dev": true, "requires": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" } }, - "cssom": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz", - "integrity": "sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw==", + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", "dev": true }, - "cssstyle": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", - "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", "dev": true, "requires": { - "cssom": "~0.3.6" - }, - "dependencies": { - "cssom": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", - "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", - "dev": true - } + "once": "^1.3.0", + "wrappy": "1" } }, - "csv-streamify": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/csv-streamify/-/csv-streamify-4.0.0.tgz", - "integrity": "sha512-d+QZf8WaKwn/8/dJDyN0TFkmSH0fHBFJSyz+ujC2a5sGvXgxEj3zBwqx/mLoPMJ+YbI9H7n3h5YL4uAHUj731w==", - "dev": true, - "requires": { - "through2": "2.0.3" - } + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true }, - "dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "ioredis": { + "version": "4.27.2", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-4.27.2.tgz", + "integrity": "sha512-7OpYymIthonkC2Jne5uGWXswdhlua1S1rWGAERaotn0hGJWTSURvxdHA9G6wNbT/qKCloCja/FHsfKXW8lpTmg==", "dev": true, "requires": { - "assert-plus": "^1.0.0" + "cluster-key-slot": "^1.1.0", + "debug": "^4.3.1", + "denque": "^1.1.0", + "lodash.defaults": "^4.2.0", + "lodash.flatten": "^4.4.0", + "p-map": "^2.1.0", + "redis-commands": "1.7.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0", + "standard-as-callback": "^2.1.0" } }, - "data-urls": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-2.0.0.tgz", - "integrity": "sha512-X5eWTSXO/BJmpdIKCRuKUgSCgAN0OwliVK3yPKbwIWU1Tdw5BRajxlzMidvh+gwko9AfQ9zIj52pzF91Q3YAvQ==", + "is-ci": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.0.tgz", + "integrity": "sha512-kDXyttuLeslKAHYL/K28F2YkM3x5jvFPEw3yXbRptXydjD9rpLEz+C5K5iutY9ZiUu6AP41JdvRQwF4Iqs4ZCQ==", "dev": true, "requires": { - "abab": "^2.0.3", - "whatwg-mimetype": "^2.3.0", - "whatwg-url": "^8.0.0" + "ci-info": "^3.1.1" } }, - "debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "is-core-module": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.7.0.tgz", + "integrity": "sha512-ByY+tjCciCr+9nLryBYcSD50EOGWt95c7tIsKTG1J2ixKKXPvF7Ej3AVd+UfDydAJom3biBGDBALaO79ktwgEQ==", "dev": true, "requires": { - "ms": "2.1.2" + "has": "^1.0.3" } }, - "decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", - "dev": true - }, - "decimal.js": { - "version": "10.2.1", - "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.2.1.tgz", - "integrity": "sha512-KaL7+6Fw6i5A2XSnsbhm/6B+NuEA7TZ4vqxnd5tXz9sbKtrN9Srj8ab4vKVdK8YAqZO9P1kg45Y6YLoduPf+kw==", - "dev": true - }, - "decode-uri-component": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", - "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", "dev": true }, - "deep-is": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", - "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true }, - "deepmerge": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", - "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==", + "is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", "dev": true }, - "define-property": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", - "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", "dev": true, "requires": { - "is-descriptor": "^1.0.2", - "isobject": "^3.0.1" - }, - "dependencies": { - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - } + "is-extglob": "^2.1.1" } }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true }, - "denque": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/denque/-/denque-1.5.0.tgz", - "integrity": "sha512-CYiCSgIF1p6EUByQPlGkKnP1M9g0ZV3qMIrqMqZqdwazygIA/YP2vrbcyl1h/WppKJTdl1F85cXIle+394iDAQ==", + "is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", "dev": true }, - "detect-newline": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", - "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", "dev": true }, - "diff-sequences": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-26.6.2.tgz", - "integrity": "sha512-Mv/TDa3nZ9sbc5soK+OoA74BsS3mL37yixCvUAQkiuA4Wz6YtwP/K47n2rv2ovzHZvoiQeA5FTQOschKkEwB0Q==", + "is-subset": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-subset/-/is-subset-0.1.1.tgz", + "integrity": "sha1-ilkRfZMt4d4A8kX83TnOQ/HpOaY=", "dev": true }, - "dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "requires": { - "path-type": "^4.0.0" - } - }, - "doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, - "requires": { - "esutils": "^2.0.2" - } + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", + "dev": true }, - "domexception": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/domexception/-/domexception-2.0.1.tgz", - "integrity": "sha512-yxJ2mFy/sibVQlu5qHjOkf9J3K6zgmCxgJ94u2EdvDOV09H+32LtRswEcUsmUWN72pVLOEnTSRaIVVzVQgS0dg==", - "dev": true, - "requires": { - "webidl-conversions": "^5.0.0" - }, - "dependencies": { - "webidl-conversions": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz", - "integrity": "sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==", - "dev": true - } - } - }, - "ecc-jsbn": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", - "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", - "dev": true, - "requires": { - "jsbn": "~0.1.0", - "safer-buffer": "^2.1.0" - } - }, - "electron-to-chromium": { - "version": "1.3.788", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.788.tgz", - "integrity": "sha512-dbMIpX4E4/Gk4gzOh1GYS7ls1vGsByWKpIqLviJi1mSmSt5BvrWLLtSqpFE5BaC7Ef4NnI0GMaiddNX2Brw6zA==", - "dev": true - }, - "emittery": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.7.2.tgz", - "integrity": "sha512-A8OG5SR/ij3SsJdWDJdkkSYUjQdCUx6APQXem0SaEePBSRg4eymGYwBkKo1Y6DU+af/Jn2dBQqDBvjnr9Vi8nQ==", - "dev": true - }, - "emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "dev": true, - "requires": { - "once": "^1.4.0" - } - }, - "enquirer": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", - "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", - "dev": true, - "requires": { - "ansi-colors": "^4.1.1" - } - }, - "error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dev": true, - "requires": { - "is-arrayish": "^0.2.1" - } - }, - "escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "dev": true - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true - }, - "escodegen": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.0.0.tgz", - "integrity": "sha512-mmHKys/C8BFUGI+MAWNcSYoORYLMdPzjrknd2Vc+bUsjN5bXcr8EhrNB+UTqfL1y3I9c4fw2ihgtMPQLBRiQxw==", - "dev": true, - "requires": { - "esprima": "^4.0.1", - "estraverse": "^5.2.0", - "esutils": "^2.0.2", - "optionator": "^0.8.1", - "source-map": "~0.6.1" - }, - "dependencies": { - "estraverse": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", - "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", - "dev": true - } - } - }, - "eslint": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.9.0.tgz", - "integrity": "sha512-V6QyhX21+uXp4T+3nrNfI3hQNBDa/P8ga7LoQOenwrlEFXrEnUEE+ok1dMtaS3b6rmLXhT1TkTIsG75HMLbknA==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "@eslint/eslintrc": "^0.1.3", - "ajv": "^6.10.0", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.0.1", - "doctrine": "^3.0.0", - "enquirer": "^2.3.5", - "eslint-scope": "^5.1.0", - "eslint-utils": "^2.1.0", - "eslint-visitor-keys": "^1.3.0", - "espree": "^7.3.0", - "esquery": "^1.2.0", - "esutils": "^2.0.2", - "file-entry-cache": "^5.0.1", - "functional-red-black-tree": "^1.0.1", - "glob-parent": "^5.0.0", - "globals": "^12.1.0", - "ignore": "^4.0.6", - "import-fresh": "^3.0.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "js-yaml": "^3.13.1", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash": "^4.17.19", - "minimatch": "^3.0.4", - "natural-compare": "^1.4.0", - "optionator": "^0.9.1", - "progress": "^2.0.0", - "regexpp": "^3.1.0", - "semver": "^7.2.1", - "strip-ansi": "^6.0.0", - "strip-json-comments": "^3.1.0", - "table": "^5.2.3", - "text-table": "^0.2.0", - "v8-compile-cache": "^2.0.3" - }, - "dependencies": { - "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", - "dev": true, - "requires": { - "@types/color-name": "^1.1.1", - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", - "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "debug": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz", - "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "eslint-visitor-keys": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", - "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", - "dev": true - }, - "globals": { - "version": "12.4.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", - "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", - "dev": true, - "requires": { - "type-fest": "^0.8.1" - } - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "ignore": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", - "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", - "dev": true - }, - "levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "requires": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "optionator": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", - "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", - "dev": true, - "requires": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.3" - } - }, - "prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true - }, - "semver": { - "version": "7.3.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", - "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - }, - "type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "requires": { - "prelude-ls": "^1.2.1" - } - } - } - }, - "eslint-plugin-compat": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-compat/-/eslint-plugin-compat-3.7.0.tgz", - "integrity": "sha512-A3uzSYqUjNj6rMyaBuU3l8wSCadZjeZRZ7WF3eU9vUT0JItiqRysjmYELkHHCpH8l7wRprUu4MZPr37lFCw7iA==", - "dev": true, - "requires": { - "ast-metadata-inferer": "^0.2.0-0", - "browserslist": "^4.12.0", - "caniuse-db": "^1.0.30001059", - "core-js": "^3.6.5", - "lodash.memoize": "4.1.2", - "mdn-browser-compat-data": "^1.0.21", - "semver": "7.3.2" - }, - "dependencies": { - "semver": { - "version": "7.3.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", - "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==", - "dev": true - } - } - }, - "eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "dev": true, - "requires": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - } - }, - "eslint-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", - "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", - "dev": true, - "requires": { - "eslint-visitor-keys": "^1.1.0" - }, - "dependencies": { - "eslint-visitor-keys": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", - "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", - "dev": true - } - } - }, - "eslint-visitor-keys": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz", - "integrity": "sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ==", - "dev": true - }, - "espree": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-7.3.0.tgz", - "integrity": "sha512-dksIWsvKCixn1yrEXO8UosNSxaDoSYpq9reEjZSbHLpT5hpaCAKTLBwq0RHtLrIr+c0ByiYzWT8KTMRzoRCNlw==", - "dev": true, - "requires": { - "acorn": "^7.4.0", - "acorn-jsx": "^5.2.0", - "eslint-visitor-keys": "^1.3.0" - }, - "dependencies": { - "eslint-visitor-keys": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", - "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", - "dev": true - } - } - }, - "esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true - }, - "esquery": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.3.1.tgz", - "integrity": "sha512-olpvt9QG0vniUBZspVRN6lwB7hOZoTRtT+jzR+tS4ffYx2mzbw+z0XCOk44aaLYKApNX5nMm+E+P6o25ip/DHQ==", - "dev": true, - "requires": { - "estraverse": "^5.1.0" - }, - "dependencies": { - "estraverse": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", - "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", - "dev": true - } - } - }, - "esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "requires": { - "estraverse": "^5.2.0" - }, - "dependencies": { - "estraverse": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", - "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", - "dev": true - } - } - }, - "estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true - }, - "esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true - }, - "exec-sh": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/exec-sh/-/exec-sh-0.3.6.tgz", - "integrity": "sha512-nQn+hI3yp+oD0huYhKwvYI32+JFeq+XkNcD1GAo3Y/MjxsfVGmrrzrnzjWiNY6f+pUCP440fThsFh5gZrRAU/w==", - "dev": true - }, - "execa": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", - "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", - "dev": true, - "requires": { - "cross-spawn": "^6.0.0", - "get-stream": "^4.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - }, - "dependencies": { - "cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", - "dev": true, - "requires": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - } - }, - "path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", - "dev": true - }, - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - }, - "shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", - "dev": true, - "requires": { - "shebang-regex": "^1.0.0" - } - }, - "shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", - "dev": true - }, - "which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - } - } - }, - "exit": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", - "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=", - "dev": true - }, - "expand-brackets": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", - "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", - "dev": true, - "requires": { - "debug": "^2.3.3", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "posix-character-classes": "^0.1.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - } - } - }, - "expect": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/expect/-/expect-26.6.2.tgz", - "integrity": "sha512-9/hlOBkQl2l/PLHJx6JjoDF6xPKcJEsUlWKb23rKE7KzeDqUZKXKNMW27KIue5JMdBV9HgmoJPcc8HtO85t9IA==", - "dev": true, - "requires": { - "@jest/types": "^26.6.2", - "ansi-styles": "^4.0.0", - "jest-get-type": "^26.3.0", - "jest-matcher-utils": "^26.6.2", - "jest-message-util": "^26.6.2", - "jest-regex-util": "^26.0.0" - }, - "dependencies": { - "@jest/types": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", - "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^15.0.0", - "chalk": "^4.0.0" - } - }, - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", - "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "dev": true - }, - "extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", - "dev": true, - "requires": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, - "dependencies": { - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "requires": { - "is-plain-object": "^2.0.4" - } - } - } - }, - "extglob": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", - "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", - "dev": true, - "requires": { - "array-unique": "^0.3.2", - "define-property": "^1.0.0", - "expand-brackets": "^2.1.4", - "extend-shallow": "^2.0.1", - "fragment-cache": "^0.2.1", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - } - } - }, - "extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", - "dev": true - }, - "fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true - }, - "fast-glob": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.4.tgz", - "integrity": "sha512-kr/Oo6PX51265qeuCYsyGypiO5uJFgBS0jksyG7FUeCyQzNwYnzrNIMR1NXfkZXsMYXYLRAHgISHBz8gQcxKHQ==", - "dev": true, - "requires": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.0", - "merge2": "^1.3.0", - "micromatch": "^4.0.2", - "picomatch": "^2.2.1" - }, - "dependencies": { - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "requires": { - "is-glob": "^4.0.1" - } - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true - }, - "micromatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", - "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", - "dev": true, - "requires": { - "braces": "^3.0.1", - "picomatch": "^2.0.5" - } - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - } - } - }, - "fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true - }, - "fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", - "dev": true - }, - "fastq": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.8.0.tgz", - "integrity": "sha512-SMIZoZdLh/fgofivvIkmknUXyPnvxRE3DhtZ5Me3Mrsk5gyPL42F0xr51TdRXskBxHfMp+07bcYzfsYEsSQA9Q==", - "dev": true, - "requires": { - "reusify": "^1.0.4" - } - }, - "fb-watchman": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.1.tgz", - "integrity": "sha512-DkPJKQeY6kKwmuMretBhr7G6Vodr7bFwDYTXIkfG1gjvNpaxBTQV3PbXg6bR1c1UP4jPOX0jHUbbHANL9vRjVg==", - "dev": true, - "requires": { - "bser": "2.1.1" - } - }, - "fetch-mock": { - "version": "9.10.7", - "resolved": "https://registry.npmjs.org/fetch-mock/-/fetch-mock-9.10.7.tgz", - "integrity": "sha512-YkiMHSL8CQ0vlWYpqGvlaZjViFk0Kar9jonPjSvaWoztkeHH6DENqUzBIsffzjVKhwchPI74SZRLRpIsEyNcZQ==", - "dev": true, - "requires": { - "babel-runtime": "^6.26.0", - "core-js": "^3.0.0", - "debug": "^4.1.1", - "glob-to-regexp": "^0.4.0", - "is-subset": "^0.1.1", - "lodash.isequal": "^4.5.0", - "path-to-regexp": "^2.2.1", - "querystring": "^0.2.0", - "whatwg-url": "^6.5.0" - }, - "dependencies": { - "debug": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz", - "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "tr46": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", - "integrity": "sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk=", - "dev": true, - "requires": { - "punycode": "^2.1.0" - } - }, - "webidl-conversions": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", - "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", - "dev": true - }, - "whatwg-url": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-6.5.0.tgz", - "integrity": "sha512-rhRZRqx/TLJQWUpQ6bmrt2UV4f0HCQ463yQuONJqC6fO2VoEb1pTYddbe59SkYq87aoM5A3bdhMZiUiVws+fzQ==", - "dev": true, - "requires": { - "lodash.sortby": "^4.7.0", - "tr46": "^1.0.1", - "webidl-conversions": "^4.0.2" - } - } - } - }, - "file-entry-cache": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz", - "integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==", - "dev": true, - "requires": { - "flat-cache": "^2.0.1" - } - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "flat-cache": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", - "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==", - "dev": true, - "requires": { - "flatted": "^2.0.0", - "rimraf": "2.6.3", - "write": "1.0.3" - }, - "dependencies": { - "rimraf": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", - "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - } - } - }, - "flatted": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz", - "integrity": "sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==", - "dev": true - }, - "for-in": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", - "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", - "dev": true - }, - "forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", - "dev": true - }, - "form-data": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", - "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", - "dev": true, - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" - } - }, - "fragment-cache": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", - "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", - "dev": true, - "requires": { - "map-cache": "^0.2.2" - } - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true - }, - "fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "optional": true - }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - }, - "functional-red-black-tree": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", - "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", - "dev": true - }, - "gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true - }, - "get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true - }, - "get-package-type": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", - "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", - "dev": true - }, - "get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "dev": true, - "requires": { - "pump": "^3.0.0" - } - }, - "get-value": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", - "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", - "dev": true - }, - "getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0" - } - }, - "glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "requires": { - "is-glob": "^4.0.1" - } - }, - "glob-to-regexp": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", - "dev": true - }, - "globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true - }, - "globby": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.1.tgz", - "integrity": "sha512-iH9RmgwCmUJHi2z5o2l3eTtGBtXek1OYlHrbcxOYugyHLmAsZrPj43OtHThd62Buh/Vv6VyCBD2bdyWcGNQqoQ==", - "dev": true, - "requires": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.1.1", - "ignore": "^5.1.4", - "merge2": "^1.3.0", - "slash": "^3.0.0" - }, - "dependencies": { - "slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true - } - } - }, - "graceful-fs": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", - "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==", - "dev": true - }, - "growly": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz", - "integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=", - "dev": true, - "optional": true - }, - "har-schema": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", - "dev": true - }, - "har-validator": { - "version": "5.1.5", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", - "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", - "dev": true, - "requires": { - "ajv": "^6.12.3", - "har-schema": "^2.0.0" - } - }, - "has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, - "requires": { - "function-bind": "^1.1.1" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, - "has-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", - "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", - "dev": true, - "requires": { - "get-value": "^2.0.6", - "has-values": "^1.0.0", - "isobject": "^3.0.0" - } - }, - "has-values": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", - "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", - "dev": true, - "requires": { - "is-number": "^3.0.0", - "kind-of": "^4.0.0" - }, - "dependencies": { - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "kind-of": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", - "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "hosted-git-info": { - "version": "2.8.9", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", - "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", - "dev": true - }, - "html-encoding-sniffer": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz", - "integrity": "sha512-D5JbOMBIR/TVZkubHT+OyT2705QvogUW4IBn6nHd756OwieSF9aDYFj4dv6HHEVGYbHaLETa3WggZYWWMyy3ZQ==", - "dev": true, - "requires": { - "whatwg-encoding": "^1.0.5" - } - }, - "html-escaper": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "dev": true - }, - "http-signature": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0", - "jsprim": "^1.2.2", - "sshpk": "^1.7.0" - } - }, - "human-signals": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", - "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==", - "dev": true - }, - "iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dev": true, - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - }, - "ignore": { - "version": "5.1.8", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz", - "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==", - "dev": true - }, - "import-fresh": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.1.tgz", - "integrity": "sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ==", - "dev": true, - "requires": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "dependencies": { - "resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true - } - } - }, - "import-local": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.0.2.tgz", - "integrity": "sha512-vjL3+w0oulAVZ0hBHnxa/Nm5TAurf9YLQJDhqRZyqb+VKGOB6LU8t9H1Nr5CIo16vh9XfJTOoHwU0B71S557gA==", - "dev": true, - "requires": { - "pkg-dir": "^4.2.0", - "resolve-cwd": "^3.0.0" - } - }, - "imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", - "dev": true - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true - }, - "ioredis": { - "version": "4.27.2", - "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-4.27.2.tgz", - "integrity": "sha512-7OpYymIthonkC2Jne5uGWXswdhlua1S1rWGAERaotn0hGJWTSURvxdHA9G6wNbT/qKCloCja/FHsfKXW8lpTmg==", - "dev": true, - "requires": { - "cluster-key-slot": "^1.1.0", - "debug": "^4.3.1", - "denque": "^1.1.0", - "lodash.defaults": "^4.2.0", - "lodash.flatten": "^4.4.0", - "p-map": "^2.1.0", - "redis-commands": "1.7.0", - "redis-errors": "^1.2.0", - "redis-parser": "^3.0.0", - "standard-as-callback": "^2.1.0" - } - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", - "dev": true - }, - "is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true - }, - "is-ci": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", - "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", - "dev": true, - "requires": { - "ci-info": "^2.0.0" - } - }, - "is-core-module": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.2.0.tgz", - "integrity": "sha512-XRAfAdyyY5F5cOXn7hYQDqh2Xmii+DEfIcQGxK/uNwMHhIkPWO0g8msXcbzLe+MpGoR951MlqM/2iIlU4vKDdQ==", - "dev": true, - "requires": { - "has": "^1.0.3" - } - }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - }, - "dependencies": { - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", - "dev": true - } - } - }, - "is-docker": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.1.1.tgz", - "integrity": "sha512-ZOoqiXfEwtGknTiuDEy8pN2CfE3TxMHprvNer1mXiqwkOT77Rw3YVrUQ52EqAOU3QAWDQ+bQdx7HJzrv7LS2Hw==", - "dev": true, - "optional": true - }, - "is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", - "dev": true - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true - }, - "is-generator-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", - "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", - "dev": true - }, - "is-glob": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", - "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true - }, - "is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dev": true, - "requires": { - "isobject": "^3.0.1" - } - }, - "is-potential-custom-element-name": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.0.tgz", - "integrity": "sha1-DFLlS8yjkbssSUsh6GJtczbG45c=", - "dev": true - }, - "is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", - "dev": true - }, - "is-subset": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-subset/-/is-subset-0.1.1.tgz", - "integrity": "sha1-ilkRfZMt4d4A8kX83TnOQ/HpOaY=", - "dev": true - }, - "is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", - "dev": true - }, - "is-windows": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", - "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", - "dev": true - }, - "is-wsl": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", - "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", - "dev": true, - "optional": true, - "requires": { - "is-docker": "^2.0.0" - } - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", - "dev": true - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - }, - "isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", - "dev": true - }, - "istanbul-lib-coverage": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz", - "integrity": "sha512-UiUIqxMgRDET6eR+o5HbfRYP1l0hqkWOs7vNxC/mggutCMUIhWMm8gAHb8tHlyfD3/l6rlgNA5cKdDzEAf6hEg==", - "dev": true - }, - "istanbul-lib-instrument": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz", - "integrity": "sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ==", - "dev": true, - "requires": { - "@babel/core": "^7.7.5", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-coverage": "^3.0.0", - "semver": "^6.3.0" - } - }, - "istanbul-lib-report": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", - "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==", - "dev": true, - "requires": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^3.0.0", - "supports-color": "^7.1.0" - }, - "dependencies": { - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "istanbul-lib-source-maps": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.0.tgz", - "integrity": "sha512-c16LpFRkR8vQXyHZ5nLpY35JZtzj1PQY1iZmesUbf1FZHbIupcWfjgOXBY9YHkLEQ6puz1u4Dgj6qmU/DisrZg==", - "dev": true, - "requires": { - "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0", - "source-map": "^0.6.1" - } - }, - "istanbul-reports": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.0.2.tgz", - "integrity": "sha512-9tZvz7AiR3PEDNGiV9vIouQ/EAcqMXFmkcA1CDFTwOB98OZVDL0PH9glHotf5Ugp6GCOTypfzGWI/OqjWNCRUw==", - "dev": true, - "requires": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" - } - }, - "jest": { - "version": "26.6.3", - "resolved": "https://registry.npmjs.org/jest/-/jest-26.6.3.tgz", - "integrity": "sha512-lGS5PXGAzR4RF7V5+XObhqz2KZIDUA1yD0DG6pBVmy10eh0ZIXQImRuzocsI/N2XZ1GrLFwTS27In2i2jlpq1Q==", - "dev": true, - "requires": { - "@jest/core": "^26.6.3", - "import-local": "^3.0.2", - "jest-cli": "^26.6.3" - }, - "dependencies": { - "@jest/types": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", - "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^15.0.0", - "chalk": "^4.0.0" - } - }, - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", - "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "jest-cli": { - "version": "26.6.3", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-26.6.3.tgz", - "integrity": "sha512-GF9noBSa9t08pSyl3CY4frMrqp+aQXFGFkf5hEPbh/pIUFYWMK6ZLTfbmadxJVcJrdRoChlWQsA2VkJcDFK8hg==", - "dev": true, - "requires": { - "@jest/core": "^26.6.3", - "@jest/test-result": "^26.6.2", - "@jest/types": "^26.6.2", - "chalk": "^4.0.0", - "exit": "^0.1.2", - "graceful-fs": "^4.2.4", - "import-local": "^3.0.2", - "is-ci": "^2.0.0", - "jest-config": "^26.6.3", - "jest-util": "^26.6.2", - "jest-validate": "^26.6.2", - "prompts": "^2.0.1", - "yargs": "^15.4.1" - } - }, - "jest-util": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-26.6.2.tgz", - "integrity": "sha512-MDW0fKfsn0OI7MS7Euz6h8HNDXVQ0gaM9uW6RjfDmd1DAFcaxX9OqIakHIqhbnmF08Cf2DLDG+ulq8YQQ0Lp0Q==", - "dev": true, - "requires": { - "@jest/types": "^26.6.2", - "@types/node": "*", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.4", - "is-ci": "^2.0.0", - "micromatch": "^4.0.2" - } - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "jest-changed-files": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-26.6.2.tgz", - "integrity": "sha512-fDS7szLcY9sCtIip8Fjry9oGf3I2ht/QT21bAHm5Dmf0mD4X3ReNUf17y+bO6fR8WgbIZTlbyG1ak/53cbRzKQ==", - "dev": true, - "requires": { - "@jest/types": "^26.6.2", - "execa": "^4.0.0", - "throat": "^5.0.0" - }, - "dependencies": { - "@jest/types": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", - "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^15.0.0", - "chalk": "^4.0.0" - } - }, - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", - "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "execa": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz", - "integrity": "sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==", - "dev": true, - "requires": { - "cross-spawn": "^7.0.0", - "get-stream": "^5.0.0", - "human-signals": "^1.1.1", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.0", - "onetime": "^5.1.0", - "signal-exit": "^3.0.2", - "strip-final-newline": "^2.0.0" - } - }, - "get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "dev": true, - "requires": { - "pump": "^3.0.0" - } - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "is-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", - "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==", - "dev": true - }, - "npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "dev": true, - "requires": { - "path-key": "^3.0.0" - } - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "jest-config": { - "version": "26.6.3", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-26.6.3.tgz", - "integrity": "sha512-t5qdIj/bCj2j7NFVHb2nFB4aUdfucDn3JRKgrZnplb8nieAirAzRSHP8uDEd+qV6ygzg9Pz4YG7UTJf94LPSyg==", - "dev": true, - "requires": { - "@babel/core": "^7.1.0", - "@jest/test-sequencer": "^26.6.3", - "@jest/types": "^26.6.2", - "babel-jest": "^26.6.3", - "chalk": "^4.0.0", - "deepmerge": "^4.2.2", - "glob": "^7.1.1", - "graceful-fs": "^4.2.4", - "jest-environment-jsdom": "^26.6.2", - "jest-environment-node": "^26.6.2", - "jest-get-type": "^26.3.0", - "jest-jasmine2": "^26.6.3", - "jest-regex-util": "^26.0.0", - "jest-resolve": "^26.6.2", - "jest-util": "^26.6.2", - "jest-validate": "^26.6.2", - "micromatch": "^4.0.2", - "pretty-format": "^26.6.2" - }, - "dependencies": { - "@jest/types": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", - "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^15.0.0", - "chalk": "^4.0.0" - } - }, - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", - "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "jest-util": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-26.6.2.tgz", - "integrity": "sha512-MDW0fKfsn0OI7MS7Euz6h8HNDXVQ0gaM9uW6RjfDmd1DAFcaxX9OqIakHIqhbnmF08Cf2DLDG+ulq8YQQ0Lp0Q==", - "dev": true, - "requires": { - "@jest/types": "^26.6.2", - "@types/node": "*", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.4", - "is-ci": "^2.0.0", - "micromatch": "^4.0.2" - } - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "istanbul-lib-coverage": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz", + "integrity": "sha512-UiUIqxMgRDET6eR+o5HbfRYP1l0hqkWOs7vNxC/mggutCMUIhWMm8gAHb8tHlyfD3/l6rlgNA5cKdDzEAf6hEg==", + "dev": true + }, + "istanbul-lib-instrument": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz", + "integrity": "sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ==", + "dev": true, + "requires": { + "@babel/core": "^7.7.5", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.0.0", + "semver": "^6.3.0" } }, - "jest-diff": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-26.6.2.tgz", - "integrity": "sha512-6m+9Z3Gv9wN0WFVasqjCL/06+EFCMTqDEUl/b87HYK2rAPTyfz4ZIuSlPhY51PIQRWx5TaxeF1qmXKe9gfN3sA==", + "istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==", "dev": true, "requires": { - "chalk": "^4.0.0", - "diff-sequences": "^26.6.2", - "jest-get-type": "^26.3.0", - "pretty-format": "^26.6.2" + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^3.0.0", + "supports-color": "^7.1.0" }, "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", - "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -4809,134 +3044,38 @@ } } }, - "jest-docblock": { - "version": "26.0.0", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-26.0.0.tgz", - "integrity": "sha512-RDZ4Iz3QbtRWycd8bUEPxQsTlYazfYn/h5R65Fc6gOfwozFhoImx+affzky/FFBuqISPTqjXomoIGJVKBWoo0w==", + "istanbul-lib-source-maps": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.0.tgz", + "integrity": "sha512-c16LpFRkR8vQXyHZ5nLpY35JZtzj1PQY1iZmesUbf1FZHbIupcWfjgOXBY9YHkLEQ6puz1u4Dgj6qmU/DisrZg==", "dev": true, "requires": { - "detect-newline": "^3.0.0" + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" } }, - "jest-each": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-26.6.2.tgz", - "integrity": "sha512-Mer/f0KaATbjl8MCJ+0GEpNdqmnVmDYqCTJYTvoo7rqmRiDllmp2AYN+06F93nXcY3ur9ShIjS+CO/uD+BbH4A==", + "istanbul-reports": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.0.3.tgz", + "integrity": "sha512-0i77ZFLsb9U3DHi22WzmIngVzfoyxxbQcZRqlF3KoKmCJGq9nhFHoGi8FqBztN2rE8w6hURnZghetn0xpkVb6A==", "dev": true, "requires": { - "@jest/types": "^26.6.2", - "chalk": "^4.0.0", - "jest-get-type": "^26.3.0", - "jest-util": "^26.6.2", - "pretty-format": "^26.6.2" - }, - "dependencies": { - "@jest/types": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", - "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^15.0.0", - "chalk": "^4.0.0" - } - }, - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", - "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "jest-util": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-26.6.2.tgz", - "integrity": "sha512-MDW0fKfsn0OI7MS7Euz6h8HNDXVQ0gaM9uW6RjfDmd1DAFcaxX9OqIakHIqhbnmF08Cf2DLDG+ulq8YQQ0Lp0Q==", - "dev": true, - "requires": { - "@jest/types": "^26.6.2", - "@types/node": "*", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.4", - "is-ci": "^2.0.0", - "micromatch": "^4.0.2" - } - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" } }, - "jest-environment-jsdom": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-26.6.2.tgz", - "integrity": "sha512-jgPqCruTlt3Kwqg5/WVFyHIOJHsiAvhcp2qiR2QQstuG9yWox5+iHpU3ZrcBxW14T4fe5Z68jAfLRh7joCSP2Q==", + "jest": { + "version": "27.2.4", + "resolved": "https://registry.npmjs.org/jest/-/jest-27.2.4.tgz", + "integrity": "sha512-h4uqb1EQLfPulWyUFFWv9e9Nn8sCqsJ/j3wk/KCY0p4s4s0ICCfP3iMf6hRf5hEhsDyvyrCgKiZXma63gMz16A==", "dev": true, "requires": { - "@jest/environment": "^26.6.2", - "@jest/fake-timers": "^26.6.2", - "@jest/types": "^26.6.2", - "@types/node": "*", - "jest-mock": "^26.6.2", - "jest-util": "^26.6.2", - "jsdom": "^16.4.0" + "@jest/core": "^27.2.4", + "import-local": "^3.0.2", + "jest-cli": "^27.2.4" }, "dependencies": { - "@jest/types": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", - "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^15.0.0", - "chalk": "^4.0.0" - } - }, "ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -4947,9 +3086,9 @@ } }, "chalk": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", - "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "requires": { "ansi-styles": "^4.1.0", @@ -4977,18 +3116,24 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, - "jest-util": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-26.6.2.tgz", - "integrity": "sha512-MDW0fKfsn0OI7MS7Euz6h8HNDXVQ0gaM9uW6RjfDmd1DAFcaxX9OqIakHIqhbnmF08Cf2DLDG+ulq8YQQ0Lp0Q==", + "jest-cli": { + "version": "27.2.4", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-27.2.4.tgz", + "integrity": "sha512-4kpQQkg74HYLaXo3nzwtg4PYxSLgL7puz1LXHj5Tu85KmlIpxQFjRkXlx4V47CYFFIDoyl3rHA/cXOxUWyMpNg==", "dev": true, "requires": { - "@jest/types": "^26.6.2", - "@types/node": "*", + "@jest/core": "^27.2.4", + "@jest/test-result": "^27.2.4", + "@jest/types": "^27.2.4", "chalk": "^4.0.0", + "exit": "^0.1.2", "graceful-fs": "^4.2.4", - "is-ci": "^2.0.0", - "micromatch": "^4.0.2" + "import-local": "^3.0.2", + "jest-config": "^27.2.4", + "jest-util": "^27.2.4", + "jest-validate": "^27.2.4", + "prompts": "^2.0.1", + "yargs": "^16.2.0" } }, "supports-color": { @@ -5002,33 +3147,44 @@ } } }, - "jest-environment-node": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-26.6.2.tgz", - "integrity": "sha512-zhtMio3Exty18dy8ee8eJ9kjnRyZC1N4C1Nt/VShN1apyXc8rWGtJ9lI7vqiWcyyXS4BVSEn9lxAM2D+07/Tag==", + "jest-changed-files": { + "version": "27.2.4", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-27.2.4.tgz", + "integrity": "sha512-eeO1C1u4ex7pdTroYXezr+rbr957myyVoKGjcY4R1TJi3A+9v+4fu1Iv9J4eLq1bgFyT3O3iRWU9lZsEE7J72Q==", "dev": true, "requires": { - "@jest/environment": "^26.6.2", - "@jest/fake-timers": "^26.6.2", - "@jest/types": "^26.6.2", + "@jest/types": "^27.2.4", + "execa": "^5.0.0", + "throat": "^6.0.1" + } + }, + "jest-circus": { + "version": "27.2.4", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-27.2.4.tgz", + "integrity": "sha512-TtheheTElrGjlsY9VxkzUU1qwIx05ItIusMVKnvNkMt4o/PeegLRcjq3Db2Jz0GGdBalJdbzLZBgeulZAJxJWA==", + "dev": true, + "requires": { + "@jest/environment": "^27.2.4", + "@jest/test-result": "^27.2.4", + "@jest/types": "^27.2.4", "@types/node": "*", - "jest-mock": "^26.6.2", - "jest-util": "^26.6.2" + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^0.7.0", + "expect": "^27.2.4", + "is-generator-fn": "^2.0.0", + "jest-each": "^27.2.4", + "jest-matcher-utils": "^27.2.4", + "jest-message-util": "^27.2.4", + "jest-runtime": "^27.2.4", + "jest-snapshot": "^27.2.4", + "jest-util": "^27.2.4", + "pretty-format": "^27.2.4", + "slash": "^3.0.0", + "stack-utils": "^2.0.3", + "throat": "^6.0.1" }, "dependencies": { - "@jest/types": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", - "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^15.0.0", - "chalk": "^4.0.0" - } - }, "ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -5039,9 +3195,9 @@ } }, "chalk": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", - "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "requires": { "ansi-styles": "^4.1.0", @@ -5069,20 +3225,6 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, - "jest-util": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-26.6.2.tgz", - "integrity": "sha512-MDW0fKfsn0OI7MS7Euz6h8HNDXVQ0gaM9uW6RjfDmd1DAFcaxX9OqIakHIqhbnmF08Cf2DLDG+ulq8YQQ0Lp0Q==", - "dev": true, - "requires": { - "@jest/types": "^26.6.2", - "@types/node": "*", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.4", - "is-ci": "^2.0.0", - "micromatch": "^4.0.2" - } - }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -5094,47 +3236,35 @@ } } }, - "jest-get-type": { - "version": "26.3.0", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-26.3.0.tgz", - "integrity": "sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig==", - "dev": true - }, - "jest-haste-map": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-26.6.2.tgz", - "integrity": "sha512-easWIJXIw71B2RdR8kgqpjQrbMRWQBgiBwXYEhtGUTaX+doCjBheluShdDMeR8IMfJiTqH4+zfhtg29apJf/8w==", + "jest-config": { + "version": "27.2.4", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-27.2.4.tgz", + "integrity": "sha512-tWy0UxhdzqiKyp4l5Vq4HxLyD+gH5td+GCF3c22/DJ0bYAOsMo+qi2XtbJI6oYMH5JOJQs9nLW/r34nvFCehjA==", "dev": true, "requires": { - "@jest/types": "^26.6.2", - "@types/graceful-fs": "^4.1.2", - "@types/node": "*", - "anymatch": "^3.0.3", - "fb-watchman": "^2.0.0", - "fsevents": "^2.1.2", + "@babel/core": "^7.1.0", + "@jest/test-sequencer": "^27.2.4", + "@jest/types": "^27.2.4", + "babel-jest": "^27.2.4", + "chalk": "^4.0.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.1", "graceful-fs": "^4.2.4", - "jest-regex-util": "^26.0.0", - "jest-serializer": "^26.6.2", - "jest-util": "^26.6.2", - "jest-worker": "^26.6.2", - "micromatch": "^4.0.2", - "sane": "^4.0.3", - "walker": "^1.0.7" + "is-ci": "^3.0.0", + "jest-circus": "^27.2.4", + "jest-environment-jsdom": "^27.2.4", + "jest-environment-node": "^27.2.4", + "jest-get-type": "^27.0.6", + "jest-jasmine2": "^27.2.4", + "jest-regex-util": "^27.0.6", + "jest-resolve": "^27.2.4", + "jest-runner": "^27.2.4", + "jest-util": "^27.2.4", + "jest-validate": "^27.2.4", + "micromatch": "^4.0.4", + "pretty-format": "^27.2.4" }, "dependencies": { - "@jest/types": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", - "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^15.0.0", - "chalk": "^4.0.0" - } - }, "ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -5145,9 +3275,9 @@ } }, "chalk": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", - "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "requires": { "ansi-styles": "^4.1.0", @@ -5175,20 +3305,6 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, - "jest-util": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-26.6.2.tgz", - "integrity": "sha512-MDW0fKfsn0OI7MS7Euz6h8HNDXVQ0gaM9uW6RjfDmd1DAFcaxX9OqIakHIqhbnmF08Cf2DLDG+ulq8YQQ0Lp0Q==", - "dev": true, - "requires": { - "@jest/types": "^26.6.2", - "@types/node": "*", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.4", - "is-ci": "^2.0.0", - "micromatch": "^4.0.2" - } - }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -5200,45 +3316,18 @@ } } }, - "jest-jasmine2": { - "version": "26.6.3", - "resolved": "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-26.6.3.tgz", - "integrity": "sha512-kPKUrQtc8aYwBV7CqBg5pu+tmYXlvFlSFYn18ev4gPFtrRzB15N2gW/Roew3187q2w2eHuu0MU9TJz6w0/nPEg==", + "jest-diff": { + "version": "27.2.4", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-27.2.4.tgz", + "integrity": "sha512-bLAVlDSCR3gqUPGv+4nzVpEXGsHh98HjUL7Vb2hVyyuBDoQmja8eJb0imUABsuxBeUVmf47taJSAd9nDrwWKEg==", "dev": true, "requires": { - "@babel/traverse": "^7.1.0", - "@jest/environment": "^26.6.2", - "@jest/source-map": "^26.6.2", - "@jest/test-result": "^26.6.2", - "@jest/types": "^26.6.2", - "@types/node": "*", "chalk": "^4.0.0", - "co": "^4.6.0", - "expect": "^26.6.2", - "is-generator-fn": "^2.0.0", - "jest-each": "^26.6.2", - "jest-matcher-utils": "^26.6.2", - "jest-message-util": "^26.6.2", - "jest-runtime": "^26.6.3", - "jest-snapshot": "^26.6.2", - "jest-util": "^26.6.2", - "pretty-format": "^26.6.2", - "throat": "^5.0.0" + "diff-sequences": "^27.0.6", + "jest-get-type": "^27.0.6", + "pretty-format": "^27.2.4" }, "dependencies": { - "@jest/types": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", - "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^15.0.0", - "chalk": "^4.0.0" - } - }, "ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -5249,9 +3338,9 @@ } }, "chalk": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", - "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "requires": { "ansi-styles": "^4.1.0", @@ -5279,20 +3368,6 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, - "jest-util": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-26.6.2.tgz", - "integrity": "sha512-MDW0fKfsn0OI7MS7Euz6h8HNDXVQ0gaM9uW6RjfDmd1DAFcaxX9OqIakHIqhbnmF08Cf2DLDG+ulq8YQQ0Lp0Q==", - "dev": true, - "requires": { - "@jest/types": "^26.6.2", - "@types/node": "*", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.4", - "is-ci": "^2.0.0", - "micromatch": "^4.0.2" - } - }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -5304,32 +3379,26 @@ } } }, - "jest-leak-detector": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-26.6.2.tgz", - "integrity": "sha512-i4xlXpsVSMeKvg2cEKdfhh0H39qlJlP5Ex1yQxwF9ubahboQYMgTtz5oML35AVA3B4Eu+YsmwaiKVev9KCvLxg==", + "jest-docblock": { + "version": "27.0.6", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-27.0.6.tgz", + "integrity": "sha512-Fid6dPcjwepTFraz0YxIMCi7dejjJ/KL9FBjPYhBp4Sv1Y9PdhImlKZqYU555BlN4TQKaTc+F2Av1z+anVyGkA==", "dev": true, "requires": { - "jest-get-type": "^26.3.0", - "pretty-format": "^26.6.2" + "detect-newline": "^3.0.0" } }, - "jest-localstorage-mock": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/jest-localstorage-mock/-/jest-localstorage-mock-2.4.3.tgz", - "integrity": "sha512-UgifkHKoWVRUoSqO4Z4Z+Hl1NbiYBVDlmkmulFFeRRneGECWAlAdGWJdyz+2NisjOZnnQoxQl0s5dQ7ch62Jxw==", - "dev": true - }, - "jest-matcher-utils": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-26.6.2.tgz", - "integrity": "sha512-llnc8vQgYcNqDrqRDXWwMr9i7rS5XFiCwvh6DTP7Jqa2mqpcCBBlpCbn+trkG0KNhPu/h8rzyBkriOtBstvWhw==", + "jest-each": { + "version": "27.2.4", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-27.2.4.tgz", + "integrity": "sha512-w9XVc+0EDBUTJS4xBNJ7N2JCcWItFd006lFjz77OarAQcQ10eFDBMrfDv2GBJMKlXe9aq0HrIIF51AXcZrRJyg==", "dev": true, "requires": { + "@jest/types": "^27.2.4", "chalk": "^4.0.0", - "jest-diff": "^26.6.2", - "jest-get-type": "^26.3.0", - "pretty-format": "^26.6.2" + "jest-get-type": "^27.0.6", + "jest-util": "^27.2.4", + "pretty-format": "^27.2.4" }, "dependencies": { "ansi-styles": { @@ -5342,9 +3411,9 @@ } }, "chalk": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", - "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "requires": { "ansi-styles": "^4.1.0", @@ -5383,36 +3452,88 @@ } } }, - "jest-message-util": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-26.6.2.tgz", - "integrity": "sha512-rGiLePzQ3AzwUshu2+Rn+UMFk0pHN58sOG+IaJbk5Jxuqo3NYO1U2/MIR4S1sKgsoYSXSzdtSa0TgrmtUwEbmA==", + "jest-environment-jsdom": { + "version": "27.2.4", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-27.2.4.tgz", + "integrity": "sha512-X70pTXFSypD7AIzKT1mLnDi5hP9w9mdTRcOGOmoDoBrNyNEg4rYm6d4LQWFLc9ps1VnMuDOkFSG0wjSNYGjkng==", "dev": true, "requires": { - "@babel/code-frame": "^7.0.0", - "@jest/types": "^26.6.2", - "@types/stack-utils": "^2.0.0", - "chalk": "^4.0.0", + "@jest/environment": "^27.2.4", + "@jest/fake-timers": "^27.2.4", + "@jest/types": "^27.2.4", + "@types/node": "*", + "jest-mock": "^27.2.4", + "jest-util": "^27.2.4", + "jsdom": "^16.6.0" + } + }, + "jest-environment-node": { + "version": "27.2.4", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-27.2.4.tgz", + "integrity": "sha512-ZbVbFSnbzTvhLOIkqh5lcLuGCCFvtG4xTXIRPK99rV2KzQT3kNg16KZwfTnLNlIiWCE8do960eToeDfcqmpSAw==", + "dev": true, + "requires": { + "@jest/environment": "^27.2.4", + "@jest/fake-timers": "^27.2.4", + "@jest/types": "^27.2.4", + "@types/node": "*", + "jest-mock": "^27.2.4", + "jest-util": "^27.2.4" + } + }, + "jest-get-type": { + "version": "27.0.6", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.0.6.tgz", + "integrity": "sha512-XTkK5exIeUbbveehcSR8w0bhH+c0yloW/Wpl+9vZrjzztCPWrxhHwkIFpZzCt71oRBsgxmuUfxEqOYoZI2macg==", + "dev": true + }, + "jest-haste-map": { + "version": "27.2.4", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-27.2.4.tgz", + "integrity": "sha512-bkJ4bT00T2K+1NZXbRcyKnbJ42I6QBvoDNMTAQQDBhaGNnZreiQKUNqax0e6hLTx7E75pKDeltVu3V1HAdu+YA==", + "dev": true, + "requires": { + "@jest/types": "^27.2.4", + "@types/graceful-fs": "^4.1.2", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "fsevents": "^2.3.2", "graceful-fs": "^4.2.4", - "micromatch": "^4.0.2", - "pretty-format": "^26.6.2", - "slash": "^3.0.0", - "stack-utils": "^2.0.2" + "jest-regex-util": "^27.0.6", + "jest-serializer": "^27.0.6", + "jest-util": "^27.2.4", + "jest-worker": "^27.2.4", + "micromatch": "^4.0.4", + "walker": "^1.0.7" + } + }, + "jest-jasmine2": { + "version": "27.2.4", + "resolved": "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-27.2.4.tgz", + "integrity": "sha512-fcffjO/xLWLVnW2ct3No4EksxM5RyPwHDYu9QU+90cC+/eSMLkFAxS55vkqsxexOO5zSsZ3foVpMQcg/amSeIQ==", + "dev": true, + "requires": { + "@babel/traverse": "^7.1.0", + "@jest/environment": "^27.2.4", + "@jest/source-map": "^27.0.6", + "@jest/test-result": "^27.2.4", + "@jest/types": "^27.2.4", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "expect": "^27.2.4", + "is-generator-fn": "^2.0.0", + "jest-each": "^27.2.4", + "jest-matcher-utils": "^27.2.4", + "jest-message-util": "^27.2.4", + "jest-runtime": "^27.2.4", + "jest-snapshot": "^27.2.4", + "jest-util": "^27.2.4", + "pretty-format": "^27.2.4", + "throat": "^6.0.1" }, "dependencies": { - "@jest/types": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", - "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^15.0.0", - "chalk": "^4.0.0" - } - }, "ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -5423,9 +3544,9 @@ } }, "chalk": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", - "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "requires": { "ansi-styles": "^4.1.0", @@ -5464,29 +3585,34 @@ } } }, - "jest-mock": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-26.6.2.tgz", - "integrity": "sha512-YyFjePHHp1LzpzYcmgqkJ0nm0gg/lJx2aZFzFy1S6eUqNjXsOqTK10zNRff2dNfssgokjkG65OlWNcIlgd3zew==", + "jest-leak-detector": { + "version": "27.2.4", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-27.2.4.tgz", + "integrity": "sha512-SrcHWbe0EHg/bw2uBjVoHacTo5xosl068x2Q0aWsjr2yYuW2XwqrSkZV4lurUop0jhv1709ymG4or+8E4sH27Q==", "dev": true, "requires": { - "@jest/types": "^26.6.2", - "@types/node": "*" + "jest-get-type": "^27.0.6", + "pretty-format": "^27.2.4" + } + }, + "jest-localstorage-mock": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/jest-localstorage-mock/-/jest-localstorage-mock-2.4.3.tgz", + "integrity": "sha512-UgifkHKoWVRUoSqO4Z4Z+Hl1NbiYBVDlmkmulFFeRRneGECWAlAdGWJdyz+2NisjOZnnQoxQl0s5dQ7ch62Jxw==", + "dev": true + }, + "jest-matcher-utils": { + "version": "27.2.4", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-27.2.4.tgz", + "integrity": "sha512-nQeLfFAIPPkyhkDfifAPfP/U5wm1x0fLtAzqXZSSKckXDNuk2aaOfQiDYv1Mgf5GY6yOsxfUnvNm3dDjXM+BXw==", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "jest-diff": "^27.2.4", + "jest-get-type": "^27.0.6", + "pretty-format": "^27.2.4" }, "dependencies": { - "@jest/types": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", - "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^15.0.0", - "chalk": "^4.0.0" - } - }, "ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -5497,9 +3623,9 @@ } }, "chalk": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", - "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "requires": { "ansi-styles": "^4.1.0", @@ -5538,47 +3664,23 @@ } } }, - "jest-pnp-resolver": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz", - "integrity": "sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w==", - "dev": true - }, - "jest-regex-util": { - "version": "26.0.0", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-26.0.0.tgz", - "integrity": "sha512-Gv3ZIs/nA48/Zvjrl34bf+oD76JHiGDUxNOVgUjh3j890sblXryjY4rss71fPtD/njchl6PSE2hIhvyWa1eT0A==", - "dev": true - }, - "jest-resolve": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-26.6.2.tgz", - "integrity": "sha512-sOxsZOq25mT1wRsfHcbtkInS+Ek7Q8jCHUB0ZUTP0tc/c41QHriU/NunqMfCUWsL4H3MHpvQD4QR9kSYhS7UvQ==", + "jest-message-util": { + "version": "27.2.4", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-27.2.4.tgz", + "integrity": "sha512-wbKT/BNGnBVB9nzi+IoaLkXt6fbSvqUxx+IYY66YFh96J3goY33BAaNG3uPqaw/Sh/FR9YpXGVDfd5DJdbh4nA==", "dev": true, "requires": { - "@jest/types": "^26.6.2", + "@babel/code-frame": "^7.12.13", + "@jest/types": "^27.2.4", + "@types/stack-utils": "^2.0.0", "chalk": "^4.0.0", "graceful-fs": "^4.2.4", - "jest-pnp-resolver": "^1.2.2", - "jest-util": "^26.6.2", - "read-pkg-up": "^7.0.1", - "resolve": "^1.18.1", - "slash": "^3.0.0" + "micromatch": "^4.0.4", + "pretty-format": "^27.2.4", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" }, "dependencies": { - "@jest/types": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", - "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^15.0.0", - "chalk": "^4.0.0" - } - }, "ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -5589,9 +3691,9 @@ } }, "chalk": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", - "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "requires": { "ansi-styles": "^4.1.0", @@ -5619,20 +3721,6 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, - "jest-util": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-26.6.2.tgz", - "integrity": "sha512-MDW0fKfsn0OI7MS7Euz6h8HNDXVQ0gaM9uW6RjfDmd1DAFcaxX9OqIakHIqhbnmF08Cf2DLDG+ulq8YQQ0Lp0Q==", - "dev": true, - "requires": { - "@jest/types": "^26.6.2", - "@types/node": "*", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.4", - "is-ci": "^2.0.0", - "micromatch": "^4.0.2" - } - }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -5644,30 +3732,46 @@ } } }, - "jest-resolve-dependencies": { - "version": "26.6.3", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-26.6.3.tgz", - "integrity": "sha512-pVwUjJkxbhe4RY8QEWzN3vns2kqyuldKpxlxJlzEYfKSvY6/bMvxoFrYYzUO1Gx28yKWN37qyV7rIoIp2h8fTg==", + "jest-mock": { + "version": "27.2.4", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-27.2.4.tgz", + "integrity": "sha512-iVRU905rutaAoUcrt5Tm1JoHHWi24YabqEGXjPJI4tAyA6wZ7mzDi3GrZ+M7ebgWBqUkZE93GAx1STk7yCMIQA==", + "dev": true, + "requires": { + "@jest/types": "^27.2.4", + "@types/node": "*" + } + }, + "jest-pnp-resolver": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz", + "integrity": "sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w==", + "dev": true + }, + "jest-regex-util": { + "version": "27.0.6", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-27.0.6.tgz", + "integrity": "sha512-SUhPzBsGa1IKm8hx2F4NfTGGp+r7BXJ4CulsZ1k2kI+mGLG+lxGrs76veN2LF/aUdGosJBzKgXmNCw+BzFqBDQ==", + "dev": true + }, + "jest-resolve": { + "version": "27.2.4", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-27.2.4.tgz", + "integrity": "sha512-IsAO/3+3BZnKjI2I4f3835TBK/90dxR7Otgufn3mnrDFTByOSXclDi3G2XJsawGV4/18IMLARJ+V7Wm7t+J89Q==", "dev": true, "requires": { - "@jest/types": "^26.6.2", - "jest-regex-util": "^26.0.0", - "jest-snapshot": "^26.6.2" + "@jest/types": "^27.2.4", + "chalk": "^4.0.0", + "escalade": "^3.1.1", + "graceful-fs": "^4.2.4", + "jest-haste-map": "^27.2.4", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^27.2.4", + "jest-validate": "^27.2.4", + "resolve": "^1.20.0", + "slash": "^3.0.0" }, "dependencies": { - "@jest/types": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", - "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^15.0.0", - "chalk": "^4.0.0" - } - }, "ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -5678,9 +3782,9 @@ } }, "chalk": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", - "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "requires": { "ansi-styles": "^4.1.0", @@ -5719,47 +3823,47 @@ } } }, + "jest-resolve-dependencies": { + "version": "27.2.4", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-27.2.4.tgz", + "integrity": "sha512-i5s7Uh9B3Q6uwxLpMhNKlgBf6pcemvWaORxsW1zNF/YCY3jd5EftvnGBI+fxVwJ1CBxkVfxqCvm1lpZkbaoGmg==", + "dev": true, + "requires": { + "@jest/types": "^27.2.4", + "jest-regex-util": "^27.0.6", + "jest-snapshot": "^27.2.4" + } + }, "jest-runner": { - "version": "26.6.3", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-26.6.3.tgz", - "integrity": "sha512-atgKpRHnaA2OvByG/HpGA4g6CSPS/1LK0jK3gATJAoptC1ojltpmVlYC3TYgdmGp+GLuhzpH30Gvs36szSL2JQ==", + "version": "27.2.4", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-27.2.4.tgz", + "integrity": "sha512-hIo5PPuNUyVDidZS8EetntuuJbQ+4IHWxmHgYZz9FIDbG2wcZjrP6b52uMDjAEQiHAn8yn8ynNe+TL8UuGFYKg==", "dev": true, "requires": { - "@jest/console": "^26.6.2", - "@jest/environment": "^26.6.2", - "@jest/test-result": "^26.6.2", - "@jest/types": "^26.6.2", + "@jest/console": "^27.2.4", + "@jest/environment": "^27.2.4", + "@jest/test-result": "^27.2.4", + "@jest/transform": "^27.2.4", + "@jest/types": "^27.2.4", "@types/node": "*", "chalk": "^4.0.0", - "emittery": "^0.7.1", + "emittery": "^0.8.1", "exit": "^0.1.2", "graceful-fs": "^4.2.4", - "jest-config": "^26.6.3", - "jest-docblock": "^26.0.0", - "jest-haste-map": "^26.6.2", - "jest-leak-detector": "^26.6.2", - "jest-message-util": "^26.6.2", - "jest-resolve": "^26.6.2", - "jest-runtime": "^26.6.3", - "jest-util": "^26.6.2", - "jest-worker": "^26.6.2", + "jest-docblock": "^27.0.6", + "jest-environment-jsdom": "^27.2.4", + "jest-environment-node": "^27.2.4", + "jest-haste-map": "^27.2.4", + "jest-leak-detector": "^27.2.4", + "jest-message-util": "^27.2.4", + "jest-resolve": "^27.2.4", + "jest-runtime": "^27.2.4", + "jest-util": "^27.2.4", + "jest-worker": "^27.2.4", "source-map-support": "^0.5.6", - "throat": "^5.0.0" + "throat": "^6.0.1" }, "dependencies": { - "@jest/types": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", - "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^15.0.0", - "chalk": "^4.0.0" - } - }, "ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -5770,9 +3874,9 @@ } }, "chalk": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", - "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "requires": { "ansi-styles": "^4.1.0", @@ -5800,20 +3904,6 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, - "jest-util": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-26.6.2.tgz", - "integrity": "sha512-MDW0fKfsn0OI7MS7Euz6h8HNDXVQ0gaM9uW6RjfDmd1DAFcaxX9OqIakHIqhbnmF08Cf2DLDG+ulq8YQQ0Lp0Q==", - "dev": true, - "requires": { - "@jest/types": "^26.6.2", - "@types/node": "*", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.4", - "is-ci": "^2.0.0", - "micromatch": "^4.0.2" - } - }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -5826,53 +3916,40 @@ } }, "jest-runtime": { - "version": "26.6.3", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-26.6.3.tgz", - "integrity": "sha512-lrzyR3N8sacTAMeonbqpnSka1dHNux2uk0qqDXVkMv2c/A3wYnvQ4EXuI013Y6+gSKSCxdaczvf4HF0mVXHRdw==", - "dev": true, - "requires": { - "@jest/console": "^26.6.2", - "@jest/environment": "^26.6.2", - "@jest/fake-timers": "^26.6.2", - "@jest/globals": "^26.6.2", - "@jest/source-map": "^26.6.2", - "@jest/test-result": "^26.6.2", - "@jest/transform": "^26.6.2", - "@jest/types": "^26.6.2", - "@types/yargs": "^15.0.0", + "version": "27.2.4", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-27.2.4.tgz", + "integrity": "sha512-ICKzzYdjIi70P17MZsLLIgIQFCQmIjMFf+xYww3aUySiUA/QBPUTdUqo5B2eg4HOn9/KkUsV0z6GVgaqAPBJvg==", + "dev": true, + "requires": { + "@jest/console": "^27.2.4", + "@jest/environment": "^27.2.4", + "@jest/fake-timers": "^27.2.4", + "@jest/globals": "^27.2.4", + "@jest/source-map": "^27.0.6", + "@jest/test-result": "^27.2.4", + "@jest/transform": "^27.2.4", + "@jest/types": "^27.2.4", + "@types/yargs": "^16.0.0", "chalk": "^4.0.0", - "cjs-module-lexer": "^0.6.0", + "cjs-module-lexer": "^1.0.0", "collect-v8-coverage": "^1.0.0", + "execa": "^5.0.0", "exit": "^0.1.2", "glob": "^7.1.3", "graceful-fs": "^4.2.4", - "jest-config": "^26.6.3", - "jest-haste-map": "^26.6.2", - "jest-message-util": "^26.6.2", - "jest-mock": "^26.6.2", - "jest-regex-util": "^26.0.0", - "jest-resolve": "^26.6.2", - "jest-snapshot": "^26.6.2", - "jest-util": "^26.6.2", - "jest-validate": "^26.6.2", + "jest-haste-map": "^27.2.4", + "jest-message-util": "^27.2.4", + "jest-mock": "^27.2.4", + "jest-regex-util": "^27.0.6", + "jest-resolve": "^27.2.4", + "jest-snapshot": "^27.2.4", + "jest-util": "^27.2.4", + "jest-validate": "^27.2.4", "slash": "^3.0.0", "strip-bom": "^4.0.0", - "yargs": "^15.4.1" + "yargs": "^16.2.0" }, "dependencies": { - "@jest/types": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", - "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^15.0.0", - "chalk": "^4.0.0" - } - }, "ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -5883,9 +3960,9 @@ } }, "chalk": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", - "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "requires": { "ansi-styles": "^4.1.0", @@ -5913,20 +3990,6 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, - "jest-util": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-26.6.2.tgz", - "integrity": "sha512-MDW0fKfsn0OI7MS7Euz6h8HNDXVQ0gaM9uW6RjfDmd1DAFcaxX9OqIakHIqhbnmF08Cf2DLDG+ulq8YQQ0Lp0Q==", - "dev": true, - "requires": { - "@jest/types": "^26.6.2", - "@types/node": "*", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.4", - "is-ci": "^2.0.0", - "micromatch": "^4.0.2" - } - }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -5939,9 +4002,9 @@ } }, "jest-serializer": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-serializer/-/jest-serializer-26.6.2.tgz", - "integrity": "sha512-S5wqyz0DXnNJPd/xfIzZ5Xnp1HrJWBczg8mMfMpN78OJ5eDxXyf+Ygld9wX1DnUWbIbhM1YDY95NjR4CBXkb2g==", + "version": "27.0.6", + "resolved": "https://registry.npmjs.org/jest-serializer/-/jest-serializer-27.0.6.tgz", + "integrity": "sha512-PtGdVK9EGC7dsaziskfqaAPib6wTViY3G8E5wz9tLVPhHyiDNTZn/xjZ4khAw+09QkoOVpn7vF5nPSN6dtBexA==", "dev": true, "requires": { "@types/node": "*", @@ -5949,42 +4012,37 @@ } }, "jest-snapshot": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-26.6.2.tgz", - "integrity": "sha512-OLhxz05EzUtsAmOMzuupt1lHYXCNib0ECyuZ/PZOx9TrZcC8vL0x+DUG3TL+GLX3yHG45e6YGjIm0XwDc3q3og==", + "version": "27.2.4", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-27.2.4.tgz", + "integrity": "sha512-5DFxK31rYS8X8C6WXsFx8XxrxW3PGa6+9IrUcZdTLg1aEyXDGIeiBh4jbwvh655bg/9vTETbEj/njfZicHTZZw==", "dev": true, "requires": { + "@babel/core": "^7.7.2", + "@babel/generator": "^7.7.2", + "@babel/parser": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/traverse": "^7.7.2", "@babel/types": "^7.0.0", - "@jest/types": "^26.6.2", + "@jest/transform": "^27.2.4", + "@jest/types": "^27.2.4", "@types/babel__traverse": "^7.0.4", - "@types/prettier": "^2.0.0", + "@types/prettier": "^2.1.5", + "babel-preset-current-node-syntax": "^1.0.0", "chalk": "^4.0.0", - "expect": "^26.6.2", + "expect": "^27.2.4", "graceful-fs": "^4.2.4", - "jest-diff": "^26.6.2", - "jest-get-type": "^26.3.0", - "jest-haste-map": "^26.6.2", - "jest-matcher-utils": "^26.6.2", - "jest-message-util": "^26.6.2", - "jest-resolve": "^26.6.2", + "jest-diff": "^27.2.4", + "jest-get-type": "^27.0.6", + "jest-haste-map": "^27.2.4", + "jest-matcher-utils": "^27.2.4", + "jest-message-util": "^27.2.4", + "jest-resolve": "^27.2.4", + "jest-util": "^27.2.4", "natural-compare": "^1.4.0", - "pretty-format": "^26.6.2", + "pretty-format": "^27.2.4", "semver": "^7.3.2" }, "dependencies": { - "@jest/types": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", - "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^15.0.0", - "chalk": "^4.0.0" - } - }, "ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -5995,9 +4053,9 @@ } }, "chalk": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", - "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "requires": { "ansi-styles": "^4.1.0", @@ -6046,42 +4104,32 @@ } }, "jest-util": { - "version": "26.3.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-26.3.0.tgz", - "integrity": "sha512-4zpn6bwV0+AMFN0IYhH/wnzIQzRaYVrz1A8sYnRnj4UXDXbOVtWmlaZkO9mipFqZ13okIfN87aDoJWB7VH6hcw==", + "version": "27.2.4", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-27.2.4.tgz", + "integrity": "sha512-mW++4u+fSvAt3YBWm5IpbmRAceUqa2B++JlUZTiuEt2AmNYn0Yw5oay4cP17TGsMINRNPSGiJ2zNnX60g+VbFg==", "dev": true, "requires": { - "@jest/types": "^26.3.0", + "@jest/types": "^27.2.4", "@types/node": "*", "chalk": "^4.0.0", "graceful-fs": "^4.2.4", - "is-ci": "^2.0.0", - "micromatch": "^4.0.2" + "is-ci": "^3.0.0", + "picomatch": "^2.2.3" }, "dependencies": { "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "requires": { - "@types/color-name": "^1.1.1", "color-convert": "^2.0.1" } }, - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, "chalk": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", - "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "requires": { "ansi-styles": "^4.1.0", @@ -6103,37 +4151,18 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "picomatch": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", + "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==", "dev": true }, - "micromatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", - "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", - "dev": true, - "requires": { - "braces": "^3.0.1", - "picomatch": "^2.0.5" - } - }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -6142,45 +4171,23 @@ "requires": { "has-flag": "^4.0.0" } - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } } } }, "jest-validate": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-26.6.2.tgz", - "integrity": "sha512-NEYZ9Aeyj0i5rQqbq+tpIOom0YS1u2MVu6+euBsvpgIme+FOfRmoC4R5p0JiAUpaFvFy24xgrpMknarR/93XjQ==", + "version": "27.2.4", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-27.2.4.tgz", + "integrity": "sha512-VMtbxbkd7LHnIH7PChdDtrluCFRJ4b1YV2YJzNwwsASMWftq/HgqiqjvptBOWyWOtevgO3f14wPxkPcLlVBRog==", "dev": true, "requires": { - "@jest/types": "^26.6.2", - "camelcase": "^6.0.0", + "@jest/types": "^27.2.4", + "camelcase": "^6.2.0", "chalk": "^4.0.0", - "jest-get-type": "^26.3.0", + "jest-get-type": "^27.0.6", "leven": "^3.1.0", - "pretty-format": "^26.6.2" + "pretty-format": "^27.2.4" }, "dependencies": { - "@jest/types": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", - "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^15.0.0", - "chalk": "^4.0.0" - } - }, "ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -6197,9 +4204,9 @@ "dev": true }, "chalk": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", - "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "requires": { "ansi-styles": "^4.1.0", @@ -6239,33 +4246,20 @@ } }, "jest-watcher": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-26.6.2.tgz", - "integrity": "sha512-WKJob0P/Em2csiVthsI68p6aGKTIcsfjH9Gsx1f0A3Italz43e3ho0geSAVsmj09RWOELP1AZ/DXyJgOgDKxXQ==", + "version": "27.2.4", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-27.2.4.tgz", + "integrity": "sha512-LXC/0+dKxhK7cfF7reflRYlzDIaQE+fL4ynhKhzg8IMILNMuI4xcjXXfUJady7OR4/TZeMg7X8eHx8uan9vqaQ==", "dev": true, "requires": { - "@jest/test-result": "^26.6.2", - "@jest/types": "^26.6.2", + "@jest/test-result": "^27.2.4", + "@jest/types": "^27.2.4", "@types/node": "*", "ansi-escapes": "^4.2.1", "chalk": "^4.0.0", - "jest-util": "^26.6.2", + "jest-util": "^27.2.4", "string-length": "^4.0.1" }, "dependencies": { - "@jest/types": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", - "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^15.0.0", - "chalk": "^4.0.0" - } - }, "ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -6276,9 +4270,9 @@ } }, "chalk": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", - "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "requires": { "ansi-styles": "^4.1.0", @@ -6306,20 +4300,6 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, - "jest-util": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-26.6.2.tgz", - "integrity": "sha512-MDW0fKfsn0OI7MS7Euz6h8HNDXVQ0gaM9uW6RjfDmd1DAFcaxX9OqIakHIqhbnmF08Cf2DLDG+ulq8YQQ0Lp0Q==", - "dev": true, - "requires": { - "@jest/types": "^26.6.2", - "@types/node": "*", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.4", - "is-ci": "^2.0.0", - "micromatch": "^4.0.2" - } - }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -6332,14 +4312,14 @@ } }, "jest-worker": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-26.6.2.tgz", - "integrity": "sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==", + "version": "27.2.4", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.2.4.tgz", + "integrity": "sha512-Zq9A2Pw59KkVjBBKD1i3iE2e22oSjXhUKKuAK1HGX8flGwkm6NMozyEYzKd41hXc64dbd/0eWFeEEuxqXyhM+g==", "dev": true, "requires": { "@types/node": "*", "merge-stream": "^2.0.0", - "supports-color": "^7.0.0" + "supports-color": "^8.0.0" }, "dependencies": { "has-flag": { @@ -6349,9 +4329,9 @@ "dev": true }, "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dev": true, "requires": { "has-flag": "^4.0.0" @@ -6375,20 +4355,14 @@ "esprima": "^4.0.0" } }, - "jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", - "dev": true - }, "jsdom": { - "version": "16.5.2", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-16.5.2.tgz", - "integrity": "sha512-JxNtPt9C1ut85boCbJmffaQ06NBnzkQY/MWO3YxPW8IWS38A26z+B1oBvA9LwKrytewdfymnhi4UNH3/RAgZrg==", + "version": "16.7.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-16.7.0.tgz", + "integrity": "sha512-u9Smc2G1USStM+s/x1ru5Sxrl6mPYCbByG1U/hUmqaVsm4tbNyS7CicOSRyuGQYZhTu0h84qkZZQ/I+dzizSVw==", "dev": true, "requires": { "abab": "^2.0.5", - "acorn": "^8.1.0", + "acorn": "^8.2.4", "acorn-globals": "^6.0.0", "cssom": "^0.4.4", "cssstyle": "^2.3.0", @@ -6396,12 +4370,13 @@ "decimal.js": "^10.2.1", "domexception": "^2.0.1", "escodegen": "^2.0.0", + "form-data": "^3.0.0", "html-encoding-sniffer": "^2.0.1", - "is-potential-custom-element-name": "^1.0.0", + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "^5.0.0", + "is-potential-custom-element-name": "^1.0.1", "nwsapi": "^2.2.0", "parse5": "6.0.1", - "request": "^2.88.2", - "request-promise-native": "^1.0.9", "saxes": "^5.0.1", "symbol-tree": "^3.2.4", "tough-cookie": "^4.0.0", @@ -6411,14 +4386,14 @@ "whatwg-encoding": "^1.0.5", "whatwg-mimetype": "^2.3.0", "whatwg-url": "^8.5.0", - "ws": "^7.4.4", + "ws": "^7.4.6", "xml-name-validator": "^3.0.0" }, "dependencies": { "acorn": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.1.0.tgz", - "integrity": "sha512-LWCF/Wn0nfHOmJ9rzQApGnxnvgfROzGilS8936rqN/lfcYkY9MYZzdMqN+2NJ4SlTc+m5HiSa+kNfDtI64dwUA==", + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.5.0.tgz", + "integrity": "sha512-yXbYeFy+jUuYd3/CDcg2NkIYE991XYX/bje7LmjJigUciaeO1JR4XxXgCIV1/Zc/dRuFEyw1L0pbA+qynJkW5Q==", "dev": true } } @@ -6429,18 +4404,6 @@ "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", "dev": true }, - "json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true - }, - "json-schema": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", - "dev": true - }, "json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -6453,39 +4416,15 @@ "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", "dev": true }, - "json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", - "dev": true - }, "json5": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz", - "integrity": "sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz", + "integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==", "dev": true, "requires": { "minimist": "^1.2.5" } }, - "jsprim": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", - "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", - "dev": true, - "requires": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.2.3", - "verror": "1.10.0" - } - }, - "kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true - }, "kleur": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", @@ -6508,12 +4447,6 @@ "type-check": "~0.3.2" } }, - "lines-and-columns": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz", - "integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=", - "dev": true - }, "locate-path": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", @@ -6529,6 +4462,12 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "dev": true }, + "lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=", + "dev": true + }, "lodash.defaults": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", @@ -6553,12 +4492,24 @@ "integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=", "dev": true }, + "lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, "lodash.sortby": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=", "dev": true }, + "lodash.truncate": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", + "integrity": "sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM=", + "dev": true + }, "lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -6592,21 +4543,6 @@ "tmpl": "1.0.x" } }, - "map-cache": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", - "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", - "dev": true - }, - "map-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", - "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", - "dev": true, - "requires": { - "object-visit": "^1.0.0" - } - }, "mdn-browser-compat-data": { "version": "1.0.35", "resolved": "https://registry.npmjs.org/mdn-browser-compat-data/-/mdn-browser-compat-data-1.0.35.tgz", @@ -6629,28 +4565,36 @@ "dev": true }, "micromatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", - "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", + "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", "dev": true, "requires": { "braces": "^3.0.1", - "picomatch": "^2.0.5" + "picomatch": "^2.2.3" + }, + "dependencies": { + "picomatch": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", + "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==", + "dev": true + } } }, "mime-db": { - "version": "1.46.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.46.0.tgz", - "integrity": "sha512-svXaP8UQRZ5K7or+ZmfNhg2xX3yKDMUzqadsSqi4NCH/KomcH75MAMYAGVlvXn4+b/xOPhS3I2uHKRUzvjY7BQ==", + "version": "1.50.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.50.0.tgz", + "integrity": "sha512-9tMZCDlYHqeERXEHO9f/hKfNXhre5dK2eE/krIvUjZbS2KPcqGDfNShIWS1uW9XOTKQKqK6qbeOci18rbfW77A==", "dev": true }, "mime-types": { - "version": "2.1.29", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.29.tgz", - "integrity": "sha512-Y/jMt/S5sR9OaqteJtslsFZKWOIIqMACsJSiHghlCAyhf7jfVYjKBmLiX8OgpWeW+fjJ2b+Az69aPFPkUOY6xQ==", + "version": "2.1.33", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.33.tgz", + "integrity": "sha512-plLElXp7pRDd0bNZHw+nMd52vRYjLwQjygaNg7ddJ2uJtTlmnTCjWuPKxVu6//AdaRuME84SvLW91sIkBqGT0g==", "dev": true, "requires": { - "mime-db": "1.46.0" + "mime-db": "1.50.0" } }, "mimic-fn": { @@ -6674,70 +4618,18 @@ "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", "dev": true }, - "mixin-deep": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", - "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", - "dev": true, - "requires": { - "for-in": "^1.0.2", - "is-extendable": "^1.0.1" - }, - "dependencies": { - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "requires": { - "is-plain-object": "^2.0.4" - } - } - } - }, - "mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "dev": true - }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, - "nanomatch": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", - "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", - "dev": true, - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "fragment-cache": "^0.2.1", - "is-windows": "^1.0.2", - "kind-of": "^6.0.2", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - } - }, "natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", "dev": true }, - "nice-try": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", - "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", - "dev": true - }, "node-fetch": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", @@ -6745,43 +4637,16 @@ "dev": true }, "node-int64": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", - "integrity": "sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs=", - "dev": true - }, - "node-modules-regexp": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-modules-regexp/-/node-modules-regexp-1.0.0.tgz", - "integrity": "sha1-jZ2+KJZKSsVxLpExZCEHxx6Q7EA=", - "dev": true - }, - "node-notifier": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-8.0.2.tgz", - "integrity": "sha512-oJP/9NAdd9+x2Q+rfphB2RJCHjod70RcRLjosiPMMu5gjIfwVnOUGq2nbTjTUbmy0DJ/tFIVT30+Qe3nzl4TJg==", - "dev": true, - "optional": true, - "requires": { - "growly": "^1.3.0", - "is-wsl": "^2.2.0", - "semver": "^7.3.2", - "shellwords": "^0.1.1", - "uuid": "^8.3.0", - "which": "^2.0.2" - }, - "dependencies": { - "semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", - "dev": true, - "optional": true, - "requires": { - "lru-cache": "^6.0.0" - } - } - } + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs=", + "dev": true + }, + "node-modules-regexp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-modules-regexp/-/node-modules-regexp-1.0.0.tgz", + "integrity": "sha1-jZ2+KJZKSsVxLpExZCEHxx6Q7EA=", + "dev": true }, "node-releases": { "version": "1.1.73", @@ -6789,26 +4654,6 @@ "integrity": "sha512-uW7fodD6pyW2FZNZnp/Z3hvWKeEW1Y8R1+1CnErE8cXFXzl5blBOoVB41CvMer6P6Q0S5FXDwcHgFd1Wj0U9zg==", "dev": true }, - "normalize-package-data": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", - "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", - "dev": true, - "requires": { - "hosted-git-info": "^2.1.4", - "resolve": "^1.10.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" - }, - "dependencies": { - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - } - } - }, "normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -6816,20 +4661,12 @@ "dev": true }, "npm-run-path": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", - "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", "dev": true, "requires": { - "path-key": "^2.0.0" - }, - "dependencies": { - "path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", - "dev": true - } + "path-key": "^3.0.0" } }, "nwsapi": { @@ -6838,66 +4675,11 @@ "integrity": "sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ==", "dev": true }, - "oauth-sign": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", - "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", - "dev": true - }, "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" }, - "object-copy": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", - "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", - "dev": true, - "requires": { - "copy-descriptor": "^0.1.0", - "define-property": "^0.2.5", - "kind-of": "^3.0.3" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "object-visit": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", - "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", - "dev": true, - "requires": { - "isobject": "^3.0.0" - } - }, - "object.pick": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", - "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", - "dev": true, - "requires": { - "isobject": "^3.0.1" - } - }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -6930,18 +4712,6 @@ "word-wrap": "~1.2.3" } }, - "p-each-series": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-each-series/-/p-each-series-2.2.0.tgz", - "integrity": "sha512-ycIL2+1V32th+8scbpTvyHNaHe02z0sjgh91XXjAk+ZeXoPN4Z46DVUnzdso0aX4KckKw0FNNFHdjZ2UsZvxiA==", - "dev": true - }, - "p-finally": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", - "dev": true - }, "p-limit": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", @@ -6981,30 +4751,12 @@ "callsites": "^3.0.0" } }, - "parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - } - }, "parse5": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", "dev": true }, - "pascalcase": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", - "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=", - "dev": true - }, "path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -7041,12 +4793,6 @@ "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", "dev": true }, - "performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", - "dev": true - }, "picomatch": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", @@ -7071,12 +4817,6 @@ "find-up": "^4.0.0" } }, - "posix-character-classes": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", - "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", - "dev": true - }, "prelude-ls": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", @@ -7084,84 +4824,22 @@ "dev": true }, "pretty-format": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.2.tgz", - "integrity": "sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg==", + "version": "27.2.4", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.2.4.tgz", + "integrity": "sha512-NUjw22WJHldzxyps2YjLZkUj6q1HvjqFezkB9Y2cklN8NtVZN/kZEXGZdFw4uny3oENzV5EEMESrkI0YDUH8vg==", "dev": true, "requires": { - "@jest/types": "^26.6.2", - "ansi-regex": "^5.0.0", - "ansi-styles": "^4.0.0", + "@jest/types": "^27.2.4", + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", "react-is": "^17.0.1" }, "dependencies": { - "@jest/types": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", - "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", - "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^15.0.0", - "chalk": "^4.0.0" - } - }, "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", - "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "react-is": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } } } }, @@ -7184,9 +4862,9 @@ "dev": true }, "prompts": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.0.tgz", - "integrity": "sha512-awZAKrk3vN6CroQukBL+R9051a4R3zCZBlJm/HBfrSZ8iTpYix3VX1vU4mveiLpiwmOJT4wokTF9m6HUk4KqWQ==", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.1.tgz", + "integrity": "sha512-EQyfIuO2hPDsX1L/blblV+H7I0knhgAd82cVneCwcdND9B8AuCDuRcBH6yIcG4dFzlOUqbazQqwGjx5xmsNLuQ==", "dev": true, "requires": { "kleur": "^3.0.3", @@ -7199,28 +4877,12 @@ "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==", "dev": true }, - "pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dev": true, - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, "punycode": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", "dev": true }, - "qs": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", - "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", - "dev": true - }, "querystring": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", @@ -7228,42 +4890,11 @@ "dev": true }, "react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true }, - "read-pkg": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", - "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", - "dev": true, - "requires": { - "@types/normalize-package-data": "^2.4.0", - "normalize-package-data": "^2.5.0", - "parse-json": "^5.0.0", - "type-fest": "^0.6.0" - }, - "dependencies": { - "type-fest": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", - "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", - "dev": true - } - } - }, - "read-pkg-up": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", - "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", - "dev": true, - "requires": { - "find-up": "^4.1.0", - "read-pkg": "^5.2.0", - "type-fest": "^0.8.1" - } - }, "readable-stream": { "version": "2.3.7", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", @@ -7309,128 +4940,22 @@ "promise-queue": "^2.2.5" } }, - "regex-not": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", - "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", - "dev": true, - "requires": { - "extend-shallow": "^3.0.2", - "safe-regex": "^1.1.0" - } - }, "regexpp": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.1.0.tgz", "integrity": "sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q==", "dev": true }, - "remove-trailing-separator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", - "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=", - "dev": true - }, - "repeat-element": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz", - "integrity": "sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==", - "dev": true - }, - "repeat-string": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", - "dev": true - }, - "request": { - "version": "2.88.2", - "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", - "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", - "dev": true, - "requires": { - "aws-sign2": "~0.7.0", - "aws4": "^1.8.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "form-data": "~2.3.2", - "har-validator": "~5.1.3", - "http-signature": "~1.2.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.19", - "oauth-sign": "~0.9.0", - "performance-now": "^2.1.0", - "qs": "~6.5.2", - "safe-buffer": "^5.1.2", - "tough-cookie": "~2.5.0", - "tunnel-agent": "^0.6.0", - "uuid": "^3.3.2" - }, - "dependencies": { - "tough-cookie": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", - "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", - "dev": true, - "requires": { - "psl": "^1.1.28", - "punycode": "^2.1.1" - } - }, - "uuid": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", - "dev": true - } - } - }, - "request-promise-core": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.4.tgz", - "integrity": "sha512-TTbAfBBRdWD7aNNOoVOBH4pN/KigV6LyapYNNlAPA8JwbovRti1E88m3sYAwsLi5ryhPKsE9APwnjFTgdUjTpw==", - "dev": true, - "requires": { - "lodash": "^4.17.19" - } - }, - "request-promise-native": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.9.tgz", - "integrity": "sha512-wcW+sIUiWnKgNY0dqCpOZkUbF/I+YPi+f09JZIDa39Ec+q82CpSYniDp+ISgTTbKmnpJWASeJBPZmoxH84wt3g==", - "dev": true, - "requires": { - "request-promise-core": "1.1.4", - "stealthy-require": "^1.1.1", - "tough-cookie": "^2.3.3" - }, - "dependencies": { - "tough-cookie": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", - "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", - "dev": true, - "requires": { - "psl": "^1.1.28", - "punycode": "^2.1.1" - } - } - } - }, "require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", "dev": true }, - "require-main-filename": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", "dev": true }, "resolve": { @@ -7458,18 +4983,6 @@ "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", "dev": true }, - "resolve-url": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", - "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", - "dev": true - }, - "ret": { - "version": "0.1.15", - "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", - "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", - "dev": true - }, "reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", @@ -7485,12 +4998,6 @@ "glob": "^7.1.3" } }, - "rsvp": { - "version": "4.8.5", - "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz", - "integrity": "sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA==", - "dev": true - }, "run-parallel": { "version": "1.1.9", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.1.9.tgz", @@ -7503,162 +5010,12 @@ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "dev": true }, - "safe-regex": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", - "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", - "dev": true, - "requires": { - "ret": "~0.1.10" - } - }, "safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "dev": true }, - "sane": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/sane/-/sane-4.1.0.tgz", - "integrity": "sha512-hhbzAgTIX8O7SHfp2c8/kREfEn4qO/9q8C9beyY6+tvZ87EpoZ3i1RIEvp27YBswnNbY9mWd6paKVmKbAgLfZA==", - "dev": true, - "requires": { - "@cnakazawa/watch": "^1.0.3", - "anymatch": "^2.0.0", - "capture-exit": "^2.0.0", - "exec-sh": "^0.3.2", - "execa": "^1.0.0", - "fb-watchman": "^2.0.0", - "micromatch": "^3.1.4", - "minimist": "^1.1.1", - "walker": "~1.0.5" - }, - "dependencies": { - "anymatch": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", - "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", - "dev": true, - "requires": { - "micromatch": "^3.1.4", - "normalize-path": "^2.1.1" - } - }, - "braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "dev": true, - "requires": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "dev": true, - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - } - }, - "normalize-path": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", - "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", - "dev": true, - "requires": { - "remove-trailing-separator": "^1.0.1" - } - }, - "to-regex-range": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", - "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", - "dev": true, - "requires": { - "is-number": "^3.0.0", - "repeat-string": "^1.6.1" - } - } - } - }, "saxes": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz", @@ -7668,41 +5025,12 @@ "xmlchars": "^2.2.0" } }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - }, - "set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", - "dev": true - }, - "set-value": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", - "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-extendable": "^0.1.1", - "is-plain-object": "^2.0.3", - "split-string": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + }, "shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -7718,17 +5046,10 @@ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true }, - "shellwords": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz", - "integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==", - "dev": true, - "optional": true - }, "signal-exit": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", - "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.5.tgz", + "integrity": "sha512-KWcOiKeQj6ZyXx7zq4YxSMgHRlod4czeBQZrPb8OKcohcqAXShm7E20kEMle9WBt26hFcAf0qLOcp5zmY7kOqQ==", "dev": true }, "sisteransi": { @@ -7744,255 +5065,68 @@ "dev": true }, "slice-ansi": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", - "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.0", - "astral-regex": "^1.0.0", - "is-fullwidth-code-point": "^2.0.0" - }, - "dependencies": { - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - } - } - }, - "snapdragon": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", - "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", "dev": true, "requires": { - "base": "^0.11.1", - "debug": "^2.2.0", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "map-cache": "^0.2.2", - "source-map": "^0.5.6", - "source-map-resolve": "^0.5.0", - "use": "^3.1.0" + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" }, "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "requires": { - "is-descriptor": "^0.1.0" + "color-convert": "^2.0.1" } }, - "extend-shallow": { + "color-convert": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, "requires": { - "is-extendable": "^0.1.0" + "color-name": "~1.1.4" } }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true } } }, - "snapdragon-node": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", - "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", - "dev": true, - "requires": { - "define-property": "^1.0.0", - "isobject": "^3.0.0", - "snapdragon-util": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - } - } - }, - "snapdragon-util": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", - "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", - "dev": true, - "requires": { - "kind-of": "^3.2.0" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true }, - "source-map-resolve": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", - "integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==", - "dev": true, - "requires": { - "atob": "^2.1.2", - "decode-uri-component": "^0.2.0", - "resolve-url": "^0.2.1", - "source-map-url": "^0.4.0", - "urix": "^0.1.0" - } - }, "source-map-support": { - "version": "0.5.19", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", - "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", + "version": "0.5.20", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.20.tgz", + "integrity": "sha512-n1lZZ8Ve4ksRqizaBQgxXDgKwttHDhyfQjA6YZZn8+AroHbsIz+JjwxQDxbp+7y5OYCI8t1Yk7etjD9CRd2hIw==", "dev": true, "requires": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, - "source-map-url": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.1.tgz", - "integrity": "sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==", - "dev": true - }, - "spdx-correct": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", - "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", - "dev": true, - "requires": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "spdx-exceptions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", - "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", - "dev": true - }, - "spdx-expression-parse": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", - "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", - "dev": true, - "requires": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "spdx-license-ids": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.7.tgz", - "integrity": "sha512-U+MTEOO0AiDzxwFvoa4JVnMV6mZlJKk2sBLt90s7G0Gd0Mlknc7kxEn3nuDPNZRta7O2uy8oLcZLVT+4sqNZHQ==", - "dev": true - }, - "split-string": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", - "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", - "dev": true, - "requires": { - "extend-shallow": "^3.0.0" - } - }, "sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", "dev": true }, - "sshpk": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", - "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", - "dev": true, - "requires": { - "asn1": "~0.2.3", - "assert-plus": "^1.0.0", - "bcrypt-pbkdf": "^1.0.0", - "dashdash": "^1.12.0", - "ecc-jsbn": "~0.1.1", - "getpass": "^0.1.1", - "jsbn": "~0.1.0", - "safer-buffer": "^2.0.2", - "tweetnacl": "~0.14.0" - } - }, "stack-utils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.3.tgz", - "integrity": "sha512-gL//fkxfWUsIlFL2Tl42Cl6+HFALEaB1FU76I/Fy+oZjRreP7OPMXFlGbxM7NQsI0ZpUfw76sHnv0WNYuTb7Iw==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.5.tgz", + "integrity": "sha512-xrQcmYhOsn/1kX+Vraq+7j4oE2j/6BFscZ0etmYg81xuM8Gq0022Pxb8+IqgOFUIaxHs0KaSb7T1+OegiNrNFA==", "dev": true, "requires": { "escape-string-regexp": "^2.0.0" @@ -8012,33 +5146,6 @@ "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==", "dev": true }, - "static-extend": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", - "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", - "dev": true, - "requires": { - "define-property": "^0.2.5", - "object-copy": "^0.1.0" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - } - } - }, - "stealthy-require": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", - "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=", - "dev": true - }, "string-length": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", @@ -8050,14 +5157,25 @@ } }, "string-width": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", - "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, "requires": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.0" + "strip-ansi": "^6.0.1" + }, + "dependencies": { + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + } } }, "string_decoder": { @@ -8084,12 +5202,6 @@ "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", "dev": true }, - "strip-eof": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", - "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", - "dev": true - }, "strip-final-newline": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", @@ -8112,9 +5224,9 @@ } }, "supports-hyperlinks": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.1.0.tgz", - "integrity": "sha512-zoE5/e+dnEijk6ASB6/qrK+oYdm2do1hjoLWrqUC/8WEIW1gbxFcKuBof7sW8ArN6e+AYvsE8HBGiVRWL/F5CA==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.2.0.tgz", + "integrity": "sha512-6sXEzV5+I5j8Bmq9/vUphGRM/RJNT9SCURJLjwfOg51heRtguGWDzcaBlgAzKhQa0EVNpPEKzQuBwZ8S8WaCeQ==", "dev": true, "requires": { "has-flag": "^4.0.0", @@ -8145,53 +5257,55 @@ "dev": true }, "table": { - "version": "5.4.6", - "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz", - "integrity": "sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==", + "version": "6.7.2", + "resolved": "https://registry.npmjs.org/table/-/table-6.7.2.tgz", + "integrity": "sha512-UFZK67uvyNivLeQbVtkiUs8Uuuxv24aSL4/Vil2PJVtMgU8Lx0CYkP12uCGa3kjyQzOSgV1+z9Wkb82fCGsO0g==", "dev": true, "requires": { - "ajv": "^6.10.2", - "lodash": "^4.17.14", - "slice-ansi": "^2.1.0", - "string-width": "^3.0.0" + "ajv": "^8.0.1", + "lodash.clonedeep": "^4.5.0", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" }, "dependencies": { - "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", - "dev": true - }, - "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", - "dev": true + "ajv": { + "version": "8.6.3", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.6.3.tgz", + "integrity": "sha512-SMJOdDP6LqTkD0Uq8qLi+gMwSt0imXLSV080qFVwJCpH9U6Mb+SUGHAXM0KNbcBPguytWyvFxcHgMLe2D2XSpw==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "dev": true }, "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" } }, "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "requires": { - "ansi-regex": "^4.1.0" + "ansi-regex": "^5.0.1" } } } @@ -8224,9 +5338,9 @@ "dev": true }, "throat": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/throat/-/throat-5.0.0.tgz", - "integrity": "sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/throat/-/throat-6.0.1.tgz", + "integrity": "sha512-8hmiGIJMDlwjg7dlJ4yKGLK8EsYqKgPWbG3b4wjJddKNwc7N7Dpn08Df4szr/sZdMVeOstrdYSsqzX6BYbcB+w==", "dev": true }, "through2": { @@ -8240,9 +5354,9 @@ } }, "tmpl": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.4.tgz", - "integrity": "sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE=", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", "dev": true }, "to-fast-properties": { @@ -8251,38 +5365,6 @@ "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", "dev": true }, - "to-object-path": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", - "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "to-regex": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", - "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", - "dev": true, - "requires": { - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "regex-not": "^1.0.2", - "safe-regex": "^1.1.0" - } - }, "to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -8304,38 +5386,38 @@ } }, "tr46": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-2.0.2.tgz", - "integrity": "sha512-3n1qG+/5kg+jrbTzwAykB5yRYtQCTqOGKq5U5PE3b0a1/mzo6snDhjGS0zJVJunO0NrT3Dg1MLy5TjWP/UJppg==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-2.1.0.tgz", + "integrity": "sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw==", "dev": true, "requires": { "punycode": "^2.1.1" } }, "ts-jest": { - "version": "26.3.0", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-26.3.0.tgz", - "integrity": "sha512-Jq2uKfx6bPd9+JDpZNMBJMdMQUC3sJ08acISj8NXlVgR2d5OqslEHOR2KHMgwymu8h50+lKIm0m0xj/ioYdW2Q==", + "version": "27.0.5", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-27.0.5.tgz", + "integrity": "sha512-lIJApzfTaSSbtlksfFNHkWOzLJuuSm4faFAfo5kvzOiRAuoN4/eKxVJ2zEAho8aecE04qX6K1pAzfH5QHL1/8w==", "dev": true, "requires": { - "@types/jest": "26.x", "bs-logger": "0.x", - "buffer-from": "1.x", "fast-json-stable-stringify": "2.x", - "jest-util": "26.x", + "jest-util": "^27.0.0", "json5": "2.x", - "lodash.memoize": "4.x", + "lodash": "4.x", "make-error": "1.x", - "mkdirp": "1.x", "semver": "7.x", - "yargs-parser": "18.x" + "yargs-parser": "20.x" }, "dependencies": { "semver": { - "version": "7.3.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", - "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==", - "dev": true + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } } } }, @@ -8361,21 +5443,6 @@ } } }, - "tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", - "dev": true, - "requires": { - "safe-buffer": "^5.0.1" - } - }, - "tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", - "dev": true - }, "type-check": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", @@ -8392,9 +5459,9 @@ "dev": true }, "type-fest": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", "dev": true }, "typedarray-to-buffer": { @@ -8412,64 +5479,12 @@ "integrity": "sha512-e4ERvRV2wb+rRZ/IQeb3jm2VxBsirQLpQhdxplZ2MEzGvDkkMmPglecnNDfSUBivMjP93vRbngYYDQqQ/78bcQ==", "dev": true }, - "union-value": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", - "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", - "dev": true, - "requires": { - "arr-union": "^3.1.0", - "get-value": "^2.0.6", - "is-extendable": "^0.1.1", - "set-value": "^2.0.1" - } - }, "universalify": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", "dev": true }, - "unset-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", - "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", - "dev": true, - "requires": { - "has-value": "^0.3.1", - "isobject": "^3.0.0" - }, - "dependencies": { - "has-value": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", - "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", - "dev": true, - "requires": { - "get-value": "^2.0.3", - "has-values": "^0.1.4", - "isobject": "^2.0.0" - }, - "dependencies": { - "isobject": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", - "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", - "dev": true, - "requires": { - "isarray": "1.0.0" - } - } - } - }, - "has-values": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", - "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=", - "dev": true - } - } - }, "uri-js": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.0.tgz", @@ -8479,41 +5494,22 @@ "punycode": "^2.1.0" } }, - "urix": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", - "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", - "dev": true - }, - "use": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", - "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", - "dev": true - }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", "dev": true }, - "uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "dev": true, - "optional": true - }, "v8-compile-cache": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.1.1.tgz", - "integrity": "sha512-8OQ9CL+VWyt3JStj7HX7/ciTL2V3Rl1Wf5OL+SNTm0yK1KvtReVulksyeRnCANHHuUxHlQig+JJDlUhBt1NQDQ==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", + "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", "dev": true }, "v8-to-istanbul": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-7.1.1.tgz", - "integrity": "sha512-p0BB09E5FRjx0ELN6RgusIPsSPhtgexSRcKETybEs6IGOTXJSZqfwxp7r//55nnu0f1AxltY5VvdVqy2vZf9AA==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-8.1.0.tgz", + "integrity": "sha512-/PRhfd8aTNp9Ggr62HPzXg2XasNFGy5PBt0Rp04du7/8GNNSgxFL6WBTkgMKSL9bFjH+8kKEG3f37FmxiTqUUA==", "dev": true, "requires": { "@types/istanbul-lib-coverage": "^2.0.1", @@ -8529,27 +5525,6 @@ } } }, - "validate-npm-package-license": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", - "dev": true, - "requires": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, - "verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - } - }, "w3c-hr-time": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", @@ -8599,13 +5574,13 @@ "dev": true }, "whatwg-url": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-8.5.0.tgz", - "integrity": "sha512-fy+R77xWv0AiqfLl4nuGUlQ3/6b5uNfQ4WAbGQVMYshCTCCPK9psC1nWh3XHuxGVCtlcDDQPQW1csmmIQo+fwg==", + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-8.7.0.tgz", + "integrity": "sha512-gAojqb/m9Q8a5IV96E3fHJM70AzCkgt4uXYX2O7EmuyOnLrViCQlsEBmF9UQIu3/aeAIp2U17rtbpZWNntQqdg==", "dev": true, "requires": { "lodash": "^4.7.0", - "tr46": "^2.0.2", + "tr46": "^2.1.0", "webidl-conversions": "^6.1.0" } }, @@ -8618,12 +5593,6 @@ "isexe": "^2.0.0" } }, - "which-module": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", - "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", - "dev": true - }, "word-wrap": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", @@ -8631,9 +5600,9 @@ "dev": true }, "wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, "requires": { "ansi-styles": "^4.0.0", @@ -8673,26 +5642,6 @@ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", "dev": true }, - "write": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/write/-/write-1.0.3.tgz", - "integrity": "sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==", - "dev": true, - "requires": { - "mkdirp": "^0.5.1" - }, - "dependencies": { - "mkdirp": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", - "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", - "dev": true, - "requires": { - "minimist": "^1.2.5" - } - } - } - }, "write-file-atomic": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", @@ -8706,9 +5655,9 @@ } }, "ws": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.3.tgz", - "integrity": "sha512-kQ/dHIzuLrS6Je9+uv81ueZomEwH0qVYstcAQ4/Z93K8zeko9gtAbttJWzoC5ukqXY1PpoouV3+VSOqEAFt5wg==", + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.5.tgz", + "integrity": "sha512-BAkMFcAzl8as1G/hArkxOxq3G7pjUqQ3gzYbLL0/5zNkph70e+lCoxBGnm6AW1+/aiNeV4fnKqZ8m4GZewmH2w==", "dev": true }, "xml-name-validator": { @@ -8730,9 +5679,9 @@ "dev": true }, "y18n": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.1.tgz", - "integrity": "sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==", + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "dev": true }, "yallist": { @@ -8742,33 +5691,25 @@ "dev": true }, "yargs": { - "version": "15.4.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", - "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", "dev": true, "requires": { - "cliui": "^6.0.0", - "decamelize": "^1.2.0", - "find-up": "^4.1.0", - "get-caller-file": "^2.0.1", + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", "string-width": "^4.2.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^18.1.2" + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" } }, "yargs-parser": { - "version": "18.1.3", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", - "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", - "dev": true, - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true } } } diff --git a/package.json b/package.json index c25e6cb1..c8daf370 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "0.1.1-canary.18", + "version": "0.1.1-rc.18", "description": "Split Javascript SDK common components", "main": "cjs/index.js", "module": "esm/index.js", @@ -50,7 +50,7 @@ "devDependencies": { "@types/google.analytics": "0.0.40", "@types/ioredis": "^4.14.1", - "@types/jest": "^26.0.10", + "@types/jest": "^27.0.0", "@types/lodash": "^4.14.162", "@types/node": "^14.14.7", "@types/object-assign": "^4.0.30", @@ -58,18 +58,18 @@ "@typescript-eslint/parser": "^4.2.0", "cross-env": "^7.0.2", "csv-streamify": "^4.0.0", - "eslint": "^7.9.0", + "eslint": "^7.32.0", "eslint-plugin-compat": "3.7.0", "fetch-mock": "^9.10.7", "ioredis": "^4.26.0", - "jest": "^26.6.3", + "jest": "^27.2.3", "jest-localstorage-mock": "^2.4.3", "js-yaml": "^3.14.0", "lodash": "^4.17.21", "node-fetch": "^2.6.1", "redis-server": "1.2.2", "rimraf": "^3.0.2", - "ts-jest": "^26.3.0", + "ts-jest": "^27.0.5", "typescript": "^4.0.2" }, "sideEffects": false From c7f8e306dee1ac589d26069d5c9ef3fa3a778ebb Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 7 Oct 2021 16:35:46 -0300 Subject: [PATCH 131/144] tests fixed --- src/listeners/__tests__/node.spec.ts | 16 ++++------------ .../__tests__/sdkReadinessManager.spec.ts | 3 +-- .../__tests__/ImpressionsCacheInRedis.spec.ts | 5 ++--- .../__tests__/settings.mocks.ts | 7 ++++++- 4 files changed, 13 insertions(+), 18 deletions(-) diff --git a/src/listeners/__tests__/node.spec.ts b/src/listeners/__tests__/node.spec.ts index 8f0805e3..4e226a6b 100644 --- a/src/listeners/__tests__/node.spec.ts +++ b/src/listeners/__tests__/node.spec.ts @@ -85,7 +85,7 @@ test('Node JS listener / Signal Listener SIGTERM callback with sync handler that processKillSpy.mockClear(); }); -test('Node JS listener / Signal Listener SIGTERM callback with async handler', async (done) => { +test('Node JS listener / Signal Listener SIGTERM callback with async handler', async () => { const fakePromise = new Promise(res => { setTimeout(() => { @@ -113,7 +113,7 @@ test('Node JS listener / Signal Listener SIGTERM callback with async handler', a expect(listener.stop).not.toBeCalled(); expect(processKillSpy).not.toBeCalled(); - fakePromise.then(() => { + await fakePromise.then(() => { // Clean up is called even if there is an error. expect(listener.stop).toBeCalledTimes(1); // It called for kill again, so the shutdown keeps going. @@ -122,14 +122,10 @@ test('Node JS listener / Signal Listener SIGTERM callback with async handler', a // Reset the kill spy since it's used on other tests. processKillSpy.mockClear(); - - done(); }); - - return fakePromise; }); -test('Node JS listener / Signal Listener SIGTERM callback with async handler that throws an error', async (done) => { +test('Node JS listener / Signal Listener SIGTERM callback with async handler that throws an error', async () => { const fakePromise = new Promise((res, rej) => { setTimeout(() => { @@ -158,7 +154,7 @@ test('Node JS listener / Signal Listener SIGTERM callback with async handler tha expect(processKillSpy).not.toBeCalled(); // Calling .then since the wrapUp handler does not throw. - (handlerPromise as Promise).then(() => { + await (handlerPromise as Promise).then(() => { // Clean up is called. expect(listener.stop).toBeCalledTimes(1); // It called for kill again, so the shutdown keeps going. @@ -169,9 +165,5 @@ test('Node JS listener / Signal Listener SIGTERM callback with async handler tha processOnSpy.mockRestore(); processRemoveListenerSpy.mockRestore(); processKillSpy.mockRestore(); - - done(); }); - - return handlerPromise; }); diff --git a/src/readiness/__tests__/sdkReadinessManager.spec.ts b/src/readiness/__tests__/sdkReadinessManager.spec.ts index 3cc3c765..5bfda13e 100644 --- a/src/readiness/__tests__/sdkReadinessManager.spec.ts +++ b/src/readiness/__tests__/sdkReadinessManager.spec.ts @@ -195,7 +195,7 @@ describe('SDK Readiness Manager - Event emitter', () => { describe('SDK Readiness Manager - Ready promise', () => { - test('.ready() promise behaviour for clients', async (done) => { + test('.ready() promise behaviour for clients', async () => { const sdkReadinessManager = sdkReadinessManagerFactory(loggerMock, EventEmitterMock); const ready = sdkReadinessManager.sdkStatus.ready(); @@ -258,7 +258,6 @@ describe('SDK Readiness Manager - Ready promise', () => { loggerMock.mockClear(); testPassedCount++; expect(testPassedCount).toBe(5); - done(); }, () => { throw new Error('It should be resolved on ready event, not rejected.'); } ); diff --git a/src/storages/inRedis/__tests__/ImpressionsCacheInRedis.spec.ts b/src/storages/inRedis/__tests__/ImpressionsCacheInRedis.spec.ts index 71591429..0701bd7b 100644 --- a/src/storages/inRedis/__tests__/ImpressionsCacheInRedis.spec.ts +++ b/src/storages/inRedis/__tests__/ImpressionsCacheInRedis.spec.ts @@ -47,7 +47,7 @@ describe('IMPRESSIONS CACHE IN REDIS', () => { await connection.quit(); }); - test('`track` should not resolve before calling expire', async (done) => { + test('`track` should not resolve before calling expire', async () => { const impressionsKey = 'impr_cache_ut_2.impressions'; const connection = new Redis(loggerMock, {}); @@ -74,7 +74,7 @@ describe('IMPRESSIONS CACHE IN REDIS', () => { await connection.del(impressionsKey); // @ts-expect-error - c.track([i1, i2]).then(() => { + await c.track([i1, i2]).then(() => { connection.del(impressionsKey); connection.quit(); // Try to disconnect right away. expect(spy1).toBeCalled(); // Redis rpush was called once before executing external callback. @@ -86,7 +86,6 @@ describe('IMPRESSIONS CACHE IN REDIS', () => { // Finally clean up and wrap up. spy1.mockRestore(); spy2.mockRestore(); - done(); }); }); diff --git a/src/utils/settingsValidation/__tests__/settings.mocks.ts b/src/utils/settingsValidation/__tests__/settings.mocks.ts index 4b8a10e4..2bcb0aa2 100644 --- a/src/utils/settingsValidation/__tests__/settings.mocks.ts +++ b/src/utils/settingsValidation/__tests__/settings.mocks.ts @@ -67,7 +67,12 @@ export const fullSettings: ISettings = { streamingEnabled: false, sync: { splitFilters: [], - impressionsMode: 'OPTIMIZED' + impressionsMode: 'OPTIMIZED', + __splitFiltersValidation: { + validFilters: [], + queryString: null, + groupedFilters: { byName: [], byPrefix: [] } + } }, version: 'jest', runtime: { From 069c5908031fcaf3df706e3992d37dd3557bc151 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 13 Oct 2021 12:28:03 -0300 Subject: [PATCH 132/144] review --- src/logger/index.ts | 14 +++++++------- src/logger/messages/error.ts | 2 +- .../LocalhostFromFile.ts} | 10 ++++------ .../LocalhostFromObject.ts} | 10 ++++------ src/sync/{ => offline}/syncManagerOffline.ts | 10 +++++----- src/types.ts | 2 +- src/utils/settingsValidation/localhost/index.ts | 3 ++- 7 files changed, 24 insertions(+), 27 deletions(-) rename src/sync/{syncManagerFromFile.ts => offline/LocalhostFromFile.ts} (56%) rename src/sync/{syncManagerFromObject.ts => offline/LocalhostFromObject.ts} (51%) rename src/sync/{ => offline}/syncManagerOffline.ts (85%) diff --git a/src/logger/index.ts b/src/logger/index.ts index 4738e191..ed2e5032 100644 --- a/src/logger/index.ts +++ b/src/logger/index.ts @@ -12,7 +12,7 @@ export const LogLevels: { [level: string]: LogLevel } = { NONE: 'NONE' }; -const LogLevelRanks = { +const LogLevelIndexes = { DEBUG: 1, INFO: 2, WARN: 3, @@ -47,28 +47,28 @@ export class Logger implements ILogger { constructor(options?: ILoggerOptions, codes?: IMap) { this.options = objectAssign({}, defaultOptions, options); this.codes = codes || new _Map(); - this.logLevel = LogLevelRanks[this.options.logLevel]; + this.logLevel = LogLevelIndexes[this.options.logLevel]; } setLogLevel(logLevel: LogLevel) { this.options.logLevel = logLevel; - this.logLevel = LogLevelRanks[logLevel]; + this.logLevel = LogLevelIndexes[logLevel]; } debug(msg: string | number, args?: any[]) { - if (this._shouldLog(LogLevelRanks.DEBUG)) this._log(LogLevels.DEBUG, msg, args); + if (this._shouldLog(LogLevelIndexes.DEBUG)) this._log(LogLevels.DEBUG, msg, args); } info(msg: string | number, args?: any[]) { - if (this._shouldLog(LogLevelRanks.INFO)) this._log(LogLevels.INFO, msg, args); + if (this._shouldLog(LogLevelIndexes.INFO)) this._log(LogLevels.INFO, msg, args); } warn(msg: string | number, args?: any[]) { - if (this._shouldLog(LogLevelRanks.WARN)) this._log(LogLevels.WARN, msg, args); + if (this._shouldLog(LogLevelIndexes.WARN)) this._log(LogLevels.WARN, msg, args); } error(msg: string | number, args?: any[]) { - if (this._shouldLog(LogLevelRanks.ERROR)) this._log(LogLevels.ERROR, msg, args); + if (this._shouldLog(LogLevelIndexes.ERROR)) this._log(LogLevels.ERROR, msg, args); } private _log(level: LogLevel, msg: string | number, args?: any[]) { diff --git a/src/logger/messages/error.ts b/src/logger/messages/error.ts index 5d4d13dd..75269bc9 100644 --- a/src/logger/messages/error.ts +++ b/src/logger/messages/error.ts @@ -30,5 +30,5 @@ export const codesError: [number, string][] = [ [c.ERROR_EMPTY_ARRAY, '%s: %s must be a non-empty array.'], // initialization / settings validation [c.ERROR_INVALID_IMPRESSIONS_MODE, c.LOG_PREFIX_SETTINGS + ': you passed an invalid "impressionsMode". It should be one of the following values: %s. Defaulting to "%s" mode.'], - [c.ERROR_LOCALHOST_MODULE_REQUIRED, c.LOG_PREFIX_SETTINGS + ': you didn\'t pass a valid "sync.localhostMode". It should be defined for using the SDK in localhost mode'] + [c.ERROR_LOCALHOST_MODULE_REQUIRED, c.LOG_PREFIX_SETTINGS + ': an invalid value was received for "sync.localhostMode" config. A valid entity should be provided for localhost mode.'] ]; diff --git a/src/sync/syncManagerFromFile.ts b/src/sync/offline/LocalhostFromFile.ts similarity index 56% rename from src/sync/syncManagerFromFile.ts rename to src/sync/offline/LocalhostFromFile.ts index f1a27fce..3a9b176f 100644 --- a/src/sync/syncManagerFromFile.ts +++ b/src/sync/offline/LocalhostFromFile.ts @@ -1,13 +1,11 @@ -import { splitsParserFromFileFactory } from './offline/splitsParser/splitsParserFromFile'; +import { splitsParserFromFileFactory } from './splitsParser/splitsParserFromFile'; import { syncManagerOfflineFactory } from './syncManagerOffline'; -import { SplitIO } from '../types'; -import { LOCALHOST_MODE } from '../utils/constants'; +import { SplitIO } from '../../types'; // Factory of Localhost SyncManager based on yaml file. // Requires Node 'fs' and 'path' APIs. export function LocalhostFromFile(): SplitIO.LocalhostFactory { - const localhost = syncManagerOfflineFactory(splitsParserFromFileFactory); - // @ts-ignore - localhost.type = LOCALHOST_MODE; + const localhost = syncManagerOfflineFactory(splitsParserFromFileFactory) as SplitIO.LocalhostFactory; + localhost.type = 'fromFile'; return localhost; } diff --git a/src/sync/syncManagerFromObject.ts b/src/sync/offline/LocalhostFromObject.ts similarity index 51% rename from src/sync/syncManagerFromObject.ts rename to src/sync/offline/LocalhostFromObject.ts index 24b4f0f3..57d8ca0f 100644 --- a/src/sync/syncManagerFromObject.ts +++ b/src/sync/offline/LocalhostFromObject.ts @@ -1,12 +1,10 @@ -import { splitsParserFromSettingsFactory } from './offline/splitsParser/splitsParserFromSettings'; +import { splitsParserFromSettingsFactory } from './splitsParser/splitsParserFromSettings'; import { syncManagerOfflineFactory } from './syncManagerOffline'; -import { SplitIO } from '../types'; -import { LOCALHOST_MODE } from '../utils/constants'; +import { SplitIO } from '../../types'; // Factory of Localhost SyncManager based on JS object. export function LocalhostFromObject(): SplitIO.LocalhostFactory { - const localhost = syncManagerOfflineFactory(splitsParserFromSettingsFactory); - // @ts-ignore - localhost.type = LOCALHOST_MODE; + const localhost = syncManagerOfflineFactory(splitsParserFromSettingsFactory) as SplitIO.LocalhostFactory; + localhost.type = 'fromObject'; return localhost; } diff --git a/src/sync/syncManagerOffline.ts b/src/sync/offline/syncManagerOffline.ts similarity index 85% rename from src/sync/syncManagerOffline.ts rename to src/sync/offline/syncManagerOffline.ts index 2ef909ee..58a75b6f 100644 --- a/src/sync/syncManagerOffline.ts +++ b/src/sync/offline/syncManagerOffline.ts @@ -1,9 +1,9 @@ -import { ISyncManager, ISyncManagerCS, ISyncManagerFactoryParams } from './types'; -import fromObjectSyncTaskFactory from './offline/syncTasks/fromObjectSyncTask'; +import { ISyncManager, ISyncManagerCS, ISyncManagerFactoryParams } from '../types'; +import fromObjectSyncTaskFactory from './syncTasks/fromObjectSyncTask'; import objectAssign from 'object-assign'; -import { ISplitsParser } from './offline/splitsParser/types'; -import { IReadinessManager } from '../readiness/types'; -import { SDK_SEGMENTS_ARRIVED } from '../readiness/constants'; +import { ISplitsParser } from './splitsParser/types'; +import { IReadinessManager } from '../../readiness/types'; +import { SDK_SEGMENTS_ARRIVED } from '../../readiness/constants'; function flush() { return Promise.resolve(); diff --git a/src/types.ts b/src/types.ts index 3b960424..92efbe6d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -587,7 +587,7 @@ export namespace SplitIO { * Localhost mode factory. */ export type LocalhostFactory = { - type?: 'localhost' + type: 'fromObject' | 'fromFile' (params: ISyncManagerFactoryParams): ISyncManagerCS } /** diff --git a/src/utils/settingsValidation/localhost/index.ts b/src/utils/settingsValidation/localhost/index.ts index 0f6aad9d..2fb40e05 100644 --- a/src/utils/settingsValidation/localhost/index.ts +++ b/src/utils/settingsValidation/localhost/index.ts @@ -12,7 +12,8 @@ import { LOCALHOST_MODE } from '../../constants'; export function validateLocalhost(settings: ISettings) { const localhostMode = settings.sync.localhostMode; - if (settings.mode === LOCALHOST_MODE && (typeof localhostMode !== 'function' || localhostMode.type !== LOCALHOST_MODE)) { + // localhostMode.type is used for internal validation. Not considered part of the public API, and might be updated eventually. + if (settings.mode === LOCALHOST_MODE && (typeof localhostMode !== 'function' || localhostMode.type !== 'fromObject')) { settings.log.error(ERROR_LOCALHOST_MODULE_REQUIRED); } return localhostMode; From 8230e89924520bf013e9494fa0bb3c3626c81898 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 13 Oct 2021 21:55:39 -0300 Subject: [PATCH 133/144] polishing on some export default --- src/integrations/__tests__/browser.spec.ts | 4 ++-- src/integrations/browser.ts | 8 ++++---- src/integrations/ga/GaToSplit.ts | 2 +- .../ga/{GaToSplitPlugin.ts => GoogleAnalyticsToSplit.ts} | 4 ++-- src/integrations/ga/SplitToGa.ts | 2 +- .../ga/{SplitToGaPlugin.ts => SplitToGoogleAnalytics.ts} | 4 ++-- src/integrations/ga/__tests__/GaToSplit.spec.ts | 2 +- src/integrations/ga/__tests__/SplitToGa.spec.ts | 2 +- src/utils/settingsValidation/__tests__/settings.mocks.ts | 2 +- 9 files changed, 15 insertions(+), 15 deletions(-) rename src/integrations/ga/{GaToSplitPlugin.ts => GoogleAnalyticsToSplit.ts} (66%) rename src/integrations/ga/{SplitToGaPlugin.ts => SplitToGoogleAnalytics.ts} (67%) diff --git a/src/integrations/__tests__/browser.spec.ts b/src/integrations/__tests__/browser.spec.ts index 9bab3e99..1b79d8bb 100644 --- a/src/integrations/__tests__/browser.spec.ts +++ b/src/integrations/__tests__/browser.spec.ts @@ -6,9 +6,9 @@ import { loggerMock } from '../../logger/__tests__/sdkLogger.mock'; // Mock integration modules (GaToSplit and SplitToGa). jest.mock('../ga/GaToSplit'); -import GaToSplitMock from '../ga/GaToSplit'; +import { GaToSplit as GaToSplitMock } from '../ga/GaToSplit'; jest.mock('../ga/SplitToGa'); -import SplitToGaMock from '../ga/SplitToGa'; +import { SplitToGa as SplitToGaMock } from '../ga/SplitToGa'; const SplitToGaQueueMethod = jest.fn(); (SplitToGaMock as unknown as jest.Mock).mockImplementation(() => { diff --git a/src/integrations/browser.ts b/src/integrations/browser.ts index f9cb8927..f3a17713 100644 --- a/src/integrations/browser.ts +++ b/src/integrations/browser.ts @@ -2,8 +2,8 @@ import { GOOGLE_ANALYTICS_TO_SPLIT, SPLIT_TO_GOOGLE_ANALYTICS } from '../utils/c import { IIntegration, IIntegrationManager, IIntegrationFactoryParams } from './types'; import { BrowserIntegration } from './ga/types'; import pluggableIntegrationsManagerFactory from './pluggable'; -import GaToSplitPlugin from './ga/GaToSplitPlugin'; -import SplitToGaPlugin from './ga/SplitToGaPlugin'; +import { GoogleAnalyticsToSplit } from './ga/GoogleAnalyticsToSplit'; +import { SplitToGoogleAnalytics } from './ga/SplitToGoogleAnalytics'; /** * IntegrationsManager factory for the browser variant of the isomorphic JS SDK. @@ -23,8 +23,8 @@ export default function integrationsManagerFactory( const integrationFactories: Array<(params: IIntegrationFactoryParams) => IIntegration | void> = integrations .map(integrationOptions => { switch (integrationOptions.type) { - case GOOGLE_ANALYTICS_TO_SPLIT: return GaToSplitPlugin(integrationOptions); - case SPLIT_TO_GOOGLE_ANALYTICS: return SplitToGaPlugin(integrationOptions); + case GOOGLE_ANALYTICS_TO_SPLIT: return GoogleAnalyticsToSplit(integrationOptions); + case SPLIT_TO_GOOGLE_ANALYTICS: return SplitToGoogleAnalytics(integrationOptions); } }) .filter(integrationFactory => { diff --git a/src/integrations/ga/GaToSplit.ts b/src/integrations/ga/GaToSplit.ts index befa31f1..360c93a1 100644 --- a/src/integrations/ga/GaToSplit.ts +++ b/src/integrations/ga/GaToSplit.ts @@ -182,7 +182,7 @@ export function fixEventTypeId(log: ILogger, eventTypeId: any) { * @param {object} coreSettings core settings used to define an identity if no one provided as SDK or plugin options * @param {object} log factory logger */ -export default function GaToSplit(sdkOptions: GoogleAnalyticsToSplitOptions, params: IIntegrationFactoryParams) { +export function GaToSplit(sdkOptions: GoogleAnalyticsToSplitOptions, params: IIntegrationFactoryParams) { const { storage, settings: { core: coreSettings, log } } = params; diff --git a/src/integrations/ga/GaToSplitPlugin.ts b/src/integrations/ga/GoogleAnalyticsToSplit.ts similarity index 66% rename from src/integrations/ga/GaToSplitPlugin.ts rename to src/integrations/ga/GoogleAnalyticsToSplit.ts index 04a16905..940447aa 100644 --- a/src/integrations/ga/GaToSplitPlugin.ts +++ b/src/integrations/ga/GoogleAnalyticsToSplit.ts @@ -1,8 +1,8 @@ import { IIntegrationFactoryParams } from '../types'; -import GaToSplit from './GaToSplit'; +import { GaToSplit } from './GaToSplit'; import { GoogleAnalyticsToSplitOptions } from './types'; -export default function GaToSplitPlugin(options: GoogleAnalyticsToSplitOptions) { +export function GoogleAnalyticsToSplit(options: GoogleAnalyticsToSplitOptions) { // GaToSplit integration factory return (params: IIntegrationFactoryParams) => { diff --git a/src/integrations/ga/SplitToGa.ts b/src/integrations/ga/SplitToGa.ts index df933c21..dd469676 100644 --- a/src/integrations/ga/SplitToGa.ts +++ b/src/integrations/ga/SplitToGa.ts @@ -10,7 +10,7 @@ const logPrefix = 'split-to-ga: '; const noGaWarning = '`ga` command queue not found.'; const noHit = 'No hit was sent.'; -export default class SplitToGa implements IIntegration { +export class SplitToGa implements IIntegration { // A falsy object represents the default tracker static defaultTrackerNames = ['']; diff --git a/src/integrations/ga/SplitToGaPlugin.ts b/src/integrations/ga/SplitToGoogleAnalytics.ts similarity index 67% rename from src/integrations/ga/SplitToGaPlugin.ts rename to src/integrations/ga/SplitToGoogleAnalytics.ts index 9001c180..4ec71177 100644 --- a/src/integrations/ga/SplitToGaPlugin.ts +++ b/src/integrations/ga/SplitToGoogleAnalytics.ts @@ -1,8 +1,8 @@ import { IIntegrationFactoryParams } from '../types'; -import SplitToGa from './SplitToGa'; +import { SplitToGa } from './SplitToGa'; import { SplitToGoogleAnalyticsOptions } from './types'; -export default function SplitToGaPlugin(options: SplitToGoogleAnalyticsOptions = {}) { +export function SplitToGoogleAnalytics(options: SplitToGoogleAnalyticsOptions = {}) { // SplitToGa integration factory return (params: IIntegrationFactoryParams) => { diff --git a/src/integrations/ga/__tests__/GaToSplit.spec.ts b/src/integrations/ga/__tests__/GaToSplit.spec.ts index 0a7ef567..3296fe86 100644 --- a/src/integrations/ga/__tests__/GaToSplit.spec.ts +++ b/src/integrations/ga/__tests__/GaToSplit.spec.ts @@ -1,7 +1,7 @@ /* eslint-disable no-undef */ import { IEventsCacheSync } from '../../../storages/types'; import { SplitIO, ISettings } from '../../../types'; -import GaToSplit, { validateIdentities, defaultPrefix, defaultMapper, validateEventData, fixEventTypeId } from '../GaToSplit'; +import { GaToSplit, validateIdentities, defaultPrefix, defaultMapper, validateEventData, fixEventTypeId } from '../GaToSplit'; import { gaMock, gaRemove, modelMock } from './gaMock'; import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; diff --git a/src/integrations/ga/__tests__/SplitToGa.spec.ts b/src/integrations/ga/__tests__/SplitToGa.spec.ts index 849350ca..d05e4bab 100644 --- a/src/integrations/ga/__tests__/SplitToGa.spec.ts +++ b/src/integrations/ga/__tests__/SplitToGa.spec.ts @@ -6,7 +6,7 @@ import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; import { gaMock, gaRemove } from './gaMock'; // Test target -import SplitToGa from '../SplitToGa'; +import { SplitToGa } from '../SplitToGa'; const fakeImpressionPayload: SplitIO.ImpressionData = { impression: { diff --git a/src/utils/settingsValidation/__tests__/settings.mocks.ts b/src/utils/settingsValidation/__tests__/settings.mocks.ts index a08da99d..80aacabf 100644 --- a/src/utils/settingsValidation/__tests__/settings.mocks.ts +++ b/src/utils/settingsValidation/__tests__/settings.mocks.ts @@ -1,7 +1,7 @@ import { InMemoryStorageCSFactory } from '../../../storages/inMemory/InMemoryStorageCS'; import { ISettings } from '../../../types'; import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock'; -import { LocalhostFromObject } from '../../../sync/syncManagerFromObject'; +import { LocalhostFromObject } from '../../../sync/offline/LocalhostFromObject'; export const settingsWithKey = { core: { From 427f56c2874009fe924a97b96d3cdb69304b67d6 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 13 Oct 2021 23:45:45 -0300 Subject: [PATCH 134/144] validator update --- package-lock.json | 2 +- package.json | 2 +- src/listeners/__tests__/node.spec.ts | 4 ++-- src/sync/offline/LocalhostFromFile.ts | 2 +- src/sync/offline/LocalhostFromObject.ts | 2 +- src/types.ts | 4 ++-- src/utils/settingsValidation/localhost/index.ts | 7 ++++--- 7 files changed, 12 insertions(+), 11 deletions(-) diff --git a/package-lock.json b/package-lock.json index 17c71d80..a8317261 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "0.1.1-rc.18", + "version": "0.1.1-rc.19", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index c8daf370..353d90fc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "0.1.1-rc.18", + "version": "0.1.1-rc.19", "description": "Split Javascript SDK common components", "main": "cjs/index.js", "module": "esm/index.js", diff --git a/src/listeners/__tests__/node.spec.ts b/src/listeners/__tests__/node.spec.ts index 4e226a6b..1755fa7d 100644 --- a/src/listeners/__tests__/node.spec.ts +++ b/src/listeners/__tests__/node.spec.ts @@ -144,7 +144,7 @@ test('Node JS listener / Signal Listener SIGTERM callback with async handler tha // Call function // @ts-expect-error - const handlerPromise = listener._sigtermHandler(); + const handlerPromise: Promise = listener._sigtermHandler(); // Handler was properly called. expect(syncManagerMock.flush).toBeCalledTimes(1); @@ -154,7 +154,7 @@ test('Node JS listener / Signal Listener SIGTERM callback with async handler tha expect(processKillSpy).not.toBeCalled(); // Calling .then since the wrapUp handler does not throw. - await (handlerPromise as Promise).then(() => { + await handlerPromise.then(() => { // Clean up is called. expect(listener.stop).toBeCalledTimes(1); // It called for kill again, so the shutdown keeps going. diff --git a/src/sync/offline/LocalhostFromFile.ts b/src/sync/offline/LocalhostFromFile.ts index 3a9b176f..2caff329 100644 --- a/src/sync/offline/LocalhostFromFile.ts +++ b/src/sync/offline/LocalhostFromFile.ts @@ -6,6 +6,6 @@ import { SplitIO } from '../../types'; // Requires Node 'fs' and 'path' APIs. export function LocalhostFromFile(): SplitIO.LocalhostFactory { const localhost = syncManagerOfflineFactory(splitsParserFromFileFactory) as SplitIO.LocalhostFactory; - localhost.type = 'fromFile'; + localhost.type = 'LocalhostFromFile'; return localhost; } diff --git a/src/sync/offline/LocalhostFromObject.ts b/src/sync/offline/LocalhostFromObject.ts index 57d8ca0f..d1510996 100644 --- a/src/sync/offline/LocalhostFromObject.ts +++ b/src/sync/offline/LocalhostFromObject.ts @@ -5,6 +5,6 @@ import { SplitIO } from '../../types'; // Factory of Localhost SyncManager based on JS object. export function LocalhostFromObject(): SplitIO.LocalhostFactory { const localhost = syncManagerOfflineFactory(splitsParserFromSettingsFactory) as SplitIO.LocalhostFactory; - localhost.type = 'fromObject'; + localhost.type = 'LocalhostFromObject'; return localhost; } diff --git a/src/types.ts b/src/types.ts index 92efbe6d..691c5beb 100644 --- a/src/types.ts +++ b/src/types.ts @@ -101,7 +101,7 @@ export interface ISettings { splitFilters: SplitIO.SplitFilter[], impressionsMode: SplitIO.ImpressionsMode, __splitFiltersValidation: ISplitFiltersValidation, - localhostMode: SplitIO.LocalhostFactory + localhostMode?: SplitIO.LocalhostFactory }, readonly runtime: { ip: string | false @@ -587,7 +587,7 @@ export namespace SplitIO { * Localhost mode factory. */ export type LocalhostFactory = { - type: 'fromObject' | 'fromFile' + type: 'LocalhostFromObject' | 'LocalhostFromFile' (params: ISyncManagerFactoryParams): ISyncManagerCS } /** diff --git a/src/utils/settingsValidation/localhost/index.ts b/src/utils/settingsValidation/localhost/index.ts index 2fb40e05..19a196b8 100644 --- a/src/utils/settingsValidation/localhost/index.ts +++ b/src/utils/settingsValidation/localhost/index.ts @@ -3,18 +3,19 @@ import { ISettings, } from '../../../types'; import { LOCALHOST_MODE } from '../../constants'; /** - * This function validates `settings.storage` object + * This function validates `settings.sync.localhostMode` object * * @param {any} settings config object provided by the user to initialize the sdk * - * @returns {Object} valid storage factory. It might be the default `InMemoryStorageCSFactory` if the provided storage is invalid. + * @returns {Object | undefined} valid localhost mode module, or undefined if the provided one is not provided or invalid. */ export function validateLocalhost(settings: ISettings) { const localhostMode = settings.sync.localhostMode; // localhostMode.type is used for internal validation. Not considered part of the public API, and might be updated eventually. - if (settings.mode === LOCALHOST_MODE && (typeof localhostMode !== 'function' || localhostMode.type !== 'fromObject')) { + if (settings.mode === LOCALHOST_MODE && (typeof localhostMode !== 'function' || localhostMode.type !== 'LocalhostFromObject')) { settings.log.error(ERROR_LOCALHOST_MODULE_REQUIRED); + return undefined; } return localhostMode; } From 60741a3e6462678315b85e248ed299b5e4088402 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 14 Oct 2021 12:46:27 -0300 Subject: [PATCH 135/144] polish comment --- src/utils/settingsValidation/localhost/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/settingsValidation/localhost/index.ts b/src/utils/settingsValidation/localhost/index.ts index 19a196b8..b97b320b 100644 --- a/src/utils/settingsValidation/localhost/index.ts +++ b/src/utils/settingsValidation/localhost/index.ts @@ -7,7 +7,7 @@ import { LOCALHOST_MODE } from '../../constants'; * * @param {any} settings config object provided by the user to initialize the sdk * - * @returns {Object | undefined} valid localhost mode module, or undefined if the provided one is not provided or invalid. + * @returns {Object | undefined} provided localhost mode module at `settings.sync.localhostMode`, or undefined if no module is provided or is invalid. */ export function validateLocalhost(settings: ISettings) { const localhostMode = settings.sync.localhostMode; From 9addabc236baed80e4f46e18f7d9d62ca9537c99 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 14 Oct 2021 14:33:57 -0300 Subject: [PATCH 136/144] polishing --- src/utils/settingsValidation/index.ts | 21 ++++++++++--------- .../settingsValidation/localhost/index.ts | 2 +- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/utils/settingsValidation/index.ts b/src/utils/settingsValidation/index.ts index 2a211f90..1840fdb5 100644 --- a/src/utils/settingsValidation/index.ts +++ b/src/utils/settingsValidation/index.ts @@ -111,17 +111,18 @@ export function settingsValidation(config: unknown, validationParams: ISettingsV withDefaults.log = log; // Scheduler periods - withDefaults.scheduler.featuresRefreshRate = fromSecondsToMillis(withDefaults.scheduler.featuresRefreshRate); - withDefaults.scheduler.segmentsRefreshRate = fromSecondsToMillis(withDefaults.scheduler.segmentsRefreshRate); - withDefaults.scheduler.metricsRefreshRate = fromSecondsToMillis(withDefaults.scheduler.metricsRefreshRate); - withDefaults.scheduler.impressionsRefreshRate = fromSecondsToMillis(withDefaults.scheduler.impressionsRefreshRate); - withDefaults.scheduler.offlineRefreshRate = fromSecondsToMillis(withDefaults.scheduler.offlineRefreshRate); - withDefaults.scheduler.eventsPushRate = fromSecondsToMillis(withDefaults.scheduler.eventsPushRate); + const { scheduler, startup } = withDefaults; + scheduler.featuresRefreshRate = fromSecondsToMillis(scheduler.featuresRefreshRate); + scheduler.segmentsRefreshRate = fromSecondsToMillis(scheduler.segmentsRefreshRate); + scheduler.metricsRefreshRate = fromSecondsToMillis(scheduler.metricsRefreshRate); + scheduler.impressionsRefreshRate = fromSecondsToMillis(scheduler.impressionsRefreshRate); + scheduler.offlineRefreshRate = fromSecondsToMillis(scheduler.offlineRefreshRate); + scheduler.eventsPushRate = fromSecondsToMillis(scheduler.eventsPushRate); // Startup periods - withDefaults.startup.requestTimeoutBeforeReady = fromSecondsToMillis(withDefaults.startup.requestTimeoutBeforeReady); - withDefaults.startup.readyTimeout = fromSecondsToMillis(withDefaults.startup.readyTimeout); - withDefaults.startup.eventsFirstPushWindow = fromSecondsToMillis(withDefaults.startup.eventsFirstPushWindow); + startup.requestTimeoutBeforeReady = fromSecondsToMillis(startup.requestTimeoutBeforeReady); + startup.readyTimeout = fromSecondsToMillis(startup.readyTimeout); + startup.eventsFirstPushWindow = fromSecondsToMillis(withDefaults.startup.eventsFirstPushWindow); // ensure a valid SDK mode // @ts-ignore, modify readonly prop @@ -152,7 +153,7 @@ export function settingsValidation(config: unknown, validationParams: ISettingsV withDefaults.streamingEnabled = true; // Backoff bases. // We are not checking if bases are positive numbers. Thus, we might be reauthenticating immediately (`setTimeout` with NaN or negative number) - withDefaults.scheduler.pushRetryBackoffBase = fromSecondsToMillis(withDefaults.scheduler.pushRetryBackoffBase); + scheduler.pushRetryBackoffBase = fromSecondsToMillis(scheduler.pushRetryBackoffBase); } // validate the `splitFilters` settings and parse splits query diff --git a/src/utils/settingsValidation/localhost/index.ts b/src/utils/settingsValidation/localhost/index.ts index b97b320b..7d5f9272 100644 --- a/src/utils/settingsValidation/localhost/index.ts +++ b/src/utils/settingsValidation/localhost/index.ts @@ -7,7 +7,7 @@ import { LOCALHOST_MODE } from '../../constants'; * * @param {any} settings config object provided by the user to initialize the sdk * - * @returns {Object | undefined} provided localhost mode module at `settings.sync.localhostMode`, or undefined if no module is provided or is invalid. + * @returns {Object | undefined} provided localhost mode module at `settings.sync.localhostMode`, or undefined if it is not provided or invalid. */ export function validateLocalhost(settings: ISettings) { const localhostMode = settings.sync.localhostMode; From 8a1d5ecd1cf734dc2456134b5264c2cca86217d1 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 14 Oct 2021 17:16:32 -0300 Subject: [PATCH 137/144] more polishing --- package-lock.json | 2 +- package.json | 2 +- src/sdkFactory/types.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index a8317261..a5324ded 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "0.1.1-rc.19", + "version": "0.1.1-rc.20", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 353d90fc..d85878ee 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "0.1.1-rc.19", + "version": "0.1.1-rc.20", "description": "Split Javascript SDK common components", "main": "cjs/index.js", "module": "esm/index.js", diff --git a/src/sdkFactory/types.ts b/src/sdkFactory/types.ts index ce5981ce..b0d87343 100644 --- a/src/sdkFactory/types.ts +++ b/src/sdkFactory/types.ts @@ -40,7 +40,7 @@ export interface ISdkFactoryParams { splitApiFactory?: (settings: ISettings, platform: IPlatform) => ISplitApi, // SyncManager factory. - // It is not required when providing an asynchronous storage. + // Not required when providing an asynchronous storage (consumer mode), but required in standalone mode to avoid SDK timeout. // It can create an offline or online sync manager, with or without streaming support. syncManagerFactory?: (params: ISyncManagerFactoryParams) => ISyncManager, From a37a8cb71891fde30e2436da7c47af55eb8cde5c Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 14 Oct 2021 17:20:20 -0300 Subject: [PATCH 138/144] polishing and tests --- src/sdkClient/sdkClientMethodCS.ts | 4 +- src/sync/offline/LocalhostFromFile.ts | 9 +-- src/sync/offline/LocalhostFromObject.ts | 10 +-- .../localhost/__tests__/index.spec.ts | 62 +++++++++++++++++++ .../settingsValidation/localhost/builtin.ts | 16 +++++ .../localhost/{index.ts => pluggable.ts} | 7 ++- 6 files changed, 95 insertions(+), 13 deletions(-) create mode 100644 src/utils/settingsValidation/localhost/__tests__/index.spec.ts create mode 100644 src/utils/settingsValidation/localhost/builtin.ts rename src/utils/settingsValidation/localhost/{index.ts => pluggable.ts} (79%) diff --git a/src/sdkClient/sdkClientMethodCS.ts b/src/sdkClient/sdkClientMethodCS.ts index 80089705..aeb29720 100644 --- a/src/sdkClient/sdkClientMethodCS.ts +++ b/src/sdkClient/sdkClientMethodCS.ts @@ -59,7 +59,7 @@ export function sdkClientMethodCSFactory(params: ISdkClientFactoryParams): (key? const sharedSdkReadiness = sdkReadinessManager.shared(readyTimeout); const sharedStorage = (storage as IStorageSyncCS).shared(matchingKey); - const sharedSyncManager = (syncManager as ISyncManagerCS).shared(matchingKey, sharedSdkReadiness.readinessManager, sharedStorage); + const sharedSyncManager = syncManager && (syncManager as ISyncManagerCS).shared(matchingKey, sharedSdkReadiness.readinessManager, sharedStorage); // As shared clients reuse all the storage information, we don't need to check here if we // will use offline or online mode. We should stick with the original decision. @@ -74,7 +74,7 @@ export function sdkClientMethodCSFactory(params: ISdkClientFactoryParams): (key? validKey ); - sharedSyncManager.start(); + sharedSyncManager && sharedSyncManager.start(); log.info(NEW_SHARED_CLIENT); } else { diff --git a/src/sync/offline/LocalhostFromFile.ts b/src/sync/offline/LocalhostFromFile.ts index 2caff329..33e6c121 100644 --- a/src/sync/offline/LocalhostFromFile.ts +++ b/src/sync/offline/LocalhostFromFile.ts @@ -2,10 +2,11 @@ import { splitsParserFromFileFactory } from './splitsParser/splitsParserFromFile import { syncManagerOfflineFactory } from './syncManagerOffline'; import { SplitIO } from '../../types'; -// Factory of Localhost SyncManager based on yaml file. +// Singleton instance of the factory function for offline SyncManager from YAML file (a.k.a. localhostFromFile) // Requires Node 'fs' and 'path' APIs. +const localhostFromFile = syncManagerOfflineFactory(splitsParserFromFileFactory) as SplitIO.LocalhostFactory; +localhostFromFile.type = 'LocalhostFromFile'; + export function LocalhostFromFile(): SplitIO.LocalhostFactory { - const localhost = syncManagerOfflineFactory(splitsParserFromFileFactory) as SplitIO.LocalhostFactory; - localhost.type = 'LocalhostFromFile'; - return localhost; + return localhostFromFile; } diff --git a/src/sync/offline/LocalhostFromObject.ts b/src/sync/offline/LocalhostFromObject.ts index d1510996..b823e7ee 100644 --- a/src/sync/offline/LocalhostFromObject.ts +++ b/src/sync/offline/LocalhostFromObject.ts @@ -2,9 +2,11 @@ import { splitsParserFromSettingsFactory } from './splitsParser/splitsParserFrom import { syncManagerOfflineFactory } from './syncManagerOffline'; import { SplitIO } from '../../types'; -// Factory of Localhost SyncManager based on JS object. +// Singleton instance of the factory function for offline SyncManager from object (a.k.a. localhostFromObject) +// SDK instances instantiate their SyncManagers with the same factory +const localhostFromObject = syncManagerOfflineFactory(splitsParserFromSettingsFactory) as SplitIO.LocalhostFactory; +localhostFromObject.type = 'LocalhostFromObject'; + export function LocalhostFromObject(): SplitIO.LocalhostFactory { - const localhost = syncManagerOfflineFactory(splitsParserFromSettingsFactory) as SplitIO.LocalhostFactory; - localhost.type = 'LocalhostFromObject'; - return localhost; + return localhostFromObject; } diff --git a/src/utils/settingsValidation/localhost/__tests__/index.spec.ts b/src/utils/settingsValidation/localhost/__tests__/index.spec.ts new file mode 100644 index 00000000..663530dc --- /dev/null +++ b/src/utils/settingsValidation/localhost/__tests__/index.spec.ts @@ -0,0 +1,62 @@ +import { validateLocalhost } from '../pluggable'; +import { validateLocalhostWithDefault } from '../builtin'; +import { LocalhostFromObject } from '../../../../sync/offline/LocalhostFromObject'; +import { loggerMock as log } from '../../../../logger/__tests__/sdkLogger.mock'; + +const localhostModeObject = LocalhostFromObject(); + +describe('validateLocalhost, for slim SplitFactory', () => { + + afterEach(() => { + log.error.mockClear(); + }); + + test('if mode is LOCALHOST_MODE and localhostMode object is invalid or not provided, returns undefined and logs an error', () => { + expect(validateLocalhost({ log, sync: {}, mode: 'localhost' })).toBe(undefined); + expect(validateLocalhost({ log, sync: { localhostMode: null }, mode: 'localhost' })).toBe(undefined); + expect(validateLocalhost({ log, sync: { localhostMode: () => { } }, mode: 'localhost' })).toBe(undefined); + expect(log.error).toBeCalledTimes(3); // logs error if provided object is invalid + }); + + test('if mode is LOCALHOST_MODE and localhostMode object is valid, returns the provided object', () => { + expect(validateLocalhost({ log, sync: { localhostMode: localhostModeObject }, mode: 'localhost' })).toBe(localhostModeObject); + expect(log.error).not.toBeCalled(); + }); + + test('if mode is not LOCALHOST_MODE, returns the provided object (it is not validated)', () => { + expect(validateLocalhost({ log, sync: {}, mode: 'standalone' })).toBe(undefined); + expect(validateLocalhost({ log, sync: { localhostMode: 'INVALID_BUT_IGNORED' }, mode: 'standalone' })).toBe('INVALID_BUT_IGNORED'); + expect(log.error).not.toBeCalled(); + }); + +}); + +describe('validateLocalhostWithDefault, for full SplitFactory', () => { + + afterEach(() => { + log.error.mockClear(); + }); + + test('if mode is LOCALHOST_MODE and localhostMode object is not provided, returns default without logging an error', () => { + expect(validateLocalhostWithDefault({ log, sync: {}, mode: 'localhost' })).toBe(localhostModeObject); + expect(validateLocalhostWithDefault({ log, sync: { localhostMode: null }, mode: 'localhost' })).toBe(localhostModeObject); + expect(log.error).not.toBeCalled(); + }); + + test('if mode is LOCALHOST_MODE and localhostMode object is invalid, returns default and logs an error', () => { + expect(validateLocalhostWithDefault({ log, sync: { localhostMode: () => { } }, mode: 'localhost' })).toBe(localhostModeObject); + expect(log.error).toBeCalledTimes(1); // logs error if provided object is invalid + }); + + test('if mode is LOCALHOST_MODE and localhostMode object is valid, returns the provided object', () => { + expect(validateLocalhostWithDefault({ log, sync: { localhostMode: localhostModeObject }, mode: 'localhost' })).toBe(localhostModeObject); + expect(log.error).not.toBeCalled(); + }); + + test('if mode is not LOCALHOST_MODE, returns the provided object or the default one. Provided object is not validated and so no errors are logged', () => { + expect(validateLocalhostWithDefault({ log, sync: {}, mode: 'standalone' })).toBe(localhostModeObject); + expect(validateLocalhostWithDefault({ log, sync: { localhostMode: 'INVALID_BUT_IGNORED' }, mode: 'standalone' })).toBe('INVALID_BUT_IGNORED'); + expect(log.error).not.toBeCalled(); + }); + +}); diff --git a/src/utils/settingsValidation/localhost/builtin.ts b/src/utils/settingsValidation/localhost/builtin.ts new file mode 100644 index 00000000..35a30145 --- /dev/null +++ b/src/utils/settingsValidation/localhost/builtin.ts @@ -0,0 +1,16 @@ +import { ILogger } from '../../../logger/types'; +import { SDKMode, } from '../../../types'; +import { LocalhostFromObject } from '../../../sync/offline/LocalhostFromObject'; +import { validateLocalhost } from './pluggable'; + +/** + * This function validates `settings.sync.localhostMode` object + * + * @param {any} settings config object provided by the user to initialize the sdk + * + * @returns {Object} provided localhost mode module at `settings.sync.localhostMode` if valid, or a default LocalhostFromObject instance if not provided or invalid + */ +export function validateLocalhostWithDefault(settings: { log: ILogger, sync: { localhostMode?: any }, mode: SDKMode }) { + if (!settings.sync.localhostMode) return LocalhostFromObject(); + return validateLocalhost(settings) || LocalhostFromObject(); +} diff --git a/src/utils/settingsValidation/localhost/index.ts b/src/utils/settingsValidation/localhost/pluggable.ts similarity index 79% rename from src/utils/settingsValidation/localhost/index.ts rename to src/utils/settingsValidation/localhost/pluggable.ts index 7d5f9272..3231e612 100644 --- a/src/utils/settingsValidation/localhost/index.ts +++ b/src/utils/settingsValidation/localhost/pluggable.ts @@ -1,5 +1,6 @@ import { ERROR_LOCALHOST_MODULE_REQUIRED } from '../../../logger/constants'; -import { ISettings, } from '../../../types'; +import { ILogger } from '../../../logger/types'; +import { SDKMode, } from '../../../types'; import { LOCALHOST_MODE } from '../../constants'; /** @@ -7,9 +8,9 @@ import { LOCALHOST_MODE } from '../../constants'; * * @param {any} settings config object provided by the user to initialize the sdk * - * @returns {Object | undefined} provided localhost mode module at `settings.sync.localhostMode`, or undefined if it is not provided or invalid. + * @returns {Object | undefined} provided localhost mode module at `settings.sync.localhostMode`, or undefined if it is not provided or invalid */ -export function validateLocalhost(settings: ISettings) { +export function validateLocalhost(settings: { log: ILogger, sync: { localhostMode?: any}, mode: SDKMode }) { const localhostMode = settings.sync.localhostMode; // localhostMode.type is used for internal validation. Not considered part of the public API, and might be updated eventually. From 54e97fa5dcf07f6c4188d91178cb7ddd91b1a4b8 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 18 Oct 2021 12:16:48 -0300 Subject: [PATCH 139/144] updated readme --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index fd90814d..4d9dee55 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ To learn more about Split, contact hello@split.io, or get started with feature f Split has built and maintains SDKs for: -* .NET [Github](https://github.com/splitio/.net-core-client) [Docs](https://help.split.io/hc/en-us/articles/360020240172--NET-SDK) +* .NET [Github](https://github.com/splitio/dotnet-client) [Docs](https://help.split.io/hc/en-us/articles/360020240172--NET-SDK) * Android [Github](https://github.com/splitio/android-client) [Docs](https://help.split.io/hc/en-us/articles/360020343291-Android-SDK) * GO [Github](https://github.com/splitio/go-client) [Docs](https://help.split.io/hc/en-us/articles/360020093652-Go-SDK) * iOS [Github](https://github.com/splitio/ios-client) [Docs](https://help.split.io/hc/en-us/articles/360020401491-iOS-SDK) @@ -35,6 +35,7 @@ Split has built and maintains SDKs for: * PHP [Github](https://github.com/splitio/php-client) [Docs](https://help.split.io/hc/en-us/articles/360020350372-PHP-SDK) * Python [Github](https://github.com/splitio/python-client) [Docs](https://help.split.io/hc/en-us/articles/360020359652-Python-SDK) * React [Github](https://github.com/splitio/react-client) [Docs](https://help.split.io/hc/en-us/articles/360038825091-React-SDK) +* React Native [Github](https://github.com/splitio/react-native-client) [Docs](https://help.split.io/hc/en-us/articles/4406066357901-React-Native-SDK) * Redux [Github](https://github.com/splitio/redux-client) [Docs](https://help.split.io/hc/en-us/articles/360038851551-Redux-SDK) * Ruby [Github](https://github.com/splitio/ruby-client) [Docs](https://help.split.io/hc/en-us/articles/360020673251-Ruby-SDK) From 4757f8ee43ae924997365ef10452af635acbf697 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 18 Oct 2021 17:36:34 -0300 Subject: [PATCH 140/144] added changelog --- CHANGES.txt | 7 +++++++ package-lock.json | 2 +- package.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 2500c73a..4aa075f8 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,10 @@ +1.0.0 (October 18, 2021) + - Updated localhost mode to emit SDK_READY_FROM_CACHE event in Browser when using localStorage (issue https://github.com/splitio/react-client/issues/34). + - Updated streaming logic to use the newest version of our streaming service. + - Updated dependencies for vulnerability fixes. + - Added pluggable storage module. + - BREAKING CHANGE on multiple modules due to general polishing, improvements and bug fixes. + 0.1.0 (March 30, 2021) - Initial public release. It includes common modules to be consumed by the different Split implementations written in JavaScript. Based on the original JS SDK in the `javascript-client` repository. - It's designed with a modular approach, with the following goals in mind: diff --git a/package-lock.json b/package-lock.json index a5324ded..f32024b1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "0.1.1-rc.20", + "version": "1.0.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index d85878ee..01e594a7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-commons", - "version": "0.1.1-rc.20", + "version": "1.0.0", "description": "Split Javascript SDK common components", "main": "cjs/index.js", "module": "esm/index.js", From cdb6363707e1a2ac1a4c0a014fd2f55fa36be0e9 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 19 Oct 2021 17:20:01 -0300 Subject: [PATCH 141/144] update changelog --- CHANGES.txt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 4aa075f8..e77fc8c0 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,8 +1,7 @@ -1.0.0 (October 18, 2021) +1.0.0 (October 19, 2021) - Updated localhost mode to emit SDK_READY_FROM_CACHE event in Browser when using localStorage (issue https://github.com/splitio/react-client/issues/34). - Updated streaming logic to use the newest version of our streaming service. - Updated dependencies for vulnerability fixes. - - Added pluggable storage module. - BREAKING CHANGE on multiple modules due to general polishing, improvements and bug fixes. 0.1.0 (March 30, 2021) From 200415b0aeb4a508821a9079cb64ab1474590a9d Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 19 Oct 2021 20:27:49 -0300 Subject: [PATCH 142/144] update changelog --- CHANGES.txt | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index e77fc8c0..46345d2e 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,6 +1,9 @@ -1.0.0 (October 19, 2021) - - Updated localhost mode to emit SDK_READY_FROM_CACHE event in Browser when using localStorage (issue https://github.com/splitio/react-client/issues/34). - - Updated streaming logic to use the newest version of our streaming service. +1.0.0 (October 20, 2021) + - Updated localhost mode to emit SDK_READY_FROM_CACHE event in Browser when using localStorage (Related to issue https://github.com/splitio/react-client/issues/34). + - Updated streaming logic to use the newest version of our streaming service, including: + - Integration with Auth service V2, connecting to the new channels and applying the received connection delay. + - Implemented handling of the new MySegmentsV2 notification types (SegmentRemoval, KeyList, Bounded and Unbounded) + - New control notification for environment scoped streaming reset. - Updated dependencies for vulnerability fixes. - BREAKING CHANGE on multiple modules due to general polishing, improvements and bug fixes. From df0592e3e60745610b86a7cf8a352fde4465d9d1 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 20 Oct 2021 09:17:51 -0300 Subject: [PATCH 143/144] updated readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4d9dee55..51caf41d 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ Split has built and maintains SDKs for: * Java [Github](https://github.com/splitio/java-client) [Docs](https://help.split.io/hc/en-us/articles/360020405151-Java-SDK) * Javascript [Github](https://github.com/splitio/javascript-client) [Docs](https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK) * Node [Github](https://github.com/splitio/javascript-client) [Docs](https://help.split.io/hc/en-us/articles/360020564931-Node-js-SDK) -* Javascript for Browser [Github](https://github.com/splitio/javascript-browser-client) [Docs](https://help.split.io/hc/en-us/articles/360058730852) +* Javascript for Browser [Github](https://github.com/splitio/javascript-browser-client) [Docs](https://help.split.io/hc/en-us/articles/360058730852-Browser-SDK) * PHP [Github](https://github.com/splitio/php-client) [Docs](https://help.split.io/hc/en-us/articles/360020350372-PHP-SDK) * Python [Github](https://github.com/splitio/python-client) [Docs](https://help.split.io/hc/en-us/articles/360020359652-Python-SDK) * React [Github](https://github.com/splitio/react-client) [Docs](https://help.split.io/hc/en-us/articles/360038825091-React-SDK) From 679545d8642a9b8083e885b637a503ce9ce60fd6 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 20 Oct 2021 13:06:09 -0300 Subject: [PATCH 144/144] updated changelog --- CHANGES.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 46345d2e..429ad003 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,11 +1,11 @@ 1.0.0 (October 20, 2021) - - Updated localhost mode to emit SDK_READY_FROM_CACHE event in Browser when using localStorage (Related to issue https://github.com/splitio/react-client/issues/34). +- BREAKING CHANGE on multiple modules due to general polishing, improvements and bug fixes. In most cases the change is to use named exports. This affected mostly modules related with synchronization and storages. - Updated streaming logic to use the newest version of our streaming service, including: - Integration with Auth service V2, connecting to the new channels and applying the received connection delay. - Implemented handling of the new MySegmentsV2 notification types (SegmentRemoval, KeyList, Bounded and Unbounded) - New control notification for environment scoped streaming reset. - - Updated dependencies for vulnerability fixes. - - BREAKING CHANGE on multiple modules due to general polishing, improvements and bug fixes. +- Updated localhost mode to emit SDK_READY_FROM_CACHE event in Browser when using localStorage (Related to issue https://github.com/splitio/react-client/issues/34). +- Updated dependencies for vulnerability fixes. 0.1.0 (March 30, 2021) - Initial public release. It includes common modules to be consumed by the different Split implementations written in JavaScript. Based on the original JS SDK in the `javascript-client` repository.