From 091aaf29bc718fee27509297d44c7b72929218d8 Mon Sep 17 00:00:00 2001 From: Zeeshan Ashraf Date: Thu, 2 Jul 2020 16:49:58 -0700 Subject: [PATCH 01/28] first implementation of offline events. Need to do code cleanup --- .../@react-native-community/async-storage.ts | 37 + .../__tests__/v1EventProcessor.spec.ts | 18 +- packages/event-processor/package-lock.json | 1820 ++++++++--------- packages/event-processor/package.json | 3 +- .../event-processor/src/eventDispatcher.ts | 8 +- .../event-processor/src/eventProcessor.ts | 4 +- .../event-processor/src/index.react_native.ts | 25 + .../src/v1/ReactNativeEventProcessor.ts | 224 ++ .../event-processor/src/v1/buildEventV1.ts | 2 + packages/optimizely-sdk/rollup.config.js | 5 +- 10 files changed, 1213 insertions(+), 933 deletions(-) create mode 100644 packages/event-processor/__mocks__/@react-native-community/async-storage.ts create mode 100644 packages/event-processor/src/index.react_native.ts create mode 100644 packages/event-processor/src/v1/ReactNativeEventProcessor.ts diff --git a/packages/event-processor/__mocks__/@react-native-community/async-storage.ts b/packages/event-processor/__mocks__/@react-native-community/async-storage.ts new file mode 100644 index 000000000..32f721144 --- /dev/null +++ b/packages/event-processor/__mocks__/@react-native-community/async-storage.ts @@ -0,0 +1,37 @@ +/** + * Copyright 2020, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export default class AsyncStorage { + static getItem(key: string, callback?: (error?: Error, result?: string) => void): Promise { + return new Promise((resolve, reject) => { + switch (key) { + case 'keyThatExists': + resolve('{ "name": "Awesome Object" }') + break + case 'keyThatDoesNotExist': + resolve(null) + break + case 'keyWithInvalidJsonObject': + resolve('bad json }') + break + } + }) + } + + static setItem(key: string, value: string, callback?: (error?: Error) => void): Promise { + return Promise.resolve() + } +} diff --git a/packages/event-processor/__tests__/v1EventProcessor.spec.ts b/packages/event-processor/__tests__/v1EventProcessor.spec.ts index 2a58047dc..57bee0866 100644 --- a/packages/event-processor/__tests__/v1EventProcessor.spec.ts +++ b/packages/event-processor/__tests__/v1EventProcessor.spec.ts @@ -114,9 +114,7 @@ describe('LogTierV1EventProcessor', () => { stubDispatcher = { dispatchEvent(event: EventV1Request, callback: EventDispatcherCallback): void { dispatchStub(event) - callback({ - statusCode: 200, - }) + callback(200) }, } }) @@ -163,9 +161,7 @@ describe('LogTierV1EventProcessor', () => { done() }) - localCallback({ - statusCode: 200, - }) + localCallback(200) }) it('should return a promise that is resolved when the dispatcher callback returns a 400 response', done => { @@ -202,9 +198,7 @@ describe('LogTierV1EventProcessor', () => { stubDispatcher = { dispatchEvent(event: EventV1Request, callback: EventDispatcherCallback): void { dispatchStub(event) - callback({ - statusCode: 200, - }) + callback(200) }, } @@ -230,7 +224,7 @@ describe('LogTierV1EventProcessor', () => { it('should stop accepting events after stop is called', () => { const dispatcher = { dispatchEvent: jest.fn((event: EventV1Request, callback: EventDispatcherCallback) => { - setTimeout(() => callback({ statusCode: 204 }), 0) + setTimeout(() => callback(204), 0) }) } const processor = new LogTierV1EventProcessor({ @@ -288,10 +282,10 @@ describe('LogTierV1EventProcessor', () => { }) expect(stopPromiseResolved).toBe(false) - dispatchCbs[0]({ statusCode: 204 }) + dispatchCbs[0](204) jest.advanceTimersByTime(100) expect(stopPromiseResolved).toBe(false) - dispatchCbs[1]({ statusCode: 204 }) + dispatchCbs[1](204) await stopPromise expect(stopPromiseResolved).toBe(true) }) diff --git a/packages/event-processor/package-lock.json b/packages/event-processor/package-lock.json index 62a5d6b93..d5cae7c22 100644 --- a/packages/event-processor/package-lock.json +++ b/packages/event-processor/package-lock.json @@ -10,7 +10,7 @@ "integrity": "sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij5HUcajsVcMCpQrcLmtxRbVFTIqmcSkSeYRBFBRxs2FiUqFJDLdiebA==", "dev": true, "requires": { - "@babel/highlight": "7.0.0" + "@babel/highlight": "^7.0.0" } }, "@babel/highlight": { @@ -19,9 +19,9 @@ "integrity": "sha512-UFMC4ZeFC48Tpvj7C8UgLvtkaUuovQX+5xNWrsIoMG8o2z+XFKjKaN9iVmS84dPwVN00W4wPmqvYoZF3EGAsfw==", "dev": true, "requires": { - "chalk": "2.4.2", - "esutils": "2.0.2", - "js-tokens": "4.0.0" + "chalk": "^2.0.0", + "esutils": "^2.0.2", + "js-tokens": "^4.0.0" }, "dependencies": { "js-tokens": { @@ -37,7 +37,7 @@ "resolved": "https://registry.npmjs.org/@optimizely/js-sdk-logging/-/js-sdk-logging-0.1.0.tgz", "integrity": "sha512-Bs2zHvsdNIk2QSg05P6mKIlROHoBIRNStbrVwlePm603CucojKRPlFJG4rt7sFZQOo8xS8I7z1BmE4QI3/ZE9A==", "requires": { - "@optimizely/js-sdk-utils": "0.1.0" + "@optimizely/js-sdk-utils": "^0.1.0" }, "dependencies": { "@optimizely/js-sdk-utils": { @@ -45,17 +45,17 @@ "resolved": "https://registry.npmjs.org/@optimizely/js-sdk-utils/-/js-sdk-utils-0.1.0.tgz", "integrity": "sha512-p7499GgVaX94YmkrwOiEtLgxgjXTPbUQsvETaAil5J7zg1TOA4Wl8ClalLSvCh+AKWkxGdkL4/uM/zfbxPSNNw==", "requires": { - "uuid": "3.3.2" + "uuid": "^3.3.2" } } } }, "@optimizely/js-sdk-utils": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@optimizely/js-sdk-utils/-/js-sdk-utils-0.2.0.tgz", - "integrity": "sha512-aHEccRVc5YjWAdIVtniKfUE3tuzHriIWZTS4sLEq/lXkNTITSL1jrBEJD91CVY5BahWu/aG/aOafrA7XGH3sDQ==", + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@optimizely/js-sdk-utils/-/js-sdk-utils-0.3.2.tgz", + "integrity": "sha512-CeGzUrpUQkJQM8NMbzr1kK0SKiNOynxEAHKwyLhJrzGOpmQ+qhMW1B8yQlQjHaDmpUsiEZzo4TF8XoqFz9JLXA==", "requires": { - "uuid": "3.3.2" + "uuid": "^3.3.2" } }, "@types/jest": { @@ -64,7 +64,7 @@ "integrity": "sha512-k3OOeevcBYLR5pdsOv5g3OP94h3mrJmLPHFEPWgbbVy2tGv0TZ/TlygiC848ogXhK8NL0I5up7YYtwpCp8xCJA==", "dev": true, "requires": { - "@types/jest-diff": "20.0.1" + "@types/jest-diff": "*" } }, "@types/jest-diff": { @@ -91,8 +91,8 @@ "integrity": "sha512-hMtHj3s5RnuhvHPowpBYvJVj3rAar82JiDQHvGs1zO0l10ocX/xEdBShNHTJaboucJUsScghp74pH3s7EnHHQw==", "dev": true, "requires": { - "acorn": "6.1.1", - "acorn-walk": "6.1.1" + "acorn": "^6.0.1", + "acorn-walk": "^6.0.1" }, "dependencies": { "acorn": { @@ -115,10 +115,10 @@ "integrity": "sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg==", "dev": true, "requires": { - "fast-deep-equal": "2.0.1", - "fast-json-stable-stringify": "2.0.0", - "json-schema-traverse": "0.4.1", - "uri-js": "4.2.2" + "fast-deep-equal": "^2.0.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" } }, "ansi-escapes": { @@ -139,7 +139,7 @@ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "dev": true, "requires": { - "color-convert": "1.9.3" + "color-convert": "^1.9.0" } }, "anymatch": { @@ -148,8 +148,8 @@ "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", "dev": true, "requires": { - "micromatch": "3.1.10", - "normalize-path": "2.1.1" + "micromatch": "^3.1.4", + "normalize-path": "^2.1.1" }, "dependencies": { "arr-diff": { @@ -170,16 +170,16 @@ "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.3", - "snapdragon": "0.8.2", - "snapdragon-node": "2.1.1", - "split-string": "3.1.0", - "to-regex": "3.0.2" + "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": { @@ -188,7 +188,7 @@ "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", "dev": true, "requires": { - "is-extendable": "0.1.1" + "is-extendable": "^0.1.0" } } } @@ -199,13 +199,13 @@ "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", "dev": true, "requires": { - "debug": "2.6.9", - "define-property": "0.2.5", - "extend-shallow": "2.0.1", - "posix-character-classes": "0.1.1", - "regex-not": "1.0.2", - "snapdragon": "0.8.2", - "to-regex": "3.0.2" + "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": { "define-property": { @@ -214,7 +214,7 @@ "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", "dev": true, "requires": { - "is-descriptor": "0.1.6" + "is-descriptor": "^0.1.0" } }, "extend-shallow": { @@ -223,7 +223,7 @@ "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", "dev": true, "requires": { - "is-extendable": "0.1.1" + "is-extendable": "^0.1.0" } }, "is-accessor-descriptor": { @@ -232,7 +232,7 @@ "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", "dev": true, "requires": { - "kind-of": "3.2.2" + "kind-of": "^3.0.2" }, "dependencies": { "kind-of": { @@ -241,7 +241,7 @@ "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", "dev": true, "requires": { - "is-buffer": "1.1.6" + "is-buffer": "^1.1.5" } } } @@ -252,7 +252,7 @@ "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", "dev": true, "requires": { - "kind-of": "3.2.2" + "kind-of": "^3.0.2" }, "dependencies": { "kind-of": { @@ -261,7 +261,7 @@ "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", "dev": true, "requires": { - "is-buffer": "1.1.6" + "is-buffer": "^1.1.5" } } } @@ -272,9 +272,9 @@ "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", "dev": true, "requires": { - "is-accessor-descriptor": "0.1.6", - "is-data-descriptor": "0.1.4", - "kind-of": "5.1.0" + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" } }, "kind-of": { @@ -291,14 +291,14 @@ "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.2", - "snapdragon": "0.8.2", - "to-regex": "3.0.2" + "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": { @@ -307,7 +307,7 @@ "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", "dev": true, "requires": { - "is-descriptor": "1.0.2" + "is-descriptor": "^1.0.0" } }, "extend-shallow": { @@ -316,7 +316,7 @@ "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", "dev": true, "requires": { - "is-extendable": "0.1.1" + "is-extendable": "^0.1.0" } } } @@ -327,10 +327,10 @@ "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.1" + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" }, "dependencies": { "extend-shallow": { @@ -339,7 +339,7 @@ "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", "dev": true, "requires": { - "is-extendable": "0.1.1" + "is-extendable": "^0.1.0" } } } @@ -350,7 +350,7 @@ "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", "dev": true, "requires": { - "kind-of": "6.0.2" + "kind-of": "^6.0.0" } }, "is-data-descriptor": { @@ -359,7 +359,7 @@ "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", "dev": true, "requires": { - "kind-of": "6.0.2" + "kind-of": "^6.0.0" } }, "is-descriptor": { @@ -368,9 +368,9 @@ "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-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" } }, "is-number": { @@ -379,7 +379,7 @@ "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", "dev": true, "requires": { - "kind-of": "3.2.2" + "kind-of": "^3.0.2" }, "dependencies": { "kind-of": { @@ -388,7 +388,7 @@ "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", "dev": true, "requires": { - "is-buffer": "1.1.6" + "is-buffer": "^1.1.5" } } } @@ -411,19 +411,19 @@ "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", "dev": true, "requires": { - "arr-diff": "4.0.0", - "array-unique": "0.3.2", - "braces": "2.3.2", - "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.13", - "object.pick": "1.3.0", - "regex-not": "1.0.2", - "snapdragon": "0.8.2", - "to-regex": "3.0.2" + "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" } } } @@ -434,7 +434,7 @@ "integrity": "sha1-126/jKlNJ24keja61EpLdKthGZE=", "dev": true, "requires": { - "default-require-extensions": "1.0.0" + "default-require-extensions": "^1.0.0" } }, "argparse": { @@ -443,7 +443,7 @@ "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", "dev": true, "requires": { - "sprintf-js": "1.0.3" + "sprintf-js": "~1.0.2" } }, "arr-diff": { @@ -452,7 +452,7 @@ "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=", "dev": true, "requires": { - "arr-flatten": "1.1.0" + "arr-flatten": "^1.0.1" } }, "arr-flatten": { @@ -491,7 +491,7 @@ "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", "dev": true, "requires": { - "safer-buffer": "2.1.2" + "safer-buffer": "~2.1.0" } }, "assert-plus": { @@ -518,7 +518,7 @@ "integrity": "sha512-H1qVYh1MYhEEFLsP97cVKqCGo7KfCyTt6uEWqsTBr9SO84oK9Uwbyd/yCW+6rKJLHksBNUVWZDAjfS+Ccx0Bbg==", "dev": true, "requires": { - "lodash": "4.17.15" + "lodash": "^4.17.11" } }, "async-limiter": { @@ -557,9 +557,9 @@ "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", "dev": true, "requires": { - "chalk": "1.1.3", - "esutils": "2.0.2", - "js-tokens": "3.0.2" + "chalk": "^1.1.3", + "esutils": "^2.0.2", + "js-tokens": "^3.0.2" }, "dependencies": { "ansi-styles": { @@ -574,11 +574,11 @@ "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", "dev": true, "requires": { - "ansi-styles": "2.2.1", - "escape-string-regexp": "1.0.5", - "has-ansi": "2.0.0", - "strip-ansi": "3.0.1", - "supports-color": "2.0.0" + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" } }, "strip-ansi": { @@ -587,7 +587,7 @@ "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "dev": true, "requires": { - "ansi-regex": "2.1.1" + "ansi-regex": "^2.0.0" } }, "supports-color": { @@ -604,25 +604,25 @@ "integrity": "sha512-6jyFLuDmeidKmUEb3NM+/yawG0M2bDZ9Z1qbZP59cyHLz8kYGKYwpJP0UwUKKUiTRNvxfLesJnTedqczP7cTDA==", "dev": true, "requires": { - "babel-code-frame": "6.26.0", - "babel-generator": "6.26.1", - "babel-helpers": "6.24.1", - "babel-messages": "6.23.0", - "babel-register": "6.26.0", - "babel-runtime": "6.26.0", - "babel-template": "6.26.0", - "babel-traverse": "6.26.0", - "babel-types": "6.26.0", - "babylon": "6.18.0", - "convert-source-map": "1.6.0", - "debug": "2.6.9", - "json5": "0.5.1", - "lodash": "4.17.15", - "minimatch": "3.0.4", - "path-is-absolute": "1.0.1", - "private": "0.1.8", - "slash": "1.0.0", - "source-map": "0.5.7" + "babel-code-frame": "^6.26.0", + "babel-generator": "^6.26.0", + "babel-helpers": "^6.24.1", + "babel-messages": "^6.23.0", + "babel-register": "^6.26.0", + "babel-runtime": "^6.26.0", + "babel-template": "^6.26.0", + "babel-traverse": "^6.26.0", + "babel-types": "^6.26.0", + "babylon": "^6.18.0", + "convert-source-map": "^1.5.1", + "debug": "^2.6.9", + "json5": "^0.5.1", + "lodash": "^4.17.4", + "minimatch": "^3.0.4", + "path-is-absolute": "^1.0.1", + "private": "^0.1.8", + "slash": "^1.0.0", + "source-map": "^0.5.7" } }, "babel-generator": { @@ -631,14 +631,14 @@ "integrity": "sha512-HyfwY6ApZj7BYTcJURpM5tznulaBvyio7/0d4zFOeMPUmfxkCjHocCuoLa2SAGzBI8AREcH3eP3758F672DppA==", "dev": true, "requires": { - "babel-messages": "6.23.0", - "babel-runtime": "6.26.0", - "babel-types": "6.26.0", - "detect-indent": "4.0.0", - "jsesc": "1.3.0", - "lodash": "4.17.15", - "source-map": "0.5.7", - "trim-right": "1.0.1" + "babel-messages": "^6.23.0", + "babel-runtime": "^6.26.0", + "babel-types": "^6.26.0", + "detect-indent": "^4.0.0", + "jsesc": "^1.3.0", + "lodash": "^4.17.4", + "source-map": "^0.5.7", + "trim-right": "^1.0.1" } }, "babel-helpers": { @@ -647,8 +647,8 @@ "integrity": "sha1-NHHenK7DiOXIUOWX5Yom3fN2ArI=", "dev": true, "requires": { - "babel-runtime": "6.26.0", - "babel-template": "6.26.0" + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1" } }, "babel-jest": { @@ -657,8 +657,8 @@ "integrity": "sha512-lqKGG6LYXYu+DQh/slrQ8nxXQkEkhugdXsU6St7GmhVS7Ilc/22ArwqXNJrf0QaOBjZB0360qZMwXqDYQHXaew==", "dev": true, "requires": { - "babel-plugin-istanbul": "4.1.6", - "babel-preset-jest": "23.2.0" + "babel-plugin-istanbul": "^4.1.6", + "babel-preset-jest": "^23.2.0" } }, "babel-messages": { @@ -667,7 +667,7 @@ "integrity": "sha1-8830cDhYA1sqKVHG7F7fbGLyYw4=", "dev": true, "requires": { - "babel-runtime": "6.26.0" + "babel-runtime": "^6.22.0" } }, "babel-plugin-istanbul": { @@ -676,10 +676,10 @@ "integrity": "sha512-PWP9FQ1AhZhS01T/4qLSKoHGY/xvkZdVBGlKM/HuxxS3+sC66HhTNR7+MpbO/so/cz/wY94MeSWJuP1hXIPfwQ==", "dev": true, "requires": { - "babel-plugin-syntax-object-rest-spread": "6.13.0", - "find-up": "2.1.0", - "istanbul-lib-instrument": "1.10.2", - "test-exclude": "4.2.3" + "babel-plugin-syntax-object-rest-spread": "^6.13.0", + "find-up": "^2.1.0", + "istanbul-lib-instrument": "^1.10.1", + "test-exclude": "^4.2.1" } }, "babel-plugin-jest-hoist": { @@ -700,8 +700,8 @@ "integrity": "sha1-jsegOhOPABoaj7HoETZSvxpV2kY=", "dev": true, "requires": { - "babel-plugin-jest-hoist": "23.2.0", - "babel-plugin-syntax-object-rest-spread": "6.13.0" + "babel-plugin-jest-hoist": "^23.2.0", + "babel-plugin-syntax-object-rest-spread": "^6.13.0" } }, "babel-register": { @@ -710,13 +710,13 @@ "integrity": "sha1-btAhFz4vy0htestFxgCahW9kcHE=", "dev": true, "requires": { - "babel-core": "6.26.3", - "babel-runtime": "6.26.0", - "core-js": "2.6.5", - "home-or-tmp": "2.0.0", - "lodash": "4.17.15", - "mkdirp": "0.5.1", - "source-map-support": "0.4.18" + "babel-core": "^6.26.0", + "babel-runtime": "^6.26.0", + "core-js": "^2.5.0", + "home-or-tmp": "^2.0.0", + "lodash": "^4.17.4", + "mkdirp": "^0.5.1", + "source-map-support": "^0.4.15" } }, "babel-runtime": { @@ -725,8 +725,8 @@ "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", "dev": true, "requires": { - "core-js": "2.6.5", - "regenerator-runtime": "0.11.1" + "core-js": "^2.4.0", + "regenerator-runtime": "^0.11.0" } }, "babel-template": { @@ -735,11 +735,11 @@ "integrity": "sha1-3gPi0WOWsGn0bdn/+FIfsaDjXgI=", "dev": true, "requires": { - "babel-runtime": "6.26.0", - "babel-traverse": "6.26.0", - "babel-types": "6.26.0", - "babylon": "6.18.0", - "lodash": "4.17.15" + "babel-runtime": "^6.26.0", + "babel-traverse": "^6.26.0", + "babel-types": "^6.26.0", + "babylon": "^6.18.0", + "lodash": "^4.17.4" } }, "babel-traverse": { @@ -748,15 +748,15 @@ "integrity": "sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4=", "dev": true, "requires": { - "babel-code-frame": "6.26.0", - "babel-messages": "6.23.0", - "babel-runtime": "6.26.0", - "babel-types": "6.26.0", - "babylon": "6.18.0", - "debug": "2.6.9", - "globals": "9.18.0", - "invariant": "2.2.4", - "lodash": "4.17.15" + "babel-code-frame": "^6.26.0", + "babel-messages": "^6.23.0", + "babel-runtime": "^6.26.0", + "babel-types": "^6.26.0", + "babylon": "^6.18.0", + "debug": "^2.6.8", + "globals": "^9.18.0", + "invariant": "^2.2.2", + "lodash": "^4.17.4" } }, "babel-types": { @@ -765,10 +765,10 @@ "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", "dev": true, "requires": { - "babel-runtime": "6.26.0", - "esutils": "2.0.2", - "lodash": "4.17.15", - "to-fast-properties": "1.0.3" + "babel-runtime": "^6.26.0", + "esutils": "^2.0.2", + "lodash": "^4.17.4", + "to-fast-properties": "^1.0.3" } }, "babylon": { @@ -789,13 +789,13 @@ "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", "dev": true, "requires": { - "cache-base": "1.0.1", - "class-utils": "0.3.6", - "component-emitter": "1.2.1", - "define-property": "1.0.0", - "isobject": "3.0.1", - "mixin-deep": "1.3.1", - "pascalcase": "0.1.1" + "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" }, "dependencies": { "define-property": { @@ -804,7 +804,7 @@ "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", "dev": true, "requires": { - "is-descriptor": "1.0.2" + "is-descriptor": "^1.0.0" } }, "is-accessor-descriptor": { @@ -813,7 +813,7 @@ "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", "dev": true, "requires": { - "kind-of": "6.0.2" + "kind-of": "^6.0.0" } }, "is-data-descriptor": { @@ -822,7 +822,7 @@ "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", "dev": true, "requires": { - "kind-of": "6.0.2" + "kind-of": "^6.0.0" } }, "is-descriptor": { @@ -831,9 +831,9 @@ "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-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" } }, "isobject": { @@ -856,7 +856,7 @@ "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", "dev": true, "requires": { - "tweetnacl": "0.14.5" + "tweetnacl": "^0.14.3" } }, "brace-expansion": { @@ -865,7 +865,7 @@ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, "requires": { - "balanced-match": "1.0.0", + "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, @@ -875,9 +875,9 @@ "integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=", "dev": true, "requires": { - "expand-range": "1.8.2", - "preserve": "0.2.0", - "repeat-element": "1.1.3" + "expand-range": "^1.8.1", + "preserve": "^0.2.0", + "repeat-element": "^1.1.2" } }, "browser-process-hrtime": { @@ -909,7 +909,7 @@ "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", "dev": true, "requires": { - "fast-json-stable-stringify": "2.0.0" + "fast-json-stable-stringify": "2.x" } }, "bser": { @@ -918,7 +918,7 @@ "integrity": "sha1-mseNPtXZFYBP2HrLFYvHlxR6Fxk=", "dev": true, "requires": { - "node-int64": "0.4.0" + "node-int64": "^0.4.0" } }, "buffer-from": { @@ -933,15 +933,15 @@ "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", "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" + "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" }, "dependencies": { "isobject": { @@ -970,7 +970,7 @@ "integrity": "sha1-HF/MSJ/QqwDU8ax64QcuMXP7q28=", "dev": true, "requires": { - "rsvp": "3.6.2" + "rsvp": "^3.3.3" } }, "caseless": { @@ -985,9 +985,9 @@ "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "dev": true, "requires": { - "ansi-styles": "3.2.1", - "escape-string-regexp": "1.0.5", - "supports-color": "5.5.0" + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" } }, "ci-info": { @@ -1002,10 +1002,10 @@ "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", "dev": true, "requires": { - "arr-union": "3.1.0", - "define-property": "0.2.5", - "isobject": "3.0.1", - "static-extend": "0.1.2" + "arr-union": "^3.1.0", + "define-property": "^0.2.5", + "isobject": "^3.0.0", + "static-extend": "^0.1.1" }, "dependencies": { "define-property": { @@ -1014,7 +1014,7 @@ "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", "dev": true, "requires": { - "is-descriptor": "0.1.6" + "is-descriptor": "^0.1.0" } }, "isobject": { @@ -1031,9 +1031,9 @@ "integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==", "dev": true, "requires": { - "string-width": "2.1.1", - "strip-ansi": "4.0.0", - "wrap-ansi": "2.1.0" + "string-width": "^2.1.1", + "strip-ansi": "^4.0.0", + "wrap-ansi": "^2.0.0" } }, "co": { @@ -1054,8 +1054,8 @@ "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", "dev": true, "requires": { - "map-visit": "1.0.0", - "object-visit": "1.0.1" + "map-visit": "^1.0.0", + "object-visit": "^1.0.0" } }, "color-convert": { @@ -1079,7 +1079,7 @@ "integrity": "sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w==", "dev": true, "requires": { - "delayed-stream": "1.0.0" + "delayed-stream": "~1.0.0" } }, "commander": { @@ -1107,7 +1107,7 @@ "integrity": "sha512-eFu7XigvxdZ1ETfbgPBohgyQ/Z++C0eEhTor0qRwBw9unw+L0/6V8wkSuGgzdThkiS5lSpdptOQPD8Ak40a+7A==", "dev": true, "requires": { - "safe-buffer": "5.1.2" + "safe-buffer": "~5.1.1" } }, "copy-descriptor": { @@ -1134,9 +1134,9 @@ "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", "dev": true, "requires": { - "lru-cache": "4.1.5", - "shebang-command": "1.2.0", - "which": "1.3.1" + "lru-cache": "^4.0.1", + "shebang-command": "^1.2.0", + "which": "^1.2.9" } }, "cssom": { @@ -1151,7 +1151,7 @@ "integrity": "sha512-7DYm8qe+gPx/h77QlCyFmX80+fGaE/6A/Ekl0zaszYOubvySO2saYFdQ78P29D0UsULxFKCetDGNaNRUdSF+2A==", "dev": true, "requires": { - "cssom": "0.3.6" + "cssom": "0.3.x" } }, "dashdash": { @@ -1160,7 +1160,7 @@ "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", "dev": true, "requires": { - "assert-plus": "1.0.0" + "assert-plus": "^1.0.0" } }, "data-urls": { @@ -1169,9 +1169,9 @@ "integrity": "sha512-YTWYI9se1P55u58gL5GkQHW4P6VJBJ5iBT+B5a7i2Tjadhv52paJG0qHX4A0OR6/t52odI64KP2YvFpkDOi3eQ==", "dev": true, "requires": { - "abab": "2.0.0", - "whatwg-mimetype": "2.3.0", - "whatwg-url": "7.0.0" + "abab": "^2.0.0", + "whatwg-mimetype": "^2.2.0", + "whatwg-url": "^7.0.0" }, "dependencies": { "whatwg-url": { @@ -1180,9 +1180,9 @@ "integrity": "sha512-37GeVSIJ3kn1JgKyjiYNmSLP1yzbpb29jdmwBSgkD9h40/hyrR/OifpVUndji3tmwGgD8qpw7iQu3RSbCrBpsQ==", "dev": true, "requires": { - "lodash.sortby": "4.7.0", - "tr46": "1.0.1", - "webidl-conversions": "4.0.2" + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" } } } @@ -1220,7 +1220,7 @@ "integrity": "sha1-836hXT4T/9m0N9M+GnW1+5eHTLg=", "dev": true, "requires": { - "strip-bom": "2.0.0" + "strip-bom": "^2.0.0" } }, "define-properties": { @@ -1229,7 +1229,7 @@ "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", "dev": true, "requires": { - "object-keys": "1.1.0" + "object-keys": "^1.0.12" } }, "define-property": { @@ -1238,8 +1238,8 @@ "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", "dev": true, "requires": { - "is-descriptor": "1.0.2", - "isobject": "3.0.1" + "is-descriptor": "^1.0.2", + "isobject": "^3.0.1" }, "dependencies": { "is-accessor-descriptor": { @@ -1248,7 +1248,7 @@ "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", "dev": true, "requires": { - "kind-of": "6.0.2" + "kind-of": "^6.0.0" } }, "is-data-descriptor": { @@ -1257,7 +1257,7 @@ "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", "dev": true, "requires": { - "kind-of": "6.0.2" + "kind-of": "^6.0.0" } }, "is-descriptor": { @@ -1266,9 +1266,9 @@ "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-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" } }, "isobject": { @@ -1297,7 +1297,7 @@ "integrity": "sha1-920GQ1LN9Docts5hnE7jqUdd4gg=", "dev": true, "requires": { - "repeating": "2.0.1" + "repeating": "^2.0.0" } }, "detect-newline": { @@ -1318,7 +1318,7 @@ "integrity": "sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug==", "dev": true, "requires": { - "webidl-conversions": "4.0.2" + "webidl-conversions": "^4.0.2" } }, "ecc-jsbn": { @@ -1327,8 +1327,8 @@ "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", "dev": true, "requires": { - "jsbn": "0.1.1", - "safer-buffer": "2.1.2" + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" } }, "error-ex": { @@ -1337,7 +1337,7 @@ "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", "dev": true, "requires": { - "is-arrayish": "0.2.1" + "is-arrayish": "^0.2.1" } }, "es-abstract": { @@ -1346,12 +1346,12 @@ "integrity": "sha512-vDZfg/ykNxQVwup/8E1BZhVzFfBxs9NqMzGcvIJrqg5k2/5Za2bWo40dK2J1pgLngZ7c+Shh8lwYtLGyrwPutg==", "dev": true, "requires": { - "es-to-primitive": "1.2.0", - "function-bind": "1.1.1", - "has": "1.0.3", - "is-callable": "1.1.4", - "is-regex": "1.0.4", - "object-keys": "1.1.0" + "es-to-primitive": "^1.2.0", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "is-callable": "^1.1.4", + "is-regex": "^1.0.4", + "object-keys": "^1.0.12" } }, "es-to-primitive": { @@ -1360,9 +1360,9 @@ "integrity": "sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg==", "dev": true, "requires": { - "is-callable": "1.1.4", - "is-date-object": "1.0.1", - "is-symbol": "1.0.2" + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" } }, "escape-string-regexp": { @@ -1377,11 +1377,11 @@ "integrity": "sha512-JwiqFD9KdGVVpeuRa68yU3zZnBEOcPs0nKW7wZzXky8Z7tffdYUHbe11bPCV5jYlK6DVdKLWLm0f5I/QlL0Kmw==", "dev": true, "requires": { - "esprima": "3.1.3", - "estraverse": "4.2.0", - "esutils": "2.0.2", - "optionator": "0.8.2", - "source-map": "0.6.1" + "esprima": "^3.1.3", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1", + "source-map": "~0.6.1" }, "dependencies": { "esprima": { @@ -1423,7 +1423,7 @@ "integrity": "sha512-FIUCJz1RbuS0FKTdaAafAByGS0CPvU3R0MeHxgtl+djzCc//F8HakL8GzmVNZanasTbTAY/3DRFA0KpVqj/eAw==", "dev": true, "requires": { - "merge": "1.2.1" + "merge": "^1.2.0" } }, "execa": { @@ -1432,13 +1432,13 @@ "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=", "dev": true, "requires": { - "cross-spawn": "5.1.0", - "get-stream": "3.0.0", - "is-stream": "1.1.0", - "npm-run-path": "2.0.2", - "p-finally": "1.0.0", - "signal-exit": "3.0.2", - "strip-eof": "1.0.0" + "cross-spawn": "^5.0.1", + "get-stream": "^3.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" } }, "exit": { @@ -1453,7 +1453,7 @@ "integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=", "dev": true, "requires": { - "is-posix-bracket": "0.1.1" + "is-posix-bracket": "^0.1.0" } }, "expand-range": { @@ -1462,7 +1462,7 @@ "integrity": "sha1-opnv/TNf4nIeuujiV+x5ZE/IUzc=", "dev": true, "requires": { - "fill-range": "2.2.4" + "fill-range": "^2.1.0" } }, "expect": { @@ -1471,12 +1471,12 @@ "integrity": "sha512-dgSoOHgmtn/aDGRVFWclQyPDKl2CQRq0hmIEoUAuQs/2rn2NcvCWcSCovm6BLeuB/7EZuLGu2QfnR+qRt5OM4w==", "dev": true, "requires": { - "ansi-styles": "3.2.1", - "jest-diff": "23.6.0", - "jest-get-type": "22.4.3", - "jest-matcher-utils": "23.6.0", - "jest-message-util": "23.4.0", - "jest-regex-util": "23.3.0" + "ansi-styles": "^3.2.0", + "jest-diff": "^23.6.0", + "jest-get-type": "^22.1.0", + "jest-matcher-utils": "^23.6.0", + "jest-message-util": "^23.4.0", + "jest-regex-util": "^23.3.0" } }, "extend": { @@ -1491,8 +1491,8 @@ "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", "dev": true, "requires": { - "assign-symbols": "1.0.0", - "is-extendable": "1.0.1" + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" }, "dependencies": { "is-extendable": { @@ -1501,7 +1501,7 @@ "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", "dev": true, "requires": { - "is-plain-object": "2.0.4" + "is-plain-object": "^2.0.4" } } } @@ -1512,7 +1512,7 @@ "integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=", "dev": true, "requires": { - "is-extglob": "1.0.0" + "is-extglob": "^1.0.0" } }, "extsprintf": { @@ -1545,7 +1545,7 @@ "integrity": "sha1-VOmr99+i8mzZsWNsWIwa/AXeXVg=", "dev": true, "requires": { - "bser": "2.0.0" + "bser": "^2.0.0" } }, "filename-regex": { @@ -1560,8 +1560,8 @@ "integrity": "sha1-jnVIqW08wjJ+5eZ0FocjozO7oqA=", "dev": true, "requires": { - "glob": "7.1.3", - "minimatch": "3.0.4" + "glob": "^7.0.3", + "minimatch": "^3.0.3" } }, "fill-range": { @@ -1570,11 +1570,11 @@ "integrity": "sha512-cnrcCbj01+j2gTG921VZPnHbjmdAf8oQV/iGeV2kZxGSyfYjjTyY79ErsK1WJWMpw6DaApEX72binqJE+/d+5Q==", "dev": true, "requires": { - "is-number": "2.1.0", - "isobject": "2.1.0", - "randomatic": "3.1.1", - "repeat-element": "1.1.3", - "repeat-string": "1.6.1" + "is-number": "^2.1.0", + "isobject": "^2.0.0", + "randomatic": "^3.0.0", + "repeat-element": "^1.1.2", + "repeat-string": "^1.5.2" } }, "find-up": { @@ -1583,7 +1583,7 @@ "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", "dev": true, "requires": { - "locate-path": "2.0.0" + "locate-path": "^2.0.0" } }, "for-in": { @@ -1598,7 +1598,7 @@ "integrity": "sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4=", "dev": true, "requires": { - "for-in": "1.0.2" + "for-in": "^1.0.1" } }, "forever-agent": { @@ -1613,9 +1613,9 @@ "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", "dev": true, "requires": { - "asynckit": "0.4.0", - "combined-stream": "1.0.7", - "mime-types": "2.1.22" + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" } }, "fragment-cache": { @@ -1624,7 +1624,7 @@ "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", "dev": true, "requires": { - "map-cache": "0.2.2" + "map-cache": "^0.2.2" } }, "fs.realpath": { @@ -1640,8 +1640,8 @@ "dev": true, "optional": true, "requires": { - "nan": "2.12.1", - "node-pre-gyp": "0.10.3" + "nan": "^2.9.2", + "node-pre-gyp": "^0.10.0" }, "dependencies": { "abbrev": { @@ -2211,7 +2211,7 @@ "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", "dev": true, "requires": { - "assert-plus": "1.0.0" + "assert-plus": "^1.0.0" } }, "glob": { @@ -2220,12 +2220,12 @@ "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", "dev": true, "requires": { - "fs.realpath": "1.0.0", - "inflight": "1.0.6", - "inherits": "2.0.3", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" + "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-base": { @@ -2234,8 +2234,8 @@ "integrity": "sha1-27Fk9iIbHAscz4Kuoyi0l98Oo8Q=", "dev": true, "requires": { - "glob-parent": "2.0.0", - "is-glob": "2.0.1" + "glob-parent": "^2.0.0", + "is-glob": "^2.0.0" } }, "glob-parent": { @@ -2244,7 +2244,7 @@ "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=", "dev": true, "requires": { - "is-glob": "2.0.1" + "is-glob": "^2.0.0" } }, "globals": { @@ -2271,10 +2271,10 @@ "integrity": "sha512-nvfrjqvt9xQ8Z/w0ijewdD/vvWDTOweBUm96NTr66Wfvo1mJenBLwcYmPs3TIBP5ruzYGD7Hx/DaM9RmhroGPw==", "dev": true, "requires": { - "neo-async": "2.6.1", - "optimist": "0.6.1", - "source-map": "0.6.1", - "uglify-js": "3.4.9" + "neo-async": "^2.6.0", + "optimist": "^0.6.1", + "source-map": "^0.6.1", + "uglify-js": "^3.1.4" }, "dependencies": { "source-map": { @@ -2297,8 +2297,8 @@ "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", "dev": true, "requires": { - "ajv": "6.10.0", - "har-schema": "2.0.0" + "ajv": "^6.5.5", + "har-schema": "^2.0.0" } }, "has": { @@ -2307,7 +2307,7 @@ "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", "dev": true, "requires": { - "function-bind": "1.1.1" + "function-bind": "^1.1.1" } }, "has-ansi": { @@ -2316,7 +2316,7 @@ "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", "dev": true, "requires": { - "ansi-regex": "2.1.1" + "ansi-regex": "^2.0.0" } }, "has-flag": { @@ -2337,9 +2337,9 @@ "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", "dev": true, "requires": { - "get-value": "2.0.6", - "has-values": "1.0.0", - "isobject": "3.0.1" + "get-value": "^2.0.6", + "has-values": "^1.0.0", + "isobject": "^3.0.0" }, "dependencies": { "isobject": { @@ -2356,8 +2356,8 @@ "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", "dev": true, "requires": { - "is-number": "3.0.0", - "kind-of": "4.0.0" + "is-number": "^3.0.0", + "kind-of": "^4.0.0" }, "dependencies": { "is-number": { @@ -2366,7 +2366,7 @@ "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", "dev": true, "requires": { - "kind-of": "3.2.2" + "kind-of": "^3.0.2" }, "dependencies": { "kind-of": { @@ -2375,7 +2375,7 @@ "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", "dev": true, "requires": { - "is-buffer": "1.1.6" + "is-buffer": "^1.1.5" } } } @@ -2386,7 +2386,7 @@ "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", "dev": true, "requires": { - "is-buffer": "1.1.6" + "is-buffer": "^1.1.5" } } } @@ -2397,8 +2397,8 @@ "integrity": "sha1-42w/LSyufXRqhX440Y1fMqeILbg=", "dev": true, "requires": { - "os-homedir": "1.0.2", - "os-tmpdir": "1.0.2" + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.1" } }, "hosted-git-info": { @@ -2413,7 +2413,7 @@ "integrity": "sha512-71lZziiDnsuabfdYiUeWdCVyKuqwWi23L8YeIgV9jSSZHCtb6wB1BKWooH7L3tn4/FuZJMVWyNaIDr4RGmaSYw==", "dev": true, "requires": { - "whatwg-encoding": "1.0.5" + "whatwg-encoding": "^1.0.1" } }, "http-signature": { @@ -2422,9 +2422,9 @@ "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", "dev": true, "requires": { - "assert-plus": "1.0.0", - "jsprim": "1.4.1", - "sshpk": "1.16.1" + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" } }, "iconv-lite": { @@ -2433,7 +2433,7 @@ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "dev": true, "requires": { - "safer-buffer": "2.1.2" + "safer-buffer": ">= 2.1.2 < 3" } }, "import-local": { @@ -2442,8 +2442,8 @@ "integrity": "sha512-vAaZHieK9qjGo58agRBg+bhHX3hoTZU/Oa3GESWLz7t1U62fk63aHuDJJEteXoDeTCcPmUT+z38gkHPZkkmpmQ==", "dev": true, "requires": { - "pkg-dir": "2.0.0", - "resolve-cwd": "2.0.0" + "pkg-dir": "^2.0.0", + "resolve-cwd": "^2.0.0" } }, "imurmurhash": { @@ -2458,8 +2458,8 @@ "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", "dev": true, "requires": { - "once": "1.4.0", - "wrappy": "1.0.2" + "once": "^1.3.0", + "wrappy": "1" } }, "inherits": { @@ -2474,7 +2474,7 @@ "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", "dev": true, "requires": { - "loose-envify": "1.4.0" + "loose-envify": "^1.0.0" } }, "invert-kv": { @@ -2489,7 +2489,7 @@ "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", "dev": true, "requires": { - "kind-of": "3.2.2" + "kind-of": "^3.0.2" } }, "is-arrayish": { @@ -2516,7 +2516,7 @@ "integrity": "sha512-s6tfsaQaQi3JNciBH6shVqEDvhGut0SUXr31ag8Pd8BBbVVlcGfWhpPmEOoM6RJ5TFhbypvf5yyRw/VXW1IiWg==", "dev": true, "requires": { - "ci-info": "1.6.0" + "ci-info": "^1.5.0" } }, "is-data-descriptor": { @@ -2525,7 +2525,7 @@ "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", "dev": true, "requires": { - "kind-of": "3.2.2" + "kind-of": "^3.0.2" } }, "is-date-object": { @@ -2540,9 +2540,9 @@ "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", "dev": true, "requires": { - "is-accessor-descriptor": "0.1.6", - "is-data-descriptor": "0.1.4", - "kind-of": "5.1.0" + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" }, "dependencies": { "kind-of": { @@ -2565,7 +2565,7 @@ "integrity": "sha1-IjgJj8Ih3gvPpdnqxMRdY4qhxTQ=", "dev": true, "requires": { - "is-primitive": "2.0.0" + "is-primitive": "^2.0.0" } }, "is-extendable": { @@ -2586,7 +2586,7 @@ "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=", "dev": true, "requires": { - "number-is-nan": "1.0.1" + "number-is-nan": "^1.0.0" } }, "is-fullwidth-code-point": { @@ -2607,7 +2607,7 @@ "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", "dev": true, "requires": { - "is-extglob": "1.0.0" + "is-extglob": "^1.0.0" } }, "is-number": { @@ -2616,7 +2616,7 @@ "integrity": "sha1-Afy7s5NGOlSPL0ZszhbezknbkI8=", "dev": true, "requires": { - "kind-of": "3.2.2" + "kind-of": "^3.0.2" } }, "is-plain-object": { @@ -2625,7 +2625,7 @@ "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", "dev": true, "requires": { - "isobject": "3.0.1" + "isobject": "^3.0.1" }, "dependencies": { "isobject": { @@ -2654,7 +2654,7 @@ "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", "dev": true, "requires": { - "has": "1.0.3" + "has": "^1.0.1" } }, "is-stream": { @@ -2669,7 +2669,7 @@ "integrity": "sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw==", "dev": true, "requires": { - "has-symbols": "1.0.0" + "has-symbols": "^1.0.0" } }, "is-typedarray": { @@ -2729,17 +2729,17 @@ "integrity": "sha512-4/ApBnMVeEPG3EkSzcw25wDe4N66wxwn+KKn6b47vyek8Xb3NBAcg4xfuQbS7BqcZuTX4wxfD5lVagdggR3gyA==", "dev": true, "requires": { - "async": "2.6.2", - "fileset": "2.0.3", - "istanbul-lib-coverage": "1.2.1", - "istanbul-lib-hook": "1.2.2", - "istanbul-lib-instrument": "1.10.2", - "istanbul-lib-report": "1.1.5", - "istanbul-lib-source-maps": "1.2.6", - "istanbul-reports": "1.5.1", - "js-yaml": "3.13.1", - "mkdirp": "0.5.1", - "once": "1.4.0" + "async": "^2.1.4", + "fileset": "^2.0.2", + "istanbul-lib-coverage": "^1.2.1", + "istanbul-lib-hook": "^1.2.2", + "istanbul-lib-instrument": "^1.10.2", + "istanbul-lib-report": "^1.1.5", + "istanbul-lib-source-maps": "^1.2.6", + "istanbul-reports": "^1.5.1", + "js-yaml": "^3.7.0", + "mkdirp": "^0.5.1", + "once": "^1.4.0" } }, "istanbul-lib-coverage": { @@ -2754,7 +2754,7 @@ "integrity": "sha512-/Jmq7Y1VeHnZEQ3TL10VHyb564mn6VrQXHchON9Jf/AEcmQ3ZIiyD1BVzNOKTZf/G3gE+kiGK6SmpF9y3qGPLw==", "dev": true, "requires": { - "append-transform": "0.4.0" + "append-transform": "^0.4.0" } }, "istanbul-lib-instrument": { @@ -2763,13 +2763,13 @@ "integrity": "sha512-aWHxfxDqvh/ZlxR8BBaEPVSWDPUkGD63VjGQn3jcw8jCp7sHEMKcrj4xfJn/ABzdMEHiQNyvDQhqm5o8+SQg7A==", "dev": true, "requires": { - "babel-generator": "6.26.1", - "babel-template": "6.26.0", - "babel-traverse": "6.26.0", - "babel-types": "6.26.0", - "babylon": "6.18.0", - "istanbul-lib-coverage": "1.2.1", - "semver": "5.6.0" + "babel-generator": "^6.18.0", + "babel-template": "^6.16.0", + "babel-traverse": "^6.18.0", + "babel-types": "^6.18.0", + "babylon": "^6.18.0", + "istanbul-lib-coverage": "^1.2.1", + "semver": "^5.3.0" } }, "istanbul-lib-report": { @@ -2778,10 +2778,10 @@ "integrity": "sha512-UsYfRMoi6QO/doUshYNqcKJqVmFe9w51GZz8BS3WB0lYxAllQYklka2wP9+dGZeHYaWIdcXUx8JGdbqaoXRXzw==", "dev": true, "requires": { - "istanbul-lib-coverage": "1.2.1", - "mkdirp": "0.5.1", - "path-parse": "1.0.6", - "supports-color": "3.2.3" + "istanbul-lib-coverage": "^1.2.1", + "mkdirp": "^0.5.1", + "path-parse": "^1.0.5", + "supports-color": "^3.1.2" }, "dependencies": { "has-flag": { @@ -2796,7 +2796,7 @@ "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", "dev": true, "requires": { - "has-flag": "1.0.0" + "has-flag": "^1.0.0" } } } @@ -2807,11 +2807,11 @@ "integrity": "sha512-TtbsY5GIHgbMsMiRw35YBHGpZ1DVFEO19vxxeiDMYaeOFOCzfnYVxvl6pOUIZR4dtPhAGpSMup8OyF8ubsaqEg==", "dev": true, "requires": { - "debug": "3.2.6", - "istanbul-lib-coverage": "1.2.1", - "mkdirp": "0.5.1", - "rimraf": "2.6.3", - "source-map": "0.5.7" + "debug": "^3.1.0", + "istanbul-lib-coverage": "^1.2.1", + "mkdirp": "^0.5.1", + "rimraf": "^2.6.1", + "source-map": "^0.5.3" }, "dependencies": { "debug": { @@ -2820,7 +2820,7 @@ "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", "dev": true, "requires": { - "ms": "2.1.1" + "ms": "^2.1.1" } }, "ms": { @@ -2837,7 +2837,7 @@ "integrity": "sha512-+cfoZ0UXzWjhAdzosCPP3AN8vvef8XDkWtTfgaN+7L3YTpNYITnCaEkceo5SEYy644VkHka/P1FvkWvrG/rrJw==", "dev": true, "requires": { - "handlebars": "4.1.2" + "handlebars": "^4.0.3" } }, "jest": { @@ -2846,8 +2846,8 @@ "integrity": "sha512-lWzcd+HSiqeuxyhG+EnZds6iO3Y3ZEnMrfZq/OTGvF/C+Z4fPMCdhWTGSAiO2Oym9rbEXfwddHhh6jqrTF3+Lw==", "dev": true, "requires": { - "import-local": "1.0.0", - "jest-cli": "23.6.0" + "import-local": "^1.0.0", + "jest-cli": "^23.6.0" }, "dependencies": { "jest-cli": { @@ -2856,42 +2856,42 @@ "integrity": "sha512-hgeD1zRUp1E1zsiyOXjEn4LzRLWdJBV//ukAHGlx6s5mfCNJTbhbHjgxnDUXA8fsKWN/HqFFF6X5XcCwC/IvYQ==", "dev": true, "requires": { - "ansi-escapes": "3.2.0", - "chalk": "2.4.2", - "exit": "0.1.2", - "glob": "7.1.3", - "graceful-fs": "4.1.15", - "import-local": "1.0.0", - "is-ci": "1.2.1", - "istanbul-api": "1.3.7", - "istanbul-lib-coverage": "1.2.1", - "istanbul-lib-instrument": "1.10.2", - "istanbul-lib-source-maps": "1.2.6", - "jest-changed-files": "23.4.2", - "jest-config": "23.6.0", - "jest-environment-jsdom": "23.4.0", - "jest-get-type": "22.4.3", - "jest-haste-map": "23.6.0", - "jest-message-util": "23.4.0", - "jest-regex-util": "23.3.0", - "jest-resolve-dependencies": "23.6.0", - "jest-runner": "23.6.0", - "jest-runtime": "23.6.0", - "jest-snapshot": "23.6.0", - "jest-util": "23.4.0", - "jest-validate": "23.6.0", - "jest-watcher": "23.4.0", - "jest-worker": "23.2.0", - "micromatch": "2.3.11", - "node-notifier": "5.4.0", - "prompts": "0.1.14", - "realpath-native": "1.1.0", - "rimraf": "2.6.3", - "slash": "1.0.0", - "string-length": "2.0.0", - "strip-ansi": "4.0.0", - "which": "1.3.1", - "yargs": "11.1.0" + "ansi-escapes": "^3.0.0", + "chalk": "^2.0.1", + "exit": "^0.1.2", + "glob": "^7.1.2", + "graceful-fs": "^4.1.11", + "import-local": "^1.0.0", + "is-ci": "^1.0.10", + "istanbul-api": "^1.3.1", + "istanbul-lib-coverage": "^1.2.0", + "istanbul-lib-instrument": "^1.10.1", + "istanbul-lib-source-maps": "^1.2.4", + "jest-changed-files": "^23.4.2", + "jest-config": "^23.6.0", + "jest-environment-jsdom": "^23.4.0", + "jest-get-type": "^22.1.0", + "jest-haste-map": "^23.6.0", + "jest-message-util": "^23.4.0", + "jest-regex-util": "^23.3.0", + "jest-resolve-dependencies": "^23.6.0", + "jest-runner": "^23.6.0", + "jest-runtime": "^23.6.0", + "jest-snapshot": "^23.6.0", + "jest-util": "^23.4.0", + "jest-validate": "^23.6.0", + "jest-watcher": "^23.4.0", + "jest-worker": "^23.2.0", + "micromatch": "^2.3.11", + "node-notifier": "^5.2.1", + "prompts": "^0.1.9", + "realpath-native": "^1.0.0", + "rimraf": "^2.5.4", + "slash": "^1.0.0", + "string-length": "^2.0.0", + "strip-ansi": "^4.0.0", + "which": "^1.2.12", + "yargs": "^11.0.0" } } } @@ -2902,7 +2902,7 @@ "integrity": "sha512-EyNhTAUWEfwnK0Is/09LxoqNDOn7mU7S3EHskG52djOFS/z+IT0jT3h3Ql61+dklcG7bJJitIWEMB4Sp1piHmA==", "dev": true, "requires": { - "throat": "4.1.0" + "throat": "^4.0.0" } }, "jest-config": { @@ -2911,20 +2911,20 @@ "integrity": "sha512-i8V7z9BeDXab1+VNo78WM0AtWpBRXJLnkT+lyT+Slx/cbP5sZJ0+NDuLcmBE5hXAoK0aUp7vI+MOxR+R4d8SRQ==", "dev": true, "requires": { - "babel-core": "6.26.3", - "babel-jest": "23.6.0", - "chalk": "2.4.2", - "glob": "7.1.3", - "jest-environment-jsdom": "23.4.0", - "jest-environment-node": "23.4.0", - "jest-get-type": "22.4.3", - "jest-jasmine2": "23.6.0", - "jest-regex-util": "23.3.0", - "jest-resolve": "23.6.0", - "jest-util": "23.4.0", - "jest-validate": "23.6.0", - "micromatch": "2.3.11", - "pretty-format": "23.6.0" + "babel-core": "^6.0.0", + "babel-jest": "^23.6.0", + "chalk": "^2.0.1", + "glob": "^7.1.1", + "jest-environment-jsdom": "^23.4.0", + "jest-environment-node": "^23.4.0", + "jest-get-type": "^22.1.0", + "jest-jasmine2": "^23.6.0", + "jest-regex-util": "^23.3.0", + "jest-resolve": "^23.6.0", + "jest-util": "^23.4.0", + "jest-validate": "^23.6.0", + "micromatch": "^2.3.11", + "pretty-format": "^23.6.0" } }, "jest-diff": { @@ -2933,10 +2933,10 @@ "integrity": "sha512-Gz9l5Ov+X3aL5L37IT+8hoCUsof1CVYBb2QEkOupK64XyRR3h+uRpYIm97K7sY8diFxowR8pIGEdyfMKTixo3g==", "dev": true, "requires": { - "chalk": "2.4.2", - "diff": "3.5.0", - "jest-get-type": "22.4.3", - "pretty-format": "23.6.0" + "chalk": "^2.0.1", + "diff": "^3.2.0", + "jest-get-type": "^22.1.0", + "pretty-format": "^23.6.0" } }, "jest-docblock": { @@ -2945,7 +2945,7 @@ "integrity": "sha1-8IXh8YVI2Z/dabICB+b9VdkTg6c=", "dev": true, "requires": { - "detect-newline": "2.1.0" + "detect-newline": "^2.1.0" } }, "jest-each": { @@ -2954,8 +2954,8 @@ "integrity": "sha512-x7V6M/WGJo6/kLoissORuvLIeAoyo2YqLOoCDkohgJ4XOXSqOtyvr8FbInlAWS77ojBsZrafbozWoKVRdtxFCg==", "dev": true, "requires": { - "chalk": "2.4.2", - "pretty-format": "23.6.0" + "chalk": "^2.0.1", + "pretty-format": "^23.6.0" } }, "jest-environment-jsdom": { @@ -2964,9 +2964,9 @@ "integrity": "sha1-BWp5UrP+pROsYqFAosNox52eYCM=", "dev": true, "requires": { - "jest-mock": "23.2.0", - "jest-util": "23.4.0", - "jsdom": "11.12.0" + "jest-mock": "^23.2.0", + "jest-util": "^23.4.0", + "jsdom": "^11.5.1" } }, "jest-environment-node": { @@ -2975,8 +2975,8 @@ "integrity": "sha1-V+gO0IQd6jAxZ8zozXlSHeuv3hA=", "dev": true, "requires": { - "jest-mock": "23.2.0", - "jest-util": "23.4.0" + "jest-mock": "^23.2.0", + "jest-util": "^23.4.0" } }, "jest-get-type": { @@ -2991,14 +2991,14 @@ "integrity": "sha512-uyNhMyl6dr6HaXGHp8VF7cK6KpC6G9z9LiMNsst+rJIZ8l7wY0tk8qwjPmEghczojZ2/ZhtEdIabZ0OQRJSGGg==", "dev": true, "requires": { - "fb-watchman": "2.0.0", - "graceful-fs": "4.1.15", - "invariant": "2.2.4", - "jest-docblock": "23.2.0", - "jest-serializer": "23.0.1", - "jest-worker": "23.2.0", - "micromatch": "2.3.11", - "sane": "2.5.2" + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.1.11", + "invariant": "^2.2.4", + "jest-docblock": "^23.2.0", + "jest-serializer": "^23.0.1", + "jest-worker": "^23.2.0", + "micromatch": "^2.3.11", + "sane": "^2.0.0" } }, "jest-jasmine2": { @@ -3007,18 +3007,18 @@ "integrity": "sha512-pe2Ytgs1nyCs8IvsEJRiRTPC0eVYd8L/dXJGU08GFuBwZ4sYH/lmFDdOL3ZmvJR8QKqV9MFuwlsAi/EWkFUbsQ==", "dev": true, "requires": { - "babel-traverse": "6.26.0", - "chalk": "2.4.2", - "co": "4.6.0", - "expect": "23.6.0", - "is-generator-fn": "1.0.0", - "jest-diff": "23.6.0", - "jest-each": "23.6.0", - "jest-matcher-utils": "23.6.0", - "jest-message-util": "23.4.0", - "jest-snapshot": "23.6.0", - "jest-util": "23.4.0", - "pretty-format": "23.6.0" + "babel-traverse": "^6.0.0", + "chalk": "^2.0.1", + "co": "^4.6.0", + "expect": "^23.6.0", + "is-generator-fn": "^1.0.0", + "jest-diff": "^23.6.0", + "jest-each": "^23.6.0", + "jest-matcher-utils": "^23.6.0", + "jest-message-util": "^23.4.0", + "jest-snapshot": "^23.6.0", + "jest-util": "^23.4.0", + "pretty-format": "^23.6.0" } }, "jest-leak-detector": { @@ -3027,7 +3027,7 @@ "integrity": "sha512-f/8zA04rsl1Nzj10HIyEsXvYlMpMPcy0QkQilVZDFOaPbv2ur71X5u2+C4ZQJGyV/xvVXtCCZ3wQ99IgQxftCg==", "dev": true, "requires": { - "pretty-format": "23.6.0" + "pretty-format": "^23.6.0" } }, "jest-localstorage-mock": { @@ -3042,9 +3042,9 @@ "integrity": "sha512-rosyCHQfBcol4NsckTn01cdelzWLU9Cq7aaigDf8VwwpIRvWE/9zLgX2bON+FkEW69/0UuYslUe22SOdEf2nog==", "dev": true, "requires": { - "chalk": "2.4.2", - "jest-get-type": "22.4.3", - "pretty-format": "23.6.0" + "chalk": "^2.0.1", + "jest-get-type": "^22.1.0", + "pretty-format": "^23.6.0" } }, "jest-message-util": { @@ -3053,11 +3053,11 @@ "integrity": "sha1-F2EMUJQjSVCNAaPR4L2iwHkIap8=", "dev": true, "requires": { - "@babel/code-frame": "7.0.0", - "chalk": "2.4.2", - "micromatch": "2.3.11", - "slash": "1.0.0", - "stack-utils": "1.0.2" + "@babel/code-frame": "^7.0.0-beta.35", + "chalk": "^2.0.1", + "micromatch": "^2.3.11", + "slash": "^1.0.0", + "stack-utils": "^1.0.1" } }, "jest-mock": { @@ -3078,9 +3078,9 @@ "integrity": "sha512-XyoRxNtO7YGpQDmtQCmZjum1MljDqUCob7XlZ6jy9gsMugHdN2hY4+Acz9Qvjz2mSsOnPSH7skBmDYCHXVZqkA==", "dev": true, "requires": { - "browser-resolve": "1.11.3", - "chalk": "2.4.2", - "realpath-native": "1.1.0" + "browser-resolve": "^1.11.3", + "chalk": "^2.0.1", + "realpath-native": "^1.0.0" } }, "jest-resolve-dependencies": { @@ -3089,8 +3089,8 @@ "integrity": "sha512-EkQWkFWjGKwRtRyIwRwI6rtPAEyPWlUC2MpzHissYnzJeHcyCn1Hc8j7Nn1xUVrS5C6W5+ZL37XTem4D4pLZdA==", "dev": true, "requires": { - "jest-regex-util": "23.3.0", - "jest-snapshot": "23.6.0" + "jest-regex-util": "^23.3.0", + "jest-snapshot": "^23.6.0" } }, "jest-runner": { @@ -3099,19 +3099,19 @@ "integrity": "sha512-kw0+uj710dzSJKU6ygri851CObtCD9cN8aNkg8jWJf4ewFyEa6kwmiH/r/M1Ec5IL/6VFa0wnAk6w+gzUtjJzA==", "dev": true, "requires": { - "exit": "0.1.2", - "graceful-fs": "4.1.15", - "jest-config": "23.6.0", - "jest-docblock": "23.2.0", - "jest-haste-map": "23.6.0", - "jest-jasmine2": "23.6.0", - "jest-leak-detector": "23.6.0", - "jest-message-util": "23.4.0", - "jest-runtime": "23.6.0", - "jest-util": "23.4.0", - "jest-worker": "23.2.0", - "source-map-support": "0.5.10", - "throat": "4.1.0" + "exit": "^0.1.2", + "graceful-fs": "^4.1.11", + "jest-config": "^23.6.0", + "jest-docblock": "^23.2.0", + "jest-haste-map": "^23.6.0", + "jest-jasmine2": "^23.6.0", + "jest-leak-detector": "^23.6.0", + "jest-message-util": "^23.4.0", + "jest-runtime": "^23.6.0", + "jest-util": "^23.4.0", + "jest-worker": "^23.2.0", + "source-map-support": "^0.5.6", + "throat": "^4.0.0" }, "dependencies": { "source-map": { @@ -3126,8 +3126,8 @@ "integrity": "sha512-YfQ3tQFTK/yzlGJuX8pTwa4tifQj4QS2Mj7UegOu8jAz59MqIiMGPXxQhVQiIMNzayuUSF/jEuVnfFF5JqybmQ==", "dev": true, "requires": { - "buffer-from": "1.1.1", - "source-map": "0.6.1" + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" } } } @@ -3138,27 +3138,27 @@ "integrity": "sha512-ycnLTNPT2Gv+TRhnAYAQ0B3SryEXhhRj1kA6hBPSeZaNQkJ7GbZsxOLUkwg6YmvWGdX3BB3PYKFLDQCAE1zNOw==", "dev": true, "requires": { - "babel-core": "6.26.3", - "babel-plugin-istanbul": "4.1.6", - "chalk": "2.4.2", - "convert-source-map": "1.6.0", - "exit": "0.1.2", - "fast-json-stable-stringify": "2.0.0", - "graceful-fs": "4.1.15", - "jest-config": "23.6.0", - "jest-haste-map": "23.6.0", - "jest-message-util": "23.4.0", - "jest-regex-util": "23.3.0", - "jest-resolve": "23.6.0", - "jest-snapshot": "23.6.0", - "jest-util": "23.4.0", - "jest-validate": "23.6.0", - "micromatch": "2.3.11", - "realpath-native": "1.1.0", - "slash": "1.0.0", + "babel-core": "^6.0.0", + "babel-plugin-istanbul": "^4.1.6", + "chalk": "^2.0.1", + "convert-source-map": "^1.4.0", + "exit": "^0.1.2", + "fast-json-stable-stringify": "^2.0.0", + "graceful-fs": "^4.1.11", + "jest-config": "^23.6.0", + "jest-haste-map": "^23.6.0", + "jest-message-util": "^23.4.0", + "jest-regex-util": "^23.3.0", + "jest-resolve": "^23.6.0", + "jest-snapshot": "^23.6.0", + "jest-util": "^23.4.0", + "jest-validate": "^23.6.0", + "micromatch": "^2.3.11", + "realpath-native": "^1.0.0", + "slash": "^1.0.0", "strip-bom": "3.0.0", - "write-file-atomic": "2.4.2", - "yargs": "11.1.0" + "write-file-atomic": "^2.1.0", + "yargs": "^11.0.0" }, "dependencies": { "strip-bom": { @@ -3181,16 +3181,16 @@ "integrity": "sha512-tM7/Bprftun6Cvj2Awh/ikS7zV3pVwjRYU2qNYS51VZHgaAMBs5l4o/69AiDHhQrj5+LA2Lq4VIvK7zYk/bswg==", "dev": true, "requires": { - "babel-types": "6.26.0", - "chalk": "2.4.2", - "jest-diff": "23.6.0", - "jest-matcher-utils": "23.6.0", - "jest-message-util": "23.4.0", - "jest-resolve": "23.6.0", - "mkdirp": "0.5.1", - "natural-compare": "1.4.0", - "pretty-format": "23.6.0", - "semver": "5.6.0" + "babel-types": "^6.0.0", + "chalk": "^2.0.1", + "jest-diff": "^23.6.0", + "jest-matcher-utils": "^23.6.0", + "jest-message-util": "^23.4.0", + "jest-resolve": "^23.6.0", + "mkdirp": "^0.5.1", + "natural-compare": "^1.4.0", + "pretty-format": "^23.6.0", + "semver": "^5.5.0" } }, "jest-util": { @@ -3199,14 +3199,14 @@ "integrity": "sha1-TQY8uSe68KI4Mf9hvsLLv0l5NWE=", "dev": true, "requires": { - "callsites": "2.0.0", - "chalk": "2.4.2", - "graceful-fs": "4.1.15", - "is-ci": "1.2.1", - "jest-message-util": "23.4.0", - "mkdirp": "0.5.1", - "slash": "1.0.0", - "source-map": "0.6.1" + "callsites": "^2.0.0", + "chalk": "^2.0.1", + "graceful-fs": "^4.1.11", + "is-ci": "^1.0.10", + "jest-message-util": "^23.4.0", + "mkdirp": "^0.5.1", + "slash": "^1.0.0", + "source-map": "^0.6.0" }, "dependencies": { "source-map": { @@ -3223,10 +3223,10 @@ "integrity": "sha512-OFKapYxe72yz7agrDAWi8v2WL8GIfVqcbKRCLbRG9PAxtzF9b1SEDdTpytNDN12z2fJynoBwpMpvj2R39plI2A==", "dev": true, "requires": { - "chalk": "2.4.2", - "jest-get-type": "22.4.3", - "leven": "2.1.0", - "pretty-format": "23.6.0" + "chalk": "^2.0.1", + "jest-get-type": "^22.1.0", + "leven": "^2.1.0", + "pretty-format": "^23.6.0" } }, "jest-watcher": { @@ -3235,9 +3235,9 @@ "integrity": "sha1-0uKM50+NrWxq/JIrksq+9u0FyRw=", "dev": true, "requires": { - "ansi-escapes": "3.2.0", - "chalk": "2.4.2", - "string-length": "2.0.0" + "ansi-escapes": "^3.0.0", + "chalk": "^2.0.1", + "string-length": "^2.0.0" } }, "jest-worker": { @@ -3246,7 +3246,7 @@ "integrity": "sha1-+vcGqNo2+uYOsmlXJX+ntdjqArk=", "dev": true, "requires": { - "merge-stream": "1.0.1" + "merge-stream": "^1.0.1" } }, "js-tokens": { @@ -3261,8 +3261,8 @@ "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", "dev": true, "requires": { - "argparse": "1.0.10", - "esprima": "4.0.1" + "argparse": "^1.0.7", + "esprima": "^4.0.0" } }, "jsbn": { @@ -3277,32 +3277,32 @@ "integrity": "sha512-y8Px43oyiBM13Zc1z780FrfNLJCXTL40EWlty/LXUtcjykRBNgLlCjWXpfSPBl2iv+N7koQN+dvqszHZgT/Fjw==", "dev": true, "requires": { - "abab": "2.0.0", - "acorn": "5.7.3", - "acorn-globals": "4.3.0", - "array-equal": "1.0.0", - "cssom": "0.3.6", - "cssstyle": "1.2.1", - "data-urls": "1.1.0", - "domexception": "1.0.1", - "escodegen": "1.11.1", - "html-encoding-sniffer": "1.0.2", - "left-pad": "1.3.0", - "nwsapi": "2.1.1", + "abab": "^2.0.0", + "acorn": "^5.5.3", + "acorn-globals": "^4.1.0", + "array-equal": "^1.0.0", + "cssom": ">= 0.3.2 < 0.4.0", + "cssstyle": "^1.0.0", + "data-urls": "^1.0.0", + "domexception": "^1.0.1", + "escodegen": "^1.9.1", + "html-encoding-sniffer": "^1.0.2", + "left-pad": "^1.3.0", + "nwsapi": "^2.0.7", "parse5": "4.0.0", - "pn": "1.1.0", - "request": "2.88.0", - "request-promise-native": "1.0.7", - "sax": "1.2.4", - "symbol-tree": "3.2.2", - "tough-cookie": "2.5.0", - "w3c-hr-time": "1.0.1", - "webidl-conversions": "4.0.2", - "whatwg-encoding": "1.0.5", - "whatwg-mimetype": "2.3.0", - "whatwg-url": "6.5.0", - "ws": "5.2.2", - "xml-name-validator": "3.0.0" + "pn": "^1.1.0", + "request": "^2.87.0", + "request-promise-native": "^1.0.5", + "sax": "^1.2.4", + "symbol-tree": "^3.2.2", + "tough-cookie": "^2.3.4", + "w3c-hr-time": "^1.0.1", + "webidl-conversions": "^4.0.2", + "whatwg-encoding": "^1.0.3", + "whatwg-mimetype": "^2.1.0", + "whatwg-url": "^6.4.1", + "ws": "^5.2.0", + "xml-name-validator": "^3.0.0" } }, "jsesc": { @@ -3353,7 +3353,7 @@ "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", "dev": true, "requires": { - "is-buffer": "1.1.6" + "is-buffer": "^1.1.5" } }, "kleur": { @@ -3368,7 +3368,7 @@ "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", "dev": true, "requires": { - "invert-kv": "1.0.0" + "invert-kv": "^1.0.0" } }, "left-pad": { @@ -3389,8 +3389,8 @@ "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", "dev": true, "requires": { - "prelude-ls": "1.1.2", - "type-check": "0.3.2" + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" } }, "load-json-file": { @@ -3399,11 +3399,11 @@ "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", "dev": true, "requires": { - "graceful-fs": "4.1.15", - "parse-json": "2.2.0", - "pify": "2.3.0", - "pinkie-promise": "2.0.1", - "strip-bom": "2.0.0" + "graceful-fs": "^4.1.2", + "parse-json": "^2.2.0", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0", + "strip-bom": "^2.0.0" } }, "locate-path": { @@ -3412,8 +3412,8 @@ "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", "dev": true, "requires": { - "p-locate": "2.0.0", - "path-exists": "3.0.0" + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" } }, "lodash": { @@ -3434,7 +3434,7 @@ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", "dev": true, "requires": { - "js-tokens": "3.0.2" + "js-tokens": "^3.0.0 || ^4.0.0" } }, "lru-cache": { @@ -3443,8 +3443,8 @@ "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", "dev": true, "requires": { - "pseudomap": "1.0.2", - "yallist": "2.1.2" + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" } }, "make-error": { @@ -3459,7 +3459,7 @@ "integrity": "sha1-4BpckQnyr3lmDk6LlYd5AYT1qWw=", "dev": true, "requires": { - "tmpl": "1.0.4" + "tmpl": "1.0.x" } }, "map-cache": { @@ -3474,7 +3474,7 @@ "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", "dev": true, "requires": { - "object-visit": "1.0.1" + "object-visit": "^1.0.0" } }, "math-random": { @@ -3489,7 +3489,7 @@ "integrity": "sha1-Xt1StIXKHZAP5kiVUFOZoN+kX3Y=", "dev": true, "requires": { - "mimic-fn": "1.2.0" + "mimic-fn": "^1.0.0" } }, "merge": { @@ -3504,7 +3504,7 @@ "integrity": "sha1-QEEgLVCKNCugAXQAjfDCUbjBNeE=", "dev": true, "requires": { - "readable-stream": "2.3.6" + "readable-stream": "^2.0.1" } }, "micromatch": { @@ -3513,19 +3513,19 @@ "integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=", "dev": true, "requires": { - "arr-diff": "2.0.0", - "array-unique": "0.2.1", - "braces": "1.8.5", - "expand-brackets": "0.1.5", - "extglob": "0.3.2", - "filename-regex": "2.0.1", - "is-extglob": "1.0.0", - "is-glob": "2.0.1", - "kind-of": "3.2.2", - "normalize-path": "2.1.1", - "object.omit": "2.0.1", - "parse-glob": "3.0.4", - "regex-cache": "0.4.4" + "arr-diff": "^2.0.0", + "array-unique": "^0.2.1", + "braces": "^1.8.2", + "expand-brackets": "^0.1.4", + "extglob": "^0.3.1", + "filename-regex": "^2.0.0", + "is-extglob": "^1.0.0", + "is-glob": "^2.0.1", + "kind-of": "^3.0.2", + "normalize-path": "^2.0.1", + "object.omit": "^2.0.0", + "parse-glob": "^3.0.4", + "regex-cache": "^0.4.2" } }, "mime-db": { @@ -3540,7 +3540,7 @@ "integrity": "sha512-aGl6TZGnhm/li6F7yx82bJiBZwgiEa4Hf6CNr8YO+r5UHr53tSTYZb102zyU50DOWWKeOv0uQLRL0/9EiKWCog==", "dev": true, "requires": { - "mime-db": "1.38.0" + "mime-db": "~1.38.0" } }, "mimic-fn": { @@ -3555,7 +3555,7 @@ "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "dev": true, "requires": { - "brace-expansion": "1.1.11" + "brace-expansion": "^1.1.7" } }, "minimist": { @@ -3570,8 +3570,8 @@ "integrity": "sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ==", "dev": true, "requires": { - "for-in": "1.0.2", - "is-extendable": "1.0.1" + "for-in": "^1.0.2", + "is-extendable": "^1.0.1" }, "dependencies": { "is-extendable": { @@ -3580,7 +3580,7 @@ "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", "dev": true, "requires": { - "is-plain-object": "2.0.4" + "is-plain-object": "^2.0.4" } } } @@ -3613,17 +3613,17 @@ "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.2", - "snapdragon": "0.8.2", - "to-regex": "3.0.2" + "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" }, "dependencies": { "arr-diff": { @@ -3670,11 +3670,11 @@ "integrity": "sha512-SUDEb+o71XR5lXSTyivXd9J7fCloE3SyP4lSgt3lU2oSANiox+SxlNRGPjDKrwU1YN3ix2KN/VGGCg0t01rttQ==", "dev": true, "requires": { - "growly": "1.3.0", - "is-wsl": "1.1.0", - "semver": "5.6.0", - "shellwords": "0.1.1", - "which": "1.3.1" + "growly": "^1.3.0", + "is-wsl": "^1.1.0", + "semver": "^5.5.0", + "shellwords": "^0.1.1", + "which": "^1.3.0" } }, "normalize-package-data": { @@ -3683,10 +3683,10 @@ "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", "dev": true, "requires": { - "hosted-git-info": "2.7.1", - "resolve": "1.10.0", - "semver": "5.6.0", - "validate-npm-package-license": "3.0.4" + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" } }, "normalize-path": { @@ -3695,7 +3695,7 @@ "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", "dev": true, "requires": { - "remove-trailing-separator": "1.1.0" + "remove-trailing-separator": "^1.0.1" } }, "npm-run-path": { @@ -3704,7 +3704,7 @@ "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", "dev": true, "requires": { - "path-key": "2.0.1" + "path-key": "^2.0.0" } }, "number-is-nan": { @@ -3737,9 +3737,9 @@ "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", "dev": true, "requires": { - "copy-descriptor": "0.1.1", - "define-property": "0.2.5", - "kind-of": "3.2.2" + "copy-descriptor": "^0.1.0", + "define-property": "^0.2.5", + "kind-of": "^3.0.3" }, "dependencies": { "define-property": { @@ -3748,7 +3748,7 @@ "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", "dev": true, "requires": { - "is-descriptor": "0.1.6" + "is-descriptor": "^0.1.0" } } } @@ -3765,7 +3765,7 @@ "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", "dev": true, "requires": { - "isobject": "3.0.1" + "isobject": "^3.0.0" }, "dependencies": { "isobject": { @@ -3782,8 +3782,8 @@ "integrity": "sha1-h1jIRvW0B62rDyNuCYbxSwUcqhY=", "dev": true, "requires": { - "define-properties": "1.1.3", - "es-abstract": "1.13.0" + "define-properties": "^1.1.2", + "es-abstract": "^1.5.1" } }, "object.omit": { @@ -3792,8 +3792,8 @@ "integrity": "sha1-Gpx0SCnznbuFjHbKNXmuKlTr0fo=", "dev": true, "requires": { - "for-own": "0.1.5", - "is-extendable": "0.1.1" + "for-own": "^0.1.4", + "is-extendable": "^0.1.1" } }, "object.pick": { @@ -3802,7 +3802,7 @@ "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", "dev": true, "requires": { - "isobject": "3.0.1" + "isobject": "^3.0.1" }, "dependencies": { "isobject": { @@ -3819,7 +3819,7 @@ "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "dev": true, "requires": { - "wrappy": "1.0.2" + "wrappy": "1" } }, "optimist": { @@ -3828,8 +3828,8 @@ "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", "dev": true, "requires": { - "minimist": "0.0.8", - "wordwrap": "0.0.3" + "minimist": "~0.0.1", + "wordwrap": "~0.0.2" } }, "optionator": { @@ -3838,12 +3838,12 @@ "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", "dev": true, "requires": { - "deep-is": "0.1.3", - "fast-levenshtein": "2.0.6", - "levn": "0.3.0", - "prelude-ls": "1.1.2", - "type-check": "0.3.2", - "wordwrap": "1.0.0" + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.4", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "wordwrap": "~1.0.0" }, "dependencies": { "wordwrap": { @@ -3866,9 +3866,9 @@ "integrity": "sha512-3sslG3zJbEYcaC4YVAvDorjGxc7tv6KVATnLPZONiljsUncvihe9BQoVCEs0RZ1kmf4Hk9OBqlZfJZWI4GanKA==", "dev": true, "requires": { - "execa": "0.7.0", - "lcid": "1.0.0", - "mem": "1.1.0" + "execa": "^0.7.0", + "lcid": "^1.0.0", + "mem": "^1.1.0" } }, "os-tmpdir": { @@ -3889,7 +3889,7 @@ "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", "dev": true, "requires": { - "p-try": "1.0.0" + "p-try": "^1.0.0" } }, "p-locate": { @@ -3898,7 +3898,7 @@ "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", "dev": true, "requires": { - "p-limit": "1.3.0" + "p-limit": "^1.1.0" } }, "p-try": { @@ -3913,10 +3913,10 @@ "integrity": "sha1-ssN2z7EfNVE7rdFz7wu246OIORw=", "dev": true, "requires": { - "glob-base": "0.3.0", - "is-dotfile": "1.0.3", - "is-extglob": "1.0.0", - "is-glob": "2.0.1" + "glob-base": "^0.3.0", + "is-dotfile": "^1.0.0", + "is-extglob": "^1.0.0", + "is-glob": "^2.0.0" } }, "parse-json": { @@ -3925,7 +3925,7 @@ "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", "dev": true, "requires": { - "error-ex": "1.3.2" + "error-ex": "^1.2.0" } }, "parse5": { @@ -3970,9 +3970,9 @@ "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", "dev": true, "requires": { - "graceful-fs": "4.1.15", - "pify": "2.3.0", - "pinkie-promise": "2.0.1" + "graceful-fs": "^4.1.2", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" } }, "performance-now": { @@ -3999,7 +3999,7 @@ "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", "dev": true, "requires": { - "pinkie": "2.0.4" + "pinkie": "^2.0.0" } }, "pkg-dir": { @@ -4008,7 +4008,7 @@ "integrity": "sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=", "dev": true, "requires": { - "find-up": "2.1.0" + "find-up": "^2.1.0" } }, "pn": { @@ -4041,8 +4041,8 @@ "integrity": "sha512-zf9NV1NSlDLDjycnwm6hpFATCGl/K1lt0R/GdkAK2O5LN/rwJoB+Mh93gGJjut4YbmecbfgLWVGSTCr0Ewvvbw==", "dev": true, "requires": { - "ansi-regex": "3.0.0", - "ansi-styles": "3.2.1" + "ansi-regex": "^3.0.0", + "ansi-styles": "^3.2.0" }, "dependencies": { "ansi-regex": { @@ -4071,8 +4071,8 @@ "integrity": "sha512-rxkyiE9YH6zAz/rZpywySLKkpaj0NMVyNw1qhsubdbjjSgcayjTShDreZGlFMcGSu5sab3bAKPfFk78PB90+8w==", "dev": true, "requires": { - "kleur": "2.0.2", - "sisteransi": "0.1.1" + "kleur": "^2.0.1", + "sisteransi": "^0.1.1" } }, "pseudomap": { @@ -4105,9 +4105,9 @@ "integrity": "sha512-TuDE5KxZ0J461RVjrJZCJc+J+zCkTb1MbH9AQUq68sMhOMcy9jLcb3BrZKgp9q9Ncltdg4QVqWrH02W2EFFVYw==", "dev": true, "requires": { - "is-number": "4.0.0", - "kind-of": "6.0.2", - "math-random": "1.0.4" + "is-number": "^4.0.0", + "kind-of": "^6.0.0", + "math-random": "^1.0.1" }, "dependencies": { "is-number": { @@ -4130,9 +4130,9 @@ "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", "dev": true, "requires": { - "load-json-file": "1.1.0", - "normalize-package-data": "2.5.0", - "path-type": "1.1.0" + "load-json-file": "^1.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^1.0.0" } }, "read-pkg-up": { @@ -4141,8 +4141,8 @@ "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", "dev": true, "requires": { - "find-up": "1.1.2", - "read-pkg": "1.1.0" + "find-up": "^1.0.0", + "read-pkg": "^1.0.0" }, "dependencies": { "find-up": { @@ -4151,8 +4151,8 @@ "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", "dev": true, "requires": { - "path-exists": "2.1.0", - "pinkie-promise": "2.0.1" + "path-exists": "^2.0.0", + "pinkie-promise": "^2.0.0" } }, "path-exists": { @@ -4161,7 +4161,7 @@ "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", "dev": true, "requires": { - "pinkie-promise": "2.0.1" + "pinkie-promise": "^2.0.0" } } } @@ -4172,13 +4172,13 @@ "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "dev": true, "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.3", - "isarray": "1.0.0", - "process-nextick-args": "2.0.0", - "safe-buffer": "5.1.2", - "string_decoder": "1.1.1", - "util-deprecate": "1.0.2" + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" } }, "realpath-native": { @@ -4187,7 +4187,7 @@ "integrity": "sha512-wlgPA6cCIIg9gKz0fgAPjnzh4yR/LnXovwuo9hvyGvx3h8nX4+/iLZplfUWasXpqD8BdnGnP5njOFjkUwPzvjA==", "dev": true, "requires": { - "util.promisify": "1.0.0" + "util.promisify": "^1.0.0" } }, "regenerator-runtime": { @@ -4202,7 +4202,7 @@ "integrity": "sha512-nVIZwtCjkC9YgvWkpM55B5rBhBYRZhAaJbgcFYXXsHnbZ9UZI9nnVWYZpBlCqv9ho2eZryPnWrZGsOdPwVWXWQ==", "dev": true, "requires": { - "is-equal-shallow": "0.1.3" + "is-equal-shallow": "^0.1.3" } }, "regex-not": { @@ -4211,8 +4211,8 @@ "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", "dev": true, "requires": { - "extend-shallow": "3.0.2", - "safe-regex": "1.1.0" + "extend-shallow": "^3.0.2", + "safe-regex": "^1.1.0" } }, "remove-trailing-separator": { @@ -4239,7 +4239,7 @@ "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", "dev": true, "requires": { - "is-finite": "1.0.2" + "is-finite": "^1.0.0" } }, "request": { @@ -4248,26 +4248,26 @@ "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", "dev": true, "requires": { - "aws-sign2": "0.7.0", - "aws4": "1.8.0", - "caseless": "0.12.0", - "combined-stream": "1.0.7", - "extend": "3.0.2", - "forever-agent": "0.6.1", - "form-data": "2.3.3", - "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.22", - "oauth-sign": "0.9.0", - "performance-now": "2.1.0", - "qs": "6.5.2", - "safe-buffer": "5.1.2", - "tough-cookie": "2.4.3", - "tunnel-agent": "0.6.0", - "uuid": "3.3.2" + "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.0", + "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.4.3", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" }, "dependencies": { "punycode": { @@ -4282,8 +4282,8 @@ "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", "dev": true, "requires": { - "psl": "1.1.31", - "punycode": "1.4.1" + "psl": "^1.1.24", + "punycode": "^1.4.1" } } } @@ -4294,7 +4294,7 @@ "integrity": "sha512-UHYyq1MO8GsefGEt7EprS8UrXsm1TxEvFUX1IMTuSLU2Rh7fTIdFtl8xD7JiEYiWU2dl+NYAjCTksTehQUxPag==", "dev": true, "requires": { - "lodash": "4.17.15" + "lodash": "^4.17.11" } }, "request-promise-native": { @@ -4304,8 +4304,8 @@ "dev": true, "requires": { "request-promise-core": "1.1.2", - "stealthy-require": "1.1.1", - "tough-cookie": "2.5.0" + "stealthy-require": "^1.1.1", + "tough-cookie": "^2.3.3" } }, "require-directory": { @@ -4326,7 +4326,7 @@ "integrity": "sha512-3sUr9aq5OfSg2S9pNtPA9hL1FVEAjvfOC4leW0SNf/mpnaakz2a9femSd6LqAww2RaFctwyf1lCqnTHuF1rxDg==", "dev": true, "requires": { - "path-parse": "1.0.6" + "path-parse": "^1.0.6" } }, "resolve-cwd": { @@ -4335,7 +4335,7 @@ "integrity": "sha1-AKn3OHVW4nA46uIyyqNypqWbZlo=", "dev": true, "requires": { - "resolve-from": "3.0.0" + "resolve-from": "^3.0.0" } }, "resolve-from": { @@ -4362,7 +4362,7 @@ "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", "dev": true, "requires": { - "glob": "7.1.3" + "glob": "^7.1.3" } }, "rsvp": { @@ -4383,7 +4383,7 @@ "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", "dev": true, "requires": { - "ret": "0.1.15" + "ret": "~0.1.10" } }, "safer-buffer": { @@ -4398,15 +4398,15 @@ "integrity": "sha1-tNwYYcIbQn6SlQej51HiosuKs/o=", "dev": true, "requires": { - "anymatch": "2.0.0", - "capture-exit": "1.2.0", - "exec-sh": "0.2.2", - "fb-watchman": "2.0.0", - "fsevents": "1.2.7", - "micromatch": "3.1.10", - "minimist": "1.2.0", - "walker": "1.0.7", - "watch": "0.18.0" + "anymatch": "^2.0.0", + "capture-exit": "^1.2.0", + "exec-sh": "^0.2.0", + "fb-watchman": "^2.0.0", + "fsevents": "^1.2.3", + "micromatch": "^3.1.4", + "minimist": "^1.1.1", + "walker": "~1.0.5", + "watch": "~0.18.0" }, "dependencies": { "arr-diff": { @@ -4427,16 +4427,16 @@ "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.3", - "snapdragon": "0.8.2", - "snapdragon-node": "2.1.1", - "split-string": "3.1.0", - "to-regex": "3.0.2" + "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": { @@ -4445,7 +4445,7 @@ "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", "dev": true, "requires": { - "is-extendable": "0.1.1" + "is-extendable": "^0.1.0" } } } @@ -4456,13 +4456,13 @@ "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", "dev": true, "requires": { - "debug": "2.6.9", - "define-property": "0.2.5", - "extend-shallow": "2.0.1", - "posix-character-classes": "0.1.1", - "regex-not": "1.0.2", - "snapdragon": "0.8.2", - "to-regex": "3.0.2" + "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": { "define-property": { @@ -4471,7 +4471,7 @@ "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", "dev": true, "requires": { - "is-descriptor": "0.1.6" + "is-descriptor": "^0.1.0" } }, "extend-shallow": { @@ -4480,7 +4480,7 @@ "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", "dev": true, "requires": { - "is-extendable": "0.1.1" + "is-extendable": "^0.1.0" } }, "is-accessor-descriptor": { @@ -4489,7 +4489,7 @@ "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", "dev": true, "requires": { - "kind-of": "3.2.2" + "kind-of": "^3.0.2" }, "dependencies": { "kind-of": { @@ -4498,7 +4498,7 @@ "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", "dev": true, "requires": { - "is-buffer": "1.1.6" + "is-buffer": "^1.1.5" } } } @@ -4509,7 +4509,7 @@ "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", "dev": true, "requires": { - "kind-of": "3.2.2" + "kind-of": "^3.0.2" }, "dependencies": { "kind-of": { @@ -4518,7 +4518,7 @@ "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", "dev": true, "requires": { - "is-buffer": "1.1.6" + "is-buffer": "^1.1.5" } } } @@ -4529,9 +4529,9 @@ "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", "dev": true, "requires": { - "is-accessor-descriptor": "0.1.6", - "is-data-descriptor": "0.1.4", - "kind-of": "5.1.0" + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" } }, "kind-of": { @@ -4548,14 +4548,14 @@ "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.2", - "snapdragon": "0.8.2", - "to-regex": "3.0.2" + "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": { @@ -4564,7 +4564,7 @@ "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", "dev": true, "requires": { - "is-descriptor": "1.0.2" + "is-descriptor": "^1.0.0" } }, "extend-shallow": { @@ -4573,7 +4573,7 @@ "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", "dev": true, "requires": { - "is-extendable": "0.1.1" + "is-extendable": "^0.1.0" } } } @@ -4584,10 +4584,10 @@ "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.1" + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" }, "dependencies": { "extend-shallow": { @@ -4596,7 +4596,7 @@ "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", "dev": true, "requires": { - "is-extendable": "0.1.1" + "is-extendable": "^0.1.0" } } } @@ -4607,7 +4607,7 @@ "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", "dev": true, "requires": { - "kind-of": "6.0.2" + "kind-of": "^6.0.0" } }, "is-data-descriptor": { @@ -4616,7 +4616,7 @@ "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", "dev": true, "requires": { - "kind-of": "6.0.2" + "kind-of": "^6.0.0" } }, "is-descriptor": { @@ -4625,9 +4625,9 @@ "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-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" } }, "is-number": { @@ -4636,7 +4636,7 @@ "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", "dev": true, "requires": { - "kind-of": "3.2.2" + "kind-of": "^3.0.2" }, "dependencies": { "kind-of": { @@ -4645,7 +4645,7 @@ "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", "dev": true, "requires": { - "is-buffer": "1.1.6" + "is-buffer": "^1.1.5" } } } @@ -4668,19 +4668,19 @@ "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", "dev": true, "requires": { - "arr-diff": "4.0.0", - "array-unique": "0.3.2", - "braces": "2.3.2", - "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.13", - "object.pick": "1.3.0", - "regex-not": "1.0.2", - "snapdragon": "0.8.2", - "to-regex": "3.0.2" + "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" } }, "minimist": { @@ -4715,10 +4715,10 @@ "integrity": "sha512-hw0yxk9GT/Hr5yJEYnHNKYXkIA8mVJgd9ditYZCe16ZczcaELYYcfvaXesNACk2O8O0nTiPQcQhGUQj8JLzeeg==", "dev": true, "requires": { - "extend-shallow": "2.0.1", - "is-extendable": "0.1.1", - "is-plain-object": "2.0.4", - "split-string": "3.1.0" + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.3", + "split-string": "^3.0.1" }, "dependencies": { "extend-shallow": { @@ -4727,7 +4727,7 @@ "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", "dev": true, "requires": { - "is-extendable": "0.1.1" + "is-extendable": "^0.1.0" } } } @@ -4738,7 +4738,7 @@ "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", "dev": true, "requires": { - "shebang-regex": "1.0.0" + "shebang-regex": "^1.0.0" } }, "shebang-regex": { @@ -4777,14 +4777,14 @@ "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", "dev": true, "requires": { - "base": "0.11.2", - "debug": "2.6.9", - "define-property": "0.2.5", - "extend-shallow": "2.0.1", - "map-cache": "0.2.2", - "source-map": "0.5.7", - "source-map-resolve": "0.5.2", - "use": "3.1.1" + "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" }, "dependencies": { "define-property": { @@ -4793,7 +4793,7 @@ "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", "dev": true, "requires": { - "is-descriptor": "0.1.6" + "is-descriptor": "^0.1.0" } }, "extend-shallow": { @@ -4802,7 +4802,7 @@ "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", "dev": true, "requires": { - "is-extendable": "0.1.1" + "is-extendable": "^0.1.0" } } } @@ -4813,9 +4813,9 @@ "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", "dev": true, "requires": { - "define-property": "1.0.0", - "isobject": "3.0.1", - "snapdragon-util": "3.0.1" + "define-property": "^1.0.0", + "isobject": "^3.0.0", + "snapdragon-util": "^3.0.1" }, "dependencies": { "define-property": { @@ -4824,7 +4824,7 @@ "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", "dev": true, "requires": { - "is-descriptor": "1.0.2" + "is-descriptor": "^1.0.0" } }, "is-accessor-descriptor": { @@ -4833,7 +4833,7 @@ "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", "dev": true, "requires": { - "kind-of": "6.0.2" + "kind-of": "^6.0.0" } }, "is-data-descriptor": { @@ -4842,7 +4842,7 @@ "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", "dev": true, "requires": { - "kind-of": "6.0.2" + "kind-of": "^6.0.0" } }, "is-descriptor": { @@ -4851,9 +4851,9 @@ "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-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" } }, "isobject": { @@ -4876,7 +4876,7 @@ "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", "dev": true, "requires": { - "kind-of": "3.2.2" + "kind-of": "^3.2.0" } }, "source-map": { @@ -4891,11 +4891,11 @@ "integrity": "sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA==", "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" + "atob": "^2.1.1", + "decode-uri-component": "^0.2.0", + "resolve-url": "^0.2.1", + "source-map-url": "^0.4.0", + "urix": "^0.1.0" } }, "source-map-support": { @@ -4904,7 +4904,7 @@ "integrity": "sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA==", "dev": true, "requires": { - "source-map": "0.5.7" + "source-map": "^0.5.6" } }, "source-map-url": { @@ -4919,8 +4919,8 @@ "integrity": "sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q==", "dev": true, "requires": { - "spdx-expression-parse": "3.0.0", - "spdx-license-ids": "3.0.3" + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" } }, "spdx-exceptions": { @@ -4935,8 +4935,8 @@ "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", "dev": true, "requires": { - "spdx-exceptions": "2.2.0", - "spdx-license-ids": "3.0.3" + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" } }, "spdx-license-ids": { @@ -4951,7 +4951,7 @@ "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", "dev": true, "requires": { - "extend-shallow": "3.0.2" + "extend-shallow": "^3.0.0" } }, "sprintf-js": { @@ -4966,15 +4966,15 @@ "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", "dev": true, "requires": { - "asn1": "0.2.4", - "assert-plus": "1.0.0", - "bcrypt-pbkdf": "1.0.2", - "dashdash": "1.14.1", - "ecc-jsbn": "0.1.2", - "getpass": "0.1.7", - "jsbn": "0.1.1", - "safer-buffer": "2.1.2", - "tweetnacl": "0.14.5" + "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": { @@ -4989,8 +4989,8 @@ "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", "dev": true, "requires": { - "define-property": "0.2.5", - "object-copy": "0.1.0" + "define-property": "^0.2.5", + "object-copy": "^0.1.0" }, "dependencies": { "define-property": { @@ -4999,7 +4999,7 @@ "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", "dev": true, "requires": { - "is-descriptor": "0.1.6" + "is-descriptor": "^0.1.0" } } } @@ -5016,8 +5016,8 @@ "integrity": "sha1-1A27aGo6zpYMHP/KVivyxF+DY+0=", "dev": true, "requires": { - "astral-regex": "1.0.0", - "strip-ansi": "4.0.0" + "astral-regex": "^1.0.0", + "strip-ansi": "^4.0.0" } }, "string-width": { @@ -5026,8 +5026,8 @@ "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", "dev": true, "requires": { - "is-fullwidth-code-point": "2.0.0", - "strip-ansi": "4.0.0" + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" } }, "string_decoder": { @@ -5036,7 +5036,7 @@ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, "requires": { - "safe-buffer": "5.1.2" + "safe-buffer": "~5.1.0" } }, "strip-ansi": { @@ -5045,7 +5045,7 @@ "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", "dev": true, "requires": { - "ansi-regex": "3.0.0" + "ansi-regex": "^3.0.0" }, "dependencies": { "ansi-regex": { @@ -5062,7 +5062,7 @@ "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", "dev": true, "requires": { - "is-utf8": "0.2.1" + "is-utf8": "^0.2.0" } }, "strip-eof": { @@ -5077,7 +5077,7 @@ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "dev": true, "requires": { - "has-flag": "3.0.0" + "has-flag": "^3.0.0" } }, "symbol-tree": { @@ -5092,11 +5092,11 @@ "integrity": "sha512-SYbXgY64PT+4GAL2ocI3HwPa4Q4TBKm0cwAVeKOt/Aoc0gSpNRjJX8w0pA1LMKZ3LBmd8pYBqApFNQLII9kavA==", "dev": true, "requires": { - "arrify": "1.0.1", - "micromatch": "2.3.11", - "object-assign": "4.1.1", - "read-pkg-up": "1.0.1", - "require-main-filename": "1.0.1" + "arrify": "^1.0.1", + "micromatch": "^2.3.11", + "object-assign": "^4.1.0", + "read-pkg-up": "^1.0.1", + "require-main-filename": "^1.0.1" } }, "throat": { @@ -5123,7 +5123,7 @@ "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", "dev": true, "requires": { - "kind-of": "3.2.2" + "kind-of": "^3.0.2" } }, "to-regex": { @@ -5132,10 +5132,10 @@ "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" + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "regex-not": "^1.0.2", + "safe-regex": "^1.1.0" } }, "to-regex-range": { @@ -5144,8 +5144,8 @@ "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", "dev": true, "requires": { - "is-number": "3.0.0", - "repeat-string": "1.6.1" + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" }, "dependencies": { "is-number": { @@ -5154,7 +5154,7 @@ "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", "dev": true, "requires": { - "kind-of": "3.2.2" + "kind-of": "^3.0.2" } } } @@ -5165,8 +5165,8 @@ "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", "dev": true, "requires": { - "psl": "1.1.31", - "punycode": "2.1.1" + "psl": "^1.1.28", + "punycode": "^2.1.1" } }, "tr46": { @@ -5175,7 +5175,7 @@ "integrity": "sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk=", "dev": true, "requires": { - "punycode": "2.1.1" + "punycode": "^2.1.0" } }, "trim-right": { @@ -5190,15 +5190,15 @@ "integrity": "sha512-MRCs9qnGoyKgFc8adDEntAOP64fWK1vZKnOYU1o2HxaqjdJvGqmkLCPCnVq1/If4zkUmEjKPnCiUisTrlX2p2A==", "dev": true, "requires": { - "bs-logger": "0.2.6", - "buffer-from": "1.1.1", - "fast-json-stable-stringify": "2.0.0", - "json5": "2.1.0", - "make-error": "1.3.5", - "mkdirp": "0.5.1", - "resolve": "1.10.0", - "semver": "5.6.0", - "yargs-parser": "10.1.0" + "bs-logger": "0.x", + "buffer-from": "1.x", + "fast-json-stable-stringify": "2.x", + "json5": "2.x", + "make-error": "1.x", + "mkdirp": "0.x", + "resolve": "1.x", + "semver": "^5.5", + "yargs-parser": "10.x" }, "dependencies": { "json5": { @@ -5207,7 +5207,7 @@ "integrity": "sha512-8Mh9h6xViijj36g7Dxi+Y4S6hNGV96vcJZr/SrlHh1LR/pEn/8j/+qIBbs44YKl69Lrfctp4QD+AdWLTMqEZAQ==", "dev": true, "requires": { - "minimist": "1.2.0" + "minimist": "^1.2.0" } }, "minimist": { @@ -5222,7 +5222,7 @@ "integrity": "sha512-VCIyR1wJoEBZUqk5PA+oOBF6ypbwh5aNB3I50guxAL/quggdfs4TtNHQrSazFA3fYZ+tEqfs0zIGlv0c/rgjbQ==", "dev": true, "requires": { - "camelcase": "4.1.0" + "camelcase": "^4.1.0" } } } @@ -5233,7 +5233,7 @@ "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", "dev": true, "requires": { - "safe-buffer": "5.1.2" + "safe-buffer": "^5.0.1" } }, "tweetnacl": { @@ -5248,7 +5248,7 @@ "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", "dev": true, "requires": { - "prelude-ls": "1.1.2" + "prelude-ls": "~1.1.2" } }, "typescript": { @@ -5264,8 +5264,8 @@ "dev": true, "optional": true, "requires": { - "commander": "2.17.1", - "source-map": "0.6.1" + "commander": "~2.17.1", + "source-map": "~0.6.1" }, "dependencies": { "source-map": { @@ -5283,10 +5283,10 @@ "integrity": "sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ=", "dev": true, "requires": { - "arr-union": "3.1.0", - "get-value": "2.0.6", - "is-extendable": "0.1.1", - "set-value": "0.4.3" + "arr-union": "^3.1.0", + "get-value": "^2.0.6", + "is-extendable": "^0.1.1", + "set-value": "^0.4.3" }, "dependencies": { "extend-shallow": { @@ -5295,7 +5295,7 @@ "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", "dev": true, "requires": { - "is-extendable": "0.1.1" + "is-extendable": "^0.1.0" } }, "set-value": { @@ -5304,10 +5304,10 @@ "integrity": "sha1-fbCPnT0i3H945Trzw79GZuzfzPE=", "dev": true, "requires": { - "extend-shallow": "2.0.1", - "is-extendable": "0.1.1", - "is-plain-object": "2.0.4", - "to-object-path": "0.3.0" + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.1", + "to-object-path": "^0.3.0" } } } @@ -5318,8 +5318,8 @@ "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", "dev": true, "requires": { - "has-value": "0.3.1", - "isobject": "3.0.1" + "has-value": "^0.3.1", + "isobject": "^3.0.0" }, "dependencies": { "has-value": { @@ -5328,9 +5328,9 @@ "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", "dev": true, "requires": { - "get-value": "2.0.6", - "has-values": "0.1.4", - "isobject": "2.1.0" + "get-value": "^2.0.3", + "has-values": "^0.1.4", + "isobject": "^2.0.0" }, "dependencies": { "isobject": { @@ -5364,7 +5364,7 @@ "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", "dev": true, "requires": { - "punycode": "2.1.1" + "punycode": "^2.1.0" } }, "urix": { @@ -5391,8 +5391,8 @@ "integrity": "sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA==", "dev": true, "requires": { - "define-properties": "1.1.3", - "object.getownpropertydescriptors": "2.0.3" + "define-properties": "^1.1.2", + "object.getownpropertydescriptors": "^2.0.3" } }, "uuid": { @@ -5406,8 +5406,8 @@ "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", "dev": true, "requires": { - "spdx-correct": "3.1.0", - "spdx-expression-parse": "3.0.0" + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" } }, "verror": { @@ -5416,9 +5416,9 @@ "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", "dev": true, "requires": { - "assert-plus": "1.0.0", + "assert-plus": "^1.0.0", "core-util-is": "1.0.2", - "extsprintf": "1.3.0" + "extsprintf": "^1.2.0" } }, "w3c-hr-time": { @@ -5427,7 +5427,7 @@ "integrity": "sha1-gqwr/2PZUOqeMYmlimViX+3xkEU=", "dev": true, "requires": { - "browser-process-hrtime": "0.1.3" + "browser-process-hrtime": "^0.1.2" } }, "walker": { @@ -5436,7 +5436,7 @@ "integrity": "sha1-L3+bj9ENZ3JisYqITijRlhjgKPs=", "dev": true, "requires": { - "makeerror": "1.0.11" + "makeerror": "1.0.x" } }, "watch": { @@ -5445,8 +5445,8 @@ "integrity": "sha1-KAlUdsbffJDJYxOJkMClQj60uYY=", "dev": true, "requires": { - "exec-sh": "0.2.2", - "minimist": "1.2.0" + "exec-sh": "^0.2.0", + "minimist": "^1.2.0" }, "dependencies": { "minimist": { @@ -5484,9 +5484,9 @@ "integrity": "sha512-rhRZRqx/TLJQWUpQ6bmrt2UV4f0HCQ463yQuONJqC6fO2VoEb1pTYddbe59SkYq87aoM5A3bdhMZiUiVws+fzQ==", "dev": true, "requires": { - "lodash.sortby": "4.7.0", - "tr46": "1.0.1", - "webidl-conversions": "4.0.2" + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" } }, "which": { @@ -5495,7 +5495,7 @@ "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", "dev": true, "requires": { - "isexe": "2.0.0" + "isexe": "^2.0.0" } }, "which-module": { @@ -5516,8 +5516,8 @@ "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", "dev": true, "requires": { - "string-width": "1.0.2", - "strip-ansi": "3.0.1" + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1" }, "dependencies": { "is-fullwidth-code-point": { @@ -5526,7 +5526,7 @@ "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", "dev": true, "requires": { - "number-is-nan": "1.0.1" + "number-is-nan": "^1.0.0" } }, "string-width": { @@ -5535,9 +5535,9 @@ "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", "dev": true, "requires": { - "code-point-at": "1.1.0", - "is-fullwidth-code-point": "1.0.0", - "strip-ansi": "3.0.1" + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" } }, "strip-ansi": { @@ -5546,7 +5546,7 @@ "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "dev": true, "requires": { - "ansi-regex": "2.1.1" + "ansi-regex": "^2.0.0" } } } @@ -5563,9 +5563,9 @@ "integrity": "sha512-s0b6vB3xIVRLWywa6X9TOMA7k9zio0TMOsl9ZnDkliA/cfJlpHXAscj0gbHVJiTdIuAYpIyqS5GW91fqm6gG5g==", "dev": true, "requires": { - "graceful-fs": "4.1.15", - "imurmurhash": "0.1.4", - "signal-exit": "3.0.2" + "graceful-fs": "^4.1.11", + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.2" } }, "ws": { @@ -5574,7 +5574,7 @@ "integrity": "sha512-jaHFD6PFv6UgoIVda6qZllptQsMlDEJkTQcybzzXDYM1XO9Y8em691FGMPmM46WGyLU4z9KMgQN+qrux/nhlHA==", "dev": true, "requires": { - "async-limiter": "1.0.0" + "async-limiter": "~1.0.0" } }, "xml-name-validator": { @@ -5601,18 +5601,18 @@ "integrity": "sha512-NwW69J42EsCSanF8kyn5upxvjp5ds+t3+udGBeTbFnERA+lF541DDpMawzo4z6W/QrzNM18D+BPMiOBibnFV5A==", "dev": true, "requires": { - "cliui": "4.1.0", - "decamelize": "1.2.0", - "find-up": "2.1.0", - "get-caller-file": "1.0.3", - "os-locale": "2.1.0", - "require-directory": "2.1.1", - "require-main-filename": "1.0.1", - "set-blocking": "2.0.0", - "string-width": "2.1.1", - "which-module": "2.0.0", - "y18n": "3.2.1", - "yargs-parser": "9.0.2" + "cliui": "^4.0.0", + "decamelize": "^1.1.1", + "find-up": "^2.1.0", + "get-caller-file": "^1.0.1", + "os-locale": "^2.0.0", + "require-directory": "^2.1.1", + "require-main-filename": "^1.0.1", + "set-blocking": "^2.0.0", + "string-width": "^2.0.0", + "which-module": "^2.0.0", + "y18n": "^3.2.1", + "yargs-parser": "^9.0.2" } }, "yargs-parser": { @@ -5621,7 +5621,7 @@ "integrity": "sha1-nM9qQ0YP5O1Aqbto9I1DuKaMwHc=", "dev": true, "requires": { - "camelcase": "4.1.0" + "camelcase": "^4.1.0" } } } diff --git a/packages/event-processor/package.json b/packages/event-processor/package.json index 8456656c9..58d8facba 100644 --- a/packages/event-processor/package.json +++ b/packages/event-processor/package.json @@ -6,6 +6,7 @@ "homepage": "https://github.com/optimizely/javascript-sdk/tree/master/packages/event-processor", "license": "Apache-2.0", "main": "lib/index.js", + "react-native": "lib/index.react_native.js", "types": "lib/index.d.ts", "directories": { "lib": "lib", @@ -39,7 +40,7 @@ }, "dependencies": { "@optimizely/js-sdk-logging": "^0.1.0", - "@optimizely/js-sdk-utils": "^0.2.0" + "@optimizely/js-sdk-utils": "^0.3.2" }, "devDependencies": { "@types/jest": "^24.0.9", diff --git a/packages/event-processor/src/eventDispatcher.ts b/packages/event-processor/src/eventDispatcher.ts index 1c6363566..c58bf4723 100644 --- a/packages/event-processor/src/eventDispatcher.ts +++ b/packages/event-processor/src/eventDispatcher.ts @@ -15,13 +15,7 @@ */ import { EventV1 } from "./v1/buildEventV1"; -export type EventDispatcherResponse = { - statusCode: number -} | { - status: number -} - -export type EventDispatcherCallback = (response: EventDispatcherResponse) => void +export type EventDispatcherCallback = (status: number) => void export interface EventDispatcher { dispatchEvent(event: EventV1Request, callback: EventDispatcherCallback): void diff --git a/packages/event-processor/src/eventProcessor.ts b/packages/event-processor/src/eventProcessor.ts index 1eeba4422..6d39d7f4e 100644 --- a/packages/event-processor/src/eventProcessor.ts +++ b/packages/event-processor/src/eventProcessor.ts @@ -38,8 +38,8 @@ const DEFAULT_MAX_QUEUE_SIZE = 10 export abstract class AbstractEventProcessor implements EventProcessor { protected dispatcher: EventDispatcher protected queue: EventQueue - private notificationCenter?: NotificationCenter - private requestTracker: RequestTracker + protected notificationCenter?: NotificationCenter + protected requestTracker: RequestTracker constructor({ dispatcher, diff --git a/packages/event-processor/src/index.react_native.ts b/packages/event-processor/src/index.react_native.ts new file mode 100644 index 000000000..5822f7313 --- /dev/null +++ b/packages/event-processor/src/index.react_native.ts @@ -0,0 +1,25 @@ +import { ReactNativeEventProcessor } from './v1/ReactNativeEventProcessor' +import { LogTierV1EventProcessor } from './v1/v1EventProcessor' + +/** + * Copyright 2019, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export * from './events' +export * from './eventProcessor' +export * from './eventDispatcher' +export * from './managed' +export * from './pendingEventsDispatcher' +export * from './v1/buildEventV1' +export { ReactNativeEventProcessor as LogTierV1EventProcessor } from './v1/ReactNativeEventProcessor' diff --git a/packages/event-processor/src/v1/ReactNativeEventProcessor.ts b/packages/event-processor/src/v1/ReactNativeEventProcessor.ts new file mode 100644 index 000000000..77aaaa0ee --- /dev/null +++ b/packages/event-processor/src/v1/ReactNativeEventProcessor.ts @@ -0,0 +1,224 @@ +/** + * Copyright 2020, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + objectValues, + NOTIFICATION_TYPES, + ReactNativeAsyncStorageCache, + generateUUID, +} from '@optimizely/js-sdk-utils' +import { getLogger } from '@optimizely/js-sdk-logging' + +import { ProcessableEvents } from "../eventProcessor" +import { LogTierV1EventProcessor } from "./v1EventProcessor" +import { EventV1Request } from '../eventDispatcher' + +const logger = getLogger('ReactNativeEventProcessor') + +export class ReactNativeEventProcessor extends LogTierV1EventProcessor { + private store: ReactNativeEventsStore = new ReactNativeEventsStore() + private bufferStore: ReactNativeBufferStore = new ReactNativeBufferStore() + + sendEventNotification(event: EventV1Request): void { + if (this.notificationCenter) { + this.notificationCenter.sendNotifications( + NOTIFICATION_TYPES.LOG_EVENT, + event, + ) + } + } + + async drainQueue(buffer: ProcessableEvents[]): Promise { + const pendingEventRequests = await this.processPendingEvents() + const reqPromise = new Promise(async (resolve) => { + logger.debug('draining queue with %s events', buffer.length) + + if (buffer.length === 0) { + resolve() + return + } + + const formattedEvent = this.formatEvents(buffer) + const cacheKey = generateUUID() + + await this.store.set(cacheKey, formattedEvent) + // Clear buffer because the buffer has become a formatted event and is already stored in pending cache. + this.bufferStore.clear() + this.dispatcher.dispatchEvent(formattedEvent, (status: number) => { + if (status < 200 || status >= 400) { + resolve() + } else { + this.store.remove(cacheKey).then(() => resolve()) + } + }) + + this.sendEventNotification(formattedEvent) + }) + this.requestTracker.trackRequest(reqPromise) + return Promise.all([ ...pendingEventRequests, reqPromise]).then(() => {}) + } + + async processPendingEvents(): Promise[]> { + const formattedEvents: {[key: string]: EventV1Request} = await this.store.getEventsMap() + const eventKeys: string[] = Object.keys(formattedEvents) + const pendingEventRequests: Promise[] = [] + for (let i = 0; i < eventKeys.length; i++) { + const eventKey = eventKeys[i] + pendingEventRequests.push(new Promise((resolve) => { + const formattedEvent = formattedEvents[eventKey] + this.dispatcher.dispatchEvent(formattedEvent, (status: number) => { + if (status < 200 || status >= 400) { + resolve() + } else { + this.store.remove(eventKey).then(() => resolve()) + } + }) + this.sendEventNotification(formattedEvent) + })) + + //TODO: Zeeshan - Add details of why i added this kludge. + await (() => new Promise(resolve => setTimeout(resolve)))() + } + return pendingEventRequests + } + + process(event: ProcessableEvents): void { + //TODO: Zeeshan - Add event to buffer store + this.bufferStore.add(event) + super.process(event) + } + + start(): void { + super.start() + this.processPendingEvents() + this.bufferStore.getAll().then((events: ProcessableEvents[]) => { + events.forEach((event: ProcessableEvents) => this.process(event)) + }) + } +} + +// A Resolvable process to support synchornization block +class ResolvablePromise extends Promise { + private resolver: any + + constructor() { + var res + super((resolve) => { + res = resolve + }) + this.resolver = res + } + + public resolve(): void { + this.resolver() + } +} + +class ReactNativeEventsStore { + private storageKey: string = 'fs_optly_pending_events' + private cache: ReactNativeAsyncStorageCache = new ReactNativeAsyncStorageCache() + private lockPromises: ResolvablePromise[] = [] + + private async getLock(): Promise { + this.lockPromises.push(new ResolvablePromise()) + if (this.lockPromises.length === 1) { + return + } + await this.lockPromises[this.lockPromises.length - 2] + } + + private releaseLock(): void { + if (this.lockPromises.length > 0) { + const promise = this.lockPromises.shift() + promise && promise.resolve() + return + } + } + + public async set(key: string, event: any): Promise { + await this.getLock() + const eventsMap = await this.cache.get(this.storageKey) || {} + eventsMap[key] = event + await this.cache.set(this.storageKey, eventsMap) + this.releaseLock() + return key + } + + public async remove(key: string): Promise { + await this.getLock() + const eventsMap = await this.cache.get(this.storageKey) || {} + eventsMap[key] && delete eventsMap[key] + await this.cache.set(this.storageKey, eventsMap) + this.releaseLock() + } + + public async get(key: string): Promise { + await this.getLock() + const eventsMap = await this.cache.get(this.storageKey) || {} + this.releaseLock() + return eventsMap[key] + } + + public async getAllEvents(): Promise { + await this.getLock() + const eventsMap = await this.cache.get(this.storageKey) || {} + //await this.cache.remove(this.storageKey) + this.releaseLock() + return objectValues(eventsMap) + } + + public async getEventsMap(): Promise { + return await this.cache.get(this.storageKey) || {} + } +} + +class ReactNativeBufferStore { + private storageKey: string = 'fs_optly_event_buffer' + private cache: ReactNativeAsyncStorageCache = new ReactNativeAsyncStorageCache() + + private lockPromises: ResolvablePromise[] = [] + + private async getLock(): Promise { + this.lockPromises.push(new ResolvablePromise()) + if (this.lockPromises.length === 1) { + return + } + await this.lockPromises[this.lockPromises.length - 2] + } + + private releaseLock(): void { + if (this.lockPromises.length > 0) { + const promise = this.lockPromises.shift() + promise && promise.resolve() + return + } + } + + public async add(event: ProcessableEvents) { + await this.getLock() + const events = await this.getAll() + events.push(event) + this.cache.set(this.storageKey, events) + await this.releaseLock() + } + + public async getAll(): Promise { + return (await this.cache.get(this.storageKey) || []) as ProcessableEvents[] + } + + public async clear(): Promise { + this.cache.remove(this.storageKey) + } +} diff --git a/packages/event-processor/src/v1/buildEventV1.ts b/packages/event-processor/src/v1/buildEventV1.ts index eeb14472c..29266a8ac 100644 --- a/packages/event-processor/src/v1/buildEventV1.ts +++ b/packages/event-processor/src/v1/buildEventV1.ts @@ -17,6 +17,7 @@ export type EventV1 = { } type Visitor = { + timestamp: number snapshots: Visitor.Snapshot[] visitor_id: string attributes: Visitor.Attribute[] @@ -160,6 +161,7 @@ function makeDecisionSnapshot(event: ImpressionEvent): Visitor.Snapshot { function makeVisitor(data: ImpressionEvent | ConversionEvent): Visitor { const visitor: Visitor = { + timestamp: data.timestamp, snapshots: [], visitor_id: data.user.id, attributes: [], diff --git a/packages/optimizely-sdk/rollup.config.js b/packages/optimizely-sdk/rollup.config.js index 129bdbc51..9b930cec6 100644 --- a/packages/optimizely-sdk/rollup.config.js +++ b/packages/optimizely-sdk/rollup.config.js @@ -63,21 +63,24 @@ const umdBundle = { 'setErrorHandler', 'getErrorHandler', ], - '@optimizely/js-sdk-event-processor': ['LogTierV1EventProcessor', 'LocalStoragePendingEventsDispatcher'], + '@optimizely/js-sdk-event-processor': ['LogTierV1EventProcessor', 'LocalStoragePendingEventsDispatcher', 'ReactNativeEventProcessor'], }, }), ], input: 'lib/index.browser.js', + external: '@react-native-community/async-storage', output: [ { name: 'optimizelySdk', format: 'umd', + globals: {'@react-native-community/async-storage': 'asyncStorage'}, file: 'dist/optimizely.browser.umd.js', exports: 'named', }, { name: 'optimizelySdk', format: 'umd', + globals: {'@react-native-community/async-storage': 'asyncStorage'}, file: 'dist/optimizely.browser.umd.min.js', exports: 'named', plugins: [terser()], From 8076ef4172aa60c74685ef33be3bafa404b6f081 Mon Sep 17 00:00:00 2001 From: Zeeshan Ashraf Date: Fri, 3 Jul 2020 09:36:34 -0700 Subject: [PATCH 02/28] added new `eventMaxQueueSize` configuration option. --- .../event-processor/src/eventProcessor.ts | 20 +++++----- .../src/v1/ReactNativeEventProcessor.ts | 39 ++++++++++++++++--- 2 files changed, 43 insertions(+), 16 deletions(-) diff --git a/packages/event-processor/src/eventProcessor.ts b/packages/event-processor/src/eventProcessor.ts index 6d39d7f4e..d27c4bda8 100644 --- a/packages/event-processor/src/eventProcessor.ts +++ b/packages/event-processor/src/eventProcessor.ts @@ -33,7 +33,7 @@ export interface EventProcessor extends Managed { } const DEFAULT_FLUSH_INTERVAL = 30000 // Unit is ms - default flush interval is 30s -const DEFAULT_MAX_QUEUE_SIZE = 10 +const DEFAULT_BATCH_SIZE = 10 export abstract class AbstractEventProcessor implements EventProcessor { protected dispatcher: EventDispatcher @@ -44,12 +44,12 @@ export abstract class AbstractEventProcessor implements EventProcessor { constructor({ dispatcher, flushInterval = 30000, - maxQueueSize = 3000, + batchSize = 3000, notificationCenter, }: { dispatcher: EventDispatcher flushInterval?: number - maxQueueSize?: number + batchSize?: number notificationCenter?: NotificationCenter }) { this.dispatcher = dispatcher @@ -61,19 +61,19 @@ export abstract class AbstractEventProcessor implements EventProcessor { flushInterval = DEFAULT_FLUSH_INTERVAL } - maxQueueSize = Math.floor(maxQueueSize) - if (maxQueueSize < 1) { + batchSize = Math.floor(batchSize) + if (batchSize < 1) { logger.warn( - `Invalid maxQueueSize ${maxQueueSize}, defaulting to ${DEFAULT_MAX_QUEUE_SIZE}`, + `Invalid batchSize ${batchSize}, defaulting to ${DEFAULT_BATCH_SIZE}`, ) - maxQueueSize = DEFAULT_MAX_QUEUE_SIZE + batchSize = DEFAULT_BATCH_SIZE } - maxQueueSize = Math.max(1, maxQueueSize) - if (maxQueueSize > 1) { + batchSize = Math.max(1, batchSize) + if (batchSize > 1) { this.queue = new DefaultEventQueue({ flushInterval, - maxQueueSize, + maxQueueSize: batchSize, sink: buffer => this.drainQueue(buffer), batchComparator: areEventContextsEqual, }) diff --git a/packages/event-processor/src/v1/ReactNativeEventProcessor.ts b/packages/event-processor/src/v1/ReactNativeEventProcessor.ts index 77aaaa0ee..74523aca6 100644 --- a/packages/event-processor/src/v1/ReactNativeEventProcessor.ts +++ b/packages/event-processor/src/v1/ReactNativeEventProcessor.ts @@ -17,20 +17,40 @@ import { objectValues, NOTIFICATION_TYPES, ReactNativeAsyncStorageCache, - generateUUID, + generateUUID, + NotificationCenter, } from '@optimizely/js-sdk-utils' import { getLogger } from '@optimizely/js-sdk-logging' import { ProcessableEvents } from "../eventProcessor" import { LogTierV1EventProcessor } from "./v1EventProcessor" -import { EventV1Request } from '../eventDispatcher' +import { EventV1Request, EventDispatcher } from '../eventDispatcher' const logger = getLogger('ReactNativeEventProcessor') +const DEFAULT_MAX_QUEUE_SIZE = 10000 + export class ReactNativeEventProcessor extends LogTierV1EventProcessor { - private store: ReactNativeEventsStore = new ReactNativeEventsStore() + private store: ReactNativeEventsStore private bufferStore: ReactNativeBufferStore = new ReactNativeBufferStore() + constructor({ + dispatcher, + flushInterval = 30000, + batchSize = 3000, + maxQueueSize = DEFAULT_MAX_QUEUE_SIZE, + notificationCenter, + }: { + dispatcher: EventDispatcher + flushInterval?: number + batchSize?: number + maxQueueSize?: number + notificationCenter?: NotificationCenter + }) { + super({ dispatcher, flushInterval, batchSize, notificationCenter }) + this.store = new ReactNativeEventsStore(maxQueueSize) + } + sendEventNotification(event: EventV1Request): void { if (this.notificationCenter) { this.notificationCenter.sendNotifications( @@ -130,6 +150,11 @@ class ReactNativeEventsStore { private storageKey: string = 'fs_optly_pending_events' private cache: ReactNativeAsyncStorageCache = new ReactNativeAsyncStorageCache() private lockPromises: ResolvablePromise[] = [] + private maxSize: number + + constructor(maxSize: number) { + this.maxSize = maxSize + } private async getLock(): Promise { this.lockPromises.push(new ResolvablePromise()) @@ -149,9 +174,11 @@ class ReactNativeEventsStore { public async set(key: string, event: any): Promise { await this.getLock() - const eventsMap = await this.cache.get(this.storageKey) || {} - eventsMap[key] = event - await this.cache.set(this.storageKey, eventsMap) + const eventsMap = await this.cache.get(this.storageKey) || {} + if (Object.keys(eventsMap).length < this.maxSize) { + eventsMap[key] = event + await this.cache.set(this.storageKey, eventsMap) + } this.releaseLock() return key } From 6d338d2c00407713bf488272ae8a8cf80b64394a Mon Sep 17 00:00:00 2001 From: Zeeshan Ashraf Date: Fri, 3 Jul 2020 13:45:16 -0700 Subject: [PATCH 03/28] some cleanup and refactor --- .../event-processor/src/eventProcessor.ts | 16 +- .../event-processor/src/index.react_native.ts | 8 +- packages/event-processor/src/index.ts | 5 +- .../src/reactNativeEventProcessor.ts | 126 +++++++++ .../src/reactNativeEventsStore.ts | 136 ++++++++++ .../src/v1/ReactNativeEventProcessor.ts | 251 ------------------ .../src/v1/v1EventProcessor.ts | 13 +- 7 files changed, 290 insertions(+), 265 deletions(-) create mode 100644 packages/event-processor/src/reactNativeEventProcessor.ts create mode 100644 packages/event-processor/src/reactNativeEventsStore.ts delete mode 100644 packages/event-processor/src/v1/ReactNativeEventProcessor.ts diff --git a/packages/event-processor/src/eventProcessor.ts b/packages/event-processor/src/eventProcessor.ts index d27c4bda8..b6be6c5eb 100644 --- a/packages/event-processor/src/eventProcessor.ts +++ b/packages/event-processor/src/eventProcessor.ts @@ -100,17 +100,21 @@ export abstract class AbstractEventProcessor implements EventProcessor { this.dispatcher.dispatchEvent(formattedEvent, () => { resolve() }) - if (this.notificationCenter) { - this.notificationCenter.sendNotifications( - NOTIFICATION_TYPES.LOG_EVENT, - formattedEvent, - ) - } + this.sendEventNotification(formattedEvent) }) this.requestTracker.trackRequest(reqPromise) return reqPromise } + protected sendEventNotification(event: EventV1Request): void { + if (this.notificationCenter) { + this.notificationCenter.sendNotifications( + NOTIFICATION_TYPES.LOG_EVENT, + event, + ) + } + } + process(event: ProcessableEvents): void { this.queue.enqueue(event) } diff --git a/packages/event-processor/src/index.react_native.ts b/packages/event-processor/src/index.react_native.ts index 5822f7313..ced080f4f 100644 --- a/packages/event-processor/src/index.react_native.ts +++ b/packages/event-processor/src/index.react_native.ts @@ -1,8 +1,5 @@ -import { ReactNativeEventProcessor } from './v1/ReactNativeEventProcessor' -import { LogTierV1EventProcessor } from './v1/v1EventProcessor' - /** - * Copyright 2019, Optimizely + * Copyright 2020, Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,10 +13,11 @@ import { LogTierV1EventProcessor } from './v1/v1EventProcessor' * See the License for the specific language governing permissions and * limitations under the License. */ + export * from './events' export * from './eventProcessor' export * from './eventDispatcher' export * from './managed' export * from './pendingEventsDispatcher' export * from './v1/buildEventV1' -export { ReactNativeEventProcessor as LogTierV1EventProcessor } from './v1/ReactNativeEventProcessor' +export { LogTierV1ReactNativeEventProcessor as LogTierV1EventProcessor } from './v1/v1EventProcessor' diff --git a/packages/event-processor/src/index.ts b/packages/event-processor/src/index.ts index e0b20f652..81242ee32 100644 --- a/packages/event-processor/src/index.ts +++ b/packages/event-processor/src/index.ts @@ -1,5 +1,5 @@ /** - * Copyright 2019, Optimizely + * Copyright 2019-2020, Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,10 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + export * from './events' export * from './eventProcessor' export * from './eventDispatcher' export * from './managed' export * from './pendingEventsDispatcher' export * from './v1/buildEventV1' -export * from './v1/v1EventProcessor' \ No newline at end of file +export { LogTierV1EventProcessor } from './v1/v1EventProcessor' diff --git a/packages/event-processor/src/reactNativeEventProcessor.ts b/packages/event-processor/src/reactNativeEventProcessor.ts new file mode 100644 index 000000000..5d0ca4ca7 --- /dev/null +++ b/packages/event-processor/src/reactNativeEventProcessor.ts @@ -0,0 +1,126 @@ +/** + * Copyright 2020, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + generateUUID, + NotificationCenter, +} from '@optimizely/js-sdk-utils' +import { getLogger } from '@optimizely/js-sdk-logging' + +import { EventDispatcher } from './eventDispatcher' +import { ProcessableEvents, AbstractEventProcessor } from "./eventProcessor" +import { ReactNativePendingEventsStore, ReactNativeEventBufferStore } from './reactNativeEventsStore' + +const logger = getLogger('ReactNativeEventProcessor') + +const DEFAULT_MAX_QUEUE_SIZE = 10000 + +export abstract class AbstractReactNativeEventProcessor extends AbstractEventProcessor { + private pendingEventsStore: ReactNativePendingEventsStore + private eventBufferStore: ReactNativeEventBufferStore = new ReactNativeEventBufferStore() + + constructor({ + dispatcher, + flushInterval = 30000, + batchSize = 3000, + maxQueueSize = DEFAULT_MAX_QUEUE_SIZE, + notificationCenter, + }: { + dispatcher: EventDispatcher + flushInterval?: number + batchSize?: number + maxQueueSize?: number + notificationCenter?: NotificationCenter + }) { + super({ dispatcher, flushInterval, batchSize, notificationCenter }) + this.pendingEventsStore = new ReactNativePendingEventsStore(maxQueueSize) + } + + isSuccessResponse(status: number): boolean { + return status >= 200 && status < 400 + } + + async drainQueue(buffer: ProcessableEvents[]): Promise { + const pendingEventRequests = await this.processPendingEvents() + const reqPromise = new Promise(async (resolve) => { + logger.debug('draining queue with %s events', buffer.length) + + if (buffer.length === 0) { + resolve() + return + } + + const formattedEvent = this.formatEvents(buffer) + const cacheKey = generateUUID() + + // Store formatted event before dispatching. + await this.pendingEventsStore.set(cacheKey, formattedEvent) + + // Clear buffer because the buffer has become a formatted event and is already stored in pending cache. + this.eventBufferStore.clear() + + this.dispatcher.dispatchEvent(formattedEvent, (status: number) => { + if (this.isSuccessResponse(status)) { + this.pendingEventsStore.remove(cacheKey).then(() => resolve()) + } else { + resolve() + } + }) + this.sendEventNotification(formattedEvent) + }) + this.requestTracker.trackRequest(reqPromise) + return Promise.all([ ...pendingEventRequests, reqPromise]).then(() => {}) + } + + async processPendingEvents(): Promise[]> { + const formattedEvents: {[key: string]: any} = await this.pendingEventsStore.getEventsMap() + return Object.keys(formattedEvents).map(async (eventKey) => { + const requestPromise = new Promise((resolve) => { + const formattedEvent = formattedEvents[eventKey] + this.dispatcher.dispatchEvent(formattedEvent, (status: number) => { + if (this.isSuccessResponse(status)) { + this.pendingEventsStore.remove(eventKey).then(() => resolve()) + } else { + resolve() + } + }) + this.sendEventNotification(formattedEvent) + }) + + //TODO: Zeeshan - Add details of why i added this kludge. + await (() => new Promise(resolve => setTimeout(resolve)))() + + return requestPromise + }) + } + + process(event: ProcessableEvents): void { + // Adding events to buffer store. If app closes before dispatch, we can reprocess next time the app initializes + this.eventBufferStore.add(event) + super.process(event) + } + + start(): void { + super.start() + + // Dispatch all the formatted pending events right away + this.processPendingEvents() + + // Process individual events pending from the buffer. + this.eventBufferStore.getAll().then((events: ProcessableEvents[]) => { + events.forEach((event: ProcessableEvents) => this.process(event)) + }) + } +} diff --git a/packages/event-processor/src/reactNativeEventsStore.ts b/packages/event-processor/src/reactNativeEventsStore.ts new file mode 100644 index 000000000..a2b81d2d9 --- /dev/null +++ b/packages/event-processor/src/reactNativeEventsStore.ts @@ -0,0 +1,136 @@ + +/** + * Copyright 2020, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { ReactNativeAsyncStorageCache, objectValues } from "@optimizely/js-sdk-utils" + +import { ProcessableEvents } from "./eventProcessor" + +// This Stores Formatted events before dispatching. The events are removed after they are successfully dispatched. +// Stored events are retried on every new event dispatch, when connection becomes available again or when SDK initializes the next time. +export class ReactNativePendingEventsStore { + private storageKey: string = 'fs_optly_pending_events' + private cache: ReactNativeAsyncStorageCache = new ReactNativeAsyncStorageCache() + private maxSize: number + private synchronizer: Synchronizer = new Synchronizer() + + constructor(maxSize: number) { + this.maxSize = maxSize + } + + public async set(key: string, event: any): Promise { + await this.synchronizer.getLock() + const eventsMap = await this.cache.get(this.storageKey) || {} + if (Object.keys(eventsMap).length < this.maxSize) { + eventsMap[key] = event + await this.cache.set(this.storageKey, eventsMap) + } + this.synchronizer.releaseLock() + return key + } + + public async remove(key: string): Promise { + await this.synchronizer.getLock() + const eventsMap = await this.cache.get(this.storageKey) || {} + eventsMap[key] && delete eventsMap[key] + await this.cache.set(this.storageKey, eventsMap) + this.synchronizer.releaseLock() + } + + public async get(key: string): Promise { + await this.synchronizer.getLock() + const eventsMap = await this.cache.get(this.storageKey) || {} + this.synchronizer.releaseLock() + return eventsMap[key] + } + + public async getAllEvents(): Promise { + await this.synchronizer.getLock() + const eventsMap = await this.cache.get(this.storageKey) || {} + this.synchronizer.releaseLock() + return objectValues(eventsMap) + } + + public async getEventsMap(): Promise { + return await this.cache.get(this.storageKey) || {} + } +} + +// This stores individual events generated from the SDK till they are part of the pending buffer. +// The store is cleared right before the event is formatted to be dispatched. +// This is to make sure that individual events are not lost when app closes before the buffer was flushed. +export class ReactNativeEventBufferStore { + private storageKey: string = 'fs_optly_event_buffer' + private cache: ReactNativeAsyncStorageCache = new ReactNativeAsyncStorageCache() + private synchronizer: Synchronizer = new Synchronizer() + + public async add(event: ProcessableEvents) { + await this.synchronizer.getLock() + const events = await this.getAll() + events.push(event) + await this.cache.set(this.storageKey, events) + this.synchronizer.releaseLock() + } + + public async getAll(): Promise { + return (await this.cache.get(this.storageKey) || []) as ProcessableEvents[] + } + + public async clear(): Promise { + this.cache.remove(this.storageKey) + } +} + +// Both the above stores use single entry in the async storage store to manage their maps and lists. +// This results in race condition when two items are added to the map or array in parallel. +// for ex. Req 1 gets the map. Req 2 gets the map. Req 1 sets the map. Req 2 sets the map. The map now loses item from Req 1. +// This synchronizer makes sure the operations are atomic using promises. +class Synchronizer { + private lockPromises: ResolvablePromise[] = [] + + public async getLock(): Promise { + this.lockPromises.push(new ResolvablePromise()) + if (this.lockPromises.length === 1) { + return + } + await this.lockPromises[this.lockPromises.length - 2] + } + + public releaseLock(): void { + if (this.lockPromises.length > 0) { + const promise = this.lockPromises.shift() + promise && promise.resolve() + return + } + } +} + + +// A Resolvable process to support synchornization block +export class ResolvablePromise extends Promise { + private resolver: any + + constructor() { + var res + super((resolve) => { + res = resolve + }) + this.resolver = res + } + + public resolve(): void { + this.resolver() + } +} diff --git a/packages/event-processor/src/v1/ReactNativeEventProcessor.ts b/packages/event-processor/src/v1/ReactNativeEventProcessor.ts deleted file mode 100644 index 74523aca6..000000000 --- a/packages/event-processor/src/v1/ReactNativeEventProcessor.ts +++ /dev/null @@ -1,251 +0,0 @@ -/** - * Copyright 2020, Optimizely - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import { - objectValues, - NOTIFICATION_TYPES, - ReactNativeAsyncStorageCache, - generateUUID, - NotificationCenter, -} from '@optimizely/js-sdk-utils' -import { getLogger } from '@optimizely/js-sdk-logging' - -import { ProcessableEvents } from "../eventProcessor" -import { LogTierV1EventProcessor } from "./v1EventProcessor" -import { EventV1Request, EventDispatcher } from '../eventDispatcher' - -const logger = getLogger('ReactNativeEventProcessor') - -const DEFAULT_MAX_QUEUE_SIZE = 10000 - -export class ReactNativeEventProcessor extends LogTierV1EventProcessor { - private store: ReactNativeEventsStore - private bufferStore: ReactNativeBufferStore = new ReactNativeBufferStore() - - constructor({ - dispatcher, - flushInterval = 30000, - batchSize = 3000, - maxQueueSize = DEFAULT_MAX_QUEUE_SIZE, - notificationCenter, - }: { - dispatcher: EventDispatcher - flushInterval?: number - batchSize?: number - maxQueueSize?: number - notificationCenter?: NotificationCenter - }) { - super({ dispatcher, flushInterval, batchSize, notificationCenter }) - this.store = new ReactNativeEventsStore(maxQueueSize) - } - - sendEventNotification(event: EventV1Request): void { - if (this.notificationCenter) { - this.notificationCenter.sendNotifications( - NOTIFICATION_TYPES.LOG_EVENT, - event, - ) - } - } - - async drainQueue(buffer: ProcessableEvents[]): Promise { - const pendingEventRequests = await this.processPendingEvents() - const reqPromise = new Promise(async (resolve) => { - logger.debug('draining queue with %s events', buffer.length) - - if (buffer.length === 0) { - resolve() - return - } - - const formattedEvent = this.formatEvents(buffer) - const cacheKey = generateUUID() - - await this.store.set(cacheKey, formattedEvent) - // Clear buffer because the buffer has become a formatted event and is already stored in pending cache. - this.bufferStore.clear() - this.dispatcher.dispatchEvent(formattedEvent, (status: number) => { - if (status < 200 || status >= 400) { - resolve() - } else { - this.store.remove(cacheKey).then(() => resolve()) - } - }) - - this.sendEventNotification(formattedEvent) - }) - this.requestTracker.trackRequest(reqPromise) - return Promise.all([ ...pendingEventRequests, reqPromise]).then(() => {}) - } - - async processPendingEvents(): Promise[]> { - const formattedEvents: {[key: string]: EventV1Request} = await this.store.getEventsMap() - const eventKeys: string[] = Object.keys(formattedEvents) - const pendingEventRequests: Promise[] = [] - for (let i = 0; i < eventKeys.length; i++) { - const eventKey = eventKeys[i] - pendingEventRequests.push(new Promise((resolve) => { - const formattedEvent = formattedEvents[eventKey] - this.dispatcher.dispatchEvent(formattedEvent, (status: number) => { - if (status < 200 || status >= 400) { - resolve() - } else { - this.store.remove(eventKey).then(() => resolve()) - } - }) - this.sendEventNotification(formattedEvent) - })) - - //TODO: Zeeshan - Add details of why i added this kludge. - await (() => new Promise(resolve => setTimeout(resolve)))() - } - return pendingEventRequests - } - - process(event: ProcessableEvents): void { - //TODO: Zeeshan - Add event to buffer store - this.bufferStore.add(event) - super.process(event) - } - - start(): void { - super.start() - this.processPendingEvents() - this.bufferStore.getAll().then((events: ProcessableEvents[]) => { - events.forEach((event: ProcessableEvents) => this.process(event)) - }) - } -} - -// A Resolvable process to support synchornization block -class ResolvablePromise extends Promise { - private resolver: any - - constructor() { - var res - super((resolve) => { - res = resolve - }) - this.resolver = res - } - - public resolve(): void { - this.resolver() - } -} - -class ReactNativeEventsStore { - private storageKey: string = 'fs_optly_pending_events' - private cache: ReactNativeAsyncStorageCache = new ReactNativeAsyncStorageCache() - private lockPromises: ResolvablePromise[] = [] - private maxSize: number - - constructor(maxSize: number) { - this.maxSize = maxSize - } - - private async getLock(): Promise { - this.lockPromises.push(new ResolvablePromise()) - if (this.lockPromises.length === 1) { - return - } - await this.lockPromises[this.lockPromises.length - 2] - } - - private releaseLock(): void { - if (this.lockPromises.length > 0) { - const promise = this.lockPromises.shift() - promise && promise.resolve() - return - } - } - - public async set(key: string, event: any): Promise { - await this.getLock() - const eventsMap = await this.cache.get(this.storageKey) || {} - if (Object.keys(eventsMap).length < this.maxSize) { - eventsMap[key] = event - await this.cache.set(this.storageKey, eventsMap) - } - this.releaseLock() - return key - } - - public async remove(key: string): Promise { - await this.getLock() - const eventsMap = await this.cache.get(this.storageKey) || {} - eventsMap[key] && delete eventsMap[key] - await this.cache.set(this.storageKey, eventsMap) - this.releaseLock() - } - - public async get(key: string): Promise { - await this.getLock() - const eventsMap = await this.cache.get(this.storageKey) || {} - this.releaseLock() - return eventsMap[key] - } - - public async getAllEvents(): Promise { - await this.getLock() - const eventsMap = await this.cache.get(this.storageKey) || {} - //await this.cache.remove(this.storageKey) - this.releaseLock() - return objectValues(eventsMap) - } - - public async getEventsMap(): Promise { - return await this.cache.get(this.storageKey) || {} - } -} - -class ReactNativeBufferStore { - private storageKey: string = 'fs_optly_event_buffer' - private cache: ReactNativeAsyncStorageCache = new ReactNativeAsyncStorageCache() - - private lockPromises: ResolvablePromise[] = [] - - private async getLock(): Promise { - this.lockPromises.push(new ResolvablePromise()) - if (this.lockPromises.length === 1) { - return - } - await this.lockPromises[this.lockPromises.length - 2] - } - - private releaseLock(): void { - if (this.lockPromises.length > 0) { - const promise = this.lockPromises.shift() - promise && promise.resolve() - return - } - } - - public async add(event: ProcessableEvents) { - await this.getLock() - const events = await this.getAll() - events.push(event) - this.cache.set(this.storageKey, events) - await this.releaseLock() - } - - public async getAll(): Promise { - return (await this.cache.get(this.storageKey) || []) as ProcessableEvents[] - } - - public async clear(): Promise { - this.cache.remove(this.storageKey) - } -} diff --git a/packages/event-processor/src/v1/v1EventProcessor.ts b/packages/event-processor/src/v1/v1EventProcessor.ts index 3796f5f63..d5bc91edd 100644 --- a/packages/event-processor/src/v1/v1EventProcessor.ts +++ b/packages/event-processor/src/v1/v1EventProcessor.ts @@ -1,5 +1,5 @@ /** - * Copyright 2019, Optimizely + * Copyright 2019-2020, Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,7 @@ import { AbstractEventProcessor, ProcessableEvents } from '../eventProcessor' import { EventV1Request } from '../eventDispatcher' import { makeBatchedEventV1 } from './buildEventV1' +import { AbstractReactNativeEventProcessor } from '../reactNativeEventProcessor' export class LogTierV1EventProcessor extends AbstractEventProcessor { protected formatEvents(events: ProcessableEvents[]): EventV1Request { @@ -27,3 +28,13 @@ export class LogTierV1EventProcessor extends AbstractEventProcessor { } } } + +export class LogTierV1ReactNativeEventProcessor extends AbstractReactNativeEventProcessor { + protected formatEvents(events: ProcessableEvents[]): EventV1Request { + return { + url: 'https://logx.optimizely.com/v1/events', + httpVerb: 'POST', + params: makeBatchedEventV1(events), + } + } +} From ab29e0e2a9e0f9a816ca9ced4d6e27d544107899 Mon Sep 17 00:00:00 2001 From: Zeeshan Ashraf Date: Fri, 3 Jul 2020 15:01:20 -0700 Subject: [PATCH 04/28] added some comments --- packages/event-processor/src/eventProcessor.ts | 2 +- packages/event-processor/src/reactNativeEventProcessor.ts | 8 +++++++- packages/optimizely-sdk/rollup.config.js | 5 +---- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/packages/event-processor/src/eventProcessor.ts b/packages/event-processor/src/eventProcessor.ts index b6be6c5eb..8b22d54b4 100644 --- a/packages/event-processor/src/eventProcessor.ts +++ b/packages/event-processor/src/eventProcessor.ts @@ -38,7 +38,7 @@ const DEFAULT_BATCH_SIZE = 10 export abstract class AbstractEventProcessor implements EventProcessor { protected dispatcher: EventDispatcher protected queue: EventQueue - protected notificationCenter?: NotificationCenter + private notificationCenter?: NotificationCenter protected requestTracker: RequestTracker constructor({ diff --git a/packages/event-processor/src/reactNativeEventProcessor.ts b/packages/event-processor/src/reactNativeEventProcessor.ts index 5d0ca4ca7..cb769a9c4 100644 --- a/packages/event-processor/src/reactNativeEventProcessor.ts +++ b/packages/event-processor/src/reactNativeEventProcessor.ts @@ -99,7 +99,13 @@ export abstract class AbstractReactNativeEventProcessor extends AbstractEventPro this.sendEventNotification(formattedEvent) }) - //TODO: Zeeshan - Add details of why i added this kludge. + // This is somehow needed to make sure events are dispatched in the correct order. + // The dispatcher always hands over the requests to XMLHttpRequest in correct order + // but for some reason, XMLHttpRequest internally mismanages the order and the events + // are received by server in random order. This peice of code probably pushes each subsequent event's + // processing to next iteration of javascript event loop somehow resulting in the correct predictable + // behavior. This is definitely a kludge which appears to be working consistently as expected + // when tested. I dont have a better logical explanation of this. await (() => new Promise(resolve => setTimeout(resolve)))() return requestPromise diff --git a/packages/optimizely-sdk/rollup.config.js b/packages/optimizely-sdk/rollup.config.js index 9b930cec6..129bdbc51 100644 --- a/packages/optimizely-sdk/rollup.config.js +++ b/packages/optimizely-sdk/rollup.config.js @@ -63,24 +63,21 @@ const umdBundle = { 'setErrorHandler', 'getErrorHandler', ], - '@optimizely/js-sdk-event-processor': ['LogTierV1EventProcessor', 'LocalStoragePendingEventsDispatcher', 'ReactNativeEventProcessor'], + '@optimizely/js-sdk-event-processor': ['LogTierV1EventProcessor', 'LocalStoragePendingEventsDispatcher'], }, }), ], input: 'lib/index.browser.js', - external: '@react-native-community/async-storage', output: [ { name: 'optimizelySdk', format: 'umd', - globals: {'@react-native-community/async-storage': 'asyncStorage'}, file: 'dist/optimizely.browser.umd.js', exports: 'named', }, { name: 'optimizelySdk', format: 'umd', - globals: {'@react-native-community/async-storage': 'asyncStorage'}, file: 'dist/optimizely.browser.umd.min.js', exports: 'named', plugins: [terser()], From cbb6aef05b7b46207578140be17fbd4b4a0b6582 Mon Sep 17 00:00:00 2001 From: Zeeshan Ashraf Date: Fri, 3 Jul 2020 18:14:03 -0700 Subject: [PATCH 05/28] added connectivity listener --- packages/event-processor/package-lock.json | 6 ++++++ packages/event-processor/package.json | 4 ++++ .../src/reactNativeEventProcessor.ts | 21 +++++++++++++++++++ 3 files changed, 31 insertions(+) diff --git a/packages/event-processor/package-lock.json b/packages/event-processor/package-lock.json index d5cae7c22..06769c69c 100644 --- a/packages/event-processor/package-lock.json +++ b/packages/event-processor/package-lock.json @@ -58,6 +58,12 @@ "uuid": "^3.3.2" } }, + "@react-native-community/netinfo": { + "version": "5.9.4", + "resolved": "https://registry.npmjs.org/@react-native-community/netinfo/-/netinfo-5.9.4.tgz", + "integrity": "sha512-mb664NOqPvyUZ4TznzdYEfdS3OhSXWGbZprgsDZn4THw2X/4wcBFcBUeWuMzeQ56KhY0rm/YBBlZWHrSf3C/Aw==", + "dev": true + }, "@types/jest": { "version": "24.0.9", "resolved": "https://registry.npmjs.org/@types/jest/-/jest-24.0.9.tgz", diff --git a/packages/event-processor/package.json b/packages/event-processor/package.json index 58d8facba..1d8e6dc3f 100644 --- a/packages/event-processor/package.json +++ b/packages/event-processor/package.json @@ -43,10 +43,14 @@ "@optimizely/js-sdk-utils": "^0.3.2" }, "devDependencies": { + "@react-native-community/netinfo": "^5.9.4", "@types/jest": "^24.0.9", "jest": "^23.6.0", "jest-localstorage-mock": "^2.4.0", "ts-jest": "^23.10.5", "typescript": "^3.3.3333" + }, + "peerDependencies": { + "@react-native-community/netinfo": "5.9.4" } } diff --git a/packages/event-processor/src/reactNativeEventProcessor.ts b/packages/event-processor/src/reactNativeEventProcessor.ts index cb769a9c4..f3388b338 100644 --- a/packages/event-processor/src/reactNativeEventProcessor.ts +++ b/packages/event-processor/src/reactNativeEventProcessor.ts @@ -18,6 +18,10 @@ import { NotificationCenter, } from '@optimizely/js-sdk-utils' import { getLogger } from '@optimizely/js-sdk-logging' +import { + NetInfoState, + addEventListener as addConnectionListener, +} from "@react-native-community/netinfo" import { EventDispatcher } from './eventDispatcher' import { ProcessableEvents, AbstractEventProcessor } from "./eventProcessor" @@ -30,6 +34,8 @@ const DEFAULT_MAX_QUEUE_SIZE = 10000 export abstract class AbstractReactNativeEventProcessor extends AbstractEventProcessor { private pendingEventsStore: ReactNativePendingEventsStore private eventBufferStore: ReactNativeEventBufferStore = new ReactNativeEventBufferStore() + private unsubscribeNetInfo: Function + private isInternetReachable: boolean = true constructor({ dispatcher, @@ -46,6 +52,16 @@ export abstract class AbstractReactNativeEventProcessor extends AbstractEventPro }) { super({ dispatcher, flushInterval, batchSize, notificationCenter }) this.pendingEventsStore = new ReactNativePendingEventsStore(maxQueueSize) + this.unsubscribeNetInfo = addConnectionListener((state: NetInfoState) => { + if (this.isInternetReachable && !state.isInternetReachable) { + this.isInternetReachable = false + } + + if (!this.isInternetReachable && state.isInternetReachable) { + this.isInternetReachable = true + this.processPendingEvents() + } + }) } isSuccessResponse(status: number): boolean { @@ -129,4 +145,9 @@ export abstract class AbstractReactNativeEventProcessor extends AbstractEventPro events.forEach((event: ProcessableEvents) => this.process(event)) }) } + + stop(): Promise { + this.unsubscribeNetInfo() + return super.stop() + } } From b26d40775a9bfb19bb2da6e3c1713df916d3bd45 Mon Sep 17 00:00:00 2001 From: Zeeshan Ashraf Date: Sun, 5 Jul 2020 23:10:58 -0700 Subject: [PATCH 06/28] fixed existing unit tests --- .../@react-native-community/netinfo.ts | 19 ++++++++++++ .../__tests__/v1EventProcessor.spec.ts | 30 +++++++++---------- .../event-processor/src/v1/buildEventV1.ts | 2 -- 3 files changed, 34 insertions(+), 17 deletions(-) create mode 100644 packages/event-processor/__mocks__/@react-native-community/netinfo.ts diff --git a/packages/event-processor/__mocks__/@react-native-community/netinfo.ts b/packages/event-processor/__mocks__/@react-native-community/netinfo.ts new file mode 100644 index 000000000..8a0fb2d77 --- /dev/null +++ b/packages/event-processor/__mocks__/@react-native-community/netinfo.ts @@ -0,0 +1,19 @@ +/** + * Copyright 2020, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// TODO: Implement mock class for testing here +export function addEventListener() { + // mock here +} diff --git a/packages/event-processor/__tests__/v1EventProcessor.spec.ts b/packages/event-processor/__tests__/v1EventProcessor.spec.ts index 57bee0866..ddedc58ce 100644 --- a/packages/event-processor/__tests__/v1EventProcessor.spec.ts +++ b/packages/event-processor/__tests__/v1EventProcessor.spec.ts @@ -138,7 +138,7 @@ describe('LogTierV1EventProcessor', () => { const processor = new LogTierV1EventProcessor({ dispatcher: stubDispatcher, flushInterval: 100, - maxQueueSize: 100, + batchSize: 100, }) processor.stop().then(() => { @@ -150,7 +150,7 @@ describe('LogTierV1EventProcessor', () => { const processor = new LogTierV1EventProcessor({ dispatcher: stubDispatcher, flushInterval: 100, - maxQueueSize: 100, + batchSize: 100, }) processor.start() @@ -178,7 +178,7 @@ describe('LogTierV1EventProcessor', () => { const processor = new LogTierV1EventProcessor({ dispatcher: stubDispatcher, flushInterval: 100, - maxQueueSize: 100, + batchSize: 100, }) processor.start() @@ -205,7 +205,7 @@ describe('LogTierV1EventProcessor', () => { const processor = new LogTierV1EventProcessor({ dispatcher: stubDispatcher, flushInterval: 100, - maxQueueSize: 100, + batchSize: 100, }) processor.start() @@ -230,7 +230,7 @@ describe('LogTierV1EventProcessor', () => { const processor = new LogTierV1EventProcessor({ dispatcher, flushInterval: 100, - maxQueueSize: 3, + batchSize: 3, }) processor.start() @@ -267,7 +267,7 @@ describe('LogTierV1EventProcessor', () => { const processor = new LogTierV1EventProcessor({ dispatcher, flushInterval: 100, - maxQueueSize: 2, + batchSize: 2, }) processor.start() @@ -291,13 +291,13 @@ describe('LogTierV1EventProcessor', () => { }) }) - describe('when maxQueueSize = 1', () => { + describe('when batchSize = 1', () => { let processor: EventProcessor beforeEach(() => { processor = new LogTierV1EventProcessor({ dispatcher: stubDispatcher, flushInterval: 100, - maxQueueSize: 1, + batchSize: 1, }) processor.start() }) @@ -319,13 +319,13 @@ describe('LogTierV1EventProcessor', () => { }) }) - describe('when maxQueueSize = 3, flushInterval = 100', () => { + describe('when batchSize = 3, flushInterval = 100', () => { let processor: EventProcessor beforeEach(() => { processor = new LogTierV1EventProcessor({ dispatcher: stubDispatcher, flushInterval: 100, - maxQueueSize: 3, + batchSize: 3, }) processor.start() }) @@ -462,7 +462,7 @@ describe('LogTierV1EventProcessor', () => { const processor = new LogTierV1EventProcessor({ dispatcher, notificationCenter, - maxQueueSize: 1, + batchSize: 1, }) processor.start() @@ -475,12 +475,12 @@ describe('LogTierV1EventProcessor', () => { }) }) - describe('invalid flushInterval or maxQueueSize', () => { + describe('invalid flushInterval or batchSize', () => { it('should ignore a flushInterval of 0 and use the default', () => { const processor = new LogTierV1EventProcessor({ dispatcher: stubDispatcher, flushInterval: 0, - maxQueueSize: 10, + batchSize: 10, }) processor.start() @@ -496,11 +496,11 @@ describe('LogTierV1EventProcessor', () => { }) }) - it('should ignore a maxQueueSize of 0 and use the default', () => { + it('should ignore a batchSize of 0 and use the default', () => { const processor = new LogTierV1EventProcessor({ dispatcher: stubDispatcher, flushInterval: 30000, - maxQueueSize: 0, + batchSize: 0, }) processor.start() diff --git a/packages/event-processor/src/v1/buildEventV1.ts b/packages/event-processor/src/v1/buildEventV1.ts index 29266a8ac..eeb14472c 100644 --- a/packages/event-processor/src/v1/buildEventV1.ts +++ b/packages/event-processor/src/v1/buildEventV1.ts @@ -17,7 +17,6 @@ export type EventV1 = { } type Visitor = { - timestamp: number snapshots: Visitor.Snapshot[] visitor_id: string attributes: Visitor.Attribute[] @@ -161,7 +160,6 @@ function makeDecisionSnapshot(event: ImpressionEvent): Visitor.Snapshot { function makeVisitor(data: ImpressionEvent | ConversionEvent): Visitor { const visitor: Visitor = { - timestamp: data.timestamp, snapshots: [], visitor_id: data.user.id, attributes: [], From 06218828f76868a5be66c6dcc98c5368b8127969 Mon Sep 17 00:00:00 2001 From: Zeeshan Ashraf Date: Tue, 7 Jul 2020 19:46:29 -0700 Subject: [PATCH 07/28] sequences the pending events one after the other --- .../src/reactNativeEventProcessor.ts | 65 ++++++++++--------- 1 file changed, 36 insertions(+), 29 deletions(-) diff --git a/packages/event-processor/src/reactNativeEventProcessor.ts b/packages/event-processor/src/reactNativeEventProcessor.ts index f3388b338..8852a2ee2 100644 --- a/packages/event-processor/src/reactNativeEventProcessor.ts +++ b/packages/event-processor/src/reactNativeEventProcessor.ts @@ -36,6 +36,8 @@ export abstract class AbstractReactNativeEventProcessor extends AbstractEventPro private eventBufferStore: ReactNativeEventBufferStore = new ReactNativeEventBufferStore() private unsubscribeNetInfo: Function private isInternetReachable: boolean = true + private isProcessingPendingEvents: boolean = false + private pendingEventsPromise: Promise | null = null constructor({ dispatcher, @@ -59,7 +61,8 @@ export abstract class AbstractReactNativeEventProcessor extends AbstractEventPro if (!this.isInternetReachable && state.isInternetReachable) { this.isInternetReachable = true - this.processPendingEvents() + // To make sure `eventProcessor.stop()` waits for pending events to completely process + this.requestTracker.trackRequest(this.processPendingEvents()) } }) } @@ -69,7 +72,9 @@ export abstract class AbstractReactNativeEventProcessor extends AbstractEventPro } async drainQueue(buffer: ProcessableEvents[]): Promise { - const pendingEventRequests = await this.processPendingEvents() + const pendingEventsPromise = this.processPendingEvents() + this.requestTracker.trackRequest(pendingEventsPromise) + await pendingEventsPromise const reqPromise = new Promise(async (resolve) => { logger.debug('draining queue with %s events', buffer.length) @@ -86,7 +91,6 @@ export abstract class AbstractReactNativeEventProcessor extends AbstractEventPro // Clear buffer because the buffer has become a formatted event and is already stored in pending cache. this.eventBufferStore.clear() - this.dispatcher.dispatchEvent(formattedEvent, (status: number) => { if (this.isSuccessResponse(status)) { this.pendingEventsStore.remove(cacheKey).then(() => resolve()) @@ -97,35 +101,38 @@ export abstract class AbstractReactNativeEventProcessor extends AbstractEventPro this.sendEventNotification(formattedEvent) }) this.requestTracker.trackRequest(reqPromise) - return Promise.all([ ...pendingEventRequests, reqPromise]).then(() => {}) + return reqPromise } - async processPendingEvents(): Promise[]> { - const formattedEvents: {[key: string]: any} = await this.pendingEventsStore.getEventsMap() - return Object.keys(formattedEvents).map(async (eventKey) => { - const requestPromise = new Promise((resolve) => { - const formattedEvent = formattedEvents[eventKey] - this.dispatcher.dispatchEvent(formattedEvent, (status: number) => { - if (this.isSuccessResponse(status)) { - this.pendingEventsStore.remove(eventKey).then(() => resolve()) - } else { - resolve() - } + async processPendingEvents(): Promise { + // If pending events are already being dispatched, return the same promise + if (this.isProcessingPendingEvents && this.pendingEventsPromise) { + return this.pendingEventsPromise + } + this.pendingEventsPromise = new Promise(async (resolvePendingEventPromise) => { + this.isProcessingPendingEvents = true + const formattedEvents: {[key: string]: any} = await this.pendingEventsStore.getEventsMap() + const eventKeys = Object.keys(formattedEvents) + for (let i = 0; i < eventKeys.length; i++) { + const eventKey = eventKeys[i] + const requestPromise = new Promise((resolve) => { + const formattedEvent = formattedEvents[eventKey] + this.dispatcher.dispatchEvent(formattedEvent, (status: number) => { + if (this.isSuccessResponse(status)) { + this.pendingEventsStore.remove(eventKey).then(() => resolve()) + } else { + resolve() + } + }) + this.sendEventNotification(formattedEvent) }) - this.sendEventNotification(formattedEvent) - }) - - // This is somehow needed to make sure events are dispatched in the correct order. - // The dispatcher always hands over the requests to XMLHttpRequest in correct order - // but for some reason, XMLHttpRequest internally mismanages the order and the events - // are received by server in random order. This peice of code probably pushes each subsequent event's - // processing to next iteration of javascript event loop somehow resulting in the correct predictable - // behavior. This is definitely a kludge which appears to be working consistently as expected - // when tested. I dont have a better logical explanation of this. - await (() => new Promise(resolve => setTimeout(resolve)))() - - return requestPromise + // Waiting for last event to finish before dispatching the new one to ensure sequence + await requestPromise + } + this.isProcessingPendingEvents = false + resolvePendingEventPromise() }) + return this.pendingEventsPromise } process(event: ProcessableEvents): void { @@ -148,6 +155,6 @@ export abstract class AbstractReactNativeEventProcessor extends AbstractEventPro stop(): Promise { this.unsubscribeNetInfo() - return super.stop() + return super.stop() } } From e94ebdc4f46a7e4ca2a81d2b57e2c229671895e2 Mon Sep 17 00:00:00 2001 From: Zeeshan Ashraf Date: Tue, 7 Jul 2020 19:59:38 -0700 Subject: [PATCH 08/28] Sequenced the buffered events after pending events on the start --- .../event-processor/src/reactNativeEventProcessor.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/event-processor/src/reactNativeEventProcessor.ts b/packages/event-processor/src/reactNativeEventProcessor.ts index 8852a2ee2..e4f584c05 100644 --- a/packages/event-processor/src/reactNativeEventProcessor.ts +++ b/packages/event-processor/src/reactNativeEventProcessor.ts @@ -143,13 +143,12 @@ export abstract class AbstractReactNativeEventProcessor extends AbstractEventPro start(): void { super.start() - // Dispatch all the formatted pending events right away - this.processPendingEvents() - - // Process individual events pending from the buffer. - this.eventBufferStore.getAll().then((events: ProcessableEvents[]) => { - events.forEach((event: ProcessableEvents) => this.process(event)) + this.processPendingEvents().then(() => { + // Process individual events pending from the buffer. + this.eventBufferStore.getAll().then((events: ProcessableEvents[]) => { + events.forEach((event: ProcessableEvents) => this.process(event)) + }) }) } From 35b5d4f0e5dc78fc4a6ad637a67f0be0be2100e0 Mon Sep 17 00:00:00 2001 From: Zeeshan Ashraf Date: Wed, 8 Jul 2020 17:29:18 -0700 Subject: [PATCH 09/28] added some extra checks to fix the behavior of stop --- .../src/reactNativeEventProcessor.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/packages/event-processor/src/reactNativeEventProcessor.ts b/packages/event-processor/src/reactNativeEventProcessor.ts index e4f584c05..719e5876e 100644 --- a/packages/event-processor/src/reactNativeEventProcessor.ts +++ b/packages/event-processor/src/reactNativeEventProcessor.ts @@ -39,6 +39,9 @@ export abstract class AbstractReactNativeEventProcessor extends AbstractEventPro private isProcessingPendingEvents: boolean = false private pendingEventsPromise: Promise | null = null + // Tracks the events which are being dispatched to prevent from dispatching twice. + private eventsInProgress: {} = {} + constructor({ dispatcher, flushInterval = 30000, @@ -82,16 +85,19 @@ export abstract class AbstractReactNativeEventProcessor extends AbstractEventPro resolve() return } - + const formattedEvent = this.formatEvents(buffer) const cacheKey = generateUUID() + this.eventsInProgress[cacheKey] = true // Store formatted event before dispatching. await this.pendingEventsStore.set(cacheKey, formattedEvent) // Clear buffer because the buffer has become a formatted event and is already stored in pending cache. this.eventBufferStore.clear() + this.dispatcher.dispatchEvent(formattedEvent, (status: number) => { + delete this.eventsInProgress[cacheKey] if (this.isSuccessResponse(status)) { this.pendingEventsStore.remove(cacheKey).then(() => resolve()) } else { @@ -115,9 +121,14 @@ export abstract class AbstractReactNativeEventProcessor extends AbstractEventPro const eventKeys = Object.keys(formattedEvents) for (let i = 0; i < eventKeys.length; i++) { const eventKey = eventKeys[i] + if (this.eventsInProgress[eventKey]) { + continue + } + this.eventsInProgress[eventKey] = true const requestPromise = new Promise((resolve) => { const formattedEvent = formattedEvents[eventKey] this.dispatcher.dispatchEvent(formattedEvent, (status: number) => { + delete this.eventsInProgress[eventKey] if (this.isSuccessResponse(status)) { this.pendingEventsStore.remove(eventKey).then(() => resolve()) } else { @@ -134,7 +145,7 @@ export abstract class AbstractReactNativeEventProcessor extends AbstractEventPro }) return this.pendingEventsPromise } - + process(event: ProcessableEvents): void { // Adding events to buffer store. If app closes before dispatch, we can reprocess next time the app initializes this.eventBufferStore.add(event) From 7db47a66eccd5763a5efd31bf4407d6aec8987e1 Mon Sep 17 00:00:00 2001 From: Zeeshan Ashraf Date: Wed, 8 Jul 2020 20:53:16 -0700 Subject: [PATCH 10/28] modified the resolvable promise to make it work with jest --- .../event-processor/src/reactNativeEventsStore.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/packages/event-processor/src/reactNativeEventsStore.ts b/packages/event-processor/src/reactNativeEventsStore.ts index a2b81d2d9..99fb0e90e 100644 --- a/packages/event-processor/src/reactNativeEventsStore.ts +++ b/packages/event-processor/src/reactNativeEventsStore.ts @@ -105,7 +105,7 @@ class Synchronizer { if (this.lockPromises.length === 1) { return } - await this.lockPromises[this.lockPromises.length - 2] + await this.lockPromises[this.lockPromises.length - 2].getPromise() } public releaseLock(): void { @@ -119,18 +119,21 @@ class Synchronizer { // A Resolvable process to support synchornization block -export class ResolvablePromise extends Promise { +export class ResolvablePromise { private resolver: any + private promise: Promise constructor() { - var res - super((resolve) => { - res = resolve + this.promise = new Promise((resolve) => { + this.resolver = resolve }) - this.resolver = res } public resolve(): void { this.resolver() } + + public getPromise(): Promise { + return this.promise + } } From d99fb66cbf719cf3d98200dae4b6711ae67ca3ca Mon Sep 17 00:00:00 2001 From: Zeeshan Ashraf Date: Thu, 9 Jul 2020 13:56:28 -0700 Subject: [PATCH 11/28] 1. added await while clearing buffer store 2. moved connectivity listener to start --- .../src/reactNativeEventProcessor.ts | 28 +++++++++---------- .../src/reactNativeEventsStore.ts | 2 +- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/packages/event-processor/src/reactNativeEventProcessor.ts b/packages/event-processor/src/reactNativeEventProcessor.ts index 719e5876e..0eee1442b 100644 --- a/packages/event-processor/src/reactNativeEventProcessor.ts +++ b/packages/event-processor/src/reactNativeEventProcessor.ts @@ -34,7 +34,7 @@ const DEFAULT_MAX_QUEUE_SIZE = 10000 export abstract class AbstractReactNativeEventProcessor extends AbstractEventProcessor { private pendingEventsStore: ReactNativePendingEventsStore private eventBufferStore: ReactNativeEventBufferStore = new ReactNativeEventBufferStore() - private unsubscribeNetInfo: Function + private unsubscribeNetInfo: Function | null = null private isInternetReachable: boolean = true private isProcessingPendingEvents: boolean = false private pendingEventsPromise: Promise | null = null @@ -57,17 +57,6 @@ export abstract class AbstractReactNativeEventProcessor extends AbstractEventPro }) { super({ dispatcher, flushInterval, batchSize, notificationCenter }) this.pendingEventsStore = new ReactNativePendingEventsStore(maxQueueSize) - this.unsubscribeNetInfo = addConnectionListener((state: NetInfoState) => { - if (this.isInternetReachable && !state.isInternetReachable) { - this.isInternetReachable = false - } - - if (!this.isInternetReachable && state.isInternetReachable) { - this.isInternetReachable = true - // To make sure `eventProcessor.stop()` waits for pending events to completely process - this.requestTracker.trackRequest(this.processPendingEvents()) - } - }) } isSuccessResponse(status: number): boolean { @@ -94,7 +83,7 @@ export abstract class AbstractReactNativeEventProcessor extends AbstractEventPro await this.pendingEventsStore.set(cacheKey, formattedEvent) // Clear buffer because the buffer has become a formatted event and is already stored in pending cache. - this.eventBufferStore.clear() + await this.eventBufferStore.clear() this.dispatcher.dispatchEvent(formattedEvent, (status: number) => { delete this.eventsInProgress[cacheKey] @@ -154,6 +143,17 @@ export abstract class AbstractReactNativeEventProcessor extends AbstractEventPro start(): void { super.start() + this.unsubscribeNetInfo = addConnectionListener((state: NetInfoState) => { + if (this.isInternetReachable && !state.isInternetReachable) { + this.isInternetReachable = false + } + + if (!this.isInternetReachable && state.isInternetReachable) { + this.isInternetReachable = true + // To make sure `eventProcessor.stop()` waits for pending events to completely process + this.requestTracker.trackRequest(this.processPendingEvents()) + } + }) // Dispatch all the formatted pending events right away this.processPendingEvents().then(() => { // Process individual events pending from the buffer. @@ -164,7 +164,7 @@ export abstract class AbstractReactNativeEventProcessor extends AbstractEventPro } stop(): Promise { - this.unsubscribeNetInfo() + this.unsubscribeNetInfo && this.unsubscribeNetInfo() return super.stop() } } diff --git a/packages/event-processor/src/reactNativeEventsStore.ts b/packages/event-processor/src/reactNativeEventsStore.ts index 99fb0e90e..c3c40d631 100644 --- a/packages/event-processor/src/reactNativeEventsStore.ts +++ b/packages/event-processor/src/reactNativeEventsStore.ts @@ -89,7 +89,7 @@ export class ReactNativeEventBufferStore { } public async clear(): Promise { - this.cache.remove(this.storageKey) + await this.cache.remove(this.storageKey) } } From 99180246cc691c52a66f6f232c21fc365cd11e59 Mon Sep 17 00:00:00 2001 From: Zeeshan Ashraf Date: Thu, 9 Jul 2020 18:49:47 -0700 Subject: [PATCH 12/28] re factored the hierarchy of event processor classes --- .../event-processor/src/eventDispatcher.ts | 6 +- .../event-processor/src/eventProcessor.ts | 141 ++++++------------ .../event-processor/src/index.react_native.ts | 2 +- packages/event-processor/src/index.ts | 2 +- .../event-processor/src/v1/buildEventV1.ts | 11 ++ .../src/v1/v1EventProcessor.ts | 96 +++++++++--- .../v1ReactNativeEventProcessor.ts} | 64 ++++++-- 7 files changed, 185 insertions(+), 137 deletions(-) rename packages/event-processor/src/{reactNativeEventProcessor.ts => v1/v1ReactNativeEventProcessor.ts} (73%) diff --git a/packages/event-processor/src/eventDispatcher.ts b/packages/event-processor/src/eventDispatcher.ts index c58bf4723..28f9ae07c 100644 --- a/packages/event-processor/src/eventDispatcher.ts +++ b/packages/event-processor/src/eventDispatcher.ts @@ -15,7 +15,11 @@ */ import { EventV1 } from "./v1/buildEventV1"; -export type EventDispatcherCallback = (status: number) => void +export type EventDispatcherResponse = { + statusCode: number +} + +export type EventDispatcherCallback = (response: EventDispatcherResponse) => void export interface EventDispatcher { dispatchEvent(event: EventV1Request, callback: EventDispatcherCallback): void diff --git a/packages/event-processor/src/eventProcessor.ts b/packages/event-processor/src/eventProcessor.ts index 8b22d54b4..aadcc9a69 100644 --- a/packages/event-processor/src/eventProcessor.ts +++ b/packages/event-processor/src/eventProcessor.ts @@ -15,12 +15,14 @@ */ // TODO change this to use Managed from js-sdk-models when available import { Managed } from './managed' -import { ConversionEvent, ImpressionEvent, areEventContextsEqual } from './events' -import { EventDispatcher, EventV1Request } from './eventDispatcher' +import { ConversionEvent, ImpressionEvent } from './events' +import { EventV1Request } from './eventDispatcher' import { EventQueue, DefaultEventQueue, SingleEventQueue } from './eventQueue' import { getLogger } from '@optimizely/js-sdk-logging' import { NOTIFICATION_TYPES, NotificationCenter } from '@optimizely/js-sdk-utils' -import RequestTracker from './requestTracker'; + +const DEFAULT_FLUSH_INTERVAL = 30000 // Unit is ms - default flush interval is 30s +const DEFAULT_BATCH_SIZE = 10 const logger = getLogger('EventProcessor') @@ -32,107 +34,48 @@ export interface EventProcessor extends Managed { process(event: ProcessableEvents): void } -const DEFAULT_FLUSH_INTERVAL = 30000 // Unit is ms - default flush interval is 30s -const DEFAULT_BATCH_SIZE = 10 - -export abstract class AbstractEventProcessor implements EventProcessor { - protected dispatcher: EventDispatcher - protected queue: EventQueue - private notificationCenter?: NotificationCenter - protected requestTracker: RequestTracker - - constructor({ - dispatcher, - flushInterval = 30000, - batchSize = 3000, - notificationCenter, - }: { - dispatcher: EventDispatcher - flushInterval?: number - batchSize?: number - notificationCenter?: NotificationCenter - }) { - this.dispatcher = dispatcher - - if (flushInterval <= 0) { - logger.warn( - `Invalid flushInterval ${flushInterval}, defaulting to ${DEFAULT_FLUSH_INTERVAL}`, - ) - flushInterval = DEFAULT_FLUSH_INTERVAL - } - - batchSize = Math.floor(batchSize) - if (batchSize < 1) { - logger.warn( - `Invalid batchSize ${batchSize}, defaulting to ${DEFAULT_BATCH_SIZE}`, - ) - batchSize = DEFAULT_BATCH_SIZE - } - - batchSize = Math.max(1, batchSize) - if (batchSize > 1) { - this.queue = new DefaultEventQueue({ - flushInterval, - maxQueueSize: batchSize, - sink: buffer => this.drainQueue(buffer), - batchComparator: areEventContextsEqual, - }) - } else { - this.queue = new SingleEventQueue({ - sink: buffer => this.drainQueue(buffer), - }) - } - this.notificationCenter = notificationCenter - - this.requestTracker = new RequestTracker() - } - - drainQueue(buffer: ProcessableEvents[]): Promise { - const reqPromise = new Promise(resolve => { - logger.debug('draining queue with %s events', buffer.length) - - if (buffer.length === 0) { - resolve() - return - } - - const formattedEvent = this.formatEvents(buffer) - this.dispatcher.dispatchEvent(formattedEvent, () => { - resolve() - }) - this.sendEventNotification(formattedEvent) - }) - this.requestTracker.trackRequest(reqPromise) - return reqPromise - } - - protected sendEventNotification(event: EventV1Request): void { - if (this.notificationCenter) { - this.notificationCenter.sendNotifications( - NOTIFICATION_TYPES.LOG_EVENT, - event, - ) - } +export function validateAndGetFlushInterval(flushInterval: number): number { + if (flushInterval <= 0) { + logger.warn( + `Invalid flushInterval ${flushInterval}, defaulting to ${DEFAULT_FLUSH_INTERVAL}`, + ) + flushInterval = DEFAULT_FLUSH_INTERVAL } + return flushInterval +} - process(event: ProcessableEvents): void { - this.queue.enqueue(event) +export function validateAndGetBatchSize(batchSize: number): number { + batchSize = Math.floor(batchSize) + if (batchSize < 1) { + logger.warn( + `Invalid batchSize ${batchSize}, defaulting to ${DEFAULT_BATCH_SIZE}`, + ) + batchSize = DEFAULT_BATCH_SIZE } + batchSize = Math.max(1, batchSize) + return batchSize +} - stop(): Promise { - // swallow - an error stopping this queue shouldn't prevent this from stopping - try { - this.queue.stop() - return this.requestTracker.onRequestsComplete() - } catch (e) { - logger.error('Error stopping EventProcessor: "%s"', e.message, e) - } - return Promise.resolve() +export function getQueue(batchSize: number, flushInterval: number, sink: any, batchComparator: any): EventQueue { + let queue: EventQueue + if (batchSize > 1) { + queue = new DefaultEventQueue({ + flushInterval, + maxQueueSize: batchSize, + sink, + batchComparator, + }) + } else { + queue = new SingleEventQueue({ sink }) } + return queue +} - start(): void { - this.queue.start() +export function sendEventNotification(notificationCenter: NotificationCenter | undefined, event: EventV1Request): void { + if (notificationCenter) { + notificationCenter.sendNotifications( + NOTIFICATION_TYPES.LOG_EVENT, + event, + ) } - - protected abstract formatEvents(events: ProcessableEvents[]): EventV1Request } diff --git a/packages/event-processor/src/index.react_native.ts b/packages/event-processor/src/index.react_native.ts index ced080f4f..33b551418 100644 --- a/packages/event-processor/src/index.react_native.ts +++ b/packages/event-processor/src/index.react_native.ts @@ -20,4 +20,4 @@ export * from './eventDispatcher' export * from './managed' export * from './pendingEventsDispatcher' export * from './v1/buildEventV1' -export { LogTierV1ReactNativeEventProcessor as LogTierV1EventProcessor } from './v1/v1EventProcessor' +export * from './v1/v1ReactNativeEventProcessor' diff --git a/packages/event-processor/src/index.ts b/packages/event-processor/src/index.ts index 81242ee32..d2f916615 100644 --- a/packages/event-processor/src/index.ts +++ b/packages/event-processor/src/index.ts @@ -20,4 +20,4 @@ export * from './eventDispatcher' export * from './managed' export * from './pendingEventsDispatcher' export * from './v1/buildEventV1' -export { LogTierV1EventProcessor } from './v1/v1EventProcessor' +export * from './v1/v1EventProcessor' diff --git a/packages/event-processor/src/v1/buildEventV1.ts b/packages/event-processor/src/v1/buildEventV1.ts index eeb14472c..8439bc6a7 100644 --- a/packages/event-processor/src/v1/buildEventV1.ts +++ b/packages/event-processor/src/v1/buildEventV1.ts @@ -1,5 +1,6 @@ import { EventTags, ConversionEvent, ImpressionEvent, VisitorAttribute } from '../events' import { ProcessableEvents } from '../eventProcessor' +import { EventV1Request } from '../eventDispatcher' const ACTIVATE_EVENT_KEY = 'campaign_activated' const CUSTOM_ATTRIBUTE_FEATURE_TYPE = 'custom' @@ -227,3 +228,13 @@ export function buildConversionEventV1(data: ConversionEvent): EventV1 { visitors: [visitor], } } + +export function formatEvents(events: ProcessableEvents[]): EventV1Request { + return { + //url: 'https://logx.optimizely.com/v1/events', + //url: 'http://localhost:4321/', + url: 'http://10.0.0.175:4321/', + httpVerb: 'POST', + params: makeBatchedEventV1(events), + } +} diff --git a/packages/event-processor/src/v1/v1EventProcessor.ts b/packages/event-processor/src/v1/v1EventProcessor.ts index d5bc91edd..61ee29b9b 100644 --- a/packages/event-processor/src/v1/v1EventProcessor.ts +++ b/packages/event-processor/src/v1/v1EventProcessor.ts @@ -13,28 +13,86 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { getLogger } from '@optimizely/js-sdk-logging' +import { NotificationCenter } from '@optimizely/js-sdk-utils' -import { AbstractEventProcessor, ProcessableEvents } from '../eventProcessor' -import { EventV1Request } from '../eventDispatcher' -import { makeBatchedEventV1 } from './buildEventV1' -import { AbstractReactNativeEventProcessor } from '../reactNativeEventProcessor' - -export class LogTierV1EventProcessor extends AbstractEventProcessor { - protected formatEvents(events: ProcessableEvents[]): EventV1Request { - return { - url: 'https://logx.optimizely.com/v1/events', - httpVerb: 'POST', - params: makeBatchedEventV1(events), - } +import { EventDispatcher } from '../eventDispatcher' +import { + getQueue, + EventProcessor, + ProcessableEvents, + sendEventNotification, + validateAndGetBatchSize, + validateAndGetFlushInterval, +} from '../eventProcessor' +import { EventQueue } from '../eventQueue' +import RequestTracker from '../requestTracker' +import { areEventContextsEqual } from '../events' +import { formatEvents } from './buildEventV1' + +const logger = getLogger('LogTierV1EventProcessor') + +export class LogTierV1EventProcessor implements EventProcessor { + protected dispatcher: EventDispatcher + protected queue: EventQueue + private notificationCenter?: NotificationCenter + protected requestTracker: RequestTracker + + constructor({ + dispatcher, + flushInterval = 30000, + batchSize = 3000, + notificationCenter, + }: { + dispatcher: EventDispatcher + flushInterval?: number + batchSize?: number + notificationCenter?: NotificationCenter + }) { + this.dispatcher = dispatcher + this.notificationCenter = notificationCenter + this.requestTracker = new RequestTracker() + + flushInterval = validateAndGetFlushInterval(flushInterval) + batchSize = validateAndGetBatchSize(batchSize) + this.queue = getQueue(batchSize, flushInterval, this.drainQueue, areEventContextsEqual) } -} -export class LogTierV1ReactNativeEventProcessor extends AbstractReactNativeEventProcessor { - protected formatEvents(events: ProcessableEvents[]): EventV1Request { - return { - url: 'https://logx.optimizely.com/v1/events', - httpVerb: 'POST', - params: makeBatchedEventV1(events), + drainQueue(buffer: ProcessableEvents[]): Promise { + const reqPromise = new Promise(resolve => { + logger.debug('draining queue with %s events', buffer.length) + + if (buffer.length === 0) { + resolve() + return + } + + const formattedEvent = formatEvents(buffer) + this.dispatcher.dispatchEvent(formattedEvent, () => { + resolve() + }) + sendEventNotification(this.notificationCenter, formattedEvent) + }) + this.requestTracker.trackRequest(reqPromise) + return reqPromise + } + + process(event: ProcessableEvents): void { + this.queue.enqueue(event) + } + + stop(): Promise { + // swallow - an error stopping this queue shouldn't prevent this from stopping + try { + this.queue.stop() + return this.requestTracker.onRequestsComplete() + } catch (e) { + logger.error('Error stopping EventProcessor: "%s"', e.message, e) } + return Promise.resolve() + } + + start(): void { + this.queue.start() } } diff --git a/packages/event-processor/src/reactNativeEventProcessor.ts b/packages/event-processor/src/v1/v1ReactNativeEventProcessor.ts similarity index 73% rename from packages/event-processor/src/reactNativeEventProcessor.ts rename to packages/event-processor/src/v1/v1ReactNativeEventProcessor.ts index 0eee1442b..6bd65394c 100644 --- a/packages/event-processor/src/reactNativeEventProcessor.ts +++ b/packages/event-processor/src/v1/v1ReactNativeEventProcessor.ts @@ -17,21 +17,40 @@ import { generateUUID, NotificationCenter, } from '@optimizely/js-sdk-utils' -import { getLogger } from '@optimizely/js-sdk-logging' import { NetInfoState, addEventListener as addConnectionListener, } from "@react-native-community/netinfo" +import { getLogger } from '@optimizely/js-sdk-logging' -import { EventDispatcher } from './eventDispatcher' -import { ProcessableEvents, AbstractEventProcessor } from "./eventProcessor" -import { ReactNativePendingEventsStore, ReactNativeEventBufferStore } from './reactNativeEventsStore' +import { + getQueue, + EventProcessor, + ProcessableEvents, + sendEventNotification, + validateAndGetBatchSize, + validateAndGetFlushInterval, +} from "../eventProcessor" +import { + ReactNativeEventBufferStore, + ReactNativePendingEventsStore, +} from '../reactNativeEventsStore' +import { EventQueue } from '../eventQueue' +import RequestTracker from '../requestTracker' +import { areEventContextsEqual } from '../events' +import { formatEvents } from './buildEventV1' +import { EventDispatcher, EventDispatcherResponse } from '../eventDispatcher' const logger = getLogger('ReactNativeEventProcessor') const DEFAULT_MAX_QUEUE_SIZE = 10000 -export abstract class AbstractReactNativeEventProcessor extends AbstractEventProcessor { +export abstract class LogTierV1EventProcessor implements EventProcessor { + protected dispatcher: EventDispatcher + protected queue: EventQueue + private notificationCenter?: NotificationCenter + protected requestTracker: RequestTracker + private pendingEventsStore: ReactNativePendingEventsStore private eventBufferStore: ReactNativeEventBufferStore = new ReactNativeEventBufferStore() private unsubscribeNetInfo: Function | null = null @@ -55,7 +74,13 @@ export abstract class AbstractReactNativeEventProcessor extends AbstractEventPro maxQueueSize?: number notificationCenter?: NotificationCenter }) { - super({ dispatcher, flushInterval, batchSize, notificationCenter }) + this.dispatcher = dispatcher + this.notificationCenter = notificationCenter + this.requestTracker = new RequestTracker() + + flushInterval = validateAndGetFlushInterval(flushInterval) + batchSize = validateAndGetBatchSize(batchSize) + this.queue = getQueue(batchSize, flushInterval, this.drainQueue.bind(this), areEventContextsEqual) this.pendingEventsStore = new ReactNativePendingEventsStore(maxQueueSize) } @@ -75,7 +100,7 @@ export abstract class AbstractReactNativeEventProcessor extends AbstractEventPro return } - const formattedEvent = this.formatEvents(buffer) + const formattedEvent = formatEvents(buffer) const cacheKey = generateUUID() this.eventsInProgress[cacheKey] = true @@ -85,15 +110,15 @@ export abstract class AbstractReactNativeEventProcessor extends AbstractEventPro // Clear buffer because the buffer has become a formatted event and is already stored in pending cache. await this.eventBufferStore.clear() - this.dispatcher.dispatchEvent(formattedEvent, (status: number) => { + this.dispatcher.dispatchEvent(formattedEvent, ({ statusCode }: EventDispatcherResponse) => { delete this.eventsInProgress[cacheKey] - if (this.isSuccessResponse(status)) { + if (this.isSuccessResponse(statusCode)) { this.pendingEventsStore.remove(cacheKey).then(() => resolve()) } else { resolve() } }) - this.sendEventNotification(formattedEvent) + sendEventNotification(this.notificationCenter, formattedEvent) }) this.requestTracker.trackRequest(reqPromise) return reqPromise @@ -116,15 +141,15 @@ export abstract class AbstractReactNativeEventProcessor extends AbstractEventPro this.eventsInProgress[eventKey] = true const requestPromise = new Promise((resolve) => { const formattedEvent = formattedEvents[eventKey] - this.dispatcher.dispatchEvent(formattedEvent, (status: number) => { + this.dispatcher.dispatchEvent(formattedEvent, ({ statusCode }: EventDispatcherResponse) => { delete this.eventsInProgress[eventKey] - if (this.isSuccessResponse(status)) { + if (this.isSuccessResponse(statusCode)) { this.pendingEventsStore.remove(eventKey).then(() => resolve()) } else { resolve() } }) - this.sendEventNotification(formattedEvent) + sendEventNotification(this.notificationCenter, formattedEvent) }) // Waiting for last event to finish before dispatching the new one to ensure sequence await requestPromise @@ -138,11 +163,11 @@ export abstract class AbstractReactNativeEventProcessor extends AbstractEventPro process(event: ProcessableEvents): void { // Adding events to buffer store. If app closes before dispatch, we can reprocess next time the app initializes this.eventBufferStore.add(event) - super.process(event) + this.queue.enqueue(event) } start(): void { - super.start() + this.queue.start() this.unsubscribeNetInfo = addConnectionListener((state: NetInfoState) => { if (this.isInternetReachable && !state.isInternetReachable) { this.isInternetReachable = false @@ -165,6 +190,13 @@ export abstract class AbstractReactNativeEventProcessor extends AbstractEventPro stop(): Promise { this.unsubscribeNetInfo && this.unsubscribeNetInfo() - return super.stop() + // swallow - an error stopping this queue shouldn't prevent this from stopping + try { + this.queue.stop() + return this.requestTracker.onRequestsComplete() + } catch (e) { + logger.error('Error stopping EventProcessor: "%s"', e.message, e) + } + return Promise.resolve() } } From 8018de07fdcfaa1a396c610f8eac90143e3fcd7a Mon Sep 17 00:00:00 2001 From: Zeeshan Ashraf Date: Fri, 10 Jul 2020 12:25:13 -0700 Subject: [PATCH 13/28] refactored how pending events are dispatched. simplified the whole event processor implementation --- .../src/v1/v1ReactNativeEventProcessor.ts | 182 +++++++++--------- 1 file changed, 89 insertions(+), 93 deletions(-) diff --git a/packages/event-processor/src/v1/v1ReactNativeEventProcessor.ts b/packages/event-processor/src/v1/v1ReactNativeEventProcessor.ts index 6bd65394c..54e32064c 100644 --- a/packages/event-processor/src/v1/v1ReactNativeEventProcessor.ts +++ b/packages/event-processor/src/v1/v1ReactNativeEventProcessor.ts @@ -16,6 +16,7 @@ import { generateUUID, NotificationCenter, + objectEntries, } from '@optimizely/js-sdk-utils' import { NetInfoState, @@ -39,28 +40,29 @@ import { EventQueue } from '../eventQueue' import RequestTracker from '../requestTracker' import { areEventContextsEqual } from '../events' import { formatEvents } from './buildEventV1' -import { EventDispatcher, EventDispatcherResponse } from '../eventDispatcher' +import { + EventV1Request, + EventDispatcher, + EventDispatcherResponse, +} from '../eventDispatcher' const logger = getLogger('ReactNativeEventProcessor') const DEFAULT_MAX_QUEUE_SIZE = 10000 export abstract class LogTierV1EventProcessor implements EventProcessor { - protected dispatcher: EventDispatcher - protected queue: EventQueue + private dispatcher: EventDispatcher + private queue: EventQueue private notificationCenter?: NotificationCenter - protected requestTracker: RequestTracker - - private pendingEventsStore: ReactNativePendingEventsStore - private eventBufferStore: ReactNativeEventBufferStore = new ReactNativeEventBufferStore() + private requestTracker: RequestTracker + private unsubscribeNetInfo: Function | null = null private isInternetReachable: boolean = true - private isProcessingPendingEvents: boolean = false private pendingEventsPromise: Promise | null = null - - // Tracks the events which are being dispatched to prevent from dispatching twice. - private eventsInProgress: {} = {} - + + private pendingEventsStore: ReactNativePendingEventsStore + private eventBufferStore: ReactNativeEventBufferStore = new ReactNativeEventBufferStore() + constructor({ dispatcher, flushInterval = 30000, @@ -84,80 +86,96 @@ export abstract class LogTierV1EventProcessor implements EventProcessor { this.pendingEventsStore = new ReactNativePendingEventsStore(maxQueueSize) } + start(): void { + this.queue.start() + this.unsubscribeNetInfo = addConnectionListener(async (state: NetInfoState) => { + if (this.isInternetReachable && !state.isInternetReachable) { + this.isInternetReachable = false + return + } + + if (!this.isInternetReachable && state.isInternetReachable) { + this.isInternetReachable = true + // To make sure `eventProcessor.stop()` waits for pending events to completely process + this.processPendingEvents() + } + }) + // Dispatch all the formatted pending events right away + this.processPendingEvents().then(() => { + this.pendingEventsPromise = null + // Process individual events pending from the buffer. + this.eventBufferStore.getAll().then((events: ProcessableEvents[]) => { + events.forEach((event: ProcessableEvents) => this.process(event)) + }) + }) + } + isSuccessResponse(status: number): boolean { return status >= 200 && status < 400 } - async drainQueue(buffer: ProcessableEvents[]): Promise { - const pendingEventsPromise = this.processPendingEvents() - this.requestTracker.trackRequest(pendingEventsPromise) - await pendingEventsPromise - const reqPromise = new Promise(async (resolve) => { - logger.debug('draining queue with %s events', buffer.length) + async drainQueue(buffer: ProcessableEvents[]): Promise { + await this.processPendingEvents() - if (buffer.length === 0) { - resolve() - return - } + logger.debug('draining queue with %s events', buffer.length) - const formattedEvent = formatEvents(buffer) - const cacheKey = generateUUID() - this.eventsInProgress[cacheKey] = true + if (buffer.length === 0) { + return + } - // Store formatted event before dispatching. - await this.pendingEventsStore.set(cacheKey, formattedEvent) - // Clear buffer because the buffer has become a formatted event and is already stored in pending cache. - await this.eventBufferStore.clear() + const eventCacheKey = generateUUID() + const formattedEvent = formatEvents(buffer) - this.dispatcher.dispatchEvent(formattedEvent, ({ statusCode }: EventDispatcherResponse) => { - delete this.eventsInProgress[cacheKey] - if (this.isSuccessResponse(statusCode)) { - this.pendingEventsStore.remove(cacheKey).then(() => resolve()) - } else { - resolve() - } - }) - sendEventNotification(this.notificationCenter, formattedEvent) - }) - this.requestTracker.trackRequest(reqPromise) - return reqPromise + // Store formatted event before dispatching. + await this.pendingEventsStore.set(eventCacheKey, formattedEvent) + + // Clear buffer because the buffer has become a formatted event and is already stored in pending cache. + await this.eventBufferStore.clear() + + return this.dispatchEvent(eventCacheKey, formattedEvent) } async processPendingEvents(): Promise { - // If pending events are already being dispatched, return the same promise - if (this.isProcessingPendingEvents && this.pendingEventsPromise) { - return this.pendingEventsPromise + // If pending events are already being dispatched, wait for the promise to complete and then return + // This is to prevent processing events twice if pending events are tried simultenously from two flows. + // For example when there were pending events and device gained back connectivity. + // At the same time, batch is complete and drainQueue attempts to process pending events. + // It will then get the same pendingEventsPromise and wait for it so that the new events are + // successfully sequenced after the pending events are dispatched. + if (this.pendingEventsPromise) { + await this.pendingEventsPromise + return + } + + this.pendingEventsPromise = this.getPendingEventsPromise() + await this.pendingEventsPromise + + // Clear pending events promise because its fulfilled now + this.pendingEventsPromise = null + } + + async getPendingEventsPromise(): Promise{ + const formattedEvents: {[key: string]: any} = await this.pendingEventsStore.getEventsMap() + const eventEntries = objectEntries(formattedEvents) + for (let i = 0; i < eventEntries.length; i++) { + const [eventKey, event] = eventEntries[i] + await this.dispatchEvent(eventKey, event) } - this.pendingEventsPromise = new Promise(async (resolvePendingEventPromise) => { - this.isProcessingPendingEvents = true - const formattedEvents: {[key: string]: any} = await this.pendingEventsStore.getEventsMap() - const eventKeys = Object.keys(formattedEvents) - for (let i = 0; i < eventKeys.length; i++) { - const eventKey = eventKeys[i] - if (this.eventsInProgress[eventKey]) { - continue + } + + async dispatchEvent(eventCacheKey: string, event: EventV1Request): Promise { + const requestPromise = new Promise((resolve) => { + this.dispatcher.dispatchEvent(event, async ({ statusCode }: EventDispatcherResponse) => { + if (this.isSuccessResponse(statusCode)) { + await this.pendingEventsStore.remove(eventCacheKey) } - this.eventsInProgress[eventKey] = true - const requestPromise = new Promise((resolve) => { - const formattedEvent = formattedEvents[eventKey] - this.dispatcher.dispatchEvent(formattedEvent, ({ statusCode }: EventDispatcherResponse) => { - delete this.eventsInProgress[eventKey] - if (this.isSuccessResponse(statusCode)) { - this.pendingEventsStore.remove(eventKey).then(() => resolve()) - } else { - resolve() - } - }) - sendEventNotification(this.notificationCenter, formattedEvent) - }) - // Waiting for last event to finish before dispatching the new one to ensure sequence - await requestPromise - } - this.isProcessingPendingEvents = false - resolvePendingEventPromise() + resolve() + }) + sendEventNotification(this.notificationCenter, event) }) - return this.pendingEventsPromise + this.requestTracker.trackRequest(requestPromise) + return requestPromise } process(event: ProcessableEvents): void { @@ -166,28 +184,6 @@ export abstract class LogTierV1EventProcessor implements EventProcessor { this.queue.enqueue(event) } - start(): void { - this.queue.start() - this.unsubscribeNetInfo = addConnectionListener((state: NetInfoState) => { - if (this.isInternetReachable && !state.isInternetReachable) { - this.isInternetReachable = false - } - - if (!this.isInternetReachable && state.isInternetReachable) { - this.isInternetReachable = true - // To make sure `eventProcessor.stop()` waits for pending events to completely process - this.requestTracker.trackRequest(this.processPendingEvents()) - } - }) - // Dispatch all the formatted pending events right away - this.processPendingEvents().then(() => { - // Process individual events pending from the buffer. - this.eventBufferStore.getAll().then((events: ProcessableEvents[]) => { - events.forEach((event: ProcessableEvents) => this.process(event)) - }) - }) - } - stop(): Promise { this.unsubscribeNetInfo && this.unsubscribeNetInfo() // swallow - an error stopping this queue shouldn't prevent this from stopping From 134c5dfb8448690073299d9b8d85dc82bb31ed3a Mon Sep 17 00:00:00 2001 From: Zeeshan Ashraf Date: Fri, 10 Jul 2020 12:35:45 -0700 Subject: [PATCH 14/28] removed localhost url --- packages/event-processor/src/v1/buildEventV1.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/event-processor/src/v1/buildEventV1.ts b/packages/event-processor/src/v1/buildEventV1.ts index 8439bc6a7..8bfd24578 100644 --- a/packages/event-processor/src/v1/buildEventV1.ts +++ b/packages/event-processor/src/v1/buildEventV1.ts @@ -231,9 +231,7 @@ export function buildConversionEventV1(data: ConversionEvent): EventV1 { export function formatEvents(events: ProcessableEvents[]): EventV1Request { return { - //url: 'https://logx.optimizely.com/v1/events', - //url: 'http://localhost:4321/', - url: 'http://10.0.0.175:4321/', + url: 'https://logx.optimizely.com/v1/events', httpVerb: 'POST', params: makeBatchedEventV1(events), } From 8467a91a2157b4cd9ce07b7c6e75e3d080621418 Mon Sep 17 00:00:00 2001 From: Zeeshan Ashraf Date: Fri, 10 Jul 2020 12:46:26 -0700 Subject: [PATCH 15/28] fixed existing unit tests --- .../__tests__/v1EventProcessor.spec.ts | 12 ++++++------ packages/event-processor/src/v1/v1EventProcessor.ts | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/event-processor/__tests__/v1EventProcessor.spec.ts b/packages/event-processor/__tests__/v1EventProcessor.spec.ts index ddedc58ce..236597bf3 100644 --- a/packages/event-processor/__tests__/v1EventProcessor.spec.ts +++ b/packages/event-processor/__tests__/v1EventProcessor.spec.ts @@ -114,7 +114,7 @@ describe('LogTierV1EventProcessor', () => { stubDispatcher = { dispatchEvent(event: EventV1Request, callback: EventDispatcherCallback): void { dispatchStub(event) - callback(200) + callback({ statusCode: 200 }) }, } }) @@ -161,7 +161,7 @@ describe('LogTierV1EventProcessor', () => { done() }) - localCallback(200) + localCallback({ statusCode: 200 }) }) it('should return a promise that is resolved when the dispatcher callback returns a 400 response', done => { @@ -198,7 +198,7 @@ describe('LogTierV1EventProcessor', () => { stubDispatcher = { dispatchEvent(event: EventV1Request, callback: EventDispatcherCallback): void { dispatchStub(event) - callback(200) + callback({ statusCode: 200 }) }, } @@ -224,7 +224,7 @@ describe('LogTierV1EventProcessor', () => { it('should stop accepting events after stop is called', () => { const dispatcher = { dispatchEvent: jest.fn((event: EventV1Request, callback: EventDispatcherCallback) => { - setTimeout(() => callback(204), 0) + setTimeout(() => callback({ statusCode: 204 }), 0) }) } const processor = new LogTierV1EventProcessor({ @@ -282,10 +282,10 @@ describe('LogTierV1EventProcessor', () => { }) expect(stopPromiseResolved).toBe(false) - dispatchCbs[0](204) + dispatchCbs[0]({ statusCode: 204 }) jest.advanceTimersByTime(100) expect(stopPromiseResolved).toBe(false) - dispatchCbs[1](204) + dispatchCbs[1]({ statusCode: 204 }) await stopPromise expect(stopPromiseResolved).toBe(true) }) diff --git a/packages/event-processor/src/v1/v1EventProcessor.ts b/packages/event-processor/src/v1/v1EventProcessor.ts index 61ee29b9b..09bc54a61 100644 --- a/packages/event-processor/src/v1/v1EventProcessor.ts +++ b/packages/event-processor/src/v1/v1EventProcessor.ts @@ -55,7 +55,7 @@ export class LogTierV1EventProcessor implements EventProcessor { flushInterval = validateAndGetFlushInterval(flushInterval) batchSize = validateAndGetBatchSize(batchSize) - this.queue = getQueue(batchSize, flushInterval, this.drainQueue, areEventContextsEqual) + this.queue = getQueue(batchSize, flushInterval, this.drainQueue.bind(this), areEventContextsEqual) } drainQueue(buffer: ProcessableEvents[]): Promise { From a49a6fa0ffe5162d6786e116e01e9d64482e0d65 Mon Sep 17 00:00:00 2001 From: Zeeshan Ashraf Date: Fri, 10 Jul 2020 12:59:23 -0700 Subject: [PATCH 16/28] renamed file to reflect its react native version of event processor --- packages/event-processor/src/index.react_native.ts | 2 +- ...NativeEventProcessor.ts => v1EventProcessor.react_native.ts} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename packages/event-processor/src/v1/{v1ReactNativeEventProcessor.ts => v1EventProcessor.react_native.ts} (100%) diff --git a/packages/event-processor/src/index.react_native.ts b/packages/event-processor/src/index.react_native.ts index 33b551418..2e3ac34e4 100644 --- a/packages/event-processor/src/index.react_native.ts +++ b/packages/event-processor/src/index.react_native.ts @@ -20,4 +20,4 @@ export * from './eventDispatcher' export * from './managed' export * from './pendingEventsDispatcher' export * from './v1/buildEventV1' -export * from './v1/v1ReactNativeEventProcessor' +export * from './v1/v1EventProcessor.react_native' diff --git a/packages/event-processor/src/v1/v1ReactNativeEventProcessor.ts b/packages/event-processor/src/v1/v1EventProcessor.react_native.ts similarity index 100% rename from packages/event-processor/src/v1/v1ReactNativeEventProcessor.ts rename to packages/event-processor/src/v1/v1EventProcessor.react_native.ts From a19f66f2ddd5e2a71a716cdcd4296a2bbaa065a3 Mon Sep 17 00:00:00 2001 From: Zeeshan Ashraf Date: Mon, 13 Jul 2020 09:03:34 -0700 Subject: [PATCH 17/28] simplified synchronizer --- .../src/reactNativeEventsStore.ts | 35 +++++-------------- 1 file changed, 9 insertions(+), 26 deletions(-) diff --git a/packages/event-processor/src/reactNativeEventsStore.ts b/packages/event-processor/src/reactNativeEventsStore.ts index c3c40d631..e06b8dd0b 100644 --- a/packages/event-processor/src/reactNativeEventsStore.ts +++ b/packages/event-processor/src/reactNativeEventsStore.ts @@ -98,42 +98,25 @@ export class ReactNativeEventBufferStore { // for ex. Req 1 gets the map. Req 2 gets the map. Req 1 sets the map. Req 2 sets the map. The map now loses item from Req 1. // This synchronizer makes sure the operations are atomic using promises. class Synchronizer { - private lockPromises: ResolvablePromise[] = [] + private lockPromises: Promise[] = [] + private resolvers: any[] = [] + // Adds a promise to the existing list and returns the promise so that the code block can wait for its turn public async getLock(): Promise { - this.lockPromises.push(new ResolvablePromise()) + this.lockPromises.push(new Promise(resolve => this.resolvers.push(resolve))) if (this.lockPromises.length === 1) { return } - await this.lockPromises[this.lockPromises.length - 2].getPromise() + await this.lockPromises[this.lockPromises.length - 2] } + // Resolves first promise in the array so that the code block waiting on the first promise can continue execution public releaseLock(): void { if (this.lockPromises.length > 0) { - const promise = this.lockPromises.shift() - promise && promise.resolve() + this.lockPromises.shift() + const resolver = this.resolvers.shift() + resolver() return } - } -} - - -// A Resolvable process to support synchornization block -export class ResolvablePromise { - private resolver: any - private promise: Promise - - constructor() { - this.promise = new Promise((resolve) => { - this.resolver = resolve - }) - } - - public resolve(): void { - this.resolver() - } - - public getPromise(): Promise { - return this.promise } } From bb7024ef569e5f47dd78bfa5a9299e8241ec8855 Mon Sep 17 00:00:00 2001 From: Zeeshan Ashraf Date: Mon, 13 Jul 2020 13:16:38 -0700 Subject: [PATCH 18/28] using only one store for both pending events and events buffer --- .../event-processor/src/eventProcessor.ts | 12 +-- .../src/reactNativeEventsStore.ts | 73 +++++-------- .../event-processor/src/v1/buildEventV1.ts | 8 +- .../src/v1/v1EventProcessor.react_native.ts | 101 +++++++++--------- .../src/v1/v1EventProcessor.ts | 12 +-- 5 files changed, 94 insertions(+), 112 deletions(-) diff --git a/packages/event-processor/src/eventProcessor.ts b/packages/event-processor/src/eventProcessor.ts index aadcc9a69..db2861dbc 100644 --- a/packages/event-processor/src/eventProcessor.ts +++ b/packages/event-processor/src/eventProcessor.ts @@ -26,12 +26,12 @@ const DEFAULT_BATCH_SIZE = 10 const logger = getLogger('EventProcessor') -export type ProcessableEvents = ConversionEvent | ImpressionEvent +export type ProcessableEvent = ConversionEvent | ImpressionEvent -export type EventDispatchResult = { result: boolean; event: ProcessableEvents } +export type EventDispatchResult = { result: boolean; event: ProcessableEvent } export interface EventProcessor extends Managed { - process(event: ProcessableEvents): void + process(event: ProcessableEvent): void } export function validateAndGetFlushInterval(flushInterval: number): number { @@ -56,10 +56,10 @@ export function validateAndGetBatchSize(batchSize: number): number { return batchSize } -export function getQueue(batchSize: number, flushInterval: number, sink: any, batchComparator: any): EventQueue { - let queue: EventQueue +export function getQueue(batchSize: number, flushInterval: number, sink: any, batchComparator: any): EventQueue { + let queue: EventQueue if (batchSize > 1) { - queue = new DefaultEventQueue({ + queue = new DefaultEventQueue({ flushInterval, maxQueueSize: batchSize, sink, diff --git a/packages/event-processor/src/reactNativeEventsStore.ts b/packages/event-processor/src/reactNativeEventsStore.ts index e06b8dd0b..62382c3c2 100644 --- a/packages/event-processor/src/reactNativeEventsStore.ts +++ b/packages/event-processor/src/reactNativeEventsStore.ts @@ -16,23 +16,23 @@ */ import { ReactNativeAsyncStorageCache, objectValues } from "@optimizely/js-sdk-utils" -import { ProcessableEvents } from "./eventProcessor" - -// This Stores Formatted events before dispatching. The events are removed after they are successfully dispatched. -// Stored events are retried on every new event dispatch, when connection becomes available again or when SDK initializes the next time. -export class ReactNativePendingEventsStore { - private storageKey: string = 'fs_optly_pending_events' - private cache: ReactNativeAsyncStorageCache = new ReactNativeAsyncStorageCache() +/** + * A key value store which stores objects of type T with string keys + */ +export class ReactNativeEventsStore { private maxSize: number + private storageKey: string private synchronizer: Synchronizer = new Synchronizer() + private cache: ReactNativeAsyncStorageCache = new ReactNativeAsyncStorageCache() - constructor(maxSize: number) { + constructor(maxSize: number, storageKey: string) { this.maxSize = maxSize + this.storageKey = storageKey } - public async set(key: string, event: any): Promise { + public async set(key: string, event: T): Promise { await this.synchronizer.getLock() - const eventsMap = await this.cache.get(this.storageKey) || {} + const eventsMap: {[key: string]: T} = await this.cache.get(this.storageKey) || {} if (Object.keys(eventsMap).length < this.maxSize) { eventsMap[key] = event await this.cache.set(this.storageKey, eventsMap) @@ -41,62 +41,43 @@ export class ReactNativePendingEventsStore { return key } - public async remove(key: string): Promise { + public async get(key: string): Promise { await this.synchronizer.getLock() - const eventsMap = await this.cache.get(this.storageKey) || {} - eventsMap[key] && delete eventsMap[key] - await this.cache.set(this.storageKey, eventsMap) + const eventsMap: {[key: string]: T} = await this.cache.get(this.storageKey) || {} this.synchronizer.releaseLock() + return eventsMap[key] } - public async get(key: string): Promise { - await this.synchronizer.getLock() - const eventsMap = await this.cache.get(this.storageKey) || {} - this.synchronizer.releaseLock() - return eventsMap[key] + public async getEventsMap(): Promise<{[key: string]: T}> { + return await this.cache.get(this.storageKey) || {} } - public async getAllEvents(): Promise { + public async getEventsList(): Promise { await this.synchronizer.getLock() - const eventsMap = await this.cache.get(this.storageKey) || {} + const eventsMap: {[key: string]: T} = await this.cache.get(this.storageKey) || {} this.synchronizer.releaseLock() return objectValues(eventsMap) } - public async getEventsMap(): Promise { - return await this.cache.get(this.storageKey) || {} - } -} - -// This stores individual events generated from the SDK till they are part of the pending buffer. -// The store is cleared right before the event is formatted to be dispatched. -// This is to make sure that individual events are not lost when app closes before the buffer was flushed. -export class ReactNativeEventBufferStore { - private storageKey: string = 'fs_optly_event_buffer' - private cache: ReactNativeAsyncStorageCache = new ReactNativeAsyncStorageCache() - private synchronizer: Synchronizer = new Synchronizer() - - public async add(event: ProcessableEvents) { + public async remove(key: string): Promise { await this.synchronizer.getLock() - const events = await this.getAll() - events.push(event) - await this.cache.set(this.storageKey, events) + const eventsMap: {[key: string]: T} = await this.cache.get(this.storageKey) || {} + eventsMap[key] && delete eventsMap[key] + await this.cache.set(this.storageKey, eventsMap) this.synchronizer.releaseLock() } - public async getAll(): Promise { - return (await this.cache.get(this.storageKey) || []) as ProcessableEvents[] - } - public async clear(): Promise { await this.cache.remove(this.storageKey) } } -// Both the above stores use single entry in the async storage store to manage their maps and lists. -// This results in race condition when two items are added to the map or array in parallel. -// for ex. Req 1 gets the map. Req 2 gets the map. Req 1 sets the map. Req 2 sets the map. The map now loses item from Req 1. -// This synchronizer makes sure the operations are atomic using promises. +/** + * Both the above stores use single entry in the async storage store to manage their maps and lists. + * This results in race condition when two items are added to the map or array in parallel. + * for ex. Req 1 gets the map. Req 2 gets the map. Req 1 sets the map. Req 2 sets the map. The map now loses item from Req 1. + * This synchronizer makes sure the operations are atomic using promises. + */ class Synchronizer { private lockPromises: Promise[] = [] private resolvers: any[] = [] diff --git a/packages/event-processor/src/v1/buildEventV1.ts b/packages/event-processor/src/v1/buildEventV1.ts index 8bfd24578..5dd65befd 100644 --- a/packages/event-processor/src/v1/buildEventV1.ts +++ b/packages/event-processor/src/v1/buildEventV1.ts @@ -1,5 +1,5 @@ import { EventTags, ConversionEvent, ImpressionEvent, VisitorAttribute } from '../events' -import { ProcessableEvents } from '../eventProcessor' +import { ProcessableEvent } from '../eventProcessor' import { EventV1Request } from '../eventDispatcher' const ACTIVATE_EVENT_KEY = 'campaign_activated' @@ -67,10 +67,10 @@ type Attributes = { * Given an array of batchable Decision or ConversionEvent events it returns * a single EventV1 with proper batching * - * @param {ProcessableEvents[]} events + * @param {ProcessableEvent[]} events * @returns {EventV1} */ -export function makeBatchedEventV1(events: ProcessableEvents[]): EventV1 { +export function makeBatchedEventV1(events: ProcessableEvent[]): EventV1 { const visitors: Visitor[] = [] const data = events[0] @@ -229,7 +229,7 @@ export function buildConversionEventV1(data: ConversionEvent): EventV1 { } } -export function formatEvents(events: ProcessableEvents[]): EventV1Request { +export function formatEvents(events: ProcessableEvent[]): EventV1Request { return { url: 'https://logx.optimizely.com/v1/events', httpVerb: 'POST', diff --git a/packages/event-processor/src/v1/v1EventProcessor.react_native.ts b/packages/event-processor/src/v1/v1EventProcessor.react_native.ts index 54e32064c..ef8b33df7 100644 --- a/packages/event-processor/src/v1/v1EventProcessor.react_native.ts +++ b/packages/event-processor/src/v1/v1EventProcessor.react_native.ts @@ -27,15 +27,12 @@ import { getLogger } from '@optimizely/js-sdk-logging' import { getQueue, EventProcessor, - ProcessableEvents, + ProcessableEvent, sendEventNotification, validateAndGetBatchSize, validateAndGetFlushInterval, } from "../eventProcessor" -import { - ReactNativeEventBufferStore, - ReactNativePendingEventsStore, -} from '../reactNativeEventsStore' +import { ReactNativeEventsStore } from '../reactNativeEventsStore' import { EventQueue } from '../eventQueue' import RequestTracker from '../requestTracker' import { areEventContextsEqual } from '../events' @@ -49,10 +46,15 @@ import { const logger = getLogger('ReactNativeEventProcessor') const DEFAULT_MAX_QUEUE_SIZE = 10000 +const PENDING_EVENTS_STORE_KEY = 'fs_optly_pending_events' +const EVENT_BUFFER_STORE_KEY = 'fs_optly_event_buffer' +/** + * React Native Events Processor with Caching support for events when app is offline. + */ export abstract class LogTierV1EventProcessor implements EventProcessor { private dispatcher: EventDispatcher - private queue: EventQueue + private queue: EventQueue private notificationCenter?: NotificationCenter private requestTracker: RequestTracker @@ -60,8 +62,18 @@ export abstract class LogTierV1EventProcessor implements EventProcessor { private isInternetReachable: boolean = true private pendingEventsPromise: Promise | null = null - private pendingEventsStore: ReactNativePendingEventsStore - private eventBufferStore: ReactNativeEventBufferStore = new ReactNativeEventBufferStore() + /** + * This Stores Formatted events before dispatching. The events are removed after they are successfully dispatched. + * Stored events are retried on every new event dispatch, when connection becomes available again or when SDK initializes the next time. + */ + private pendingEventsStore: ReactNativeEventsStore + + /** + * This stores individual events generated from the SDK till they are part of the pending buffer. + * The store is cleared right before the event is formatted to be dispatched. + * This is to make sure that individual events are not lost when app closes before the buffer was flushed. + */ + private eventBufferStore: ReactNativeEventsStore constructor({ dispatcher, @@ -83,51 +95,48 @@ export abstract class LogTierV1EventProcessor implements EventProcessor { flushInterval = validateAndGetFlushInterval(flushInterval) batchSize = validateAndGetBatchSize(batchSize) this.queue = getQueue(batchSize, flushInterval, this.drainQueue.bind(this), areEventContextsEqual) - this.pendingEventsStore = new ReactNativePendingEventsStore(maxQueueSize) + this.pendingEventsStore = new ReactNativeEventsStore(maxQueueSize, PENDING_EVENTS_STORE_KEY) + this.eventBufferStore = new ReactNativeEventsStore(maxQueueSize, EVENT_BUFFER_STORE_KEY) } - start(): void { + async start(): Promise { this.queue.start() - this.unsubscribeNetInfo = addConnectionListener(async (state: NetInfoState) => { - if (this.isInternetReachable && !state.isInternetReachable) { - this.isInternetReachable = false - return - } - - if (!this.isInternetReachable && state.isInternetReachable) { - this.isInternetReachable = true - // To make sure `eventProcessor.stop()` waits for pending events to completely process - this.processPendingEvents() - } - }) - // Dispatch all the formatted pending events right away - this.processPendingEvents().then(() => { - this.pendingEventsPromise = null - // Process individual events pending from the buffer. - this.eventBufferStore.getAll().then((events: ProcessableEvents[]) => { - events.forEach((event: ProcessableEvents) => this.process(event)) - }) - }) + this.unsubscribeNetInfo = addConnectionListener(this.connectionListener.bind(this)) + await this.processPendingEvents() + // Process individual events pending from the buffer. + const events: ProcessableEvent[] = await this.eventBufferStore.getEventsList() + events.forEach(this.process.bind(this)) + } + + private connectionListener(state: NetInfoState) { + if (this.isInternetReachable && !state.isInternetReachable) { + this.isInternetReachable = false + return + } + if (!this.isInternetReachable && state.isInternetReachable) { + this.isInternetReachable = true + this.processPendingEvents() + } } isSuccessResponse(status: number): boolean { return status >= 200 && status < 400 } - async drainQueue(buffer: ProcessableEvents[]): Promise { + async drainQueue(buffer: ProcessableEvent[]): Promise { + // Retry pending failed events while draining queue await this.processPendingEvents() - logger.debug('draining queue with %s events', buffer.length) - if (buffer.length === 0) { return } + logger.debug('draining queue with %s events', buffer.length) const eventCacheKey = generateUUID() const formattedEvent = formatEvents(buffer) - // Store formatted event before dispatching. + // Store formatted event before dispatching to be retried later in case of failure. await this.pendingEventsStore.set(eventCacheKey, formattedEvent) // Clear buffer because the buffer has become a formatted event and is already stored in pending cache. @@ -137,33 +146,24 @@ export abstract class LogTierV1EventProcessor implements EventProcessor { } async processPendingEvents(): Promise { - // If pending events are already being dispatched, wait for the promise to complete and then return - // This is to prevent processing events twice if pending events are tried simultenously from two flows. - // For example when there were pending events and device gained back connectivity. - // At the same time, batch is complete and drainQueue attempts to process pending events. - // It will then get the same pendingEventsPromise and wait for it so that the new events are - // successfully sequenced after the pending events are dispatched. - if (this.pendingEventsPromise) { - await this.pendingEventsPromise - return + if (!this.pendingEventsPromise){ + // Only process events if existing promise is not in progress + this.pendingEventsPromise = this.getPendingEventsPromise() } - - this.pendingEventsPromise = this.getPendingEventsPromise() await this.pendingEventsPromise - - // Clear pending events promise because its fulfilled now this.pendingEventsPromise = null } async getPendingEventsPromise(): Promise{ const formattedEvents: {[key: string]: any} = await this.pendingEventsStore.getEventsMap() const eventEntries = objectEntries(formattedEvents) + // Using for loop to be able to wait for previous dispatch to finish before moving on to the new one for (let i = 0; i < eventEntries.length; i++) { const [eventKey, event] = eventEntries[i] await this.dispatchEvent(eventKey, event) } } - + async dispatchEvent(eventCacheKey: string, event: EventV1Request): Promise { const requestPromise = new Promise((resolve) => { this.dispatcher.dispatchEvent(event, async ({ statusCode }: EventDispatcherResponse) => { @@ -174,13 +174,14 @@ export abstract class LogTierV1EventProcessor implements EventProcessor { }) sendEventNotification(this.notificationCenter, event) }) + // Tracking all the requests to dispatch to make sure request is completed before fulfilling the `stop` promise this.requestTracker.trackRequest(requestPromise) return requestPromise } - process(event: ProcessableEvents): void { + process(event: ProcessableEvent): void { // Adding events to buffer store. If app closes before dispatch, we can reprocess next time the app initializes - this.eventBufferStore.add(event) + this.eventBufferStore.set(generateUUID(), event) this.queue.enqueue(event) } diff --git a/packages/event-processor/src/v1/v1EventProcessor.ts b/packages/event-processor/src/v1/v1EventProcessor.ts index 09bc54a61..36807d85e 100644 --- a/packages/event-processor/src/v1/v1EventProcessor.ts +++ b/packages/event-processor/src/v1/v1EventProcessor.ts @@ -20,7 +20,7 @@ import { EventDispatcher } from '../eventDispatcher' import { getQueue, EventProcessor, - ProcessableEvents, + ProcessableEvent, sendEventNotification, validateAndGetBatchSize, validateAndGetFlushInterval, @@ -33,10 +33,10 @@ import { formatEvents } from './buildEventV1' const logger = getLogger('LogTierV1EventProcessor') export class LogTierV1EventProcessor implements EventProcessor { - protected dispatcher: EventDispatcher - protected queue: EventQueue + private dispatcher: EventDispatcher + private queue: EventQueue private notificationCenter?: NotificationCenter - protected requestTracker: RequestTracker + private requestTracker: RequestTracker constructor({ dispatcher, @@ -58,7 +58,7 @@ export class LogTierV1EventProcessor implements EventProcessor { this.queue = getQueue(batchSize, flushInterval, this.drainQueue.bind(this), areEventContextsEqual) } - drainQueue(buffer: ProcessableEvents[]): Promise { + drainQueue(buffer: ProcessableEvent[]): Promise { const reqPromise = new Promise(resolve => { logger.debug('draining queue with %s events', buffer.length) @@ -77,7 +77,7 @@ export class LogTierV1EventProcessor implements EventProcessor { return reqPromise } - process(event: ProcessableEvents): void { + process(event: ProcessableEvent): void { this.queue.enqueue(event) } From 01ef03e1bafdab13bcc4b8820e4bcdb3e91503fe Mon Sep 17 00:00:00 2001 From: zashraf1985 <35262377+zashraf1985@users.noreply.github.com> Date: Mon, 13 Jul 2020 15:26:02 -0700 Subject: [PATCH 19/28] Update packages/event-processor/src/v1/v1EventProcessor.react_native.ts Co-authored-by: Matt Carroll --- .../event-processor/src/v1/v1EventProcessor.react_native.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/event-processor/src/v1/v1EventProcessor.react_native.ts b/packages/event-processor/src/v1/v1EventProcessor.react_native.ts index ef8b33df7..01767ecaa 100644 --- a/packages/event-processor/src/v1/v1EventProcessor.react_native.ts +++ b/packages/event-processor/src/v1/v1EventProcessor.react_native.ts @@ -158,8 +158,7 @@ export abstract class LogTierV1EventProcessor implements EventProcessor { const formattedEvents: {[key: string]: any} = await this.pendingEventsStore.getEventsMap() const eventEntries = objectEntries(formattedEvents) // Using for loop to be able to wait for previous dispatch to finish before moving on to the new one - for (let i = 0; i < eventEntries.length; i++) { - const [eventKey, event] = eventEntries[i] + for (const [eventKey, event] of eventEntries) { await this.dispatchEvent(eventKey, event) } } From 3411d52cfb2c542fed8682df36137ff5e6a1c211 Mon Sep 17 00:00:00 2001 From: Zeeshan Ashraf Date: Mon, 13 Jul 2020 19:09:37 -0700 Subject: [PATCH 20/28] added some logs --- .../src/reactNativeEventsStore.ts | 27 +++++++++++-------- .../src/v1/v1EventProcessor.react_native.ts | 8 ++++++ 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/packages/event-processor/src/reactNativeEventsStore.ts b/packages/event-processor/src/reactNativeEventsStore.ts index 62382c3c2..a09d6f57f 100644 --- a/packages/event-processor/src/reactNativeEventsStore.ts +++ b/packages/event-processor/src/reactNativeEventsStore.ts @@ -14,28 +14,33 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { getLogger } from '@optimizely/js-sdk-logging' import { ReactNativeAsyncStorageCache, objectValues } from "@optimizely/js-sdk-utils" +const logger = getLogger('ReactNativeEventsStore') + /** * A key value store which stores objects of type T with string keys */ export class ReactNativeEventsStore { private maxSize: number - private storageKey: string + private storeKey: string private synchronizer: Synchronizer = new Synchronizer() private cache: ReactNativeAsyncStorageCache = new ReactNativeAsyncStorageCache() - constructor(maxSize: number, storageKey: string) { + constructor(maxSize: number, storeKey: string) { this.maxSize = maxSize - this.storageKey = storageKey + this.storeKey = storeKey } public async set(key: string, event: T): Promise { await this.synchronizer.getLock() - const eventsMap: {[key: string]: T} = await this.cache.get(this.storageKey) || {} + const eventsMap: {[key: string]: T} = await this.cache.get(this.storeKey) || {} if (Object.keys(eventsMap).length < this.maxSize) { eventsMap[key] = event - await this.cache.set(this.storageKey, eventsMap) + await this.cache.set(this.storeKey, eventsMap) + } else { + logger.warn('React native events store is full. Store key: %s', this.storeKey) } this.synchronizer.releaseLock() return key @@ -43,32 +48,32 @@ export class ReactNativeEventsStore { public async get(key: string): Promise { await this.synchronizer.getLock() - const eventsMap: {[key: string]: T} = await this.cache.get(this.storageKey) || {} + const eventsMap: {[key: string]: T} = await this.cache.get(this.storeKey) || {} this.synchronizer.releaseLock() return eventsMap[key] } public async getEventsMap(): Promise<{[key: string]: T}> { - return await this.cache.get(this.storageKey) || {} + return await this.cache.get(this.storeKey) || {} } public async getEventsList(): Promise { await this.synchronizer.getLock() - const eventsMap: {[key: string]: T} = await this.cache.get(this.storageKey) || {} + const eventsMap: {[key: string]: T} = await this.cache.get(this.storeKey) || {} this.synchronizer.releaseLock() return objectValues(eventsMap) } public async remove(key: string): Promise { await this.synchronizer.getLock() - const eventsMap: {[key: string]: T} = await this.cache.get(this.storageKey) || {} + const eventsMap: {[key: string]: T} = await this.cache.get(this.storeKey) || {} eventsMap[key] && delete eventsMap[key] - await this.cache.set(this.storageKey, eventsMap) + await this.cache.set(this.storeKey, eventsMap) this.synchronizer.releaseLock() } public async clear(): Promise { - await this.cache.remove(this.storageKey) + await this.cache.remove(this.storeKey) } } diff --git a/packages/event-processor/src/v1/v1EventProcessor.react_native.ts b/packages/event-processor/src/v1/v1EventProcessor.react_native.ts index 01767ecaa..042c2893d 100644 --- a/packages/event-processor/src/v1/v1EventProcessor.react_native.ts +++ b/packages/event-processor/src/v1/v1EventProcessor.react_native.ts @@ -111,10 +111,12 @@ export abstract class LogTierV1EventProcessor implements EventProcessor { private connectionListener(state: NetInfoState) { if (this.isInternetReachable && !state.isInternetReachable) { this.isInternetReachable = false + logger.debug('Internet connection lost') return } if (!this.isInternetReachable && state.isInternetReachable) { this.isInternetReachable = true + logger.debug('Internet connection is restored, attempting to dispatch pending events') this.processPendingEvents() } } @@ -146,9 +148,12 @@ export abstract class LogTierV1EventProcessor implements EventProcessor { } async processPendingEvents(): Promise { + logger.debug('Processing pending events from offline storage') if (!this.pendingEventsPromise){ // Only process events if existing promise is not in progress this.pendingEventsPromise = this.getPendingEventsPromise() + } else { + logger.debug('Already processing pending events, returning the existing promise') } await this.pendingEventsPromise this.pendingEventsPromise = null @@ -157,6 +162,7 @@ export abstract class LogTierV1EventProcessor implements EventProcessor { async getPendingEventsPromise(): Promise{ const formattedEvents: {[key: string]: any} = await this.pendingEventsStore.getEventsMap() const eventEntries = objectEntries(formattedEvents) + logger.debug('Processing %s pending events', eventEntries.length) // Using for loop to be able to wait for previous dispatch to finish before moving on to the new one for (const [eventKey, event] of eventEntries) { await this.dispatchEvent(eventKey, event) @@ -168,6 +174,8 @@ export abstract class LogTierV1EventProcessor implements EventProcessor { this.dispatcher.dispatchEvent(event, async ({ statusCode }: EventDispatcherResponse) => { if (this.isSuccessResponse(statusCode)) { await this.pendingEventsStore.remove(eventCacheKey) + } else { + logger.warn('Failed to dispatch event, Response status Code: %s', statusCode) } resolve() }) From 108fea630c27277c841deb0e3b54be241f47f994 Mon Sep 17 00:00:00 2001 From: Zeeshan Ashraf Date: Tue, 14 Jul 2020 14:05:27 -0700 Subject: [PATCH 21/28] Added unit tests for React Native store --- .../@react-native-community/async-storage.ts | 38 +-- .../__tests__/reactNativeEventsStore.spec.ts | 219 ++++++++++++++++++ 2 files changed, 243 insertions(+), 14 deletions(-) create mode 100644 packages/event-processor/__tests__/reactNativeEventsStore.spec.ts diff --git a/packages/event-processor/__mocks__/@react-native-community/async-storage.ts b/packages/event-processor/__mocks__/@react-native-community/async-storage.ts index 32f721144..3c5e5f6fc 100644 --- a/packages/event-processor/__mocks__/@react-native-community/async-storage.ts +++ b/packages/event-processor/__mocks__/@react-native-community/async-storage.ts @@ -13,25 +13,35 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +const items: {[key: string]: string} = {} export default class AsyncStorage { + static getItem(key: string, callback?: (error?: Error, result?: string) => void): Promise { - return new Promise((resolve, reject) => { - switch (key) { - case 'keyThatExists': - resolve('{ "name": "Awesome Object" }') - break - case 'keyThatDoesNotExist': - resolve(null) - break - case 'keyWithInvalidJsonObject': - resolve('bad json }') - break - } + return new Promise(resolve => { + setTimeout(() => resolve(items[key] || null), 1) + }) + } + + static setItem(key: string, value: string, callback?: (error?: Error) => void): Promise { + return new Promise((resolve) => { + setTimeout(() => { + items[key] = value + resolve() + }, 1) + }) + } + + static removeItem(key: string, callback?: (error?: Error, result?: string) => void): Promise { + return new Promise(resolve => { + setTimeout(() => { + items[key] && delete items[key] + resolve() + }, 1) }) } - static setItem(key: string, value: string, callback?: (error?: Error) => void): Promise { - return Promise.resolve() + static dumpItems(): {[key: string]: string} { + return items } } diff --git a/packages/event-processor/__tests__/reactNativeEventsStore.spec.ts b/packages/event-processor/__tests__/reactNativeEventsStore.spec.ts new file mode 100644 index 000000000..c09fb4dff --- /dev/null +++ b/packages/event-processor/__tests__/reactNativeEventsStore.spec.ts @@ -0,0 +1,219 @@ +/** + * Copyright 2020, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/// + +import { ReactNativeEventsStore } from '../src/reactNativeEventsStore' +import AsyncStorage from '../__mocks__/@react-native-community/async-storage' + +const STORE_KEY = 'test-store' + +describe('ReactNativeEventsStore', () => { + let store: ReactNativeEventsStore + + beforeEach(() => { + store = new ReactNativeEventsStore(5, STORE_KEY) + }) + + describe('set', () => { + it('should store all the events correctly in the store', async () => { + await store.set('event1', {'name': 'event1'}) + await store.set('event2', {'name': 'event2'}) + await store.set('event3', {'name': 'event3'}) + await store.set('event4', {'name': 'event4'}) + const storedPendingEvents = JSON.parse(AsyncStorage.dumpItems()[STORE_KEY]) + expect(storedPendingEvents).toEqual({ + "event1": { "name": "event1" }, + "event2": { "name": "event2" }, + "event3": { "name": "event3" }, + "event4": { "name": "event4" }, + }) + }) + + it('should store all the events when set asynchronously', async (done) => { + const promises = [] + promises.push(store.set('event1', {'name': 'event1'})) + promises.push(store.set('event2', {'name': 'event2'})) + promises.push(store.set('event3', {'name': 'event3'})) + promises.push(store.set('event4', {'name': 'event4'})) + Promise.all(promises).then(() => { + const storedPendingEvents = JSON.parse(AsyncStorage.dumpItems()[STORE_KEY]) + expect(storedPendingEvents).toEqual({ + "event1": { "name": "event1" }, + "event2": { "name": "event2" }, + "event3": { "name": "event3" }, + "event4": { "name": "event4" }, + }) + done() + }) + }) + }) + + describe('get', () => { + it('should correctly get items', async () => { + await store.set('event1', {'name': 'event1'}) + await store.set('event2', {'name': 'event2'}) + await store.set('event3', {'name': 'event3'}) + await store.set('event4', {'name': 'event4'}) + expect(await store.get('event1')).toEqual({'name': 'event1'}) + expect(await store.get('event2')).toEqual({'name': 'event2'}) + expect(await store.get('event3')).toEqual({'name': 'event3'}) + expect(await store.get('event4')).toEqual({'name': 'event4'}) + }) + }) + + describe('getEventsMap', () => { + it('should get the whole map correctly', async () => { + await store.set('event1', {'name': 'event1'}) + await store.set('event2', {'name': 'event2'}) + await store.set('event3', {'name': 'event3'}) + await store.set('event4', {'name': 'event4'}) + const mapResult = await store.getEventsMap() + expect(mapResult).toEqual({ + "event1": { "name": "event1" }, + "event2": { "name": "event2" }, + "event3": { "name": "event3" }, + "event4": { "name": "event4" }, + }) + }) + }) + + describe('getEventsList', () => { + it('should get all the events as a list', async () => { + await store.set('event1', {'name': 'event1'}) + await store.set('event2', {'name': 'event2'}) + await store.set('event3', {'name': 'event3'}) + await store.set('event4', {'name': 'event4'}) + const listResult = await store.getEventsList() + expect(listResult).toEqual([ + { "name": "event1" }, + { "name": "event2" }, + { "name": "event3" }, + { "name": "event4" }, + ]) + }) + }) + + describe('remove', () => { + it('should correctly remove items from the store', async () => { + await store.set('event1', {'name': 'event1'}) + await store.set('event2', {'name': 'event2'}) + await store.set('event3', {'name': 'event3'}) + await store.set('event4', {'name': 'event4'}) + let storedPendingEvents = JSON.parse(AsyncStorage.dumpItems()[STORE_KEY]) + expect(storedPendingEvents).toEqual({ + "event1": { "name": "event1" }, + "event2": { "name": "event2" }, + "event3": { "name": "event3" }, + "event4": { "name": "event4" }, + }) + + await store.remove('event1') + storedPendingEvents = JSON.parse(AsyncStorage.dumpItems()[STORE_KEY]) + expect(storedPendingEvents).toEqual({ + "event2": { "name": "event2" }, + "event3": { "name": "event3" }, + "event4": { "name": "event4" }, + }) + + await store.remove('event2') + storedPendingEvents = JSON.parse(AsyncStorage.dumpItems()[STORE_KEY]) + expect(storedPendingEvents).toEqual({ + "event3": { "name": "event3" }, + "event4": { "name": "event4" }, + }) + }) + + it('should correctly remove items from the store when removed asynchronously', async (done) => { + await store.set('event1', {'name': 'event1'}) + await store.set('event2', {'name': 'event2'}) + await store.set('event3', {'name': 'event3'}) + await store.set('event4', {'name': 'event4'}) + let storedPendingEvents = JSON.parse(AsyncStorage.dumpItems()[STORE_KEY]) + expect(storedPendingEvents).toEqual({ + "event1": { "name": "event1" }, + "event2": { "name": "event2" }, + "event3": { "name": "event3" }, + "event4": { "name": "event4" }, + }) + + const promises = [] + promises.push(store.remove('event1')) + promises.push(store.remove('event2')) + promises.push(store.remove('event3')) + Promise.all(promises).then(() => { + let storedPendingEvents = JSON.parse(AsyncStorage.dumpItems()[STORE_KEY]) + expect(storedPendingEvents).toEqual({ "event4": { "name": "event4" }}) + done() + }) + }) + }) + + describe('clear', () => { + it('should clear the whole store',async () => { + await store.set('event1', {'name': 'event1'}) + await store.set('event2', {'name': 'event2'}) + await store.set('event3', {'name': 'event3'}) + await store.set('event4', {'name': 'event4'}) + let storedPendingEvents = JSON.parse(AsyncStorage.dumpItems()[STORE_KEY]) + expect(storedPendingEvents).toEqual({ + "event1": { "name": "event1" }, + "event2": { "name": "event2" }, + "event3": { "name": "event3" }, + "event4": { "name": "event4" }, + }) + await store.clear() + storedPendingEvents = JSON.parse(AsyncStorage.dumpItems()[STORE_KEY] || '{}') + expect(storedPendingEvents).toEqual({}) + }) + }) + + describe('maxSize', () => { + it('should not add anymore events if the store if full', async () => { + await store.set('event1', {'name': 'event1'}) + await store.set('event2', {'name': 'event2'}) + await store.set('event3', {'name': 'event3'}) + await store.set('event4', {'name': 'event4'}) + + let storedPendingEvents = JSON.parse(AsyncStorage.dumpItems()[STORE_KEY]) + expect(storedPendingEvents).toEqual({ + "event1": { "name": "event1" }, + "event2": { "name": "event2" }, + "event3": { "name": "event3" }, + "event4": { "name": "event4" }, + }) + await store.set('event5', {'name': 'event5'}) + + storedPendingEvents = JSON.parse(AsyncStorage.dumpItems()[STORE_KEY]) + expect(storedPendingEvents).toEqual({ + "event1": { "name": "event1" }, + "event2": { "name": "event2" }, + "event3": { "name": "event3" }, + "event4": { "name": "event4" }, + "event5": { "name": "event5" }, + }) + + await store.set('event6', {'name': 'event6'}) + storedPendingEvents = JSON.parse(AsyncStorage.dumpItems()[STORE_KEY]) + expect(storedPendingEvents).toEqual({ + "event1": { "name": "event1" }, + "event2": { "name": "event2" }, + "event3": { "name": "event3" }, + "event4": { "name": "event4" }, + "event5": { "name": "event5" }, + }) + }) + }) +}) From 8dadf855b264e10fb018c75a12bb9c063e72ac59 Mon Sep 17 00:00:00 2001 From: Zeeshan Ashraf Date: Wed, 15 Jul 2020 18:39:11 -0700 Subject: [PATCH 22/28] incorporated review feedback from jae and owais --- .../event-processor/src/eventProcessor.ts | 4 +- .../src/v1/v1EventProcessor.react_native.ts | 72 ++++++++++++------- .../src/v1/v1EventProcessor.ts | 6 +- 3 files changed, 52 insertions(+), 30 deletions(-) diff --git a/packages/event-processor/src/eventProcessor.ts b/packages/event-processor/src/eventProcessor.ts index db2861dbc..57826e41e 100644 --- a/packages/event-processor/src/eventProcessor.ts +++ b/packages/event-processor/src/eventProcessor.ts @@ -21,8 +21,8 @@ import { EventQueue, DefaultEventQueue, SingleEventQueue } from './eventQueue' import { getLogger } from '@optimizely/js-sdk-logging' import { NOTIFICATION_TYPES, NotificationCenter } from '@optimizely/js-sdk-utils' -const DEFAULT_FLUSH_INTERVAL = 30000 // Unit is ms - default flush interval is 30s -const DEFAULT_BATCH_SIZE = 10 +export const DEFAULT_FLUSH_INTERVAL = 30000 // Unit is ms - default flush interval is 30s +export const DEFAULT_BATCH_SIZE = 10 const logger = getLogger('EventProcessor') diff --git a/packages/event-processor/src/v1/v1EventProcessor.react_native.ts b/packages/event-processor/src/v1/v1EventProcessor.react_native.ts index 042c2893d..804630c50 100644 --- a/packages/event-processor/src/v1/v1EventProcessor.react_native.ts +++ b/packages/event-processor/src/v1/v1EventProcessor.react_native.ts @@ -31,6 +31,8 @@ import { sendEventNotification, validateAndGetBatchSize, validateAndGetFlushInterval, + DEFAULT_BATCH_SIZE, + DEFAULT_FLUSH_INTERVAL, } from "../eventProcessor" import { ReactNativeEventsStore } from '../reactNativeEventsStore' import { EventQueue } from '../eventQueue' @@ -52,16 +54,19 @@ const EVENT_BUFFER_STORE_KEY = 'fs_optly_event_buffer' /** * React Native Events Processor with Caching support for events when app is offline. */ -export abstract class LogTierV1EventProcessor implements EventProcessor { +export class LogTierV1EventProcessor implements EventProcessor { private dispatcher: EventDispatcher private queue: EventQueue private notificationCenter?: NotificationCenter private requestTracker: RequestTracker - + private unsubscribeNetInfo: Function | null = null private isInternetReachable: boolean = true private pendingEventsPromise: Promise | null = null - + + // If a pending event fails to dispatch, this indicates skipping further events to preserve sequence in the next retry. + private shouldSkipDispatchToPreserveSequence: boolean = false + /** * This Stores Formatted events before dispatching. The events are removed after they are successfully dispatched. * Stored events are retried on every new event dispatch, when connection becomes available again or when SDK initializes the next time. @@ -77,8 +82,8 @@ export abstract class LogTierV1EventProcessor implements EventProcessor { constructor({ dispatcher, - flushInterval = 30000, - batchSize = 3000, + flushInterval = DEFAULT_FLUSH_INTERVAL, + batchSize = DEFAULT_BATCH_SIZE, maxQueueSize = DEFAULT_MAX_QUEUE_SIZE, notificationCenter, }: { @@ -99,16 +104,7 @@ export abstract class LogTierV1EventProcessor implements EventProcessor { this.eventBufferStore = new ReactNativeEventsStore(maxQueueSize, EVENT_BUFFER_STORE_KEY) } - async start(): Promise { - this.queue.start() - this.unsubscribeNetInfo = addConnectionListener(this.connectionListener.bind(this)) - await this.processPendingEvents() - // Process individual events pending from the buffer. - const events: ProcessableEvent[] = await this.eventBufferStore.getEventsList() - events.forEach(this.process.bind(this)) - } - - private connectionListener(state: NetInfoState) { + private async connectionListener(state: NetInfoState) { if (this.isInternetReachable && !state.isInternetReachable) { this.isInternetReachable = false logger.debug('Internet connection lost') @@ -117,15 +113,16 @@ export abstract class LogTierV1EventProcessor implements EventProcessor { if (!this.isInternetReachable && state.isInternetReachable) { this.isInternetReachable = true logger.debug('Internet connection is restored, attempting to dispatch pending events') - this.processPendingEvents() + await this.processPendingEvents() + this.shouldSkipDispatchToPreserveSequence = false } } - isSuccessResponse(status: number): boolean { + private isSuccessResponse(status: number): boolean { return status >= 200 && status < 400 } - async drainQueue(buffer: ProcessableEvent[]): Promise { + private async drainQueue(buffer: ProcessableEvent[]): Promise { // Retry pending failed events while draining queue await this.processPendingEvents() @@ -144,12 +141,17 @@ export abstract class LogTierV1EventProcessor implements EventProcessor { // Clear buffer because the buffer has become a formatted event and is already stored in pending cache. await this.eventBufferStore.clear() - return this.dispatchEvent(eventCacheKey, formattedEvent) + if (!this.shouldSkipDispatchToPreserveSequence) { + await this.dispatchEvent(eventCacheKey, formattedEvent) + } + + // Resetting skip flag because current sequence of events have all been processed + this.shouldSkipDispatchToPreserveSequence = false } - async processPendingEvents(): Promise { + private async processPendingEvents(): Promise { logger.debug('Processing pending events from offline storage') - if (!this.pendingEventsPromise){ + if (!this.pendingEventsPromise) { // Only process events if existing promise is not in progress this.pendingEventsPromise = this.getPendingEventsPromise() } else { @@ -159,22 +161,27 @@ export abstract class LogTierV1EventProcessor implements EventProcessor { this.pendingEventsPromise = null } - async getPendingEventsPromise(): Promise{ + private async getPendingEventsPromise(): Promise { const formattedEvents: {[key: string]: any} = await this.pendingEventsStore.getEventsMap() const eventEntries = objectEntries(formattedEvents) logger.debug('Processing %s pending events', eventEntries.length) // Using for loop to be able to wait for previous dispatch to finish before moving on to the new one for (const [eventKey, event] of eventEntries) { + // If one event dispatch failed, skip subsequent events to preserve sequence + if (this.shouldSkipDispatchToPreserveSequence) { + return + } await this.dispatchEvent(eventKey, event) } } - async dispatchEvent(eventCacheKey: string, event: EventV1Request): Promise { + private async dispatchEvent(eventCacheKey: string, event: EventV1Request): Promise { const requestPromise = new Promise((resolve) => { this.dispatcher.dispatchEvent(event, async ({ statusCode }: EventDispatcherResponse) => { if (this.isSuccessResponse(statusCode)) { await this.pendingEventsStore.remove(eventCacheKey) } else { + this.shouldSkipDispatchToPreserveSequence = true logger.warn('Failed to dispatch event, Response status Code: %s', statusCode) } resolve() @@ -186,16 +193,29 @@ export abstract class LogTierV1EventProcessor implements EventProcessor { return requestPromise } - process(event: ProcessableEvent): void { + public async start(): Promise { + this.queue.start() + this.unsubscribeNetInfo = addConnectionListener(this.connectionListener.bind(this)) + + await this.processPendingEvents() + this.shouldSkipDispatchToPreserveSequence = false + + // Process individual events pending from the buffer. + const events: ProcessableEvent[] = await this.eventBufferStore.getEventsList() + await this.eventBufferStore.clear() + events.forEach(this.process.bind(this)) + } + + public process(event: ProcessableEvent): void { // Adding events to buffer store. If app closes before dispatch, we can reprocess next time the app initializes this.eventBufferStore.set(generateUUID(), event) this.queue.enqueue(event) } - stop(): Promise { - this.unsubscribeNetInfo && this.unsubscribeNetInfo() + public stop(): Promise { // swallow - an error stopping this queue shouldn't prevent this from stopping try { + this.unsubscribeNetInfo && this.unsubscribeNetInfo() this.queue.stop() return this.requestTracker.onRequestsComplete() } catch (e) { diff --git a/packages/event-processor/src/v1/v1EventProcessor.ts b/packages/event-processor/src/v1/v1EventProcessor.ts index 36807d85e..2973bc9d1 100644 --- a/packages/event-processor/src/v1/v1EventProcessor.ts +++ b/packages/event-processor/src/v1/v1EventProcessor.ts @@ -24,6 +24,8 @@ import { sendEventNotification, validateAndGetBatchSize, validateAndGetFlushInterval, + DEFAULT_BATCH_SIZE, + DEFAULT_FLUSH_INTERVAL, } from '../eventProcessor' import { EventQueue } from '../eventQueue' import RequestTracker from '../requestTracker' @@ -40,8 +42,8 @@ export class LogTierV1EventProcessor implements EventProcessor { constructor({ dispatcher, - flushInterval = 30000, - batchSize = 3000, + flushInterval = DEFAULT_FLUSH_INTERVAL, + batchSize = DEFAULT_BATCH_SIZE, notificationCenter, }: { dispatcher: EventDispatcher From b5267fb1b0c5f3810eb13402948aabdb35c32b62 Mon Sep 17 00:00:00 2001 From: Zeeshan Ashraf Date: Wed, 15 Jul 2020 19:37:19 -0700 Subject: [PATCH 23/28] Added skeleton for react native event processor unit tests. Its not complete. might add more tests to the list --- .../v1EventProcessor.react_native.spec.ts | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 packages/event-processor/__tests__/v1EventProcessor.react_native.spec.ts diff --git a/packages/event-processor/__tests__/v1EventProcessor.react_native.spec.ts b/packages/event-processor/__tests__/v1EventProcessor.react_native.spec.ts new file mode 100644 index 000000000..63d6bcddc --- /dev/null +++ b/packages/event-processor/__tests__/v1EventProcessor.react_native.spec.ts @@ -0,0 +1,93 @@ +/** + * Copyright 2020, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/// + +describe('LogTierV1EventProcessorReactNative', () => { + + describe('VERIFYING EXISTING EVENT PROCESSOR TESTS ON REACT NATIVE EVENT PROCESSOR', () => { + // Copy all the existing browser/node event processor test here and modify them to work. + // They wont work as they are because we have changed a lot of functions to asyn for react native EP. + }) + + describe('Sequence', () => { + it('should dispatch pending events in correct sequence', () => { + // Add Pending events to the store and that try dispatching them and check the sequence + }) + it('should dispatch new event after pending events', () => { + // Add pending events to the store and + }) + }) + + describe('Retry Pending Events', () => { + describe('App start', () => { + it('should dispatch all the pending events in correct order', () => { + // store some events in the store. + // call start + // verify if all dispatched + // verify they dispatched in correct order + }) + + it('should process all the events left in buffer when the app closed last time', () => { + // add events to buffer store + // call start + // wait for the flush interval + // verify correct event was dispatched based on the buffer + }) + + it('should dispatch pending events first and then process events in buffer store', () => { + + }) + }) + + describe('When a new event is dispatched', () => { + it('should dispatch all the pending events first', () => { + + }) + + it('should dispatch pending events and new event in correct order', () => { + + }) + + it('should skip dispatching subsequent events if an event fails to dispatch', () => { + + }) + }) + + describe('When internet connection is restored', () => { + it('should dispatch all the pending events in correct order when internet connection is restored', () => { + + }) + + it('should not dispatch duplicate events if internet is lost and restored twice in a short interval', () => { + + }) + }) + }) + + describe('Race Conditions', () => { + it('should not dispatch pending events twice if retyring is triggered simultenously from internet connection and new event', () => { + + }) + + it('should dispatch pending events in correct order if retyring is triggered from multiple sources simultenously', () => { + + }) + }) + + describe('stop', () => { + // Add tests to make sure stop behaves correctly + }) +}) From 6c3c40d984d20807c89b546877e49fbda2bc6f19 Mon Sep 17 00:00:00 2001 From: Zeeshan Ashraf Date: Thu, 16 Jul 2020 19:12:59 -0700 Subject: [PATCH 24/28] moved cache code from utils to this repo to resolve dependency issues --- packages/event-processor/package-lock.json | 1878 ++++++++--------- packages/event-processor/package.json | 6 +- .../src/persistentKeyValueCache.ts | 60 + .../src/reactNativeAsyncStorageCache.ts | 49 + .../src/reactNativeEventsStore.ts | 4 +- .../src/v1/v1EventProcessor.ts | 2 +- 6 files changed, 945 insertions(+), 1054 deletions(-) create mode 100644 packages/event-processor/src/persistentKeyValueCache.ts create mode 100644 packages/event-processor/src/reactNativeAsyncStorageCache.ts diff --git a/packages/event-processor/package-lock.json b/packages/event-processor/package-lock.json index 06769c69c..7d32b0266 100644 --- a/packages/event-processor/package-lock.json +++ b/packages/event-processor/package-lock.json @@ -5,22 +5,28 @@ "requires": true, "dependencies": { "@babel/code-frame": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0.tgz", - "integrity": "sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij5HUcajsVcMCpQrcLmtxRbVFTIqmcSkSeYRBFBRxs2FiUqFJDLdiebA==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", + "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", "dev": true, "requires": { - "@babel/highlight": "^7.0.0" + "@babel/highlight": "^7.10.4" } }, + "@babel/helper-validator-identifier": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", + "dev": true + }, "@babel/highlight": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.0.0.tgz", - "integrity": "sha512-UFMC4ZeFC48Tpvj7C8UgLvtkaUuovQX+5xNWrsIoMG8o2z+XFKjKaN9iVmS84dPwVN00W4wPmqvYoZF3EGAsfw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", + "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", "dev": true, "requires": { + "@babel/helper-validator-identifier": "^7.10.4", "chalk": "^2.0.0", - "esutils": "^2.0.2", "js-tokens": "^4.0.0" }, "dependencies": { @@ -32,6 +38,17 @@ } } }, + "@jest/types": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-24.9.0.tgz", + "integrity": "sha512-XKK7ze1apu5JWQ5eZjHITP66AX+QsLlbaJRBGYr8pNzwcAE2JVkwnf0yqjHTsDRcjR0mujy/NmZMXw5kl+kGBw==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^1.1.1", + "@types/yargs": "^13.0.0" + } + }, "@optimizely/js-sdk-logging": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/@optimizely/js-sdk-logging/-/js-sdk-logging-0.1.0.tgz", @@ -51,50 +68,93 @@ } }, "@optimizely/js-sdk-utils": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@optimizely/js-sdk-utils/-/js-sdk-utils-0.3.2.tgz", - "integrity": "sha512-CeGzUrpUQkJQM8NMbzr1kK0SKiNOynxEAHKwyLhJrzGOpmQ+qhMW1B8yQlQjHaDmpUsiEZzo4TF8XoqFz9JLXA==", + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@optimizely/js-sdk-utils/-/js-sdk-utils-0.2.0.tgz", + "integrity": "sha512-aHEccRVc5YjWAdIVtniKfUE3tuzHriIWZTS4sLEq/lXkNTITSL1jrBEJD91CVY5BahWu/aG/aOafrA7XGH3sDQ==", "requires": { "uuid": "^3.3.2" } }, + "@react-native-community/async-storage": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@react-native-community/async-storage/-/async-storage-1.11.0.tgz", + "integrity": "sha512-Pq9LlmvtCEKAGdkyrgTcRxNh2fnHFykEj2qnRYijOl1pDIl2MkD5IxaXu5eOL0wgOtAl4U//ff4z40Td6XR5rw==", + "dev": true, + "requires": { + "deep-assign": "^3.0.0" + } + }, "@react-native-community/netinfo": { - "version": "5.9.4", - "resolved": "https://registry.npmjs.org/@react-native-community/netinfo/-/netinfo-5.9.4.tgz", - "integrity": "sha512-mb664NOqPvyUZ4TznzdYEfdS3OhSXWGbZprgsDZn4THw2X/4wcBFcBUeWuMzeQ56KhY0rm/YBBlZWHrSf3C/Aw==", + "version": "5.9.5", + "resolved": "https://registry.npmjs.org/@react-native-community/netinfo/-/netinfo-5.9.5.tgz", + "integrity": "sha512-PbSsRmhRwYIMdeVJTf9gJtvW0TVq/hmgz1xyjsrTIsQ7QS7wbMEiv1Eb/M/y6AEEsdUped5Axm5xykq9TGISHg==", "dev": true }, + "@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": "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": "*" + } + }, "@types/jest": { - "version": "24.0.9", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-24.0.9.tgz", - "integrity": "sha512-k3OOeevcBYLR5pdsOv5g3OP94h3mrJmLPHFEPWgbbVy2tGv0TZ/TlygiC848ogXhK8NL0I5up7YYtwpCp8xCJA==", + "version": "24.9.1", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-24.9.1.tgz", + "integrity": "sha512-Fb38HkXSVA4L8fGKEZ6le5bB8r6MRWlOCZbVuWZcmOMSCd2wCYOwN1ibj8daIoV9naq7aaOZjrLCoCMptKU/4Q==", + "dev": true, + "requires": { + "jest-diff": "^24.3.0" + } + }, + "@types/yargs": { + "version": "13.0.9", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-13.0.9.tgz", + "integrity": "sha512-xrvhZ4DZewMDhoH1utLtOAwYQy60eYFoXeje30TzM3VOvQlBwQaEpKFq5m34k1wOw2AKIi2pwtiAjdmhvlBUzg==", "dev": true, "requires": { - "@types/jest-diff": "*" + "@types/yargs-parser": "*" } }, - "@types/jest-diff": { - "version": "20.0.1", - "resolved": "https://registry.npmjs.org/@types/jest-diff/-/jest-diff-20.0.1.tgz", - "integrity": "sha512-yALhelO3i0hqZwhjtcr6dYyaLoCHbAMshwtj6cGxTvHZAKXHsYGdff6E8EPw3xLKY0ELUTQ69Q1rQiJENnccMA==", + "@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 }, "abab": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.0.tgz", - "integrity": "sha512-sY5AXXVZv4Y1VACTtR11UJCPHHudgY5i26Qj5TypE6DKlIApbwb5uqhXcJ5UUGbvZNRh7EeIoW+LrJumBsKp7w==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.3.tgz", + "integrity": "sha512-tsFzPpcttalNjFBCFMqsKYQcWxxen1pgJR56by//QwvJc4/OUS3kPOOttx2tSIfjsylB0pYu7f5D3K1RCxUnUg==", "dev": true }, "acorn": { - "version": "5.7.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.3.tgz", - "integrity": "sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw==", + "version": "5.7.4", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.4.tgz", + "integrity": "sha512-1D++VG7BhrtvQpNbBzovKNc1FLGGEE/oGe7b9xJm/RFHMBeUaUGpluV9RLjZa47YFdPcDAenEYuq9pQPcMdLJg==", "dev": true }, "acorn-globals": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-4.3.0.tgz", - "integrity": "sha512-hMtHj3s5RnuhvHPowpBYvJVj3rAar82JiDQHvGs1zO0l10ocX/xEdBShNHTJaboucJUsScghp74pH3s7EnHHQw==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-4.3.4.tgz", + "integrity": "sha512-clfQEh21R+D0leSbUdWf3OcfqyaCSAQ8Ryq00bofSekfr9W8u1jyYZo6ir0xu9Gtcf7BjcHJpnbZH7JOCpP60A==", "dev": true, "requires": { "acorn": "^6.0.1", @@ -102,26 +162,26 @@ }, "dependencies": { "acorn": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.1.1.tgz", - "integrity": "sha512-jPTiwtOxaHNaAPg/dmrJ/beuzLRnXtB0kQPQ8JpotKJgTB6rX6c8mlf315941pyjBSaPg8NHXS9fhP4u17DpGA==", + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.1.tgz", + "integrity": "sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA==", "dev": true } } }, "acorn-walk": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-6.1.1.tgz", - "integrity": "sha512-OtUw6JUTgxA2QoqqmrmQ7F2NYqiBPi/L2jqHyFtllhOUvXYQXf0Z1CYUinIfyT4bTCGmrA7gX9FvHA81uzCoVw==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-6.2.0.tgz", + "integrity": "sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA==", "dev": true }, "ajv": { - "version": "6.10.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.0.tgz", - "integrity": "sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg==", + "version": "6.12.3", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.3.tgz", + "integrity": "sha512-4K0cK3L1hsqk9xIb2z9vs/XU+PGJZ9PNpJRDS9YLzmNdX6jmVPfamLvTJr0aDAusnHyCHO6MjzlkAsgtqp9teA==", "dev": true, "requires": { - "fast-deep-equal": "^2.0.1", + "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" @@ -134,9 +194,9 @@ "dev": true }, "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", "dev": true }, "ansi-styles": { @@ -406,9 +466,9 @@ "dev": true }, "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "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 }, "micromatch": { @@ -519,18 +579,18 @@ "dev": true }, "async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.2.tgz", - "integrity": "sha512-H1qVYh1MYhEEFLsP97cVKqCGo7KfCyTt6uEWqsTBr9SO84oK9Uwbyd/yCW+6rKJLHksBNUVWZDAjfS+Ccx0Bbg==", + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz", + "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==", "dev": true, "requires": { - "lodash": "^4.17.11" + "lodash": "^4.17.14" } }, "async-limiter": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", - "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", + "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==", "dev": true }, "asynckit": { @@ -552,9 +612,9 @@ "dev": true }, "aws4": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", - "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.10.0.tgz", + "integrity": "sha512-3YDiu347mtVtjpyV3u5kVqQLP242c06zwDOgpeRnybmXlYYsLbtTrUBUm8i8srONt+FWobl5aibnU1030PeeuA==", "dev": true }, "babel-code-frame": { @@ -568,6 +628,12 @@ "js-tokens": "^3.0.2" }, "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, "ansi-styles": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", @@ -849,9 +915,9 @@ "dev": true }, "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "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 } } @@ -865,6 +931,16 @@ "tweetnacl": "^0.14.3" } }, + "bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dev": true, + "optional": true, + "requires": { + "file-uri-to-path": "1.0.0" + } + }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -887,9 +963,9 @@ } }, "browser-process-hrtime": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-0.1.3.tgz", - "integrity": "sha512-bRFnI4NnjO6cnyLmOV/7PVoDEMJChlcfN0z4s1YMBY989/SvlfMI1lgCnkFUs53e9gQF+w7qu7XdllSTiSl8Aw==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz", + "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==", "dev": true }, "browser-resolve": { @@ -919,9 +995,9 @@ } }, "bser": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/bser/-/bser-2.0.0.tgz", - "integrity": "sha1-mseNPtXZFYBP2HrLFYvHlxR6Fxk=", + "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": { "node-int64": "^0.4.0" @@ -1080,25 +1156,18 @@ "dev": true }, "combined-stream": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.7.tgz", - "integrity": "sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w==", + "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" } }, - "commander": { - "version": "2.17.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.17.1.tgz", - "integrity": "sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==", - "dev": true, - "optional": true - }, "component-emitter": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", - "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=", + "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": { @@ -1108,9 +1177,9 @@ "dev": true }, "convert-source-map": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.6.0.tgz", - "integrity": "sha512-eFu7XigvxdZ1ETfbgPBohgyQ/Z++C0eEhTor0qRwBw9unw+L0/6V8wkSuGgzdThkiS5lSpdptOQPD8Ak40a+7A==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", + "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==", "dev": true, "requires": { "safe-buffer": "~5.1.1" @@ -1123,9 +1192,9 @@ "dev": true }, "core-js": { - "version": "2.6.5", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.5.tgz", - "integrity": "sha512-klh/kDpwX8hryYL14M9w/xei6vrv6sE8gTHDG7/T/+SEovB/G4ejwcfE/CBzO6Edsu+OETZMZ3wcX/EjUkrl5A==", + "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 }, "core-util-is": { @@ -1135,26 +1204,28 @@ "dev": true }, "cross-spawn": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", - "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", "dev": true, "requires": { - "lru-cache": "^4.0.1", + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", "shebang-command": "^1.2.0", "which": "^1.2.9" } }, "cssom": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.6.tgz", - "integrity": "sha512-DtUeseGk9/GBW0hl0vVPpU22iHL6YB5BUX7ml1hB+GMpo0NX5G4voX3kdWiMSEguFtcW3Vh3djqNF4aIe6ne0A==", + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", + "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", "dev": true }, "cssstyle": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-1.2.1.tgz", - "integrity": "sha512-7DYm8qe+gPx/h77QlCyFmX80+fGaE/6A/Ekl0zaszYOubvySO2saYFdQ78P29D0UsULxFKCetDGNaNRUdSF+2A==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-1.4.0.tgz", + "integrity": "sha512-GBrLZYZ4X4x6/QEoBnIrqb8B/f5l4+8me2dkom/j1Gtbxy0kBv6OGzKuAsGM75bkGwGAFkt56Iwg28S3XTZgSA==", "dev": true, "requires": { "cssom": "0.3.x" @@ -1181,9 +1252,9 @@ }, "dependencies": { "whatwg-url": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.0.0.tgz", - "integrity": "sha512-37GeVSIJ3kn1JgKyjiYNmSLP1yzbpb29jdmwBSgkD9h40/hyrR/OifpVUndji3tmwGgD8qpw7iQu3RSbCrBpsQ==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", + "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", "dev": true, "requires": { "lodash.sortby": "^4.7.0", @@ -1214,6 +1285,15 @@ "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", "dev": true }, + "deep-assign": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/deep-assign/-/deep-assign-3.0.0.tgz", + "integrity": "sha512-YX2i9XjJ7h5q/aQ/IM9PEwEnDqETAIYbggmdDB3HLTlSgo1CxPsj6pvhPG68rq6SVE0+p+6Ywsm5fTYNrYtBWw==", + "dev": true, + "requires": { + "is-obj": "^1.0.0" + } + }, "deep-is": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", @@ -1284,9 +1364,9 @@ "dev": true }, "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "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 } } @@ -1318,6 +1398,12 @@ "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", "dev": true }, + "diff-sequences": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-24.9.0.tgz", + "integrity": "sha512-Dj6Wk3tWyTE+Fo1rW8v0Xhwk80um6yFYKbuAxc9c3EZxIHFDYwbi34Uk42u1CdnIiVorvt4RmlSDjIPyzGC2ew==", + "dev": true + }, "domexception": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/domexception/-/domexception-1.0.1.tgz", @@ -1337,6 +1423,15 @@ "safer-buffer": "^2.1.0" } }, + "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" + } + }, "error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -1347,23 +1442,28 @@ } }, "es-abstract": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.13.0.tgz", - "integrity": "sha512-vDZfg/ykNxQVwup/8E1BZhVzFfBxs9NqMzGcvIJrqg5k2/5Za2bWo40dK2J1pgLngZ7c+Shh8lwYtLGyrwPutg==", + "version": "1.17.6", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.6.tgz", + "integrity": "sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==", "dev": true, "requires": { - "es-to-primitive": "^1.2.0", + "es-to-primitive": "^1.2.1", "function-bind": "^1.1.1", "has": "^1.0.3", - "is-callable": "^1.1.4", - "is-regex": "^1.0.4", - "object-keys": "^1.0.12" + "has-symbols": "^1.0.1", + "is-callable": "^1.2.0", + "is-regex": "^1.1.0", + "object-inspect": "^1.7.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.0", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" } }, "es-to-primitive": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.0.tgz", - "integrity": "sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", "dev": true, "requires": { "is-callable": "^1.1.4", @@ -1378,24 +1478,18 @@ "dev": true }, "escodegen": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.11.1.tgz", - "integrity": "sha512-JwiqFD9KdGVVpeuRa68yU3zZnBEOcPs0nKW7wZzXky8Z7tffdYUHbe11bPCV5jYlK6DVdKLWLm0f5I/QlL0Kmw==", + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", + "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", "dev": true, "requires": { - "esprima": "^3.1.3", + "esprima": "^4.0.1", "estraverse": "^4.2.0", "esutils": "^2.0.2", "optionator": "^0.8.1", "source-map": "~0.6.1" }, "dependencies": { - "esprima": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz", - "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=", - "dev": true - }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -1412,15 +1506,15 @@ "dev": true }, "estraverse": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", - "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=", + "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.2", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", - "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", + "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": { @@ -1433,13 +1527,13 @@ } }, "execa": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", - "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=", + "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": "^5.0.1", - "get-stream": "^3.0.0", + "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", @@ -1483,6 +1577,42 @@ "jest-matcher-utils": "^23.6.0", "jest-message-util": "^23.4.0", "jest-regex-util": "^23.3.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "jest-diff": { + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-23.6.0.tgz", + "integrity": "sha512-Gz9l5Ov+X3aL5L37IT+8hoCUsof1CVYBb2QEkOupK64XyRR3h+uRpYIm97K7sY8diFxowR8pIGEdyfMKTixo3g==", + "dev": true, + "requires": { + "chalk": "^2.0.1", + "diff": "^3.2.0", + "jest-get-type": "^22.1.0", + "pretty-format": "^23.6.0" + } + }, + "jest-get-type": { + "version": "22.4.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-22.4.3.tgz", + "integrity": "sha512-/jsz0Y+V29w1chdXVygEKSz2nBoHoYqNShPe+QgxSNjAuP1i8+k4LbQNrfoliKej0P45sivkSCh7yiD6ubHS3w==", + "dev": true + }, + "pretty-format": { + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-23.6.0.tgz", + "integrity": "sha512-zf9NV1NSlDLDjycnwm6hpFATCGl/K1lt0R/GdkAK2O5LN/rwJoB+Mh93gGJjut4YbmecbfgLWVGSTCr0Ewvvbw==", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0", + "ansi-styles": "^3.2.0" + } + } } }, "extend": { @@ -1528,15 +1658,15 @@ "dev": true }, "fast-deep-equal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", - "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", + "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-json-stable-stringify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", - "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=", + "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": { @@ -1546,14 +1676,21 @@ "dev": true }, "fb-watchman": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.0.tgz", - "integrity": "sha1-VOmr99+i8mzZsWNsWIwa/AXeXVg=", + "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.0.0" + "bser": "2.1.1" } }, + "file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "dev": true, + "optional": true + }, "filename-regex": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz", @@ -1640,551 +1777,14 @@ "dev": true }, "fsevents": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.7.tgz", - "integrity": "sha512-Pxm6sI2MeBD7RdD12RYsqaP0nMiwx8eZBXCa6z2L+mRHm2DYrOYwihmhjpkdjUHwQhslWQjRpEgNq4XvBmaAuw==", + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", + "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", "dev": true, "optional": true, "requires": { - "nan": "^2.9.2", - "node-pre-gyp": "^0.10.0" - }, - "dependencies": { - "abbrev": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "ansi-regex": { - "version": "2.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "aproba": { - "version": "1.2.0", - "bundled": true, - "dev": true, - "optional": true - }, - "are-we-there-yet": { - "version": "1.1.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "delegates": "^1.0.0", - "readable-stream": "^2.0.6" - } - }, - "balanced-match": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "brace-expansion": { - "version": "1.1.11", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "chownr": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "code-point-at": { - "version": "1.1.0", - "bundled": true, - "dev": true, - "optional": true - }, - "concat-map": { - "version": "0.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "console-control-strings": { - "version": "1.1.0", - "bundled": true, - "dev": true, - "optional": true - }, - "core-util-is": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "debug": { - "version": "2.6.9", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ms": "2.0.0" - } - }, - "deep-extend": { - "version": "0.6.0", - "bundled": true, - "dev": true, - "optional": true - }, - "delegates": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "detect-libc": { - "version": "1.0.3", - "bundled": true, - "dev": true, - "optional": true - }, - "fs-minipass": { - "version": "1.2.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minipass": "^2.2.1" - } - }, - "fs.realpath": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "gauge": { - "version": "2.7.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "aproba": "^1.0.3", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.0", - "object-assign": "^4.1.0", - "signal-exit": "^3.0.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wide-align": "^1.1.0" - } - }, - "glob": { - "version": "7.1.3", - "bundled": true, - "dev": true, - "optional": 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" - } - }, - "has-unicode": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "iconv-lite": { - "version": "0.4.24", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - }, - "ignore-walk": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minimatch": "^3.0.4" - } - }, - "inflight": { - "version": "1.0.6", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.3", - "bundled": true, - "dev": true, - "optional": true - }, - "ini": { - "version": "1.3.5", - "bundled": true, - "dev": true, - "optional": true - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "isarray": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "minimatch": { - "version": "3.0.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "0.0.8", - "bundled": true, - "dev": true, - "optional": true - }, - "minipass": { - "version": "2.3.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safe-buffer": "^5.1.2", - "yallist": "^3.0.0" - } - }, - "minizlib": { - "version": "1.2.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minipass": "^2.2.1" - } - }, - "mkdirp": { - "version": "0.5.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minimist": "0.0.8" - } - }, - "ms": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "needle": { - "version": "2.2.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "debug": "^2.1.2", - "iconv-lite": "^0.4.4", - "sax": "^1.2.4" - } - }, - "node-pre-gyp": { - "version": "0.10.3", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "detect-libc": "^1.0.2", - "mkdirp": "^0.5.1", - "needle": "^2.2.1", - "nopt": "^4.0.1", - "npm-packlist": "^1.1.6", - "npmlog": "^4.0.2", - "rc": "^1.2.7", - "rimraf": "^2.6.1", - "semver": "^5.3.0", - "tar": "^4" - } - }, - "nopt": { - "version": "4.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "abbrev": "1", - "osenv": "^0.1.4" - } - }, - "npm-bundled": { - "version": "1.0.5", - "bundled": true, - "dev": true, - "optional": true - }, - "npm-packlist": { - "version": "1.2.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ignore-walk": "^3.0.1", - "npm-bundled": "^1.0.1" - } - }, - "npmlog": { - "version": "4.1.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "object-assign": { - "version": "4.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "once": { - "version": "1.4.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "wrappy": "1" - } - }, - "os-homedir": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "os-tmpdir": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "osenv": { - "version": "0.1.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "process-nextick-args": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "rc": { - "version": "1.2.8", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "bundled": true, - "dev": true, - "optional": true - } - } - }, - "readable-stream": { - "version": "2.3.6", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "rimraf": { - "version": "2.6.3", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "glob": "^7.1.3" - } - }, - "safe-buffer": { - "version": "5.1.2", - "bundled": true, - "dev": true, - "optional": true - }, - "safer-buffer": { - "version": "2.1.2", - "bundled": true, - "dev": true, - "optional": true - }, - "sax": { - "version": "1.2.4", - "bundled": true, - "dev": true, - "optional": true - }, - "semver": { - "version": "5.6.0", - "bundled": true, - "dev": true, - "optional": true - }, - "set-blocking": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "signal-exit": { - "version": "3.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "string-width": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - }, - "string_decoder": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "strip-json-comments": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "tar": { - "version": "4.4.8", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "chownr": "^1.1.1", - "fs-minipass": "^1.2.5", - "minipass": "^2.3.4", - "minizlib": "^1.1.1", - "mkdirp": "^0.5.0", - "safe-buffer": "^5.1.2", - "yallist": "^3.0.2" - } - }, - "util-deprecate": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "wide-align": { - "version": "1.1.3", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "string-width": "^1.0.2 || 2" - } - }, - "wrappy": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "yallist": { - "version": "3.0.3", - "bundled": true, - "dev": true, - "optional": true - } + "bindings": "^1.5.0", + "nan": "^2.12.1" } }, "function-bind": { @@ -2200,10 +1800,13 @@ "dev": true }, "get-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", - "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", - "dev": true + "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", @@ -2221,9 +1824,9 @@ } }, "glob": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", - "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", + "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", @@ -2260,9 +1863,9 @@ "dev": true }, "graceful-fs": { - "version": "4.1.15", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz", - "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==", + "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": { @@ -2272,15 +1875,16 @@ "dev": true }, "handlebars": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.1.2.tgz", - "integrity": "sha512-nvfrjqvt9xQ8Z/w0ijewdD/vvWDTOweBUm96NTr66Wfvo1mJenBLwcYmPs3TIBP5ruzYGD7Hx/DaM9RmhroGPw==", + "version": "4.7.6", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.6.tgz", + "integrity": "sha512-1f2BACcBfiwAfStCKZNrUCgqNZkGsAT7UM3kkYtXuLo0KnaVfjKOyf7PRzB6++aK9STyT1Pd2ZCPe3EGOXleXA==", "dev": true, "requires": { + "minimist": "^1.2.5", "neo-async": "^2.6.0", - "optimist": "^0.6.1", "source-map": "^0.6.1", - "uglify-js": "^3.1.4" + "uglify-js": "^3.1.4", + "wordwrap": "^1.0.0" }, "dependencies": { "source-map": { @@ -2323,6 +1927,14 @@ "dev": true, "requires": { "ansi-regex": "^2.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + } } }, "has-flag": { @@ -2332,9 +1944,9 @@ "dev": true }, "has-symbols": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz", - "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", + "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", "dev": true }, "has-value": { @@ -2408,9 +2020,9 @@ } }, "hosted-git-info": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.7.1.tgz", - "integrity": "sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w==", + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz", + "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==", "dev": true }, "html-encoding-sniffer": { @@ -2469,9 +2081,9 @@ } }, "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "dev": true }, "invariant": { @@ -2484,9 +2096,9 @@ } }, "invert-kv": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", - "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", + "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==", "dev": true }, "is-accessor-descriptor": { @@ -2511,9 +2123,9 @@ "dev": true }, "is-callable": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.4.tgz", - "integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.0.tgz", + "integrity": "sha512-pyVD9AaGLxtg6srb2Ng6ynWJqkHU9bEM087AKck0w8QwDarTfNcpIYoU8x8Hv2Icm8u6kFJM18Dag8lyqGkviw==", "dev": true }, "is-ci": { @@ -2535,9 +2147,9 @@ } }, "is-date-object": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz", - "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.2.tgz", + "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==", "dev": true }, "is-descriptor": { @@ -2587,13 +2199,10 @@ "dev": true }, "is-finite": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz", - "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=", - "dev": true, - "requires": { - "number-is-nan": "^1.0.0" - } + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.1.0.tgz", + "integrity": "sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w==", + "dev": true }, "is-fullwidth-code-point": { "version": "2.0.0", @@ -2625,6 +2234,12 @@ "kind-of": "^3.0.2" } }, + "is-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", + "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=", + "dev": true + }, "is-plain-object": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", @@ -2655,12 +2270,12 @@ "dev": true }, "is-regex": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", - "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.0.tgz", + "integrity": "sha512-iI97M8KTWID2la5uYXlkbSDQIg4F6o1sYboZKKTDpnDQMLtUL86zxhgDet3Q2SriaYsyGqZ6Mn2SjbRKeLHdqw==", "dev": true, "requires": { - "has": "^1.0.1" + "has-symbols": "^1.0.1" } }, "is-stream": { @@ -2670,12 +2285,12 @@ "dev": true }, "is-symbol": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.2.tgz", - "integrity": "sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz", + "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==", "dev": true, "requires": { - "has-symbols": "^1.0.0" + "has-symbols": "^1.0.1" } }, "is-typedarray": { @@ -2830,9 +2445,9 @@ } }, "ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true } } @@ -2899,6 +2514,12 @@ "which": "^1.2.12", "yargs": "^11.0.0" } + }, + "jest-get-type": { + "version": "22.4.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-22.4.3.tgz", + "integrity": "sha512-/jsz0Y+V29w1chdXVygEKSz2nBoHoYqNShPe+QgxSNjAuP1i8+k4LbQNrfoliKej0P45sivkSCh7yiD6ubHS3w==", + "dev": true } } }, @@ -2931,18 +2552,42 @@ "jest-validate": "^23.6.0", "micromatch": "^2.3.11", "pretty-format": "^23.6.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "jest-get-type": { + "version": "22.4.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-22.4.3.tgz", + "integrity": "sha512-/jsz0Y+V29w1chdXVygEKSz2nBoHoYqNShPe+QgxSNjAuP1i8+k4LbQNrfoliKej0P45sivkSCh7yiD6ubHS3w==", + "dev": true + }, + "pretty-format": { + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-23.6.0.tgz", + "integrity": "sha512-zf9NV1NSlDLDjycnwm6hpFATCGl/K1lt0R/GdkAK2O5LN/rwJoB+Mh93gGJjut4YbmecbfgLWVGSTCr0Ewvvbw==", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0", + "ansi-styles": "^3.2.0" + } + } } }, "jest-diff": { - "version": "23.6.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-23.6.0.tgz", - "integrity": "sha512-Gz9l5Ov+X3aL5L37IT+8hoCUsof1CVYBb2QEkOupK64XyRR3h+uRpYIm97K7sY8diFxowR8pIGEdyfMKTixo3g==", + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-24.9.0.tgz", + "integrity": "sha512-qMfrTs8AdJE2iqrTp0hzh7kTd2PQWrsFyj9tORoKmu32xjPjeE4NyjVRDz8ybYwqS2ik8N4hsIpiVTyFeo2lBQ==", "dev": true, "requires": { "chalk": "^2.0.1", - "diff": "^3.2.0", - "jest-get-type": "^22.1.0", - "pretty-format": "^23.6.0" + "diff-sequences": "^24.9.0", + "jest-get-type": "^24.9.0", + "pretty-format": "^24.9.0" } }, "jest-docblock": { @@ -2962,6 +2607,24 @@ "requires": { "chalk": "^2.0.1", "pretty-format": "^23.6.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "pretty-format": { + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-23.6.0.tgz", + "integrity": "sha512-zf9NV1NSlDLDjycnwm6hpFATCGl/K1lt0R/GdkAK2O5LN/rwJoB+Mh93gGJjut4YbmecbfgLWVGSTCr0Ewvvbw==", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0", + "ansi-styles": "^3.2.0" + } + } } }, "jest-environment-jsdom": { @@ -2986,9 +2649,9 @@ } }, "jest-get-type": { - "version": "22.4.3", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-22.4.3.tgz", - "integrity": "sha512-/jsz0Y+V29w1chdXVygEKSz2nBoHoYqNShPe+QgxSNjAuP1i8+k4LbQNrfoliKej0P45sivkSCh7yiD6ubHS3w==", + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-24.9.0.tgz", + "integrity": "sha512-lUseMzAley4LhIcpSP9Jf+fTrQ4a1yHQwLNeeVa2cEmbCGeoZAtYPOIv8JaxLD/sUpKxetKGP+gsHl8f8TSj8Q==", "dev": true }, "jest-haste-map": { @@ -3025,6 +2688,42 @@ "jest-snapshot": "^23.6.0", "jest-util": "^23.4.0", "pretty-format": "^23.6.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "jest-diff": { + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-23.6.0.tgz", + "integrity": "sha512-Gz9l5Ov+X3aL5L37IT+8hoCUsof1CVYBb2QEkOupK64XyRR3h+uRpYIm97K7sY8diFxowR8pIGEdyfMKTixo3g==", + "dev": true, + "requires": { + "chalk": "^2.0.1", + "diff": "^3.2.0", + "jest-get-type": "^22.1.0", + "pretty-format": "^23.6.0" + } + }, + "jest-get-type": { + "version": "22.4.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-22.4.3.tgz", + "integrity": "sha512-/jsz0Y+V29w1chdXVygEKSz2nBoHoYqNShPe+QgxSNjAuP1i8+k4LbQNrfoliKej0P45sivkSCh7yiD6ubHS3w==", + "dev": true + }, + "pretty-format": { + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-23.6.0.tgz", + "integrity": "sha512-zf9NV1NSlDLDjycnwm6hpFATCGl/K1lt0R/GdkAK2O5LN/rwJoB+Mh93gGJjut4YbmecbfgLWVGSTCr0Ewvvbw==", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0", + "ansi-styles": "^3.2.0" + } + } } }, "jest-leak-detector": { @@ -3034,12 +2733,30 @@ "dev": true, "requires": { "pretty-format": "^23.6.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "pretty-format": { + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-23.6.0.tgz", + "integrity": "sha512-zf9NV1NSlDLDjycnwm6hpFATCGl/K1lt0R/GdkAK2O5LN/rwJoB+Mh93gGJjut4YbmecbfgLWVGSTCr0Ewvvbw==", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0", + "ansi-styles": "^3.2.0" + } + } } }, "jest-localstorage-mock": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/jest-localstorage-mock/-/jest-localstorage-mock-2.4.0.tgz", - "integrity": "sha512-/mC1JxnMeuIlAaQBsDMilskC/x/BicsQ/BXQxEOw+5b1aGZkkOAqAF3nu8yq449CpzGtp5jJ5wCmDNxLgA2m6A==", + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/jest-localstorage-mock/-/jest-localstorage-mock-2.4.2.tgz", + "integrity": "sha512-bywlhvs7RM2vpZ0EN12XOU5C2WAXRUbxl1VOxel4cqRUHD6zaUmh99UzwOTx0fWuqjfd0y/NDvEbewNaJaz+UQ==", "dev": true }, "jest-matcher-utils": { @@ -3051,6 +2768,30 @@ "chalk": "^2.0.1", "jest-get-type": "^22.1.0", "pretty-format": "^23.6.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "jest-get-type": { + "version": "22.4.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-22.4.3.tgz", + "integrity": "sha512-/jsz0Y+V29w1chdXVygEKSz2nBoHoYqNShPe+QgxSNjAuP1i8+k4LbQNrfoliKej0P45sivkSCh7yiD6ubHS3w==", + "dev": true + }, + "pretty-format": { + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-23.6.0.tgz", + "integrity": "sha512-zf9NV1NSlDLDjycnwm6hpFATCGl/K1lt0R/GdkAK2O5LN/rwJoB+Mh93gGJjut4YbmecbfgLWVGSTCr0Ewvvbw==", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0", + "ansi-styles": "^3.2.0" + } + } } }, "jest-message-util": { @@ -3127,9 +2868,9 @@ "dev": true }, "source-map-support": { - "version": "0.5.10", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.10.tgz", - "integrity": "sha512-YfQ3tQFTK/yzlGJuX8pTwa4tifQj4QS2Mj7UegOu8jAz59MqIiMGPXxQhVQiIMNzayuUSF/jEuVnfFF5JqybmQ==", + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", + "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", "dev": true, "requires": { "buffer-from": "^1.0.0", @@ -3197,6 +2938,42 @@ "natural-compare": "^1.4.0", "pretty-format": "^23.6.0", "semver": "^5.5.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "jest-diff": { + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-23.6.0.tgz", + "integrity": "sha512-Gz9l5Ov+X3aL5L37IT+8hoCUsof1CVYBb2QEkOupK64XyRR3h+uRpYIm97K7sY8diFxowR8pIGEdyfMKTixo3g==", + "dev": true, + "requires": { + "chalk": "^2.0.1", + "diff": "^3.2.0", + "jest-get-type": "^22.1.0", + "pretty-format": "^23.6.0" + } + }, + "jest-get-type": { + "version": "22.4.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-22.4.3.tgz", + "integrity": "sha512-/jsz0Y+V29w1chdXVygEKSz2nBoHoYqNShPe+QgxSNjAuP1i8+k4LbQNrfoliKej0P45sivkSCh7yiD6ubHS3w==", + "dev": true + }, + "pretty-format": { + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-23.6.0.tgz", + "integrity": "sha512-zf9NV1NSlDLDjycnwm6hpFATCGl/K1lt0R/GdkAK2O5LN/rwJoB+Mh93gGJjut4YbmecbfgLWVGSTCr0Ewvvbw==", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0", + "ansi-styles": "^3.2.0" + } + } } }, "jest-util": { @@ -3233,6 +3010,30 @@ "jest-get-type": "^22.1.0", "leven": "^2.1.0", "pretty-format": "^23.6.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "jest-get-type": { + "version": "22.4.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-22.4.3.tgz", + "integrity": "sha512-/jsz0Y+V29w1chdXVygEKSz2nBoHoYqNShPe+QgxSNjAuP1i8+k4LbQNrfoliKej0P45sivkSCh7yiD6ubHS3w==", + "dev": true + }, + "pretty-format": { + "version": "23.6.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-23.6.0.tgz", + "integrity": "sha512-zf9NV1NSlDLDjycnwm6hpFATCGl/K1lt0R/GdkAK2O5LN/rwJoB+Mh93gGJjut4YbmecbfgLWVGSTCr0Ewvvbw==", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0", + "ansi-styles": "^3.2.0" + } + } } }, "jest-watcher": { @@ -3262,9 +3063,9 @@ "dev": true }, "js-yaml": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", - "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.0.tgz", + "integrity": "sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==", "dev": true, "requires": { "argparse": "^1.0.7", @@ -3369,12 +3170,12 @@ "dev": true }, "lcid": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", - "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", + "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", "dev": true, "requires": { - "invert-kv": "^1.0.0" + "invert-kv": "^2.0.0" } }, "left-pad": { @@ -3423,9 +3224,9 @@ } }, "lodash": { - "version": "4.17.15", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", + "version": "4.17.19", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", + "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==", "dev": true }, "lodash.sortby": { @@ -3443,20 +3244,10 @@ "js-tokens": "^3.0.0 || ^4.0.0" } }, - "lru-cache": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", - "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", - "dev": true, - "requires": { - "pseudomap": "^1.0.2", - "yallist": "^2.1.2" - } - }, "make-error": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.5.tgz", - "integrity": "sha512-c3sIjNUow0+8swNwVpqoH4YCShKNFkMaw6oH1mNS2haDZQqkeZFlHS3dhoeEbKKmJB4vXpJucU6oH75aDYeE9g==", + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", "dev": true }, "makeerror": { @@ -3468,6 +3259,15 @@ "tmpl": "1.0.x" } }, + "map-age-cleaner": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", + "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", + "dev": true, + "requires": { + "p-defer": "^1.0.0" + } + }, "map-cache": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", @@ -3490,12 +3290,14 @@ "dev": true }, "mem": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/mem/-/mem-1.1.0.tgz", - "integrity": "sha1-Xt1StIXKHZAP5kiVUFOZoN+kX3Y=", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz", + "integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==", "dev": true, "requires": { - "mimic-fn": "^1.0.0" + "map-age-cleaner": "^0.1.1", + "mimic-fn": "^2.0.0", + "p-is-promise": "^2.0.0" } }, "merge": { @@ -3535,24 +3337,24 @@ } }, "mime-db": { - "version": "1.38.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.38.0.tgz", - "integrity": "sha512-bqVioMFFzc2awcdJZIzR3HjZFX20QhilVS7hytkKrv7xFAn8bM1gzc/FOX2awLISvWe0PV8ptFKcon+wZ5qYkg==", + "version": "1.44.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz", + "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==", "dev": true }, "mime-types": { - "version": "2.1.22", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.22.tgz", - "integrity": "sha512-aGl6TZGnhm/li6F7yx82bJiBZwgiEa4Hf6CNr8YO+r5UHr53tSTYZb102zyU50DOWWKeOv0uQLRL0/9EiKWCog==", + "version": "2.1.27", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz", + "integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==", "dev": true, "requires": { - "mime-db": "~1.38.0" + "mime-db": "1.44.0" } }, "mimic-fn": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", - "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", "dev": true }, "minimatch": { @@ -3565,15 +3367,15 @@ } }, "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", "dev": true }, "mixin-deep": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.1.tgz", - "integrity": "sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ==", + "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", @@ -3592,12 +3394,12 @@ } }, "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "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": "0.0.8" + "minimist": "^1.2.5" } }, "ms": { @@ -3607,9 +3409,9 @@ "dev": true }, "nan": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.12.1.tgz", - "integrity": "sha512-JY7V6lRkStKcKTvHO5NVSQRv+RV+FIL5pvDoLiAtSL9pKlC5x9PKQcZDsq7m4FO4d57mkhC6Z+QhAh3Jdk5JFw==", + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.1.tgz", + "integrity": "sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw==", "dev": true, "optional": true }, @@ -3645,9 +3447,9 @@ "dev": true }, "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "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 } } @@ -3659,9 +3461,15 @@ "dev": true }, "neo-async": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.1.tgz", - "integrity": "sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==", + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "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-int64": { @@ -3671,9 +3479,9 @@ "dev": true }, "node-notifier": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-5.4.0.tgz", - "integrity": "sha512-SUDEb+o71XR5lXSTyivXd9J7fCloE3SyP4lSgt3lU2oSANiox+SxlNRGPjDKrwU1YN3ix2KN/VGGCg0t01rttQ==", + "version": "5.4.3", + "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-5.4.3.tgz", + "integrity": "sha512-M4UBGcs4jeOK9CjTsYwkvH6/MzuUmGCyTW+kCY7uO+1ZVr0+FHGdPdIf5CCLqAaxnRrWidyoQlNkMIIVwbKB8Q==", "dev": true, "requires": { "growly": "^1.3.0", @@ -3720,9 +3528,9 @@ "dev": true }, "nwsapi": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.1.1.tgz", - "integrity": "sha512-T5GaA1J/d34AC8mkrFD2O0DR17kwJ702ZOtJOsS8RpbsQZVOC2/xYFb1i/cw+xdM54JIlMuojjDOYct8GIWtwg==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.0.tgz", + "integrity": "sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ==", "dev": true }, "oauth-sign": { @@ -3759,10 +3567,16 @@ } } }, + "object-inspect": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.8.0.tgz", + "integrity": "sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==", + "dev": true + }, "object-keys": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.0.tgz", - "integrity": "sha512-6OO5X1+2tYkNyNEx6TsCxEqFfRWaqx6EtMiSbGrw8Ob8v9Ne+Hl8rBAgLBZn5wjEz3s/s6U1WXFUFOcxxAwUpg==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", "dev": true }, "object-visit": { @@ -3782,14 +3596,26 @@ } } }, - "object.getownpropertydescriptors": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz", - "integrity": "sha1-h1jIRvW0B62rDyNuCYbxSwUcqhY=", + "object.assign": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", + "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", "dev": true, "requires": { "define-properties": "^1.1.2", - "es-abstract": "^1.5.1" + "function-bind": "^1.1.1", + "has-symbols": "^1.0.0", + "object-keys": "^1.0.11" + } + }, + "object.getownpropertydescriptors": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.0.tgz", + "integrity": "sha512-Z53Oah9A3TdLoblT7VKJaTDdXdT+lQO+cNpKVnya5JDe9uLvzu1YyY1yFDFrcxrlRgWrEFH0jJtD/IbuwjcEVg==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0-next.1" } }, "object.omit": { @@ -3828,36 +3654,18 @@ "wrappy": "1" } }, - "optimist": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", - "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", - "dev": true, - "requires": { - "minimist": "~0.0.1", - "wordwrap": "~0.0.2" - } - }, "optionator": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", - "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", "dev": true, "requires": { "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.4", + "fast-levenshtein": "~2.0.6", "levn": "~0.3.0", "prelude-ls": "~1.1.2", "type-check": "~0.3.2", - "wordwrap": "~1.0.0" - }, - "dependencies": { - "wordwrap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", - "dev": true - } + "word-wrap": "~1.2.3" } }, "os-homedir": { @@ -3867,14 +3675,14 @@ "dev": true }, "os-locale": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-2.1.0.tgz", - "integrity": "sha512-3sslG3zJbEYcaC4YVAvDorjGxc7tv6KVATnLPZONiljsUncvihe9BQoVCEs0RZ1kmf4Hk9OBqlZfJZWI4GanKA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", + "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", "dev": true, "requires": { - "execa": "^0.7.0", - "lcid": "^1.0.0", - "mem": "^1.1.0" + "execa": "^1.0.0", + "lcid": "^2.0.0", + "mem": "^4.0.0" } }, "os-tmpdir": { @@ -3883,12 +3691,24 @@ "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", "dev": true }, + "p-defer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", + "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=", + "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-is-promise": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-2.1.0.tgz", + "integrity": "sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==", + "dev": true + }, "p-limit": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", @@ -4042,21 +3862,15 @@ "dev": true }, "pretty-format": { - "version": "23.6.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-23.6.0.tgz", - "integrity": "sha512-zf9NV1NSlDLDjycnwm6hpFATCGl/K1lt0R/GdkAK2O5LN/rwJoB+Mh93gGJjut4YbmecbfgLWVGSTCr0Ewvvbw==", + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-24.9.0.tgz", + "integrity": "sha512-00ZMZUiHaJrNfk33guavqgvfJS30sLYf0f8+Srklv0AMPodGGHcoHgksZ3OThYnIvOd+8yMCn0YiEOogjlgsnA==", "dev": true, "requires": { - "ansi-regex": "^3.0.0", - "ansi-styles": "^3.2.0" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true - } + "@jest/types": "^24.9.0", + "ansi-regex": "^4.0.0", + "ansi-styles": "^3.2.0", + "react-is": "^16.8.4" } }, "private": { @@ -4066,9 +3880,9 @@ "dev": true }, "process-nextick-args": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", - "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "dev": true }, "prompts": { @@ -4081,18 +3895,22 @@ "sisteransi": "^0.1.1" } }, - "pseudomap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", - "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", - "dev": true - }, "psl": { - "version": "1.1.31", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.31.tgz", - "integrity": "sha512-/6pt4+C+T+wZUieKR620OpzN/LlnNKuWjy1iFLQ/UG35JqHlR/89MP1d96dUfkf6Dne3TuLQzOYEYshJ+Hx8mw==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", + "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", @@ -4123,13 +3941,19 @@ "dev": true }, "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "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 } } }, + "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==", + "dev": true + }, "read-pkg": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", @@ -4173,9 +3997,9 @@ } }, "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", "dev": true, "requires": { "core-util-is": "~1.0.0", @@ -4249,9 +4073,9 @@ } }, "request": { - "version": "2.88.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", - "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", + "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", @@ -4261,7 +4085,7 @@ "extend": "~3.0.2", "forever-agent": "~0.6.1", "form-data": "~2.3.2", - "har-validator": "~5.1.0", + "har-validator": "~5.1.3", "http-signature": "~1.2.0", "is-typedarray": "~1.0.0", "isstream": "~0.1.2", @@ -4271,45 +4095,27 @@ "performance-now": "^2.1.0", "qs": "~6.5.2", "safe-buffer": "^5.1.2", - "tough-cookie": "~2.4.3", + "tough-cookie": "~2.5.0", "tunnel-agent": "^0.6.0", "uuid": "^3.3.2" - }, - "dependencies": { - "punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", - "dev": true - }, - "tough-cookie": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", - "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", - "dev": true, - "requires": { - "psl": "^1.1.24", - "punycode": "^1.4.1" - } - } } }, "request-promise-core": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.2.tgz", - "integrity": "sha512-UHYyq1MO8GsefGEt7EprS8UrXsm1TxEvFUX1IMTuSLU2Rh7fTIdFtl8xD7JiEYiWU2dl+NYAjCTksTehQUxPag==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.3.tgz", + "integrity": "sha512-QIs2+ArIGQVp5ZYbWD5ZLCY29D5CfWizP8eWnm8FoGD1TX61veauETVQbrV60662V0oFBkrDOuaBI8XgtuyYAQ==", "dev": true, "requires": { - "lodash": "^4.17.11" + "lodash": "^4.17.15" } }, "request-promise-native": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.7.tgz", - "integrity": "sha512-rIMnbBdgNViL37nZ1b3L/VfPOpSi0TqVDQPAvO6U14lMzOLrt5nilxCQqtDKhZeDiW0/hkCXGoQjhgJd/tCh6w==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.8.tgz", + "integrity": "sha512-dapwLGqkHtwL5AEbfenuzjTYg35Jd6KPytsC2/TLkVMz8rm+tNt72MGUWT1RP/aYawMpN6HqbNGBQaRcBtjQMQ==", "dev": true, "requires": { - "request-promise-core": "1.1.2", + "request-promise-core": "1.1.3", "stealthy-require": "^1.1.1", "tough-cookie": "^2.3.3" } @@ -4327,9 +4133,9 @@ "dev": true }, "resolve": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.10.0.tgz", - "integrity": "sha512-3sUr9aq5OfSg2S9pNtPA9hL1FVEAjvfOC4leW0SNf/mpnaakz2a9femSd6LqAww2RaFctwyf1lCqnTHuF1rxDg==", + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz", + "integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==", "dev": true, "requires": { "path-parse": "^1.0.6" @@ -4363,9 +4169,9 @@ "dev": true }, "rimraf": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", - "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", "dev": true, "requires": { "glob": "^7.1.3" @@ -4663,9 +4469,9 @@ "dev": true }, "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "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 }, "micromatch": { @@ -4688,12 +4494,6 @@ "snapdragon": "^0.8.1", "to-regex": "^3.0.2" } - }, - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true } } }, @@ -4704,9 +4504,9 @@ "dev": true }, "semver": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz", - "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==", + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", "dev": true }, "set-blocking": { @@ -4716,9 +4516,9 @@ "dev": true }, "set-value": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.0.tgz", - "integrity": "sha512-hw0yxk9GT/Hr5yJEYnHNKYXkIA8mVJgd9ditYZCe16ZczcaELYYcfvaXesNACk2O8O0nTiPQcQhGUQj8JLzeeg==", + "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", @@ -4760,9 +4560,9 @@ "dev": true }, "signal-exit": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", - "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", + "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", "dev": true }, "sisteransi": { @@ -4869,9 +4669,9 @@ "dev": true }, "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "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 } } @@ -4892,12 +4692,12 @@ "dev": true }, "source-map-resolve": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.2.tgz", - "integrity": "sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA==", + "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.1", + "atob": "^2.1.2", "decode-uri-component": "^0.2.0", "resolve-url": "^0.2.1", "source-map-url": "^0.4.0", @@ -4920,9 +4720,9 @@ "dev": true }, "spdx-correct": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz", - "integrity": "sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q==", + "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", @@ -4930,15 +4730,15 @@ } }, "spdx-exceptions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz", - "integrity": "sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA==", + "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.0", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", - "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", + "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", @@ -4946,9 +4746,9 @@ } }, "spdx-license-ids": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.3.tgz", - "integrity": "sha512-uBIcIl3Ih6Phe3XHK1NqboJLdGfwr1UN3k6wSD1dZpmPsIkb8AGNbZYJ1fOBk834+Gxy8rpfDxrS6XLEMZMY2g==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz", + "integrity": "sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==", "dev": true }, "split-string": { @@ -5036,6 +4836,26 @@ "strip-ansi": "^4.0.0" } }, + "string.prototype.trimend": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz", + "integrity": "sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "string.prototype.trimstart": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz", + "integrity": "sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, "string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -5087,9 +4907,9 @@ } }, "symbol-tree": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.2.tgz", - "integrity": "sha1-rifbOPZgp64uHDt9G8KQgZuFGeY=", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", "dev": true }, "test-exclude": { @@ -5208,20 +5028,14 @@ }, "dependencies": { "json5": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.0.tgz", - "integrity": "sha512-8Mh9h6xViijj36g7Dxi+Y4S6hNGV96vcJZr/SrlHh1LR/pEn/8j/+qIBbs44YKl69Lrfctp4QD+AdWLTMqEZAQ==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz", + "integrity": "sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==", "dev": true, "requires": { - "minimist": "^1.2.0" + "minimist": "^1.2.5" } }, - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true - }, "yargs-parser": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-10.1.0.tgz", @@ -5258,64 +5072,28 @@ } }, "typescript": { - "version": "3.3.3333", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.3.3333.tgz", - "integrity": "sha512-JjSKsAfuHBE/fB2oZ8NxtRTk5iGcg6hkYXMnZ3Wc+b2RSqejEqTaem11mHASMnFilHrax3sLK0GDzcJrekZYLw==", + "version": "3.9.7", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.7.tgz", + "integrity": "sha512-BLbiRkiBzAwsjut4x/dsibSTB6yWpwT5qWmC2OfuCg3GgVQCSgMs4vEctYPhsaGtd0AeuuHMkjZ2h2WG8MSzRw==", "dev": true }, "uglify-js": { - "version": "3.4.9", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.4.9.tgz", - "integrity": "sha512-8CJsbKOtEbnJsTyv6LE6m6ZKniqMiFWmm9sRbopbkGs3gMPPfd3Fh8iIA4Ykv5MgaTbqHr4BaoGLJLZNhsrW1Q==", + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.10.0.tgz", + "integrity": "sha512-Esj5HG5WAyrLIdYU74Z3JdG2PxdIusvj6IWHMtlyESxc7kcDz7zYlYjpnSokn1UbpV0d/QX9fan7gkCNd/9BQA==", "dev": true, - "optional": true, - "requires": { - "commander": "~2.17.1", - "source-map": "~0.6.1" - }, - "dependencies": { - "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, - "optional": true - } - } + "optional": true }, "union-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz", - "integrity": "sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ=", + "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": "^0.4.3" - }, - "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" - } - }, - "set-value": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-0.4.3.tgz", - "integrity": "sha1-fbCPnT0i3H945Trzw79GZuzfzPE=", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-extendable": "^0.1.1", - "is-plain-object": "^2.0.1", - "to-object-path": "^0.3.0" - } - } + "set-value": "^2.0.1" } }, "unset-value": { @@ -5392,19 +5170,21 @@ "dev": true }, "util.promisify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.0.tgz", - "integrity": "sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.1.tgz", + "integrity": "sha512-g9JpC/3He3bm38zsLupWryXHoEcS22YHthuPQSJdMy6KNrzIRzWqcsHzD/WUnqe45whVou4VIsPew37DoXWNrA==", "dev": true, "requires": { - "define-properties": "^1.1.2", - "object.getownpropertydescriptors": "^2.0.3" + "define-properties": "^1.1.3", + "es-abstract": "^1.17.2", + "has-symbols": "^1.0.1", + "object.getownpropertydescriptors": "^2.1.0" } }, "uuid": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", - "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" }, "validate-npm-package-license": { "version": "3.0.4", @@ -5428,12 +5208,12 @@ } }, "w3c-hr-time": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.1.tgz", - "integrity": "sha1-gqwr/2PZUOqeMYmlimViX+3xkEU=", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", + "integrity": "sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==", "dev": true, "requires": { - "browser-process-hrtime": "^0.1.2" + "browser-process-hrtime": "^1.0.0" } }, "walker": { @@ -5453,14 +5233,6 @@ "requires": { "exec-sh": "^0.2.0", "minimist": "^1.2.0" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true - } } }, "webidl-conversions": { @@ -5510,10 +5282,16 @@ "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", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true + }, "wordwrap": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", - "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", "dev": true }, "wrap-ansi": { @@ -5526,6 +5304,12 @@ "strip-ansi": "^3.0.1" }, "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, "is-fullwidth-code-point": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", @@ -5564,9 +5348,9 @@ "dev": true }, "write-file-atomic": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.2.tgz", - "integrity": "sha512-s0b6vB3xIVRLWywa6X9TOMA7k9zio0TMOsl9ZnDkliA/cfJlpHXAscj0gbHVJiTdIuAYpIyqS5GW91fqm6gG5g==", + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.3.tgz", + "integrity": "sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ==", "dev": true, "requires": { "graceful-fs": "^4.1.11", @@ -5595,23 +5379,17 @@ "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=", "dev": true }, - "yallist": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", - "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", - "dev": true - }, "yargs": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-11.1.0.tgz", - "integrity": "sha512-NwW69J42EsCSanF8kyn5upxvjp5ds+t3+udGBeTbFnERA+lF541DDpMawzo4z6W/QrzNM18D+BPMiOBibnFV5A==", + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-11.1.1.tgz", + "integrity": "sha512-PRU7gJrJaXv3q3yQZ/+/X6KBswZiaQ+zOmdprZcouPYtQgvNU35i+68M4b1ZHLZtYFT5QObFLV+ZkmJYcwKdiw==", "dev": true, "requires": { "cliui": "^4.0.0", "decamelize": "^1.1.1", "find-up": "^2.1.0", "get-caller-file": "^1.0.1", - "os-locale": "^2.0.0", + "os-locale": "^3.1.0", "require-directory": "^2.1.1", "require-main-filename": "^1.0.1", "set-blocking": "^2.0.0", diff --git a/packages/event-processor/package.json b/packages/event-processor/package.json index 1d8e6dc3f..d4aa41044 100644 --- a/packages/event-processor/package.json +++ b/packages/event-processor/package.json @@ -40,10 +40,11 @@ }, "dependencies": { "@optimizely/js-sdk-logging": "^0.1.0", - "@optimizely/js-sdk-utils": "^0.3.2" + "@optimizely/js-sdk-utils": "0.2.0" }, "devDependencies": { "@react-native-community/netinfo": "^5.9.4", + "@react-native-community/async-storage": "^1.2.0", "@types/jest": "^24.0.9", "jest": "^23.6.0", "jest-localstorage-mock": "^2.4.0", @@ -51,6 +52,7 @@ "typescript": "^3.3.3333" }, "peerDependencies": { - "@react-native-community/netinfo": "5.9.4" + "@react-native-community/netinfo": "5.9.4", + "@react-native-community/async-storage": "^1.2.0" } } diff --git a/packages/event-processor/src/persistentKeyValueCache.ts b/packages/event-processor/src/persistentKeyValueCache.ts new file mode 100644 index 000000000..9b3ef9fac --- /dev/null +++ b/packages/event-processor/src/persistentKeyValueCache.ts @@ -0,0 +1,60 @@ +/** + * Copyright 2020, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * An Interface to implement a persistent key value cache which supports strings as keys + * and JSON Object as value. + */ +export default interface PersistentKeyValueCache { + /** + * Returns value stored against a key or null if not found. + * @param key + * @returns + * Resolves promise with + * 1. Object if value found was stored as a JSON Object. + * 2. null if the key does not exist in the cache. + * Rejects the promise in case of an error + */ + get(key: string): Promise; + + /** + * Stores Object in the persistent cache against a key + * @param key + * @param val + * @returns + * Resolves promise without a value if successful + * Rejects the promise in case of an error + */ + set(key: string, val: any): Promise; + + /** + * Checks if a key exists in the cache + * @param key + * Resolves promise with + * 1. true if the key exists + * 2. false if the key does not exist + * Rejects the promise in case of an error + */ + contains(key: string): Promise; + + /** + * Removes the key value pair from cache. + * @param key + * Resolves promise without a value if successful + * Rejects the promise in case of an error + */ + remove(key: string): Promise; +} diff --git a/packages/event-processor/src/reactNativeAsyncStorageCache.ts b/packages/event-processor/src/reactNativeAsyncStorageCache.ts new file mode 100644 index 000000000..7f488617a --- /dev/null +++ b/packages/event-processor/src/reactNativeAsyncStorageCache.ts @@ -0,0 +1,49 @@ +/** + * Copyright 2020, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import AsyncStorage from '@react-native-community/async-storage'; + +import PersistentKeyValueCache from './persistentKeyValueCache'; + +export default class ReactNativeAsyncStorageCache implements PersistentKeyValueCache { + get(key: string): Promise { + return AsyncStorage.getItem(key).then((val: string | null) => { + if (!val) { + return null; + } + try { + return JSON.parse(val); + } catch (ex) { + throw ex; + } + }); + } + + set(key: string, val: any): Promise { + try { + return AsyncStorage.setItem(key, JSON.stringify(val)); + } catch (ex) { + return Promise.reject(ex); + } + } + + contains(key: string): Promise { + return AsyncStorage.getItem(key).then((val: string | null) => val !== null); + } + + remove(key: string): Promise { + return AsyncStorage.removeItem(key); + } +} diff --git a/packages/event-processor/src/reactNativeEventsStore.ts b/packages/event-processor/src/reactNativeEventsStore.ts index a09d6f57f..e518b7a73 100644 --- a/packages/event-processor/src/reactNativeEventsStore.ts +++ b/packages/event-processor/src/reactNativeEventsStore.ts @@ -15,7 +15,9 @@ * limitations under the License. */ import { getLogger } from '@optimizely/js-sdk-logging' -import { ReactNativeAsyncStorageCache, objectValues } from "@optimizely/js-sdk-utils" +import { objectValues } from "@optimizely/js-sdk-utils" + +import ReactNativeAsyncStorageCache from './reactNativeAsyncStorageCache' const logger = getLogger('ReactNativeEventsStore') diff --git a/packages/event-processor/src/v1/v1EventProcessor.ts b/packages/event-processor/src/v1/v1EventProcessor.ts index 2973bc9d1..e6559060f 100644 --- a/packages/event-processor/src/v1/v1EventProcessor.ts +++ b/packages/event-processor/src/v1/v1EventProcessor.ts @@ -94,7 +94,7 @@ export class LogTierV1EventProcessor implements EventProcessor { return Promise.resolve() } - start(): void { + async start(): Promise { this.queue.start() } } From a028ffd73468226e4aa096610605eb1beab45943 Mon Sep 17 00:00:00 2001 From: Zeeshan Ashraf Date: Fri, 17 Jul 2020 16:12:06 -0700 Subject: [PATCH 25/28] added unit tests and fixed some issues found during unit testing --- .../@react-native-community/async-storage.ts | 6 +- .../v1EventProcessor.react_native.spec.ts | 476 +++++++++++++++++- .../src/reactNativeEventsStore.ts | 31 +- packages/event-processor/src/synchronizer.ts | 42 ++ .../src/v1/v1EventProcessor.react_native.ts | 19 +- 5 files changed, 528 insertions(+), 46 deletions(-) create mode 100644 packages/event-processor/src/synchronizer.ts diff --git a/packages/event-processor/__mocks__/@react-native-community/async-storage.ts b/packages/event-processor/__mocks__/@react-native-community/async-storage.ts index 3c5e5f6fc..6e1109103 100644 --- a/packages/event-processor/__mocks__/@react-native-community/async-storage.ts +++ b/packages/event-processor/__mocks__/@react-native-community/async-storage.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -const items: {[key: string]: string} = {} +let items: {[key: string]: string} = {} export default class AsyncStorage { @@ -44,4 +44,8 @@ export default class AsyncStorage { static dumpItems(): {[key: string]: string} { return items } + + static clearStore(): void { + items = {} + } } diff --git a/packages/event-processor/__tests__/v1EventProcessor.react_native.spec.ts b/packages/event-processor/__tests__/v1EventProcessor.react_native.spec.ts index 63d6bcddc..ad0401945 100644 --- a/packages/event-processor/__tests__/v1EventProcessor.react_native.spec.ts +++ b/packages/event-processor/__tests__/v1EventProcessor.react_native.spec.ts @@ -14,14 +14,478 @@ * limitations under the License. */ /// +import { NotificationCenter, NOTIFICATION_TYPES } from '@optimizely/js-sdk-utils' + +import { LogTierV1EventProcessor } from '../src/v1/v1EventProcessor.react_native' +import { + EventDispatcher, + EventV1Request, + EventDispatcherCallback, +} from '../src/eventDispatcher' +import { EventProcessor } from '../src/eventProcessor' +import { buildImpressionEventV1, makeBatchedEventV1 } from '../src/v1/buildEventV1' +import AsyncStorage from '../__mocks__/@react-native-community/async-storage' + +function createImpressionEvent() { + return { + type: 'impression' as 'impression', + timestamp: 69, + uuid: 'uuid', + + context: { + accountId: 'accountId', + projectId: 'projectId', + clientName: 'node-sdk', + clientVersion: '3.0.0', + revision: '1', + botFiltering: true, + anonymizeIP: true, + }, + + user: { + id: 'userId', + attributes: [{ entityId: 'attr1-id', key: 'attr1-key', value: 'attr1-value' }], + }, + + layer: { + id: 'layerId', + }, + + experiment: { + id: 'expId', + key: 'expKey', + }, + + variation: { + id: 'varId', + key: 'varKey', + }, + } +} + +function createConversionEvent() { + return { + type: 'conversion' as 'conversion', + timestamp: 69, + uuid: 'uuid', + + context: { + accountId: 'accountId', + projectId: 'projectId', + clientName: 'node-sdk', + clientVersion: '3.0.0', + revision: '1', + botFiltering: true, + anonymizeIP: true, + }, + + user: { + id: 'userId', + attributes: [{ entityId: 'attr1-id', key: 'attr1-key', value: 'attr1-value' }], + }, + + event: { + id: 'event-id', + key: 'event-key', + }, + + tags: { + foo: 'bar', + value: '123', + revenue: '1000', + }, + + revenue: 1000, + value: 123, + } +} describe('LogTierV1EventProcessorReactNative', () => { + let stubDispatcher: EventDispatcher + let dispatchStub: jest.Mock + // TODO change this to ProjectConfig when js-sdk-models is available + let testProjectConfig: any + + beforeEach(() => { + //jest.useFakeTimers() + testProjectConfig = {} + dispatchStub = jest.fn() + + stubDispatcher = { + dispatchEvent(event: EventV1Request, callback: EventDispatcherCallback): void { + dispatchStub(event) + callback({ statusCode: 200 }) + }, + } + }) + + afterEach(() => { + jest.resetAllMocks() + AsyncStorage.clearStore() + }) + + describe('stop()', () => { + let localCallback: EventDispatcherCallback + beforeEach(async () => { + stubDispatcher = { + dispatchEvent(event: EventV1Request, callback: EventDispatcherCallback): void { + dispatchStub(event) + localCallback = callback + }, + } + }) + + it('should return a resolved promise when there is nothing in queue', async () => { + const processor = new LogTierV1EventProcessor({ + dispatcher: stubDispatcher, + flushInterval: 100, + batchSize: 100, + }) + + await processor.start() + + await processor.stop() + }) + + it('should return a promise that is resolved when the dispatcher callback returns a 200 response', async (done) => { + const processor = new LogTierV1EventProcessor({ + dispatcher: stubDispatcher, + flushInterval: 100, + batchSize: 100, + }) + await processor.start() + const impressionEvent = createImpressionEvent() + processor.process(impressionEvent) + + await new Promise(resolve => setTimeout(resolve, 150)) + processor.stop().then(() => { + done() + }) + + localCallback({ statusCode: 200 }) + }) - describe('VERIFYING EXISTING EVENT PROCESSOR TESTS ON REACT NATIVE EVENT PROCESSOR', () => { - // Copy all the existing browser/node event processor test here and modify them to work. - // They wont work as they are because we have changed a lot of functions to asyn for react native EP. - }) + it('should return a promise that is resolved when the dispatcher callback returns a 400 response', async (done) => { + // This test is saying that even if the request fails to send but + // the `dispatcher` yielded control back, then the `.stop()` promise should be resolved + let localCallback: any + stubDispatcher = { + dispatchEvent(event: EventV1Request, callback: EventDispatcherCallback): void { + dispatchStub(event) + localCallback = callback + }, + } + const processor = new LogTierV1EventProcessor({ + dispatcher: stubDispatcher, + flushInterval: 100, + batchSize: 100, + }) + await processor.start() + + const impressionEvent = createImpressionEvent() + processor.process(impressionEvent) + + await new Promise(resolve => setTimeout(resolve, 150)) + processor.stop().then(() => { + done() + }) + + localCallback({ statusCode: 400 }) + }) + + it('should return a promise when multiple event batches are sent', async () => { + await new Promise(resolve => setTimeout(resolve, 2000)) + stubDispatcher = { + dispatchEvent(event: EventV1Request, callback: EventDispatcherCallback): void { + dispatchStub(event) + callback({ statusCode: 200 }) + }, + } + + const processor = new LogTierV1EventProcessor({ + dispatcher: stubDispatcher, + flushInterval: 100, + batchSize: 100, + }) + + await processor.start() + + const impressionEvent1 = createImpressionEvent() + const impressionEvent2 = createImpressionEvent() + impressionEvent2.context.revision = '2' + processor.process(impressionEvent1) + processor.process(impressionEvent2) + + await new Promise(resolve => setTimeout(resolve, 150)) + await processor.stop() + expect(dispatchStub).toBeCalledTimes(2) + }) + + it('should stop accepting events after stop is called', async () => { + const dispatcher = { + dispatchEvent: jest.fn((event: EventV1Request, callback: EventDispatcherCallback) => { + setTimeout(() => callback({ statusCode: 204 }), 0) + }) + } + const processor = new LogTierV1EventProcessor({ + dispatcher, + flushInterval: 100, + batchSize: 3, + }) + await processor.start() + + const impressionEvent1 = createImpressionEvent() + processor.process(impressionEvent1) + await new Promise(resolve => setTimeout(resolve, 200)) + + await processor.stop() + // calling stop should haver flushed the current batch of size 1 + expect(dispatcher.dispatchEvent).toBeCalledTimes(1) + + dispatcher.dispatchEvent.mockClear(); + + // From now on, subsequent events should be ignored. + // Process 3 more, which ordinarily would have triggered + // a flush due to the batch size. + const impressionEvent2 = createImpressionEvent() + processor.process(impressionEvent2) + const impressionEvent3 = createImpressionEvent() + processor.process(impressionEvent3) + const impressionEvent4 = createImpressionEvent() + processor.process(impressionEvent4) + // Since we already stopped the processor, the dispatcher should + // not have been called again. + await new Promise(resolve => setTimeout(resolve, 150)) + expect(dispatcher.dispatchEvent).toBeCalledTimes(0) + }) + }) + + describe('when batchSize = 1', () => { + let processor: EventProcessor + beforeEach(async () => { + processor = new LogTierV1EventProcessor({ + dispatcher: stubDispatcher, + flushInterval: 100, + batchSize: 1, + }) + await processor.start() + }) + + afterEach(async () => { + await processor.stop() + }) + + it('should immediately flush events as they are processed', async () => { + const impressionEvent = createImpressionEvent() + processor.process(impressionEvent) + + await new Promise(resolve => setTimeout(resolve, 50)) + + expect(dispatchStub).toHaveBeenCalledTimes(1) + expect(dispatchStub).toHaveBeenCalledWith({ + url: 'https://logx.optimizely.com/v1/events', + httpVerb: 'POST', + params: buildImpressionEventV1(impressionEvent), + }) + }) + }) + + describe('when batchSize = 3, flushInterval = 300', () => { + let processor: EventProcessor + beforeEach(async () => { + processor = new LogTierV1EventProcessor({ + dispatcher: stubDispatcher, + flushInterval: 300, + batchSize: 3, + }) + await processor.start() + }) + + afterEach(async () => { + await processor.stop() + }) + + it('should wait until 3 events to be in the queue before it flushes', async () => { + const impressionEvent1 = createImpressionEvent() + const impressionEvent2 = createImpressionEvent() + const impressionEvent3 = createImpressionEvent() + + processor.process(impressionEvent1) + processor.process(impressionEvent2) + + await new Promise(resolve => setTimeout(resolve, 150)) + expect(dispatchStub).toHaveBeenCalledTimes(0) + + processor.process(impressionEvent3) + + await new Promise(resolve => setTimeout(resolve, 150)) + expect(dispatchStub).toHaveBeenCalledTimes(1) + expect(dispatchStub).toHaveBeenCalledWith({ + url: 'https://logx.optimizely.com/v1/events', + httpVerb: 'POST', + params: makeBatchedEventV1([ + impressionEvent1, + impressionEvent2, + impressionEvent3, + ]), + }) + }) + + it('should flush the current batch when it receives an event with a different context revision than the current batch', async () => { + const impressionEvent1 = createImpressionEvent() + const conversionEvent = createConversionEvent() + const impressionEvent2 = createImpressionEvent() + + // createImpressionEvent and createConversionEvent create events with revision '1' + // We modify this one's revision to '2' in order to test that the queue is flushed + // when an event with a different revision is processed. + impressionEvent2.context.revision = '2' + + processor.process(impressionEvent1) + processor.process(conversionEvent) + + await new Promise(resolve => setTimeout(resolve, 150)) + expect(dispatchStub).toHaveBeenCalledTimes(0) + + processor.process(impressionEvent2) + + await new Promise(resolve => setTimeout(resolve, 150)) + expect(dispatchStub).toHaveBeenCalledTimes(1) + expect(dispatchStub).toHaveBeenCalledWith({ + url: 'https://logx.optimizely.com/v1/events', + httpVerb: 'POST', + params: makeBatchedEventV1([impressionEvent1, conversionEvent]), + }) + }) + + it('should flush the current batch when it receives an event with a different context projectId than the current batch', async () => { + const impressionEvent1 = createImpressionEvent() + const conversionEvent = createConversionEvent() + const impressionEvent2 = createImpressionEvent() + + impressionEvent2.context.projectId = 'projectId2' + + processor.process(impressionEvent1) + processor.process(conversionEvent) + + await new Promise(resolve => setTimeout(resolve, 150)) + expect(dispatchStub).toHaveBeenCalledTimes(0) + + processor.process(impressionEvent2) + + await new Promise(resolve => setTimeout(resolve, 150)) + expect(dispatchStub).toHaveBeenCalledTimes(1) + expect(dispatchStub).toHaveBeenCalledWith({ + url: 'https://logx.optimizely.com/v1/events', + httpVerb: 'POST', + params: makeBatchedEventV1([impressionEvent1, conversionEvent]), + }) + }) + + it('should flush the queue when the flush interval happens', async () => { + const impressionEvent1 = createImpressionEvent() + + processor.process(impressionEvent1) + + expect(dispatchStub).toHaveBeenCalledTimes(0) + + await new Promise(resolve => setTimeout(resolve, 350)) + + expect(dispatchStub).toHaveBeenCalledTimes(1) + expect(dispatchStub).toHaveBeenCalledWith({ + url: 'https://logx.optimizely.com/v1/events', + httpVerb: 'POST', + params: makeBatchedEventV1([impressionEvent1]), + }) + + processor.process(createImpressionEvent()) + processor.process(createImpressionEvent()) + // flushing should reset queue, at this point only has two events + expect(dispatchStub).toHaveBeenCalledTimes(1) + }) + }) + + describe('when a notification center is provided', () => { + it('should trigger a notification when the event dispatcher dispatches an event', async () => { + const dispatcher: EventDispatcher = { + dispatchEvent: jest.fn() + } + + const notificationCenter: NotificationCenter = { + sendNotifications: jest.fn() + } + + const processor = new LogTierV1EventProcessor({ + dispatcher, + notificationCenter, + batchSize: 1, + }) + await processor.start() + + const impressionEvent1 = createImpressionEvent() + processor.process(impressionEvent1) + + await new Promise(resolve => setTimeout(resolve, 150)) + expect(notificationCenter.sendNotifications).toBeCalledTimes(1) + const event = (dispatcher.dispatchEvent as jest.Mock).mock.calls[0][0] + expect(notificationCenter.sendNotifications).toBeCalledWith(NOTIFICATION_TYPES.LOG_EVENT, event) + }) + }) + + describe('invalid flushInterval or batchSize', () => { + it.skip('should ignore a flushInterval of 0 and use the default', () => { + const processor = new LogTierV1EventProcessor({ + dispatcher: stubDispatcher, + flushInterval: 0, + batchSize: 10, + }) + processor.start() + + const impressionEvent1 = createImpressionEvent() + processor.process(impressionEvent1) + expect(dispatchStub).toHaveBeenCalledTimes(0) + jest.advanceTimersByTime(30000) + expect(dispatchStub).toHaveBeenCalledTimes(1) + expect(dispatchStub).toHaveBeenCalledWith({ + url: 'https://logx.optimizely.com/v1/events', + httpVerb: 'POST', + params: makeBatchedEventV1([impressionEvent1]), + }) + }) + + it('should ignore a batchSize of 0 and use the default', async () => { + const processor = new LogTierV1EventProcessor({ + dispatcher: stubDispatcher, + flushInterval: 30000, + batchSize: 0, + }) + await processor.start() + + const impressionEvent1 = createImpressionEvent() + processor.process(impressionEvent1) + + await new Promise(resolve => setTimeout(resolve, 150)) + expect(dispatchStub).toHaveBeenCalledTimes(0) + const impressionEvents = [impressionEvent1] + for (let i = 0; i < 9; i++) { + const evt = createImpressionEvent() + processor.process(evt) + impressionEvents.push(evt) + } + + await new Promise(resolve => setTimeout(resolve, 150)) + expect(dispatchStub).toHaveBeenCalledTimes(1) + expect(dispatchStub).toHaveBeenCalledWith({ + url: 'https://logx.optimizely.com/v1/events', + httpVerb: 'POST', + params: makeBatchedEventV1(impressionEvents), + }) + }) + }) +}) + +describe.skip('LogTierV1EventProcessorReactNative2', () => { describe('Sequence', () => { it('should dispatch pending events in correct sequence', () => { // Add Pending events to the store and that try dispatching them and check the sequence @@ -86,8 +550,4 @@ describe('LogTierV1EventProcessorReactNative', () => { }) }) - - describe('stop', () => { - // Add tests to make sure stop behaves correctly - }) }) diff --git a/packages/event-processor/src/reactNativeEventsStore.ts b/packages/event-processor/src/reactNativeEventsStore.ts index e518b7a73..ff303f01c 100644 --- a/packages/event-processor/src/reactNativeEventsStore.ts +++ b/packages/event-processor/src/reactNativeEventsStore.ts @@ -17,6 +17,7 @@ import { getLogger } from '@optimizely/js-sdk-logging' import { objectValues } from "@optimizely/js-sdk-utils" +import { Synchronizer } from './synchronizer' import ReactNativeAsyncStorageCache from './reactNativeAsyncStorageCache' const logger = getLogger('ReactNativeEventsStore') @@ -78,33 +79,3 @@ export class ReactNativeEventsStore { await this.cache.remove(this.storeKey) } } - -/** - * Both the above stores use single entry in the async storage store to manage their maps and lists. - * This results in race condition when two items are added to the map or array in parallel. - * for ex. Req 1 gets the map. Req 2 gets the map. Req 1 sets the map. Req 2 sets the map. The map now loses item from Req 1. - * This synchronizer makes sure the operations are atomic using promises. - */ -class Synchronizer { - private lockPromises: Promise[] = [] - private resolvers: any[] = [] - - // Adds a promise to the existing list and returns the promise so that the code block can wait for its turn - public async getLock(): Promise { - this.lockPromises.push(new Promise(resolve => this.resolvers.push(resolve))) - if (this.lockPromises.length === 1) { - return - } - await this.lockPromises[this.lockPromises.length - 2] - } - - // Resolves first promise in the array so that the code block waiting on the first promise can continue execution - public releaseLock(): void { - if (this.lockPromises.length > 0) { - this.lockPromises.shift() - const resolver = this.resolvers.shift() - resolver() - return - } - } -} diff --git a/packages/event-processor/src/synchronizer.ts b/packages/event-processor/src/synchronizer.ts new file mode 100644 index 000000000..2d5d861f2 --- /dev/null +++ b/packages/event-processor/src/synchronizer.ts @@ -0,0 +1,42 @@ +/** + * Copyright 2020, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * This synchronizer makes sure the operations are atomic using promises. + */ +export class Synchronizer { + private lockPromises: Promise[] = [] + private resolvers: any[] = [] + + // Adds a promise to the existing list and returns the promise so that the code block can wait for its turn + public async getLock(): Promise { + this.lockPromises.push(new Promise(resolve => this.resolvers.push(resolve))) + if (this.lockPromises.length === 1) { + return + } + await this.lockPromises[this.lockPromises.length - 2] + } + + // Resolves first promise in the array so that the code block waiting on the first promise can continue execution + public releaseLock(): void { + if (this.lockPromises.length > 0) { + this.lockPromises.shift() + const resolver = this.resolvers.shift() + resolver() + return + } + } +} diff --git a/packages/event-processor/src/v1/v1EventProcessor.react_native.ts b/packages/event-processor/src/v1/v1EventProcessor.react_native.ts index 804630c50..a75cdcaf0 100644 --- a/packages/event-processor/src/v1/v1EventProcessor.react_native.ts +++ b/packages/event-processor/src/v1/v1EventProcessor.react_native.ts @@ -35,6 +35,7 @@ import { DEFAULT_FLUSH_INTERVAL, } from "../eventProcessor" import { ReactNativeEventsStore } from '../reactNativeEventsStore' +import { Synchronizer } from '../synchronizer' import { EventQueue } from '../eventQueue' import RequestTracker from '../requestTracker' import { areEventContextsEqual } from '../events' @@ -63,6 +64,7 @@ export class LogTierV1EventProcessor implements EventProcessor { private unsubscribeNetInfo: Function | null = null private isInternetReachable: boolean = true private pendingEventsPromise: Promise | null = null + private synchronizer: Synchronizer = new Synchronizer() // If a pending event fails to dispatch, this indicates skipping further events to preserve sequence in the next retry. private shouldSkipDispatchToPreserveSequence: boolean = false @@ -123,13 +125,15 @@ export class LogTierV1EventProcessor implements EventProcessor { } private async drainQueue(buffer: ProcessableEvent[]): Promise { - // Retry pending failed events while draining queue - await this.processPendingEvents() - - if (buffer.length === 0) { + if (buffer.length === 0) { return } + await this.synchronizer.getLock() + + // Retry pending failed events while draining queue + await this.processPendingEvents() + logger.debug('draining queue with %s events', buffer.length) const eventCacheKey = generateUUID() @@ -147,6 +151,8 @@ export class LogTierV1EventProcessor implements EventProcessor { // Resetting skip flag because current sequence of events have all been processed this.shouldSkipDispatchToPreserveSequence = false + + this.synchronizer.releaseLock() } private async processPendingEvents(): Promise { @@ -212,15 +218,14 @@ export class LogTierV1EventProcessor implements EventProcessor { this.queue.enqueue(event) } - public stop(): Promise { + public async stop(): Promise { // swallow - an error stopping this queue shouldn't prevent this from stopping try { this.unsubscribeNetInfo && this.unsubscribeNetInfo() - this.queue.stop() + await this.queue.stop() return this.requestTracker.onRequestsComplete() } catch (e) { logger.error('Error stopping EventProcessor: "%s"', e.message, e) } - return Promise.resolve() } } From b92d95d7cb56fe291f506cec0c2bbd1604c9ae3a Mon Sep 17 00:00:00 2001 From: Zeeshan Ashraf Date: Sat, 18 Jul 2020 00:09:12 -0700 Subject: [PATCH 26/28] added more test and fixed an issue with buffer store --- .../v1EventProcessor.react_native.spec.ts | 867 ++++++++++-------- .../src/v1/v1EventProcessor.react_native.ts | 12 +- 2 files changed, 518 insertions(+), 361 deletions(-) diff --git a/packages/event-processor/__tests__/v1EventProcessor.react_native.spec.ts b/packages/event-processor/__tests__/v1EventProcessor.react_native.spec.ts index ad0401945..30f856293 100644 --- a/packages/event-processor/__tests__/v1EventProcessor.react_native.spec.ts +++ b/packages/event-processor/__tests__/v1EventProcessor.react_native.spec.ts @@ -22,9 +22,11 @@ import { EventV1Request, EventDispatcherCallback, } from '../src/eventDispatcher' -import { EventProcessor } from '../src/eventProcessor' +import { EventProcessor, ProcessableEvent } from '../src/eventProcessor' import { buildImpressionEventV1, makeBatchedEventV1 } from '../src/v1/buildEventV1' import AsyncStorage from '../__mocks__/@react-native-community/async-storage' +import { DefaultEventQueue } from '../src/eventQueue' +import { ReactNativeEventsStore } from '../src/reactNativeEventsStore' function createImpressionEvent() { return { @@ -101,453 +103,604 @@ function createConversionEvent() { } describe('LogTierV1EventProcessorReactNative', () => { - let stubDispatcher: EventDispatcher - let dispatchStub: jest.Mock - // TODO change this to ProjectConfig when js-sdk-models is available - let testProjectConfig: any - - beforeEach(() => { - //jest.useFakeTimers() - testProjectConfig = {} - dispatchStub = jest.fn() - - stubDispatcher = { - dispatchEvent(event: EventV1Request, callback: EventDispatcherCallback): void { - dispatchStub(event) - callback({ statusCode: 200 }) - }, - } - }) + describe('New Events', () => { + let stubDispatcher: EventDispatcher + let dispatchStub: jest.Mock - afterEach(() => { - jest.resetAllMocks() - AsyncStorage.clearStore() - }) + beforeEach(() => { + dispatchStub = jest.fn() - describe('stop()', () => { - let localCallback: EventDispatcherCallback - beforeEach(async () => { stubDispatcher = { - dispatchEvent(event: EventV1Request, callback: EventDispatcherCallback): void { + dispatchEvent(event: EventV1Request, callback: EventDispatcherCallback): void { dispatchStub(event) - localCallback = callback + callback({ statusCode: 200 }) }, } }) - it('should return a resolved promise when there is nothing in queue', async () => { - const processor = new LogTierV1EventProcessor({ - dispatcher: stubDispatcher, - flushInterval: 100, - batchSize: 100, - }) - - await processor.start() - - await processor.stop() + afterEach(() => { + jest.resetAllMocks() + AsyncStorage.clearStore() }) - it('should return a promise that is resolved when the dispatcher callback returns a 200 response', async (done) => { - const processor = new LogTierV1EventProcessor({ - dispatcher: stubDispatcher, - flushInterval: 100, - batchSize: 100, + describe('stop()', () => { + let localCallback: EventDispatcherCallback + beforeEach(async () => { + stubDispatcher = { + dispatchEvent(event: EventV1Request, callback: EventDispatcherCallback): void { + dispatchStub(event) + localCallback = callback + }, + } }) - await processor.start() - const impressionEvent = createImpressionEvent() - processor.process(impressionEvent) - await new Promise(resolve => setTimeout(resolve, 150)) - processor.stop().then(() => { - done() + it('should return a resolved promise when there is nothing in queue', async () => { + const processor = new LogTierV1EventProcessor({ + dispatcher: stubDispatcher, + flushInterval: 100, + batchSize: 100, + }) + + await processor.start() + + await processor.stop() }) - localCallback({ statusCode: 200 }) - }) + it('should return a promise that is resolved when the dispatcher callback returns a 200 response', async (done) => { + const processor = new LogTierV1EventProcessor({ + dispatcher: stubDispatcher, + flushInterval: 100, + batchSize: 100, + }) + await processor.start() + const impressionEvent = createImpressionEvent() + processor.process(impressionEvent) - it('should return a promise that is resolved when the dispatcher callback returns a 400 response', async (done) => { - // This test is saying that even if the request fails to send but - // the `dispatcher` yielded control back, then the `.stop()` promise should be resolved - let localCallback: any - stubDispatcher = { - dispatchEvent(event: EventV1Request, callback: EventDispatcherCallback): void { - dispatchStub(event) - localCallback = callback - }, - } + await new Promise(resolve => setTimeout(resolve, 150)) + processor.stop().then(() => { + done() + }) - const processor = new LogTierV1EventProcessor({ - dispatcher: stubDispatcher, - flushInterval: 100, - batchSize: 100, + localCallback({ statusCode: 200 }) }) - await processor.start() - const impressionEvent = createImpressionEvent() - processor.process(impressionEvent) + it('should return a promise that is resolved when the dispatcher callback returns a 400 response', async (done) => { + // This test is saying that even if the request fails to send but + // the `dispatcher` yielded control back, then the `.stop()` promise should be resolved + let localCallback: any + stubDispatcher = { + dispatchEvent(event: EventV1Request, callback: EventDispatcherCallback): void { + dispatchStub(event) + localCallback = callback + }, + } + + const processor = new LogTierV1EventProcessor({ + dispatcher: stubDispatcher, + flushInterval: 100, + batchSize: 100, + }) + await processor.start() + + const impressionEvent = createImpressionEvent() + processor.process(impressionEvent) + + await new Promise(resolve => setTimeout(resolve, 150)) + processor.stop().then(() => { + done() + }) - await new Promise(resolve => setTimeout(resolve, 150)) - processor.stop().then(() => { - done() + localCallback({ statusCode: 400 }) }) - localCallback({ statusCode: 400 }) - }) + it('should return a promise when multiple event batches are sent', async () => { + await new Promise(resolve => setTimeout(resolve, 2000)) + stubDispatcher = { + dispatchEvent(event: EventV1Request, callback: EventDispatcherCallback): void { + dispatchStub(event) + callback({ statusCode: 200 }) + }, + } + + const processor = new LogTierV1EventProcessor({ + dispatcher: stubDispatcher, + flushInterval: 100, + batchSize: 100, + }) - it('should return a promise when multiple event batches are sent', async () => { - await new Promise(resolve => setTimeout(resolve, 2000)) - stubDispatcher = { - dispatchEvent(event: EventV1Request, callback: EventDispatcherCallback): void { - dispatchStub(event) - callback({ statusCode: 200 }) - }, - } + await processor.start() - const processor = new LogTierV1EventProcessor({ - dispatcher: stubDispatcher, - flushInterval: 100, - batchSize: 100, + const impressionEvent1 = createImpressionEvent() + const impressionEvent2 = createImpressionEvent() + impressionEvent2.context.revision = '2' + processor.process(impressionEvent1) + processor.process(impressionEvent2) + + await new Promise(resolve => setTimeout(resolve, 150)) + await processor.stop() + expect(dispatchStub).toBeCalledTimes(2) }) - await processor.start() - - const impressionEvent1 = createImpressionEvent() - const impressionEvent2 = createImpressionEvent() - impressionEvent2.context.revision = '2' - processor.process(impressionEvent1) - processor.process(impressionEvent2) - - await new Promise(resolve => setTimeout(resolve, 150)) - await processor.stop() - expect(dispatchStub).toBeCalledTimes(2) - }) - - it('should stop accepting events after stop is called', async () => { - const dispatcher = { - dispatchEvent: jest.fn((event: EventV1Request, callback: EventDispatcherCallback) => { - setTimeout(() => callback({ statusCode: 204 }), 0) + it('should stop accepting events after stop is called', async () => { + const dispatcher = { + dispatchEvent: jest.fn((event: EventV1Request, callback: EventDispatcherCallback) => { + setTimeout(() => callback({ statusCode: 204 }), 0) + }) + } + const processor = new LogTierV1EventProcessor({ + dispatcher, + flushInterval: 100, + batchSize: 3, }) - } - const processor = new LogTierV1EventProcessor({ - dispatcher, - flushInterval: 100, - batchSize: 3, + await processor.start() + + const impressionEvent1 = createImpressionEvent() + processor.process(impressionEvent1) + await new Promise(resolve => setTimeout(resolve, 200)) + + await processor.stop() + // calling stop should haver flushed the current batch of size 1 + expect(dispatcher.dispatchEvent).toBeCalledTimes(1) + + dispatcher.dispatchEvent.mockClear(); + + // From now on, subsequent events should be ignored. + // Process 3 more, which ordinarily would have triggered + // a flush due to the batch size. + const impressionEvent2 = createImpressionEvent() + processor.process(impressionEvent2) + const impressionEvent3 = createImpressionEvent() + processor.process(impressionEvent3) + const impressionEvent4 = createImpressionEvent() + processor.process(impressionEvent4) + // Since we already stopped the processor, the dispatcher should + // not have been called again. + await new Promise(resolve => setTimeout(resolve, 150)) + expect(dispatcher.dispatchEvent).toBeCalledTimes(0) }) - await processor.start() - - const impressionEvent1 = createImpressionEvent() - processor.process(impressionEvent1) - await new Promise(resolve => setTimeout(resolve, 200)) - - await processor.stop() - // calling stop should haver flushed the current batch of size 1 - expect(dispatcher.dispatchEvent).toBeCalledTimes(1) - - dispatcher.dispatchEvent.mockClear(); - - // From now on, subsequent events should be ignored. - // Process 3 more, which ordinarily would have triggered - // a flush due to the batch size. - const impressionEvent2 = createImpressionEvent() - processor.process(impressionEvent2) - const impressionEvent3 = createImpressionEvent() - processor.process(impressionEvent3) - const impressionEvent4 = createImpressionEvent() - processor.process(impressionEvent4) - // Since we already stopped the processor, the dispatcher should - // not have been called again. - await new Promise(resolve => setTimeout(resolve, 150)) - expect(dispatcher.dispatchEvent).toBeCalledTimes(0) }) - }) - describe('when batchSize = 1', () => { - let processor: EventProcessor - beforeEach(async () => { - processor = new LogTierV1EventProcessor({ - dispatcher: stubDispatcher, - flushInterval: 100, - batchSize: 1, + describe('when batchSize = 1', () => { + let processor: EventProcessor + beforeEach(async () => { + processor = new LogTierV1EventProcessor({ + dispatcher: stubDispatcher, + flushInterval: 100, + batchSize: 1, + }) + await processor.start() }) - await processor.start() - }) - afterEach(async () => { - await processor.stop() - }) + afterEach(async () => { + await processor.stop() + }) - it('should immediately flush events as they are processed', async () => { - const impressionEvent = createImpressionEvent() - processor.process(impressionEvent) + it('should immediately flush events as they are processed', async () => { + const impressionEvent = createImpressionEvent() + processor.process(impressionEvent) - await new Promise(resolve => setTimeout(resolve, 50)) + await new Promise(resolve => setTimeout(resolve, 50)) - expect(dispatchStub).toHaveBeenCalledTimes(1) - expect(dispatchStub).toHaveBeenCalledWith({ - url: 'https://logx.optimizely.com/v1/events', - httpVerb: 'POST', - params: buildImpressionEventV1(impressionEvent), + expect(dispatchStub).toHaveBeenCalledTimes(1) + expect(dispatchStub).toHaveBeenCalledWith({ + url: 'https://logx.optimizely.com/v1/events', + httpVerb: 'POST', + params: buildImpressionEventV1(impressionEvent), + }) }) }) - }) - describe('when batchSize = 3, flushInterval = 300', () => { - let processor: EventProcessor - beforeEach(async () => { - processor = new LogTierV1EventProcessor({ - dispatcher: stubDispatcher, - flushInterval: 300, - batchSize: 3, + describe('when batchSize = 3, flushInterval = 300', () => { + let processor: EventProcessor + beforeEach(async () => { + processor = new LogTierV1EventProcessor({ + dispatcher: stubDispatcher, + flushInterval: 300, + batchSize: 3, + }) + await processor.start() }) - await processor.start() - }) - afterEach(async () => { - await processor.stop() - }) + afterEach(async () => { + await processor.stop() + }) - it('should wait until 3 events to be in the queue before it flushes', async () => { - const impressionEvent1 = createImpressionEvent() - const impressionEvent2 = createImpressionEvent() - const impressionEvent3 = createImpressionEvent() - - processor.process(impressionEvent1) - processor.process(impressionEvent2) - - await new Promise(resolve => setTimeout(resolve, 150)) - expect(dispatchStub).toHaveBeenCalledTimes(0) - - processor.process(impressionEvent3) - - await new Promise(resolve => setTimeout(resolve, 150)) - expect(dispatchStub).toHaveBeenCalledTimes(1) - expect(dispatchStub).toHaveBeenCalledWith({ - url: 'https://logx.optimizely.com/v1/events', - httpVerb: 'POST', - params: makeBatchedEventV1([ - impressionEvent1, - impressionEvent2, - impressionEvent3, - ]), - }) - }) + it('should wait until 3 events to be in the queue before it flushes', async () => { + const impressionEvent1 = createImpressionEvent() + const impressionEvent2 = createImpressionEvent() + const impressionEvent3 = createImpressionEvent() + + processor.process(impressionEvent1) + processor.process(impressionEvent2) + + await new Promise(resolve => setTimeout(resolve, 150)) + expect(dispatchStub).toHaveBeenCalledTimes(0) + + processor.process(impressionEvent3) + + await new Promise(resolve => setTimeout(resolve, 150)) + expect(dispatchStub).toHaveBeenCalledTimes(1) + expect(dispatchStub).toHaveBeenCalledWith({ + url: 'https://logx.optimizely.com/v1/events', + httpVerb: 'POST', + params: makeBatchedEventV1([ + impressionEvent1, + impressionEvent2, + impressionEvent3, + ]), + }) + }) - it('should flush the current batch when it receives an event with a different context revision than the current batch', async () => { - const impressionEvent1 = createImpressionEvent() - const conversionEvent = createConversionEvent() - const impressionEvent2 = createImpressionEvent() + it('should flush the current batch when it receives an event with a different context revision than the current batch', async () => { + const impressionEvent1 = createImpressionEvent() + const conversionEvent = createConversionEvent() + const impressionEvent2 = createImpressionEvent() - // createImpressionEvent and createConversionEvent create events with revision '1' - // We modify this one's revision to '2' in order to test that the queue is flushed - // when an event with a different revision is processed. - impressionEvent2.context.revision = '2' + // createImpressionEvent and createConversionEvent create events with revision '1' + // We modify this one's revision to '2' in order to test that the queue is flushed + // when an event with a different revision is processed. + impressionEvent2.context.revision = '2' - processor.process(impressionEvent1) - processor.process(conversionEvent) + processor.process(impressionEvent1) + processor.process(conversionEvent) - await new Promise(resolve => setTimeout(resolve, 150)) - expect(dispatchStub).toHaveBeenCalledTimes(0) + await new Promise(resolve => setTimeout(resolve, 150)) + expect(dispatchStub).toHaveBeenCalledTimes(0) - processor.process(impressionEvent2) + processor.process(impressionEvent2) - await new Promise(resolve => setTimeout(resolve, 150)) - expect(dispatchStub).toHaveBeenCalledTimes(1) - expect(dispatchStub).toHaveBeenCalledWith({ - url: 'https://logx.optimizely.com/v1/events', - httpVerb: 'POST', - params: makeBatchedEventV1([impressionEvent1, conversionEvent]), - }) - }) + await new Promise(resolve => setTimeout(resolve, 150)) + expect(dispatchStub).toHaveBeenCalledTimes(1) + expect(dispatchStub).toHaveBeenCalledWith({ + url: 'https://logx.optimizely.com/v1/events', + httpVerb: 'POST', + params: makeBatchedEventV1([impressionEvent1, conversionEvent]), + }) + }) - it('should flush the current batch when it receives an event with a different context projectId than the current batch', async () => { - const impressionEvent1 = createImpressionEvent() - const conversionEvent = createConversionEvent() - const impressionEvent2 = createImpressionEvent() + it('should flush the current batch when it receives an event with a different context projectId than the current batch', async () => { + const impressionEvent1 = createImpressionEvent() + const conversionEvent = createConversionEvent() + const impressionEvent2 = createImpressionEvent() - impressionEvent2.context.projectId = 'projectId2' + impressionEvent2.context.projectId = 'projectId2' - processor.process(impressionEvent1) - processor.process(conversionEvent) + processor.process(impressionEvent1) + processor.process(conversionEvent) - await new Promise(resolve => setTimeout(resolve, 150)) - expect(dispatchStub).toHaveBeenCalledTimes(0) + await new Promise(resolve => setTimeout(resolve, 150)) + expect(dispatchStub).toHaveBeenCalledTimes(0) - processor.process(impressionEvent2) + processor.process(impressionEvent2) - await new Promise(resolve => setTimeout(resolve, 150)) - expect(dispatchStub).toHaveBeenCalledTimes(1) - expect(dispatchStub).toHaveBeenCalledWith({ - url: 'https://logx.optimizely.com/v1/events', - httpVerb: 'POST', - params: makeBatchedEventV1([impressionEvent1, conversionEvent]), + await new Promise(resolve => setTimeout(resolve, 150)) + expect(dispatchStub).toHaveBeenCalledTimes(1) + expect(dispatchStub).toHaveBeenCalledWith({ + url: 'https://logx.optimizely.com/v1/events', + httpVerb: 'POST', + params: makeBatchedEventV1([impressionEvent1, conversionEvent]), + }) }) - }) - it('should flush the queue when the flush interval happens', async () => { - const impressionEvent1 = createImpressionEvent() + it('should flush the queue when the flush interval happens', async () => { + const impressionEvent1 = createImpressionEvent() - processor.process(impressionEvent1) + processor.process(impressionEvent1) - expect(dispatchStub).toHaveBeenCalledTimes(0) + expect(dispatchStub).toHaveBeenCalledTimes(0) - await new Promise(resolve => setTimeout(resolve, 350)) + await new Promise(resolve => setTimeout(resolve, 350)) - expect(dispatchStub).toHaveBeenCalledTimes(1) - expect(dispatchStub).toHaveBeenCalledWith({ - url: 'https://logx.optimizely.com/v1/events', - httpVerb: 'POST', - params: makeBatchedEventV1([impressionEvent1]), - }) + expect(dispatchStub).toHaveBeenCalledTimes(1) + expect(dispatchStub).toHaveBeenCalledWith({ + url: 'https://logx.optimizely.com/v1/events', + httpVerb: 'POST', + params: makeBatchedEventV1([impressionEvent1]), + }) - processor.process(createImpressionEvent()) - processor.process(createImpressionEvent()) - // flushing should reset queue, at this point only has two events - expect(dispatchStub).toHaveBeenCalledTimes(1) + processor.process(createImpressionEvent()) + processor.process(createImpressionEvent()) + // flushing should reset queue, at this point only has two events + expect(dispatchStub).toHaveBeenCalledTimes(1) + }) }) - }) - describe('when a notification center is provided', () => { - it('should trigger a notification when the event dispatcher dispatches an event', async () => { - const dispatcher: EventDispatcher = { - dispatchEvent: jest.fn() - } + describe('when a notification center is provided', () => { + it('should trigger a notification when the event dispatcher dispatches an event', async () => { + const dispatcher: EventDispatcher = { + dispatchEvent: jest.fn() + } - const notificationCenter: NotificationCenter = { - sendNotifications: jest.fn() - } - - const processor = new LogTierV1EventProcessor({ - dispatcher, - notificationCenter, - batchSize: 1, - }) - await processor.start() - - const impressionEvent1 = createImpressionEvent() - processor.process(impressionEvent1) + const notificationCenter: NotificationCenter = { + sendNotifications: jest.fn() + } + + const processor = new LogTierV1EventProcessor({ + dispatcher, + notificationCenter, + batchSize: 1, + }) + await processor.start() - await new Promise(resolve => setTimeout(resolve, 150)) - expect(notificationCenter.sendNotifications).toBeCalledTimes(1) - const event = (dispatcher.dispatchEvent as jest.Mock).mock.calls[0][0] - expect(notificationCenter.sendNotifications).toBeCalledWith(NOTIFICATION_TYPES.LOG_EVENT, event) - }) - }) + const impressionEvent1 = createImpressionEvent() + processor.process(impressionEvent1) - describe('invalid flushInterval or batchSize', () => { - it.skip('should ignore a flushInterval of 0 and use the default', () => { - const processor = new LogTierV1EventProcessor({ - dispatcher: stubDispatcher, - flushInterval: 0, - batchSize: 10, - }) - processor.start() - - const impressionEvent1 = createImpressionEvent() - processor.process(impressionEvent1) - expect(dispatchStub).toHaveBeenCalledTimes(0) - jest.advanceTimersByTime(30000) - expect(dispatchStub).toHaveBeenCalledTimes(1) - expect(dispatchStub).toHaveBeenCalledWith({ - url: 'https://logx.optimizely.com/v1/events', - httpVerb: 'POST', - params: makeBatchedEventV1([impressionEvent1]), + await new Promise(resolve => setTimeout(resolve, 150)) + expect(notificationCenter.sendNotifications).toBeCalledTimes(1) + const event = (dispatcher.dispatchEvent as jest.Mock).mock.calls[0][0] + expect(notificationCenter.sendNotifications).toBeCalledWith(NOTIFICATION_TYPES.LOG_EVENT, event) }) }) - it('should ignore a batchSize of 0 and use the default', async () => { - const processor = new LogTierV1EventProcessor({ - dispatcher: stubDispatcher, - flushInterval: 30000, - batchSize: 0, - }) - await processor.start() - - const impressionEvent1 = createImpressionEvent() - processor.process(impressionEvent1) - - await new Promise(resolve => setTimeout(resolve, 150)) - expect(dispatchStub).toHaveBeenCalledTimes(0) - const impressionEvents = [impressionEvent1] - for (let i = 0; i < 9; i++) { - const evt = createImpressionEvent() - processor.process(evt) - impressionEvents.push(evt) - } - - await new Promise(resolve => setTimeout(resolve, 150)) - expect(dispatchStub).toHaveBeenCalledTimes(1) - expect(dispatchStub).toHaveBeenCalledWith({ - url: 'https://logx.optimizely.com/v1/events', - httpVerb: 'POST', - params: makeBatchedEventV1(impressionEvents), + describe('invalid batchSize', () => { + it('should ignore a batchSize of 0 and use the default', async () => { + const processor = new LogTierV1EventProcessor({ + dispatcher: stubDispatcher, + flushInterval: 30000, + batchSize: 0, + }) + await processor.start() + + const impressionEvent1 = createImpressionEvent() + processor.process(impressionEvent1) + + await new Promise(resolve => setTimeout(resolve, 150)) + expect(dispatchStub).toHaveBeenCalledTimes(0) + const impressionEvents = [impressionEvent1] + for (let i = 0; i < 9; i++) { + const evt = createImpressionEvent() + processor.process(evt) + impressionEvents.push(evt) + } + + await new Promise(resolve => setTimeout(resolve, 150)) + expect(dispatchStub).toHaveBeenCalledTimes(1) + expect(dispatchStub).toHaveBeenCalledWith({ + url: 'https://logx.optimizely.com/v1/events', + httpVerb: 'POST', + params: makeBatchedEventV1(impressionEvents), + }) }) }) }) -}) -describe.skip('LogTierV1EventProcessorReactNative2', () => { - describe('Sequence', () => { - it('should dispatch pending events in correct sequence', () => { - // Add Pending events to the store and that try dispatching them and check the sequence + describe('Pending Events', () => { + let stubDispatcher: EventDispatcher + let dispatchStub: jest.Mock + + beforeEach(() => { + dispatchStub = jest.fn() }) - it('should dispatch new event after pending events', () => { - // Add pending events to the store and + + afterEach(() => { + jest.clearAllMocks() }) - }) - describe('Retry Pending Events', () => { - describe('App start', () => { - it('should dispatch all the pending events in correct order', () => { - // store some events in the store. - // call start - // verify if all dispatched - // verify they dispatched in correct order + describe('Sequence', () => { + it('should dispatch pending events in correct sequence', () => { + // Add Pending events to the store and that try dispatching them and check the sequence }) - - it('should process all the events left in buffer when the app closed last time', () => { - // add events to buffer store - // call start - // wait for the flush interval - // verify correct event was dispatched based on the buffer - }) - - it('should dispatch pending events first and then process events in buffer store', () => { - + it('should dispatch new event after pending events', () => { + // Add pending events to the store and }) }) - - describe('When a new event is dispatched', () => { - it('should dispatch all the pending events first', () => { - }) + describe('Retry Pending Events', () => { + describe('App start', () => { + it('should dispatch all the pending events in correct order', async () => { + let receivedEvents: EventV1Request[] = [] + + stubDispatcher = { + dispatchEvent(event: EventV1Request, callback: EventDispatcherCallback): void { + dispatchStub(event) + callback({ statusCode: 400 }) + }, + } + + let processor = new LogTierV1EventProcessor({ + dispatcher: stubDispatcher, + flushInterval: 100, + batchSize: 1, + }) + + await processor.start() + let event1 = createConversionEvent() + event1.user.id = 'user1' + let event2 = createConversionEvent() + event2.user.id = 'user2' + let event3 = createConversionEvent() + event3.user.id = 'user3' + let event4 = createConversionEvent() + event4.user.id = 'user4' + + processor.process(event1) + processor.process(event2) + processor.process(event3) + processor.process(event4) + + await new Promise(resolve => setTimeout(resolve, 100)) + + expect(dispatchStub).toBeCalledTimes(4) + + await processor.stop() + + jest.clearAllMocks() + + receivedEvents = [] + stubDispatcher = { + dispatchEvent(event: EventV1Request, callback: EventDispatcherCallback): void { + receivedEvents.push(event) + dispatchStub(event) + callback({ statusCode: 200 }) + }, + } + + processor = new LogTierV1EventProcessor({ + dispatcher: stubDispatcher, + flushInterval: 100, + batchSize: 1, + }) + + await processor.start() + + receivedEvents.forEach((e, i) => { + expect(e.params.visitors[0].visitor_id).toEqual(`user${i+1}`) + }) + + expect(dispatchStub).toBeCalledTimes(4) + + await processor.stop() + }) - it('should dispatch pending events and new event in correct order', () => { - - }) + it('should process all the events left in buffer when the app closed last time', async () => { + stubDispatcher = { + dispatchEvent(event: EventV1Request, callback: EventDispatcherCallback): void { + dispatchStub(event) + callback({ statusCode: 200 }) + }, + } + + let processor = new LogTierV1EventProcessor({ + dispatcher: stubDispatcher, + flushInterval: 1000, + batchSize: 4, + }) + + await processor.start() + let event1 = createConversionEvent() + event1.user.id = 'user1' + event1.uuid = 'user1' + let event2 = createConversionEvent() + event2.user.id = 'user2' + event2.uuid = 'user2' + + processor.process(event1) + processor.process(event2) + + await new Promise(resolve => setTimeout(resolve, 100)) + + // Explicitly stopping the timer to simulate app close + ;(processor.queue as DefaultEventQueue).timer.stop() + + let receivedEvents: EventV1Request[] = [] + stubDispatcher = { + dispatchEvent(event: EventV1Request, callback: EventDispatcherCallback): void { + receivedEvents.push(event) + dispatchStub(event) + callback({ statusCode: 200 }) + }, + } + + processor = new LogTierV1EventProcessor({ + dispatcher: stubDispatcher, + flushInterval: 1000, + batchSize: 4, + }) + + await processor.start() + + await new Promise(resolve => setTimeout(resolve, 1500)) + expect(dispatchStub).toBeCalledTimes(1) + expect(receivedEvents.length).toEqual(1) + const receivedEvent = receivedEvents[0] + + receivedEvent.params.visitors.forEach((v, i) => { + expect(v.visitor_id).toEqual(`user${i+1}`) + }) + + await processor.stop() + }) - it('should skip dispatching subsequent events if an event fails to dispatch', () => { - + it('should dispatch pending events first and then process events in buffer store', async () => { + const store = new ReactNativeEventsStore(100, 'fs_optly_event_buffer') + + stubDispatcher = { + dispatchEvent(event: EventV1Request, callback: EventDispatcherCallback): void { + dispatchStub(event) + callback({ statusCode: 400 }) + }, + } + + let processor = new LogTierV1EventProcessor({ + dispatcher: stubDispatcher, + flushInterval: 300, + batchSize: 3, + }) + + await processor.start() + + for (let i = 0; i < 8; i++) { + let event = createConversionEvent() + event.user.id = `user${i}` + event.uuid = `user${i}` + processor.process(event) + } + + await new Promise(resolve => setTimeout(resolve, 150)) + + expect(dispatchStub).toBeCalledTimes(2) + + ;(processor.queue as DefaultEventQueue).timer.stop() + + jest.clearAllMocks() + + const visitorIds: string[] = [] + stubDispatcher = { + dispatchEvent(event: EventV1Request, callback: EventDispatcherCallback): void { + dispatchStub(event) + event.params.visitors.forEach(visitor => visitorIds.push(visitor.visitor_id)) + callback({ statusCode: 200 }) + }, + } + + processor = new LogTierV1EventProcessor({ + dispatcher: stubDispatcher, + flushInterval: 200, + batchSize: 3, + }) + + await processor.start() + + expect(dispatchStub).toBeCalledTimes(2) + + await new Promise(resolve => setTimeout(resolve, 250)) + expect(visitorIds.length).toEqual(8) + }) }) - }) - describe('When internet connection is restored', () => { - it('should dispatch all the pending events in correct order when internet connection is restored', () => { + describe.skip('When a new event is dispatched', () => { + it('should dispatch all the pending events first', () => { + + }) + + it('should dispatch pending events and new event in correct order', () => { + + }) + it('should skip dispatching subsequent events if an event fails to dispatch', () => { + + }) }) - it('should not dispatch duplicate events if internet is lost and restored twice in a short interval', () => { + describe.skip('When internet connection is restored', () => { + it('should dispatch all the pending events in correct order when internet connection is restored', () => { + + }) + + it('should not dispatch duplicate events if internet is lost and restored twice in a short interval', () => { + }) }) }) - }) - describe('Race Conditions', () => { - it('should not dispatch pending events twice if retyring is triggered simultenously from internet connection and new event', () => { - - }) + describe.skip('Race Conditions', () => { + it('should not dispatch pending events twice if retyring is triggered simultenously from internet connection and new event', () => { + + }) - it('should dispatch pending events in correct order if retyring is triggered from multiple sources simultenously', () => { + it('should dispatch pending events in correct order if retyring is triggered from multiple sources simultenously', () => { + }) }) }) }) diff --git a/packages/event-processor/src/v1/v1EventProcessor.react_native.ts b/packages/event-processor/src/v1/v1EventProcessor.react_native.ts index a75cdcaf0..0a0dc8b12 100644 --- a/packages/event-processor/src/v1/v1EventProcessor.react_native.ts +++ b/packages/event-processor/src/v1/v1EventProcessor.react_native.ts @@ -57,7 +57,8 @@ const EVENT_BUFFER_STORE_KEY = 'fs_optly_event_buffer' */ export class LogTierV1EventProcessor implements EventProcessor { private dispatcher: EventDispatcher - private queue: EventQueue + // expose for testing + public queue: EventQueue private notificationCenter?: NotificationCenter private requestTracker: RequestTracker @@ -143,7 +144,9 @@ export class LogTierV1EventProcessor implements EventProcessor { await this.pendingEventsStore.set(eventCacheKey, formattedEvent) // Clear buffer because the buffer has become a formatted event and is already stored in pending cache. - await this.eventBufferStore.clear() + for (const {uuid} of buffer) { + await this.eventBufferStore.remove(uuid) + } if (!this.shouldSkipDispatchToPreserveSequence) { await this.dispatchEvent(eventCacheKey, formattedEvent) @@ -214,8 +217,9 @@ export class LogTierV1EventProcessor implements EventProcessor { public process(event: ProcessableEvent): void { // Adding events to buffer store. If app closes before dispatch, we can reprocess next time the app initializes - this.eventBufferStore.set(generateUUID(), event) - this.queue.enqueue(event) + this.eventBufferStore.set(event.uuid, event).then(() => { + this.queue.enqueue(event) + }) } public async stop(): Promise { From 2b1594decb6ca42adb11356389e5210c8415b7c4 Mon Sep 17 00:00:00 2001 From: Zeeshan Ashraf Date: Sat, 18 Jul 2020 19:58:44 -0700 Subject: [PATCH 27/28] added more tests --- .../@react-native-community/netinfo.ts | 11 +- .../v1EventProcessor.react_native.spec.ts | 223 ++++++++++++++++-- 2 files changed, 217 insertions(+), 17 deletions(-) diff --git a/packages/event-processor/__mocks__/@react-native-community/netinfo.ts b/packages/event-processor/__mocks__/@react-native-community/netinfo.ts index 8a0fb2d77..4a007aab7 100644 --- a/packages/event-processor/__mocks__/@react-native-community/netinfo.ts +++ b/packages/event-processor/__mocks__/@react-native-community/netinfo.ts @@ -13,7 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -// TODO: Implement mock class for testing here -export function addEventListener() { - // mock here +let localCallback: any + +export function addEventListener(callback: any) { + localCallback = callback +} + +export function triggerInternetState(isInternetReachable: boolean) { + localCallback({ isInternetReachable }) } diff --git a/packages/event-processor/__tests__/v1EventProcessor.react_native.spec.ts b/packages/event-processor/__tests__/v1EventProcessor.react_native.spec.ts index 30f856293..a6d49528f 100644 --- a/packages/event-processor/__tests__/v1EventProcessor.react_native.spec.ts +++ b/packages/event-processor/__tests__/v1EventProcessor.react_native.spec.ts @@ -25,8 +25,8 @@ import { import { EventProcessor, ProcessableEvent } from '../src/eventProcessor' import { buildImpressionEventV1, makeBatchedEventV1 } from '../src/v1/buildEventV1' import AsyncStorage from '../__mocks__/@react-native-community/async-storage' +import { triggerInternetState } from '../__mocks__/@react-native-community/netinfo' import { DefaultEventQueue } from '../src/eventQueue' -import { ReactNativeEventsStore } from '../src/reactNativeEventsStore' function createImpressionEvent() { return { @@ -474,6 +474,7 @@ describe('LogTierV1EventProcessorReactNative', () => { afterEach(() => { jest.clearAllMocks() + AsyncStorage.clearStore() }) describe('Sequence', () => { @@ -612,8 +613,6 @@ describe('LogTierV1EventProcessorReactNative', () => { }) it('should dispatch pending events first and then process events in buffer store', async () => { - const store = new ReactNativeEventsStore(100, 'fs_optly_event_buffer') - stubDispatcher = { dispatchEvent(event: EventV1Request, callback: EventDispatcherCallback): void { dispatchStub(event) @@ -665,30 +664,226 @@ describe('LogTierV1EventProcessorReactNative', () => { await new Promise(resolve => setTimeout(resolve, 250)) expect(visitorIds.length).toEqual(8) + expect(visitorIds).toEqual(['user0', 'user1', 'user2', 'user3', 'user4', 'user5', 'user6', 'user7']) }) }) - describe.skip('When a new event is dispatched', () => { - it('should dispatch all the pending events first', () => { + describe('When a new event is dispatched', () => { + it('should dispatch all the pending events first and then new event in correct order', async () => { + let receivedVisitorIds: string[] = [] + let dispatchCount = 0 + stubDispatcher = { + dispatchEvent(event: EventV1Request, callback: EventDispatcherCallback): void { + dispatchStub(event) + dispatchCount++ + if (dispatchCount > 4) { + event.params.visitors.forEach(visitor => receivedVisitorIds.push(visitor.visitor_id)) + callback({ statusCode: 200 }) + } else { + callback({ statusCode: 400 }) + } + }, + } - }) + let processor = new LogTierV1EventProcessor({ + dispatcher: stubDispatcher, + flushInterval: 100, + batchSize: 1, + }) - it('should dispatch pending events and new event in correct order', () => { - - }) + await processor.start() + let event1 = createConversionEvent() + event1.user.id = event1.uuid = 'user1' + let event2 = createConversionEvent() + event2.user.id = event2.uuid = 'user2' + let event3 = createConversionEvent() + event3.user.id = event3.uuid = 'user3' + let event4 = createConversionEvent() + event4.user.id = event4.uuid = 'user4' - it('should skip dispatching subsequent events if an event fails to dispatch', () => { - + processor.process(event1) + processor.process(event2) + processor.process(event3) + processor.process(event4) + + await new Promise(resolve => setTimeout(resolve, 100)) + + // Four events will return response code 400 which means only the first pending event will be tried each time and rest will be skipped + expect(dispatchStub).toBeCalledTimes(4) + + jest.resetAllMocks() + + let event5 = createConversionEvent() + event5.user.id = event5.uuid = 'user5' + + processor.process(event5) + + await new Promise(resolve => setTimeout(resolve, 100)) + expect(dispatchStub).toBeCalledTimes(5) + expect(receivedVisitorIds).toEqual(['user1', 'user2', 'user3', 'user4', 'user5']) + await processor.stop() + }) + + it('should skip dispatching subsequent events if an event fails to dispatch', async () => { + let receivedVisitorIds: string[] = [] + let dispatchCount = 0 + stubDispatcher = { + dispatchEvent(event: EventV1Request, callback: EventDispatcherCallback): void { + dispatchStub(event) + dispatchCount++ + event.params.visitors.forEach(visitor => receivedVisitorIds.push(visitor.visitor_id)) + callback({ statusCode: 400 }) + }, + } + + let processor = new LogTierV1EventProcessor({ + dispatcher: stubDispatcher, + flushInterval: 100, + batchSize: 1, + }) + + await processor.start() + let event1 = createConversionEvent() + event1.user.id = event1.uuid = 'user1' + let event2 = createConversionEvent() + event2.user.id = event2.uuid = 'user2' + let event3 = createConversionEvent() + event3.user.id = event3.uuid = 'user3' + let event4 = createConversionEvent() + event4.user.id = event4.uuid = 'user4' + + processor.process(event1) + await new Promise(resolve => setTimeout(resolve, 100)) + expect(dispatchStub).toBeCalledTimes(1) + + processor.process(event2) + await new Promise(resolve => setTimeout(resolve, 100)) + expect(dispatchStub).toBeCalledTimes(2) + + processor.process(event3) + await new Promise(resolve => setTimeout(resolve, 100)) + expect(dispatchStub).toBeCalledTimes(3) + + processor.process(event4) + await new Promise(resolve => setTimeout(resolve, 100)) + expect(dispatchStub).toBeCalledTimes(4) + + expect(dispatchCount).toEqual(4) + + // subsequent events were skipped with each attempt because of request failure + expect(receivedVisitorIds).toEqual(['user1', 'user1', 'user1', 'user1']) + await processor.stop() }) }) - describe.skip('When internet connection is restored', () => { - it('should dispatch all the pending events in correct order when internet connection is restored', () => { + describe('When internet connection is restored', () => { + it('should dispatch all the pending events in correct order when internet connection is restored', async () => { + let receivedVisitorIds: string[] = [] + let dispatchCount = 0 + stubDispatcher = { + dispatchEvent(event: EventV1Request, callback: EventDispatcherCallback): void { + dispatchStub(event) + dispatchCount++ + if (dispatchCount > 4) { + event.params.visitors.forEach(visitor => receivedVisitorIds.push(visitor.visitor_id)) + callback({ statusCode: 200 }) + } else { + callback({ statusCode: 400 }) + } + }, + } + + let processor = new LogTierV1EventProcessor({ + dispatcher: stubDispatcher, + flushInterval: 100, + batchSize: 1, + }) + await processor.start() + triggerInternetState(false) + let event1 = createConversionEvent() + event1.user.id = event1.uuid = 'user1' + let event2 = createConversionEvent() + event2.user.id = event2.uuid = 'user2' + let event3 = createConversionEvent() + event3.user.id = event3.uuid = 'user3' + let event4 = createConversionEvent() + event4.user.id = event4.uuid = 'user4' + + processor.process(event1) + processor.process(event2) + processor.process(event3) + processor.process(event4) + + await new Promise(resolve => setTimeout(resolve, 100)) + + // Four events will return response code 400 which means only the first pending event will be tried each time and rest will be skipped + expect(dispatchStub).toBeCalledTimes(4) + + jest.resetAllMocks() + + triggerInternetState(true) + await new Promise(resolve => setTimeout(resolve, 100)) + expect(dispatchStub).toBeCalledTimes(4) + expect(receivedVisitorIds).toEqual(['user1', 'user2', 'user3', 'user4']) + await processor.stop() }) - it('should not dispatch duplicate events if internet is lost and restored twice in a short interval', () => { + it('should not dispatch duplicate events if internet is lost and restored twice in a short interval', async () => { + let receivedVisitorIds: string[] = [] + let dispatchCount = 0 + stubDispatcher = { + dispatchEvent(event: EventV1Request, callback: EventDispatcherCallback): void { + dispatchStub(event) + dispatchCount++ + if (dispatchCount > 4) { + event.params.visitors.forEach(visitor => receivedVisitorIds.push(visitor.visitor_id)) + callback({ statusCode: 200 }) + } else { + callback({ statusCode: 400 }) + } + }, + } + let processor = new LogTierV1EventProcessor({ + dispatcher: stubDispatcher, + flushInterval: 100, + batchSize: 1, + }) + + await processor.start() + triggerInternetState(false) + let event1 = createConversionEvent() + event1.user.id = event1.uuid = 'user1' + let event2 = createConversionEvent() + event2.user.id = event2.uuid = 'user2' + let event3 = createConversionEvent() + event3.user.id = event3.uuid = 'user3' + let event4 = createConversionEvent() + event4.user.id = event4.uuid = 'user4' + + processor.process(event1) + processor.process(event2) + processor.process(event3) + processor.process(event4) + + await new Promise(resolve => setTimeout(resolve, 100)) + + // Four events will return response code 400 which means only the first pending event will be tried each time and rest will be skipped + expect(dispatchStub).toBeCalledTimes(4) + + jest.resetAllMocks() + + triggerInternetState(true) + triggerInternetState(false) + triggerInternetState(true) + triggerInternetState(false) + triggerInternetState(true) + + await new Promise(resolve => setTimeout(resolve, 100)) + expect(dispatchStub).toBeCalledTimes(4) + expect(receivedVisitorIds).toEqual(['user1', 'user2', 'user3', 'user4']) + await processor.stop() }) }) }) From dd7c837be25519a3e9847002effe469bba649810 Mon Sep 17 00:00:00 2001 From: Zeeshan Ashraf Date: Sat, 18 Jul 2020 20:09:03 -0700 Subject: [PATCH 28/28] reduced timeouts to make tests faster --- .../v1EventProcessor.react_native.spec.ts | 52 ++++++------------- 1 file changed, 16 insertions(+), 36 deletions(-) diff --git a/packages/event-processor/__tests__/v1EventProcessor.react_native.spec.ts b/packages/event-processor/__tests__/v1EventProcessor.react_native.spec.ts index a6d49528f..fd7fca71d 100644 --- a/packages/event-processor/__tests__/v1EventProcessor.react_native.spec.ts +++ b/packages/event-processor/__tests__/v1EventProcessor.react_native.spec.ts @@ -194,7 +194,6 @@ describe('LogTierV1EventProcessorReactNative', () => { }) it('should return a promise when multiple event batches are sent', async () => { - await new Promise(resolve => setTimeout(resolve, 2000)) stubDispatcher = { dispatchEvent(event: EventV1Request, callback: EventDispatcherCallback): void { dispatchStub(event) @@ -236,7 +235,7 @@ describe('LogTierV1EventProcessorReactNative', () => { const impressionEvent1 = createImpressionEvent() processor.process(impressionEvent1) - await new Promise(resolve => setTimeout(resolve, 200)) + await new Promise(resolve => setTimeout(resolve, 150)) await processor.stop() // calling stop should haver flushed the current batch of size 1 @@ -313,12 +312,12 @@ describe('LogTierV1EventProcessorReactNative', () => { processor.process(impressionEvent1) processor.process(impressionEvent2) - await new Promise(resolve => setTimeout(resolve, 150)) + await new Promise(resolve => setTimeout(resolve, 50)) expect(dispatchStub).toHaveBeenCalledTimes(0) processor.process(impressionEvent3) - await new Promise(resolve => setTimeout(resolve, 150)) + await new Promise(resolve => setTimeout(resolve, 50)) expect(dispatchStub).toHaveBeenCalledTimes(1) expect(dispatchStub).toHaveBeenCalledWith({ url: 'https://logx.optimizely.com/v1/events', @@ -344,12 +343,12 @@ describe('LogTierV1EventProcessorReactNative', () => { processor.process(impressionEvent1) processor.process(conversionEvent) - await new Promise(resolve => setTimeout(resolve, 150)) + await new Promise(resolve => setTimeout(resolve, 50)) expect(dispatchStub).toHaveBeenCalledTimes(0) processor.process(impressionEvent2) - await new Promise(resolve => setTimeout(resolve, 150)) + await new Promise(resolve => setTimeout(resolve, 50)) expect(dispatchStub).toHaveBeenCalledTimes(1) expect(dispatchStub).toHaveBeenCalledWith({ url: 'https://logx.optimizely.com/v1/events', @@ -368,12 +367,12 @@ describe('LogTierV1EventProcessorReactNative', () => { processor.process(impressionEvent1) processor.process(conversionEvent) - await new Promise(resolve => setTimeout(resolve, 150)) + await new Promise(resolve => setTimeout(resolve, 50)) expect(dispatchStub).toHaveBeenCalledTimes(0) processor.process(impressionEvent2) - await new Promise(resolve => setTimeout(resolve, 150)) + await new Promise(resolve => setTimeout(resolve, 50)) expect(dispatchStub).toHaveBeenCalledTimes(1) expect(dispatchStub).toHaveBeenCalledWith({ url: 'https://logx.optimizely.com/v1/events', @@ -477,15 +476,6 @@ describe('LogTierV1EventProcessorReactNative', () => { AsyncStorage.clearStore() }) - describe('Sequence', () => { - it('should dispatch pending events in correct sequence', () => { - // Add Pending events to the store and that try dispatching them and check the sequence - }) - it('should dispatch new event after pending events', () => { - // Add pending events to the store and - }) - }) - describe('Retry Pending Events', () => { describe('App start', () => { it('should dispatch all the pending events in correct order', async () => { @@ -594,13 +584,13 @@ describe('LogTierV1EventProcessorReactNative', () => { processor = new LogTierV1EventProcessor({ dispatcher: stubDispatcher, - flushInterval: 1000, + flushInterval: 100, batchSize: 4, }) await processor.start() - await new Promise(resolve => setTimeout(resolve, 1500)) + await new Promise(resolve => setTimeout(resolve, 150)) expect(dispatchStub).toBeCalledTimes(1) expect(receivedEvents.length).toEqual(1) const receivedEvent = receivedEvents[0] @@ -635,7 +625,7 @@ describe('LogTierV1EventProcessorReactNative', () => { processor.process(event) } - await new Promise(resolve => setTimeout(resolve, 150)) + await new Promise(resolve => setTimeout(resolve, 50)) expect(dispatchStub).toBeCalledTimes(2) @@ -753,19 +743,19 @@ describe('LogTierV1EventProcessorReactNative', () => { event4.user.id = event4.uuid = 'user4' processor.process(event1) - await new Promise(resolve => setTimeout(resolve, 100)) + await new Promise(resolve => setTimeout(resolve, 50)) expect(dispatchStub).toBeCalledTimes(1) processor.process(event2) - await new Promise(resolve => setTimeout(resolve, 100)) + await new Promise(resolve => setTimeout(resolve, 50)) expect(dispatchStub).toBeCalledTimes(2) processor.process(event3) - await new Promise(resolve => setTimeout(resolve, 100)) + await new Promise(resolve => setTimeout(resolve, 50)) expect(dispatchStub).toBeCalledTimes(3) processor.process(event4) - await new Promise(resolve => setTimeout(resolve, 100)) + await new Promise(resolve => setTimeout(resolve, 50)) expect(dispatchStub).toBeCalledTimes(4) expect(dispatchCount).toEqual(4) @@ -815,7 +805,7 @@ describe('LogTierV1EventProcessorReactNative', () => { processor.process(event3) processor.process(event4) - await new Promise(resolve => setTimeout(resolve, 100)) + await new Promise(resolve => setTimeout(resolve, 50)) // Four events will return response code 400 which means only the first pending event will be tried each time and rest will be skipped expect(dispatchStub).toBeCalledTimes(4) @@ -823,7 +813,7 @@ describe('LogTierV1EventProcessorReactNative', () => { jest.resetAllMocks() triggerInternetState(true) - await new Promise(resolve => setTimeout(resolve, 100)) + await new Promise(resolve => setTimeout(resolve, 50)) expect(dispatchStub).toBeCalledTimes(4) expect(receivedVisitorIds).toEqual(['user1', 'user2', 'user3', 'user4']) await processor.stop() @@ -887,15 +877,5 @@ describe('LogTierV1EventProcessorReactNative', () => { }) }) }) - - describe.skip('Race Conditions', () => { - it('should not dispatch pending events twice if retyring is triggered simultenously from internet connection and new event', () => { - - }) - - it('should dispatch pending events in correct order if retyring is triggered from multiple sources simultenously', () => { - - }) - }) }) })