diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..eb19536 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": ["@diplomatiq/eslint-config-tslib"] +} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..a20d51a --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,74 @@ +name: CI + +on: [push, pull_request] + +jobs: + lint-build-test-scan: + name: Lint, build, test, scan + runs-on: ubuntu-latest + + strategy: + matrix: + node: [13, 12, 11, 10] + fail-fast: false + + steps: + - name: Checkout push or pull request HEAD + uses: actions/checkout@v2 + - name: Convert the shallow clone to an unshallow one + run: git fetch --unshallow + - name: Request the number of commits on the pull request + id: number_of_commits_on_pr_request + if: github.event_name == 'pull_request' + uses: octokit/graphql-action@v2.x + with: + query: | + query NumberOfCommitsOnPR($repositoryowner: String!, $repositoryname: String!, $prnumber: Int!) { + repository(owner: $repositoryowner, name: $repositoryname) { + pullRequest(number: $prnumber) { + commits { + totalCount + } + } + } + } + repositoryowner: ${{ github.event.repository.owner.login }} + repositoryname: ${{ github.event.repository.name }} + prnumber: ${{ github.event.number }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Get the number of commits on the pull request from the response + id: number_of_commits_on_pr_result + if: github.event_name == 'pull_request' + uses: gr2m/get-json-paths-action@v1.x + with: + json: ${{ steps.number_of_commits_on_pr_request.outputs.data }} + commits_count: 'repository.pullRequest.commits.totalCount' + - name: Check if the number of commits on the pull request is equal to one + if: github.event_name == 'pull_request' + run: | + if [ "${{ steps.number_of_commits_on_pr_result.outputs.commits_count }}" -ne 1 ]; then + echo "The pull request must consist of exactly one commit. Please squash your commits into one." + exit 1 + fi + - name: Set up Node.js version + uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node }} + - name: Install dependencies + run: npm ci + - name: Lint the commit message of the pull request + if: github.event_name == 'pull_request' + run: npx commitlint --from HEAD^ --to HEAD --config .commitlintrc.json + - name: Lint the code + run: npm run lint + - name: Build the code + run: npm run build + - name: Test the code + run: npm run test + - name: Scan the code with SonarCloud + if: matrix.node == 13 + uses: sonarsource/sonarcloud-github-action@34eca22d1c5760f6ec08cb4b5c2f026796eb8e30 + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1879c54 --- /dev/null +++ b/.gitignore @@ -0,0 +1,124 @@ +# Created by https://www.gitignore.io/api/node,macos,visualstudiocode +# Edit at https://www.gitignore.io/?templates=node,macos,visualstudiocode + +### macOS ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### Node ### +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# TypeScript v1 declaration files +typings/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env + +# parcel-bundler cache (https://parceljs.org/) +.cache + +# next.js build output +.next + +# nuxt.js build output +.nuxt + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless + +# FuseBox cache +.fusebox/ + +### VisualStudioCode ### +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history + +# End of https://www.gitignore.io/api/node,macos,visualstudiocode + +### Built files ### +dist/ diff --git a/.nycrc.json b/.nycrc.json new file mode 100644 index 0000000..079e2e9 --- /dev/null +++ b/.nycrc.json @@ -0,0 +1,4 @@ +{ + "extension": [".ts"], + "include": ["src/**/*.ts"] +} diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..82f6022 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,3 @@ +package.json +package-lock.json +tsconfig.json diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..6a667b9 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,6 @@ +{ + "printWidth": 120, + "singleQuote": true, + "trailingComma": "all", + "tabWidth": 4 +} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..d74991a --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,17 @@ + +# 1.0.0 (2020-04-02) + + +### Features + +* Implement BulkheadIsolationPolicy ([0a6fd61](https://github.com/Diplomatiq/resily/commit/0a6fd61)), closes [#199](https://github.com/Diplomatiq/resily/issues/199) +* Implement CachePolicy ([0e39ded](https://github.com/Diplomatiq/resily/commit/0e39ded)), closes [#207](https://github.com/Diplomatiq/resily/issues/207) +* Implement CircuitBreakerPolicy ([1351ea1](https://github.com/Diplomatiq/resily/commit/1351ea1)), closes [#193](https://github.com/Diplomatiq/resily/issues/193) +* Implement FallbackPolicy ([5225f73](https://github.com/Diplomatiq/resily/commit/5225f73)), closes [#185](https://github.com/Diplomatiq/resily/issues/185) +* Implement NopPolicy ([4b257ef](https://github.com/Diplomatiq/resily/commit/4b257ef)), closes [#208](https://github.com/Diplomatiq/resily/issues/208) +* Implement PolicyCombination ([e92d537](https://github.com/Diplomatiq/resily/commit/e92d537)), closes [#209](https://github.com/Diplomatiq/resily/issues/209) +* Implement RetryPolicy ([1b0ce44](https://github.com/Diplomatiq/resily/commit/1b0ce44)), closes [#46](https://github.com/Diplomatiq/resily/issues/46) +* Implement TimeoutPolicy ([75bd2ee](https://github.com/Diplomatiq/resily/commit/75bd2ee)), closes [#164](https://github.com/Diplomatiq/resily/issues/164) + + + diff --git a/README.md b/README.md index 846e06d..195c29e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,1065 @@ # resily -Resily is a TypeScript resilience and transient-fault-handling library that allows developers to express policies such as Retry, Circuit Breaker, Timeout, Bulkhead Isolation, and Fallback. Inspired by App-vNext/Polly. +Resily is a TypeScript resilience and transient-fault-handling library that allows developers to express policies such as Retry, Fallback, Circuit Breaker, Timeout, Bulkhead Isolation, and Cache. Inspired by [App-vNext/Polly](https://github.com/App-vNext/Polly). + +

+ + build status + + + + languages used + + + + downloads from npm + + + + latest released version on npm + + + + license + +

+ +

+ + Quality Gate + + + + Coverage + + + + Maintainability Rating + + + + Reliability Rating + + + + Security Rating + + + + Dependabot + +

+ +

+ + Gitter + +

+ +--- + +## Installation + +Being an npm package, you can install resily with the following command: + +```bash +npm install -P @diplomatiq/resily +``` + +## Testing + +Run tests with the following: + +```bash +npm test +``` + +## Usage + +_Note: This package is built as an ES6 package. You will not be able to use `require()`._ + +After installation, you can import policies and other helper classes into your project, then wrap your code into one or more policies. + +Every policy extends the abstract `Policy` class, which has an `execute` method. Your code wrapped into a policy gets executed when you invoke `execute`. The `execute` method is asynchronous, so it returns a `Promise` resolving with the return value of the executed method (or rejecting with an exception thrown by the method). + +The wrapped method can be synchronous or asynchronous, it will be awaited in either case: + +```typescript +async function main() { + const policy = … // any policy + + // configure the policy before executing code, see below + + // then execute some code wrapped into the policy + // execute is async, so it returns a Promise + const result = await policy.execute( + // the wrapped method can be sync or async + async () => { + // the executed code + return 5; + }, + ); + + // the value of result is 5 +} +``` + +See concrete usage examples below at the policies' documentation. + +## Policies + +Resily offers **reactive** and **proactive** policies: + +- A **reactive** policy executes the wrapped method, then reacts to the outcome (which in practice is the result of or an exception thrown by the executed method) by acting as specified in the policy itself. Examples for reactive policies include retry, fallback, circuit-breaker. +- A **proactive** policy executes the wrapped method, then acts on its own as specified in the policy itself, regardless of the outcome of the executed code. Examples for proactive policies include timeout, bulkhead isolation, cache. + +#### Reactive policies summary + +| Policy | What does it claim? | How does it work? | +| ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | +| [**RetryPolicy**](#retrypolicy) | Many faults are transient and will not occur again after a delay. | Allows configuring automatic retries on specified conditions. | +| [**FallbackPolicy**](#fallbackpolicy) | Failures happen, and we can prepare for them. | Allows configuring substitute values or automated fallback actions. | +| [**CircuitBreakerPolicy**](#circuitbreakerpolicy) | Systems faulting under heavy load can recover easier without even more load — in these cases it's better to fail fast than to keep callers on hold for a long time. | If there are more consecutive faulty responses than the configured number, it breaks the circuit (blocks the executions) for a specified time period. | + +#### Proactive policies summary + +| Policy | What does it claim? | How does it work? | +| ------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | +| [**TimeoutPolicy**](#timeoutpolicy) | After some time, it is unlikely that the call will be successful. | Ensures the caller does not have to wait more than the specified timeout. | +| [**BulkheadIsolationPolicy**](#bulkheadisolationpolicy) | Too many concurrent calls can overload a resource. | Limits the number of concurrently executed actions as specified. | +| [**CachePolicy**](#cachepolicy) | Within a given time frame, a system may respond with the same answer, thus there is no need to actually perform the query. | Retrieves the response from a local cache within the time frame, after storing it on the first query. | + +#### Helpers and utilities summary + +| Policy | What does it claim? | How does it work? | +| ------------------------------------------- | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------- | +| [**NopPolicy**](#noppolicy) | Does not claim anything. | Executes the wrapped method, and returns its result or throws its exceptions, without any intervention. | +| [**PolicyCombination**](#policycombination) | Combining policies leads to better resilience. | Allows any policies to be combined together. | + +### Reactive policies + +Every reactive policy extends the `ReactivePolicy` class, which means they can be configured with predicates to react on specific results and/or exceptions: + +```typescript +const policy = … // any reactive policy + +// if the executed code returns 5, the policy will react +policy.reactOnResult(r => r === 5); + +// will react +await policy.execute(() => 5); + +// will react +await policy.execute(async () => 5); + +// will not react +await policy.execute(() => 2); + +// will not react +await policy.execute(async () => 2); +``` + +```typescript +const policy = … // any reactive policy + +// if the executed code throws a ConcurrentAccessException, the policy will react +policy.reactOnException(e => e instanceof ConcurrentAccessException); + +// will react +await policy.execute(() => { + throw new ConcurrentAccessException(); +}); + +// will react +await policy.execute(async () => { + throw new ConcurrentAccessException(); +}); + +// will not react +await policy.execute(() => { + throw new OutOfRangeException(); +}); + +// will not react +await policy.execute(async () => { + throw new OutOfRangeException(); +}); +``` + +If the policy is configured to react on multiple kinds of results or exceptions, it will react if any of them occurs: + +```typescript +const policy = … // any reactive policy + +policy.reactOnResult(r => r === 5); +policy.reactOnResult(r => r === 7); +policy.reactOnException(e => e instanceof ConcurrentAccessException); +policy.reactOnException(e => e instanceof InvalidArgumentException); + +// will react +await policy.execute(() => 5); + +// will react +await policy.execute(async () => 5); + +// will react +await policy.execute(() => 7); + +// will react +await policy.execute(async () => 7); + +// will react +await policy.execute(() => { + throw new ConcurrentAccessException(); +}); + +// will react +await policy.execute(async () => { + throw new ConcurrentAccessException(); +}); + +// will react +await policy.execute(() => { + throw new InvalidArgumentException(); +}); + +// will react +await policy.execute(async () => { + throw new InvalidArgumentException(); +}); + +// will not react +await policy.execute(() => 2); + +// will not react +await policy.execute(async () => 2); + +// will not react +await policy.execute(() => { + throw new OutOfRangeException(); +}); + +// will not react +await policy.execute(async () => { + throw new OutOfRangeException(); +}); +``` + +You can configure the policy to react on any result and/or to any exception: + +```typescript +const policy = … // any reactive policy + +// react on any result +policy.reactOnResult(() => true); + +// react on any exception +policy.reactOnException(() => true); +``` + +#### RetryPolicy + +`RetryPolicy` claims that many faults are transient and will not occur again after a delay. It allows configuring automatic retries on specified conditions. + +Since `RetryPolicy` is a reactive policy, you need to configure the policy to retry the execution on specific results or exceptions with `reactOnResult` and `reactOnException`. See the [Reactive policies](#reactive-policies) section for details. + +Configure how many retries you need or retry forever: + +```typescript +import { RetryPolicy } from '@diplomatiq/resily'; + +// the wrapped method is supposed to return a string +const policy = new RetryPolicy(); + +// retry until the result/exception is reactive, but maximum 3 times +policy.retryCount(3); + +// this overwrites the previous value +policy.retryCount(5); + +// this also overwrites the previous value +// this is the same as policy.retryCount(Number.POSITIVE_INFINITY) +policy.retryForever(); +``` + +Perform certain actions before retrying: + +```typescript +import { RetryPolicy } from '@diplomatiq/resily'; + +// the wrapped method is supposed to return a string +const policy = new RetryPolicy(); + +policy.onRetry( + // onRetryFns can be sync or async, they will be awaited + async (result, error, currentRetryCount) => { + // this code will be executed before the currentRetryCount-th retry occurs + // result is undefined if reacting upon a thrown error + // error is undefined if reacting upon a result + }, +); + +// you can set multiple onRetryFns, they will run sequentially +policy.onRetry(async () => { + // this will be awaited first +}); +policy.onRetry(async () => { + // then this will be awaited +}); + +// errors thrown by an onRetryFn will be caught and ignored +policy.onRetry(() => { + // throwing an error has no effect outside the method + throw new Error(); +}); +``` + +Wait for the specified number of milliseconds before retrying: + +```typescript +import { RetryPolicy } from '@diplomatiq/resily'; + +// the wrapped method is supposed to return a string +const policy = new RetryPolicy(); + +// wait for 100 ms before each retry +policy.waitBeforeRetry(() => 100); + +// this overwrites the previous backoff strategy +// wait for 100 ms before the first retry, 200 ms before the second retry, etc. +policy.waitBeforeRetry(currentRetryCount => currentRetryCount * 100); +``` + +The waiting happens _before_ the execution of onRetryFns. + +Although you can code any kind of backoff, there are also predefined, ready-to-use backoff strategies: + +```typescript +import { BackoffStrategyFactory, RetryPolicy } from '@diplomatiq/resily'; + +// the wrapped method is supposed to return a string +const policy = new RetryPolicy(); + +// wait for 100 ms before each retry +// 100 100 100 100 100 … +policy.waitBeforeRetry(BackoffStrategyFactory.constantBackoff(100)); + +// retry immediately for the first time, then wait for 100 ms before each retry +// 0 100 100 100 100 … +policy.waitBeforeRetry(BackoffStrategyFactory.constantBackoff(100, true)); + +// wait for (currentRetryCount * 100) ms before each retry +// 100 200 300 400 500 … +policy.waitBeforeRetry(BackoffStrategyFactory.linearBackoff(100)); + +// retry immediately for the first time, then wait for ((currentRetryCount - 1) * 100) ms before each retry +// 0 100 200 300 400 … +policy.waitBeforeRetry(BackoffStrategyFactory.linearBackoff(100, true)); + +// wait for (100 * 2 ** (currentRetryCount - 1)) ms before each retry +// 100 200 400 800 1600 … +policy.waitBeforeRetry(BackoffStrategyFactory.exponentialBackoff(100)); + +// retry immediately for the first time, then wait for (100 * 2 ** (currentRetryCount - 2)) ms before each retry +// 0 100 200 400 800 … +policy.waitBeforeRetry(BackoffStrategyFactory.exponentialBackoff(100, true)); + +// wait for (100 * 3 ** (currentRetryCount - 1)) ms before each retry +// 100 300 900 2700 8100 … +policy.waitBeforeRetry(BackoffStrategyFactory.exponentialBackoff(100, false, 3)); + +// retry immediately for the first time, then wait for (100 * 3 ** (currentRetryCount - 2)) ms before each retry +// 0 100 300 900 2700 … +policy.waitBeforeRetry(BackoffStrategyFactory.exponentialBackoff(100, true, 3)); + +// wait for a [random between 1-100, inclusive] ms before each retry +policy.waitBeforeRetry(BackoffStrategyFactory.jitteredBackoff(1, 100)); + +// retry immediately for the first time, then wait for a [random between 1-100, inclusive] ms before each retry +policy.waitBeforeRetry(BackoffStrategyFactory.jitteredBackoff(1, 100, true)); +``` + +For using `jitteredBackoff` in Node.js environments, you will need to inject a Node.js-based entropy source into the default RandomGenerator ([@diplomatiq/crypto-random](https://github.com/Diplomatiq/crypto-random) requires `window.crypto.getRandomValues` to be available by default). Create the following in your project: + +```typescript +import { EntropyProvider, UnsignedTypedArray } from '@diplomatiq/crypto-random'; +import { randomFill } from 'crypto'; + +export class NodeJsEntropyProvider implements EntropyProvider { + public async getRandomValues(array: T): Promise { + return new Promise((resolve, reject): void => { + randomFill(array, (error: Error | null, array: T): void => { + if (error !== null) { + reject(error); + return; + } + resolve(array); + }); + }); + } +} +``` + +Then use it as follows: + +```typescript +import { RandomGenerator } from '@diplomatiq/crypto-random'; +import { NodeJsEntropyProvider } from './nodeJsEntropyProvider'; + +const entropyProvider = new NodeJsEntropyProvider(); +const randomGenerator = new RandomGenerator(entropyProvider); + +const jitteredBackoff = BackoffStrategyFactory.jitteredBackoff(1, 100, true, randomGenerator); +``` + +Perform certain actions after the execution and all retries finished: + +```typescript +import { RetryPolicy } from '@diplomatiq/resily'; + +// the wrapped method is supposed to return a string +const policy = new RetryPolicy(); + +policy.onFinally( + // onFinallyFns can be sync or async, they will be awaited + async () => {}, +); + +// you can set multiple onFinallyFns, they will run sequentially +policy.onFinally(async () => { + // this will be awaited first +}); +policy.onFinally(async () => { + // then this will be awaited +}); + +// errors thrown by an onFinallyFn will be caught and ignored +policy.onFinally(() => { + // throwing an error has no effect outside the method + throw new Error(); +}); +``` + +#### FallbackPolicy + +`FallbackPolicy` claims that failures happen, and we can prepare for them. It allows configuring substitute values or automated fallback actions. + +Since `FallbackPolicy` is a reactive policy, you need to configure the policy to fallback along its fallback chain on specific results or exceptions with `reactOnResult` and `reactOnException`. See the [Reactive policies](#reactive-policies) section for details. + +Configure the fallback chain: + +```typescript +import { FallbackPolicy } from '@diplomatiq/resily'; + +// the wrapped method and its fallbacks are supposed to return a string +const policy = new FallbackPolicy(); + +// if the wrapped method's result/exception is reactive, configure a fallback method onto the fallback chain +policy.fallback( + // the fallback methods can be sync or async, they will be awaited + () => { + // do something + }, +); + +// if the previous fallback method's result/exception is reactive, configure another fallback onto the fallback chain +policy.fallback( + // the fallback methods can be sync or async, they will be awaited + async () => { + // do something + }, +); + +// you can configure any number of fallback methods onto the fallback chain +``` + +If there are no more elements on the fallback chain but the last result/exception is still reactive — meaning there are no more fallbacks when needed —, a `FallbackChainExhaustedException` is thrown. + +Perform certain actions before the fallback: + +```typescript +import { FallbackPolicy } from '@diplomatiq/resily'; + +// the wrapped method and its fallbacks are supposed to return a string +const policy = new FallbackPolicy(); + +policy.onFallback( + // onFallbackFns can be sync or async, they will be awaited + async (result, error) => { + // result is undefined if reacting upon a thrown error + // error is undefined if reacting upon a result + }, +); + +// you can set multiple onFallbackFns, they will run sequentially +policy.onFallback(async () => { + // this will be awaited first +}); +policy.onFallback(async () => { + // then this will be awaited +}); + +// errors thrown by an onFallbackFn will be caught and ignored +policy.onFallback(() => { + // throwing an error has no effect outside the method + throw new Error(); +}); +``` + +Perform certain actions after the execution and all fallbacks finished: + +```typescript +import { FallbackPolicy } from '@diplomatiq/resily'; + +// the wrapped method and its fallbacks are supposed to return a string +const policy = new FallbackPolicy(); + +policy.onFinally( + // onFinallyFns can be sync or async, they will be awaited + async () => {}, +); + +// you can set multiple onFinallyFns, they will run sequentially +policy.onFinally(async () => { + // this will be awaited first +}); +policy.onFinally(async () => { + // then this will be awaited +}); + +// errors thrown by an onFinallyFn will be caught and ignored +policy.onFinally(() => { + // throwing an error has no effect outside the method + throw new Error(); +}); +``` + +#### CircuitBreakerPolicy + +`CircuitBreakerPolicy` claims that systems faulting under heavy load can recover easier without even more load — in these cases it's better to fail fast than to keep callers on hold for a long time. + +If there are more consecutive faulty responses than the configured number, it breaks the circuit (blocks the executions) for a specified time period. + +Since `CircuitBreakerPolicy` is a reactive policy, you need to configure the policy to break the circuit on specific results or exceptions with `reactOnResult` and `reactOnException`. See the [Reactive policies](#reactive-policies) section for details. + +The `CircuitBreakerPolicy` has 4 states, and works as follows: + +`Closed` + +- This is the initial state. +- When closed, the circuit allows executions, while measuring reactive results and exceptions. All results (reactive or not) are returned and all exceptions (reactive or not) are rethrown. +- When encountering altogether `numberOfConsecutiveReactionsBeforeCircuitBreak` reactive results or exceptions _consecutively_, the circuit transitions to `Open` state, meaning the circuit is broken. + +`Open` + +- While the circuit is in `Open` state, no action wrapped into the policy gets executed. Every call will fail fast with a `BrokenCircuitException`. +- The circuit remains open for the specified duration. After the duration elapses, the subsequent execution call transitions the circuit to `AttemptingClose` state. + +`AttemptingClose` + +- As the name implies, this state is an attempt to close the circuit. +- This is a temporary state of the circuit, existing only between the subsequent execution call to the circuit after the break duration elapsed in `Open` state, and the actual execution of the wrapped method. +- The next circuit state is determined by the result or exception produced by the executed method. + + - If the result or exception is reactive to the policy, the circuit transitions back to `Open` state for the specified circuit break duration. + - If the result or exception is not reactive to the policy, the circuit transitions to `Closed` state. + +`Isolated` + +- You can manually break the circuit by calling `policy.isolate()`, from any state. This transitions the circuit to `Isolated` state. +- While the circuit is in `Isolated` state, no action wrapped into the policy gets executed. Every call will fail fast with an `IsolatedCircuitException`. +- The circuit remains in `Isolated` state until `policy.reset()` is called. + +Configure how many consecutive reactions should break the circuit: + +```typescript +import { CircuitBreakerPolicy } from '@diplomatiq/resily'; + +// the wrapped method is supposed to return a string +const policy = new CircuitBreakerPolicy(); + +// break the circuit after encountering 3 reactive results/exceptions consecutively +policy.breakAfter(3); + +// this overwrites the previous value +policy.breakAfter(5); +``` + +Configure how long the circuit should be broken: + +```typescript +import { CircuitBreakerPolicy } from '@diplomatiq/resily'; + +// the wrapped method is supposed to return a string +const policy = new CircuitBreakerPolicy(); + +// break the circuit for 5000 ms +policy.breakFor(5000); + +// this overwrites the previous value +policy.breakFor(20000); +``` + +Manage the circuit manually: + +```typescript +import { CircuitBreakerPolicy } from '@diplomatiq/resily'; + +// the wrapped method is supposed to return a string +const policy = new CircuitBreakerPolicy(); + +// break the circuit manually - it will be open indefinitely +await policy.isolate(); + +// get the circuit's current state +const state = policy.getCircuitState(); +// 'Closed' | 'Open' | 'AttemptingClose' | 'Isolated' + +// reset the circuit after isolating - it will close +if (state === 'Isolated') { + await policy.reset(); +} +``` + +Perform actions on state transitions: + +```typescript +import { CircuitBreakerPolicy } from '@diplomatiq/resily'; + +// the wrapped method is supposed to return a string +const policy = new CircuitBreakerPolicy(); +``` + +```typescript +policy.onClose( + // onCloseFns can be sync or async, they will be awaited + async () => {}, +); + +// you can set multiple onCloseFns, they will run sequentially +policy.onClose(async () => { + // this will be awaited first +}); +policy.onClose(async () => { + // then this will be awaited +}); + +// errors thrown by an onCloseFn will be caught and ignored +policy.onClose(() => { + // throwing an error has no effect outside the method + throw new Error(); +}); +``` + +```typescript +policy.onOpen( + // onOpenFns can be sync or async, they will be awaited + async () => {}, +); + +// you can set multiple onOpenFns, they will run sequentially +policy.onOpen(async () => { + // this will be awaited first +}); +policy.onOpen(async () => { + // then this will be awaited +}); + +// errors thrown by an onOpenFn will be caught and ignored +policy.onOpen(() => { + // throwing an error has no effect outside the method + throw new Error(); +}); +``` + +```typescript +policy.onAttemptingClose( + // onAttemptingCloseFns can be sync or async, they will be awaited + async () => {}, +); + +// you can set multiple onAttemptingCloseFns, they will run sequentially +policy.onAttemptingClose(async () => { + // this will be awaited first +}); +policy.onAttemptingClose(async () => { + // then this will be awaited +}); + +// errors thrown by an onAttemptingCloseFn will be caught and ignored +policy.onAttemptingClose(() => { + // throwing an error has no effect outside the method + throw new Error(); +}); +``` + +```typescript +policy.onIsolate( + // onIsolateFns can be sync or async, they will be awaited + async () => {}, +); + +// you can set multiple onIsolateFns, they will run sequentially +policy.onIsolate(async () => { + // this will be awaited first +}); +policy.onIsolate(async () => { + // then this will be awaited +}); + +// errors thrown by an onIsolateFn will be caught and ignored +policy.onIsolate(() => { + // throwing an error has no effect outside the method + throw new Error(); +}); +``` + +### Proactive policies + +Every proactive policy extends the `ProactivePolicy` class. + +#### TimeoutPolicy + +`TimeoutPolicy` claims that after some time, it is unlikely the call will be successful. It ensures the caller does not have to wait more than the specified timeout. + +Only asynchronous methods can be executed within a `TimeoutPolicy`, or else no timeout happens. `TimeoutPolicy` is implemented with `Promise.race()`, racing the promise returned by the executed method (`executionPromise`) with a promise that is rejected after the specified time elapses (`timeoutPromise`). If the executed method is not asynchronous (i.e. it does not have at least one point to pause its execution at), no timeout will happen even if the execution takes longer than the specified timeout duration, since there is no point in time for taking the control out from the executed method's hands to reject the `timeoutPromise`. + +The executed method is fully executed to its end (unless it throws an exception), regardless of whether a timeout has occured or not. `TimeoutPolicy` ensures that the caller does not have to wait more than the specified timeout, but it does neither cancel nor abort\* the execution of the method. This means that if the executed method has side effects, these side effects can occur even after the timeout happened. + +\*TypeScript/JavaScript has no _generic_ way of canceling or aborting an executing method, either synchronous or asynchronous. `TimeoutPolicy` runs arbitrary user-provided code: it cannot be assumed the code is prepared in any way (e.g. it has cancel points). The provided code _could_ be executed in a separate worker thread so it can be aborted instantaneously by terminating the worker, but run-time compiling a worker from user-provided code is ugly and error-prone. + +On timeout, the promise returned by the policy's `execute` method is rejected with a `TimeoutException`: + +```typescript +import { TimeoutException, TimeoutPolicy } from '@diplomatiq/resily'; + +// the wrapped method is supposed to return a string +const policy = new TimeoutPolicy(); + +try { + const result = await policy.execute(async () => { + // the executed code + }); +} catch (ex) { + if (ex instanceof TimeoutException) { + // the operation timed out + } else { + // the executed method thrown an exception + } +} +``` + +Configure how long the waiting period should be: + +```typescript +import { TimeoutPolicy } from '@diplomatiq/resily'; + +// the wrapped method is supposed to return a string +const policy = new TimeoutPolicy(); +policy.timeoutAfter(1000); // timeout after 1000 ms +``` + +Perform certain actions on timeout: + +```typescript +import { TimeoutPolicy } from '@diplomatiq/resily'; + +// the wrapped method is supposed to return a string +const policy = new TimeoutPolicy(); +policy.onTimeout( + // onTimeoutFns can be sync or async, they will be awaited + async timedOutAfterMs => { + // the policy was configured to timeout after timedOutAfterMs + }, +); + +// you can set multiple onTimeoutFns, they will run sequentially +policy.onTimeout(async () => { + // this will be awaited first +}); +policy.onTimeout(async () => { + // then this will be awaited +}); + +// errors thrown by an onTimeoutFn will be caught and ignored +policy.onTimeout(() => { + // throwing an error has no effect outside the method + throw new Error(); +}); +``` + +Throwing a `TimeoutException` from the executed method is not a timeout, therefore it does not trigger running `onTimeout` functions: + +```typescript +import { TimeoutException, TimeoutPolicy } from '@diplomatiq/resily'; + +// the wrapped method is supposed to return a string +const policy = new TimeoutPolicy(); + +let onTimeoutRan = false; +policy.onTimeout(() => { + onTimeoutRan = true; +}); + +try { + await policy.execute(async () => { + throw new TimeoutException(); + }); +} catch (ex) { + // ex is a TimeoutException (thrown by the executed method) + const isTimeoutException = ex instanceof TimeoutException; // true +} + +// onTimeoutRan is false +``` + +#### BulkheadIsolationPolicy + +`BulkheadIsolationPolicy` claims that too many concurrent calls can overload a resource. It limits the number of concurrently executed actions as specified. + +Method calls executed via the policy are placed into a size-limited bulkhead compartment, limiting the maximum number of concurrent executions. + +If the bulkhead compartment is full — meaning the maximum number of concurrent executions is reached —, additional calls can be queued up, ready to be executed whenever a place falls vacant in the bulkhead compartment (i.e. an execution finishes). Queuing up these calls ensures that the resource protected by the policy is always at maximum utilization, while limiting the number of concurrent actions ensures that the resource is not overloaded. The queue is a simple FIFO buffer. + +When the policy's `execute` method is invoked with a method to be executed, the policy's operation can be described as follows: + +- `(1)` If there is an execution slot available in the bulkhead compartment, execute the method immediately. + +- `(2)` Else if there is still space in the queue, enqueue the execution intent of the method — without actually executing the method —, then wait asynchronously until the method can be executed. + + An execution intent gets dequeued — and its corresponding method gets executed — each time an execution slot becomes available in the bulkhead compartment. + +- `(3)` Else throw a `BulkheadCompartmentRejectedException`. + +From the caller's point of view, this is all transparent: the promise returned by the `execute` method is + +- either eventually resolved with the return value of the wrapped method (cases `(1)` and `(2)`), +- or eventually rejected with an exception thrown by the wrapped method (cases `(1)` and `(2)`), +- or rejected with a `BulkheadCompartmentRejectedException` (case `(3)`). + +Configure the size of the bulkhead compartment: + +```typescript +import { BulkheadIsolationPolicy } from '@diplomatiq/resily'; + +// the wrapped method is supposed to return a string +const policy = new BulkheadIsolationPolicy(); + +// allow maximum 3 concurrent executions +policy.maxConcurrency(3); + +// this overwrites the previous value +policy.maxConcurrency(5); +``` + +Configure the size of the queue: + +```typescript +import { BulkheadIsolationPolicy } from '@diplomatiq/resily'; + +// the wrapped method is supposed to return a string +const policy = new BulkheadIsolationPolicy(); + +// allow maximum 3 queued actions +policy.maxQueuedActions(3); + +// this overwrites the previous value +policy.maxQueuedActions(5); +``` + +Get usage information about the bulkhead compartment: + +```typescript +import { BulkheadIsolationPolicy } from '@diplomatiq/resily'; + +// the wrapped method is supposed to return a string +const policy = new BulkheadIsolationPolicy(); + +// the number of available (free) execution slots in the bulkhead compartment +policy.getAvailableSlotsCount(); + +// the number of available (free) spaces in the queue +policy.getAvailableQueuedActionsCount(); +``` + +#### CachePolicy + +`CachePolicy` claims that within a given time frame, a system may respond with the same answer, thus there is no need to actually perform the query. It retrieves the response from a local cache within the time frame, after storing it on the first query. + +The `CachePolicy` is implemented as a simple in-memory cache. It works as follows: + +- For the first time (and every further time the cache is invalid), the `CachePolicy` executes the wrapped method, and caches its result. +- For subsequent execution calls, the cached result is returned and the wrapped method is not executed — as long as the cache remains valid. +- The cache is valid as long as it is not expired (see time to live settings below) or manually invalidated. + +Configure how long the cache should be valid: + +```typescript +import { CachePolicy } from '@diplomatiq/resily'; + +// the wrapped method is supposed to return a string +const policy = new CachePolicy; + +// the cache is valid for 10000ms from the moment the value is stored in the cache +policy.timeToLive('relative', 10000); + +// the cache is valid as long as Date.now() < 772149600000 +// this overwrites the previous setting +policy.timeToLive('absolute', 772149600000); + +// the cache is valid for 10000ms from the moment the value is stored in or retrieved from the cache +// this overwrites the previous setting +policy.timeToLive('sliding', 10000); +``` + +Invalidate the cache manually, causing the next `execute` call to run the wrapped method: + +```typescript +import { CachePolicy } from '@diplomatiq/resily'; + +// the wrapped method is supposed to return a string +const policy = new CachePolicy; + +policy.invalidate(); +``` + +Perform actions on caching events: + +```typescript +import { CachePolicy } from '@diplomatiq/resily'; + +// the wrapped method is supposed to return a string +const policy = new CachePolicy; +``` + +```typescript +// perform an action before the value is retrieved from the cache +policy.onCacheGet( + // onCacheGetFns can be sync or async, they will be awaited + async () => {}, +); + +// you can set multiple onCacheGetFns, they will run sequentially +policy.onCacheGet(async () => { + // this will be awaited first +}); +policy.onCacheGet(async () => { + // then this will be awaited +}); + +// errors thrown by an onCacheGetFn will be caught and ignored +policy.onCacheGet(() => { + // throwing an error has no effect outside the method + throw new Error(); +}); +``` + +```typescript +// perform an action before the wrapped method is executed and its result is cached +policy.onCacheMiss( + // onCacheMissFns can be sync or async, they will be awaited + async () => {}, +); + +// you can set multiple onCacheMissFns, they will run sequentially +policy.onCacheMiss(async () => { + // this will be awaited first +}); +policy.onCacheMiss(async () => { + // then this will be awaited +}); + +// errors thrown by an onCacheMissFn will be caught and ignored +policy.onCacheMiss(() => { + // throwing an error has no effect outside the method + throw new Error(); +}); +``` + +```typescript +// perform an action after the wrapped method is executed and its result is cached +policy.onCachePut( + // onCachePutFns can be sync or async, they will be awaited + async () => {}, +); + +// you can set multiple onCachePutFns, they will run sequentially +policy.onCachePut(async () => { + // this will be awaited first +}); +policy.onCachePut(async () => { + // then this will be awaited +}); + +// errors thrown by an onCachePutFn will be caught and ignored +policy.onCachePut(() => { + // throwing an error has no effect outside the method + throw new Error(); +}); +``` + +### Helpers and utilities + +#### NopPolicy + +`NopPolicy` does not claim anything. It executes the wrapped method, and returns its result or throws its exceptions, without any intervention. + +#### PolicyCombination + +Policies can be combined in multiple ways. Given the following: + +```typescript +import { FallbackPolicy, RetryPolicy, TimeoutPolicy } from '@diplomatiq/resily'; + +const timeoutPolicy = new TimeoutPolicy(); +const retryPolicy = new RetryPolicy(); +const fallbackPolicy = new FallbackPolicy(); + +const fn = () => { + // the executed code +}; +``` + +The naïve way to combine the above policies would be: + +```typescript +fallbackPolicy.execute(() => retryPolicy.execute(() => timeoutPolicy.execute(fn))); +``` + +The previous example is equivalent to the following: + +```typescript +fallbackPolicy.wrap(retryPolicy); +retryPolicy.wrap(timeoutPolicy); + +fallbackPolicy.execute(fn); +``` + +And also equivalent to the following: + +```typescript +PolicyCombination.wrap([fallbackPolicy, retryPolicy, timeoutPolicy]).execute(fn); +``` + +PolicyCombination expects at least two policies to be combined. + +### Modifying a policy's configuration + +All policies' configuration parameters are set via setter methods. This could imply that all policies can be safely reconfigured whenever needed, but providing setter methods instead of constructor parameters is merely because this way the policies are more convenient to use. If you need to reconfigure a policy, you can do that, but not while it is still executing one or more methods: reconfiguring while executing could lead to unexpected side-effects. Therefore, if you tries to reconfigure a policy while executing, a `PolicyModificationNotAllowedException` is thrown. + +To safely reconfigure a policy, check whether it is executing or not: + +```typescript +const policy = … // any policy + +if (!policy.isExecuting()) { + // you can reconfigure the policy +} +``` + +## Development + +See [CONTRIBUTING.md](https://github.com/Diplomatiq/resily/blob/develop/CONTRIBUTING.md) for details. --- diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..5e95153 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,5363 @@ +{ + "name": "@diplomatiq/resily", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@babel/code-frame": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz", + "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==", + "dev": true, + "requires": { + "@babel/highlight": "^7.8.3" + } + }, + "@babel/core": { + "version": "7.8.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.8.7.tgz", + "integrity": "sha512-rBlqF3Yko9cynC5CCFy6+K/w2N+Sq/ff2BPy+Krp7rHlABIr5epbA7OxVeKoMHB39LZOp1UY5SuLjy6uWi35yA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.8.3", + "@babel/generator": "^7.8.7", + "@babel/helpers": "^7.8.4", + "@babel/parser": "^7.8.7", + "@babel/template": "^7.8.6", + "@babel/traverse": "^7.8.6", + "@babel/types": "^7.8.7", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.1", + "json5": "^2.1.0", + "lodash": "^4.17.13", + "resolve": "^1.3.2", + "semver": "^5.4.1", + "source-map": "^0.5.0" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } + } + }, + "@babel/generator": { + "version": "7.8.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.8.8.tgz", + "integrity": "sha512-HKyUVu69cZoclptr8t8U5b6sx6zoWjh8jiUhnuj3MpZuKT2dJ8zPTuiy31luq32swhI0SpwItCIlU8XW7BZeJg==", + "dev": true, + "requires": { + "@babel/types": "^7.8.7", + "jsesc": "^2.5.1", + "lodash": "^4.17.13", + "source-map": "^0.5.0" + }, + "dependencies": { + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } + } + }, + "@babel/helper-function-name": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.8.3.tgz", + "integrity": "sha512-BCxgX1BC2hD/oBlIFUgOCQDOPV8nSINxCwM3o93xP4P9Fq6aV5sgv2cOOITDMtCfQ+3PvHp3l689XZvAM9QyOA==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "^7.8.3", + "@babel/template": "^7.8.3", + "@babel/types": "^7.8.3" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.8.3.tgz", + "integrity": "sha512-FVDR+Gd9iLjUMY1fzE2SR0IuaJToR4RkCDARVfsBBPSP53GEqSFjD8gNyxg246VUyc/ALRxFaAK8rVG7UT7xRA==", + "dev": true, + "requires": { + "@babel/types": "^7.8.3" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.8.3.tgz", + "integrity": "sha512-3x3yOeyBhW851hroze7ElzdkeRXQYQbFIb7gLK1WQYsw2GWDay5gAJNw1sWJ0VFP6z5J1whqeXH/WCdCjZv6dA==", + "dev": true, + "requires": { + "@babel/types": "^7.8.3" + } + }, + "@babel/helpers": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.8.4.tgz", + "integrity": "sha512-VPbe7wcQ4chu4TDQjimHv/5tj73qz88o12EPkO2ValS2QiQS/1F2SsjyIGNnAD0vF/nZS6Cf9i+vW6HIlnaR8w==", + "dev": true, + "requires": { + "@babel/template": "^7.8.3", + "@babel/traverse": "^7.8.4", + "@babel/types": "^7.8.3" + } + }, + "@babel/highlight": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.8.3.tgz", + "integrity": "sha512-PX4y5xQUvy0fnEVHrYOarRPXVWafSjTW9T0Hab8gVIawpl2Sj0ORyrygANq+KjcNlSSTw0YCLSNA8OyZ1I4yEg==", + "dev": true, + "requires": { + "chalk": "^2.0.0", + "esutils": "^2.0.2", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.8.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.8.8.tgz", + "integrity": "sha512-mO5GWzBPsPf6865iIbzNE0AvkKF3NE+2S3eRUpE+FE07BOAkXh6G+GW/Pj01hhXjve1WScbaIO4UlY1JKeqCcA==", + "dev": true + }, + "@babel/runtime": { + "version": "7.8.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.8.7.tgz", + "integrity": "sha512-+AATMUFppJDw6aiR5NVPHqIQBlV/Pj8wY/EZH+lmvRdUo9xBaz/rF3alAwFJQavvKfeOlPE7oaaDHVbcySbCsg==", + "dev": true, + "requires": { + "regenerator-runtime": "^0.13.4" + }, + "dependencies": { + "regenerator-runtime": { + "version": "0.13.5", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz", + "integrity": "sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA==", + "dev": true + } + } + }, + "@babel/template": { + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.8.6.tgz", + "integrity": "sha512-zbMsPMy/v0PWFZEhQJ66bqjhH+z0JgMoBWuikXybgG3Gkd/3t5oQ1Rw2WQhnSrsOmsKXnZOx15tkC4qON/+JPg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.8.3", + "@babel/parser": "^7.8.6", + "@babel/types": "^7.8.6" + } + }, + "@babel/traverse": { + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.8.6.tgz", + "integrity": "sha512-2B8l0db/DPi8iinITKuo7cbPznLCEk0kCxDoB9/N6gGNg/gxOXiR/IcymAFPiBwk5w6TtQ27w4wpElgp9btR9A==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.8.3", + "@babel/generator": "^7.8.6", + "@babel/helper-function-name": "^7.8.3", + "@babel/helper-split-export-declaration": "^7.8.3", + "@babel/parser": "^7.8.6", + "@babel/types": "^7.8.6", + "debug": "^4.1.0", + "globals": "^11.1.0", + "lodash": "^4.17.13" + }, + "dependencies": { + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true + } + } + }, + "@babel/types": { + "version": "7.8.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.7.tgz", + "integrity": "sha512-k2TreEHxFA4CjGkL+GYjRyx35W0Mr7DP5+9q6WMkyKXB+904bYmG40syjMFV0oLlhhFCwWl0vA0DyzTDkwAiJw==", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "lodash": "^4.17.13", + "to-fast-properties": "^2.0.0" + } + }, + "@commitlint/cli": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/@commitlint/cli/-/cli-8.3.5.tgz", + "integrity": "sha512-6+L0vbw55UEdht71pgWOE55SRgb+8OHcEwGDB234VlIBFGK9P2QOBU7MHiYJ5cjdjCQ0rReNrGjOHmJ99jwf0w==", + "dev": true, + "requires": { + "@commitlint/format": "^8.3.4", + "@commitlint/lint": "^8.3.5", + "@commitlint/load": "^8.3.5", + "@commitlint/read": "^8.3.4", + "babel-polyfill": "6.26.0", + "chalk": "2.4.2", + "get-stdin": "7.0.0", + "lodash": "4.17.15", + "meow": "5.0.0", + "resolve-from": "5.0.0", + "resolve-global": "1.0.0" + } + }, + "@commitlint/ensure": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/@commitlint/ensure/-/ensure-8.3.4.tgz", + "integrity": "sha512-8NW77VxviLhD16O3EUd02lApMFnrHexq10YS4F4NftNoErKbKaJ0YYedktk2boKrtNRf/gQHY/Qf65edPx4ipw==", + "dev": true, + "requires": { + "lodash": "4.17.15" + } + }, + "@commitlint/execute-rule": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/@commitlint/execute-rule/-/execute-rule-8.3.4.tgz", + "integrity": "sha512-f4HigYjeIBn9f7OuNv5zh2y5vWaAhNFrfeul8CRJDy82l3Y+09lxOTGxfF3uMXKrZq4LmuK6qvvRCZ8mUrVvzQ==", + "dev": true + }, + "@commitlint/format": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/@commitlint/format/-/format-8.3.4.tgz", + "integrity": "sha512-809wlQ/ND6CLZON+w2Rb3YM2TLNDfU2xyyqpZeqzf2reJNpySMSUAeaO/fNDJSOKIsOsR3bI01rGu6hv28k+Nw==", + "dev": true, + "requires": { + "chalk": "^2.0.1" + } + }, + "@commitlint/is-ignored": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/@commitlint/is-ignored/-/is-ignored-8.3.5.tgz", + "integrity": "sha512-Zo+8a6gJLFDTqyNRx53wQi/XTiz8mncvmWf/4oRG+6WRcBfjSSHY7KPVj5Y6UaLy2EgZ0WQ2Tt6RdTDeQiQplA==", + "dev": true, + "requires": { + "semver": "6.3.0" + } + }, + "@commitlint/lint": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/@commitlint/lint/-/lint-8.3.5.tgz", + "integrity": "sha512-02AkI0a6PU6rzqUvuDkSi6rDQ2hUgkq9GpmdJqfai5bDbxx2939mK4ZO+7apbIh4H6Pae7EpYi7ffxuJgm+3hQ==", + "dev": true, + "requires": { + "@commitlint/is-ignored": "^8.3.5", + "@commitlint/parse": "^8.3.4", + "@commitlint/rules": "^8.3.4", + "babel-runtime": "^6.23.0", + "lodash": "4.17.15" + } + }, + "@commitlint/load": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/@commitlint/load/-/load-8.3.5.tgz", + "integrity": "sha512-poF7R1CtQvIXRmVIe63FjSQmN9KDqjRtU5A6hxqXBga87yB2VUJzic85TV6PcQc+wStk52cjrMI+g0zFx+Zxrw==", + "dev": true, + "requires": { + "@commitlint/execute-rule": "^8.3.4", + "@commitlint/resolve-extends": "^8.3.5", + "babel-runtime": "^6.23.0", + "chalk": "2.4.2", + "cosmiconfig": "^5.2.0", + "lodash": "4.17.15", + "resolve-from": "^5.0.0" + } + }, + "@commitlint/message": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/@commitlint/message/-/message-8.3.4.tgz", + "integrity": "sha512-nEj5tknoOKXqBsaQtCtgPcsAaf5VCg3+fWhss4Vmtq40633xLq0irkdDdMEsYIx8rGR0XPBTukqzln9kAWCkcA==", + "dev": true + }, + "@commitlint/parse": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/@commitlint/parse/-/parse-8.3.4.tgz", + "integrity": "sha512-b3uQvpUQWC20EBfKSfMRnyx5Wc4Cn778bVeVOFErF/cXQK725L1bYFvPnEjQO/GT8yGVzq2wtLaoEqjm1NJ/Bw==", + "dev": true, + "requires": { + "conventional-changelog-angular": "^1.3.3", + "conventional-commits-parser": "^3.0.0", + "lodash": "^4.17.11" + } + }, + "@commitlint/read": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/@commitlint/read/-/read-8.3.4.tgz", + "integrity": "sha512-FKv1kHPrvcAG5j+OSbd41IWexsbLhfIXpxVC/YwQZO+FR0EHmygxQNYs66r+GnhD1EfYJYM4WQIqd5bJRx6OIw==", + "dev": true, + "requires": { + "@commitlint/top-level": "^8.3.4", + "@marionebl/sander": "^0.6.0", + "babel-runtime": "^6.23.0", + "git-raw-commits": "^2.0.0" + } + }, + "@commitlint/resolve-extends": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/@commitlint/resolve-extends/-/resolve-extends-8.3.5.tgz", + "integrity": "sha512-nHhFAK29qiXNe6oH6uG5wqBnCR+BQnxlBW/q5fjtxIaQALgfoNLHwLS9exzbIRFqwJckpR6yMCfgMbmbAOtklQ==", + "dev": true, + "requires": { + "import-fresh": "^3.0.0", + "lodash": "4.17.15", + "resolve-from": "^5.0.0", + "resolve-global": "^1.0.0" + } + }, + "@commitlint/rules": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/@commitlint/rules/-/rules-8.3.4.tgz", + "integrity": "sha512-xuC9dlqD5xgAoDFgnbs578cJySvwOSkMLQyZADb1xD5n7BNcUJfP8WjT9W1Aw8K3Wf8+Ym/ysr9FZHXInLeaRg==", + "dev": true, + "requires": { + "@commitlint/ensure": "^8.3.4", + "@commitlint/message": "^8.3.4", + "@commitlint/to-lines": "^8.3.4", + "babel-runtime": "^6.23.0" + } + }, + "@commitlint/to-lines": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/@commitlint/to-lines/-/to-lines-8.3.4.tgz", + "integrity": "sha512-5AvcdwRsMIVq0lrzXTwpbbG5fKRTWcHkhn/hCXJJ9pm1JidsnidS1y0RGkb3O50TEHGewhXwNoavxW9VToscUA==", + "dev": true + }, + "@commitlint/top-level": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/@commitlint/top-level/-/top-level-8.3.4.tgz", + "integrity": "sha512-nOaeLBbAqSZNpKgEtO6NAxmui1G8ZvLG+0wb4rvv6mWhPDzK1GNZkCd8FUZPahCoJ1iHDoatw7F8BbJLg4nDjg==", + "dev": true, + "requires": { + "find-up": "^4.0.0" + }, + "dependencies": { + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-limit": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.2.tgz", + "integrity": "sha512-WGR+xHecKTr7EbUEhyLSh5Dube9JtdiG78ufaeLxTgpudf/20KqyMioIUZJAezlTIi6evxuoUs9YXc11cU+yzQ==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + } + } + }, + "@diplomatiq/crypto-random": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@diplomatiq/crypto-random/-/crypto-random-2.2.0.tgz", + "integrity": "sha512-3B6EhsGLTxUzpK6ShaNwg4t3sqOknlMqI7q07z6CUupwplW2As295YgoJR5LWQ5HpzRwEQ37BseOyxs7VOmjFw==" + }, + "@diplomatiq/eslint-config-tslib": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@diplomatiq/eslint-config-tslib/-/eslint-config-tslib-3.0.0.tgz", + "integrity": "sha512-a3wpZU2ruYMktnvu0lP6gNl4+93VKUPpBdLmGxAATI8VJOLyHKj8QcciscwJYstXarR3440C9xnhp2m9wp6mqA==", + "dev": true, + "requires": { + "@typescript-eslint/eslint-plugin": "^2.19.2", + "@typescript-eslint/parser": "^2.19.2", + "eslint": "^6.8.0", + "eslint-config-prettier": "^6.10.0", + "eslint-plugin-import": "^2.20.1", + "eslint-plugin-prettier": "^3.1.2", + "eslint-plugin-promise": "^4.2.1", + "prettier": "^1.19.1", + "typescript": "^3.8.2" + } + }, + "@istanbuljs/load-nyc-config": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.0.0.tgz", + "integrity": "sha512-ZR0rq/f/E4f4XcgnDvtMWXCUJpi8eO0rssVhmztsZqLIEFA9UUP9zmpE0VxlM+kv/E1ul2I876Fwil2ayptDVg==", + "dev": true, + "requires": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "dependencies": { + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-limit": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.2.tgz", + "integrity": "sha512-WGR+xHecKTr7EbUEhyLSh5Dube9JtdiG78ufaeLxTgpudf/20KqyMioIUZJAezlTIi6evxuoUs9YXc11cU+yzQ==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + } + } + }, + "@istanbuljs/schema": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.2.tgz", + "integrity": "sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw==", + "dev": true + }, + "@marionebl/sander": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@marionebl/sander/-/sander-0.6.1.tgz", + "integrity": "sha1-GViWWHTyS8Ub5Ih1/rUNZC/EH3s=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.3", + "mkdirp": "^0.5.1", + "rimraf": "^2.5.2" + } + }, + "@sinonjs/commons": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.7.1.tgz", + "integrity": "sha512-Debi3Baff1Qu1Unc3mjJ96MgpbwTn43S1+9yJ0llWygPwDNu2aaWBD6yc9y/Z8XDRNhx7U+u2UDg2OGQXkclUQ==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "@sinonjs/fake-timers": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-6.0.0.tgz", + "integrity": "sha512-atR1J/jRXvQAb47gfzSK8zavXy7BcpnYq21ALon0U99etu99vsir0trzIO3wpeLtW+LLVY6X7EkfVTbjGSH8Ww==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.7.0" + } + }, + "@sinonjs/formatio": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-5.0.1.tgz", + "integrity": "sha512-KaiQ5pBf1MpS09MuA0kp6KBQt2JUOQycqVG1NZXvzeaXe5LGFqAKueIS0bw4w0P9r7KuBSVdUk5QjXsUdu2CxQ==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1", + "@sinonjs/samsam": "^5.0.2" + } + }, + "@sinonjs/samsam": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-5.0.3.tgz", + "integrity": "sha512-QucHkc2uMJ0pFGjJUDP3F9dq5dx8QIaqISl9QgwLOh6P9yv877uONPGXh/OH/0zmM3tW1JjuJltAZV2l7zU+uQ==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.6.0", + "lodash.get": "^4.4.2", + "type-detect": "^4.0.8" + } + }, + "@sinonjs/text-encoding": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz", + "integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==", + "dev": true + }, + "@types/chai": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.2.11.tgz", + "integrity": "sha512-t7uW6eFafjO+qJ3BIV2gGUyZs27egcNRkUdalkud+Qa3+kg//f129iuOFivHDXQ+vnU3fDXuwgv0cqMCbcE8sw==", + "dev": true + }, + "@types/color-name": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", + "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", + "dev": true + }, + "@types/eslint-visitor-keys": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz", + "integrity": "sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag==", + "dev": true + }, + "@types/json-schema": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.4.tgz", + "integrity": "sha512-8+KAKzEvSUdeo+kmqnKrqgeE+LcA0tjYWFY7RPProVYwnqDjukzO+3b6dLD56rYX5TdWejnEOLJYOIeh4CXKuA==", + "dev": true + }, + "@types/mocha": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-7.0.2.tgz", + "integrity": "sha512-ZvO2tAcjmMi8V/5Z3JsyofMe3hasRcaw88cto5etSVMwVQfeivGAlEYmaQgceUSVYFofVjT+ioHsATjdWcFt1w==", + "dev": true + }, + "@types/node": { + "version": "13.11.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-13.11.0.tgz", + "integrity": "sha512-uM4mnmsIIPK/yeO+42F2RQhGUIs39K2RFmugcJANppXe6J1nvH87PvzPZYpza7Xhhs8Yn9yIAVdLZ84z61+0xQ==", + "dev": true + }, + "@types/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==", + "dev": true + }, + "@types/sinon": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-9.0.0.tgz", + "integrity": "sha512-v2TkYHkts4VXshMkcmot/H+ERZ2SevKa10saGaJPGCJ8vh3lKrC4u663zYEeRZxep+VbG6YRDtQ6gVqw9dYzPA==", + "dev": true, + "requires": { + "@types/sinonjs__fake-timers": "*" + } + }, + "@types/sinonjs__fake-timers": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-6.0.1.tgz", + "integrity": "sha512-yYezQwGWty8ziyYLdZjwxyMb0CZR49h8JALHGrxjQHWlqGgc8kLdHEgWrgL0uZ29DMvEVBDnHU2Wg36zKSIUtA==", + "dev": true + }, + "@typescript-eslint/eslint-plugin": { + "version": "2.24.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.24.0.tgz", + "integrity": "sha512-wJRBeaMeT7RLQ27UQkDFOu25MqFOBus8PtOa9KaT5ZuxC1kAsd7JEHqWt4YXuY9eancX0GK9C68i5OROnlIzBA==", + "dev": true, + "requires": { + "@typescript-eslint/experimental-utils": "2.24.0", + "eslint-utils": "^1.4.3", + "functional-red-black-tree": "^1.0.1", + "regexpp": "^3.0.0", + "tsutils": "^3.17.1" + } + }, + "@typescript-eslint/experimental-utils": { + "version": "2.24.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-2.24.0.tgz", + "integrity": "sha512-DXrwuXTdVh3ycNCMYmWhUzn/gfqu9N0VzNnahjiDJvcyhfBy4gb59ncVZVxdp5XzBC77dCncu0daQgOkbvPwBw==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.3", + "@typescript-eslint/typescript-estree": "2.24.0", + "eslint-scope": "^5.0.0" + } + }, + "@typescript-eslint/parser": { + "version": "2.24.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-2.24.0.tgz", + "integrity": "sha512-H2Y7uacwSSg8IbVxdYExSI3T7uM1DzmOn2COGtCahCC3g8YtM1xYAPi2MAHyfPs61VKxP/J/UiSctcRgw4G8aw==", + "dev": true, + "requires": { + "@types/eslint-visitor-keys": "^1.0.0", + "@typescript-eslint/experimental-utils": "2.24.0", + "@typescript-eslint/typescript-estree": "2.24.0", + "eslint-visitor-keys": "^1.1.0" + } + }, + "@typescript-eslint/typescript-estree": { + "version": "2.24.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-2.24.0.tgz", + "integrity": "sha512-RJ0yMe5owMSix55qX7Mi9V6z2FDuuDpN6eR5fzRJrp+8in9UF41IGNQHbg5aMK4/PjVaEQksLvz0IA8n+Mr/FA==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "eslint-visitor-keys": "^1.1.0", + "glob": "^7.1.6", + "is-glob": "^4.0.1", + "lodash": "^4.17.15", + "semver": "^6.3.0", + "tsutils": "^3.17.1" + } + }, + "JSONStream": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz", + "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==", + "dev": true, + "requires": { + "jsonparse": "^1.2.0", + "through": ">=2.2.7 <3" + } + }, + "acorn": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.1.tgz", + "integrity": "sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg==", + "dev": true + }, + "acorn-jsx": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.2.0.tgz", + "integrity": "sha512-HiUX/+K2YpkpJ+SzBffkM/AQ2YE03S0U1kjTLVpoJdhZMOWy8qvXVN9JdLqv2QsaQ6MPYQIuNmwD8zOiYUofLQ==", + "dev": true + }, + "add-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/add-stream/-/add-stream-1.0.0.tgz", + "integrity": "sha1-anmQQ3ynNtXhKI25K9MmbV9csqo=", + "dev": true + }, + "aggregate-error": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.0.1.tgz", + "integrity": "sha512-quoaXsZ9/BLNae5yiNoUz+Nhkwz83GhWwtYFglcjEQB2NDHCIpApbqXxIFnm4Pq/Nvhrsq5sYJFyohrrxnTGAA==", + "dev": true, + "requires": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "dependencies": { + "indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true + } + } + }, + "ajv": { + "version": "6.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.0.tgz", + "integrity": "sha512-D6gFiFA0RRLyUbvijN74DWAjXSFxWKaWP7mldxkVhyhAV3+SWA9HEJPHQ2c9soIeTFJqcSdFDGFgdqs1iUU2Hw==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ansi-colors": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.3.tgz", + "integrity": "sha512-LEHHyuhlPY3TmuUYMh2oz89lTShfvgbmzaBcxve9t/9Wuy7Dwf4yoAKcND7KFT1HAQfqZ12qtc+DUrBMeKF9nw==", + "dev": true + }, + "ansi-escapes": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.1.tgz", + "integrity": "sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA==", + "dev": true, + "requires": { + "type-fest": "^0.11.0" + }, + "dependencies": { + "type-fest": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.11.0.tgz", + "integrity": "sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ==", + "dev": true + } + } + }, + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "anymatch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", + "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "append-transform": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-2.0.0.tgz", + "integrity": "sha512-7yeyCEurROLQJFv5Xj4lEGTy0borxepjFv1g22oAdqFu//SrAlDl1O1Nxx15SH1RoliUml6p8dwJW9jvZughhg==", + "dev": true, + "requires": { + "default-require-extensions": "^3.0.0" + } + }, + "archy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", + "integrity": "sha1-+cjBN1fMHde8N5rHeyxipcKGjEA=", + "dev": true + }, + "arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "array-find-index": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", + "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=", + "dev": true + }, + "array-ify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-ify/-/array-ify-1.0.0.tgz", + "integrity": "sha1-nlKHYrSpBmrRY6aWKjZEGOlibs4=", + "dev": true + }, + "array-includes": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.1.tgz", + "integrity": "sha512-c2VXaCHl7zPsvpkFsw4nxvFie4fh1ur9bpcgsVkIjqn0H/Xwdg+7fv3n2r/isyS8EBj5b06M9kHyZuIr4El6WQ==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0", + "is-string": "^1.0.5" + } + }, + "array.prototype.flat": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.2.3.tgz", + "integrity": "sha512-gBlRZV0VSmfPIeWfuuy56XZMvbVfbEUnOXUvt3F/eUUUSyzlgLxhEX4YAEpxNAogRGehPSnfXyPtYyKAhkzQhQ==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0-next.1" + } + }, + "arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", + "dev": true + }, + "assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true + }, + "astral-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", + "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", + "dev": true + }, + "babel-polyfill": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-polyfill/-/babel-polyfill-6.26.0.tgz", + "integrity": "sha1-N5k3q8Z9eJWXCtxiHyhM2WbPIVM=", + "dev": true, + "requires": { + "babel-runtime": "^6.26.0", + "core-js": "^2.5.0", + "regenerator-runtime": "^0.10.5" + }, + "dependencies": { + "regenerator-runtime": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz", + "integrity": "sha1-M2w+/BIgrc7dosn6tntaeVWjNlg=", + "dev": true + } + } + }, + "babel-runtime": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", + "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", + "dev": true, + "requires": { + "core-js": "^2.4.0", + "regenerator-runtime": "^0.11.0" + } + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "binary-extensions": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.0.0.tgz", + "integrity": "sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow==", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, + "buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", + "dev": true + }, + "caching-transform": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/caching-transform/-/caching-transform-4.0.0.tgz", + "integrity": "sha512-kpqOvwXnjjN44D89K5ccQC+RUrsy7jB/XLlRrx0D7/2HNcTPqzsb6XgYoErwko6QsV184CA2YgS1fxDiiDZMWA==", + "dev": true, + "requires": { + "hasha": "^5.0.0", + "make-dir": "^3.0.0", + "package-hash": "^4.0.0", + "write-file-atomic": "^3.0.0" + } + }, + "caller-callsite": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/caller-callsite/-/caller-callsite-2.0.0.tgz", + "integrity": "sha1-hH4PzgoiN1CpoCfFSzNzGtMVQTQ=", + "dev": true, + "requires": { + "callsites": "^2.0.0" + }, + "dependencies": { + "callsites": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz", + "integrity": "sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA=", + "dev": true + } + } + }, + "caller-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-2.0.0.tgz", + "integrity": "sha1-Ro+DBE42mrIBD6xfBs7uFbsssfQ=", + "dev": true, + "requires": { + "caller-callsite": "^2.0.0" + } + }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + }, + "camelcase": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", + "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", + "dev": true + }, + "camelcase-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-4.2.0.tgz", + "integrity": "sha1-oqpfsa9oh1glnDLBQUJteJI7m3c=", + "dev": true, + "requires": { + "camelcase": "^4.1.0", + "map-obj": "^2.0.0", + "quick-lru": "^1.0.0" + } + }, + "chai": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz", + "integrity": "sha512-XQU3bhBukrOsQCuwZndwGcCVQHyZi53fQ6Ys1Fym7E4olpIqqZZhhoFJoaKVvV17lWQoXYwgWN2nF5crA8J2jw==", + "dev": true, + "requires": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.2", + "deep-eql": "^3.0.1", + "get-func-name": "^2.0.0", + "pathval": "^1.1.0", + "type-detect": "^4.0.5" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "dev": true + }, + "check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", + "dev": true + }, + "chokidar": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.3.0.tgz", + "integrity": "sha512-dGmKLDdT3Gdl7fBUe8XK+gAtGmzy5Fn0XkkWQuYxGIgWVPPse2CxFA5mtrlD0TOHaHjEUqkWNyP1XdHoJES/4A==", + "dev": true, + "requires": { + "anymatch": "~3.1.1", + "braces": "~3.0.2", + "fsevents": "~2.1.1", + "glob-parent": "~5.1.0", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.2.0" + } + }, + "ci-info": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", + "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", + "dev": true + }, + "clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true + }, + "cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "requires": { + "restore-cursor": "^3.1.0" + } + }, + "cli-width": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", + "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=", + "dev": true + }, + "cliui": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", + "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "dev": true, + "requires": { + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" + }, + "dependencies": { + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + } + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "optional": true + }, + "commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", + "dev": true + }, + "compare-func": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/compare-func/-/compare-func-1.3.2.tgz", + "integrity": "sha1-md0LpFfh+bxyKxLAjsM+6rMfpkg=", + "dev": true, + "requires": { + "array-ify": "^1.0.0", + "dot-prop": "^3.0.0" + } + }, + "compare-versions": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-3.6.0.tgz", + "integrity": "sha512-W6Af2Iw1z4CB7q4uU4hv646dW9GQuBM+YpC0UvUCWSD8w90SJjp+ujJuXaEMtAXBtSqGfMPuFOVn4/+FlaqfBA==", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "contains-path": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/contains-path/-/contains-path-0.1.0.tgz", + "integrity": "sha1-/ozxhP9mcLa67wGp1IYaXL7EEgo=", + "dev": true + }, + "conventional-changelog": { + "version": "3.1.18", + "resolved": "https://registry.npmjs.org/conventional-changelog/-/conventional-changelog-3.1.18.tgz", + "integrity": "sha512-aN6a3rjgV8qwAJj3sC/Lme2kvswWO7fFSGQc32gREcwIOsaiqBaO6f2p0NomFaPDnTqZ+mMZFLL3hlzvEnZ0mQ==", + "dev": true, + "requires": { + "conventional-changelog-angular": "^5.0.6", + "conventional-changelog-atom": "^2.0.3", + "conventional-changelog-codemirror": "^2.0.3", + "conventional-changelog-conventionalcommits": "^4.2.3", + "conventional-changelog-core": "^4.1.4", + "conventional-changelog-ember": "^2.0.4", + "conventional-changelog-eslint": "^3.0.4", + "conventional-changelog-express": "^2.0.1", + "conventional-changelog-jquery": "^3.0.6", + "conventional-changelog-jshint": "^2.0.3", + "conventional-changelog-preset-loader": "^2.3.0" + }, + "dependencies": { + "conventional-changelog-angular": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-5.0.6.tgz", + "integrity": "sha512-QDEmLa+7qdhVIv8sFZfVxU1VSyVvnXPsxq8Vam49mKUcO1Z8VTLEJk9uI21uiJUsnmm0I4Hrsdc9TgkOQo9WSA==", + "dev": true, + "requires": { + "compare-func": "^1.3.1", + "q": "^1.5.1" + } + } + } + }, + "conventional-changelog-angular": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-1.6.6.tgz", + "integrity": "sha512-suQnFSqCxRwyBxY68pYTsFkG0taIdinHLNEAX5ivtw8bCRnIgnpvcHmlR/yjUyZIrNPYAoXlY1WiEKWgSE4BNg==", + "dev": true, + "requires": { + "compare-func": "^1.3.1", + "q": "^1.5.1" + } + }, + "conventional-changelog-atom": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/conventional-changelog-atom/-/conventional-changelog-atom-2.0.3.tgz", + "integrity": "sha512-szZe2ut97qNO6vCCMkm1I/tWu6ol4Rr8a9Lx0y/VlpDnpY0PNp+oGpFgU55lplhx+I3Lro9Iv4/gRj0knfgjzg==", + "dev": true, + "requires": { + "q": "^1.5.1" + } + }, + "conventional-changelog-cli": { + "version": "2.0.31", + "resolved": "https://registry.npmjs.org/conventional-changelog-cli/-/conventional-changelog-cli-2.0.31.tgz", + "integrity": "sha512-nMINylKAamBLM3OmD7/44d9TPZ3V58IDTXoGC/QtXxve+1Sj37BQTzIEW3TNaviZ2ZV/b5Dqg0eSk4DNP5fBdA==", + "dev": true, + "requires": { + "add-stream": "^1.0.0", + "conventional-changelog": "^3.1.18", + "lodash": "^4.17.15", + "meow": "^5.0.0", + "tempfile": "^3.0.0" + } + }, + "conventional-changelog-codemirror": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/conventional-changelog-codemirror/-/conventional-changelog-codemirror-2.0.3.tgz", + "integrity": "sha512-t2afackdgFV2yBdHhWPqrKbpaQeVnz2hSJKdWqjasPo5EpIB6TBL0er3cOP1mnGQmuzk9JSvimNSuqjWGDtU5Q==", + "dev": true, + "requires": { + "q": "^1.5.1" + } + }, + "conventional-changelog-conventionalcommits": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/conventional-changelog-conventionalcommits/-/conventional-changelog-conventionalcommits-4.2.3.tgz", + "integrity": "sha512-atGa+R4vvEhb8N/8v3IoW59gCBJeeFiX6uIbPu876ENAmkMwsenyn0R21kdDHJFLQdy6zW4J6b4xN8KI3b9oww==", + "dev": true, + "requires": { + "compare-func": "^1.3.1", + "lodash": "^4.17.15", + "q": "^1.5.1" + } + }, + "conventional-changelog-core": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/conventional-changelog-core/-/conventional-changelog-core-4.1.4.tgz", + "integrity": "sha512-LO58ZbEpp1Ul+y/vOI8rJRsWkovsYkCFbOCVgi6UnVfU8WC0F8K8VQQwaBZWWUpb6JvEiN4GBR5baRP2txZ+Vg==", + "dev": true, + "requires": { + "add-stream": "^1.0.0", + "conventional-changelog-writer": "^4.0.11", + "conventional-commits-parser": "^3.0.8", + "dateformat": "^3.0.0", + "get-pkg-repo": "^1.0.0", + "git-raw-commits": "2.0.0", + "git-remote-origin-url": "^2.0.0", + "git-semver-tags": "^3.0.1", + "lodash": "^4.17.15", + "normalize-package-data": "^2.3.5", + "q": "^1.5.1", + "read-pkg": "^3.0.0", + "read-pkg-up": "^3.0.0", + "through2": "^3.0.0" + }, + "dependencies": { + "git-raw-commits": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/git-raw-commits/-/git-raw-commits-2.0.0.tgz", + "integrity": "sha512-w4jFEJFgKXMQJ0H0ikBk2S+4KP2VEjhCvLCNqbNRQC8BgGWgLKNCO7a9K9LI+TVT7Gfoloje502sEnctibffgg==", + "dev": true, + "requires": { + "dargs": "^4.0.1", + "lodash.template": "^4.0.2", + "meow": "^4.0.0", + "split2": "^2.0.0", + "through2": "^2.0.0" + }, + "dependencies": { + "through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "requires": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + } + } + }, + "meow": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/meow/-/meow-4.0.1.tgz", + "integrity": "sha512-xcSBHD5Z86zaOc+781KrupuHAzeGXSLtiAOmBsiLDiPSaYSB6hdew2ng9EBAnZ62jagG9MHAOdxpDi/lWBFJ/A==", + "dev": true, + "requires": { + "camelcase-keys": "^4.0.0", + "decamelize-keys": "^1.0.0", + "loud-rejection": "^1.0.0", + "minimist": "^1.1.3", + "minimist-options": "^3.0.1", + "normalize-package-data": "^2.3.4", + "read-pkg-up": "^3.0.0", + "redent": "^2.0.0", + "trim-newlines": "^2.0.0" + } + }, + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "dev": true + } + } + }, + "conventional-changelog-ember": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/conventional-changelog-ember/-/conventional-changelog-ember-2.0.4.tgz", + "integrity": "sha512-q1u73sO9uCnxN4TSw8xu6MRU8Y1h9kpwtcdJuNRwu/LSKI1IE/iuNSH5eQ6aLlQ3HTyrIpTfUuVybW4W0F17rA==", + "dev": true, + "requires": { + "q": "^1.5.1" + } + }, + "conventional-changelog-eslint": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/conventional-changelog-eslint/-/conventional-changelog-eslint-3.0.4.tgz", + "integrity": "sha512-CPwTUENzhLGl3auunrJxiIEWncAGaby7gOFCdj2gslIuOFJ0KPJVOUhRz4Da/I53sdo/7UncUJkiLg94jEsjxg==", + "dev": true, + "requires": { + "q": "^1.5.1" + } + }, + "conventional-changelog-express": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/conventional-changelog-express/-/conventional-changelog-express-2.0.1.tgz", + "integrity": "sha512-G6uCuCaQhLxdb4eEfAIHpcfcJ2+ao3hJkbLrw/jSK/eROeNfnxCJasaWdDAfFkxsbpzvQT4W01iSynU3OoPLIw==", + "dev": true, + "requires": { + "q": "^1.5.1" + } + }, + "conventional-changelog-jquery": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/conventional-changelog-jquery/-/conventional-changelog-jquery-3.0.6.tgz", + "integrity": "sha512-gHAABCXUNA/HjnZEm+vxAfFPJkgtrZvCDIlCKfdPVXtCIo/Q0lN5VKpx8aR5p8KdVRQFF3OuTlvv5kv6iPuRqA==", + "dev": true, + "requires": { + "q": "^1.5.1" + } + }, + "conventional-changelog-jshint": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/conventional-changelog-jshint/-/conventional-changelog-jshint-2.0.3.tgz", + "integrity": "sha512-Pc2PnMPcez634ckzr4EOWviwRSpZcURaK7bjyD9oK6N5fsC/a+3G7LW5m/JpcHPhA9ZxsfIbm7uqZ3ZDGsQ/sw==", + "dev": true, + "requires": { + "compare-func": "^1.3.1", + "q": "^1.5.1" + } + }, + "conventional-changelog-preset-loader": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/conventional-changelog-preset-loader/-/conventional-changelog-preset-loader-2.3.0.tgz", + "integrity": "sha512-/rHb32J2EJnEXeK4NpDgMaAVTFZS3o1ExmjKMtYVgIC4MQn0vkNSbYpdGRotkfGGRWiqk3Ri3FBkiZGbAfIfOQ==", + "dev": true + }, + "conventional-changelog-writer": { + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/conventional-changelog-writer/-/conventional-changelog-writer-4.0.11.tgz", + "integrity": "sha512-g81GQOR392I+57Cw3IyP1f+f42ME6aEkbR+L7v1FBBWolB0xkjKTeCWVguzRrp6UiT1O6gBpJbEy2eq7AnV1rw==", + "dev": true, + "requires": { + "compare-func": "^1.3.1", + "conventional-commits-filter": "^2.0.2", + "dateformat": "^3.0.0", + "handlebars": "^4.4.0", + "json-stringify-safe": "^5.0.1", + "lodash": "^4.17.15", + "meow": "^5.0.0", + "semver": "^6.0.0", + "split": "^1.0.0", + "through2": "^3.0.0" + } + }, + "conventional-commits-filter": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/conventional-commits-filter/-/conventional-commits-filter-2.0.2.tgz", + "integrity": "sha512-WpGKsMeXfs21m1zIw4s9H5sys2+9JccTzpN6toXtxhpw2VNF2JUXwIakthKBy+LN4DvJm+TzWhxOMWOs1OFCFQ==", + "dev": true, + "requires": { + "lodash.ismatch": "^4.4.0", + "modify-values": "^1.0.0" + } + }, + "conventional-commits-parser": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-3.0.8.tgz", + "integrity": "sha512-YcBSGkZbYp7d+Cr3NWUeXbPDFUN6g3SaSIzOybi8bjHL5IJ5225OSCxJJ4LgziyEJ7AaJtE9L2/EU6H7Nt/DDQ==", + "dev": true, + "requires": { + "JSONStream": "^1.0.4", + "is-text-path": "^1.0.1", + "lodash": "^4.17.15", + "meow": "^5.0.0", + "split2": "^2.0.0", + "through2": "^3.0.0", + "trim-off-newlines": "^1.0.0" + } + }, + "convert-source-map": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", + "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.1" + } + }, + "core-js": { + "version": "2.6.11", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.11.tgz", + "integrity": "sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg==", + "dev": true + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "dev": true + }, + "cosmiconfig": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.2.1.tgz", + "integrity": "sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA==", + "dev": true, + "requires": { + "import-fresh": "^2.0.0", + "is-directory": "^0.3.1", + "js-yaml": "^3.13.1", + "parse-json": "^4.0.0" + }, + "dependencies": { + "import-fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz", + "integrity": "sha1-2BNVwVYS04bGH53dOSLUMEgipUY=", + "dev": true, + "requires": { + "caller-path": "^2.0.0", + "resolve-from": "^3.0.0" + } + }, + "resolve-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", + "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=", + "dev": true + } + } + }, + "cross-env": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.2.tgz", + "integrity": "sha512-KZP/bMEOJEDCkDQAyRhu3RL2ZO/SUVrxQVI0G3YEQ+OLbRA3c6zgixe8Mq8a/z7+HKlNEjo8oiLUs8iRijY2Rw==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.1" + }, + "dependencies": { + "cross-spawn": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.1.tgz", + "integrity": "sha512-u7v4o84SwFpD32Z8IIcPZ6z1/ie24O6RU3RbtL5Y316l3KuHVPx9ItBgWQ6VlfAFnRnTtMUrsQ9MUUTuEZjogg==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } + } + }, + "currently-unhandled": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", + "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=", + "dev": true, + "requires": { + "array-find-index": "^1.0.1" + } + }, + "dargs": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/dargs/-/dargs-4.1.0.tgz", + "integrity": "sha1-A6nbtLXC8Tm/FK5T8LiipqhvThc=", + "dev": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "dateformat": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-3.0.3.tgz", + "integrity": "sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==", + "dev": true + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true + }, + "decamelize-keys": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.0.tgz", + "integrity": "sha1-0XGoeTMlKAfrPLYdwcFEXQeN8tk=", + "dev": true, + "requires": { + "decamelize": "^1.1.0", + "map-obj": "^1.0.0" + }, + "dependencies": { + "map-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", + "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=", + "dev": true + } + } + }, + "deep-eql": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", + "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", + "dev": true, + "requires": { + "type-detect": "^4.0.0" + } + }, + "deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", + "dev": true + }, + "default-require-extensions": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-3.0.0.tgz", + "integrity": "sha512-ek6DpXq/SCpvjhpFsLFRVtIxJCRw6fUR42lYMVZuUMK7n8eMz4Uh5clckdBjEpLhn/gEBZo7hDJnJcwdKLKQjg==", + "dev": true, + "requires": { + "strip-bom": "^4.0.0" + }, + "dependencies": { + "strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true + } + } + }, + "define-properties": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", + "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "dev": true, + "requires": { + "object-keys": "^1.0.12" + } + }, + "diff": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", + "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", + "dev": true + }, + "doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "dot-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-3.0.0.tgz", + "integrity": "sha1-G3CK8JSknJoOfbyteQq6U52sEXc=", + "dev": true, + "requires": { + "is-obj": "^1.0.0" + } + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "es-abstract": { + "version": "1.17.4", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.4.tgz", + "integrity": "sha512-Ae3um/gb8F0mui/jPL+QiqmglkUsaQf7FwBEHYIFkztkneosu9imhqHpBzQ3h1vit8t5iQ74t6PEVvphBZiuiQ==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.1.5", + "is-regex": "^1.0.5", + "object-inspect": "^1.7.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.0", + "string.prototype.trimleft": "^2.1.1", + "string.prototype.trimright": "^2.1.1" + } + }, + "es-to-primitive": { + "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", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "eslint": { + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.8.0.tgz", + "integrity": "sha512-K+Iayyo2LtyYhDSYwz5D5QdWw0hCacNzyq1Y821Xna2xSJj7cijoLLYmLxTQgcgZ9mC61nryMy9S7GRbYpI5Ig==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "ajv": "^6.10.0", + "chalk": "^2.1.0", + "cross-spawn": "^6.0.5", + "debug": "^4.0.1", + "doctrine": "^3.0.0", + "eslint-scope": "^5.0.0", + "eslint-utils": "^1.4.3", + "eslint-visitor-keys": "^1.1.0", + "espree": "^6.1.2", + "esquery": "^1.0.1", + "esutils": "^2.0.2", + "file-entry-cache": "^5.0.1", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^5.0.0", + "globals": "^12.1.0", + "ignore": "^4.0.6", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "inquirer": "^7.0.0", + "is-glob": "^4.0.0", + "js-yaml": "^3.13.1", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.3.0", + "lodash": "^4.17.14", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.1", + "natural-compare": "^1.4.0", + "optionator": "^0.8.3", + "progress": "^2.0.0", + "regexpp": "^2.0.1", + "semver": "^6.1.2", + "strip-ansi": "^5.2.0", + "strip-json-comments": "^3.0.1", + "table": "^5.2.3", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" + }, + "dependencies": { + "regexpp": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", + "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", + "dev": true + } + } + }, + "eslint-config-prettier": { + "version": "6.10.1", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-6.10.1.tgz", + "integrity": "sha512-svTy6zh1ecQojvpbJSgH3aei/Rt7C6i090l5f2WQ4aB05lYHeZIR1qL4wZyyILTbtmnbHP5Yn8MrsOJMGa8RkQ==", + "dev": true, + "requires": { + "get-stdin": "^6.0.0" + }, + "dependencies": { + "get-stdin": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-6.0.0.tgz", + "integrity": "sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g==", + "dev": true + } + } + }, + "eslint-import-resolver-node": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.3.tgz", + "integrity": "sha512-b8crLDo0M5RSe5YG8Pu2DYBj71tSB6OvXkfzwbJU2w7y8P4/yo0MyF8jU26IEuEuHF2K5/gcAJE3LhQGqBBbVg==", + "dev": true, + "requires": { + "debug": "^2.6.9", + "resolve": "^1.13.1" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "eslint-module-utils": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.5.2.tgz", + "integrity": "sha512-LGScZ/JSlqGKiT8OC+cYRxseMjyqt6QO54nl281CK93unD89ijSeRV6An8Ci/2nvWVKe8K/Tqdm75RQoIOCr+Q==", + "dev": true, + "requires": { + "debug": "^2.6.9", + "pkg-dir": "^2.0.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "eslint-plugin-import": { + "version": "2.20.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.20.1.tgz", + "integrity": "sha512-qQHgFOTjguR+LnYRoToeZWT62XM55MBVXObHM6SKFd1VzDcX/vqT1kAz8ssqigh5eMj8qXcRoXXGZpPP6RfdCw==", + "dev": true, + "requires": { + "array-includes": "^3.0.3", + "array.prototype.flat": "^1.2.1", + "contains-path": "^0.1.0", + "debug": "^2.6.9", + "doctrine": "1.5.0", + "eslint-import-resolver-node": "^0.3.2", + "eslint-module-utils": "^2.4.1", + "has": "^1.0.3", + "minimatch": "^3.0.4", + "object.values": "^1.1.0", + "read-pkg-up": "^2.0.0", + "resolve": "^1.12.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "doctrine": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-1.5.0.tgz", + "integrity": "sha1-N53Ocw9hZvds76TmcHoVmwLFpvo=", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "isarray": "^1.0.0" + } + }, + "load-json-file": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", + "integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^2.2.0", + "pify": "^2.0.0", + "strip-bom": "^3.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", + "dev": true, + "requires": { + "error-ex": "^1.2.0" + } + }, + "path-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz", + "integrity": "sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM=", + "dev": true, + "requires": { + "pify": "^2.0.0" + } + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + }, + "read-pkg": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", + "integrity": "sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg=", + "dev": true, + "requires": { + "load-json-file": "^2.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^2.0.0" + } + }, + "read-pkg-up": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-2.0.0.tgz", + "integrity": "sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4=", + "dev": true, + "requires": { + "find-up": "^2.0.0", + "read-pkg": "^2.0.0" + } + } + } + }, + "eslint-plugin-prettier": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-3.1.2.tgz", + "integrity": "sha512-GlolCC9y3XZfv3RQfwGew7NnuFDKsfI4lbvRK+PIIo23SFH+LemGs4cKwzAaRa+Mdb+lQO/STaIayno8T5sJJA==", + "dev": true, + "requires": { + "prettier-linter-helpers": "^1.0.0" + } + }, + "eslint-plugin-promise": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-4.2.1.tgz", + "integrity": "sha512-VoM09vT7bfA7D+upt+FjeBO5eHIJQBUWki1aPvB+vbNiHS3+oGIJGIeyBtKQTME6UPXXy3vV07OL1tHd3ANuDw==", + "dev": true + }, + "eslint-scope": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.0.0.tgz", + "integrity": "sha512-oYrhJW7S0bxAFDvWqzvMPRm6pcgcnWc4QnofCAqRTRfQC0JcwenzGglTtsLyIuuWFfkqDG9vz67cnttSd53djw==", + "dev": true, + "requires": { + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + } + }, + "eslint-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz", + "integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^1.1.0" + } + }, + "eslint-visitor-keys": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz", + "integrity": "sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A==", + "dev": true + }, + "esm": { + "version": "3.2.25", + "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz", + "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==", + "dev": true + }, + "espree": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-6.2.1.tgz", + "integrity": "sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw==", + "dev": true, + "requires": { + "acorn": "^7.1.1", + "acorn-jsx": "^5.2.0", + "eslint-visitor-keys": "^1.1.0" + } + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + }, + "esquery": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.1.0.tgz", + "integrity": "sha512-MxYW9xKmROWF672KqjO75sszsA8Mxhw06YFeS5VHlB98KDHbOSurm3ArsjO60Eaf3QmGMCP1yn+0JQkNLo/97Q==", + "dev": true, + "requires": { + "estraverse": "^4.0.0" + } + }, + "esrecurse": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz", + "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==", + "dev": true, + "requires": { + "estraverse": "^4.1.0" + } + }, + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true + }, + "external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dev": true, + "requires": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + } + }, + "fast-deep-equal": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz", + "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==", + "dev": true + }, + "fast-diff": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz", + "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==", + "dev": true + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, + "figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.5" + } + }, + "file-entry-cache": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz", + "integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==", + "dev": true, + "requires": { + "flat-cache": "^2.0.1" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "find-cache-dir": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.1.tgz", + "integrity": "sha512-t2GDMt3oGC/v+BMwzmllWDuJF/xcDtE5j/fCGbqDD7OLuJkj0cfh1YSA5VKPvwMeLFLNDBkwOKZ2X85jGLVftQ==", + "dev": true, + "requires": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + }, + "dependencies": { + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-limit": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.2.tgz", + "integrity": "sha512-WGR+xHecKTr7EbUEhyLSh5Dube9JtdiG78ufaeLxTgpudf/20KqyMioIUZJAezlTIi6evxuoUs9YXc11cU+yzQ==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "requires": { + "find-up": "^4.0.0" + } + } + } + }, + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "dev": true, + "requires": { + "locate-path": "^2.0.0" + } + }, + "find-versions": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/find-versions/-/find-versions-3.2.0.tgz", + "integrity": "sha512-P8WRou2S+oe222TOCHitLy8zj+SIsVJh52VP4lvXkaFVnOFFdoWv1H1Jjvel1aI6NCFOAaeAVm8qrI0odiLcww==", + "dev": true, + "requires": { + "semver-regex": "^2.0.0" + } + }, + "flat": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/flat/-/flat-4.1.0.tgz", + "integrity": "sha512-Px/TiLIznH7gEDlPXcUD4KnBusa6kR6ayRUVcnEAbreRIuhkqow/mun59BuRXwoYk7ZQOLW1ZM05ilIvK38hFw==", + "dev": true, + "requires": { + "is-buffer": "~2.0.3" + } + }, + "flat-cache": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", + "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==", + "dev": true, + "requires": { + "flatted": "^2.0.0", + "rimraf": "2.6.3", + "write": "1.0.3" + }, + "dependencies": { + "rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } + } + }, + "flatted": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.1.tgz", + "integrity": "sha512-a1hQMktqW9Nmqr5aktAux3JMNqaucxGcjtjWnZLHX7yyPCmlSV3M54nGYbqT8K+0GhF3NBgmJCc3ma+WOgX8Jg==", + "dev": true + }, + "foreground-child": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz", + "integrity": "sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.0", + "signal-exit": "^3.0.2" + }, + "dependencies": { + "cross-spawn": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.1.tgz", + "integrity": "sha512-u7v4o84SwFpD32Z8IIcPZ6z1/ie24O6RU3RbtL5Y316l3KuHVPx9ItBgWQ6VlfAFnRnTtMUrsQ9MUUTuEZjogg==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "fromentries": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fromentries/-/fromentries-1.2.0.tgz", + "integrity": "sha512-33X7H/wdfO99GdRLLgkjUrD4geAFdq/Uv0kl3HD4da6HDixd2GUg8Mw7dahLCV9r/EARkmtYBB6Tch4EEokFTQ==", + "dev": true + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "fsevents": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.2.tgz", + "integrity": "sha512-R4wDiBwZ0KzpgOWetKDug1FZcYhqYnUYKtfZYt4mD5SBz76q0KR4Q9o7GIPamsVPGmW3EYPPJ0dOOjvx32ldZA==", + "dev": true, + "optional": true + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", + "dev": true + }, + "gensync": { + "version": "1.0.0-beta.1", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.1.tgz", + "integrity": "sha512-r8EC6NO1sngH/zdD9fiRDLdcgnbayXah+mLgManTaIZJqEC1MZstmnox8KpnI2/fxQwrp5OpCOYWLp4rBl4Jcg==", + "dev": true + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true + }, + "get-func-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", + "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", + "dev": true + }, + "get-pkg-repo": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/get-pkg-repo/-/get-pkg-repo-1.4.0.tgz", + "integrity": "sha1-xztInAbYDMVTbCyFP54FIyBWly0=", + "dev": true, + "requires": { + "hosted-git-info": "^2.1.4", + "meow": "^3.3.0", + "normalize-package-data": "^2.3.0", + "parse-github-repo-url": "^1.3.0", + "through2": "^2.0.0" + }, + "dependencies": { + "camelcase": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", + "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=", + "dev": true + }, + "camelcase-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", + "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=", + "dev": true, + "requires": { + "camelcase": "^2.0.0", + "map-obj": "^1.0.0" + } + }, + "find-up": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", + "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", + "dev": true, + "requires": { + "path-exists": "^2.0.0", + "pinkie-promise": "^2.0.0" + } + }, + "get-stdin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", + "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=", + "dev": true + }, + "indent-string": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz", + "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=", + "dev": true, + "requires": { + "repeating": "^2.0.0" + } + }, + "load-json-file": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", + "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^2.2.0", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0", + "strip-bom": "^2.0.0" + } + }, + "map-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", + "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=", + "dev": true + }, + "meow": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz", + "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=", + "dev": true, + "requires": { + "camelcase-keys": "^2.0.0", + "decamelize": "^1.1.2", + "loud-rejection": "^1.0.0", + "map-obj": "^1.0.1", + "minimist": "^1.1.3", + "normalize-package-data": "^2.3.4", + "object-assign": "^4.0.1", + "read-pkg-up": "^1.0.1", + "redent": "^1.0.0", + "trim-newlines": "^1.0.0" + } + }, + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "dev": true + }, + "parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", + "dev": true, + "requires": { + "error-ex": "^1.2.0" + } + }, + "path-exists": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", + "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", + "dev": true, + "requires": { + "pinkie-promise": "^2.0.0" + } + }, + "path-type": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", + "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + } + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + }, + "read-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", + "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", + "dev": true, + "requires": { + "load-json-file": "^1.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^1.0.0" + } + }, + "read-pkg-up": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", + "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", + "dev": true, + "requires": { + "find-up": "^1.0.0", + "read-pkg": "^1.0.0" + } + }, + "redent": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz", + "integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=", + "dev": true, + "requires": { + "indent-string": "^2.1.0", + "strip-indent": "^1.0.1" + } + }, + "strip-bom": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", + "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", + "dev": true, + "requires": { + "is-utf8": "^0.2.0" + } + }, + "strip-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz", + "integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=", + "dev": true, + "requires": { + "get-stdin": "^4.0.1" + } + }, + "through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "requires": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, + "trim-newlines": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz", + "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=", + "dev": true + } + } + }, + "get-stdin": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-7.0.0.tgz", + "integrity": "sha512-zRKcywvrXlXsA0v0i9Io4KDRaAw7+a1ZpjRwl9Wox8PFlVCCHra7E9c4kqXCoCM9nR5tBkaTTZRBoCm60bFqTQ==", + "dev": true + }, + "git-raw-commits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/git-raw-commits/-/git-raw-commits-2.0.3.tgz", + "integrity": "sha512-SoSsFL5lnixVzctGEi2uykjA7B5I0AhO9x6kdzvGRHbxsa6JSEgrgy1esRKsfOKE1cgyOJ/KDR2Trxu157sb8w==", + "dev": true, + "requires": { + "dargs": "^4.0.1", + "lodash.template": "^4.0.2", + "meow": "^5.0.0", + "split2": "^2.0.0", + "through2": "^3.0.0" + } + }, + "git-remote-origin-url": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/git-remote-origin-url/-/git-remote-origin-url-2.0.0.tgz", + "integrity": "sha1-UoJlna4hBxRaERJhEq0yFuxfpl8=", + "dev": true, + "requires": { + "gitconfiglocal": "^1.0.0", + "pify": "^2.3.0" + }, + "dependencies": { + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + } + } + }, + "git-semver-tags": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/git-semver-tags/-/git-semver-tags-3.0.1.tgz", + "integrity": "sha512-Hzd1MOHXouITfCasrpVJbRDg9uvW7LfABk3GQmXYZByerBDrfrEMP9HXpNT7RxAbieiocP6u+xq20DkvjwxnCA==", + "dev": true, + "requires": { + "meow": "^5.0.0", + "semver": "^6.0.0" + } + }, + "gitconfiglocal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/gitconfiglocal/-/gitconfiglocal-1.0.0.tgz", + "integrity": "sha1-QdBF84UaXqiPA/JMocYXgRRGS5s=", + "dev": true, + "requires": { + "ini": "^1.3.2" + } + }, + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.0.tgz", + "integrity": "sha512-qjtRgnIVmOfnKUE3NJAQEdk+lKrxfw8t5ke7SXtfMTHcjsBfOfWXCQfdb30zfDoZQ2IRSIiidmjtbHZPZ++Ihw==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "global-dirs": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-0.1.1.tgz", + "integrity": "sha1-sxnA3UYH81PzvpzKTHL8FIxJ9EU=", + "dev": true, + "requires": { + "ini": "^1.3.4" + } + }, + "globals": { + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", + "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", + "dev": true, + "requires": { + "type-fest": "^0.8.1" + } + }, + "graceful-fs": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz", + "integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==", + "dev": true + }, + "growl": { + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", + "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", + "dev": true + }, + "handlebars": { + "version": "4.7.3", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.3.tgz", + "integrity": "sha512-SRGwSYuNfx8DwHD/6InAPzD6RgeruWLT+B8e8a7gGs8FWgHzlExpTFMEq2IA6QpAfOClpKHy6+8IqTjeBCu6Kg==", + "dev": true, + "requires": { + "neo-async": "^2.6.0", + "optimist": "^0.6.1", + "source-map": "^0.6.1", + "uglify-js": "^3.1.4" + } + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "has-symbols": { + "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 + }, + "hasha": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/hasha/-/hasha-5.2.0.tgz", + "integrity": "sha512-2W+jKdQbAdSIrggA8Q35Br8qKadTrqCTC8+XZvBWepKDK6m9XkX6Iz1a2yh2KP01kzAR/dpuMeUnocoLYDcskw==", + "dev": true, + "requires": { + "is-stream": "^2.0.0", + "type-fest": "^0.8.0" + }, + "dependencies": { + "is-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", + "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==", + "dev": true + } + } + }, + "he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true + }, + "hosted-git-info": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz", + "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==", + "dev": true + }, + "html-escaper": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.0.tgz", + "integrity": "sha512-a4u9BeERWGu/S8JiWEAQcdrg9v4QArtP9keViQjGMdff20fBdd8waotXaNmODqBe6uZ3Nafi7K/ho4gCQHV3Ig==", + "dev": true + }, + "husky": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/husky/-/husky-4.2.3.tgz", + "integrity": "sha512-VxTsSTRwYveKXN4SaH1/FefRJYCtx+wx04sSVcOpD7N2zjoHxa+cEJ07Qg5NmV3HAK+IRKOyNVpi2YBIVccIfQ==", + "dev": true, + "requires": { + "chalk": "^3.0.0", + "ci-info": "^2.0.0", + "compare-versions": "^3.5.1", + "cosmiconfig": "^6.0.0", + "find-versions": "^3.2.0", + "opencollective-postinstall": "^2.0.2", + "pkg-dir": "^4.2.0", + "please-upgrade-node": "^3.2.0", + "slash": "^3.0.0", + "which-pm-runs": "^1.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "cosmiconfig": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz", + "integrity": "sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==", + "dev": true, + "requires": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.1.0", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.7.2" + } + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-limit": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.2.tgz", + "integrity": "sha512-WGR+xHecKTr7EbUEhyLSh5Dube9JtdiG78ufaeLxTgpudf/20KqyMioIUZJAezlTIi6evxuoUs9YXc11cU+yzQ==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "parse-json": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.0.0.tgz", + "integrity": "sha512-OOY5b7PAEFV0E2Fir1KOkxchnZNCdowAJgQ5NuxjpBKTRP3pQhwkrkxqQjeoKJ+fO7bCpmIZaogI4eZGDMEGOw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1", + "lines-and-columns": "^1.1.6" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true + }, + "pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "requires": { + "find-up": "^4.0.0" + } + }, + "supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true + }, + "import-fresh": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.1.tgz", + "integrity": "sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "dependencies": { + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + } + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true + }, + "indent-string": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-3.2.0.tgz", + "integrity": "sha1-Sl/W0nzDMvN+VBmlBNu4NxBckok=", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "ini": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", + "dev": true + }, + "inquirer": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.1.0.tgz", + "integrity": "sha512-5fJMWEmikSYu0nv/flMc475MhGbB7TSPd/2IpFV4I4rMklboCH2rQjYY5kKiYGHqUF9gvaambupcJFFG9dvReg==", + "dev": true, + "requires": { + "ansi-escapes": "^4.2.1", + "chalk": "^3.0.0", + "cli-cursor": "^3.1.0", + "cli-width": "^2.0.0", + "external-editor": "^3.0.3", + "figures": "^3.0.0", + "lodash": "^4.17.15", + "mute-stream": "0.0.8", + "run-async": "^2.4.0", + "rxjs": "^6.5.3", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "through": "^2.3.6" + }, + "dependencies": { + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.0" + } + }, + "supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", + "dev": true + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-buffer": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.4.tgz", + "integrity": "sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A==", + "dev": true + }, + "is-callable": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.5.tgz", + "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==", + "dev": true + }, + "is-date-object": { + "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-directory": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz", + "integrity": "sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=", + "dev": true + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true + }, + "is-finite": { + "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": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "is-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-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=", + "dev": true + }, + "is-promise": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", + "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=", + "dev": true + }, + "is-regex": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz", + "integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, + "is-string": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.5.tgz", + "integrity": "sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ==", + "dev": true + }, + "is-symbol": { + "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.1" + } + }, + "is-text-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-text-path/-/is-text-path-1.0.1.tgz", + "integrity": "sha1-Thqg+1G/vLPpJogAE5cgLBd1tm4=", + "dev": true, + "requires": { + "text-extensions": "^1.0.0" + } + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", + "dev": true + }, + "is-utf8": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", + "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=", + "dev": true + }, + "is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "istanbul-lib-coverage": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz", + "integrity": "sha512-UiUIqxMgRDET6eR+o5HbfRYP1l0hqkWOs7vNxC/mggutCMUIhWMm8gAHb8tHlyfD3/l6rlgNA5cKdDzEAf6hEg==", + "dev": true + }, + "istanbul-lib-hook": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-3.0.0.tgz", + "integrity": "sha512-Pt/uge1Q9s+5VAZ+pCo16TYMWPBIl+oaNIjgLQxcX0itS6ueeaA+pEfThZpH8WxhFgCiEb8sAJY6MdUKgiIWaQ==", + "dev": true, + "requires": { + "append-transform": "^2.0.0" + } + }, + "istanbul-lib-instrument": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.1.tgz", + "integrity": "sha512-imIchxnodll7pvQBYOqUu88EufLCU56LMeFPZZM/fJZ1irYcYdqroaV+ACK1Ila8ls09iEYArp+nqyC6lW1Vfg==", + "dev": true, + "requires": { + "@babel/core": "^7.7.5", + "@babel/parser": "^7.7.5", + "@babel/template": "^7.7.4", + "@babel/traverse": "^7.7.4", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.0.0", + "semver": "^6.3.0" + } + }, + "istanbul-lib-processinfo": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-processinfo/-/istanbul-lib-processinfo-2.0.2.tgz", + "integrity": "sha512-kOwpa7z9hme+IBPZMzQ5vdQj8srYgAtaRqeI48NGmAQ+/5yKiHLV0QbYqQpxsdEF0+w14SoB8YbnHKcXE2KnYw==", + "dev": true, + "requires": { + "archy": "^1.0.0", + "cross-spawn": "^7.0.0", + "istanbul-lib-coverage": "^3.0.0-alpha.1", + "make-dir": "^3.0.0", + "p-map": "^3.0.0", + "rimraf": "^3.0.0", + "uuid": "^3.3.3" + }, + "dependencies": { + "cross-spawn": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.1.tgz", + "integrity": "sha512-u7v4o84SwFpD32Z8IIcPZ6z1/ie24O6RU3RbtL5Y316l3KuHVPx9ItBgWQ6VlfAFnRnTtMUrsQ9MUUTuEZjogg==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==", + "dev": true, + "requires": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^3.0.0", + "supports-color": "^7.1.0" + }, + "dependencies": { + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "istanbul-lib-source-maps": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.0.tgz", + "integrity": "sha512-c16LpFRkR8vQXyHZ5nLpY35JZtzj1PQY1iZmesUbf1FZHbIupcWfjgOXBY9YHkLEQ6puz1u4Dgj6qmU/DisrZg==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + } + }, + "istanbul-reports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.0.0.tgz", + "integrity": "sha512-2osTcC8zcOSUkImzN2EWQta3Vdi4WjjKw99P2yWx5mLnigAM0Rd5uYFn1cf2i/Ois45GkNjaoTqc5CxgMSX80A==", + "dev": true, + "requires": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + } + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "js-yaml": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", + "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true + }, + "json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "dev": true + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", + "dev": true + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", + "dev": true + }, + "json5": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.1.tgz", + "integrity": "sha512-l+3HXD0GEI3huGq1njuqtzYK8OYJyXMkOLtQ53pjWh89tvWS2h6l+1zMkYWqlb57+SiQodKZyvMEFb2X+KrFhQ==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + }, + "dependencies": { + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "dev": true + } + } + }, + "jsonparse": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", + "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=", + "dev": true + }, + "just-extend": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.1.0.tgz", + "integrity": "sha512-ApcjaOdVTJ7y4r08xI5wIqpvwS48Q0PBG4DJROcEkH1f8MdAiNFyFxz3xoL0LWAVwjrwPYZdVHHxhRHcx/uGLA==", + "dev": true + }, + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + } + }, + "lines-and-columns": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz", + "integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=", + "dev": true + }, + "load-json-file": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", + "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0", + "strip-bom": "^3.0.0" + } + }, + "locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "dev": true, + "requires": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + } + }, + "lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", + "dev": true + }, + "lodash._reinterpolate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz", + "integrity": "sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=", + "dev": true + }, + "lodash.flattendeep": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", + "integrity": "sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=", + "dev": true + }, + "lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=", + "dev": true + }, + "lodash.ismatch": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz", + "integrity": "sha1-dWy1FQyjum8RCFp4hJZF8Yj4Xzc=", + "dev": true + }, + "lodash.template": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-4.5.0.tgz", + "integrity": "sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A==", + "dev": true, + "requires": { + "lodash._reinterpolate": "^3.0.0", + "lodash.templatesettings": "^4.0.0" + } + }, + "lodash.templatesettings": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-4.2.0.tgz", + "integrity": "sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ==", + "dev": true, + "requires": { + "lodash._reinterpolate": "^3.0.0" + } + }, + "log-symbols": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-3.0.0.tgz", + "integrity": "sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ==", + "dev": true, + "requires": { + "chalk": "^2.4.2" + } + }, + "loud-rejection": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", + "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=", + "dev": true, + "requires": { + "currently-unhandled": "^0.4.1", + "signal-exit": "^3.0.0" + } + }, + "make-dir": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.0.2.tgz", + "integrity": "sha512-rYKABKutXa6vXTXhoV18cBE7PaewPXHe/Bdq4v+ZLMhxbWApkFFplT0LcbMW+6BbjnQXzZ/sAvSE/JdguApG5w==", + "dev": true, + "requires": { + "semver": "^6.0.0" + } + }, + "make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "map-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-2.0.0.tgz", + "integrity": "sha1-plzSkIepJZi4eRJXpSPgISIqwfk=", + "dev": true + }, + "meow": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-5.0.0.tgz", + "integrity": "sha512-CbTqYU17ABaLefO8vCU153ZZlprKYWDljcndKKDCFcYQITzWCXZAVk4QMFZPgvzrnUQ3uItnIE/LoUOwrT15Ig==", + "dev": true, + "requires": { + "camelcase-keys": "^4.0.0", + "decamelize-keys": "^1.0.0", + "loud-rejection": "^1.0.0", + "minimist-options": "^3.0.1", + "normalize-package-data": "^2.3.4", + "read-pkg-up": "^3.0.0", + "redent": "^2.0.0", + "trim-newlines": "^2.0.0", + "yargs-parser": "^10.0.0" + } + }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + }, + "minimist-options": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-3.0.2.tgz", + "integrity": "sha512-FyBrT/d0d4+uiZRbqznPXqw3IpZZG3gl3wKWiX784FycUKVwBt0uLBFkQrtE4tZOrgo78nZp2jnKz3L65T5LdQ==", + "dev": true, + "requires": { + "arrify": "^1.0.1", + "is-plain-obj": "^1.1.0" + } + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + } + }, + "mocha": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-7.1.1.tgz", + "integrity": "sha512-3qQsu3ijNS3GkWcccT5Zw0hf/rWvu1fTN9sPvEd81hlwsr30GX2GcDSSoBxo24IR8FelmrAydGC6/1J5QQP4WA==", + "dev": true, + "requires": { + "ansi-colors": "3.2.3", + "browser-stdout": "1.3.1", + "chokidar": "3.3.0", + "debug": "3.2.6", + "diff": "3.5.0", + "escape-string-regexp": "1.0.5", + "find-up": "3.0.0", + "glob": "7.1.3", + "growl": "1.10.5", + "he": "1.2.0", + "js-yaml": "3.13.1", + "log-symbols": "3.0.0", + "minimatch": "3.0.4", + "mkdirp": "0.5.3", + "ms": "2.1.1", + "node-environment-flags": "1.0.6", + "object.assign": "4.1.0", + "strip-json-comments": "2.0.1", + "supports-color": "6.0.0", + "which": "1.3.1", + "wide-align": "1.1.3", + "yargs": "13.3.2", + "yargs-parser": "13.1.2", + "yargs-unparser": "1.6.0" + }, + "dependencies": { + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "glob": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "dev": true + }, + "mkdirp": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.3.tgz", + "integrity": "sha512-P+2gwrFqx8lhew375MQHHeTlY8AuOJSrGf0R5ddkEndUkmwpgUob/vQuBD1V22/Cw1/lJr4x+EjllSezBThzBg==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + }, + "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==", + "dev": true + }, + "p-limit": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.2.tgz", + "integrity": "sha512-WGR+xHecKTr7EbUEhyLSh5Dube9JtdiG78ufaeLxTgpudf/20KqyMioIUZJAezlTIi6evxuoUs9YXc11cU+yzQ==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "dev": true + }, + "supports-color": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.0.0.tgz", + "integrity": "sha512-on9Kwidc1IUQo+bQdhi8+Tijpo0e1SS6RoGo2guUwn5vdaxw8RXOF9Vb2ws+ihWOmh4JnCJOvaziZWP1VABaLg==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "yargs-parser": { + "version": "13.1.2", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", + "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } + }, + "modify-values": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/modify-values/-/modify-values-1.0.1.tgz", + "integrity": "sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw==", + "dev": true + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "dev": true + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "dev": true + }, + "neo-async": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.1.tgz", + "integrity": "sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==", + "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 + }, + "nise": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/nise/-/nise-4.0.3.tgz", + "integrity": "sha512-EGlhjm7/4KvmmE6B/UFsKh7eHykRl9VH+au8dduHLCyWUO/hr7+N+WtTvDUwc9zHuM1IaIJs/0lQ6Ag1jDkQSg==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.7.0", + "@sinonjs/fake-timers": "^6.0.0", + "@sinonjs/text-encoding": "^0.7.1", + "just-extend": "^4.0.2", + "path-to-regexp": "^1.7.0" + } + }, + "node-environment-flags": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/node-environment-flags/-/node-environment-flags-1.0.6.tgz", + "integrity": "sha512-5Evy2epuL+6TM0lCQGpFIj6KwiEsGh1SrHUhTbNX+sLbBtjidPZFAnVK9y5yU1+h//RitLbRHTIMyxQPtxMdHw==", + "dev": true, + "requires": { + "object.getownpropertydescriptors": "^2.0.3", + "semver": "^5.7.0" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } + } + }, + "node-preload": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/node-preload/-/node-preload-0.2.1.tgz", + "integrity": "sha512-RM5oyBy45cLEoHqCeh+MNuFAxO0vTFBLskvQbOKnEE7YTTSN4tbN8QWDIPQ6L+WvKsB/qLEGpYe2ZZ9d4W9OIQ==", + "dev": true, + "requires": { + "process-on-spawn": "^1.0.0" + } + }, + "normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "requires": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } + } + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "dev": true + }, + "nyc": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/nyc/-/nyc-15.0.0.tgz", + "integrity": "sha512-qcLBlNCKMDVuKb7d1fpxjPR8sHeMVX0CHarXAVzrVWoFrigCkYR8xcrjfXSPi5HXM7EU78L6ywO7w1c5rZNCNg==", + "dev": true, + "requires": { + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "caching-transform": "^4.0.0", + "convert-source-map": "^1.7.0", + "decamelize": "^1.2.0", + "find-cache-dir": "^3.2.0", + "find-up": "^4.1.0", + "foreground-child": "^2.0.0", + "glob": "^7.1.6", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-hook": "^3.0.0", + "istanbul-lib-instrument": "^4.0.0", + "istanbul-lib-processinfo": "^2.0.2", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.0.0", + "js-yaml": "^3.13.1", + "make-dir": "^3.0.0", + "node-preload": "^0.2.0", + "p-map": "^3.0.0", + "process-on-spawn": "^1.0.0", + "resolve-from": "^5.0.0", + "rimraf": "^3.0.0", + "signal-exit": "^3.0.2", + "spawn-wrap": "^2.0.0", + "test-exclude": "^6.0.0", + "uuid": "^3.3.3", + "yargs": "^15.0.2" + }, + "dependencies": { + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, + "cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-limit": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.2.tgz", + "integrity": "sha512-WGR+xHecKTr7EbUEhyLSh5Dube9JtdiG78ufaeLxTgpudf/20KqyMioIUZJAezlTIi6evxuoUs9YXc11cU+yzQ==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.0" + } + }, + "wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "yargs": { + "version": "15.3.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.3.0.tgz", + "integrity": "sha512-g/QCnmjgOl1YJjGsnUg2SatC7NUYEiLXJqxNOQU9qSpjzGtGXda9b+OKccr1kLTy8BN9yqEyqfq5lxlwdc13TA==", + "dev": true, + "requires": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.0" + } + }, + "yargs-parser": { + "version": "18.1.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.0.tgz", + "integrity": "sha512-o/Jr6JBOv6Yx3pL+5naWSoIA2jJ+ZkMYQG/ie9qFbukBe4uzmBatlXFOiu/tNKRWEtyf+n5w7jc/O16ufqOTdQ==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "dev": true + }, + "object-inspect": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.7.0.tgz", + "integrity": "sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw==", + "dev": true + }, + "object-keys": { + "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.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", + "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.values": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.1.tgz", + "integrity": "sha512-WTa54g2K8iu0kmS/us18jEmdv1a4Wi//BZ/DTVYEcH0XhLM5NYdpDHja3gt57VrZLcNAO2WGA+KpWsDBaHt6eA==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0-next.1", + "function-bind": "^1.1.1", + "has": "^1.0.3" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "onetime": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.0.tgz", + "integrity": "sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q==", + "dev": true, + "requires": { + "mimic-fn": "^2.1.0" + } + }, + "opencollective-postinstall": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/opencollective-postinstall/-/opencollective-postinstall-2.0.2.tgz", + "integrity": "sha512-pVOEP16TrAO2/fjej1IdOyupJY8KDUM1CvsaScRbw6oddvpQoOfGk4ywha0HKKVAD6RkW4x6Q+tNBwhf3Bgpuw==", + "dev": true + }, + "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.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.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + } + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "dev": true + }, + "p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "dev": true, + "requires": { + "p-try": "^1.0.0" + } + }, + "p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "dev": true, + "requires": { + "p-limit": "^1.1.0" + } + }, + "p-map": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", + "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==", + "dev": true, + "requires": { + "aggregate-error": "^3.0.0" + } + }, + "p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", + "dev": true + }, + "package-hash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/package-hash/-/package-hash-4.0.0.tgz", + "integrity": "sha512-whdkPIooSu/bASggZ96BWVvZTRMOFxnyUG5PnTSGKoJE2gd5mbVNmR2Nj20QFzxYYgAXpoqC+AiXzl+UMRh7zQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.15", + "hasha": "^5.0.0", + "lodash.flattendeep": "^4.4.0", + "release-zalgo": "^1.0.0" + } + }, + "parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "requires": { + "callsites": "^3.0.0" + } + }, + "parse-github-repo-url": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/parse-github-repo-url/-/parse-github-repo-url-1.4.1.tgz", + "integrity": "sha1-nn2LslKmy2ukJZUGC3v23z28H1A=", + "dev": true + }, + "parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "dev": true, + "requires": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + } + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", + "dev": true + }, + "path-parse": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", + "dev": true + }, + "path-to-regexp": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", + "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "dev": true, + "requires": { + "isarray": "0.0.1" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + } + } + }, + "path-type": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", + "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", + "dev": true, + "requires": { + "pify": "^3.0.0" + } + }, + "pathval": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", + "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=", + "dev": true + }, + "picomatch": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.1.tgz", + "integrity": "sha512-ISBaA8xQNmwELC7eOjqFKMESB2VIqt4PPDD0nsS95b/9dZXvVKOlz9keMSnoGGKcOHXfTvDD6WMaRoSc9UuhRA==", + "dev": true + }, + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + }, + "pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", + "dev": true + }, + "pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "dev": true, + "requires": { + "pinkie": "^2.0.0" + } + }, + "pkg-dir": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz", + "integrity": "sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=", + "dev": true, + "requires": { + "find-up": "^2.1.0" + } + }, + "please-upgrade-node": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz", + "integrity": "sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg==", + "dev": true, + "requires": { + "semver-compare": "^1.0.0" + } + }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "dev": true + }, + "prettier": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.19.1.tgz", + "integrity": "sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==", + "dev": true + }, + "prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "requires": { + "fast-diff": "^1.1.2" + } + }, + "process-nextick-args": { + "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 + }, + "process-on-spawn": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/process-on-spawn/-/process-on-spawn-1.0.0.tgz", + "integrity": "sha512-1WsPDsUSMmZH5LeMLegqkPDrsGgsWwk1Exipy2hvB0o/F0ASzbpIctSCcZIK1ykJvtTJULEH+20WOFjMvGnCTg==", + "dev": true, + "requires": { + "fromentries": "^1.2.0" + } + }, + "progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true + }, + "q": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", + "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=", + "dev": true + }, + "quick-lru": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-1.1.0.tgz", + "integrity": "sha1-Q2CxfGETatOAeDl/8RQW4Ybc+7g=", + "dev": true + }, + "read-pkg": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", + "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", + "dev": true, + "requires": { + "load-json-file": "^4.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^3.0.0" + } + }, + "read-pkg-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-3.0.0.tgz", + "integrity": "sha1-PtSWaF26D4/hGNBpHcUfSh/5bwc=", + "dev": true, + "requires": { + "find-up": "^2.0.0", + "read-pkg": "^3.0.0" + } + }, + "readable-stream": { + "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", + "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" + } + }, + "readdirp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.2.0.tgz", + "integrity": "sha512-crk4Qu3pmXwgxdSgGhgA/eXiJAPQiX4GMOZZMXnqKxHX7TaoL+3gQVo/WeuAiogr07DpnfjIMpXXa+PAIvwPGQ==", + "dev": true, + "requires": { + "picomatch": "^2.0.4" + } + }, + "redent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-2.0.0.tgz", + "integrity": "sha1-wbIAe0LVfrE4kHmzyDM2OdXhzKo=", + "dev": true, + "requires": { + "indent-string": "^3.0.0", + "strip-indent": "^2.0.0" + } + }, + "regenerator-runtime": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", + "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==", + "dev": true + }, + "regexpp": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.0.0.tgz", + "integrity": "sha512-Z+hNr7RAVWxznLPuA7DIh8UNX1j9CDrUQxskw9IrBE1Dxue2lyXT+shqEIeLUjrokxIP8CMy1WkjgG3rTsd5/g==", + "dev": true + }, + "release-zalgo": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/release-zalgo/-/release-zalgo-1.0.0.tgz", + "integrity": "sha1-CXALflB0Mpc5Mw5TXFqQ+2eFFzA=", + "dev": true, + "requires": { + "es6-error": "^4.0.1" + } + }, + "repeating": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", + "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", + "dev": true, + "requires": { + "is-finite": "^1.0.0" + } + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "dev": true + }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + }, + "resolve": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.15.1.tgz", + "integrity": "sha512-84oo6ZTtoTUpjgNEr5SJyzQhzL72gaRodsSfyxC/AXRvwu0Yse9H8eF9IpGo7b8YetZhlI6v7ZQ6bKBFV/6S7w==", + "dev": true, + "requires": { + "path-parse": "^1.0.6" + } + }, + "resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true + }, + "resolve-global": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-global/-/resolve-global-1.0.0.tgz", + "integrity": "sha512-zFa12V4OLtT5XUX/Q4VLvTfBf+Ok0SPc1FNGM/z9ctUdiU618qwKpWnd0CHs3+RqROfyEg/DhuHbMWYqcgljEw==", + "dev": true, + "requires": { + "global-dirs": "^0.1.1" + } + }, + "restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "requires": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + } + }, + "rimraf": { + "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" + } + }, + "run-async": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.0.tgz", + "integrity": "sha512-xJTbh/d7Lm7SBhc1tNvTpeCHaEzoyxPrqNlvSdMfBTYwaY++UJFyXUOxAtsRUXjlqOfj8luNaR9vjCh4KeV+pg==", + "dev": true, + "requires": { + "is-promise": "^2.1.0" + } + }, + "rxjs": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.4.tgz", + "integrity": "sha512-naMQXcgEo3csAEGvw/NydRA0fuS2nDZJiw1YUWFKU7aPPAPGZEsD4Iimit96qwCieH6y614MCLYwdkrWx7z/7Q==", + "dev": true, + "requires": { + "tslib": "^1.9.0" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + }, + "semver-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha1-De4hahyUGrN+nvsXiPavxf9VN/w=", + "dev": true + }, + "semver-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/semver-regex/-/semver-regex-2.0.0.tgz", + "integrity": "sha512-mUdIBBvdn0PLOeP3TEkMH7HHeUP3GjsXCwKarjv/kGmUFOYg1VqEemKhoQpWMu6X2I8kHeuVdGibLGkVK+/5Qw==", + "dev": true + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "dev": true + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "dev": true, + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", + "dev": true + }, + "signal-exit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", + "dev": true + }, + "sinon": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-9.0.1.tgz", + "integrity": "sha512-iTTyiQo5T94jrOx7X7QLBZyucUJ2WvL9J13+96HMfm2CGoJYbIPqRfl6wgNcqmzk0DI28jeGx5bUTXizkrqBmg==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.7.0", + "@sinonjs/fake-timers": "^6.0.0", + "@sinonjs/formatio": "^5.0.1", + "@sinonjs/samsam": "^5.0.3", + "diff": "^4.0.2", + "nise": "^4.0.1", + "supports-color": "^7.1.0" + }, + "dependencies": { + "diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, + "slice-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", + "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.0", + "astral-regex": "^1.0.0", + "is-fullwidth-code-point": "^2.0.0" + }, + "dependencies": { + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + } + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "source-map-support": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.16.tgz", + "integrity": "sha512-efyLRJDr68D9hBBNIPWFjhpFzURh+KJykQwvMyW5UiZzYwoF6l4YMMDIJJEyFWxWCqfyxLzz6tSfUFR+kXXsVQ==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "spawn-wrap": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-2.0.0.tgz", + "integrity": "sha512-EeajNjfN9zMnULLwhZZQU3GWBoFNkbngTUPfaawT4RkMiviTxcX0qfhVbGey39mfctfDHkWtuecgQ8NJcyQWHg==", + "dev": true, + "requires": { + "foreground-child": "^2.0.0", + "is-windows": "^1.0.2", + "make-dir": "^3.0.0", + "rimraf": "^3.0.0", + "signal-exit": "^3.0.2", + "which": "^2.0.1" + }, + "dependencies": { + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "spdx-correct": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz", + "integrity": "sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q==", + "dev": true, + "requires": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-exceptions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz", + "integrity": "sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA==", + "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==", + "dev": true, + "requires": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-license-ids": { + "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": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz", + "integrity": "sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==", + "dev": true, + "requires": { + "through": "2" + } + }, + "split2": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-2.2.0.tgz", + "integrity": "sha512-RAb22TG39LhI31MbreBgIuKiIKhVsawfTgEGqKHTK87aG+ul/PB8Sqoi3I7kVdRWiCfrKxK3uo4/YUkpNvhPbw==", + "dev": true, + "requires": { + "through2": "^2.0.2" + }, + "dependencies": { + "through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "requires": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + } + } + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "string-width": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.0" + } + } + } + }, + "string.prototype.trimleft": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.1.tgz", + "integrity": "sha512-iu2AGd3PuP5Rp7x2kEZCrB2Nf41ehzh+goo8TV7z8/XDBbsvc6HQIlUl9RjkZ4oyrW1XM5UwlGl1oVEaDjg6Ag==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "function-bind": "^1.1.1" + } + }, + "string.prototype.trimright": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.1.tgz", + "integrity": "sha512-qFvWL3/+QIgZXVmJBfpHmxLB7xsUXz6HsUmP8+5dRaC3Q7oKUv9Vo6aMCRZC1smrtyECFsIT30PqBJ1gTjAs+g==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "function-bind": "^1.1.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + } + } + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true + }, + "strip-indent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-2.0.0.tgz", + "integrity": "sha1-XvjbKV0B5u1sv3qrlpmNeCJSe2g=", + "dev": true + }, + "strip-json-comments": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.0.1.tgz", + "integrity": "sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw==", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "table": { + "version": "5.4.6", + "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz", + "integrity": "sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==", + "dev": true, + "requires": { + "ajv": "^6.10.2", + "lodash": "^4.17.14", + "slice-ansi": "^2.1.0", + "string-width": "^3.0.0" + }, + "dependencies": { + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + } + } + }, + "temp-dir": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz", + "integrity": "sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==", + "dev": true + }, + "tempfile": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tempfile/-/tempfile-3.0.0.tgz", + "integrity": "sha512-uNFCg478XovRi85iD42egu+eSFUmmka750Jy7L5tfHI5hQKKtbPnxaSaXAbBqCDYrw3wx4tXjKwci4/QmsZJxw==", + "dev": true, + "requires": { + "temp-dir": "^2.0.0", + "uuid": "^3.3.2" + } + }, + "test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "requires": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + } + }, + "text-extensions": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/text-extensions/-/text-extensions-1.9.0.tgz", + "integrity": "sha512-wiBrwC1EhBelW12Zy26JeOUkQ5mRu+5o8rpsJk5+2t+Y5vE7e842qtZDQ2g1NpX/29HdyFeJ4nSIhI47ENSxlQ==", + "dev": true + }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "dev": true + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "dev": true + }, + "through2": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.1.tgz", + "integrity": "sha512-M96dvTalPT3YbYLaKaCuwu+j06D/8Jfib0o/PxbVt6Amhv3dUAtW6rTV1jPgJSBG83I/e04Y6xkVdVhSRhi0ww==", + "dev": true, + "requires": { + "readable-stream": "2 || 3" + } + }, + "tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "requires": { + "os-tmpdir": "~1.0.2" + } + }, + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", + "dev": true + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "trim-newlines": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-2.0.0.tgz", + "integrity": "sha1-tAPQuRvlDDMd/EuC7s6yLD3hbSA=", + "dev": true + }, + "trim-off-newlines": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/trim-off-newlines/-/trim-off-newlines-1.0.1.tgz", + "integrity": "sha1-n5up2e+odkw4dpi8v+sshI8RrbM=", + "dev": true + }, + "ts-node": { + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-8.8.1.tgz", + "integrity": "sha512-10DE9ONho06QORKAaCBpPiFCdW+tZJuY/84tyypGtl6r+/C7Asq0dhqbRZURuUlLQtZxxDvT8eoj8cGW0ha6Bg==", + "dev": true, + "requires": { + "arg": "^4.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "source-map-support": "^0.5.6", + "yn": "3.1.1" + }, + "dependencies": { + "diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true + } + } + }, + "tslib": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.11.1.tgz", + "integrity": "sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA==", + "dev": true + }, + "tsutils": { + "version": "3.17.1", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.17.1.tgz", + "integrity": "sha512-kzeQ5B8H3w60nFY2g8cJIuH7JDpsALXySGtwGJ0p2LSjLgay3NdIpqq5SoOBe46bKDW2iq25irHCr8wjomUS2g==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + } + }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2" + } + }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + }, + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true + }, + "typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "dev": true, + "requires": { + "is-typedarray": "^1.0.0" + } + }, + "typescript": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.8.3.tgz", + "integrity": "sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w==", + "dev": true + }, + "uglify-js": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.8.0.tgz", + "integrity": "sha512-ugNSTT8ierCsDHso2jkBHXYrU8Y5/fY2ZUprfrJUiD7YpuFvV4jODLFmb3h4btQjqr5Nh4TX4XtgDfCU1WdioQ==", + "dev": true, + "optional": true, + "requires": { + "commander": "~2.20.3", + "source-map": "~0.6.1" + } + }, + "uri-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", + "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true + }, + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "dev": true + }, + "v8-compile-cache": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.1.0.tgz", + "integrity": "sha512-usZBT3PW+LOjM25wbqIlZwPeJV+3OSz3M1k1Ws8snlW39dZyYL9lOGC5FgPVHfk0jKmjiDV8Z0mIbVQPiwFs7g==", + "dev": true + }, + "validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "requires": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", + "dev": true + }, + "which-pm-runs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/which-pm-runs/-/which-pm-runs-1.0.0.tgz", + "integrity": "sha1-Zws6+8VS4LVd9rd4DKdGFfI60cs=", + "dev": true + }, + "wide-align": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", + "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", + "dev": true, + "requires": { + "string-width": "^1.0.2 || 2" + }, + "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 + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, + "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=", + "dev": true + }, + "wrap-ansi": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", + "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" + }, + "dependencies": { + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + } + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "write": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/write/-/write-1.0.3.tgz", + "integrity": "sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==", + "dev": true, + "requires": { + "mkdirp": "^0.5.1" + } + }, + "write-file-atomic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "dev": true, + "requires": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, + "xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true + }, + "y18n": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", + "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", + "dev": true + }, + "yaml": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.8.2.tgz", + "integrity": "sha512-omakb0d7FjMo3R1D2EbTKVIk6dAVLRxFXdLZMEUToeAvuqgG/YuHMuQOZ5fgk+vQ8cx+cnGKwyg+8g8PNT0xQg==", + "dev": true, + "requires": { + "@babel/runtime": "^7.8.7" + } + }, + "yargs": { + "version": "13.3.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", + "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", + "dev": true, + "requires": { + "cliui": "^5.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^13.1.2" + }, + "dependencies": { + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "p-limit": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.2.tgz", + "integrity": "sha512-WGR+xHecKTr7EbUEhyLSh5Dube9JtdiG78ufaeLxTgpudf/20KqyMioIUZJAezlTIi6evxuoUs9YXc11cU+yzQ==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "yargs-parser": { + "version": "13.1.2", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", + "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } + }, + "yargs-parser": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-10.1.0.tgz", + "integrity": "sha512-VCIyR1wJoEBZUqk5PA+oOBF6ypbwh5aNB3I50guxAL/quggdfs4TtNHQrSazFA3fYZ+tEqfs0zIGlv0c/rgjbQ==", + "dev": true, + "requires": { + "camelcase": "^4.1.0" + } + }, + "yargs-unparser": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-1.6.0.tgz", + "integrity": "sha512-W9tKgmSn0DpSatfri0nx52Joq5hVXgeLiqR/5G0sZNDoLZFOr/xjBUDcShCOGNsBnEMNo1KAMBkTej1Hm62HTw==", + "dev": true, + "requires": { + "flat": "^4.1.0", + "lodash": "^4.17.15", + "yargs": "^13.3.0" + } + }, + "yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..ab4bb01 --- /dev/null +++ b/package.json @@ -0,0 +1,71 @@ +{ + "name": "@diplomatiq/resily", + "version": "1.0.0", + "description": "Resily is a TypeScript resilience and transient-fault-handling library that allows developers to express policies such as Retry, Fallback, Circuit Breaker, Timeout, Bulkhead Isolation, and Cache. Inspired by App-vNext/Polly.", + "main": "dist/main.js", + "module": "dist/main.js", + "types": "dist/main.d.ts", + "engines": { + "node": ">=10.0.0" + }, + "scripts": { + "build": "tsc", + "check-release-tag": "node --experimental-modules scripts/check-release-tag.mjs", + "clean": "rm -r ./dist/", + "lint": "eslint ./src/ ./test/ --ext .ts", + "prepublishOnly": "npm run check-release-tag && npm run lint && npm run build && npm run test", + "test": "cross-env-shell TS_NODE_PROJECT=tsconfig.test.json nyc --reporter=lcov --reporter=text mocha --require ts-node/register --require source-map-support/register --require esm --recursive test/specs/**/*.test.ts", + "version": "node --experimental-modules scripts/sync-sonar-version.mjs && conventional-changelog -p angular -i CHANGELOG.md -s && git add sonar-project.properties CHANGELOG.md" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/Diplomatiq/resily.git" + }, + "keywords": [ + "resilience", + "fault-handling", + "retry", + "circuit-breaker", + "timeout", + "bulkhead-isolation", + "fallback" + ], + "author": "Diplomatiq", + "license": "MIT", + "bugs": { + "url": "https://github.com/Diplomatiq/resily/issues" + }, + "homepage": "https://github.com/Diplomatiq/resily#readme", + "devDependencies": { + "@commitlint/cli": "^8.3.5", + "@diplomatiq/eslint-config-tslib": "^3.0.0", + "@types/chai": "^4.2.11", + "@types/mocha": "^7.0.2", + "@types/node": "^13.11.0", + "@types/sinon": "^9.0.0", + "chai": "^4.2.0", + "conventional-changelog-cli": "^2.0.31", + "cross-env": "^7.0.2", + "esm": "^3.2.25", + "husky": "^4.2.3", + "mocha": "^7.1.1", + "nyc": "^15.0.0", + "prettier": "^1.19.1", + "sinon": "^9.0.1", + "source-map-support": "^0.5.16", + "ts-node": "^8.8.1", + "typescript": "^3.8.3" + }, + "files": [ + "dist/**/*" + ], + "husky": { + "hooks": { + "commit-msg": "commitlint -E HUSKY_GIT_PARAMS", + "pre-push": "npm run lint && npm run build && npm run test" + } + }, + "dependencies": { + "@diplomatiq/crypto-random": "^2.2.0" + } +} diff --git a/scripts/check-release-tag.mjs b/scripts/check-release-tag.mjs new file mode 100644 index 0000000..e927bd7 --- /dev/null +++ b/scripts/check-release-tag.mjs @@ -0,0 +1,33 @@ +/** + * This script reads the version field from package.json + * and checks if the current git HEAD has a matching tag. + * + * E.g. if the current version is '1.0.0', the matching tag is 'v1.0.0'. + * + * Runs automatically on `npm publish`. + */ + +import { spawnSync } from 'child_process'; +import { readFileSync } from 'fs'; +import { dirname, join } from 'path'; +import { fileURLToPath } from 'url'; + +const { stdout } = spawnSync('git', ['describe', '--tags', '--exact-match'], { encoding: 'utf-8' }); +const tag = stdout.trim(); + +const currentDir = dirname(fileURLToPath(import.meta.url)); +const packageJsonPath = join(currentDir, '..', 'package.json'); +const packageJsonContents = readFileSync(packageJsonPath, { encoding: 'utf-8' }); +const packageJson = JSON.parse(packageJsonContents); +const packageVersion = packageJson.version; + +if (tag !== `v${packageVersion}`) { + if (tag) { + console.log( + `Current tag (${tag}) does not match package version (${packageVersion}). Publishing from wrong branch?`, + ); + } else { + console.log(`Current commit has no tag. Publishing from wrong branch?`); + } + process.exit(1); +} diff --git a/scripts/sync-sonar-version.mjs b/scripts/sync-sonar-version.mjs new file mode 100644 index 0000000..d9314f9 --- /dev/null +++ b/scripts/sync-sonar-version.mjs @@ -0,0 +1,23 @@ +/** + * This script reads the version from package.json and writes it into + * the sonar-project.properties file's sonar.projectVersion field. + * + * Runs automatically on `npm version`. + */ + +import { readFileSync, writeFileSync } from 'fs'; +import { dirname, join } from 'path'; +import { fileURLToPath } from 'url'; + +const sonarVersionRegex = /(sonar\.projectVersion=)[^\s]*/; + +const currentDir = dirname(fileURLToPath(import.meta.url)); + +const packageJsonPath = join(currentDir, '..', 'package.json'); +const packageJsonContents = readFileSync(packageJsonPath, { encoding: 'utf-8' }); +const packageJson = JSON.parse(packageJsonContents); + +const sonarPropsPath = join(currentDir, '..', 'sonar-project.properties'); +let sonarProps = readFileSync(sonarPropsPath, { encoding: 'utf-8' }); +sonarProps = sonarProps.replace(sonarVersionRegex, `$1${packageJson.version}`); +writeFileSync(sonarPropsPath, sonarProps, { encoding: 'utf-8' }); diff --git a/sonar-project.properties b/sonar-project.properties new file mode 100644 index 0000000..dbf6bfa --- /dev/null +++ b/sonar-project.properties @@ -0,0 +1,18 @@ +# Standard properties +sonar.organization=diplomatiq +sonar.projectKey=Diplomatiq_resily +sonar.projectName=resily +sonar.projectVersion=1.0.0 + +sonar.sources=src +sonar.tests=test +sonar.sourceEncoding=UTF-8 + +# Meta-data for the project +sonar.links.homepage=https://github.com/Diplomatiq/resily +sonar.links.ci=https://github.com/Diplomatiq/resily/actions +sonar.links.scm=https://github.com/Diplomatiq/resily +sonar.links.issue=https://github.com/Diplomatiq/resily/issues + +# TypeScript-specific properties +sonar.typescript.lcov.reportPaths=coverage/lcov.info diff --git a/src/interfaces/randomGenerator.ts b/src/interfaces/randomGenerator.ts new file mode 100644 index 0000000..ccf58a8 --- /dev/null +++ b/src/interfaces/randomGenerator.ts @@ -0,0 +1,3 @@ +export interface RandomGenerator { + integer(min: number, max: number): Promise; +} diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..edb9507 --- /dev/null +++ b/src/main.ts @@ -0,0 +1,38 @@ +export { RandomGenerator } from './interfaces/randomGenerator'; +export { NopPolicy } from './policies/nopPolicy'; +export { Policy } from './policies/policy'; +export { PolicyCombination } from './policies/policyCombination'; +export { BulkheadCompartmentRejectedException } from './policies/proactive/bulkheadIsolationPolicy/bulkheadCompartmentRejectedException'; +export { BulkheadIsolationPolicy } from './policies/proactive/bulkheadIsolationPolicy/bulkheadIsolationPolicy'; +export { CachePolicy } from './policies/proactive/cachePolicy/cachePolicy'; +export { OnCacheGetFn } from './policies/proactive/cachePolicy/onCacheGetFn'; +export { OnCacheMissFn } from './policies/proactive/cachePolicy/onCacheMissFn'; +export { OnCachePutFn } from './policies/proactive/cachePolicy/onCachePutFn'; +export { TimeToLiveStrategy } from './policies/proactive/cachePolicy/timeToLiveStrategy'; +export { ProactivePolicy } from './policies/proactive/proactivePolicy'; +export { ExecutionException } from './policies/proactive/timeoutPolicy/executionException'; +export { OnTimeoutFn } from './policies/proactive/timeoutPolicy/onTimeoutFn'; +export { TimeoutException } from './policies/proactive/timeoutPolicy/timeoutException'; +export { TimeoutPolicy } from './policies/proactive/timeoutPolicy/timeoutPolicy'; +export { BrokenCircuitException } from './policies/reactive/circuitBreakerPolicy/brokenCircuitException'; +export { CircuitBreakerPolicy } from './policies/reactive/circuitBreakerPolicy/circuitBreakerPolicy'; +export { CircuitState } from './policies/reactive/circuitBreakerPolicy/circuitState'; +export { IsolatedCircuitException } from './policies/reactive/circuitBreakerPolicy/isolatedCircuitException'; +export { OnAttemptingCloseFn } from './policies/reactive/circuitBreakerPolicy/onAttemptingCloseFn'; +export { OnCloseFn } from './policies/reactive/circuitBreakerPolicy/onCloseFn'; +export { OnIsolateFn } from './policies/reactive/circuitBreakerPolicy/onIsolateFn'; +export { OnOpenFn } from './policies/reactive/circuitBreakerPolicy/onOpenFn'; +export { FallbackChainExhaustedException } from './policies/reactive/fallbackPolicy/fallbackChainExhaustedException'; +export { FallbackChainLink } from './policies/reactive/fallbackPolicy/fallbackChainLink'; +export { FallbackPolicy } from './policies/reactive/fallbackPolicy/fallbackPolicy'; +export { OnFallbackFn } from './policies/reactive/fallbackPolicy/onFallbackFn'; +export { ReactivePolicy } from './policies/reactive/reactivePolicy'; +export { BackoffStrategy } from './policies/reactive/retryPolicy/backoffStrategy'; +export { BackoffStrategyFactory } from './policies/reactive/retryPolicy/backoffStrategyFactory'; +export { OnRetryFn } from './policies/reactive/retryPolicy/onRetryFn'; +export { RetryPolicy } from './policies/reactive/retryPolicy/retryPolicy'; +export { ExecutedFn } from './types/executedFn'; +export { OnFinallyFn } from './types/onFinallyFn'; +export { PolicyModificationNotAllowedException } from './types/policyModificationNotAllowedException'; +export { Predicate } from './types/predicate'; +export { SuccessDeferred } from './utils/successDeferred'; diff --git a/src/policies/nopPolicy.ts b/src/policies/nopPolicy.ts new file mode 100644 index 0000000..115078a --- /dev/null +++ b/src/policies/nopPolicy.ts @@ -0,0 +1,8 @@ +import { ExecutedFn } from '../types/executedFn'; +import { Policy } from './policy'; + +export class NopPolicy extends Policy { + protected async policyExecutorImpl(fn: ExecutedFn): Promise { + return fn(); + } +} diff --git a/src/policies/policy.ts b/src/policies/policy.ts new file mode 100644 index 0000000..14f0d9d --- /dev/null +++ b/src/policies/policy.ts @@ -0,0 +1,43 @@ +import { ExecutedFn } from '../types/executedFn'; +import { PolicyModificationNotAllowedException } from '../types/policyModificationNotAllowedException'; + +export abstract class Policy { + private executing = 0; + private wrappedPolicy?: Policy; + + public async execute(fn: ExecutedFn): Promise { + try { + this.executing++; + + return await this.policyExecutorImpl( + async (): Promise => { + if (this.wrappedPolicy !== undefined) { + return this.wrappedPolicy.execute(fn); + } + + return fn(); + }, + ); + } finally { + this.executing--; + } + } + + public isExecuting(): boolean { + return this.executing > 0; + } + + public wrap(policy: Policy): void { + this.throwForPolicyModificationIfExecuting(); + + this.wrappedPolicy = policy; + } + + protected throwForPolicyModificationIfExecuting(): void { + if (this.isExecuting()) { + throw new PolicyModificationNotAllowedException(); + } + } + + protected abstract async policyExecutorImpl(fn: ExecutedFn): Promise; +} diff --git a/src/policies/policyCombination.ts b/src/policies/policyCombination.ts new file mode 100644 index 0000000..909b1a0 --- /dev/null +++ b/src/policies/policyCombination.ts @@ -0,0 +1,14 @@ +import { Policy } from './policy'; + +export class PolicyCombination { + public static combine( + policies: [Policy, Policy, ...Array>], + ): Policy { + return policies.reduceRight( + (prev, next): Policy => { + next.wrap(prev); + return next; + }, + ); + } +} diff --git a/src/policies/proactive/bulkheadIsolationPolicy/bulkheadCompartmentRejectedException.ts b/src/policies/proactive/bulkheadIsolationPolicy/bulkheadCompartmentRejectedException.ts new file mode 100644 index 0000000..9c6a4f3 --- /dev/null +++ b/src/policies/proactive/bulkheadIsolationPolicy/bulkheadCompartmentRejectedException.ts @@ -0,0 +1 @@ +export class BulkheadCompartmentRejectedException extends Error {} diff --git a/src/policies/proactive/bulkheadIsolationPolicy/bulkheadIsolationPolicy.ts b/src/policies/proactive/bulkheadIsolationPolicy/bulkheadIsolationPolicy.ts new file mode 100644 index 0000000..5962faf --- /dev/null +++ b/src/policies/proactive/bulkheadIsolationPolicy/bulkheadIsolationPolicy.ts @@ -0,0 +1,75 @@ +import { SuccessDeferred } from '../../../utils/successDeferred'; +import { ProactivePolicy } from '../proactivePolicy'; +import { BulkheadCompartmentRejectedException } from './bulkheadCompartmentRejectedException'; + +export class BulkheadIsolationPolicy extends ProactivePolicy { + private bulkheadCompartmentSize = Number.POSITIVE_INFINITY; + private queueSize = 0; + + private bulkheadCompartmentUsage = 0; + private readonly queue: Array> = []; + + public maxConcurrency(bulkheadCompartmentSize: number): void { + if (!Number.isInteger(bulkheadCompartmentSize)) { + throw new Error('bulkheadCompartmentSize must be integer'); + } + + if (bulkheadCompartmentSize <= 0) { + throw new Error('bulkheadCompartmentSize must be greater than 0'); + } + + if (!Number.isSafeInteger(bulkheadCompartmentSize)) { + throw new Error('bulkheadCompartmentSize must be less than or equal to 2^53 - 1'); + } + + this.throwForPolicyModificationIfExecuting(); + + this.bulkheadCompartmentSize = bulkheadCompartmentSize; + } + + public maxQueuedActions(queueSize: number): void { + if (!Number.isInteger(queueSize)) { + throw new Error('queueSize must be integer'); + } + + if (queueSize < 0) { + throw new Error('queueSize must be greater than or equal to 0'); + } + + if (!Number.isSafeInteger(queueSize)) { + throw new Error('queueSize must be less than or equal to 2^53 - 1'); + } + + this.throwForPolicyModificationIfExecuting(); + + this.queueSize = queueSize; + } + + public getAvailableSlotsCount(): number { + return this.bulkheadCompartmentSize - this.bulkheadCompartmentUsage; + } + + public getAvailableQueuedActionsCount(): number { + return this.queueSize - this.queue.length; + } + + protected async policyExecutorImpl(fn: () => ResultType | Promise): Promise { + if (this.bulkheadCompartmentUsage >= this.bulkheadCompartmentSize) { + if (this.queue.length >= this.queueSize) { + throw new BulkheadCompartmentRejectedException(); + } + + const queuingDeferred = new SuccessDeferred(); + this.queue.push(queuingDeferred); + await queuingDeferred.promise; + } + + try { + this.bulkheadCompartmentUsage++; + return await fn(); + } finally { + this.bulkheadCompartmentUsage--; + this.queue.shift()?.resolve(); + } + } +} diff --git a/src/policies/proactive/cachePolicy/cachePolicy.ts b/src/policies/proactive/cachePolicy/cachePolicy.ts new file mode 100644 index 0000000..300420a --- /dev/null +++ b/src/policies/proactive/cachePolicy/cachePolicy.ts @@ -0,0 +1,124 @@ +import { ExecutedFn } from '../../../types/executedFn'; +import { ProactivePolicy } from '../proactivePolicy'; +import { OnCacheGetFn } from './onCacheGetFn'; +import { OnCacheMissFn } from './onCacheMissFn'; +import { OnCachePutFn } from './onCachePutFn'; +import { TimeToLiveStrategy } from './timeToLiveStrategy'; + +export class CachePolicy extends ProactivePolicy { + private timeToLiveStrategy: TimeToLiveStrategy = 'relative'; + private timeToLiveValue = 1000; + private readonly onCacheGetFns: OnCacheGetFn[] = []; + private readonly onCachePutFns: OnCachePutFn[] = []; + private readonly onCacheMissFns: OnCacheMissFn[] = []; + + private cache!: ResultType; + private validUntil = -1; + + public timeToLive(strategy: TimeToLiveStrategy, value: number): void { + if (!Number.isInteger(value)) { + throw new Error('value must be integer'); + } + + if (value <= 0) { + throw new Error('value must be greater than 0'); + } + + if (!Number.isSafeInteger(value)) { + throw new Error('value must be less than or equal to 2^53 - 1'); + } + + this.throwForPolicyModificationIfExecuting(); + + this.timeToLiveStrategy = strategy; + this.timeToLiveValue = value; + } + + public onCacheGet(fn: OnCacheGetFn): void { + this.throwForPolicyModificationIfExecuting(); + + this.onCacheGetFns.push(fn); + } + + public onCachePut(fn: OnCachePutFn): void { + this.throwForPolicyModificationIfExecuting(); + + this.onCachePutFns.push(fn); + } + + public onCacheMiss(fn: OnCacheMissFn): void { + this.throwForPolicyModificationIfExecuting(); + + this.onCacheMissFns.push(fn); + } + + public invalidate(): void { + this.resetValidity(true); + } + + protected async policyExecutorImpl(fn: ExecutedFn): Promise { + if (this.isInvalid()) { + for (const onCacheMissFn of this.onCacheMissFns) { + try { + await onCacheMissFn(); + } catch (onCacheMissError) { + // ignore + } + } + + await this.updateCache(fn); + + for (const onCachePutFn of this.onCachePutFns) { + try { + await onCachePutFn(); + } catch (onCachePutError) { + // ignore + } + } + } else { + for (const onCacheGetFn of this.onCacheGetFns) { + try { + await onCacheGetFn(); + } catch (onCacheGetError) { + // ignore + } + } + } + + return this.cache; + } + + private async updateCache(fn: ExecutedFn): Promise { + this.cache = await fn(); + this.resetValidity(false); + } + + private isInvalid(): boolean { + if (this.cache === undefined || Date.now() >= this.validUntil) { + return true; + } + + if (this.timeToLiveStrategy === 'sliding') { + this.resetValidity(false); + } + + return false; + } + + private resetValidity(invalidate: boolean): void { + if (invalidate) { + this.validUntil = -1; + } else { + switch (this.timeToLiveStrategy) { + case 'relative': + case 'sliding': + this.validUntil = Date.now() + this.timeToLiveValue; + break; + + case 'absolute': + this.validUntil = this.timeToLiveValue; + break; + } + } + } +} diff --git a/src/policies/proactive/cachePolicy/onCacheGetFn.ts b/src/policies/proactive/cachePolicy/onCacheGetFn.ts new file mode 100644 index 0000000..4e3f6c1 --- /dev/null +++ b/src/policies/proactive/cachePolicy/onCacheGetFn.ts @@ -0,0 +1 @@ +export type OnCacheGetFn = () => void | Promise; diff --git a/src/policies/proactive/cachePolicy/onCacheMissFn.ts b/src/policies/proactive/cachePolicy/onCacheMissFn.ts new file mode 100644 index 0000000..d36aaaa --- /dev/null +++ b/src/policies/proactive/cachePolicy/onCacheMissFn.ts @@ -0,0 +1 @@ +export type OnCacheMissFn = () => void | Promise; diff --git a/src/policies/proactive/cachePolicy/onCachePutFn.ts b/src/policies/proactive/cachePolicy/onCachePutFn.ts new file mode 100644 index 0000000..f164601 --- /dev/null +++ b/src/policies/proactive/cachePolicy/onCachePutFn.ts @@ -0,0 +1 @@ +export type OnCachePutFn = () => void | Promise; diff --git a/src/policies/proactive/cachePolicy/timeToLiveStrategy.ts b/src/policies/proactive/cachePolicy/timeToLiveStrategy.ts new file mode 100644 index 0000000..f375113 --- /dev/null +++ b/src/policies/proactive/cachePolicy/timeToLiveStrategy.ts @@ -0,0 +1 @@ +export type TimeToLiveStrategy = 'relative' | 'absolute' | 'sliding'; diff --git a/src/policies/proactive/proactivePolicy.ts b/src/policies/proactive/proactivePolicy.ts new file mode 100644 index 0000000..c63986b --- /dev/null +++ b/src/policies/proactive/proactivePolicy.ts @@ -0,0 +1,3 @@ +import { Policy } from '../policy'; + +export abstract class ProactivePolicy extends Policy {} diff --git a/src/policies/proactive/timeoutPolicy/executionException.ts b/src/policies/proactive/timeoutPolicy/executionException.ts new file mode 100644 index 0000000..3381e09 --- /dev/null +++ b/src/policies/proactive/timeoutPolicy/executionException.ts @@ -0,0 +1,5 @@ +export class ExecutionException extends Error { + public constructor(public innerException: unknown) { + super(); + } +} diff --git a/src/policies/proactive/timeoutPolicy/onTimeoutFn.ts b/src/policies/proactive/timeoutPolicy/onTimeoutFn.ts new file mode 100644 index 0000000..6314a64 --- /dev/null +++ b/src/policies/proactive/timeoutPolicy/onTimeoutFn.ts @@ -0,0 +1 @@ +export type OnTimeoutFn = (timedOutAfter: number) => void | Promise; diff --git a/src/policies/proactive/timeoutPolicy/timeoutException.ts b/src/policies/proactive/timeoutPolicy/timeoutException.ts new file mode 100644 index 0000000..989cca8 --- /dev/null +++ b/src/policies/proactive/timeoutPolicy/timeoutException.ts @@ -0,0 +1,5 @@ +export class TimeoutException extends Error { + public constructor(public readonly timedOutAfterMs: number) { + super(); + } +} diff --git a/src/policies/proactive/timeoutPolicy/timeoutPolicy.ts b/src/policies/proactive/timeoutPolicy/timeoutPolicy.ts new file mode 100644 index 0000000..832fe3f --- /dev/null +++ b/src/policies/proactive/timeoutPolicy/timeoutPolicy.ts @@ -0,0 +1,74 @@ +import { ProactivePolicy } from '../proactivePolicy'; +import { ExecutionException } from './executionException'; +import { OnTimeoutFn } from './onTimeoutFn'; +import { TimeoutException } from './timeoutException'; + +export class TimeoutPolicy extends ProactivePolicy { + private timeoutMs: number | undefined; + private readonly onTimeoutFns: OnTimeoutFn[] = []; + + public timeoutAfter(timeoutMs: number): void { + if (!Number.isInteger(timeoutMs)) { + throw new Error('timeoutMs must be integer'); + } + + if (timeoutMs <= 0) { + throw new Error('timeoutMs must be greater than 0'); + } + + if (!Number.isSafeInteger(timeoutMs)) { + throw new Error('timeoutMs must be less than or equal to 2^53 - 1'); + } + + this.throwForPolicyModificationIfExecuting(); + + this.timeoutMs = timeoutMs; + } + + public onTimeout(fn: OnTimeoutFn): void { + this.throwForPolicyModificationIfExecuting(); + + this.onTimeoutFns.push(fn); + } + + protected async policyExecutorImpl(fn: () => Promise): Promise { + const executionPromise = (async (): Promise => { + try { + return await fn(); + } catch (ex) { + throw new ExecutionException(ex); + } + })(); + + let timeoutId; + const timeoutPromise = new Promise((_, reject): void => { + if (this.timeoutMs !== undefined) { + const currentTimeoutMs = this.timeoutMs; + timeoutId = setTimeout((): void => { + reject(new TimeoutException(currentTimeoutMs)); + }, this.timeoutMs); + } + }); + + try { + return await Promise.race([executionPromise, timeoutPromise]); + } catch (ex) { + const typedEx: ExecutionException | TimeoutException = ex; + if (typedEx instanceof TimeoutException) { + for (const onTimeoutFn of this.onTimeoutFns) { + try { + await onTimeoutFn(typedEx.timedOutAfterMs); + } catch (onTimeoutError) { + // ignored + } + } + + throw typedEx; + } + + throw typedEx.innerException; + } finally { + clearTimeout(timeoutId); + } + } +} diff --git a/src/policies/reactive/circuitBreakerPolicy/brokenCircuitException.ts b/src/policies/reactive/circuitBreakerPolicy/brokenCircuitException.ts new file mode 100644 index 0000000..7ab52b5 --- /dev/null +++ b/src/policies/reactive/circuitBreakerPolicy/brokenCircuitException.ts @@ -0,0 +1 @@ +export class BrokenCircuitException extends Error {} diff --git a/src/policies/reactive/circuitBreakerPolicy/circuitBreakerPolicy.ts b/src/policies/reactive/circuitBreakerPolicy/circuitBreakerPolicy.ts new file mode 100644 index 0000000..0f3fbed --- /dev/null +++ b/src/policies/reactive/circuitBreakerPolicy/circuitBreakerPolicy.ts @@ -0,0 +1,233 @@ +import { ReactivePolicy } from '../reactivePolicy'; +import { BrokenCircuitException } from './brokenCircuitException'; +import { CircuitState } from './circuitState'; +import { IsolatedCircuitException } from './isolatedCircuitException'; +import { OnAttemptingCloseFn } from './onAttemptingCloseFn'; +import { OnCloseFn } from './onCloseFn'; +import { OnIsolateFn } from './onIsolateFn'; +import { OnOpenFn } from './onOpenFn'; + +export class CircuitBreakerPolicy extends ReactivePolicy { + private numberOfConsecutiveReactionsBeforeCircuitBreak = 1; + private durationOfCircuitBreakMs = 1000; + private readonly onOpenFns: OnOpenFn[] = []; + private readonly onCloseFns: OnCloseFn[] = []; + private readonly onAttemptingCloseFns: OnAttemptingCloseFn[] = []; + private readonly onIsolateFns: OnIsolateFn[] = []; + + private state: CircuitState = 'Closed'; + private lastStateTransition: number = Date.now(); + private consecutiveReactionCounter = 0; + + public breakAfter(numberOfConsecutiveReactionsBeforeCircuitBreak: number): void { + if (!Number.isInteger(numberOfConsecutiveReactionsBeforeCircuitBreak)) { + throw new Error('numberOfConsecutiveReactionsBeforeCircuitBreak must be integer'); + } + + if (numberOfConsecutiveReactionsBeforeCircuitBreak <= 0) { + throw new Error('numberOfConsecutiveReactionsBeforeCircuitBreak must be greater than 0'); + } + + if (!Number.isSafeInteger(numberOfConsecutiveReactionsBeforeCircuitBreak)) { + throw new Error('numberOfConsecutiveReactionsBeforeCircuitBreak must be less than or equal to 2^53 - 1'); + } + + this.throwForPolicyModificationIfExecuting(); + + this.numberOfConsecutiveReactionsBeforeCircuitBreak = numberOfConsecutiveReactionsBeforeCircuitBreak; + } + + public breakFor(durationOfCircuitBreakMs: number): void { + if (!Number.isInteger(durationOfCircuitBreakMs)) { + throw new Error('durationOfCircuitBreakMs must be integer'); + } + + if (durationOfCircuitBreakMs <= 0) { + throw new Error('durationOfCircuitBreakMs must be greater than 0'); + } + + if (!Number.isSafeInteger(durationOfCircuitBreakMs)) { + throw new Error('durationOfCircuitBreakMs must be less than or equal to 2^53 - 1'); + } + + this.throwForPolicyModificationIfExecuting(); + + this.durationOfCircuitBreakMs = durationOfCircuitBreakMs; + } + + public async isolate(): Promise { + await this.transitionState('Isolated'); + } + + public async reset(): Promise { + if (this.state !== 'Isolated') { + throw new Error('cannot reset if not in Isolated state'); + } + + await this.transitionState('Closed'); + } + + public getCircuitState(): CircuitState { + return this.state; + } + + public onClose(onCloseFn: OnCloseFn): void { + this.throwForPolicyModificationIfExecuting(); + + this.onCloseFns.push(onCloseFn); + } + + public onOpen(onOpenFn: OnOpenFn): void { + this.throwForPolicyModificationIfExecuting(); + + this.onOpenFns.push(onOpenFn); + } + + public onAttemptingClose(onAttemptingCloseFn: OnAttemptingCloseFn): void { + this.throwForPolicyModificationIfExecuting(); + + this.onAttemptingCloseFns.push(onAttemptingCloseFn); + } + + public onIsolate(onIsolateFn: OnIsolateFn): void { + this.throwForPolicyModificationIfExecuting(); + + this.onIsolateFns.push(onIsolateFn); + } + + protected async policyExecutorImpl(fn: () => ResultType | Promise): Promise { + await this.attemptClosingIfShould(); + + if (this.state === 'Open') { + throw new BrokenCircuitException(); + } + + if (this.state === 'Isolated') { + throw new IsolatedCircuitException(); + } + + try { + const result = await fn(); + + const isReactiveToResult = await this.isReactiveToResult(result); + if (!isReactiveToResult) { + this.consecutiveReactionCounter = 0; + + if (this.state === 'AttemptingClose') { + await this.transitionState('Closed'); + } + + return result; + } + + if (this.state === 'AttemptingClose') { + await this.transitionState('Open'); + } + + if (this.state === 'Closed') { + this.consecutiveReactionCounter++; + if (this.consecutiveReactionCounter >= this.numberOfConsecutiveReactionsBeforeCircuitBreak) { + await this.transitionState('Open'); + } + } + + return result; + } catch (ex) { + const isReactiveToException = await this.isReactiveToException(ex); + if (!isReactiveToException) { + this.consecutiveReactionCounter = 0; + + if (this.state === 'AttemptingClose') { + await this.transitionState('Closed'); + } + + throw ex; + } + + if (this.state === 'AttemptingClose') { + await this.transitionState('Open'); + } + + if (this.state === 'Closed') { + this.consecutiveReactionCounter++; + if (this.consecutiveReactionCounter >= this.numberOfConsecutiveReactionsBeforeCircuitBreak) { + await this.transitionState('Open'); + } + } + + throw ex; + } + } + + private async attemptClosingIfShould(): Promise { + if (this.state !== 'Open') { + return; + } + + const shouldAttemptClosing = Date.now() >= this.lastStateTransition + this.durationOfCircuitBreakMs; + if (shouldAttemptClosing) { + await this.transitionState('AttemptingClose'); + } + } + + private async transitionState(newState: CircuitState): Promise { + switch (newState) { + case 'Closed': { + this.validateTransition(new Set(['AttemptingClose', 'Isolated'])); + for (const onCloseFn of this.onCloseFns) { + try { + await onCloseFn(); + } catch (ex) { + // ignored + } + } + break; + } + + case 'Open': { + this.validateTransition(new Set(['Closed', 'AttemptingClose'])); + for (const onOpenFn of this.onOpenFns) { + try { + await onOpenFn(); + } catch (ex) { + // ignored + } + } + break; + } + + case 'AttemptingClose': { + this.validateTransition(new Set(['Open'])); + for (const onAttemptingCloseFn of this.onAttemptingCloseFns) { + try { + await onAttemptingCloseFn(); + } catch (ex) { + // ignored + } + } + break; + } + + case 'Isolated': { + this.validateTransition(new Set(['Closed', 'Open', 'AttemptingClose'])); + for (const onIsolateFn of this.onIsolateFns) { + try { + await onIsolateFn(); + } catch (ex) { + // ignored + } + } + break; + } + } + + this.state = newState; + this.lastStateTransition = Date.now(); + } + + private validateTransition(validFrom: Set): void { + if (!validFrom.has(this.state)) { + throw new Error('invalid transition'); + } + } +} diff --git a/src/policies/reactive/circuitBreakerPolicy/circuitState.ts b/src/policies/reactive/circuitBreakerPolicy/circuitState.ts new file mode 100644 index 0000000..da6ea2b --- /dev/null +++ b/src/policies/reactive/circuitBreakerPolicy/circuitState.ts @@ -0,0 +1 @@ +export type CircuitState = 'Closed' | 'Open' | 'AttemptingClose' | 'Isolated'; diff --git a/src/policies/reactive/circuitBreakerPolicy/isolatedCircuitException.ts b/src/policies/reactive/circuitBreakerPolicy/isolatedCircuitException.ts new file mode 100644 index 0000000..dbe6709 --- /dev/null +++ b/src/policies/reactive/circuitBreakerPolicy/isolatedCircuitException.ts @@ -0,0 +1,3 @@ +import { BrokenCircuitException } from './brokenCircuitException'; + +export class IsolatedCircuitException extends BrokenCircuitException {} diff --git a/src/policies/reactive/circuitBreakerPolicy/onAttemptingCloseFn.ts b/src/policies/reactive/circuitBreakerPolicy/onAttemptingCloseFn.ts new file mode 100644 index 0000000..accf0dc --- /dev/null +++ b/src/policies/reactive/circuitBreakerPolicy/onAttemptingCloseFn.ts @@ -0,0 +1 @@ +export type OnAttemptingCloseFn = () => void | Promise; diff --git a/src/policies/reactive/circuitBreakerPolicy/onCloseFn.ts b/src/policies/reactive/circuitBreakerPolicy/onCloseFn.ts new file mode 100644 index 0000000..3dcd2dc --- /dev/null +++ b/src/policies/reactive/circuitBreakerPolicy/onCloseFn.ts @@ -0,0 +1 @@ +export type OnCloseFn = () => void | Promise; diff --git a/src/policies/reactive/circuitBreakerPolicy/onIsolateFn.ts b/src/policies/reactive/circuitBreakerPolicy/onIsolateFn.ts new file mode 100644 index 0000000..c9be8ec --- /dev/null +++ b/src/policies/reactive/circuitBreakerPolicy/onIsolateFn.ts @@ -0,0 +1 @@ +export type OnIsolateFn = () => void | Promise; diff --git a/src/policies/reactive/circuitBreakerPolicy/onOpenFn.ts b/src/policies/reactive/circuitBreakerPolicy/onOpenFn.ts new file mode 100644 index 0000000..5fe8f97 --- /dev/null +++ b/src/policies/reactive/circuitBreakerPolicy/onOpenFn.ts @@ -0,0 +1 @@ +export type OnOpenFn = () => void | Promise; diff --git a/src/policies/reactive/fallbackPolicy/fallbackChainExhaustedException.ts b/src/policies/reactive/fallbackPolicy/fallbackChainExhaustedException.ts new file mode 100644 index 0000000..c245e28 --- /dev/null +++ b/src/policies/reactive/fallbackPolicy/fallbackChainExhaustedException.ts @@ -0,0 +1 @@ +export class FallbackChainExhaustedException extends Error {} diff --git a/src/policies/reactive/fallbackPolicy/fallbackChainLink.ts b/src/policies/reactive/fallbackPolicy/fallbackChainLink.ts new file mode 100644 index 0000000..ffd0819 --- /dev/null +++ b/src/policies/reactive/fallbackPolicy/fallbackChainLink.ts @@ -0,0 +1 @@ +export type FallbackChainLink = () => ResultType | Promise; diff --git a/src/policies/reactive/fallbackPolicy/fallbackPolicy.ts b/src/policies/reactive/fallbackPolicy/fallbackPolicy.ts new file mode 100644 index 0000000..0177db9 --- /dev/null +++ b/src/policies/reactive/fallbackPolicy/fallbackPolicy.ts @@ -0,0 +1,99 @@ +import { OnFinallyFn } from '../../../types/onFinallyFn'; +import { ReactivePolicy } from '../reactivePolicy'; +import { FallbackChainExhaustedException } from './fallbackChainExhaustedException'; +import { FallbackChainLink } from './fallbackChainLink'; +import { OnFallbackFn } from './onFallbackFn'; + +export class FallbackPolicy extends ReactivePolicy { + private readonly fallbackChain: Array> = []; + private readonly onFallbackFns: Array> = []; + private readonly onFinallyFns: OnFinallyFn[] = []; + + public fallback(fallbackChainLink: FallbackChainLink): void { + this.throwForPolicyModificationIfExecuting(); + + this.fallbackChain.push(fallbackChainLink); + } + + public onFallback(onFallbackFn: OnFallbackFn): void { + this.throwForPolicyModificationIfExecuting(); + + this.onFallbackFns.push(onFallbackFn); + } + + public onFinally(fn: OnFinallyFn): void { + this.throwForPolicyModificationIfExecuting(); + + this.onFinallyFns.push(fn); + } + + protected async policyExecutorImpl(fn: () => ResultType | Promise): Promise { + try { + const remainingFallbackChain = [...this.fallbackChain]; + let executor = fn; + + // eslint-disable-next-line no-constant-condition + while (true) { + try { + const result = await executor(); + + const shouldFallbackOnResult = await this.isReactiveToResult(result); + if (!shouldFallbackOnResult) { + return result; + } + + const nextExecutor = remainingFallbackChain.shift(); + if (nextExecutor === undefined) { + throw new FallbackChainExhaustedException(); + } + + executor = nextExecutor; + + for (const onFallbackFn of this.onFallbackFns) { + try { + await onFallbackFn(result, undefined); + } catch (onFallbackError) { + // ignored + } + } + + continue; + } catch (ex) { + if (ex instanceof FallbackChainExhaustedException) { + throw ex; + } + + const shouldFallbackOnException = await this.isReactiveToException(ex); + if (!shouldFallbackOnException) { + throw ex; + } + + const nextExecutor = remainingFallbackChain.shift(); + if (nextExecutor === undefined) { + throw new FallbackChainExhaustedException(); + } + + executor = nextExecutor; + + for (const onFallbackFn of this.onFallbackFns) { + try { + await onFallbackFn(undefined, ex); + } catch (onFallbackError) { + // ignored + } + } + + continue; + } + } + } finally { + for (const onFinallyFn of this.onFinallyFns) { + try { + await onFinallyFn(); + } catch (onFinallyError) { + // ignored + } + } + } + } +} diff --git a/src/policies/reactive/fallbackPolicy/onFallbackFn.ts b/src/policies/reactive/fallbackPolicy/onFallbackFn.ts new file mode 100644 index 0000000..cdd7487 --- /dev/null +++ b/src/policies/reactive/fallbackPolicy/onFallbackFn.ts @@ -0,0 +1,4 @@ +export type OnFallbackFn = ( + result: ResultType | undefined, + error: unknown | undefined, +) => void | Promise; diff --git a/src/policies/reactive/reactivePolicy.ts b/src/policies/reactive/reactivePolicy.ts new file mode 100644 index 0000000..48fb478 --- /dev/null +++ b/src/policies/reactive/reactivePolicy.ts @@ -0,0 +1,24 @@ +import { Predicate } from '../../types/predicate'; +import { PredicateChecker } from '../../utils/predicateChecker'; +import { Policy } from '../policy'; + +export abstract class ReactivePolicy extends Policy { + protected resultPredicates: Array> = []; + protected exceptionPredicates: Array> = []; + + public reactOnResult(resultPredicate: Predicate): void { + this.resultPredicates.push(resultPredicate); + } + + public reactOnException(exceptionPredicate: Predicate): void { + this.exceptionPredicates.push(exceptionPredicate); + } + + protected async isReactiveToResult(result: ResultType): Promise { + return PredicateChecker.some(result, this.resultPredicates); + } + + protected async isReactiveToException(exception: unknown): Promise { + return PredicateChecker.some(exception, this.exceptionPredicates); + } +} diff --git a/src/policies/reactive/retryPolicy/backoffStrategy.ts b/src/policies/reactive/retryPolicy/backoffStrategy.ts new file mode 100644 index 0000000..06a52a4 --- /dev/null +++ b/src/policies/reactive/retryPolicy/backoffStrategy.ts @@ -0,0 +1 @@ +export type BackoffStrategy = (currentRetryCount: number) => number | Promise; diff --git a/src/policies/reactive/retryPolicy/backoffStrategyFactory.ts b/src/policies/reactive/retryPolicy/backoffStrategyFactory.ts new file mode 100644 index 0000000..fb6e6b9 --- /dev/null +++ b/src/policies/reactive/retryPolicy/backoffStrategyFactory.ts @@ -0,0 +1,75 @@ +import { RandomGenerator as DefaultRandomGenerator } from '@diplomatiq/crypto-random'; +import { RandomGenerator } from '../../../interfaces/randomGenerator'; + +export class BackoffStrategyFactory { + public static constantBackoff(delayMs: number, fastFirst = false): (currentRetryCount: number) => number { + this.validateNumericArgument(delayMs, 'delayMs'); + + return fastFirst + ? (currentRetryCount: number): number => (currentRetryCount === 1 ? 0 : delayMs) + : (): number => delayMs; + } + + public static linearBackoff(delayMs: number, fastFirst = false): (currentRetryCount: number) => number { + this.validateNumericArgument(delayMs, 'delayMs'); + + return fastFirst + ? (currentRetryCount: number): number => delayMs * (currentRetryCount - 1) + : (currentRetryCount: number): number => delayMs * currentRetryCount; + } + + public static exponentialBackoff( + delayMs: number, + fastFirst = false, + base = 2, + ): (currentRetryCount: number) => number { + this.validateNumericArgument(delayMs, 'delayMs'); + this.validateNumericArgument(base, 'base'); + + return fastFirst + ? (currentRetryCount: number): number => + currentRetryCount === 1 ? 0 : delayMs * base ** (currentRetryCount - 2) + : (currentRetryCount: number): number => delayMs * base ** (currentRetryCount - 1); + } + + public static jitteredBackoff( + minDelayMs: number, + maxDelayMs: number, + fastFirst = false, + randomGenerator: RandomGenerator = new DefaultRandomGenerator(), + ): (currentRetryCount: number) => Promise { + this.validateNumericArgument(minDelayMs, 'minDelayMs'); + this.validateNumericArgument(maxDelayMs, 'maxDelayMs'); + + if (maxDelayMs <= minDelayMs) { + throw new Error('maxDelayMs must be greater than minDelayMs'); + } + + return fastFirst + ? async (currentRetryCount: number): Promise => { + if (currentRetryCount === 1) { + return 0; + } + const [ms] = await randomGenerator.integer(minDelayMs, maxDelayMs); + return ms; + } + : async (): Promise => { + const [ms] = await randomGenerator.integer(minDelayMs, maxDelayMs); + return ms; + }; + } + + private static validateNumericArgument(arg: number, name: string): void { + if (!Number.isInteger(arg)) { + throw new Error(`${name} must be integer`); + } + + if (arg <= 0) { + throw new Error(`${name} must be greater than 0`); + } + + if (!Number.isSafeInteger(arg)) { + throw new Error(`${name} must be less than or equal to 2^53 - 1`); + } + } +} diff --git a/src/policies/reactive/retryPolicy/onRetryFn.ts b/src/policies/reactive/retryPolicy/onRetryFn.ts new file mode 100644 index 0000000..e24e3cf --- /dev/null +++ b/src/policies/reactive/retryPolicy/onRetryFn.ts @@ -0,0 +1,5 @@ +export type OnRetryFn = ( + result: ResultType | undefined, + error: unknown | undefined, + currentRetryCount: number, +) => void | Promise; diff --git a/src/policies/reactive/retryPolicy/retryPolicy.ts b/src/policies/reactive/retryPolicy/retryPolicy.ts new file mode 100644 index 0000000..879d94b --- /dev/null +++ b/src/policies/reactive/retryPolicy/retryPolicy.ts @@ -0,0 +1,135 @@ +import { OnFinallyFn } from '../../../types/onFinallyFn'; +import { ReactivePolicy } from '../reactivePolicy'; +import { BackoffStrategy } from './backoffStrategy'; +import { OnRetryFn } from './onRetryFn'; + +export class RetryPolicy extends ReactivePolicy { + private totalRetryCount = 1; + private readonly onRetryFns: Array> = []; + private readonly onFinallyFns: OnFinallyFn[] = []; + + public retryCount(retryCount: number): void { + if (!Number.isInteger(retryCount)) { + throw new Error('retryCount must be integer'); + } + + if (retryCount <= 0) { + throw new Error('retryCount must be greater than 0'); + } + + if (!Number.isSafeInteger(retryCount)) { + throw new Error('retryCount must be less than or equal to 2^53 - 1'); + } + + this.throwForPolicyModificationIfExecuting(); + + this.totalRetryCount = retryCount; + } + + public retryForever(): void { + this.throwForPolicyModificationIfExecuting(); + + this.totalRetryCount = Number.POSITIVE_INFINITY; + } + + public onRetry(fn: OnRetryFn): void { + this.throwForPolicyModificationIfExecuting(); + + this.onRetryFns.push(fn); + } + + public waitBeforeRetry(strategy: BackoffStrategy): void { + this.throwForPolicyModificationIfExecuting(); + + this.backoffStrategy = strategy; + } + + public onFinally(fn: OnFinallyFn): void { + this.throwForPolicyModificationIfExecuting(); + + this.onFinallyFns.push(fn); + } + + protected async policyExecutorImpl(fn: () => ResultType | Promise): Promise { + try { + let currentRetryCount = 0; + + // eslint-disable-next-line no-constant-condition + while (true) { + try { + const result = await fn(); + + const shouldRetryOnResult = await this.isReactiveToResult(result); + if (!shouldRetryOnResult) { + return result; + } + + currentRetryCount++; + if (!this.hasRetryLeft(currentRetryCount)) { + return result; + } + + const waitFor = await this.backoffStrategy(currentRetryCount); + if (waitFor > 0) { + await this.waitFor(waitFor); + } + + for (const onRetryFn of this.onRetryFns) { + try { + await onRetryFn(result, undefined, currentRetryCount); + } catch (onRetryError) { + // ignored + } + } + + continue; + } catch (ex) { + const shouldRetryOnException = await this.isReactiveToException(ex); + if (!shouldRetryOnException) { + throw ex; + } + + currentRetryCount++; + if (!this.hasRetryLeft(currentRetryCount)) { + throw ex; + } + + const waitFor = await this.backoffStrategy(currentRetryCount); + if (waitFor > 0) { + await this.waitFor(waitFor); + } + + for (const onRetryFn of this.onRetryFns) { + try { + await onRetryFn(undefined, ex, currentRetryCount); + } catch (onRetryError) { + // ignored + } + } + + continue; + } + } + } finally { + for (const onFinallyFn of this.onFinallyFns) { + try { + await onFinallyFn(); + } catch (onFinallyError) { + // ignored + } + } + } + } + + private backoffStrategy: BackoffStrategy = (): number => 0; + + private hasRetryLeft(currentRetryCount: number): boolean { + return currentRetryCount <= this.totalRetryCount; + } + + private async waitFor(ms: number): Promise { + return new Promise((resolve): void => { + setTimeout(resolve, ms); + }); + } +} diff --git a/src/types/executedFn.ts b/src/types/executedFn.ts new file mode 100644 index 0000000..3dd664d --- /dev/null +++ b/src/types/executedFn.ts @@ -0,0 +1 @@ +export type ExecutedFn = () => ResultType | Promise; diff --git a/src/types/onFinallyFn.ts b/src/types/onFinallyFn.ts new file mode 100644 index 0000000..cc6a174 --- /dev/null +++ b/src/types/onFinallyFn.ts @@ -0,0 +1 @@ +export type OnFinallyFn = () => void | Promise; diff --git a/src/types/policyModificationNotAllowedException.ts b/src/types/policyModificationNotAllowedException.ts new file mode 100644 index 0000000..03c89eb --- /dev/null +++ b/src/types/policyModificationNotAllowedException.ts @@ -0,0 +1 @@ +export class PolicyModificationNotAllowedException extends Error {} diff --git a/src/types/predicate.ts b/src/types/predicate.ts new file mode 100644 index 0000000..17656f2 --- /dev/null +++ b/src/types/predicate.ts @@ -0,0 +1 @@ +export type Predicate = (subject: T) => boolean | Promise; diff --git a/src/utils/predicateChecker.ts b/src/utils/predicateChecker.ts new file mode 100644 index 0000000..84f918e --- /dev/null +++ b/src/utils/predicateChecker.ts @@ -0,0 +1,37 @@ +import { Predicate } from '../types/predicate'; + +export class PredicateChecker { + public static async single(subject: T, predicate: Predicate): Promise { + return predicate(subject); + } + + public static async some(subject: T, predicates: Array>): Promise { + if (predicates.length === 0) { + return false; + } + + for (const predicate of predicates) { + const positive = await predicate(subject); + if (positive) { + return true; + } + } + + return false; + } + + public static async every(subject: T, predicates: Array>): Promise { + if (predicates.length === 0) { + return false; + } + + for (const predicate of predicates) { + const positive = await predicate(subject); + if (!positive) { + return false; + } + } + + return true; + } +} diff --git a/src/utils/successDeferred.ts b/src/utils/successDeferred.ts new file mode 100644 index 0000000..7d528bc --- /dev/null +++ b/src/utils/successDeferred.ts @@ -0,0 +1,10 @@ +export class SuccessDeferred { + public resolve!: (value?: T | Promise) => void; + public promise: Promise; + + public constructor() { + this.promise = new Promise((resolve): void => { + this.resolve = resolve; + }); + } +} diff --git a/test/specs/backoffStrategyFactory.test.ts b/test/specs/backoffStrategyFactory.test.ts new file mode 100644 index 0000000..40dd134 --- /dev/null +++ b/test/specs/backoffStrategyFactory.test.ts @@ -0,0 +1,411 @@ +import { RandomGenerator } from '@diplomatiq/crypto-random'; +import { expect } from 'chai'; +import { BackoffStrategyFactory } from '../../src/policies/reactive/retryPolicy/backoffStrategyFactory'; +import { NodeJsEntropyProvider } from '../utils/nodeJsEntropyProvider'; +import { windowMock } from '../utils/windowMock'; + +describe('BackoffStrategyFactory', (): void => { + describe('constantBackoff', (): void => { + it('should produce a constant backoff strategy', (): void => { + const strategy = BackoffStrategyFactory.constantBackoff(100); + expect(strategy(1)).to.equal(100); + expect(strategy(2)).to.equal(100); + expect(strategy(3)).to.equal(100); + expect(strategy(4)).to.equal(100); + expect(strategy(5)).to.equal(100); + }); + + it('should produce a constant backoff strategy with an immediate first retry if set', (): void => { + const strategy = BackoffStrategyFactory.constantBackoff(100, true); + expect(strategy(1)).to.equal(0); + expect(strategy(2)).to.equal(100); + expect(strategy(3)).to.equal(100); + expect(strategy(4)).to.equal(100); + expect(strategy(5)).to.equal(100); + }); + + it('should throw error when setting delayMs to 0', (): void => { + try { + BackoffStrategyFactory.constantBackoff(0); + } catch (ex) { + expect((ex as Error).message).to.equal('delayMs must be greater than 0'); + } + }); + + it('should throw error when setting delayMs to <0', (): void => { + try { + BackoffStrategyFactory.constantBackoff(-1); + } catch (ex) { + expect((ex as Error).message).to.equal('delayMs must be greater than 0'); + } + }); + + it('should throw error when setting delayMs to a non-integer', (): void => { + try { + BackoffStrategyFactory.constantBackoff(0.1); + } catch (ex) { + expect((ex as Error).message).to.equal('delayMs must be integer'); + } + }); + + it('should throw error when setting delayMs to a non-safe integer', (): void => { + try { + BackoffStrategyFactory.constantBackoff(2 ** 53); + } catch (ex) { + expect((ex as Error).message).to.equal('delayMs must be less than or equal to 2^53 - 1'); + } + }); + }); + + describe('linearBackoff', (): void => { + it('should produce a linear backoff strategy', (): void => { + const strategy = BackoffStrategyFactory.linearBackoff(100); + expect(strategy(1)).to.equal(100); + expect(strategy(2)).to.equal(200); + expect(strategy(3)).to.equal(300); + expect(strategy(4)).to.equal(400); + expect(strategy(5)).to.equal(500); + }); + + it('should produce a linear backoff strategy with an immediate first retry if set', (): void => { + const strategy = BackoffStrategyFactory.linearBackoff(100, true); + expect(strategy(1)).to.equal(0); + expect(strategy(2)).to.equal(100); + expect(strategy(3)).to.equal(200); + expect(strategy(4)).to.equal(300); + expect(strategy(5)).to.equal(400); + }); + + it('should throw error when setting delayMs to 0', (): void => { + try { + BackoffStrategyFactory.linearBackoff(0); + } catch (ex) { + expect((ex as Error).message).to.equal('delayMs must be greater than 0'); + } + }); + + it('should throw error when setting delayMs to <0', (): void => { + try { + BackoffStrategyFactory.linearBackoff(-1); + } catch (ex) { + expect((ex as Error).message).to.equal('delayMs must be greater than 0'); + } + }); + + it('should throw error when setting delayMs to a non-integer', (): void => { + try { + BackoffStrategyFactory.linearBackoff(0.1); + } catch (ex) { + expect((ex as Error).message).to.equal('delayMs must be integer'); + } + }); + + it('should throw error when setting delayMs to a non-safe integer', (): void => { + try { + BackoffStrategyFactory.linearBackoff(2 ** 53); + } catch (ex) { + expect((ex as Error).message).to.equal('delayMs must be less than or equal to 2^53 - 1'); + } + }); + }); + + describe('exponentialBackoff', (): void => { + it('should produce an exponential backoff strategy', (): void => { + const strategy = BackoffStrategyFactory.exponentialBackoff(100); + expect(strategy(1)).to.equal(100); + expect(strategy(2)).to.equal(200); + expect(strategy(3)).to.equal(400); + expect(strategy(4)).to.equal(800); + expect(strategy(5)).to.equal(1600); + }); + + it('should produce an exponential backoff strategy with an immediate first retry if set', (): void => { + const strategy = BackoffStrategyFactory.exponentialBackoff(100, true); + expect(strategy(1)).to.equal(0); + expect(strategy(2)).to.equal(100); + expect(strategy(3)).to.equal(200); + expect(strategy(4)).to.equal(400); + expect(strategy(5)).to.equal(800); + }); + + it('should produce an exponential backoff strategy with a custom base if set', (): void => { + const strategy = BackoffStrategyFactory.exponentialBackoff(100, false, 3); + expect(strategy(1)).to.equal(100); + expect(strategy(2)).to.equal(300); + expect(strategy(3)).to.equal(900); + expect(strategy(4)).to.equal(2700); + expect(strategy(5)).to.equal(8100); + }); + + it('should produce an exponential backoff strategy with an immediate first retry and a custom base if set', (): void => { + const strategy = BackoffStrategyFactory.exponentialBackoff(100, true, 3); + expect(strategy(1)).to.equal(0); + expect(strategy(2)).to.equal(100); + expect(strategy(3)).to.equal(300); + expect(strategy(4)).to.equal(900); + expect(strategy(5)).to.equal(2700); + }); + + it('should throw error when setting delayMs to 0', (): void => { + try { + BackoffStrategyFactory.exponentialBackoff(0); + } catch (ex) { + expect((ex as Error).message).to.equal('delayMs must be greater than 0'); + } + }); + + it('should throw error when setting delayMs to <0', (): void => { + try { + BackoffStrategyFactory.exponentialBackoff(-1); + } catch (ex) { + expect((ex as Error).message).to.equal('delayMs must be greater than 0'); + } + }); + + it('should throw error when setting delayMs to a non-integer', (): void => { + try { + BackoffStrategyFactory.exponentialBackoff(0.1); + } catch (ex) { + expect((ex as Error).message).to.equal('delayMs must be integer'); + } + }); + + it('should throw error when setting delayMs to a non-safe integer', (): void => { + try { + BackoffStrategyFactory.exponentialBackoff(2 ** 53); + } catch (ex) { + expect((ex as Error).message).to.equal('delayMs must be less than or equal to 2^53 - 1'); + } + }); + + it('should throw error when setting base to 0', (): void => { + try { + BackoffStrategyFactory.exponentialBackoff(100, false, 0); + } catch (ex) { + expect((ex as Error).message).to.equal('base must be greater than 0'); + } + }); + + it('should throw error when setting base to <0', (): void => { + try { + BackoffStrategyFactory.exponentialBackoff(100, false, -1); + } catch (ex) { + expect((ex as Error).message).to.equal('base must be greater than 0'); + } + }); + + it('should throw error when setting base to a non-integer', (): void => { + try { + BackoffStrategyFactory.exponentialBackoff(100, false, 0.1); + } catch (ex) { + expect((ex as Error).message).to.equal('base must be integer'); + } + }); + + it('should throw error when setting base to a non-safe integer', (): void => { + try { + BackoffStrategyFactory.exponentialBackoff(100, false, 2 ** 53); + } catch (ex) { + expect((ex as Error).message).to.equal('base must be less than or equal to 2^53 - 1'); + } + }); + }); + + describe('jitteredBackoff', (): void => { + it('should produce a jittered backoff strategy', async (): Promise => { + const entropyProvider = new NodeJsEntropyProvider(); + const randomGenerator = new RandomGenerator(entropyProvider); + const strategy = BackoffStrategyFactory.jitteredBackoff(1, 100, false, randomGenerator); + + expect(await strategy(1)) + .to.be.at.least(0) + .and.to.be.at.most(100); + + expect(await strategy(2)) + .to.be.at.least(0) + .and.to.be.at.most(100); + + expect(await strategy(3)) + .to.be.at.least(0) + .and.to.be.at.most(100); + + expect(await strategy(4)) + .to.be.at.least(0) + .and.to.be.at.most(100); + + expect(await strategy(5)) + .to.be.at.least(0) + .and.to.be.at.most(100); + }); + + it('should produce a jittered backoff strategy with an immediate first retry if set', async (): Promise< + void + > => { + const entropyProvider = new NodeJsEntropyProvider(); + const randomGenerator = new RandomGenerator(entropyProvider); + const strategy = BackoffStrategyFactory.jitteredBackoff(1, 100, true, randomGenerator); + + expect(await strategy(1)).to.equal(0); + + expect(await strategy(2)) + .to.be.at.least(0) + .and.to.be.at.most(100); + + expect(await strategy(3)) + .to.be.at.least(0) + .and.to.be.at.most(100); + + expect(await strategy(4)) + .to.be.at.least(0) + .and.to.be.at.most(100); + + expect(await strategy(5)) + .to.be.at.least(0) + .and.to.be.at.most(100); + }); + + it('should work with the default random generator where window.crypto.getRandomValues() is available', async (): Promise< + void + > => { + // eslint-disable-next-line @typescript-eslint/ban-ts-ignore + // @ts-ignore + global.window = windowMock(); + + const strategy = BackoffStrategyFactory.jitteredBackoff(1, 100); + + expect(await strategy(1)) + .to.be.at.least(0) + .and.to.be.at.most(100); + + expect(await strategy(2)) + .to.be.at.least(0) + .and.to.be.at.most(100); + + expect(await strategy(3)) + .to.be.at.least(0) + .and.to.be.at.most(100); + + expect(await strategy(4)) + .to.be.at.least(0) + .and.to.be.at.most(100); + + expect(await strategy(5)) + .to.be.at.least(0) + .and.to.be.at.most(100); + + // eslint-disable-next-line @typescript-eslint/ban-ts-ignore + // @ts-ignore + global.window = undefined; + }); + + it('should throw error when setting minDelayMs to 0', (): void => { + const entropyProvider = new NodeJsEntropyProvider(); + const randomGenerator = new RandomGenerator(entropyProvider); + + try { + BackoffStrategyFactory.jitteredBackoff(0, 100, false, randomGenerator); + } catch (ex) { + expect((ex as Error).message).to.equal('minDelayMs must be greater than 0'); + } + }); + + it('should throw error when setting minDelayMs to <0', (): void => { + const entropyProvider = new NodeJsEntropyProvider(); + const randomGenerator = new RandomGenerator(entropyProvider); + + try { + BackoffStrategyFactory.jitteredBackoff(-1, 100, false, randomGenerator); + } catch (ex) { + expect((ex as Error).message).to.equal('minDelayMs must be greater than 0'); + } + }); + + it('should throw error when setting minDelayMs to a non-integer', (): void => { + const entropyProvider = new NodeJsEntropyProvider(); + const randomGenerator = new RandomGenerator(entropyProvider); + + try { + BackoffStrategyFactory.jitteredBackoff(0.1, 100, false, randomGenerator); + } catch (ex) { + expect((ex as Error).message).to.equal('minDelayMs must be integer'); + } + }); + + it('should throw error when setting minDelayMs to a non-safe integer', (): void => { + const entropyProvider = new NodeJsEntropyProvider(); + const randomGenerator = new RandomGenerator(entropyProvider); + + try { + BackoffStrategyFactory.jitteredBackoff(2 ** 53, 100, false, randomGenerator); + } catch (ex) { + expect((ex as Error).message).to.equal('minDelayMs must be less than or equal to 2^53 - 1'); + } + }); + + it('should throw error when setting maxDelayMs to 0', (): void => { + const entropyProvider = new NodeJsEntropyProvider(); + const randomGenerator = new RandomGenerator(entropyProvider); + + try { + BackoffStrategyFactory.jitteredBackoff(1, 0, false, randomGenerator); + } catch (ex) { + expect((ex as Error).message).to.equal('maxDelayMs must be greater than 0'); + } + }); + + it('should throw error when setting maxDelayMs to <0', (): void => { + const entropyProvider = new NodeJsEntropyProvider(); + const randomGenerator = new RandomGenerator(entropyProvider); + + try { + BackoffStrategyFactory.jitteredBackoff(1, -1, false, randomGenerator); + } catch (ex) { + expect((ex as Error).message).to.equal('maxDelayMs must be greater than 0'); + } + }); + + it('should throw error when setting maxDelayMs to a non-integer', (): void => { + const entropyProvider = new NodeJsEntropyProvider(); + const randomGenerator = new RandomGenerator(entropyProvider); + + try { + BackoffStrategyFactory.jitteredBackoff(1, 0.1, false, randomGenerator); + } catch (ex) { + expect((ex as Error).message).to.equal('maxDelayMs must be integer'); + } + }); + + it('should throw error when setting maxDelayMs to a non-safe integer', (): void => { + const entropyProvider = new NodeJsEntropyProvider(); + const randomGenerator = new RandomGenerator(entropyProvider); + + try { + BackoffStrategyFactory.jitteredBackoff(1, 2 ** 53, false, randomGenerator); + } catch (ex) { + expect((ex as Error).message).to.equal('maxDelayMs must be less than or equal to 2^53 - 1'); + } + }); + + it('should throw error when setting minDelayMs = maxDelayMs', (): void => { + const entropyProvider = new NodeJsEntropyProvider(); + const randomGenerator = new RandomGenerator(entropyProvider); + + try { + BackoffStrategyFactory.jitteredBackoff(1, 1, false, randomGenerator); + } catch (ex) { + expect((ex as Error).message).to.equal('maxDelayMs must be greater than minDelayMs'); + } + }); + + it('should throw error when setting minDelayMs > maxDelayMs', (): void => { + const entropyProvider = new NodeJsEntropyProvider(); + const randomGenerator = new RandomGenerator(entropyProvider); + + try { + BackoffStrategyFactory.jitteredBackoff(2, 1, false, randomGenerator); + } catch (ex) { + expect((ex as Error).message).to.equal('maxDelayMs must be greater than minDelayMs'); + } + }); + }); +}); diff --git a/test/specs/bulkheadIsolationPolicy.test.ts b/test/specs/bulkheadIsolationPolicy.test.ts new file mode 100644 index 0000000..c380d16 --- /dev/null +++ b/test/specs/bulkheadIsolationPolicy.test.ts @@ -0,0 +1,419 @@ +import { expect } from 'chai'; +import { BulkheadCompartmentRejectedException } from '../../src/policies/proactive/bulkheadIsolationPolicy/bulkheadCompartmentRejectedException'; +import { BulkheadIsolationPolicy } from '../../src/policies/proactive/bulkheadIsolationPolicy/bulkheadIsolationPolicy'; +import { PolicyModificationNotAllowedException } from '../../src/types/policyModificationNotAllowedException'; +import { SuccessDeferred } from '../../src/utils/successDeferred'; + +describe('BulkheadIsolationPolicy', (): void => { + it('should run the synchronous execution callback and return its result', async (): Promise => { + const policy = new BulkheadIsolationPolicy(); + const result = await policy.execute((): string => { + return 'Diplomatiq is cool.'; + }); + + expect(result).to.equal('Diplomatiq is cool.'); + }); + + it('should run the asynchronous execution callback and return its result', async (): Promise => { + const policy = new BulkheadIsolationPolicy(); + const result = await policy.execute( + // eslint-disable-next-line @typescript-eslint/require-await + async (): Promise => { + return 'Diplomatiq is cool.'; + }, + ); + + expect(result).to.equal('Diplomatiq is cool.'); + }); + + it('should run the synchronous execution callback and throw its exceptions', async (): Promise => { + const policy = new BulkheadIsolationPolicy(); + + try { + await policy.execute((): string => { + throw new Error('TestException'); + }); + expect.fail('did not throw'); + } catch (ex) { + expect((ex as Error).message).to.equal('TestException'); + } + }); + + it('should run the asynchronous execution callback and throw its exceptions', async (): Promise => { + const policy = new BulkheadIsolationPolicy(); + + try { + await policy.execute( + // eslint-disable-next-line @typescript-eslint/require-await + async (): Promise => { + throw new Error('TestException'); + }, + ); + expect.fail('did not throw'); + } catch (ex) { + expect((ex as Error).message).to.equal('TestException'); + } + }); + + it('should limit the concurrently running actions to bulkheadCompartmentSize', async (): Promise => { + const policy = new BulkheadIsolationPolicy(); + policy.maxConcurrency(1); + + expect(policy.getAvailableSlotsCount()).to.equal(1); + expect(policy.getAvailableQueuedActionsCount()).to.equal(0); + + policy.execute( + async (): Promise => { + await new Promise((): void => { + // will not resolve + }); + }, + ); + + expect(policy.getAvailableSlotsCount()).to.equal(0); + expect(policy.getAvailableQueuedActionsCount()).to.equal(0); + + try { + await policy.execute((): void => { + // empty + }); + } catch (ex) { + expect(ex instanceof BulkheadCompartmentRejectedException).to.be.true; + } + + expect(policy.getAvailableSlotsCount()).to.equal(0); + expect(policy.getAvailableQueuedActionsCount()).to.equal(0); + }); + + it('should queue up maximum queueSize actions on hold if already executing bulkheadCompartmentSize actions', (): void => { + const policy = new BulkheadIsolationPolicy(); + policy.maxConcurrency(1); + policy.maxQueuedActions(1); + + expect(policy.getAvailableSlotsCount()).to.equal(1); + expect(policy.getAvailableQueuedActionsCount()).to.equal(1); + + policy.execute( + async (): Promise => { + await new Promise((): void => { + // will not resolve + }); + }, + ); + + expect(policy.getAvailableSlotsCount()).to.equal(0); + expect(policy.getAvailableQueuedActionsCount()).to.equal(1); + + let executed = false; + policy.execute((): void => { + executed = true; + }); + + expect(executed).to.be.false; + + expect(policy.getAvailableSlotsCount()).to.equal(0); + expect(policy.getAvailableQueuedActionsCount()).to.equal(0); + }); + + it('should execute the first queued action if an executed action finishes', async (): Promise => { + const policy = new BulkheadIsolationPolicy(); + policy.maxConcurrency(1); + policy.maxQueuedActions(2); + + const deferred1 = new SuccessDeferred(); + let execute1Started = false; + let execute1Finished = false; + + const deferred2 = new SuccessDeferred(); + let execute2Started = false; + let execute2Finished = false; + + const deferred3 = new SuccessDeferred(); + let execute3Started = false; + let execute3Finished = false; + + expect(policy.getAvailableSlotsCount()).to.equal(1); + expect(policy.getAvailableQueuedActionsCount()).to.equal(2); + expect(execute1Started).to.be.false; + expect(execute1Finished).to.be.false; + expect(execute2Started).to.be.false; + expect(execute2Finished).to.be.false; + expect(execute3Started).to.be.false; + expect(execute3Finished).to.be.false; + + const executionPromise1 = policy.execute( + async (): Promise => { + execute1Started = true; + await deferred1.promise; + execute1Finished = true; + }, + ); + + expect(policy.getAvailableSlotsCount()).to.equal(0); + expect(policy.getAvailableQueuedActionsCount()).to.equal(2); + expect(execute1Started).to.be.true; + expect(execute1Finished).to.be.false; + expect(execute2Started).to.be.false; + expect(execute2Finished).to.be.false; + expect(execute3Started).to.be.false; + expect(execute3Finished).to.be.false; + + const executionPromise2 = policy.execute( + async (): Promise => { + execute2Started = true; + await deferred2.promise; + execute2Finished = true; + }, + ); + + expect(policy.getAvailableSlotsCount()).to.equal(0); + expect(policy.getAvailableQueuedActionsCount()).to.equal(1); + expect(execute1Started).to.be.true; + expect(execute1Finished).to.be.false; + expect(execute2Started).to.be.false; + expect(execute2Finished).to.be.false; + expect(execute3Started).to.be.false; + expect(execute3Finished).to.be.false; + + const executionPromise3 = policy.execute( + async (): Promise => { + execute3Started = true; + await deferred3.promise; + execute3Finished = true; + }, + ); + + expect(policy.getAvailableSlotsCount()).to.equal(0); + expect(policy.getAvailableQueuedActionsCount()).to.equal(0); + expect(execute1Started).to.be.true; + expect(execute1Finished).to.be.false; + expect(execute2Started).to.be.false; + expect(execute2Finished).to.be.false; + expect(execute3Started).to.be.false; + expect(execute3Finished).to.be.false; + + deferred1.resolve(); + await executionPromise1; + + expect(policy.getAvailableSlotsCount()).to.equal(0); + expect(policy.getAvailableQueuedActionsCount()).to.equal(1); + expect(execute1Started).to.be.true; + expect(execute1Finished).to.be.true; + expect(execute2Started).to.be.true; + expect(execute2Finished).to.be.false; + expect(execute3Started).to.be.false; + expect(execute3Finished).to.be.false; + + deferred2.resolve(); + await executionPromise2; + + expect(policy.getAvailableSlotsCount()).to.equal(0); + expect(policy.getAvailableQueuedActionsCount()).to.equal(2); + expect(execute1Started).to.be.true; + expect(execute1Finished).to.be.true; + expect(execute2Started).to.be.true; + expect(execute2Finished).to.be.true; + expect(execute3Started).to.be.true; + expect(execute3Finished).to.be.false; + + deferred3.resolve(); + await executionPromise3; + + expect(policy.getAvailableSlotsCount()).to.equal(1); + expect(policy.getAvailableQueuedActionsCount()).to.equal(2); + expect(execute1Started).to.be.true; + expect(execute1Finished).to.be.true; + expect(execute2Started).to.be.true; + expect(execute2Finished).to.be.true; + expect(execute3Started).to.be.true; + expect(execute3Finished).to.be.true; + }); + + it('should not allow to set maxConcurrency during execution', (): void => { + const policy = new BulkheadIsolationPolicy(); + policy.execute( + async (): Promise => { + await new Promise((): void => { + // will not resolve + }); + }, + ); + + try { + policy.maxConcurrency(1); + expect.fail('did not throw'); + } catch (ex) { + expect(ex instanceof PolicyModificationNotAllowedException).to.be.true; + } + }); + + it('should not allow to set maxQueuedActions during execution', (): void => { + const policy = new BulkheadIsolationPolicy(); + policy.execute( + async (): Promise => { + await new Promise((): void => { + // will not resolve + }); + }, + ); + + try { + policy.maxQueuedActions(1); + expect.fail('did not throw'); + } catch (ex) { + expect(ex instanceof PolicyModificationNotAllowedException).to.be.true; + } + }); + + it("should be properly mutex'd for running an instance multiple times simultaneously", async (): Promise => { + const policy = new BulkheadIsolationPolicy(); + + const attemptPolicyModification = (expectFailure: boolean): void => { + try { + policy.maxConcurrency(100); + if (expectFailure) { + expect.fail('did not throw'); + } + } catch (ex) { + expect(ex instanceof PolicyModificationNotAllowedException).to.be.true; + } + + try { + policy.maxQueuedActions(0); + if (expectFailure) { + expect.fail('did not throw'); + } + } catch (ex) { + expect(ex instanceof PolicyModificationNotAllowedException).to.be.true; + } + }; + + const executionResolverAddedDeferreds: Array<{ + resolverAddedPromise: Promise; + resolverAddedResolver: () => void; + }> = new Array(100).fill(undefined).map((): { + resolverAddedPromise: Promise; + resolverAddedResolver: () => void; + } => { + let resolverAddedResolver!: () => void; + const resolverAddedPromise = new Promise((resolve): void => { + resolverAddedResolver = resolve; + }); + + return { + resolverAddedPromise, + resolverAddedResolver, + }; + }); + + const executionResolvers: Array<() => void> = []; + + attemptPolicyModification(false); + + for (let i = 0; i < 100; i++) { + policy.execute( + // eslint-disable-next-line no-loop-func + async (): Promise => { + await new Promise((resolve): void => { + executionResolvers.push(resolve); + executionResolverAddedDeferreds[i].resolverAddedResolver(); + }); + }, + ); + + await executionResolverAddedDeferreds[i].resolverAddedPromise; + expect(executionResolvers.length).to.equal(i + 1); + + attemptPolicyModification(true); + } + + for (let i = 0; i < 100; i++) { + executionResolvers[i](); + attemptPolicyModification(true); + } + + attemptPolicyModification(false); + }); + + it('should throw error when setting maxConcurrency to 0', (): void => { + const policy = new BulkheadIsolationPolicy(); + + try { + policy.maxConcurrency(0); + expect.fail('did not throw'); + } catch (ex) { + expect((ex as Error).message).to.equal('bulkheadCompartmentSize must be greater than 0'); + } + }); + + it('should throw error when setting maxConcurrency to <0', (): void => { + const policy = new BulkheadIsolationPolicy(); + + try { + policy.maxConcurrency(-1); + expect.fail('did not throw'); + } catch (ex) { + expect((ex as Error).message).to.equal('bulkheadCompartmentSize must be greater than 0'); + } + }); + + it('should throw error when setting maxConcurrency to a non-integer', (): void => { + const policy = new BulkheadIsolationPolicy(); + + try { + policy.maxConcurrency(0.1); + expect.fail('did not throw'); + } catch (ex) { + expect((ex as Error).message).to.equal('bulkheadCompartmentSize must be integer'); + } + }); + + it('should throw error when setting maxConcurrency to a non-safe integer', (): void => { + const policy = new BulkheadIsolationPolicy(); + + try { + policy.maxConcurrency(2 ** 53); + expect.fail('did not throw'); + } catch (ex) { + expect((ex as Error).message).to.equal('bulkheadCompartmentSize must be less than or equal to 2^53 - 1'); + } + }); + + it('should not throw error when setting maxQueuedActions to 0', (): void => { + const policy = new BulkheadIsolationPolicy(); + policy.maxQueuedActions(0); + }); + + it('should throw error when setting maxQueuedActions to <0', (): void => { + const policy = new BulkheadIsolationPolicy(); + + try { + policy.maxQueuedActions(-1); + expect.fail('did not throw'); + } catch (ex) { + expect((ex as Error).message).to.equal('queueSize must be greater than or equal to 0'); + } + }); + + it('should throw error when setting maxQueuedActions to a non-integer', (): void => { + const policy = new BulkheadIsolationPolicy(); + + try { + policy.maxQueuedActions(0.1); + expect.fail('did not throw'); + } catch (ex) { + expect((ex as Error).message).to.equal('queueSize must be integer'); + } + }); + + it('should throw error when setting maxQueuedActions to a non-safe integer', (): void => { + const policy = new BulkheadIsolationPolicy(); + + try { + policy.maxQueuedActions(2 ** 53); + expect.fail('did not throw'); + } catch (ex) { + expect((ex as Error).message).to.equal('queueSize must be less than or equal to 2^53 - 1'); + } + }); +}); diff --git a/test/specs/cachePolicy.test.ts b/test/specs/cachePolicy.test.ts new file mode 100644 index 0000000..7c7011b --- /dev/null +++ b/test/specs/cachePolicy.test.ts @@ -0,0 +1,591 @@ +import { expect } from 'chai'; +import { SinonFakeTimers, useFakeTimers } from 'sinon'; +import { CachePolicy } from '../../src/policies/proactive/cachePolicy/cachePolicy'; +import { PolicyModificationNotAllowedException } from '../../src/types/policyModificationNotAllowedException'; + +describe('CachePolicy', (): void => { + let clock: SinonFakeTimers; + + beforeEach((): void => { + clock = useFakeTimers({ + now: Date.now(), + toFake: ['Date'], + shouldAdvanceTime: false, + }); + }); + + afterEach((): void => { + clock.restore(); + }); + + it('should run the synchronous execution callback and return its result', async (): Promise => { + const policy = new CachePolicy(); + const result = await policy.execute((): string => { + return 'Diplomatiq is cool.'; + }); + + expect(result).to.equal('Diplomatiq is cool.'); + }); + + it('should run the asynchronous execution callback and return its result', async (): Promise => { + const policy = new CachePolicy(); + const result = await policy.execute( + // eslint-disable-next-line @typescript-eslint/require-await + async (): Promise => { + return 'Diplomatiq is cool.'; + }, + ); + + expect(result).to.equal('Diplomatiq is cool.'); + }); + + it('should run the synchronous execution callback and throw its exceptions', async (): Promise => { + const policy = new CachePolicy(); + + try { + await policy.execute((): string => { + throw new Error('TestException'); + }); + expect.fail('did not throw'); + } catch (ex) { + expect((ex as Error).message).to.equal('TestException'); + } + }); + + it('should run the asynchronous execution callback and throw its exceptions', async (): Promise => { + const policy = new CachePolicy(); + + try { + await policy.execute( + // eslint-disable-next-line @typescript-eslint/require-await + async (): Promise => { + throw new Error('TestException'); + }, + ); + expect.fail('did not throw'); + } catch (ex) { + expect((ex as Error).message).to.equal('TestException'); + } + }); + + it("should hold the cache valid for Date.now() + 10000ms from each update if setting timeToLive('relative', 10000)", async (): Promise< + void + > => { + const policy = new CachePolicy(); + policy.timeToLive('relative', 10000); + + let result: string; + let executed = 0; + + const checkIntervalMs = 100; + + const executor = (returnValue: string): string => { + executed++; + return returnValue; + }; + + result = await policy.execute((): string => executor('Diplomatiq is cool.')); + expect(result).to.equal('Diplomatiq is cool.'); + expect(executed).to.equal(1); + + for (let elapsedMs = 0; elapsedMs < 10000; elapsedMs += checkIntervalMs) { + result = await policy.execute((): string => executor('Diplomatiq is bad.')); + expect(result).to.equal('Diplomatiq is cool.'); + expect(executed).to.equal(1); + clock.tick(checkIntervalMs); + } + + result = await policy.execute((): string => executor('No, Diplomatiq is cool.')); + expect(result).to.equal('No, Diplomatiq is cool.'); + expect(executed).to.equal(2); + + for (let elapsedMs = 0; elapsedMs < 10000; elapsedMs += checkIntervalMs) { + result = await policy.execute((): string => executor('No, Diplomatiq is bad.')); + expect(result).to.equal('No, Diplomatiq is cool.'); + expect(executed).to.equal(2); + clock.tick(checkIntervalMs); + } + + result = await policy.execute((): string => executor('Diplomatiq is actually cool.')); + expect(result).to.equal('Diplomatiq is actually cool.'); + expect(executed).to.equal(3); + }); + + it("should hold the cache valid for Date.now() + 10000ms if setting timeToLive('absolute', Date.now() + 10000ms)", async (): Promise< + void + > => { + const policy = new CachePolicy(); + policy.timeToLive('absolute', Date.now() + 10000); + + let result: string; + let executed = 0; + + const executor = (returnValue: string): string => { + executed++; + return returnValue; + }; + + const checkIntervalMs = 100; + + result = await policy.execute((): string => executor('Diplomatiq is cool.')); + expect(result).to.equal('Diplomatiq is cool.'); + expect(executed).to.equal(1); + + for (let elapsedMs = 0; elapsedMs < 10000; elapsedMs += checkIntervalMs) { + result = await policy.execute((): string => executor('Diplomatiq is bad.')); + expect(result).to.equal('Diplomatiq is cool.'); + expect(executed).to.equal(1); + clock.tick(checkIntervalMs); + } + + result = await policy.execute((): string => executor('Diplomatiq is cool.')); + expect(result).to.equal('Diplomatiq is cool.'); + expect(executed).to.equal(2); + + result = await policy.execute((): string => executor('Diplomatiq is cooler.')); + expect(result).to.equal('Diplomatiq is cooler.'); + expect(executed).to.equal(3); + + result = await policy.execute((): string => executor('Diplomatiq is way cooler.')); + expect(result).to.equal('Diplomatiq is way cooler.'); + expect(executed).to.equal(4); + + result = await policy.execute((): string => executor('Diplomatiq is the best.')); + expect(result).to.equal('Diplomatiq is the best.'); + expect(executed).to.equal(5); + }); + + it("should hold the cache valid for Date.now() + 10000ms from each interaction if setting timeToLive('sliding', 10000)", async (): Promise< + void + > => { + const policy = new CachePolicy(); + policy.timeToLive('sliding', 10000); + + let result: string; + let executed = 0; + + const executor = (returnValue: string): string => { + executed++; + return returnValue; + }; + + const checkIntervalMs = 100; + + result = await policy.execute((): string => executor('Diplomatiq is cool.')); + expect(result).to.equal('Diplomatiq is cool.'); + expect(executed).to.equal(1); + + for (let elapsedMs = 0; elapsedMs < 20000; elapsedMs += checkIntervalMs) { + result = await policy.execute((): string => executor('Diplomatiq is bad.')); + expect(result).to.equal('Diplomatiq is cool.'); + expect(executed).to.equal(1); + clock.tick(checkIntervalMs); + } + + clock.tick(10000); + + result = await policy.execute((): string => executor('No, Diplomatiq is cool.')); + expect(result).to.equal('No, Diplomatiq is cool.'); + expect(executed).to.equal(2); + + for (let elapsedMs = 0; elapsedMs < 20000; elapsedMs += checkIntervalMs) { + result = await policy.execute((): string => executor('No, Diplomatiq is bad.')); + expect(result).to.equal('No, Diplomatiq is cool.'); + expect(executed).to.equal(2); + clock.tick(checkIntervalMs); + } + + clock.tick(10000); + + result = await policy.execute((): string => executor('Diplomatiq is actually cool.')); + expect(result).to.equal('Diplomatiq is actually cool.'); + expect(executed).to.equal(3); + + for (let elapsedMs = 0; elapsedMs < 20000; elapsedMs += checkIntervalMs) { + result = await policy.execute((): string => executor('Diplomatiq is actually bad.')); + expect(result).to.equal('Diplomatiq is actually cool.'); + expect(executed).to.equal(3); + clock.tick(checkIntervalMs); + } + }); + + it('should invalidate the cache on invalidate', async (): Promise => { + const policy = new CachePolicy(); + policy.timeToLive('relative', 10000); + + let result: string; + let executed = 0; + + const executor = (returnValue: string): string => { + executed++; + return returnValue; + }; + + const checkIntervalMs = 100; + + result = await policy.execute((): string => executor('Diplomatiq is cool.')); + expect(result).to.equal('Diplomatiq is cool.'); + expect(executed).to.equal(1); + + for (let elapsedMs = 0; elapsedMs < 5000; elapsedMs += checkIntervalMs) { + result = await policy.execute((): string => executor('Diplomatiq is bad.')); + expect(result).to.equal('Diplomatiq is cool.'); + expect(executed).to.equal(1); + clock.tick(checkIntervalMs); + } + + policy.invalidate(); + + result = await policy.execute((): string => executor('Diplomatiq is very cool.')); + expect(result).to.equal('Diplomatiq is very cool.'); + expect(executed).to.equal(2); + + for (let elapsedMs = 0; elapsedMs < 10000; elapsedMs += checkIntervalMs) { + result = await policy.execute((): string => executor('Diplomatiq is very bad.')); + expect(result).to.equal('Diplomatiq is very cool.'); + expect(executed).to.equal(2); + clock.tick(checkIntervalMs); + } + + result = await policy.execute((): string => executor('Diplomatiq is very cool.')); + expect(result).to.equal('Diplomatiq is very cool.'); + expect(executed).to.equal(3); + }); + + it('should run onCacheGetFns (but should not run onCacheMissFns and onCachePutFns) each time when the value is retrieved from the cache', async (): Promise< + void + > => { + const policy = new CachePolicy(); + policy.timeToLive('relative', 10000); + + await policy.execute((): string => { + return 'Diplomatiq is cool.'; + }); + + let onCacheGetExecuted = 0; + let onCacheMissExecuted = 0; + let onCachePutExecuted = 0; + + policy.onCacheGet((): void => { + onCacheGetExecuted++; + }); + + policy.onCacheMiss((): void => { + onCacheMissExecuted++; + }); + + policy.onCachePut((): void => { + onCachePutExecuted++; + }); + + expect(onCacheGetExecuted).to.equal(0); + expect(onCacheMissExecuted).to.equal(0); + expect(onCachePutExecuted).to.equal(0); + + await policy.execute((): string => { + return 'Diplomatiq is cool.'; + }); + + expect(onCacheGetExecuted).to.equal(1); + expect(onCacheMissExecuted).to.equal(0); + expect(onCachePutExecuted).to.equal(0); + + await policy.execute((): string => { + return 'Diplomatiq is cool.'; + }); + + expect(onCacheGetExecuted).to.equal(2); + expect(onCacheMissExecuted).to.equal(0); + expect(onCachePutExecuted).to.equal(0); + }); + + it('should run onCacheMissFns before, and onCachePutFns after the value is retreived by executing the wrapped method (but should not run onCacheGetFns)', async (): Promise< + void + > => { + const policy = new CachePolicy(); + policy.timeToLive('relative', 10000); + + let onCacheGetExecuted = 0; + let onCacheMissExecuted = 0; + let onCachePutExecuted = 0; + + policy.onCacheGet((): void => { + onCacheGetExecuted++; + }); + + policy.onCacheMiss((): void => { + onCacheMissExecuted++; + }); + + policy.onCachePut((): void => { + onCachePutExecuted++; + }); + + expect(onCacheGetExecuted).to.equal(0); + expect(onCacheMissExecuted).to.equal(0); + expect(onCachePutExecuted).to.equal(0); + + await policy.execute((): string => { + expect(onCacheGetExecuted).to.equal(0); + expect(onCacheMissExecuted).to.equal(1); + expect(onCachePutExecuted).to.equal(0); + return 'Diplomatiq is cool.'; + }); + + expect(onCacheGetExecuted).to.equal(0); + expect(onCacheMissExecuted).to.equal(1); + expect(onCachePutExecuted).to.equal(1); + }); + + it('should not allow to set timeToLive during exception', (): void => { + const policy = new CachePolicy(); + policy.execute( + async (): Promise => { + // will not resolve + }, + ); + + try { + policy.timeToLive('relative', 1000); + expect.fail('did not throw'); + } catch (ex) { + expect(ex instanceof PolicyModificationNotAllowedException).to.be.true; + } + }); + + it('should not allow to add onCacheGetFns during exception', (): void => { + const policy = new CachePolicy(); + policy.execute( + async (): Promise => { + // will not resolve + }, + ); + + try { + policy.onCacheGet((): void => { + // empty + }); + expect.fail('did not throw'); + } catch (ex) { + expect(ex instanceof PolicyModificationNotAllowedException).to.be.true; + } + }); + + it('should not allow to add onCachePutFns during exception', (): void => { + const policy = new CachePolicy(); + policy.execute( + async (): Promise => { + // will not resolve + }, + ); + + try { + policy.onCachePut((): void => { + // empty + }); + expect.fail('did not throw'); + } catch (ex) { + expect(ex instanceof PolicyModificationNotAllowedException).to.be.true; + } + }); + + it('should not allow to add onCacheMissFns during exception', (): void => { + const policy = new CachePolicy(); + policy.execute( + async (): Promise => { + // will not resolve + }, + ); + + try { + policy.onCacheMiss((): void => { + // empty + }); + expect.fail('did not throw'); + } catch (ex) { + expect(ex instanceof PolicyModificationNotAllowedException).to.be.true; + } + }); + + it("should be properly mutex'd for running an instance multiple times simultaneously", async (): Promise => { + const policy = new CachePolicy(); + + const attemptPolicyModification = (expectFailure: boolean): void => { + try { + policy.timeToLive('relative', 1000); + if (expectFailure) { + expect.fail('did not throw'); + } + } catch (ex) { + expect(ex instanceof PolicyModificationNotAllowedException).to.be.true; + } + + try { + policy.onCacheGet((): void => { + // empty + }); + if (expectFailure) { + expect.fail('did not throw'); + } + } catch (ex) { + expect(ex instanceof PolicyModificationNotAllowedException).to.be.true; + } + + try { + policy.onCachePut((): void => { + // empty + }); + if (expectFailure) { + expect.fail('did not throw'); + } + } catch (ex) { + expect(ex instanceof PolicyModificationNotAllowedException).to.be.true; + } + + try { + policy.onCacheMiss((): void => { + // empty + }); + if (expectFailure) { + expect.fail('did not throw'); + } + } catch (ex) { + expect(ex instanceof PolicyModificationNotAllowedException).to.be.true; + } + }; + + const executionResolverAddedDeferreds: Array<{ + resolverAddedPromise: Promise; + resolverAddedResolver: () => void; + }> = new Array(100).fill(undefined).map((): { + resolverAddedPromise: Promise; + resolverAddedResolver: () => void; + } => { + let resolverAddedResolver!: () => void; + const resolverAddedPromise = new Promise((resolve): void => { + resolverAddedResolver = resolve; + }); + + return { + resolverAddedPromise, + resolverAddedResolver, + }; + }); + + const executionResolvers: Array<() => void> = []; + + attemptPolicyModification(false); + + for (let i = 0; i < 100; i++) { + policy.execute( + // eslint-disable-next-line no-loop-func + async (): Promise => { + await new Promise((resolve): void => { + executionResolvers.push(resolve); + executionResolverAddedDeferreds[i].resolverAddedResolver(); + }); + }, + ); + + await executionResolverAddedDeferreds[i].resolverAddedPromise; + expect(executionResolvers.length).to.equal(i + 1); + + attemptPolicyModification(true); + } + + for (let i = 0; i < 100; i++) { + executionResolvers[i](); + attemptPolicyModification(true); + } + + attemptPolicyModification(false); + }); + + it('should throw error when setting timeToLive to 0', (): void => { + const policy = new CachePolicy(); + + try { + policy.timeToLive('relative', 0); + } catch (ex) { + expect((ex as Error).message).to.equal('value must be greater than 0'); + } + + try { + policy.timeToLive('absolute', 0); + } catch (ex) { + expect((ex as Error).message).to.equal('value must be greater than 0'); + } + + try { + policy.timeToLive('sliding', 0); + } catch (ex) { + expect((ex as Error).message).to.equal('value must be greater than 0'); + } + }); + + it('should throw error when setting timeToLive to <0', (): void => { + const policy = new CachePolicy(); + + try { + policy.timeToLive('relative', -1); + } catch (ex) { + expect((ex as Error).message).to.equal('value must be greater than 0'); + } + + try { + policy.timeToLive('absolute', -1); + } catch (ex) { + expect((ex as Error).message).to.equal('value must be greater than 0'); + } + + try { + policy.timeToLive('sliding', -1); + } catch (ex) { + expect((ex as Error).message).to.equal('value must be greater than 0'); + } + }); + + it('should throw error when setting timeToLive to a non-integer', (): void => { + const policy = new CachePolicy(); + + try { + policy.timeToLive('relative', 0.1); + } catch (ex) { + expect((ex as Error).message).to.equal('value must be integer'); + } + + try { + policy.timeToLive('absolute', 0.1); + } catch (ex) { + expect((ex as Error).message).to.equal('value must be integer'); + } + + try { + policy.timeToLive('sliding', 0.1); + } catch (ex) { + expect((ex as Error).message).to.equal('value must be integer'); + } + }); + + it('should throw error when setting timeToLive to a non-safe integer', (): void => { + const policy = new CachePolicy(); + + try { + policy.timeToLive('relative', 2 ** 53); + } catch (ex) { + expect((ex as Error).message).to.equal('value must be less than or equal to 2^53 - 1'); + } + + try { + policy.timeToLive('absolute', 2 ** 53); + } catch (ex) { + expect((ex as Error).message).to.equal('value must be less than or equal to 2^53 - 1'); + } + + try { + policy.timeToLive('sliding', 2 ** 53); + } catch (ex) { + expect((ex as Error).message).to.equal('value must be less than or equal to 2^53 - 1'); + } + }); +}); diff --git a/test/specs/circuitBreakerPolicy.test.ts b/test/specs/circuitBreakerPolicy.test.ts new file mode 100644 index 0000000..b0a6b66 --- /dev/null +++ b/test/specs/circuitBreakerPolicy.test.ts @@ -0,0 +1,1256 @@ +import { expect } from 'chai'; +import { SinonFakeTimers, useFakeTimers } from 'sinon'; +import { BrokenCircuitException } from '../../src/policies/reactive/circuitBreakerPolicy/brokenCircuitException'; +import { CircuitBreakerPolicy } from '../../src/policies/reactive/circuitBreakerPolicy/circuitBreakerPolicy'; +import { IsolatedCircuitException } from '../../src/policies/reactive/circuitBreakerPolicy/isolatedCircuitException'; +import { PolicyModificationNotAllowedException } from '../../src/types/policyModificationNotAllowedException'; + +describe('CircuitBreakerPolicy', (): void => { + let clock: SinonFakeTimers; + + beforeEach((): void => { + clock = useFakeTimers({ + toFake: ['Date'], + shouldAdvanceTime: false, + }); + }); + + afterEach((): void => { + clock.restore(); + }); + + it('should run the synchronous execution callback and return its result by default', async (): Promise => { + const policy = new CircuitBreakerPolicy(); + const result = await policy.execute((): string => { + return 'Diplomatiq is cool.'; + }); + + expect(result).to.equal('Diplomatiq is cool.'); + }); + + it('should run the asynchronous execution callback and return its result by default', async (): Promise => { + const policy = new CircuitBreakerPolicy(); + const result = await policy.execute( + // eslint-disable-next-line @typescript-eslint/require-await + async (): Promise => { + return 'Diplomatiq is cool.'; + }, + ); + + expect(result).to.equal('Diplomatiq is cool.'); + }); + + it('should run the synchronous execution callback and throw its exceptions by default', async (): Promise => { + const policy = new CircuitBreakerPolicy(); + + try { + await policy.execute((): string => { + throw new Error('TestException'); + }); + expect.fail('did not throw'); + } catch (ex) { + expect((ex as Error).message).to.equal('TestException'); + } + }); + + it('should run the asynchronous execution callback and throw its exceptions by default', async (): Promise< + void + > => { + const policy = new CircuitBreakerPolicy(); + + try { + await policy.execute( + // eslint-disable-next-line @typescript-eslint/require-await + async (): Promise => { + throw new Error('TestException'); + }, + ); + expect.fail('did not throw'); + } catch (ex) { + expect((ex as Error).message).to.equal('TestException'); + } + }); + + it('should break the circuit on encountering a reactive given result once, then hold the circuit broken for 1 second', async (): Promise< + void + > => { + const policy = new CircuitBreakerPolicy(); + policy.reactOnResult((r: string): boolean => r === 'Diplomatiq is broken.'); + + const breakingResult = await policy.execute((): string => { + return 'Diplomatiq is broken.'; + }); + expect(breakingResult).to.equal('Diplomatiq is broken.'); + + expect(policy.getCircuitState()).to.equal('Open'); + + const checkIntervalMs = 100; + for (let elapsedMs = 0; elapsedMs < 1000; elapsedMs += checkIntervalMs) { + expect(policy.getCircuitState()).to.equal('Open'); + + try { + await policy.execute((): string => { + return 'Is Diplomatiq broken?'; + }); + expect.fail('did not throw'); + } catch (ex) { + expect(ex instanceof BrokenCircuitException).to.be.true; + } + + expect(policy.getCircuitState()).to.equal('Open'); + + clock.tick(checkIntervalMs); + + expect(policy.getCircuitState()).to.equal('Open'); + } + + expect(policy.getCircuitState()).to.equal('Open'); + + const successResult = await policy.execute((): string => { + expect(policy.getCircuitState() === 'AttemptingClose'); + return 'Diplomatiq is cool.'; + }); + expect(successResult).to.equal('Diplomatiq is cool.'); + + expect(policy.getCircuitState()).to.equal('Closed'); + }); + + it('should not break the circuit on encountering a non-reactive result', async (): Promise => { + const policy = new CircuitBreakerPolicy(); + policy.reactOnResult((r: string): boolean => r === 'Diplomatiq is broken.'); + + const nonBreakingResult1 = await policy.execute((): string => { + return 'Diplomatiq is cool.'; + }); + expect(nonBreakingResult1).to.equal('Diplomatiq is cool.'); + + expect(policy.getCircuitState()).to.equal('Closed'); + + const nonBreakingResult2 = await policy.execute((): string => { + return 'Diplomatiq is cool.'; + }); + expect(nonBreakingResult2).to.equal('Diplomatiq is cool.'); + + expect(policy.getCircuitState()).to.equal('Closed'); + }); + + it('should break the circuit on encountering a reactive result 10 consecutive times, then hold the circuit broken for 30 seconds', async (): Promise< + void + > => { + const policy = new CircuitBreakerPolicy(); + policy.reactOnResult((r: string): boolean => r === 'Diplomatiq is broken.'); + policy.breakAfter(10); + policy.breakFor(30000); + + for (let i = 1; i <= 10; i++) { + const breakingResult = await policy.execute((): string => { + return 'Diplomatiq is broken.'; + }); + expect(breakingResult).to.equal('Diplomatiq is broken.'); + } + + expect(policy.getCircuitState()).to.equal('Open'); + + const checkIntervalMs = 100; + for (let elapsedMs = 0; elapsedMs < 30000; elapsedMs += checkIntervalMs) { + expect(policy.getCircuitState()).to.equal('Open'); + + try { + await policy.execute((): string => { + return 'Is Diplomatiq broken?'; + }); + expect.fail('did not throw'); + } catch (ex) { + expect(ex instanceof BrokenCircuitException).to.be.true; + } + + expect(policy.getCircuitState()).to.equal('Open'); + + clock.tick(checkIntervalMs); + + expect(policy.getCircuitState()).to.equal('Open'); + } + + expect(policy.getCircuitState()).to.equal('Open'); + + const successResult = await policy.execute((): string => { + expect(policy.getCircuitState() === 'AttemptingClose'); + return 'Diplomatiq is cool.'; + }); + expect(successResult).to.equal('Diplomatiq is cool.'); + + expect(policy.getCircuitState()).to.equal('Closed'); + }); + + it('should not break the circuit on encountering a non-reactive result 10 consecutive times', async (): Promise< + void + > => { + const policy = new CircuitBreakerPolicy(); + policy.reactOnResult((r: string): boolean => r === 'Diplomatiq is broken.'); + policy.breakAfter(10); + + for (let i = 1; i <= 10; i++) { + const result = await policy.execute((): string => { + return 'Diplomatiq is cool.'; + }); + expect(result).to.equal('Diplomatiq is cool.'); + } + + expect(policy.getCircuitState()).to.equal('Closed'); + + const result = await policy.execute((): string => { + return 'Diplomatiq is cool.'; + }); + expect(result).to.equal('Diplomatiq is cool.'); + + expect(policy.getCircuitState()).to.equal('Closed'); + }); + + it('should break the circuit on encountering multiple reactive results, altogether 10 consecutive times', async (): Promise< + void + > => { + const policy = new CircuitBreakerPolicy(); + policy.reactOnResult((r: string): boolean => r === 'Diplomatiq is broken.'); + policy.reactOnResult((r: string): boolean => r === 'Diplomatiq is not cool.'); + policy.breakAfter(10); + + for (let i = 1; i <= 5; i++) { + await policy.execute((): string => { + return 'Diplomatiq is broken.'; + }); + } + + expect(policy.getCircuitState()).to.equal('Closed'); + + for (let i = 1; i <= 5; i++) { + await policy.execute((): string => { + return 'Diplomatiq is not cool.'; + }); + } + + expect(policy.getCircuitState()).to.equal('Open'); + + try { + await policy.execute((): string => { + return 'Diplomatiq is broken.'; + }); + expect.fail('did not throw'); + } catch (ex) { + expect(ex instanceof BrokenCircuitException).to.be.true; + } + + expect(policy.getCircuitState()).to.equal('Open'); + }); + + it('should break the circuit on encountering a reactive exception once, then hold the circuit broken for 1 second', async (): Promise< + void + > => { + const policy = new CircuitBreakerPolicy(); + policy.reactOnException((e: unknown): boolean => (e as Error).message === 'TestException'); + + try { + await policy.execute((): string => { + throw new Error('TestException'); + }); + expect.fail('did not throw'); + } catch (ex) { + expect((ex as Error).message).to.equal('TestException'); + } + + expect(policy.getCircuitState()).to.equal('Open'); + + const checkIntervalMs = 100; + for (let elapsedMs = 0; elapsedMs < 1000; elapsedMs += checkIntervalMs) { + expect(policy.getCircuitState()).to.equal('Open'); + + try { + await policy.execute((): string => { + return 'Is Diplomatiq broken?'; + }); + expect.fail('did not throw'); + } catch (ex) { + expect(ex instanceof BrokenCircuitException).to.be.true; + } + + expect(policy.getCircuitState()).to.equal('Open'); + + clock.tick(checkIntervalMs); + + expect(policy.getCircuitState()).to.equal('Open'); + } + + expect(policy.getCircuitState()).to.equal('Open'); + + const successResult = await policy.execute((): string => { + expect(policy.getCircuitState() === 'AttemptingClose'); + return 'Diplomatiq is cool.'; + }); + + expect(successResult).to.equal('Diplomatiq is cool.'); + + expect(policy.getCircuitState()).to.equal('Closed'); + }); + + it('should not break the circuit on encountering a non-reactive exception', async (): Promise => { + const policy = new CircuitBreakerPolicy(); + policy.reactOnException((e: unknown): boolean => (e as Error).message === 'TestException'); + + try { + await policy.execute((): string => { + throw new Error('NotTestException'); + }); + expect.fail('did not throw'); + } catch (ex) { + expect((ex as Error).message).to.equal('NotTestException'); + } + + expect(policy.getCircuitState()).to.equal('Closed'); + + try { + await policy.execute((): string => { + throw new Error('NotTestException'); + }); + expect.fail('did not throw'); + } catch (ex) { + expect((ex as Error).message).to.equal('NotTestException'); + } + + expect(policy.getCircuitState()).to.equal('Closed'); + }); + + it('should break the circuit on encountering a reactive exception 10 consecutive times, then hold the circuit broken for 30 seconds', async (): Promise< + void + > => { + const policy = new CircuitBreakerPolicy(); + policy.reactOnException((e: unknown): boolean => (e as Error).message === 'TestException'); + policy.breakAfter(10); + policy.breakFor(30000); + + for (let i = 1; i <= 10; i++) { + try { + await policy.execute((): string => { + throw new Error('TestException'); + }); + expect.fail('did not throw'); + } catch (ex) { + expect((ex as Error).message).to.equal('TestException'); + } + } + + expect(policy.getCircuitState()).to.equal('Open'); + + const checkIntervalMs = 100; + for (let elapsedMs = 0; elapsedMs < 30000; elapsedMs += checkIntervalMs) { + expect(policy.getCircuitState()).to.equal('Open'); + + try { + await policy.execute((): string => { + return 'Is Diplomatiq broken?'; + }); + expect.fail('did not throw'); + } catch (ex) { + expect(ex instanceof BrokenCircuitException).to.be.true; + } + + expect(policy.getCircuitState()).to.equal('Open'); + + clock.tick(checkIntervalMs); + + expect(policy.getCircuitState()).to.equal('Open'); + } + + expect(policy.getCircuitState()).to.equal('Open'); + + const successResult = await policy.execute((): string => { + expect(policy.getCircuitState() === 'AttemptingClose'); + return 'Diplomatiq is cool.'; + }); + expect(successResult).to.equal('Diplomatiq is cool.'); + + expect(policy.getCircuitState()).to.equal('Closed'); + }); + + it('should not break the circuit on encountering a non-reactive exception 10 consecutive times', async (): Promise< + void + > => { + const policy = new CircuitBreakerPolicy(); + policy.reactOnException((e: unknown): boolean => (e as Error).message === 'TestException'); + policy.breakAfter(10); + + for (let i = 1; i <= 10; i++) { + try { + await policy.execute((): string => { + throw new Error('NotTestException'); + }); + expect.fail('did not throw'); + } catch (ex) { + expect((ex as Error).message).to.equal('NotTestException'); + } + } + + expect(policy.getCircuitState()).to.equal('Closed'); + + try { + await policy.execute((): string => { + throw new Error('NotTestException'); + }); + expect.fail('did not throw'); + } catch (ex) { + expect((ex as Error).message).to.equal('NotTestException'); + } + + expect(policy.getCircuitState()).to.equal('Closed'); + }); + + it('should break the circuit on encountering multiple reactive exceptions, altogether 10 consecutive times', async (): Promise< + void + > => { + const policy = new CircuitBreakerPolicy(); + policy.reactOnException((e: unknown): boolean => (e as Error).message === 'TestException'); + policy.reactOnException((e: unknown): boolean => (e as Error).message === 'AnotherTestException'); + policy.breakAfter(10); + + for (let i = 1; i <= 5; i++) { + try { + await policy.execute((): void => { + throw new Error('TestException'); + }); + expect.fail('did not throw'); + } catch (ex) { + expect((ex as Error).message).to.equal('TestException'); + } + } + + expect(policy.getCircuitState()).to.equal('Closed'); + + for (let i = 1; i <= 5; i++) { + try { + await policy.execute((): void => { + throw new Error('AnotherTestException'); + }); + expect.fail('did not throw'); + } catch (ex) { + expect((ex as Error).message).to.equal('AnotherTestException'); + } + } + + expect(policy.getCircuitState()).to.equal('Open'); + + try { + await policy.execute((): void => { + // empty + }); + expect.fail('did not throw'); + } catch (ex) { + expect(ex instanceof BrokenCircuitException).to.be.true; + } + + expect(policy.getCircuitState()).to.equal('Open'); + }); + + it('should close the circuit after the circuit break duration elapsed, if encountering a non-reactive result', async (): Promise< + void + > => { + const policy = new CircuitBreakerPolicy(); + policy.reactOnResult((r: string): boolean => r === 'Diplomatiq is broken.'); + + await policy.execute((): string => { + return 'Diplomatiq is broken.'; + }); + + expect(policy.getCircuitState()).to.equal('Open'); + + clock.tick(1000); + + expect(policy.getCircuitState()).to.equal('Open'); + + await policy.execute((): string => { + expect(policy.getCircuitState() === 'AttemptingClose'); + return 'Diplomatiq is cool.'; + }); + + expect(policy.getCircuitState()).to.equal('Closed'); + }); + + it('should not close the circuit after the circuit break duration elapsed, if encountering a reactive result', async (): Promise< + void + > => { + const policy = new CircuitBreakerPolicy(); + policy.reactOnResult((r: string): boolean => r === 'Diplomatiq is broken.'); + + await policy.execute((): string => { + return 'Diplomatiq is broken.'; + }); + + expect(policy.getCircuitState()).to.equal('Open'); + + clock.tick(1000); + + expect(policy.getCircuitState()).to.equal('Open'); + + await policy.execute((): string => { + expect(policy.getCircuitState() === 'AttemptingClose'); + return 'Diplomatiq is broken.'; + }); + + expect(policy.getCircuitState()).to.equal('Open'); + }); + + it('should close the circuit after the circuit break duration elapsed, if encountering a non-reactive exception', async (): Promise< + void + > => { + const policy = new CircuitBreakerPolicy(); + policy.reactOnException((e: unknown): boolean => (e as Error).message === 'TestException'); + + try { + await policy.execute((): string => { + throw new Error('TestException'); + }); + expect.fail('did not throw'); + } catch (ex) { + expect((ex as Error).message).to.equal('TestException'); + } + + expect(policy.getCircuitState()).to.equal('Open'); + + clock.tick(1000); + + expect(policy.getCircuitState()).to.equal('Open'); + + try { + expect(policy.getCircuitState() === 'AttemptingClose'); + await policy.execute((): string => { + throw new Error('NotHandledTestException'); + }); + expect.fail('did not throw'); + } catch (ex) { + expect((ex as Error).message).to.equal('NotHandledTestException'); + } + + expect(policy.getCircuitState()).to.equal('Closed'); + }); + + it('should not close the circuit after the circuit break duration elapsed, if encountering a reactive exception', async (): Promise< + void + > => { + const policy = new CircuitBreakerPolicy(); + policy.reactOnException((e: unknown): boolean => (e as Error).message === 'TestException'); + + try { + await policy.execute((): string => { + throw new Error('TestException'); + }); + expect.fail('did not throw'); + } catch (ex) { + expect((ex as Error).message).to.equal('TestException'); + } + + expect(policy.getCircuitState()).to.equal('Open'); + + clock.tick(1000); + + expect(policy.getCircuitState()).to.equal('Open'); + + try { + expect(policy.getCircuitState() === 'AttemptingClose'); + await policy.execute((): string => { + throw new Error('TestException'); + }); + expect.fail('did not throw'); + } catch (ex) { + expect((ex as Error).message).to.equal('TestException'); + } + + expect(policy.getCircuitState()).to.equal('Open'); + }); + + it('should isolate the circuit on calling isolate when closed', async (): Promise => { + const policy = new CircuitBreakerPolicy(); + expect(policy.getCircuitState()).to.equal('Closed'); + await policy.isolate(); + expect(policy.getCircuitState()).to.equal('Isolated'); + }); + + it('should isolate the circuit on calling isolate when open', async (): Promise => { + const policy = new CircuitBreakerPolicy(); + policy.reactOnResult((): boolean => true); + await policy.execute((): void => { + // empty + }); + expect(policy.getCircuitState()).to.equal('Open'); + await policy.isolate(); + expect(policy.getCircuitState()).to.equal('Isolated'); + }); + + it('should throw IsolatedCircuitException when the circuit is isolated', async (): Promise => { + const policy = new CircuitBreakerPolicy(); + await policy.isolate(); + + try { + await policy.execute((): void => { + // empty + }); + expect.fail('did not throw'); + } catch (ex) { + expect(ex instanceof IsolatedCircuitException).to.be.true; + } + }); + + it('should reset the circuit on calling reset when isolated', async (): Promise => { + const policy = new CircuitBreakerPolicy(); + await policy.isolate(); + expect(policy.getCircuitState()).to.equal('Isolated'); + await policy.reset(); + expect(policy.getCircuitState()).to.equal('Closed'); + }); + + it('should not reset the circuit on calling reset when closed', async (): Promise => { + const policy = new CircuitBreakerPolicy(); + expect(policy.getCircuitState()).to.equal('Closed'); + + try { + await policy.reset(); + expect.fail('did not throw'); + } catch (ex) { + expect((ex as Error).message).to.equal('cannot reset if not in Isolated state'); + } + }); + + it('should not reset the circuit on calling reset when open', async (): Promise => { + const policy = new CircuitBreakerPolicy(); + policy.reactOnResult((): boolean => true); + await policy.execute((): void => { + // empty + }); + expect(policy.getCircuitState()).to.equal('Open'); + + try { + await policy.reset(); + expect.fail('did not throw'); + } catch (ex) { + expect((ex as Error).message).to.equal('cannot reset if not in Isolated state'); + } + }); + + it('should run synchronous onOpenFns sequentially when the circuit transitions to Open state', async (): Promise< + void + > => { + const policy = new CircuitBreakerPolicy(); + policy.reactOnResult((r: string): boolean => r === 'Diplomatiq is broken.'); + + let onOpenRun = 0; + policy.onOpen((): void => { + expect(onOpenRun).to.equal(0); + onOpenRun++; + expect(onOpenRun).to.equal(1); + }); + policy.onOpen((): void => { + expect(onOpenRun).to.equal(1); + onOpenRun++; + expect(onOpenRun).to.equal(2); + }); + policy.onOpen((): void => { + expect(onOpenRun).to.equal(2); + onOpenRun++; + expect(onOpenRun).to.equal(3); + }); + + await policy.execute((): string => { + return 'Diplomatiq is broken.'; + }); + + expect(onOpenRun).to.equal(3); + }); + + it('should run asynchronous onOpenFns sequentially when the circuit transitions to Open state', async (): Promise< + void + > => { + const policy = new CircuitBreakerPolicy(); + policy.reactOnResult((r: string): boolean => r === 'Diplomatiq is broken.'); + + let onOpenRun = 0; + policy.onOpen( + // eslint-disable-next-line @typescript-eslint/require-await + async (): Promise => { + expect(onOpenRun).to.equal(0); + onOpenRun++; + expect(onOpenRun).to.equal(1); + }, + ); + policy.onOpen( + // eslint-disable-next-line @typescript-eslint/require-await + async (): Promise => { + expect(onOpenRun).to.equal(1); + onOpenRun++; + expect(onOpenRun).to.equal(2); + }, + ); + policy.onOpen( + // eslint-disable-next-line @typescript-eslint/require-await + async (): Promise => { + expect(onOpenRun).to.equal(2); + onOpenRun++; + expect(onOpenRun).to.equal(3); + }, + ); + + await policy.execute((): string => { + return 'Diplomatiq is broken.'; + }); + + expect(onOpenRun).to.equal(3); + }); + + it('should run synchronous onCloseFns sequentially when the circuit transitions to Closed state', async (): Promise< + void + > => { + const policy = new CircuitBreakerPolicy(); + policy.reactOnResult((r: string): boolean => r === 'Diplomatiq is broken.'); + + let onCloseRun = 0; + policy.onClose((): void => { + expect(onCloseRun).to.equal(0); + onCloseRun++; + expect(onCloseRun).to.equal(1); + }); + policy.onClose((): void => { + expect(onCloseRun).to.equal(1); + onCloseRun++; + expect(onCloseRun).to.equal(2); + }); + policy.onClose((): void => { + expect(onCloseRun).to.equal(2); + onCloseRun++; + expect(onCloseRun).to.equal(3); + }); + + await policy.execute((): string => { + return 'Diplomatiq is broken.'; + }); + + clock.tick(1000); + + await policy.execute((): string => { + return 'Diplomatiq is cool.'; + }); + + expect(onCloseRun).to.equal(3); + }); + + it('should run asynchronous onCloseFns sequentially when the circuit transitions to Closed state', async (): Promise< + void + > => { + const policy = new CircuitBreakerPolicy(); + policy.reactOnResult((r: string): boolean => r === 'Diplomatiq is broken.'); + + let onCloseRun = 0; + policy.onClose( + // eslint-disable-next-line @typescript-eslint/require-await + async (): Promise => { + expect(onCloseRun).to.equal(0); + onCloseRun++; + expect(onCloseRun).to.equal(1); + }, + ); + policy.onClose( + // eslint-disable-next-line @typescript-eslint/require-await + async (): Promise => { + expect(onCloseRun).to.equal(1); + onCloseRun++; + expect(onCloseRun).to.equal(2); + }, + ); + policy.onClose( + // eslint-disable-next-line @typescript-eslint/require-await + async (): Promise => { + expect(onCloseRun).to.equal(2); + onCloseRun++; + expect(onCloseRun).to.equal(3); + }, + ); + + await policy.execute((): string => { + return 'Diplomatiq is broken.'; + }); + + clock.tick(1000); + + await policy.execute((): string => { + return 'Diplomatiq is cool.'; + }); + + expect(onCloseRun).to.equal(3); + }); + + it('should run synchronous onAttemptingCloseFns sequentially when the circuit transitions to AttemptingClose state', async (): Promise< + void + > => { + const policy = new CircuitBreakerPolicy(); + policy.reactOnResult((r: string): boolean => r === 'Diplomatiq is broken.'); + + let onAttemptingCloseRun = 0; + policy.onAttemptingClose((): void => { + expect(onAttemptingCloseRun).to.equal(0); + onAttemptingCloseRun++; + expect(onAttemptingCloseRun).to.equal(1); + }); + policy.onAttemptingClose((): void => { + expect(onAttemptingCloseRun).to.equal(1); + onAttemptingCloseRun++; + expect(onAttemptingCloseRun).to.equal(2); + }); + policy.onAttemptingClose((): void => { + expect(onAttemptingCloseRun).to.equal(2); + onAttemptingCloseRun++; + expect(onAttemptingCloseRun).to.equal(3); + }); + + await policy.execute((): string => { + return 'Diplomatiq is broken.'; + }); + + clock.tick(1000); + + await policy.execute((): string => { + return 'Diplomatiq is cool.'; + }); + + expect(onAttemptingCloseRun).to.equal(3); + }); + + it('should run asynchronous onAttemptingCloseFns sequentially when the circuit transitions to AttemptingClose state', async (): Promise< + void + > => { + const policy = new CircuitBreakerPolicy(); + policy.reactOnResult((r: string): boolean => r === 'Diplomatiq is broken.'); + + let onAttemptingCloseRun = 0; + policy.onAttemptingClose( + // eslint-disable-next-line @typescript-eslint/require-await + async (): Promise => { + expect(onAttemptingCloseRun).to.equal(0); + onAttemptingCloseRun++; + expect(onAttemptingCloseRun).to.equal(1); + }, + ); + policy.onAttemptingClose( + // eslint-disable-next-line @typescript-eslint/require-await + async (): Promise => { + expect(onAttemptingCloseRun).to.equal(1); + onAttemptingCloseRun++; + expect(onAttemptingCloseRun).to.equal(2); + }, + ); + policy.onAttemptingClose( + // eslint-disable-next-line @typescript-eslint/require-await + async (): Promise => { + expect(onAttemptingCloseRun).to.equal(2); + onAttemptingCloseRun++; + expect(onAttemptingCloseRun).to.equal(3); + }, + ); + + await policy.execute((): string => { + return 'Diplomatiq is broken.'; + }); + + clock.tick(1000); + + await policy.execute((): string => { + return 'Diplomatiq is cool.'; + }); + + expect(onAttemptingCloseRun).to.equal(3); + }); + + it('should run synchronous onIsolateFns sequentially when the circuit transitions to Isolated state', async (): Promise< + void + > => { + const policy = new CircuitBreakerPolicy(); + + let onIsolateRun = 0; + policy.onIsolate((): void => { + expect(onIsolateRun).to.equal(0); + onIsolateRun++; + expect(onIsolateRun).to.equal(1); + }); + policy.onIsolate((): void => { + expect(onIsolateRun).to.equal(1); + onIsolateRun++; + expect(onIsolateRun).to.equal(2); + }); + policy.onIsolate((): void => { + expect(onIsolateRun).to.equal(2); + onIsolateRun++; + expect(onIsolateRun).to.equal(3); + }); + + await policy.isolate(); + + expect(onIsolateRun).to.equal(3); + }); + + it('should run asynchronous onIsolateFns sequentially when the circuit transitions to Isolated state', async (): Promise< + void + > => { + const policy = new CircuitBreakerPolicy(); + + let onIsolateRun = 0; + policy.onIsolate( + // eslint-disable-next-line @typescript-eslint/require-await + async (): Promise => { + expect(onIsolateRun).to.equal(0); + onIsolateRun++; + expect(onIsolateRun).to.equal(1); + }, + ); + policy.onIsolate( + // eslint-disable-next-line @typescript-eslint/require-await + async (): Promise => { + expect(onIsolateRun).to.equal(1); + onIsolateRun++; + expect(onIsolateRun).to.equal(2); + }, + ); + policy.onIsolate( + // eslint-disable-next-line @typescript-eslint/require-await + async (): Promise => { + expect(onIsolateRun).to.equal(2); + onIsolateRun++; + expect(onIsolateRun).to.equal(3); + }, + ); + + await policy.isolate(); + + expect(onIsolateRun).to.equal(3); + }); + + it('should not allow to set breakAfter during execution', (): void => { + const policy = new CircuitBreakerPolicy(); + policy.execute( + async (): Promise => { + await new Promise((): void => { + // will not resolve + }); + }, + ); + + try { + policy.breakAfter(1); + expect.fail('did not throw'); + } catch (ex) { + expect(ex instanceof PolicyModificationNotAllowedException).to.be.true; + } + }); + + it('should not allow to set breakFor during execution', (): void => { + const policy = new CircuitBreakerPolicy(); + policy.execute( + async (): Promise => { + await new Promise((): void => { + // will not resolve + }); + }, + ); + + try { + policy.breakFor(1); + expect.fail('did not throw'); + } catch (ex) { + expect(ex instanceof PolicyModificationNotAllowedException).to.be.true; + } + }); + + it('should not allow to add onOpenFns during execution', (): void => { + const policy = new CircuitBreakerPolicy(); + policy.execute( + async (): Promise => { + await new Promise((): void => { + // will not resolve + }); + }, + ); + + try { + policy.onOpen((): void => { + // empty + }); + expect.fail('did not throw'); + } catch (ex) { + expect(ex instanceof PolicyModificationNotAllowedException).to.be.true; + } + }); + + it('should not allow to add onCloseFns during execution', (): void => { + const policy = new CircuitBreakerPolicy(); + policy.execute( + async (): Promise => { + await new Promise((): void => { + // will not resolve + }); + }, + ); + + try { + policy.onClose((): void => { + // empty + }); + expect.fail('did not throw'); + } catch (ex) { + expect(ex instanceof PolicyModificationNotAllowedException).to.be.true; + } + }); + + it('should not allow to add onAttemptingCloseFns during execution', (): void => { + const policy = new CircuitBreakerPolicy(); + policy.execute( + async (): Promise => { + await new Promise((): void => { + // will not resolve + }); + }, + ); + + try { + policy.onAttemptingClose((): void => { + // empty + }); + expect.fail('did not throw'); + } catch (ex) { + expect(ex instanceof PolicyModificationNotAllowedException).to.be.true; + } + }); + + it('should not allow to add onIsolateFns during execution', (): void => { + const policy = new CircuitBreakerPolicy(); + policy.execute( + async (): Promise => { + await new Promise((): void => { + // will not resolve + }); + }, + ); + + try { + policy.onIsolate((): void => { + // empty + }); + expect.fail('did not throw'); + } catch (ex) { + expect(ex instanceof PolicyModificationNotAllowedException).to.be.true; + } + }); + + it("should be properly mutex'd for running an instance multiple times simultaneously", async (): Promise => { + const policy = new CircuitBreakerPolicy(); + + const attemptPolicyModification = (expectFailure: boolean): void => { + try { + policy.breakAfter(1); + if (expectFailure) { + expect.fail('did not throw'); + } + } catch (ex) { + expect(ex instanceof PolicyModificationNotAllowedException).to.be.true; + } + + try { + policy.breakFor(1000); + if (expectFailure) { + expect.fail('did not throw'); + } + } catch (ex) { + expect(ex instanceof PolicyModificationNotAllowedException).to.be.true; + } + + try { + policy.onClose((): void => { + // empty + }); + if (expectFailure) { + expect.fail('did not throw'); + } + } catch (ex) { + expect(ex instanceof PolicyModificationNotAllowedException).to.be.true; + } + + try { + policy.onOpen((): void => { + // empty + }); + if (expectFailure) { + expect.fail('did not throw'); + } + } catch (ex) { + expect(ex instanceof PolicyModificationNotAllowedException).to.be.true; + } + + try { + policy.onAttemptingClose((): void => { + // empty + }); + if (expectFailure) { + expect.fail('did not throw'); + } + } catch (ex) { + expect(ex instanceof PolicyModificationNotAllowedException).to.be.true; + } + + try { + policy.onIsolate((): void => { + // empty + }); + if (expectFailure) { + expect.fail('did not throw'); + } + } catch (ex) { + expect(ex instanceof PolicyModificationNotAllowedException).to.be.true; + } + }; + + const executionResolverAddedDeferreds: Array<{ + resolverAddedPromise: Promise; + resolverAddedResolver: () => void; + }> = new Array(100).fill(undefined).map((): { + resolverAddedPromise: Promise; + resolverAddedResolver: () => void; + } => { + let resolverAddedResolver!: () => void; + const resolverAddedPromise = new Promise((resolve): void => { + resolverAddedResolver = resolve; + }); + + return { + resolverAddedPromise, + resolverAddedResolver, + }; + }); + + const executionResolvers: Array<() => void> = []; + + attemptPolicyModification(false); + + for (let i = 0; i < 100; i++) { + policy.execute( + // eslint-disable-next-line no-loop-func + async (): Promise => { + await new Promise((resolve): void => { + executionResolvers.push(resolve); + executionResolverAddedDeferreds[i].resolverAddedResolver(); + }); + }, + ); + + await executionResolverAddedDeferreds[i].resolverAddedPromise; + expect(executionResolvers.length).to.equal(i + 1); + + attemptPolicyModification(true); + } + + for (let i = 0; i < 100; i++) { + executionResolvers[i](); + attemptPolicyModification(true); + } + + attemptPolicyModification(false); + }); + + it('should throw error when setting numberOfConsecutiveReactionsBeforeCircuitBreak to 0', (): void => { + const policy = new CircuitBreakerPolicy(); + try { + policy.breakAfter(0); + expect.fail('did not throw'); + } catch (ex) { + expect((ex as Error).message).to.equal( + 'numberOfConsecutiveReactionsBeforeCircuitBreak must be greater than 0', + ); + } + }); + + it('should throw error when setting numberOfConsecutiveReactionsBeforeCircuitBreak to <0', (): void => { + const policy = new CircuitBreakerPolicy(); + try { + policy.breakAfter(-1); + expect.fail('did not throw'); + } catch (ex) { + expect((ex as Error).message).to.equal( + 'numberOfConsecutiveReactionsBeforeCircuitBreak must be greater than 0', + ); + } + }); + + it('should throw error when setting numberOfConsecutiveReactionsBeforeCircuitBreak to a non-integer', (): void => { + const policy = new CircuitBreakerPolicy(); + try { + policy.breakAfter(0.1); + expect.fail('did not throw'); + } catch (ex) { + expect((ex as Error).message).to.equal('numberOfConsecutiveReactionsBeforeCircuitBreak must be integer'); + } + }); + + it('should throw error when setting numberOfConsecutiveReactionsBeforeCircuitBreak to a non-safe integer', (): void => { + const policy = new CircuitBreakerPolicy(); + try { + policy.breakAfter(2 ** 53); + expect.fail('did not throw'); + } catch (ex) { + expect((ex as Error).message).to.equal( + 'numberOfConsecutiveReactionsBeforeCircuitBreak must be less than or equal to 2^53 - 1', + ); + } + }); + + it('should throw error when setting durationOfCircuitBreakMs to 0', (): void => { + const policy = new CircuitBreakerPolicy(); + try { + policy.breakFor(0); + expect.fail('did not throw'); + } catch (ex) { + expect((ex as Error).message).to.equal('durationOfCircuitBreakMs must be greater than 0'); + } + }); + + it('should throw error when setting durationOfCircuitBreakMs to <0', (): void => { + const policy = new CircuitBreakerPolicy(); + try { + policy.breakFor(-1); + expect.fail('did not throw'); + } catch (ex) { + expect((ex as Error).message).to.equal('durationOfCircuitBreakMs must be greater than 0'); + } + }); + + it('should throw error when setting durationOfCircuitBreakMs to a non-integer', (): void => { + const policy = new CircuitBreakerPolicy(); + try { + policy.breakFor(0.1); + expect.fail('did not throw'); + } catch (ex) { + expect((ex as Error).message).to.equal('durationOfCircuitBreakMs must be integer'); + } + }); + + it('should throw error when setting durationOfCircuitBreakMs to a non-safe integer', (): void => { + const policy = new CircuitBreakerPolicy(); + try { + policy.breakFor(2 ** 53); + expect.fail('did not throw'); + } catch (ex) { + expect((ex as Error).message).to.equal('durationOfCircuitBreakMs must be less than or equal to 2^53 - 1'); + } + }); + + it('should not allow to transition to invalid states', (): void => { + const policy = new CircuitBreakerPolicy(); + expect(policy.getCircuitState()).to.equal('Closed'); + try { + // eslint-disable-next-line @typescript-eslint/ban-ts-ignore + // @ts-ignore + policy.validateTransition(new Set(['AttemptingClose'])); + } catch (ex) { + expect((ex as Error).message === 'invalid transition'); + } + }); +}); diff --git a/test/specs/fallbackPolicy.test.ts b/test/specs/fallbackPolicy.test.ts new file mode 100644 index 0000000..2488529 --- /dev/null +++ b/test/specs/fallbackPolicy.test.ts @@ -0,0 +1,1128 @@ +import { expect } from 'chai'; +import { FallbackChainExhaustedException } from '../../src/policies/reactive/fallbackPolicy/fallbackChainExhaustedException'; +import { FallbackPolicy } from '../../src/policies/reactive/fallbackPolicy/fallbackPolicy'; +import { PolicyModificationNotAllowedException } from '../../src/types/policyModificationNotAllowedException'; + +describe('FallbackPolicy', (): void => { + it('should run the synchronous execution callback and return its result by default', async (): Promise => { + const policy = new FallbackPolicy(); + const result = await policy.execute((): string => { + return 'Diplomatiq is cool.'; + }); + + expect(result).to.equal('Diplomatiq is cool.'); + }); + + it('should run the asynchronous execution callback and return its result by default', async (): Promise => { + const policy = new FallbackPolicy(); + const result = await policy.execute( + // eslint-disable-next-line @typescript-eslint/require-await + async (): Promise => { + return 'Diplomatiq is cool.'; + }, + ); + + expect(result).to.equal('Diplomatiq is cool.'); + }); + + it('should run the synchronous execution callback and throw its exceptions by default', async (): Promise => { + const policy = new FallbackPolicy(); + + try { + await policy.execute((): string => { + throw new Error('TestException'); + }); + expect.fail('did not throw'); + } catch (ex) { + expect((ex as Error).message).to.equal('TestException'); + } + }); + + it('should run the asynchronous execution callback and throw its exceptions by default', async (): Promise< + void + > => { + const policy = new FallbackPolicy(); + + try { + await policy.execute( + // eslint-disable-next-line @typescript-eslint/require-await + async (): Promise => { + throw new Error('TestException'); + }, + ); + expect.fail('did not throw'); + } catch (ex) { + expect((ex as Error).message).to.equal('TestException'); + } + }); + + it('should fallback on a reactive (i.e. wrong) result, then return the result of the synchronous fallback function', async (): Promise< + void + > => { + const policy = new FallbackPolicy(); + policy.reactOnResult((r: string): boolean => r === 'Diplomatiq is cool.'); + policy.fallback((): string => { + return 'Diplomatiq is the coolest.'; + }); + + const result = await policy.execute((): string => { + return 'Diplomatiq is cool.'; + }); + + expect(result).to.equal('Diplomatiq is the coolest.'); + }); + + it('should fallback on a reactive (i.e. wrong) result, then return the result of the asynchronous fallback function', async (): Promise< + void + > => { + const policy = new FallbackPolicy(); + policy.reactOnResult((r: string): boolean => r === 'Diplomatiq is cool.'); + policy.fallback( + // eslint-disable-next-line @typescript-eslint/require-await + async (): Promise => { + return 'Diplomatiq is the coolest.'; + }, + ); + + const result = await policy.execute((): string => { + return 'Diplomatiq is cool.'; + }); + + expect(result).to.equal('Diplomatiq is the coolest.'); + }); + + it('should not fallback on a non-reactive (i.e. right) result, but return the result', async (): Promise => { + const policy = new FallbackPolicy(); + policy.reactOnResult((r: string): boolean => r === 'Diplomatiq is not cool.'); + + const result = await policy.execute((): string => { + return 'Diplomatiq is cool.'; + }); + + expect(result).to.equal('Diplomatiq is cool.'); + }); + + it('should fallback along a synchronous fallback chain sequentially while it produces reactive (i.e. wrong) result until the first non-reactive (i.e. right) result is produced', async (): Promise< + void + > => { + const policy = new FallbackPolicy(); + + let fallbacksExecuted = 0; + + policy.reactOnResult((r: string): boolean => r === 'Diplomatiq is cool.'); + + policy.fallback((): string => { + expect(fallbacksExecuted).to.equal(0); + fallbacksExecuted++; + expect(fallbacksExecuted).to.equal(1); + return 'Diplomatiq is cool.'; + }); + policy.fallback((): string => { + expect(fallbacksExecuted).to.equal(1); + fallbacksExecuted++; + expect(fallbacksExecuted).to.equal(2); + return 'Diplomatiq is cool.'; + }); + policy.fallback((): string => { + expect(fallbacksExecuted).to.equal(2); + fallbacksExecuted++; + expect(fallbacksExecuted).to.equal(3); + return 'Diplomatiq is the coolest.'; + }); + + const result = await policy.execute((): string => { + return 'Diplomatiq is cool.'; + }); + + expect(result).to.equal('Diplomatiq is the coolest.'); + expect(fallbacksExecuted).to.equal(3); + }); + + it('should fallback along an asynchronous fallback chain sequentially while it produces reactive (i.e. wrong) result, until the first non-reactive (i.e. right) result is produced', async (): Promise< + void + > => { + const policy = new FallbackPolicy(); + + let fallbacksExecuted = 0; + + policy.reactOnResult((r: string): boolean => r === 'Diplomatiq is cool.'); + + policy.fallback( + // eslint-disable-next-line @typescript-eslint/require-await + async (): Promise => { + expect(fallbacksExecuted).to.equal(0); + fallbacksExecuted++; + expect(fallbacksExecuted).to.equal(1); + return 'Diplomatiq is cool.'; + }, + ); + policy.fallback( + // eslint-disable-next-line @typescript-eslint/require-await + async (): Promise => { + expect(fallbacksExecuted).to.equal(1); + fallbacksExecuted++; + expect(fallbacksExecuted).to.equal(2); + return 'Diplomatiq is cool.'; + }, + ); + policy.fallback( + // eslint-disable-next-line @typescript-eslint/require-await + async (): Promise => { + expect(fallbacksExecuted).to.equal(2); + fallbacksExecuted++; + expect(fallbacksExecuted).to.equal(3); + return 'Diplomatiq is the coolest.'; + }, + ); + + const result = await policy.execute((): string => { + return 'Diplomatiq is cool.'; + }); + + expect(result).to.equal('Diplomatiq is the coolest.'); + expect(fallbacksExecuted).to.equal(3); + }); + + it('should fallback on multiple reactive (i.e. wrong) results if any of them occurs', async (): Promise => { + const policy = new FallbackPolicy(); + + let fallbacksExecuted = 0; + + policy.reactOnResult((r: string): boolean => r === 'Diplomatiq is not cool.'); + policy.reactOnResult((r: string): boolean => r === 'Diplomatiq is bad.'); + policy.reactOnResult((r: string): boolean => r === 'Diplomatiq is the worst.'); + + policy.fallback((): string => { + expect(fallbacksExecuted).to.equal(0); + fallbacksExecuted++; + expect(fallbacksExecuted).to.equal(1); + return 'Diplomatiq is bad.'; + }); + policy.fallback((): string => { + expect(fallbacksExecuted).to.equal(1); + fallbacksExecuted++; + expect(fallbacksExecuted).to.equal(2); + return 'Diplomatiq is the worst.'; + }); + policy.fallback((): string => { + expect(fallbacksExecuted).to.equal(2); + fallbacksExecuted++; + expect(fallbacksExecuted).to.equal(3); + return 'Diplomatiq is cool.'; + }); + + const result = await policy.execute((): string => { + return 'Diplomatiq is not cool.'; + }); + + expect(fallbacksExecuted).to.equal(3); + expect(result).to.equal('Diplomatiq is cool.'); + }); + + it('should fallback on any result until an exception is thrown', async (): Promise => { + const policy = new FallbackPolicy(); + + let fallbacksExecuted = 0; + + policy.reactOnResult((): boolean => true); + + policy.fallback((): string => { + expect(fallbacksExecuted).to.equal(0); + fallbacksExecuted++; + expect(fallbacksExecuted).to.equal(1); + return 'Diplomatiq is cool.'; + }); + policy.fallback((): string => { + expect(fallbacksExecuted).to.equal(1); + fallbacksExecuted++; + expect(fallbacksExecuted).to.equal(2); + return 'Diplomatiq is cool.'; + }); + policy.fallback((): string => { + expect(fallbacksExecuted).to.equal(2); + fallbacksExecuted++; + expect(fallbacksExecuted).to.equal(3); + throw new Error('TestException'); + }); + + try { + await policy.execute((): string => { + return 'Diplomatiq is cool.'; + }); + expect.fail('did not throw'); + } catch (ex) { + expect((ex as Error).message).to.equal('TestException'); + } + + expect(fallbacksExecuted).to.equal(3); + }); + + it('should fallback on a reactive exception, then return the result of the synchronous fallback function', async (): Promise< + void + > => { + const policy = new FallbackPolicy(); + policy.reactOnException((e: unknown): boolean => (e as Error).message === 'TestException'); + policy.fallback((): string => { + return 'Diplomatiq is cool.'; + }); + + const result = await policy.execute((): string => { + throw new Error('TestException'); + }); + + expect(result).to.equal('Diplomatiq is cool.'); + }); + + it('should fallback on a reactive exception, then return the result of the asynchronous fallback function', async (): Promise< + void + > => { + const policy = new FallbackPolicy(); + policy.reactOnException((e: unknown): boolean => (e as Error).message === 'TestException'); + policy.fallback( + // eslint-disable-next-line @typescript-eslint/require-await + async (): Promise => { + return 'Diplomatiq is cool.'; + }, + ); + + const result = await policy.execute((): string => { + throw new Error('TestException'); + }); + + expect(result).to.equal('Diplomatiq is cool.'); + }); + + it('should not fallback on a non-reactive exception, but throw the exception', async (): Promise => { + const policy = new FallbackPolicy(); + policy.reactOnException((e: unknown): boolean => (e as Error).message === 'AnotherException'); + + try { + await policy.execute((): void => { + throw new Error('TestException'); + }); + expect.fail('did not throw'); + } catch (ex) { + expect((ex as Error).message).to.equal('TestException'); + } + }); + + it('should fallback along a synchronous fallback chain sequentially while it throws reactive exception result until the first non-reactive exception is thrown', async (): Promise< + void + > => { + const policy = new FallbackPolicy(); + + let fallbacksExecuted = 0; + + policy.reactOnException((e: unknown): boolean => (e as Error).message === 'TestException'); + + policy.fallback((): string => { + expect(fallbacksExecuted).to.equal(0); + fallbacksExecuted++; + expect(fallbacksExecuted).to.equal(1); + throw new Error('TestException'); + }); + policy.fallback((): string => { + expect(fallbacksExecuted).to.equal(1); + fallbacksExecuted++; + expect(fallbacksExecuted).to.equal(2); + throw new Error('TestException'); + }); + policy.fallback((): string => { + expect(fallbacksExecuted).to.equal(2); + fallbacksExecuted++; + expect(fallbacksExecuted).to.equal(3); + return 'Diplomatiq is cool.'; + }); + + const result = await policy.execute((): string => { + throw new Error('TestException'); + }); + + expect(result).to.equal('Diplomatiq is cool.'); + expect(fallbacksExecuted).to.equal(3); + }); + + it('should fallback along an asynchronous fallback chain sequentially while it throws reactive exception until the first non-reactive exception is thrown', async (): Promise< + void + > => { + const policy = new FallbackPolicy(); + + let fallbacksExecuted = 0; + + policy.reactOnException((e: unknown): boolean => (e as Error).message === 'TestException'); + + policy.fallback( + // eslint-disable-next-line @typescript-eslint/require-await + async (): Promise => { + expect(fallbacksExecuted).to.equal(0); + fallbacksExecuted++; + expect(fallbacksExecuted).to.equal(1); + throw new Error('TestException'); + }, + ); + policy.fallback( + // eslint-disable-next-line @typescript-eslint/require-await + async (): Promise => { + expect(fallbacksExecuted).to.equal(1); + fallbacksExecuted++; + expect(fallbacksExecuted).to.equal(2); + throw new Error('TestException'); + }, + ); + policy.fallback( + // eslint-disable-next-line @typescript-eslint/require-await + async (): Promise => { + expect(fallbacksExecuted).to.equal(2); + fallbacksExecuted++; + expect(fallbacksExecuted).to.equal(3); + return 'Diplomatiq is cool.'; + }, + ); + + const result = await policy.execute((): string => { + throw new Error('TestException'); + }); + + expect(result).to.equal('Diplomatiq is cool.'); + expect(fallbacksExecuted).to.equal(3); + }); + + it('should fallback on multiple reactive exceptions if any of them occurs', async (): Promise => { + const policy = new FallbackPolicy(); + + let fallbacksExecuted = 0; + + policy.reactOnException((e: unknown): boolean => (e as Error).message === 'ExceptionOne'); + policy.reactOnException((e: unknown): boolean => (e as Error).message === 'ExceptionTwo'); + policy.reactOnException((e: unknown): boolean => (e as Error).message === 'ExceptionThree'); + + policy.fallback((): string => { + expect(fallbacksExecuted).to.equal(0); + fallbacksExecuted++; + expect(fallbacksExecuted).to.equal(1); + throw new Error('ExceptionTwo'); + }); + policy.fallback((): string => { + expect(fallbacksExecuted).to.equal(1); + fallbacksExecuted++; + expect(fallbacksExecuted).to.equal(2); + throw new Error('ExceptionThree'); + }); + policy.fallback((): string => { + expect(fallbacksExecuted).to.equal(2); + fallbacksExecuted++; + expect(fallbacksExecuted).to.equal(3); + return 'Diplomatiq is cool.'; + }); + + const result = await policy.execute((): string => { + throw new Error('ExceptionOne'); + }); + + expect(fallbacksExecuted).to.equal(3); + expect(result).to.equal('Diplomatiq is cool.'); + }); + + it('should fallback on any exception until a result is returned', async (): Promise => { + const policy = new FallbackPolicy(); + + let fallbacksExecuted = 0; + + policy.reactOnException((): boolean => true); + + policy.fallback((): string => { + expect(fallbacksExecuted).to.equal(0); + fallbacksExecuted++; + expect(fallbacksExecuted).to.equal(1); + throw new Error('ExceptionTwo'); + }); + policy.fallback((): string => { + expect(fallbacksExecuted).to.equal(1); + fallbacksExecuted++; + expect(fallbacksExecuted).to.equal(2); + throw new Error('ExceptionThree'); + }); + policy.fallback((): string => { + expect(fallbacksExecuted).to.equal(2); + fallbacksExecuted++; + expect(fallbacksExecuted).to.equal(3); + return 'Diplomatiq is cool.'; + }); + + const result = await policy.execute((): string => { + throw new Error('ExceptionOne'); + }); + + expect(result).to.equal('Diplomatiq is cool.'); + expect(fallbacksExecuted).to.equal(3); + }); + + it('should throw FallbackChainExhaustedException if falling back on result and there are no (more) links on the fallback chain', async (): Promise< + void + > => { + const policy = new FallbackPolicy(); + policy.reactOnResult((): boolean => true); + + try { + await policy.execute((): string => { + return 'Diplomatiq is cool.'; + }); + expect.fail('did not throw'); + } catch (ex) { + expect(ex instanceof FallbackChainExhaustedException).to.be.true; + } + }); + + it('should throw FallbackChainExhaustedException if falling back on exception and there are no (more) links on the fallback chain', async (): Promise< + void + > => { + const policy = new FallbackPolicy(); + policy.reactOnException((): boolean => true); + + try { + await policy.execute((): string => { + throw new Error('TestException'); + }); + expect.fail('did not throw'); + } catch (ex) { + expect(ex instanceof FallbackChainExhaustedException).to.be.true; + } + }); + + it('should run onFallbackFn with result filled on fallback, before the fallback', async (): Promise => { + const policy = new FallbackPolicy(); + + let fallbacksExecuted = 0; + let onFallbackExecuted = 0; + + policy.reactOnResult((r: string): boolean => r === 'Diplomatiq is bad.'); + policy.fallback((): string => { + expect(fallbacksExecuted).to.equal(0); + fallbacksExecuted++; + expect(fallbacksExecuted).to.equal(1); + return 'Diplomatiq is cool.'; + }); + policy.onFallback((result: string | undefined, error: unknown | undefined): void => { + expect(result).to.equal('Diplomatiq is bad.'); + expect(error).to.equal(undefined); + + expect(onFallbackExecuted).to.equal(fallbacksExecuted); + + onFallbackExecuted++; + }); + + const result = await policy.execute((): string => { + return 'Diplomatiq is bad.'; + }); + + expect(result).to.equal('Diplomatiq is cool.'); + expect(fallbacksExecuted).to.equal(1); + expect(onFallbackExecuted).to.equal(1); + }); + + it('should run onFallbackFn with result filled on fallback, every time before a fallback', async (): Promise< + void + > => { + const policy = new FallbackPolicy(); + + let fallbacksExecuted = 0; + let onFallbackExecuted = 0; + + policy.reactOnResult((r: string): boolean => r === 'Diplomatiq is bad.'); + policy.fallback((): string => { + expect(fallbacksExecuted).to.equal(0); + fallbacksExecuted++; + expect(fallbacksExecuted).to.equal(1); + return 'Diplomatiq is bad.'; + }); + policy.fallback((): string => { + expect(fallbacksExecuted).to.equal(1); + fallbacksExecuted++; + expect(fallbacksExecuted).to.equal(2); + return 'Diplomatiq is bad.'; + }); + policy.fallback((): string => { + expect(fallbacksExecuted).to.equal(2); + fallbacksExecuted++; + expect(fallbacksExecuted).to.equal(3); + return 'Diplomatiq is cool.'; + }); + policy.onFallback((result: string | undefined, error: unknown | undefined): void => { + expect(result).to.equal('Diplomatiq is bad.'); + expect(error).to.equal(undefined); + + expect(onFallbackExecuted).to.equal(fallbacksExecuted); + onFallbackExecuted++; + }); + + const result = await policy.execute((): string => { + return 'Diplomatiq is bad.'; + }); + + expect(result).to.equal('Diplomatiq is cool.'); + expect(fallbacksExecuted).to.equal(3); + expect(onFallbackExecuted).to.equal(3); + }); + + it('should run onFallbackFn with error filled on fallback, before the fallback', async (): Promise => { + const policy = new FallbackPolicy(); + + let fallbacksExecuted = 0; + let onFallbackExecuted = 0; + + policy.reactOnException((e: unknown): boolean => (e as Error).message === 'TestException'); + policy.fallback((): string => { + expect(fallbacksExecuted).to.equal(0); + fallbacksExecuted++; + expect(fallbacksExecuted).to.equal(1); + return 'Diplomatiq is cool.'; + }); + policy.onFallback((result: string | undefined, error: unknown | undefined): void => { + expect(result).to.equal(undefined); + expect((error as Error).message).to.equal('TestException'); + + expect(onFallbackExecuted).to.equal(fallbacksExecuted); + + onFallbackExecuted++; + }); + + const result = await policy.execute((): string => { + throw new Error('TestException'); + }); + + expect(result).to.equal('Diplomatiq is cool.'); + expect(fallbacksExecuted).to.equal(1); + expect(onFallbackExecuted).to.equal(1); + }); + + it('should run onFallbackFn with error filled on fallback, every time before a fallback', async (): Promise< + void + > => { + const policy = new FallbackPolicy(); + + let fallbacksExecuted = 0; + let onFallbackExecuted = 0; + + policy.reactOnException((e: unknown): boolean => (e as Error).message === 'TestException'); + policy.fallback((): string => { + expect(fallbacksExecuted).to.equal(0); + fallbacksExecuted++; + expect(fallbacksExecuted).to.equal(1); + throw new Error('TestException'); + }); + policy.fallback((): string => { + expect(fallbacksExecuted).to.equal(1); + fallbacksExecuted++; + expect(fallbacksExecuted).to.equal(2); + throw new Error('TestException'); + }); + policy.fallback((): string => { + expect(fallbacksExecuted).to.equal(2); + fallbacksExecuted++; + expect(fallbacksExecuted).to.equal(3); + return 'Diplomatiq is cool.'; + }); + policy.onFallback((result: string | undefined, error: unknown | undefined): void => { + expect(result).to.equal(undefined); + expect((error as Error).message).to.equal('TestException'); + + expect(onFallbackExecuted).to.equal(fallbacksExecuted); + onFallbackExecuted++; + }); + + const result = await policy.execute((): string => { + throw new Error('TestException'); + }); + + expect(result).to.equal('Diplomatiq is cool.'); + expect(fallbacksExecuted).to.equal(3); + expect(onFallbackExecuted).to.equal(3); + }); + + it('should not run onFallbackFn if no fallback happened', async (): Promise => { + const policy = new FallbackPolicy(); + + let onFallbackExecuted = 0; + + policy.onFallback((): void => { + onFallbackExecuted++; + }); + + const result = await policy.execute((): string => { + return 'Diplomatiq is cool.'; + }); + + expect(result).to.equal('Diplomatiq is cool.'); + expect(onFallbackExecuted).to.equal(0); + }); + + it('should await an asynchronous onFallbackFn before fallback', async (): Promise => { + const policy = new FallbackPolicy(); + + let fallbacksExecuted = 0; + let onFallbackExecuted = 0; + + policy.reactOnResult((r: string): boolean => r === 'Diplomatiq is bad.'); + policy.fallback((): string => { + expect(fallbacksExecuted).to.equal(0); + fallbacksExecuted++; + expect(fallbacksExecuted).to.equal(1); + return 'Diplomatiq is cool.'; + }); + policy.onFallback( + async ( + result: string | undefined, + error: unknown | undefined, + // eslint-disable-next-line @typescript-eslint/require-await + ): Promise => { + expect(result).to.equal('Diplomatiq is bad.'); + expect(error).to.equal(undefined); + + expect(onFallbackExecuted).to.equal(fallbacksExecuted); + onFallbackExecuted++; + }, + ); + + const result = await policy.execute((): string => { + return 'Diplomatiq is bad.'; + }); + + expect(result).to.equal('Diplomatiq is cool.'); + expect(fallbacksExecuted).to.equal(1); + expect(onFallbackExecuted).to.equal(1); + }); + + it('should run multiple synchronous onFallbackFns sequentially on fallback', async (): Promise => { + const policy = new FallbackPolicy(); + + let fallbacksExecuted = 0; + let onFallbackExecuted = 0; + + policy.reactOnResult((r: string): boolean => r === 'Diplomatiq is bad.'); + policy.fallback((): string => { + expect(fallbacksExecuted).to.equal(0); + fallbacksExecuted++; + expect(fallbacksExecuted).to.equal(1); + return 'Diplomatiq is bad.'; + }); + policy.fallback((): string => { + expect(fallbacksExecuted).to.equal(1); + fallbacksExecuted++; + expect(fallbacksExecuted).to.equal(2); + return 'Diplomatiq is bad.'; + }); + policy.fallback((): string => { + expect(fallbacksExecuted).to.equal(2); + fallbacksExecuted++; + expect(fallbacksExecuted).to.equal(3); + return 'Diplomatiq is cool.'; + }); + policy.onFallback((result: string | undefined, error: unknown | undefined): void => { + expect(result).to.equal('Diplomatiq is bad.'); + expect(error).to.equal(undefined); + + expect(onFallbackExecuted).to.equal(fallbacksExecuted * 3); + onFallbackExecuted++; + expect(onFallbackExecuted).to.equal(fallbacksExecuted * 3 + 1); + }); + policy.onFallback((result: string | undefined, error: unknown | undefined): void => { + expect(result).to.equal('Diplomatiq is bad.'); + expect(error).to.equal(undefined); + + expect(onFallbackExecuted).to.equal(fallbacksExecuted * 3 + 1); + onFallbackExecuted++; + expect(onFallbackExecuted).to.equal(fallbacksExecuted * 3 + 2); + }); + policy.onFallback((result: string | undefined, error: unknown | undefined): void => { + expect(result).to.equal('Diplomatiq is bad.'); + expect(error).to.equal(undefined); + + expect(onFallbackExecuted).to.equal(fallbacksExecuted * 3 + 2); + onFallbackExecuted++; + expect(onFallbackExecuted).to.equal(fallbacksExecuted * 3 + 3); + }); + + const result = await policy.execute((): string => { + return 'Diplomatiq is bad.'; + }); + + expect(result).to.equal('Diplomatiq is cool.'); + expect(fallbacksExecuted).to.equal(3); + expect(onFallbackExecuted).to.equal(9); + }); + + it('should run multiple asynchronous onFallbackFns sequentially on fallback', async (): Promise => { + const policy = new FallbackPolicy(); + + let fallbacksExecuted = 0; + let onFallbackExecuted = 0; + + policy.reactOnResult((r: string): boolean => r === 'Diplomatiq is bad.'); + policy.fallback((): string => { + expect(fallbacksExecuted).to.equal(0); + fallbacksExecuted++; + expect(fallbacksExecuted).to.equal(1); + return 'Diplomatiq is bad.'; + }); + policy.fallback((): string => { + expect(fallbacksExecuted).to.equal(1); + fallbacksExecuted++; + expect(fallbacksExecuted).to.equal(2); + return 'Diplomatiq is bad.'; + }); + policy.fallback((): string => { + expect(fallbacksExecuted).to.equal(2); + fallbacksExecuted++; + expect(fallbacksExecuted).to.equal(3); + return 'Diplomatiq is cool.'; + }); + policy.onFallback( + // eslint-disable-next-line @typescript-eslint/require-await + async (result: string | undefined, error: unknown | undefined): Promise => { + expect(result).to.equal('Diplomatiq is bad.'); + expect(error).to.equal(undefined); + + expect(onFallbackExecuted).to.equal(fallbacksExecuted * 3); + onFallbackExecuted++; + expect(onFallbackExecuted).to.equal(fallbacksExecuted * 3 + 1); + }, + ); + policy.onFallback( + // eslint-disable-next-line @typescript-eslint/require-await + async (result: string | undefined, error: unknown | undefined): Promise => { + expect(result).to.equal('Diplomatiq is bad.'); + expect(error).to.equal(undefined); + + expect(onFallbackExecuted).to.equal(fallbacksExecuted * 3 + 1); + onFallbackExecuted++; + expect(onFallbackExecuted).to.equal(fallbacksExecuted * 3 + 2); + }, + ); + policy.onFallback( + // eslint-disable-next-line @typescript-eslint/require-await + async (result: string | undefined, error: unknown | undefined): Promise => { + expect(result).to.equal('Diplomatiq is bad.'); + expect(error).to.equal(undefined); + + expect(onFallbackExecuted).to.equal(fallbacksExecuted * 3 + 2); + onFallbackExecuted++; + expect(onFallbackExecuted).to.equal(fallbacksExecuted * 3 + 3); + }, + ); + + const result = await policy.execute((): string => { + return 'Diplomatiq is bad.'; + }); + + expect(result).to.equal('Diplomatiq is cool.'); + expect(fallbacksExecuted).to.equal(3); + expect(onFallbackExecuted).to.equal(9); + }); + + it('should run onFinallyFn after all execution and fallbacks if fallback happened', async (): Promise => { + const policy = new FallbackPolicy(); + + let fallbacksExecuted = 0; + let onFallbackExecuted = 0; + let onFinallyExecuted = 0; + + policy.reactOnResult((r: string): boolean => r === 'Diplomatiq is bad.'); + policy.fallback((): string => { + expect(fallbacksExecuted).to.equal(0); + fallbacksExecuted++; + expect(fallbacksExecuted).to.equal(1); + return 'Diplomatiq is bad.'; + }); + policy.fallback((): string => { + expect(fallbacksExecuted).to.equal(1); + fallbacksExecuted++; + expect(fallbacksExecuted).to.equal(2); + return 'Diplomatiq is bad.'; + }); + policy.fallback((): string => { + expect(fallbacksExecuted).to.equal(2); + fallbacksExecuted++; + expect(fallbacksExecuted).to.equal(3); + return 'Diplomatiq is cool.'; + }); + policy.onFallback((result: string | undefined, error: unknown | undefined): void => { + expect(result).to.equal('Diplomatiq is bad.'); + expect(error).to.equal(undefined); + + expect(onFallbackExecuted).to.equal(fallbacksExecuted); + onFallbackExecuted++; + }); + policy.onFinally((): void => { + onFinallyExecuted++; + + expect(fallbacksExecuted).to.equal(3); + expect(onFallbackExecuted).to.equal(3); + }); + + const result = await policy.execute((): string => { + return 'Diplomatiq is bad.'; + }); + + expect(result).to.equal('Diplomatiq is cool.'); + + expect(fallbacksExecuted).to.equal(3); + expect(onFallbackExecuted).to.equal(3); + expect(onFinallyExecuted).to.equal(1); + }); + + it('should run onFinallyFn after the execution if no fallback happened', async (): Promise => { + const policy = new FallbackPolicy(); + + let onFinallyExecuted = 0; + + policy.onFinally((): void => { + onFinallyExecuted++; + }); + + const result = await policy.execute((): string => { + return 'Diplomatiq is cool.'; + }); + + expect(result).to.equal('Diplomatiq is cool.'); + expect(onFinallyExecuted).to.equal(1); + }); + + it('should run multiple synchronous onFinallyFns sequentially', async (): Promise => { + const policy = new FallbackPolicy(); + + let onFinallyExecuted = 0; + + policy.onFinally((): void => { + expect(onFinallyExecuted).to.equal(0); + onFinallyExecuted++; + expect(onFinallyExecuted).to.equal(1); + }); + policy.onFinally((): void => { + expect(onFinallyExecuted).to.equal(1); + onFinallyExecuted++; + expect(onFinallyExecuted).to.equal(2); + }); + policy.onFinally((): void => { + expect(onFinallyExecuted).to.equal(2); + onFinallyExecuted++; + expect(onFinallyExecuted).to.equal(3); + }); + + const result = await policy.execute((): string => { + return 'Diplomatiq is cool.'; + }); + + expect(result).to.equal('Diplomatiq is cool.'); + expect(onFinallyExecuted).to.equal(3); + }); + + it('should run multiple asynchronous onFinallyFns sequentially', async (): Promise => { + const policy = new FallbackPolicy(); + + let onFinallyExecuted = 0; + + policy.onFinally( + // eslint-disable-next-line @typescript-eslint/require-await + async (): Promise => { + expect(onFinallyExecuted).to.equal(0); + onFinallyExecuted++; + expect(onFinallyExecuted).to.equal(1); + }, + ); + policy.onFinally( + // eslint-disable-next-line @typescript-eslint/require-await + async (): Promise => { + expect(onFinallyExecuted).to.equal(1); + onFinallyExecuted++; + expect(onFinallyExecuted).to.equal(2); + }, + ); + policy.onFinally( + // eslint-disable-next-line @typescript-eslint/require-await + async (): Promise => { + expect(onFinallyExecuted).to.equal(2); + onFinallyExecuted++; + expect(onFinallyExecuted).to.equal(3); + }, + ); + + const result = await policy.execute((): string => { + return 'Diplomatiq is cool.'; + }); + + expect(result).to.equal('Diplomatiq is cool.'); + expect(onFinallyExecuted).to.equal(3); + }); + + it('should run onFinallyFn once, regardless of how many link the fallback chain has', async (): Promise => { + const policy = new FallbackPolicy(); + + let onFinallyExecuted = 0; + + policy.reactOnResult((r: string): boolean => r === 'Diplomatiq is bad.'); + policy.fallback((): string => { + return 'Diplomatiq is bad.'; + }); + policy.fallback((): string => { + return 'Diplomatiq is bad.'; + }); + policy.fallback((): string => { + return 'Diplomatiq is cool.'; + }); + policy.onFinally((): void => { + onFinallyExecuted++; + }); + + const result = await policy.execute((): string => { + return 'Diplomatiq is bad.'; + }); + + expect(result).to.equal('Diplomatiq is cool.'); + expect(onFinallyExecuted).to.equal(1); + }); + + it('should not allow to set fallback during execution', (): void => { + const policy = new FallbackPolicy(); + policy.execute( + async (): Promise => { + await new Promise((): void => { + // will not resolve + }); + }, + ); + + try { + policy.fallback((): void => { + // empty + }); + expect.fail('did not throw'); + } catch (ex) { + expect(ex instanceof PolicyModificationNotAllowedException).to.be.true; + } + }); + + it('should not allow to set onFallbackFns during execution', (): void => { + const policy = new FallbackPolicy(); + policy.execute( + async (): Promise => { + await new Promise((): void => { + // will not resolve + }); + }, + ); + + try { + policy.onFallback((): void => { + // empty + }); + expect.fail('did not throw'); + } catch (ex) { + expect(ex instanceof PolicyModificationNotAllowedException).to.be.true; + } + }); + + it('should not allow to add onFinallyFns during execution', (): void => { + const policy = new FallbackPolicy(); + policy.execute( + async (): Promise => { + await new Promise((): void => { + // will not resolve + }); + }, + ); + + try { + policy.onFinally((): void => { + // empty + }); + expect.fail('did not throw'); + } catch (ex) { + expect(ex instanceof PolicyModificationNotAllowedException).to.be.true; + } + }); + + it("should be properly mutex'd for running an instance multiple times simultaneously", async (): Promise => { + const policy = new FallbackPolicy(); + + const attemptPolicyModification = (expectFailure: boolean): void => { + try { + policy.fallback((): void => { + // empty + }); + if (expectFailure) { + expect.fail('did not throw'); + } + } catch (ex) { + expect(ex instanceof PolicyModificationNotAllowedException).to.be.true; + } + + try { + policy.onFallback((): void => { + // empty + }); + if (expectFailure) { + expect.fail('did not throw'); + } + } catch (ex) { + expect(ex instanceof PolicyModificationNotAllowedException).to.be.true; + } + + try { + policy.onFinally((): void => { + // empty + }); + if (expectFailure) { + expect.fail('did not throw'); + } + } catch (ex) { + expect(ex instanceof PolicyModificationNotAllowedException).to.be.true; + } + }; + + const executionResolverAddedDeferreds: Array<{ + resolverAddedPromise: Promise; + resolverAddedResolver: () => void; + }> = new Array(100).fill(undefined).map((): { + resolverAddedPromise: Promise; + resolverAddedResolver: () => void; + } => { + let resolverAddedResolver!: () => void; + const resolverAddedPromise = new Promise((resolve): void => { + resolverAddedResolver = resolve; + }); + + return { + resolverAddedPromise, + resolverAddedResolver, + }; + }); + + const executionResolvers: Array<() => void> = []; + + attemptPolicyModification(false); + + for (let i = 0; i < 100; i++) { + policy.execute( + // eslint-disable-next-line no-loop-func + async (): Promise => { + await new Promise((resolve): void => { + executionResolvers.push(resolve); + executionResolverAddedDeferreds[i].resolverAddedResolver(); + }); + }, + ); + + await executionResolverAddedDeferreds[i].resolverAddedPromise; + expect(executionResolvers.length).to.equal(i + 1); + + attemptPolicyModification(true); + } + + for (let i = 0; i < 100; i++) { + executionResolvers[i](); + attemptPolicyModification(true); + } + + attemptPolicyModification(false); + }); +}); diff --git a/test/specs/nopPolicy.test.ts b/test/specs/nopPolicy.test.ts new file mode 100644 index 0000000..c1e9610 --- /dev/null +++ b/test/specs/nopPolicy.test.ts @@ -0,0 +1,54 @@ +import { expect } from 'chai'; +import { NopPolicy } from '../../src/policies/nopPolicy'; + +describe('NopPolicy', (): void => { + it('should run the synchronous execution callback and return its result', async (): Promise => { + const policy = new NopPolicy(); + const result = await policy.execute((): string => { + return 'Diplomatiq is cool.'; + }); + + expect(result).to.equal('Diplomatiq is cool.'); + }); + + it('should run the asynchronous execution callback and return its result', async (): Promise => { + const policy = new NopPolicy(); + const result = await policy.execute( + // eslint-disable-next-line @typescript-eslint/require-await + async (): Promise => { + return 'Diplomatiq is cool.'; + }, + ); + + expect(result).to.equal('Diplomatiq is cool.'); + }); + + it('should run the synchronous execution callback and throw its exceptions', async (): Promise => { + const policy = new NopPolicy(); + + try { + await policy.execute((): string => { + throw new Error('TestException'); + }); + expect.fail('did not throw'); + } catch (ex) { + expect((ex as Error).message).to.equal('TestException'); + } + }); + + it('should run the asynchronous execution callback and throw its exceptions', async (): Promise => { + const policy = new NopPolicy(); + + try { + await policy.execute( + // eslint-disable-next-line @typescript-eslint/require-await + async (): Promise => { + throw new Error('TestException'); + }, + ); + expect.fail('did not throw'); + } catch (ex) { + expect((ex as Error).message).to.equal('TestException'); + } + }); +}); diff --git a/test/specs/policyCombination.test.ts b/test/specs/policyCombination.test.ts new file mode 100644 index 0000000..0b33b49 --- /dev/null +++ b/test/specs/policyCombination.test.ts @@ -0,0 +1,122 @@ +import { expect } from 'chai'; +import { PolicyCombination } from '../../src/policies/policyCombination'; +import { TimeoutException } from '../../src/policies/proactive/timeoutPolicy/timeoutException'; +import { TimeoutPolicy } from '../../src/policies/proactive/timeoutPolicy/timeoutPolicy'; +import { FallbackPolicy } from '../../src/policies/reactive/fallbackPolicy/fallbackPolicy'; +import { RetryPolicy } from '../../src/policies/reactive/retryPolicy/retryPolicy'; + +describe('PolicyCombination', (): void => { + it('should run the wrapped policy inside the wrapper policy (wrapped with singular wrapping)', async (): Promise< + void + > => { + let onRetryExecuted = 0; + let onFallbackExecuted = 0; + + const fallbackPolicy = new FallbackPolicy(); + fallbackPolicy.reactOnResult((r): boolean => r); + fallbackPolicy.fallback((): boolean => { + return false; + }); + fallbackPolicy.onFallback((): void => { + expect(onRetryExecuted).to.equal(1); + onFallbackExecuted++; + }); + + const retryPolicy = new RetryPolicy(); + retryPolicy.reactOnResult((r): boolean => r); + retryPolicy.onRetry((): void => { + expect(onFallbackExecuted).to.equal(0); + onRetryExecuted++; + }); + + fallbackPolicy.wrap(retryPolicy); + + await fallbackPolicy.execute((): boolean => { + return true; + }); + + expect(onRetryExecuted).to.equal(1); + expect(onFallbackExecuted).to.equal(1); + }); + + it('should run the wrapped policy inside the wrapper policy (combined with PolicyCombination)', async (): Promise< + void + > => { + let onRetryExecuted = 0; + let onFallbackExecuted = 0; + + const fallbackPolicy = new FallbackPolicy(); + fallbackPolicy.reactOnResult((r): boolean => r); + fallbackPolicy.fallback((): boolean => { + return false; + }); + fallbackPolicy.onFallback((): void => { + expect(onRetryExecuted).to.equal(1); + onFallbackExecuted++; + }); + + const retryPolicy = new RetryPolicy(); + retryPolicy.reactOnResult((r): boolean => r); + retryPolicy.onRetry((): void => { + expect(onFallbackExecuted).to.equal(0); + onRetryExecuted++; + }); + + const wrappedPolicy = PolicyCombination.combine([fallbackPolicy, retryPolicy]); + await wrappedPolicy.execute((): boolean => { + return true; + }); + + expect(onRetryExecuted).to.equal(1); + expect(onFallbackExecuted).to.equal(1); + }); + + it('should construct a policy which combines the other policies sequentially', async (): Promise => { + let onTimeoutExecuted = 0; + let onRetryExecuted = 0; + let onFallbackExecuted = 0; + + const fallbackPolicy = new FallbackPolicy(); + fallbackPolicy.reactOnException((e): boolean => e instanceof TimeoutException); + fallbackPolicy.fallback((): void => { + // empty + }); + fallbackPolicy.onFallback((): void => { + expect(onTimeoutExecuted).to.equal(1); + expect(onRetryExecuted).to.equal(1); + expect(onFallbackExecuted).to.equal(0); + onFallbackExecuted++; + }); + + const retryPolicy = new RetryPolicy(); + retryPolicy.reactOnException((e): boolean => e instanceof TimeoutException); + retryPolicy.onRetry((): void => { + expect(onTimeoutExecuted).to.equal(1); + expect(onRetryExecuted).to.equal(0); + expect(onFallbackExecuted).to.equal(0); + onRetryExecuted++; + }); + + const timeoutPolicy = new TimeoutPolicy(); + timeoutPolicy.timeoutAfter(1); + timeoutPolicy.onTimeout((): void => { + expect(onTimeoutExecuted).to.equal(0); + expect(onRetryExecuted).to.equal(0); + expect(onFallbackExecuted).to.equal(0); + onTimeoutExecuted++; + }); + + const wrappedPolicies = PolicyCombination.combine([fallbackPolicy, retryPolicy, timeoutPolicy]); + await wrappedPolicies.execute( + async (): Promise => { + return new Promise((resolve): void => { + setTimeout(resolve, 5); + }); + }, + ); + + expect(onTimeoutExecuted).to.equal(1); + expect(onRetryExecuted).to.equal(1); + expect(onFallbackExecuted).to.equal(1); + }); +}); diff --git a/test/specs/predicateChecker.test.ts b/test/specs/predicateChecker.test.ts new file mode 100644 index 0000000..ec79a1f --- /dev/null +++ b/test/specs/predicateChecker.test.ts @@ -0,0 +1,128 @@ +import { expect } from 'chai'; +import { PredicateChecker } from '../../src/utils/predicateChecker'; + +describe('PredicateChecker', (): void => { + describe('single', (): void => { + it('should return true, when the predicate returns true for the subject', async (): Promise => { + const result = await PredicateChecker.single( + 'Diplomatiq is cool.', + (subject: string): boolean => subject === 'Diplomatiq is cool.', + ); + expect(result).to.be.true; + }); + + it('should return false, when the predicate returns false for the subject', async (): Promise => { + const result = await PredicateChecker.single( + 'Diplomatiq is not cool.', + (subject: string): boolean => subject === 'Diplomatiq is cool.', + ); + expect(result).to.be.false; + }); + + it('should return true, when the async predicate returns true for the subject', async (): Promise => { + const result = await PredicateChecker.single( + 'Diplomatiq is cool.', + (subject: string): boolean => subject === 'Diplomatiq is cool.', + ); + expect(result).to.be.true; + }); + + it('should return false, when the async predicate returns false for the subject', async (): Promise => { + const result = await PredicateChecker.single( + 'Diplomatiq is not cool.', + (subject: string): boolean => subject === 'Diplomatiq is cool.', + ); + expect(result).to.be.false; + }); + }); + + describe('some', (): void => { + it('should return true if at least one of the predicates returns true for the subject', async (): Promise< + void + > => { + const result = await PredicateChecker.some('Diplomatiq is cool.', [ + (subject: string): boolean => subject === 'Diplomatiq is cool.', + (subject: string): boolean => subject === 'Diplomatiq is the coolest.', + ]); + expect(result).to.be.true; + }); + + it('should return false if none of the predicates returns true for the subject', async (): Promise => { + const result = await PredicateChecker.some('Diplomatiq is not cool.', [ + (subject: string): boolean => subject === 'Diplomatiq is cool.', + (subject: string): boolean => subject === 'Diplomatiq is the coolest.', + ]); + expect(result).to.be.false; + }); + + it('should return true if at least one of the async predicates returns true for the subject', async (): Promise< + void + > => { + const result = await PredicateChecker.some('Diplomatiq is cool.', [ + (subject: string): boolean => subject === 'Diplomatiq is cool.', + (subject: string): boolean => subject === 'Diplomatiq is the coolest.', + ]); + expect(result).to.be.true; + }); + + it('should return false if none of the async predicates returns true for the subject', async (): Promise< + void + > => { + const result = await PredicateChecker.some('Diplomatiq is not cool.', [ + (subject: string): boolean => subject === 'Diplomatiq is cool.', + (subject: string): boolean => subject === 'Diplomatiq is the coolest.', + ]); + expect(result).to.be.false; + }); + + it('should return false if predicates is empty', async (): Promise => { + const result = await PredicateChecker.some('Diplomatiq is cool.', []); + expect(result).to.be.false; + }); + }); + + describe('all', (): void => { + it('should return true if all of the predicates returns true for the subject', async (): Promise => { + const result = await PredicateChecker.every('Diplomatiq is cool.', [ + (subject: string): boolean => subject.includes('Diplomatiq'), + (subject: string): boolean => subject.includes('cool'), + ]); + expect(result).to.be.true; + }); + + it('should return false if at least one of the predicates returns false for the subject', async (): Promise< + void + > => { + const result = await PredicateChecker.every('Diplomatiq is cool.', [ + (subject: string): boolean => subject.includes('Diplomatiq'), + (subject: string): boolean => subject.includes('not cool'), + ]); + expect(result).to.be.false; + }); + + it('should return true if all of the async predicates returns true for the subject', async (): Promise< + void + > => { + const result = await PredicateChecker.every('Diplomatiq is cool.', [ + (subject: string): boolean => subject.includes('Diplomatiq'), + (subject: string): boolean => subject.includes('cool'), + ]); + expect(result).to.be.true; + }); + + it('should return false if at least one of the async predicates returns false for the subject', async (): Promise< + void + > => { + const result = await PredicateChecker.every('Diplomatiq is cool.', [ + (subject: string): boolean => subject.includes('Diplomatiq'), + (subject: string): boolean => subject.includes('not cool'), + ]); + expect(result).to.be.false; + }); + + it('should return false if predicates is empty', async (): Promise => { + const result = await PredicateChecker.every('Diplomatiq is cool.', []); + expect(result).to.be.false; + }); + }); +}); diff --git a/test/specs/retryPolicy.test.ts b/test/specs/retryPolicy.test.ts new file mode 100644 index 0000000..c4eacf6 --- /dev/null +++ b/test/specs/retryPolicy.test.ts @@ -0,0 +1,1140 @@ +import { expect } from 'chai'; +import { SinonFakeTimers, useFakeTimers } from 'sinon'; +import { RetryPolicy } from '../../src/policies/reactive/retryPolicy/retryPolicy'; +import { PolicyModificationNotAllowedException } from '../../src/types/policyModificationNotAllowedException'; + +describe('RetryPolicy', (): void => { + let clock: SinonFakeTimers; + + beforeEach((): void => { + clock = useFakeTimers({ + toFake: ['Date', 'setTimeout', 'clearTimeout'], + shouldAdvanceTime: false, + }); + }); + + afterEach((): void => { + clock.restore(); + }); + + it('should run the synchronous execution callback and return its result by default', async (): Promise => { + const policy = new RetryPolicy(); + const result = await policy.execute((): string => { + return 'Diplomatiq is cool.'; + }); + + expect(result).to.equal('Diplomatiq is cool.'); + }); + + it('should run the asynchronous execution callback and return its result by default', async (): Promise => { + const policy = new RetryPolicy(); + const result = await policy.execute( + // eslint-disable-next-line @typescript-eslint/require-await + async (): Promise => { + return 'Diplomatiq is cool.'; + }, + ); + + expect(result).to.equal('Diplomatiq is cool.'); + }); + + it('should run the synchronous execution callback and throw its exceptions by default', async (): Promise => { + const policy = new RetryPolicy(); + + try { + await policy.execute((): string => { + throw new Error('TestException'); + }); + expect.fail('did not throw'); + } catch (ex) { + expect((ex as Error).message).to.equal('TestException'); + } + }); + + it('should run the asynchronous execution callback and throw its exceptions by default', async (): Promise< + void + > => { + const policy = new RetryPolicy(); + + try { + await policy.execute( + // eslint-disable-next-line @typescript-eslint/require-await + async (): Promise => { + throw new Error('TestException'); + }, + ); + expect.fail('did not throw'); + } catch (ex) { + expect((ex as Error).message).to.equal('TestException'); + } + }); + + it('should retry on a reactive result once, then return the result by default', async (): Promise => { + const policy = new RetryPolicy(); + policy.reactOnResult((r: string): boolean => r === 'Diplomatiq is cool.'); + + let executed = 0; + + const result = await policy.execute((): string => { + executed++; + return 'Diplomatiq is cool.'; + }); + + expect(executed).to.equal(2); + expect(result).to.equal('Diplomatiq is cool.'); + }); + + it('should not retry on a non-reactive result, but return the result', async (): Promise => { + const policy = new RetryPolicy(); + policy.reactOnResult((r: string): boolean => r === 'Diplomatiq is not cool.'); + + let executed = 0; + + const result = await policy.execute((): string => { + executed++; + return 'Diplomatiq is cool.'; + }); + + expect(executed).to.equal(1); + expect(result).to.equal('Diplomatiq is cool.'); + }); + + it('should retry on a reactive result thrice when setting retryCount to 3, then return the result', async (): Promise< + void + > => { + const policy = new RetryPolicy(); + policy.reactOnResult((r: string): boolean => r === 'Diplomatiq is cool.'); + policy.retryCount(3); + + let executed = 0; + + const result = await policy.execute((): string => { + executed++; + return 'Diplomatiq is cool.'; + }); + + expect(executed).to.equal(4); + expect(result).to.equal('Diplomatiq is cool.'); + }); + + it('should retry on multiple reactive results, then return the result', async (): Promise => { + const policy = new RetryPolicy(); + policy.reactOnResult((r: string): boolean => r === 'Diplomatiq is cool.'); + policy.reactOnResult((r: string): boolean => r === 'Diplomatiq is the coolest.'); + + let executed = 0; + let result: string; + + result = await policy.execute((): string => { + executed++; + return 'Diplomatiq is cool.'; + }); + + expect(executed).to.equal(2); + expect(result).to.equal('Diplomatiq is cool.'); + + result = await policy.execute((): string => { + executed++; + return 'Diplomatiq is the coolest.'; + }); + + expect(executed).to.equal(4); + expect(result).to.equal('Diplomatiq is the coolest.'); + }); + + it('should retry on a reactive exception once, then throw by default', async (): Promise => { + const policy = new RetryPolicy(); + policy.reactOnException((e: unknown): boolean => (e as Error).message === 'TestException'); + + let executed = 0; + + try { + await policy.execute((): unknown => { + executed++; + throw new Error('TestException'); + }); + expect.fail('did not throw'); + } catch (ex) { + expect((ex as Error).message).to.equal('TestException'); + } + + expect(executed).to.equal(2); + }); + + it('should not retry on a non-reactive exception, but throw', async (): Promise => { + const policy = new RetryPolicy(); + policy.reactOnException((e: unknown): boolean => (e as Error).message === 'TestException'); + + let executed = 0; + + try { + await policy.execute((): unknown => { + executed++; + throw new Error('ArgumentException'); + }); + expect.fail('did not throw'); + } catch (ex) { + expect((ex as Error).message).to.equal('ArgumentException'); + } + + expect(executed).to.equal(1); + }); + + it('should retry on a reactive exception thrice when setting retryCount to 3, then throw', async (): Promise< + void + > => { + const policy = new RetryPolicy(); + policy.reactOnException((e: unknown): boolean => (e as Error).message === 'TestException'); + policy.retryCount(3); + + let executed = 0; + + try { + await policy.execute((): unknown => { + executed++; + throw new Error('TestException'); + }); + expect.fail('did not throw'); + } catch (ex) { + expect((ex as Error).message).to.equal('TestException'); + } + + expect(executed).to.equal(4); + }); + + it('should retry on multiple reactive exceptions, then throw', async (): Promise => { + const policy = new RetryPolicy(); + policy.reactOnException((e: unknown): boolean => (e as Error).message === 'TestException'); + policy.reactOnException((e: unknown): boolean => (e as Error).message === 'ArgumentException'); + + let executed = 0; + + try { + await policy.execute((): unknown => { + executed++; + throw new Error('TestException'); + }); + expect.fail('did not throw'); + } catch (ex) { + expect((ex as Error).message).to.equal('TestException'); + } + + expect(executed).to.equal(2); + + try { + await policy.execute((): unknown => { + executed++; + throw new Error('ArgumentException'); + }); + expect.fail('did not throw'); + } catch (ex) { + expect((ex as Error).message).to.equal('ArgumentException'); + } + + expect(executed).to.equal(4); + }); + + it('should retry on a reactive result and on a reactive exception as well, then return/throw', async (): Promise< + void + > => { + const policy = new RetryPolicy(); + policy.reactOnResult((r: string): boolean => r === 'Diplomatiq is cool.'); + policy.reactOnException((e: unknown): boolean => (e as Error).message === 'TestException'); + + let executed = 0; + + await policy.execute((): string => { + executed++; + return 'Diplomatiq is cool.'; + }); + + expect(executed).to.equal(2); + + try { + await policy.execute((): string => { + executed++; + throw new Error('TestException'); + }); + expect.fail('did not throw'); + } catch (ex) { + expect((ex as Error).message).to.equal('TestException'); + } + + expect(executed).to.equal(4); + }); + + it('should not retry without a reactive result or exception to be handled', async (): Promise => { + const policy = new RetryPolicy(); + + let executed = 0; + + const result = await policy.execute((): string => { + executed++; + return 'Diplomatiq is cool.'; + }); + + expect(executed).to.equal(1); + expect(result).to.equal('Diplomatiq is cool.'); + }); + + it('should retry forever if set', async (): Promise => { + const policy = new RetryPolicy(); + policy.reactOnResult((r: string): boolean => r === 'Diplomatiq is cool.'); + policy.retryForever(); + + let executed = 0; + + await policy.execute((): string => { + executed++; + + if (executed < 10) { + return 'Diplomatiq is cool.'; + } + + return ''; + }); + + expect(executed).to.equal(10); + }); + + it('should run onRetryFn with result filled on retry, before the retried execution', async (): Promise => { + const policy = new RetryPolicy(); + + let executed = 0; + let onRetryExecuted = 0; + + policy.reactOnResult((r: string): boolean => r === 'Diplomatiq is cool.'); + policy.onRetry((result: string | undefined, error: unknown | undefined, currentRetryCount: number): void => { + expect(result).to.equal('Diplomatiq is cool.'); + expect(error).to.equal(undefined); + expect(currentRetryCount).to.equal(executed); + + onRetryExecuted++; + expect(onRetryExecuted).to.equal(currentRetryCount); + }); + + await policy.execute((): string => { + executed++; + return 'Diplomatiq is cool.'; + }); + + expect(executed).to.equal(2); + expect(onRetryExecuted).to.equal(1); + }); + + it('should run onRetryFn with result filled on retry, before the retried execution thrice when setting retryCount to 3', async (): Promise< + void + > => { + const policy = new RetryPolicy(); + + let executed = 0; + let onRetryExecuted = 0; + + policy.reactOnResult((r: string): boolean => r === 'Diplomatiq is cool.'); + policy.retryCount(3); + policy.onRetry((result: string | undefined, error: unknown | undefined, currentRetryCount: number): void => { + expect(result).to.equal('Diplomatiq is cool.'); + expect(error).to.equal(undefined); + expect(currentRetryCount).to.equal(executed); + + onRetryExecuted++; + expect(onRetryExecuted).to.equal(currentRetryCount); + }); + + await policy.execute((): string => { + executed++; + return 'Diplomatiq is cool.'; + }); + + expect(executed).to.equal(4); + expect(onRetryExecuted).to.equal(3); + }); + + it('should run onRetryFn with error filled on retry, before the retried execution', async (): Promise => { + const policy = new RetryPolicy(); + + let executed = 0; + let onRetryExecuted = 0; + + policy.reactOnException((e: unknown): boolean => (e as Error).message === 'TestException'); + policy.onRetry((result: unknown | undefined, error: unknown | undefined, currentRetryCount: number): void => { + expect(result).to.equal(undefined); + expect((error as Error).message).to.equal('TestException'); + expect(currentRetryCount).to.equal(executed); + + onRetryExecuted++; + expect(onRetryExecuted).to.equal(currentRetryCount); + }); + + try { + await policy.execute((): unknown => { + executed++; + throw new Error('TestException'); + }); + expect.fail('did not throw'); + } catch (ex) { + expect((ex as Error).message).to.equal('TestException'); + } + + expect(executed).to.equal(2); + expect(onRetryExecuted).to.equal(1); + }); + + it('should not run onRetryFn if not retried', async (): Promise => { + const policy = new RetryPolicy(); + + let executed = 0; + let onRetryExecuted = 0; + + policy.onRetry((): void => { + onRetryExecuted++; + }); + + await policy.execute((): void => { + executed++; + }); + + expect(executed).to.equal(1); + expect(onRetryExecuted).to.equal(0); + }); + + it('should run onRetryFn with error filled on retry, before the retried execution thrice when setting retryCount to 3', async (): Promise< + void + > => { + const policy = new RetryPolicy(); + + let executed = 0; + let onRetryExecuted = 0; + + policy.reactOnException((e: unknown): boolean => (e as Error).message === 'TestException'); + policy.retryCount(3); + policy.onRetry((result: unknown | undefined, error: unknown | undefined, currentRetryCount: number): void => { + expect(result).to.equal(undefined); + expect((error as Error).message).to.equal('TestException'); + expect(currentRetryCount).to.equal(executed); + + onRetryExecuted++; + expect(onRetryExecuted).to.equal(currentRetryCount); + }); + + try { + await policy.execute((): unknown => { + executed++; + throw new Error('TestException'); + }); + expect.fail('did not throw'); + } catch (ex) { + expect((ex as Error).message).to.equal('TestException'); + } + + expect(executed).to.equal(4); + expect(onRetryExecuted).to.equal(3); + }); + + it('should await an asynchronous onRetryFn before retrying', async (): Promise => { + const policy = new RetryPolicy(); + + let executed = 0; + let onRetryExecuted = 0; + + policy.reactOnResult((r: string): boolean => r === 'Diplomatiq is cool.'); + policy.onRetry( + async ( + result: string | undefined, + error: unknown | undefined, + currentRetryCount: number, + // eslint-disable-next-line @typescript-eslint/require-await + ): Promise => { + expect(result).to.equal('Diplomatiq is cool.'); + expect(error).to.equal(undefined); + expect(currentRetryCount).to.equal(executed); + + onRetryExecuted++; + expect(onRetryExecuted).to.equal(currentRetryCount); + }, + ); + + await policy.execute((): string => { + executed++; + return 'Diplomatiq is cool.'; + }); + + expect(executed).to.equal(2); + expect(onRetryExecuted).to.equal(1); + }); + + it('should run multiple onRetryFns sequentially on retry', async (): Promise => { + const policy = new RetryPolicy(); + + let executed = 0; + let onRetryExecuted = 0; + + policy.reactOnResult((r: string): boolean => r === 'Diplomatiq is cool.'); + policy.retryCount(3); + policy.onRetry((result: string | undefined, error: unknown | undefined, currentRetryCount: number): void => { + expect(result).to.equal('Diplomatiq is cool.'); + expect(error).to.equal(undefined); + expect(currentRetryCount).to.equal(executed); + + expect(onRetryExecuted).to.equal((currentRetryCount - 1) * 3); + onRetryExecuted++; + expect(onRetryExecuted).to.equal((currentRetryCount - 1) * 3 + 1); + }); + policy.onRetry((result: string | undefined, error: unknown | undefined, currentRetryCount: number): void => { + expect(result).to.equal('Diplomatiq is cool.'); + expect(error).to.equal(undefined); + expect(currentRetryCount).to.equal(executed); + + expect(onRetryExecuted).to.equal((currentRetryCount - 1) * 3 + 1); + onRetryExecuted++; + expect(onRetryExecuted).to.equal((currentRetryCount - 1) * 3 + 2); + }); + policy.onRetry((result: string | undefined, error: unknown | undefined, currentRetryCount: number): void => { + expect(result).to.equal('Diplomatiq is cool.'); + expect(error).to.equal(undefined); + expect(currentRetryCount).to.equal(executed); + + expect(onRetryExecuted).to.equal((currentRetryCount - 1) * 3 + 2); + onRetryExecuted++; + expect(onRetryExecuted).to.equal((currentRetryCount - 1) * 3 + 3); + }); + + await policy.execute((): string => { + executed++; + return 'Diplomatiq is cool.'; + }); + + expect(executed).to.equal(4); + expect(onRetryExecuted).to.equal(9); + }); + + it('should run multiple async onRetryFns sequentially on retry', async (): Promise => { + const policy = new RetryPolicy(); + + let executed = 0; + let onRetryExecuted = 0; + + policy.reactOnResult((r: string): boolean => r === 'Diplomatiq is cool.'); + policy.retryCount(3); + policy.onRetry( + async ( + result: string | undefined, + error: unknown | undefined, + currentRetryCount: number, + // eslint-disable-next-line @typescript-eslint/require-await + ): Promise => { + expect(result).to.equal('Diplomatiq is cool.'); + expect(error).to.equal(undefined); + expect(currentRetryCount).to.equal(executed); + + expect(onRetryExecuted).to.equal((currentRetryCount - 1) * 3); + onRetryExecuted++; + expect(onRetryExecuted).to.equal((currentRetryCount - 1) * 3 + 1); + }, + ); + policy.onRetry( + async ( + result: string | undefined, + error: unknown | undefined, + currentRetryCount: number, + // eslint-disable-next-line @typescript-eslint/require-await + ): Promise => { + expect(result).to.equal('Diplomatiq is cool.'); + expect(error).to.equal(undefined); + expect(currentRetryCount).to.equal(executed); + + expect(onRetryExecuted).to.equal((currentRetryCount - 1) * 3 + 1); + onRetryExecuted++; + expect(onRetryExecuted).to.equal((currentRetryCount - 1) * 3 + 2); + }, + ); + policy.onRetry( + async ( + result: string | undefined, + error: unknown | undefined, + currentRetryCount: number, + // eslint-disable-next-line @typescript-eslint/require-await + ): Promise => { + expect(result).to.equal('Diplomatiq is cool.'); + expect(error).to.equal(undefined); + expect(currentRetryCount).to.equal(executed); + + expect(onRetryExecuted).to.equal((currentRetryCount - 1) * 3 + 2); + onRetryExecuted++; + expect(onRetryExecuted).to.equal((currentRetryCount - 1) * 3 + 3); + }, + ); + + await policy.execute((): string => { + executed++; + return 'Diplomatiq is cool.'; + }); + + expect(executed).to.equal(4); + expect(onRetryExecuted).to.equal(9); + }); + + it('should run onFinallyFn after all execution and retries if retried', async (): Promise => { + const policy = new RetryPolicy(); + + let executed = 0; + let onRetryExecuted = 0; + let onFinallyExecuted = 0; + + policy.reactOnResult((r: string): boolean => r === 'Diplomatiq is cool.'); + policy.retryCount(3); + policy.onRetry((result: string | undefined, error: unknown | undefined, currentRetryCount: number): void => { + expect(result).to.equal('Diplomatiq is cool.'); + expect(error).to.equal(undefined); + expect(currentRetryCount).to.equal(executed); + + onRetryExecuted++; + expect(onRetryExecuted).to.equal(currentRetryCount); + }); + policy.onFinally((): void => { + onFinallyExecuted++; + + expect(executed).to.equal(4); + expect(onRetryExecuted).to.equal(3); + }); + + await policy.execute((): string => { + executed++; + return 'Diplomatiq is cool.'; + }); + + expect(executed).to.equal(4); + expect(onRetryExecuted).to.equal(3); + expect(onFinallyExecuted).to.equal(1); + }); + + it('should run onFinallyFn after the execution if not retried', async (): Promise => { + const policy = new RetryPolicy(); + + let executed = 0; + let onFinallyExecuted = 0; + + policy.reactOnResult((r: string): boolean => r === 'Diplomatiq is not cool.'); + policy.retryCount(3); + policy.onFinally((): void => { + onFinallyExecuted++; + + expect(executed).to.equal(1); + }); + + await policy.execute((): string => { + executed++; + return 'Diplomatiq is cool.'; + }); + + expect(executed).to.equal(1); + expect(onFinallyExecuted).to.equal(1); + }); + + it('should run multiple synchronous onFinallyFns sequentially', async (): Promise => { + const policy = new RetryPolicy(); + + let executed = 0; + let onFinallyExecuted = 0; + + policy.reactOnResult((r: string): boolean => r === 'Diplomatiq is cool.'); + policy.onFinally((): void => { + expect(onFinallyExecuted).to.equal(0); + onFinallyExecuted++; + expect(onFinallyExecuted).to.equal(1); + }); + policy.onFinally((): void => { + expect(onFinallyExecuted).to.equal(1); + onFinallyExecuted++; + expect(onFinallyExecuted).to.equal(2); + }); + policy.onFinally((): void => { + expect(onFinallyExecuted).to.equal(2); + onFinallyExecuted++; + expect(onFinallyExecuted).to.equal(3); + }); + + await policy.execute((): string => { + executed++; + return 'Diplomatiq is cool.'; + }); + + expect(executed).to.equal(2); + expect(onFinallyExecuted).to.equal(3); + }); + + it('should run multiple asynchronous onFinallyFns sequentially', async (): Promise => { + const policy = new RetryPolicy(); + + let executed = 0; + let onFinallyExecuted = 0; + + policy.reactOnResult((r: string): boolean => r === 'Diplomatiq is cool.'); + policy.onFinally( + // eslint-disable-next-line @typescript-eslint/require-await + async (): Promise => { + expect(onFinallyExecuted).to.equal(0); + onFinallyExecuted++; + expect(onFinallyExecuted).to.equal(1); + }, + ); + policy.onFinally( + // eslint-disable-next-line @typescript-eslint/require-await + async (): Promise => { + expect(onFinallyExecuted).to.equal(1); + onFinallyExecuted++; + expect(onFinallyExecuted).to.equal(2); + }, + ); + policy.onFinally( + // eslint-disable-next-line @typescript-eslint/require-await + async (): Promise => { + expect(onFinallyExecuted).to.equal(2); + onFinallyExecuted++; + expect(onFinallyExecuted).to.equal(3); + }, + ); + + await policy.execute((): string => { + executed++; + return 'Diplomatiq is cool.'; + }); + + expect(executed).to.equal(2); + expect(onFinallyExecuted).to.equal(3); + }); + + it('should run onFinallyFn once, regardless of retryCount', async (): Promise => { + const policy = new RetryPolicy(); + + let executed = 0; + let onFinallyExecuted = 0; + + policy.reactOnResult((r: string): boolean => r === 'Diplomatiq is cool.'); + policy.retryCount(3); + policy.onFinally((): void => { + onFinallyExecuted++; + }); + + await policy.execute((): string => { + executed++; + return 'Diplomatiq is cool.'; + }); + + expect(executed).to.equal(4); + expect(onFinallyExecuted).to.equal(1); + }); + + it('should wait for the specified interval before retry on result if set', async (): Promise => { + const policy = new RetryPolicy(); + policy.reactOnResult((r: string): boolean => r === 'Diplomatiq is cool.'); + policy.retryCount(3); + policy.waitBeforeRetry((): number => 1000); + + let executed = 0; + + const executionPromise = policy.execute((): string => { + expect(Date.now()).to.equal(executed * 1000); + executed++; + return 'Diplomatiq is cool.'; + }); + + expect(executed).to.equal(1); + + // eslint-disable-next-line @typescript-eslint/ban-ts-ignore + // @ts-ignore + await clock.tickAsync(1000); + expect(Date.now()).to.equal(1000); + expect(executed).to.equal(2); + + // eslint-disable-next-line @typescript-eslint/ban-ts-ignore + // @ts-ignore + await clock.tickAsync(1000); + expect(Date.now()).to.equal(2000); + expect(executed).to.equal(3); + + // eslint-disable-next-line @typescript-eslint/ban-ts-ignore + // @ts-ignore + await clock.tickAsync(1000); + expect(Date.now()).to.equal(3000); + expect(executed).to.equal(4); + + await executionPromise; + expect(executed).to.equal(4); + }); + + it('should wait for the specified interval (depending on the current retry count) before retry on result if set', async (): Promise< + void + > => { + const elapsedTimeHelper = (executed: number): number => { + return new Array(executed) + .fill(undefined) + .map((_value, index): number => (index + 1) * 1000) + .reduce((acc, curr): number => acc + curr, 0); + }; + + const policy = new RetryPolicy(); + policy.reactOnResult((r: string): boolean => r === 'Diplomatiq is cool.'); + policy.retryCount(3); + policy.waitBeforeRetry((currentRetryCount: number): number => currentRetryCount * 1000); + + let executed = 0; + + const executionPromise = policy.execute((): string => { + expect(Date.now()).to.equal(elapsedTimeHelper(executed)); + executed++; + return 'Diplomatiq is cool.'; + }); + + expect(executed).to.equal(1); + + // eslint-disable-next-line @typescript-eslint/ban-ts-ignore + // @ts-ignore + await clock.tickAsync(1000); + expect(Date.now()).to.equal(1000); + expect(executed).to.equal(2); + + // eslint-disable-next-line @typescript-eslint/ban-ts-ignore + // @ts-ignore + await clock.tickAsync(2000); + expect(Date.now()).to.equal(3000); + expect(executed).to.equal(3); + + // eslint-disable-next-line @typescript-eslint/ban-ts-ignore + // @ts-ignore + await clock.tickAsync(3000); + expect(Date.now()).to.equal(6000); + expect(executed).to.equal(4); + + await executionPromise; + expect(executed).to.equal(4); + }); + + it('should wait for the specified interval before retry on exception if set', async (): Promise => { + const policy = new RetryPolicy(); + policy.reactOnException((e: unknown): boolean => (e as Error).message === 'TestException'); + policy.retryCount(3); + policy.waitBeforeRetry((): number => 1000); + + let executed = 0; + + const executionPromise = policy + .execute((): string => { + expect(Date.now()).to.equal(executed * 1000); + executed++; + throw new Error('TestException'); + }) + .catch((ex: Error): void => { + expect(ex.message).to.equal('TestException'); + }); + + expect(executed).to.equal(1); + + // eslint-disable-next-line @typescript-eslint/ban-ts-ignore + // @ts-ignore + await clock.tickAsync(1000); + expect(Date.now()).to.equal(1000); + expect(executed).to.equal(2); + + // eslint-disable-next-line @typescript-eslint/ban-ts-ignore + // @ts-ignore + await clock.tickAsync(1000); + expect(Date.now()).to.equal(2000); + expect(executed).to.equal(3); + + // eslint-disable-next-line @typescript-eslint/ban-ts-ignore + // @ts-ignore + await clock.tickAsync(1000); + expect(Date.now()).to.equal(3000); + expect(executed).to.equal(4); + + await executionPromise; + expect(executed).to.equal(4); + }); + + it('should wait for the specified interval (depending on the current retry count) before retry on exception if set', async (): Promise< + void + > => { + const elapsedTimeHelper = (executed: number): number => { + return new Array(executed) + .fill(undefined) + .map((_value, index): number => (index + 1) * 1000) + .reduce((acc, curr): number => acc + curr, 0); + }; + + const policy = new RetryPolicy(); + policy.reactOnException((e: unknown): boolean => (e as Error).message === 'TestException'); + policy.retryCount(3); + policy.waitBeforeRetry((currentRetryCount: number): number => currentRetryCount * 1000); + + let executed = 0; + + const executionPromise = policy + .execute((): string => { + expect(Date.now()).to.equal(elapsedTimeHelper(executed)); + executed++; + throw new Error('TestException'); + }) + .catch((ex: Error): void => { + expect(ex.message).to.equal('TestException'); + }); + + expect(executed).to.equal(1); + + // eslint-disable-next-line @typescript-eslint/ban-ts-ignore + // @ts-ignore + await clock.tickAsync(1000); + expect(Date.now()).to.equal(1000); + expect(executed).to.equal(2); + + // eslint-disable-next-line @typescript-eslint/ban-ts-ignore + // @ts-ignore + await clock.tickAsync(2000); + expect(Date.now()).to.equal(3000); + expect(executed).to.equal(3); + + // eslint-disable-next-line @typescript-eslint/ban-ts-ignore + // @ts-ignore + await clock.tickAsync(3000); + expect(Date.now()).to.equal(6000); + expect(executed).to.equal(4); + + await executionPromise; + expect(executed).to.equal(4); + }); + + it('should not allow to set retryCount during execution', (): void => { + const policy = new RetryPolicy(); + policy.execute( + async (): Promise => { + await new Promise((): void => { + // will not resolve + }); + }, + ); + + try { + policy.retryCount(2); + expect.fail('did not throw'); + } catch (ex) { + expect(ex instanceof PolicyModificationNotAllowedException).to.be.true; + } + }); + + it('should not allow to set retryForever during execution', (): void => { + const policy = new RetryPolicy(); + policy.execute( + async (): Promise => { + await new Promise((): void => { + // will not resolve + }); + }, + ); + + try { + policy.retryForever(); + expect.fail('did not throw'); + } catch (ex) { + expect(ex instanceof PolicyModificationNotAllowedException).to.be.true; + } + }); + + it('should not allow to add onRetryFns during execution', (): void => { + const policy = new RetryPolicy(); + policy.execute( + async (): Promise => { + await new Promise((): void => { + // will not resolve + }); + }, + ); + + try { + policy.onRetry((): void => { + // empty + }); + expect.fail('did not throw'); + } catch (ex) { + expect(ex instanceof PolicyModificationNotAllowedException).to.be.true; + } + }); + + it('should not allow to set waitBeforeRetry during execution', (): void => { + const policy = new RetryPolicy(); + policy.execute( + async (): Promise => { + await new Promise((): void => { + // will not resolve + }); + }, + ); + + try { + policy.waitBeforeRetry((): number => 100); + expect.fail('did not throw'); + } catch (ex) { + expect(ex instanceof PolicyModificationNotAllowedException).to.be.true; + } + }); + + it('should not allow to add onFinallyFns during execution', (): void => { + const policy = new RetryPolicy(); + policy.execute( + async (): Promise => { + await new Promise((): void => { + // will not resolve + }); + }, + ); + + try { + policy.onFinally((): void => { + // empty + }); + expect.fail('did not throw'); + } catch (ex) { + expect(ex instanceof PolicyModificationNotAllowedException).to.be.true; + } + }); + + it("should be properly mutex'd for running an instance multiple times simultaneously", async (): Promise => { + const policy = new RetryPolicy(); + + const attemptPolicyModification = (expectFailure: boolean): void => { + try { + policy.retryCount(1); + if (expectFailure) { + expect.fail('did not throw'); + } + } catch (ex) { + expect(ex instanceof PolicyModificationNotAllowedException).to.be.true; + } + + try { + policy.retryForever(); + if (expectFailure) { + expect.fail('did not throw'); + } + } catch (ex) { + expect(ex instanceof PolicyModificationNotAllowedException).to.be.true; + } + + try { + policy.onRetry((): void => { + // empty + }); + if (expectFailure) { + expect.fail('did not throw'); + } + } catch (ex) { + expect(ex instanceof PolicyModificationNotAllowedException).to.be.true; + } + + try { + policy.waitBeforeRetry((): number => 0); + if (expectFailure) { + expect.fail('did not throw'); + } + } catch (ex) { + expect(ex instanceof PolicyModificationNotAllowedException).to.be.true; + } + + try { + policy.onFinally((): void => { + // empty + }); + if (expectFailure) { + expect.fail('did not throw'); + } + } catch (ex) { + expect(ex instanceof PolicyModificationNotAllowedException).to.be.true; + } + }; + + const executionResolverAddedDeferreds: Array<{ + resolverAddedPromise: Promise; + resolverAddedResolver: () => void; + }> = new Array(100).fill(undefined).map((): { + resolverAddedPromise: Promise; + resolverAddedResolver: () => void; + } => { + let resolverAddedResolver!: () => void; + const resolverAddedPromise = new Promise((resolve): void => { + resolverAddedResolver = resolve; + }); + + return { + resolverAddedPromise, + resolverAddedResolver, + }; + }); + + const executionResolvers: Array<() => void> = []; + + attemptPolicyModification(false); + + for (let i = 0; i < 100; i++) { + policy.execute( + // eslint-disable-next-line no-loop-func + async (): Promise => { + await new Promise((resolve): void => { + executionResolvers.push(resolve); + executionResolverAddedDeferreds[i].resolverAddedResolver(); + }); + }, + ); + + await executionResolverAddedDeferreds[i].resolverAddedPromise; + expect(executionResolvers.length).to.equal(i + 1); + + attemptPolicyModification(true); + } + + for (let i = 0; i < 100; i++) { + executionResolvers[i](); + attemptPolicyModification(true); + } + + attemptPolicyModification(false); + }); + + it('should throw error when setting retry count to 0', (): void => { + const policy = new RetryPolicy(); + try { + policy.retryCount(0); + expect.fail('did not throw'); + } catch (ex) { + expect((ex as Error).message).to.equal('retryCount must be greater than 0'); + } + }); + + it('should throw error when setting retry count to <0', (): void => { + const policy = new RetryPolicy(); + try { + policy.retryCount(-1); + expect.fail('did not throw'); + } catch (ex) { + expect((ex as Error).message).to.equal('retryCount must be greater than 0'); + } + }); + + it('should throw error when setting retry count to a non-integer', (): void => { + const policy = new RetryPolicy(); + try { + policy.retryCount(0.1); + expect.fail('did not throw'); + } catch (ex) { + expect((ex as Error).message).to.equal('retryCount must be integer'); + } + }); + + it('should throw error when setting retry count to a non-safe integer', (): void => { + const policy = new RetryPolicy(); + try { + policy.retryCount(2 ** 53); + expect.fail('did not throw'); + } catch (ex) { + expect((ex as Error).message).to.equal('retryCount must be less than or equal to 2^53 - 1'); + } + }); +}); diff --git a/test/specs/timeoutPolicy.test.ts b/test/specs/timeoutPolicy.test.ts new file mode 100644 index 0000000..58d51a3 --- /dev/null +++ b/test/specs/timeoutPolicy.test.ts @@ -0,0 +1,362 @@ +import { expect } from 'chai'; +import { TimeoutException } from '../../src/policies/proactive/timeoutPolicy/timeoutException'; +import { TimeoutPolicy } from '../../src/policies/proactive/timeoutPolicy/timeoutPolicy'; +import { PolicyModificationNotAllowedException } from '../../src/types/policyModificationNotAllowedException'; + +describe('TimeoutPolicy', (): void => { + it('should run the execution callback and return its result if no timeout is set', async (): Promise => { + const policy = new TimeoutPolicy(); + const result = await policy.execute( + // eslint-disable-next-line @typescript-eslint/require-await + async (): Promise => { + return 'Diplomatiq is cool.'; + }, + ); + + expect(result).to.equal('Diplomatiq is cool.'); + }); + + it('should run the execution callback and throw its exceptions if no timeout is set', async (): Promise => { + const policy = new TimeoutPolicy(); + + try { + await policy.execute( + // eslint-disable-next-line @typescript-eslint/require-await + async (): Promise => { + throw new Error('TestException'); + }, + ); + expect.fail('did not throw'); + } catch (ex) { + expect((ex as Error).message).to.equal('TestException'); + } + }); + + it('should throw a TimeoutException if the execution takes more than the specified time', async (): Promise< + void + > => { + const policy = new TimeoutPolicy(); + policy.timeoutAfter(10); + + try { + await policy.execute( + async (): Promise => { + return new Promise((resolve): void => { + setTimeout(resolve, 20); + }); + }, + ); + } catch (ex) { + expect(ex instanceof TimeoutException).to.be.true; + } + }); + + it('should not throw a TimeoutException if the execution takes less than the specified time', async (): Promise< + void + > => { + const policy = new TimeoutPolicy(); + policy.timeoutAfter(20); + await policy.execute( + async (): Promise => { + return new Promise((resolve): void => { + setTimeout(resolve, 10); + }); + }, + ); + }); + + it('should run onTimeoutFn on timeout with timedOutAfter filled out', async (): Promise => { + const policy = new TimeoutPolicy(); + policy.timeoutAfter(10); + policy.onTimeout((timedOutAfter): void => { + expect(timedOutAfter).to.equal(10); + }); + + try { + await policy.execute( + async (): Promise => { + return new Promise((resolve): void => { + setTimeout(resolve, 20); + }); + }, + ); + } catch (ex) { + expect(ex instanceof TimeoutException).to.be.true; + } + }); + + it('should run multiple onTimeoutFns sequentially on timeout', async (): Promise => { + const policy = new TimeoutPolicy(); + policy.timeoutAfter(10); + + let onTimeoutCounter = 0; + policy.onTimeout((): void => { + expect(onTimeoutCounter).to.equal(0); + onTimeoutCounter++; + }); + policy.onTimeout((): void => { + expect(onTimeoutCounter).to.equal(1); + onTimeoutCounter++; + }); + policy.onTimeout((): void => { + expect(onTimeoutCounter).to.equal(2); + onTimeoutCounter++; + }); + + try { + await policy.execute( + async (): Promise => { + // empty + }, + ); + return new Promise((resolve): void => { + setTimeout(resolve, 20); + }); + } catch (ex) { + expect(ex instanceof TimeoutException).to.be.true; + } + }); + + it('should run multiple async onTimeoutFns sequentially on timeout', async (): Promise => { + const policy = new TimeoutPolicy(); + policy.timeoutAfter(10); + + let onTimeoutCounter = 0; + policy.onTimeout( + // eslint-disable-next-line @typescript-eslint/require-await + async (): Promise => { + expect(onTimeoutCounter).to.equal(0); + onTimeoutCounter++; + }, + ); + policy.onTimeout( + // eslint-disable-next-line @typescript-eslint/require-await + async (): Promise => { + expect(onTimeoutCounter).to.equal(1); + onTimeoutCounter++; + }, + ); + policy.onTimeout( + // eslint-disable-next-line @typescript-eslint/require-await + async (): Promise => { + expect(onTimeoutCounter).to.equal(2); + onTimeoutCounter++; + }, + ); + + try { + await policy.execute( + async (): Promise => { + return new Promise((resolve): void => { + setTimeout(resolve, 20); + }); + }, + ); + } catch (ex) { + expect(ex instanceof TimeoutException).to.be.true; + } + }); + + it('should not run onTimeoutFns if the execution callback throws a TimeoutException (if timeout is not set)', async (): Promise< + void + > => { + const policy = new TimeoutPolicy(); + + let onTimeoutCounter = 0; + policy.onTimeout((): void => { + onTimeoutCounter++; + }); + + try { + await policy.execute( + // eslint-disable-next-line @typescript-eslint/require-await + async (): Promise => { + throw new TimeoutException(1); + }, + ); + } catch (ex) { + expect(ex instanceof TimeoutException).to.be.true; + } + + expect(onTimeoutCounter).to.equal(0); + }); + + it('should not run onTimeoutFns if the execution callback throws a TimeoutException (if timeout is set)', async (): Promise< + void + > => { + const policy = new TimeoutPolicy(); + policy.timeoutAfter(1000); + + let onTimeoutCounter = 0; + policy.onTimeout((): void => { + onTimeoutCounter++; + }); + + try { + await policy.execute( + // eslint-disable-next-line @typescript-eslint/require-await + async (): Promise => { + throw new TimeoutException(1); + }, + ); + } catch (ex) { + expect(ex instanceof TimeoutException).to.be.true; + } + + expect(onTimeoutCounter).to.equal(0); + }); + + it('should not allow to set timeoutAfter during execution', (): void => { + const policy = new TimeoutPolicy(); + policy.execute( + async (): Promise => { + await new Promise((): void => { + // will not resolve + }); + }, + ); + + try { + policy.timeoutAfter(10); + expect.fail('did not throw'); + } catch (ex) { + expect(ex instanceof PolicyModificationNotAllowedException).to.be.true; + } + }); + + it('should not allow to add onTimeoutFns during execution', (): void => { + const policy = new TimeoutPolicy(); + policy.execute( + async (): Promise => { + await new Promise((): void => { + // will not resolve + }); + }, + ); + + try { + policy.onTimeout((): void => { + // empty + }); + expect.fail('did not throw'); + } catch (ex) { + expect(ex instanceof PolicyModificationNotAllowedException).to.be.true; + } + }); + + it("should be properly mutex'd for running an instance multiple times simultaneously", async (): Promise => { + const policy = new TimeoutPolicy(); + + const attemptPolicyModification = (expectFailure: boolean): void => { + try { + policy.timeoutAfter(1000); + if (expectFailure) { + expect.fail('did not throw'); + } + } catch (ex) { + expect(ex instanceof PolicyModificationNotAllowedException).to.be.true; + } + + try { + policy.onTimeout((): void => { + // empty + }); + if (expectFailure) { + expect.fail('did not throw'); + } + } catch (ex) { + expect(ex instanceof PolicyModificationNotAllowedException).to.be.true; + } + }; + + const executionResolverAddedDeferreds: Array<{ + resolverAddedPromise: Promise; + resolverAddedResolver: () => void; + }> = new Array(100).fill(undefined).map((): { + resolverAddedPromise: Promise; + resolverAddedResolver: () => void; + } => { + let resolverAddedResolver!: () => void; + const resolverAddedPromise = new Promise((resolve): void => { + resolverAddedResolver = resolve; + }); + + return { + resolverAddedPromise, + resolverAddedResolver, + }; + }); + + const executionResolvers: Array<() => void> = []; + + attemptPolicyModification(false); + + for (let i = 0; i < 100; i++) { + policy.execute( + // eslint-disable-next-line no-loop-func + async (): Promise => { + await new Promise((resolve): void => { + executionResolvers.push(resolve); + executionResolverAddedDeferreds[i].resolverAddedResolver(); + }); + }, + ); + + await executionResolverAddedDeferreds[i].resolverAddedPromise; + expect(executionResolvers.length).to.equal(i + 1); + + attemptPolicyModification(true); + } + + for (let i = 0; i < 100; i++) { + executionResolvers[i](); + attemptPolicyModification(true); + } + + attemptPolicyModification(false); + }); + + it('should throw error when setting timeoutAfter to 0', (): void => { + const policy = new TimeoutPolicy(); + + try { + policy.timeoutAfter(0); + expect.fail('did not throw'); + } catch (ex) { + expect((ex as Error).message).to.equal('timeoutMs must be greater than 0'); + } + }); + + it('should throw error when setting timeoutAfter to <0', (): void => { + const policy = new TimeoutPolicy(); + + try { + policy.timeoutAfter(-1); + expect.fail('did not throw'); + } catch (ex) { + expect((ex as Error).message).to.equal('timeoutMs must be greater than 0'); + } + }); + + it('should throw error when setting timeoutAfter to a non-integer', (): void => { + const policy = new TimeoutPolicy(); + + try { + policy.timeoutAfter(0.1); + expect.fail('did not throw'); + } catch (ex) { + expect((ex as Error).message).to.equal('timeoutMs must be integer'); + } + }); + + it('should throw error when setting timeoutAfter to a non-safe integer', (): void => { + const policy = new TimeoutPolicy(); + + try { + policy.timeoutAfter(2 ** 53); + expect.fail('did not throw'); + } catch (ex) { + expect((ex as Error).message).to.equal('timeoutMs must be less than or equal to 2^53 - 1'); + } + }); +}); diff --git a/test/utils/nodeJsEntropyProvider.ts b/test/utils/nodeJsEntropyProvider.ts new file mode 100644 index 0000000..eced153 --- /dev/null +++ b/test/utils/nodeJsEntropyProvider.ts @@ -0,0 +1,16 @@ +import { EntropyProvider, UnsignedTypedArray } from '@diplomatiq/crypto-random'; +import { randomFill } from 'crypto'; + +export class NodeJsEntropyProvider implements EntropyProvider { + public async getRandomValues(array: T): Promise { + return new Promise((resolve, reject): void => { + randomFill(array, (error: Error | null, array: T): void => { + if (error !== null) { + reject(error); + return; + } + resolve(array); + }); + }); + } +} diff --git a/test/utils/windowMock.ts b/test/utils/windowMock.ts new file mode 100644 index 0000000..618ac8c --- /dev/null +++ b/test/utils/windowMock.ts @@ -0,0 +1,11 @@ +import { randomFillSync } from 'crypto'; + +export const windowMock = (): { + crypto: { + getRandomValues: (array: Uint8Array) => Uint8Array; + }; +} => ({ + crypto: { + getRandomValues: (array: Uint8Array): Uint8Array => randomFillSync(array), + }, +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..49cd075 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "declaration": true, + "declarationDir": "dist", + "module": "esnext", + "moduleResolution": "node", + "noErrorTruncation": true, + "noFallthroughCasesInSwitch": true, + "noImplicitReturns": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "outDir": "dist", + "rootDir": "src", + "strict": true, + "target": "esnext" + }, + "files": ["src/main.ts"] +} diff --git a/tsconfig.test.json b/tsconfig.test.json new file mode 100644 index 0000000..77b1230 --- /dev/null +++ b/tsconfig.test.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "declaration": true, + "declarationDir": "dist", + "module": "commonjs", + "moduleResolution": "node", + "noErrorTruncation": true, + "noFallthroughCasesInSwitch": true, + "noImplicitReturns": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "outDir": "dist", + "rootDir": "src", + "strict": true, + "target": "es2017" + }, + "include": ["test/**/*.ts"] +}