diff --git a/.coveralls.yml b/.coveralls.yml index 6e64999..9160059 100644 --- a/.coveralls.yml +++ b/.coveralls.yml @@ -1 +1 @@ -service_name: travis-ci \ No newline at end of file +service_name: travis-ci diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..e69de29 diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..c925c21 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,2 @@ +/dist +/node_modules diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..96ddf74 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,18 @@ +{ + "parser": "@typescript-eslint/parser", + "extends": ["plugin:@typescript-eslint/recommended", "prettier"], + "rules": { + "@typescript-eslint/no-explicit-any": [0], + "@typescript-eslint/no-use-before-define": [0], + "@typescript-eslint/no-inferrable-types": [0], + "@typescript-eslint/array-type": [0], + "@typescript-eslint/explicit-function-return-type": [ + 1, + { + "allowExpressions": true, + "allowTypedFunctionExpressions": true + } + ] + }, + "plugins": ["@typescript-eslint", "prettier"] +} diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..2fa8218 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,8 @@ +version: 2 +updates: + - package-ecosystem: npm + directory: "/" + schedule: + interval: daily + time: "13:00" + open-pull-requests-limit: 10 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..c704778 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,35 @@ +name: Continuous Integration + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + test: + name: Test with Node.js v${{ matrix.node }} and ${{ matrix.os }} + runs-on: "ubuntu-latest" + container: "node:${{matrix.node}}" + strategy: + matrix: + node: ["12", "14", "16"] + services: + redis-single-instance: + image: redis + redis-multi-instance-a: + image: redis + redis-multi-instance-b: + image: redis + redis-multi-instance-c: + image: redis + steps: + - uses: actions/checkout@v2 + - name: Install dependancies + run: yarn install --frozen-lockfile + - name: Lint the source + run: yarn lint + - name: Transpile into dist + run: yarn build + - name: Run tests + run: yarn test diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 0000000..05dfd83 --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,29 @@ +name: "CodeQL" + +on: + push: + branches: [main] + pull_request: + branches: [main] + schedule: + - cron: "43 22 * * 6" + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + language: ["javascript"] + steps: + - name: Checkout repository + uses: actions/checkout@v2 + - name: Initialize CodeQL + uses: github/codeql-action/init@v1 + with: + languages: ${{ matrix.language }} + - name: Autobuild + uses: github/codeql-action/autobuild@v1 + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v1 diff --git a/.gitignore b/.gitignore index d9f6318..7eeb4ad 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,11 @@ -# Logs -logs -*.log - -# Runtime data -pids -*.pid -*.seed +.DS_Store +*.sublime-workspace +/dist +/node_modules +/npm-debug.log +/package-lock.json +/yarn-error.log +/yarn.lock # Redis database *.rdb @@ -14,20 +14,4 @@ pids lib-cov # Coverage directory used by tools like istanbul -coverage - -# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (http://nodejs.org/api/addons.html) -build/Release - -# Dependency directory -# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git -node_modules - -# OS Files -.DS_Store +coverage \ No newline at end of file diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..43c97e7 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +package-lock=false diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..c925c21 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,2 @@ +/dist +/node_modules diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..2fdd754 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,8 @@ +{ + "overrides": [ + { + "files": ".ts", + "options": { "parser": "typescript" } + } + ] +} diff --git a/.travis.yml b/.travis.yml index 0c5849c..2bd2c57 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,4 +5,4 @@ node_js: - "10" services: - redis-server -script: npm run test-ci \ No newline at end of file +script: npm run test-ci diff --git a/CHANGELOG.md b/CHANGELOG.md index a33840d..f03170a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,3 +14,10 @@ - Stop testing on node version 8. (Due to dev dependency requirements only.) - Update docs (@ricmatsui via [#80](https://github.com/mike-marcacci/node-redlock/pull/80)). - Use evalsha for scripts (@yosiat via [#77](https://github.com/mike-marcacci/node-redlock/pull/77)). + +## v5.0.0-alpha1 + +- Complete rewrite using TypeScript. +- **BREAKING** Significant API changes; see [README.md](./README.md) +- **BREAKING** Remove all production dependencies (replacing Bluebird with native promises). +- **BREAKING** Drop support for Node < 12 diff --git a/README.md b/README.md index 2069602..5d295f2 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,26 @@ -[![npm version](https://badge.fury.io/js/redlock.svg)](https://www.npmjs.com/package/redlock) -[![Build Status](https://travis-ci.org/mike-marcacci/node-redlock.svg)](https://travis-ci.org/mike-marcacci/node-redlock) -[![Coverage Status](https://coveralls.io/repos/mike-marcacci/node-redlock/badge.svg)](https://coveralls.io/r/mike-marcacci/node-redlock) +[![Continuous Integration](https://github.com/mike-marcacci/node-redlock/workflows/Continuous%20Integration/badge.svg)](https://github.com/mike-marcacci/node-redlock/actions/workflows/ci.yml) +[![Current Version](https://badgen.net/npm/v/redlock)](https://npm.im/redlock) +[![Supported Node.js Versions](https://badgen.net/npm/node/redlock)](https://npm.im/redlock) + +# Redlock -Redlock -======= This is a node.js implementation of the [redlock](http://redis.io/topics/distlock) algorithm for distributed redis locks. It provides strong guarantees in both single-redis and multi-redis environments, and provides fault tolerance through use of multiple independent redis instances or clusters. - [Installation](#installation) -- [Usage (Promise Style)](#usage-promise-style) -- [Usage (Disposer Style)](#usage-disposer-style) -- [Usage (Callback Style)](#usage-callback-style) -- [Locking multiple resources](#locking-multiple-resources) -- [API Docs](#api-docs) +- [Usage](#usage) ### High-Availability Recommendations + - Use at least 3 independent servers or clusters -- Use an odd number of independent redis ***servers*** for most installations -- Use an odd number of independent redis ***clusters*** for massive installations +- Use an odd number of independent redis **_servers_** for most installations +- Use an odd number of independent redis **_clusters_** for massive installations - When possible, distribute redis nodes across different physical machines - ### Using Cluster/Sentinel -***Please make sure to use a client with built-in cluster support, such as [ioredis](https://github.com/luin/ioredis).*** +**_Please make sure to use a client with built-in cluster support, such as [ioredis](https://github.com/luin/ioredis)._** -It is completely possible to use a *single* redis cluster or sentinal configuration by passing one preconfigured client to redlock. While you do gain high availability and vastly increased throughput under this scheme, the failure modes are a bit different, and it becomes theoretically possible that a lock is acquired twice: +It is completely possible to use a _single_ redis cluster or sentinal configuration by passing one preconfigured client to redlock. While you do gain high availability and vastly increased throughput under this scheme, the failure modes are a bit different, and it becomes theoretically possible that a lock is acquired twice: Assume you are using eventually-consistent redis replication, and you acquire a lock for a resource. Immediately after acquiring your lock, the redis master for that shard crashes. Redis does its thing and fails over to the slave which hasn't yet synced your lock. If another process attempts to acquire a lock for the same resource, it will succeed! @@ -32,362 +28,120 @@ This is why redlock allows you to specify multiple independent nodes/clusters: b To learn more about the the algorithm, check out the [redis distlock page](http://redis.io/topics/distlock). - ### How do I check if something is locked? -Redlock cannot tell you *with certainty* if a resource is currently locked. For example, if you are on the smaller side of a network partition you will fail to acquire a lock, but you don't know if the lock exists on the other side; all you know is that you can't guarantee exclusivity on yours. -That said, for many tasks it's sufficient to attempt a lock with `retryCount=0`, and treat a failure as the resource being "locked" or (more correctly) "unavailable", +The purpose of redlock is to provide exclusivity guarantees on a resource over a duration of time, and is not designed to report the ownership status of a resource. For example, if you are on the smaller side of a network partition you will fail to acquire a lock, but you don't know if the lock exists on the other side; all you know is that you can't guarantee exclusivity on yours. This is further complicated by retry behavior, and even moreso when acquiring a lock on more than one resource. -With `retryCount=-1` there will be unlimited retries until the lock is aquired. +That said, for many tasks it's sufficient to attempt a lock with `retryCount=0`, and treat a failure as the resource being "locked" or (more correctly) "unavailable". +Note that with `retryCount=-1` there will be unlimited retries until the lock is aquired. + +## Installation -Installation ------------- ```bash npm install --save redlock ``` -Configuration -------------- -Redlock can use [node redis](https://github.com/mranney/node_redis), [ioredis](https://github.com/luin/ioredis) or any other compatible redis library to keep its client connections. +## Configuration + +Redlock is designed to use [ioredis](https://github.com/luin/ioredis) to keep its client connections and handle the cluster protocols. A redlock object is instantiated with an array of at least one redis client and an optional `options` object. Properties of the Redlock object should NOT be changed after it is first used, as doing so could have unintended consequences for live locks. -```js -var client1 = require('redis').createClient(6379, 'redis1.example.com'); -var client2 = require('redis').createClient(6379, 'redis2.example.com'); -var client3 = require('redis').createClient(6379, 'redis3.example.com'); -var Redlock = require('redlock'); - -var redlock = new Redlock( - // you should have one client for each independent redis node - // or cluster - [client1, client2, client3], - { - // the expected clock drift; for more details - // see http://redis.io/topics/distlock - driftFactor: 0.01, // multiplied by lock ttl to determine drift time - - // the max number of times Redlock will attempt - // to lock a resource before erroring - retryCount: 10, - - // the time in ms between attempts - retryDelay: 200, // time in ms - - // the max time in ms randomly added to retries - // to improve performance under high contention - // see https://www.awsarchitectureblog.com/2015/03/backoff.html - retryJitter: 200 // time in ms - } +```ts +import Client from "ioredis"; +import Redlock from "./redlock"; + +const redisA = new Client({ host: "a.redis.example.com" }); +const redisB = new Client({ host: "b.redis.example.com" }); +const redisC = new Client({ host: "c.redis.example.com" }); + +const redlock = new Redlock( + // You should have one client for each independent redis node + // or cluster. + [redisA, redisB, redisC], + { + // The expected clock drift; for more details see: + // http://redis.io/topics/distlock + driftFactor: 0.01, // multiplied by lock ttl to determine drift time + + // The max number of times Redlock will attempt to lock a resource + // before erroring. + retryCount: 10, + + // the time in ms between attempts + retryDelay: 200, // time in ms + + // the max time in ms randomly added to retries + // to improve performance under high contention + // see https://www.awsarchitectureblog.com/2015/03/backoff.html + retryJitter: 200, // time in ms + + // The minimum remaining time on a lock before an extension is automatically + // attempted with the `using` API. + automaticExtensionThreshold: 500, // time in ms + } ); ``` +## Error Handling -Error Handling --------------- - -Because redlock is designed for high availability, it does not care if a minority of redis instances/clusters fail at an operation. If you want to write logs or take another action when a redis client fails, you can listen for the `clientError` event: +Because redlock is designed for high availability, it does not care if a minority of redis instances/clusters fail at an operation. -```js +However, it can be helpful to monitor and log such cases. Redlock emits an "error" event whenever it encounters an error, even if the error is ignored in its normal operation. -// ... +```ts +redlock.on("error", (error) => { + // Ignore cases where a resource is explicitly marked as locked on a client. + if (error instanceof ResourceLockedError) { + return; + } -redlock.on('clientError', function(err) { - console.error('A redis error has occurred:', err); + // Log all other errors. + console.error(error); }); - -// ... - ``` +Additionally, a per-attempt and per-client stats (including errors) are made available on the `attempt` propert of both `Lock` and `ExecutionError` classes. -Usage (promise style) ---------------------- - - -### Locking & Unlocking - -```js - -// the string identifier for the resource you want to lock -var resource = 'locks:account:322456'; +## Usage -// the maximum amount of time you want the resource locked in milliseconds, -// keeping in mind that you can extend the lock up until -// the point when it expires -var ttl = 1000; +The `using` method wraps and executes a routine in the context of an auto-extending lock, returning a promise of the routine's value. In the case that auto-extension fails, an AbortSignal will be updated to indicate that abortion of the routine is in order, and to pass along the encountered error. -redlock.lock(resource, ttl).then(function(lock) { +```ts +await redlock.using([senderId, recipientId], 5000, async (signal) => { + // Do something... + await something(); - // ...do something here... + // Make sure any necessary lock extension has not failed. + if (signal.aborted) { + throw signal.error; + } - // unlock your resource when you are done - return lock.unlock() - .catch(function(err) { - // we weren't able to reach redis; your lock will eventually - // expire, but you probably want to log this error - console.error(err); - }); + // Do something else... + await somethingElse(); }); - ``` +Alternatively, locks can be acquired and released directly: -### Locking and Extending - -```js -redlock.lock('locks:account:322456', 1000).then(function(lock) { +```ts +// Acquire a lock. +let lock = await redlock.acquire(["a"], 5000); - // ...do something here... +// Do something... +await something(); - // if you need more time, you can continue to extend - // the lock as long as you never let it expire +// Extend the lock. +lock = await lock.extend(5000); - // this will extend the lock so that it expires - // approximitely 1s from when `extend` is called - return lock.extend(1000).then(function(lock){ - - // ...do something here... - - // unlock your resource when you are done - return lock.unlock() - .catch(function(err) { - // we weren't able to reach redis; your lock will eventually - // expire, but you probably want to log this error - console.error(err); - }); - }); -}); +// Do something else... +await somethingElse(); +// Release the lock. +await lock.release(); ``` +## API -Usage (disposer style) ----------------------- - - -### Locking & Unlocking - -```js -var using = require('bluebird').using; - -// the string identifier for the resource you want to lock -var resource = 'locks:account:322456'; - -// the maximum amount of time you want the resource locked, -// keeping in mind that you can extend the lock up until -// the point when it expires -var ttl = 1000; - -// if we weren't able to reach redis, your lock will eventually -// expire, but you probably want to do something like log that -// an error occurred; if you don't pass a handler, this error -// will be ignored -function unlockErrorHandler(err) { - console.error(err); -} - -using(redlock.disposer(resource, ttl, unlockErrorHandler), function(lock) { - - // ...do something here... - -}); // <-- unlock is automatically handled by bluebird - -``` - - -### Locking and Extending - -```js -using(redlock.disposer('locks:account:322456', 1000, unlockErrorHandler), function(lock) { - - // ...do something here... - - // if you need more time, you can continue to extend - // the lock as long as you never let it expire - - // this will extend the lock so that it expires - // approximitely 1s from when `extend` is called - return lock.extend(1000).then(function(extended){ - - // Note that redlock modifies the original lock, - // so the vars `lock` and `extended` point to the - // exact same object - - // ...do something here... - - }); -}); // <-- unlock is automatically handled by bluebird - -``` - - -Usage (callback style) ----------------------- - - -### Locking & Unlocking - -```js - -// the string identifier for the resource you want to lock -var resource = 'locks:account:322456'; - -// the maximum amount of time you want the resource locked, -// keeping in mind that you can extend the lock up until -// the point when it expires -var ttl = 1000; - -redlock.lock(resource, ttl, function(err, lock) { - - // we failed to lock the resource - if(err) { - // ... - } - - // we have the lock - else { - - - // ...do something here... - - - // unlock your resource when you are done - lock.unlock(function(err) { - // we weren't able to reach redis; your lock will eventually - // expire, but you probably want to log this error - console.error(err); - }); - } -}); - -``` - - -### Locking and Extending - -```js -redlock.lock('locks:account:322456', 1000, function(err, lock) { - - // we failed to lock the resource - if(err) { - // ... - } - - // we have the lock - else { - - - // ...do something here... - - - // if you need more time, you can continue to extend - // the lock as long as you never let it expire - - // this will extend the lock so that it expires - // approximitely 1s from when `extend` is called - lock.extend(1000, function(err, lock){ - - // we failed to extend the lock on the resource - if(err) { - // ... - } - - - // ...do something here... - - - // unlock your resource when you are done - lock.unlock(); - } - } -}); - -``` - -## Locking multiple resources -Multiple resources can be locked by providing an `Array` of strings to `Redlock.prototype.lock` call. Internally a single attempt is made to `redis` by evaluating script which executes lock statements. For more details about atomicity of scripts please see [redis reference](https://redis.io/commands/eval#atomicity-of-scripts). - -There are however some limitations of which you need to be aware of: -- When requesting a lock it will fail if any of requested resources is already set -- If lock attempt fails for any resource (due to whatever reason) an attempt for removing already set resources is made. However there are no guarantees that it will succeed (`redis` doesn't provide them) -- Releasing lock will fail if any of requested resources is missing -- Extending lock will fail if any of requested resources is missing - -Example: -```js -redlock.lock(['locks:account:322456', 'locks:account:322457', 'locks:account:322458'], 1000).then(function(lock) { - - // ...do something here... - - // if you need more time, you can continue to extend - // the lock as long as you never let it expire - - // this will extend the lock so that it expires - // approximitely 1s from when `extend` is called - return lock.extend(1000).then(function(lock){ - - // ...do something here... - - // unlock your resource when you are done - return lock.unlock() - .catch(function(err) { - // we weren't able to reach redis; your lock will eventually - // expire, but you probably want to log this error - console.error(err); - }); - }); -}); -``` - - -API Docs --------- - -### `Redlock.prototype.lock(resource, ttl, ?callback) => Promise` -- `resource (string or string[])` resource(s) to be locked -- `ttl (number)` time in ms until the lock expires -- `callback (function)` callback returning: - - `err (Error)` - - `lock (Lock)` - - -### `Redlock.prototype.unlock(lock, ?callback) => Promise` -- `lock (Lock)` lock to be released -- `callback (function)` callback returning: - - `err (Error)` - - -### `Redlock.prototype.extend(lock, ttl, ?callback) => Promise` -- `lock (Lock)` lock to be extended -- `ttl (number)` time in ms to extend the lock's expiration -- `callback (function)` callback returning: - - `err (Error)` - - `lock (Lock)` - - -### `Redlock.prototype.disposer(resource, ttl, ?unlockErrorHandler)` -- `resource (string or string[])` resource(s) to be locked -- `ttl (number)` time in ms to extend the lock's expiration -- `callback (function)` error handler called with: - - `err (Error)` - - -### `Redlock.prototype.quit(?callback) => Promise<*[]>` -- `callback (function)` error handler called with: - - `err (Error)` - - `*[]` results of calling `.quit()` on each client - - -### `Lock.prototype.unlock(?callback) => Promise` -- `callback (function)` callback returning: - - `err (Error)` - - -### `Lock.prototype.extend(ttl, ?callback) => Promise` -- `ttl (number)` time from now in ms to set as the lock's new expiration -- `callback (function)` callback returning: - - `err (Error)` - - `lock (Lock)` - +Please view the (very concise) source code or TypeScript definitions for a detailed breakdown of the API. diff --git a/docker-compose.yml b/docker-compose.yml index 54970bd..10f17ab 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,19 +4,83 @@ volumes: node_modules: services: + redis-single-instance: + image: redis - # This container builds the server. - redlock: - build: - context: . - target: build - command: yarn build:development + redis-multi-instance-a: + image: redis + redis-multi-instance-b: + image: redis + redis-multi-instance-c: + image: redis + + # redis-single-cluster-1: + # image: redis + + # redis-single-cluster-2: + # image: redis + + # redis-multi-cluster-a-1: + # image: redis + # redis-multi-cluster-a-2: + # image: redis + + # redis-multi-cluster-b-1: + # image: redis + # redis-multi-cluster-b-2: + # image: redis + + # redis-multi-cluster-c-1: + # image: redis + # redis-multi-cluster-c-2: + # image: redis + + # This container installs node modules into the node_modules volume. + installer: + image: node:16 + working_dir: /workspace + command: yarn + environment: + NODE_ENV: development + volumes: + - type: bind + source: . + target: /workspace + - type: volume + source: node_modules + target: /workspace/node_modules + + # This container watches for changes and builds the application. + builder: + depends_on: + - installer + image: node:16 + working_dir: /workspace + command: ./scripts/await.sh node_modules/.bin/tsc yarn build:development + environment: + NODE_ENV: development + volumes: + - type: bind + source: . + target: /workspace + - type: volume + source: node_modules + target: /workspace/node_modules + + # This container runs the tests. + tester: + depends_on: + - builder + - redis_single_instance + image: node:16 + working_dir: /workspace + command: ./scripts/await.sh node_modules/.bin/ava ./scripts/await.sh dist/index.js yarn test:development environment: NODE_ENV: development volumes: - type: bind source: . - target: /build + target: /workspace - type: volume source: node_modules - target: /build/node_modules + target: /workspace/node_modules diff --git a/main.sublime-project b/main.sublime-project new file mode 100644 index 0000000..04532b0 --- /dev/null +++ b/main.sublime-project @@ -0,0 +1,10 @@ +{ + "folders": + [ + { + "path": ".", + "folder_exclude_patterns": ["**/node_modules", "**/dist"], + "name": "redlock" + } + ] +} diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index ed61119..0000000 --- a/package-lock.json +++ /dev/null @@ -1,1992 +0,0 @@ -{ - "name": "redlock", - "version": "4.2.0", - "lockfileVersion": 1, - "requires": true, - "dependencies": { - "@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 - }, - "abbrev": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.9.tgz", - "integrity": "sha1-kbR5JYinc4wl813W9jdSovh3YTU=", - "dev": true - }, - "ajv": { - "version": "6.12.5", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.5.tgz", - "integrity": "sha512-lRF8RORchjpKG50/WFf8xmg7sgCLFiYNNnqdKflk63whMQcWR5ngGjiSXkL9bjxy6B2npOK2HSMN49jEBMSkag==", - "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" - } - }, - "amdefine": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", - "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=", - "dev": true, - "optional": true - }, - "ansi-colors": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", - "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", - "dev": true - }, - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true - }, - "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" - } - }, - "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" - } - }, - "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.prototype.map": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array.prototype.map/-/array.prototype.map-1.0.2.tgz", - "integrity": "sha512-Az3OYxgsa1g7xDYp86l0nnN4bcmuEITGe1rbdEBVkrqkzMgDcbdQ2R7r41pNzti+4NMces3H8gMmuioZUilLgw==", - "dev": true, - "requires": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.0-next.1", - "es-array-method-boxes-properly": "^1.0.0", - "is-string": "^1.0.4" - } - }, - "asn1": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", - "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", - "dev": true, - "requires": { - "safer-buffer": "~2.1.0" - } - }, - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", - "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 - }, - "async": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", - "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", - "dev": true - }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", - "dev": true - }, - "aws-sign2": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", - "dev": true - }, - "aws4": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.10.1.tgz", - "integrity": "sha512-zg7Hz2k5lI8kb7U32998pRRFin7zJlkfezGJjUc2heaD4Pw2wObakCDVzkKztTm/Ln7eiVvYsjqak0Ed4LkMDA==", - "dev": true - }, - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true - }, - "bcrypt-pbkdf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", - "dev": true, - "requires": { - "tweetnacl": "^0.14.3" - } - }, - "binary-extensions": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.1.0.tgz", - "integrity": "sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ==", - "dev": true - }, - "bluebird": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", - "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==" - }, - "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 - }, - "camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true - }, - "caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", - "dev": true - }, - "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": "4.1.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", - "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "dependencies": { - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "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.4.2", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.2.tgz", - "integrity": "sha512-IZHaDeBeI+sZJRX7lGcXsdzgvZqKv6sECqsbErJA4mHWfpRrD8B97kSFN4cQz6nGBGiuFia1MKR4d6c1o8Cv7A==", - "dev": true, - "requires": { - "anymatch": "~3.1.1", - "braces": "~3.0.2", - "fsevents": "~2.1.2", - "glob-parent": "~5.1.0", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.4.0" - } - }, - "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": { - "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 - }, - "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" - } - }, - "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" - } - } - } - }, - "cluster-key-slot": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.0.tgz", - "integrity": "sha512-2Nii8p3RwAPiFwsnZvukotvow2rIHM+yQ6ZcBXGHdniadkYGZYiGmkHJIbZPIV9nfv7m/U1IPMVVcAhoWFeklw==", - "dev": true - }, - "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 - }, - "combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, - "requires": { - "delayed-stream": "~1.0.0" - } - }, - "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 - }, - "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 - }, - "coveralls": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/coveralls/-/coveralls-3.1.0.tgz", - "integrity": "sha512-sHxOu2ELzW8/NC1UP5XVLbZDzO4S3VxfFye3XYCznopHy02YjNkHcj5bKaVw2O7hVaBdBjEdQGpie4II1mWhuQ==", - "dev": true, - "requires": { - "js-yaml": "^3.13.1", - "lcov-parse": "^1.0.0", - "log-driver": "^1.2.7", - "minimist": "^1.2.5", - "request": "^2.88.2" - } - }, - "dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0" - } - }, - "debug": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz", - "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", - "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 - }, - "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==", - "requires": { - "object-keys": "^1.0.12" - } - }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", - "dev": true - }, - "denque": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/denque/-/denque-1.4.1.tgz", - "integrity": "sha512-OfzPuSZKGcgr96rf1oODnfjqBFmr1DVoc/TrItj3Ohe0Ah1C5WX5Baquw/9U9KovnQ88EqmJbD66rKYUQYN1tQ==", - "dev": true - }, - "diff": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", - "dev": true - }, - "ecc-jsbn": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", - "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", - "dev": true, - "requires": { - "jsbn": "~0.1.0", - "safer-buffer": "^2.1.0" - } - }, - "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 - }, - "es-abstract": { - "version": "1.17.7", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz", - "integrity": "sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==", - "requires": { - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1", - "is-callable": "^1.2.2", - "is-regex": "^1.1.1", - "object-inspect": "^1.8.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.1", - "string.prototype.trimend": "^1.0.1", - "string.prototype.trimstart": "^1.0.1" - }, - "dependencies": { - "es-abstract": { - "version": "1.18.0-next.1", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.1.tgz", - "integrity": "sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==", - "requires": { - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1", - "is-callable": "^1.2.2", - "is-regex": "^1.1.1", - "object-inspect": "^1.8.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.1", - "string.prototype.trimend": "^1.0.1", - "string.prototype.trimstart": "^1.0.1" - } - }, - "object.assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.1.tgz", - "integrity": "sha512-VT/cxmx5yaoHSOTSyrCygIDFco+RsibY2NM0a4RdEeY/4KgqezwFtK1yr3U67xYhqJSlASm2pKhLVzPj2lr4bA==", - "requires": { - "define-properties": "^1.1.3", - "has-symbols": "^1.0.1", - "object-keys": "^1.1.1" - } - } - } - }, - "es-array-method-boxes-properly": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz", - "integrity": "sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==", - "dev": true - }, - "es-get-iterator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.0.tgz", - "integrity": "sha512-UfrmHuWQlNMTs35e1ypnvikg6jCz3SK8v8ImvmDsh36fCVUR1MqoFDiyn0/k52C8NqO3YsO8Oe0azeesNuqSsQ==", - "dev": true, - "requires": { - "es-abstract": "^1.17.4", - "has-symbols": "^1.0.1", - "is-arguments": "^1.0.4", - "is-map": "^2.0.1", - "is-set": "^2.0.1", - "is-string": "^1.0.5", - "isarray": "^2.0.5" - } - }, - "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==", - "requires": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" - } - }, - "escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true - }, - "escodegen": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.8.1.tgz", - "integrity": "sha1-WltTr0aTEQvrsIZ6o0MN07cKEBg=", - "dev": true, - "requires": { - "esprima": "^2.7.1", - "estraverse": "^1.9.1", - "esutils": "^2.0.2", - "optionator": "^0.8.1", - "source-map": "~0.2.0" - }, - "dependencies": { - "esprima": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", - "integrity": "sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE=", - "dev": true - } - } - }, - "esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true - }, - "estraverse": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-1.9.3.tgz", - "integrity": "sha1-r2fy3JIlgkFZUJJgkaQAXSnJu0Q=", - "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 - }, - "extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "dev": true - }, - "extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", - "dev": true - }, - "fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true - }, - "fast-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 - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "requires": { - "locate-path": "^6.0.0", - "path-exists": "^4.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" - } - }, - "forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", - "dev": true - }, - "form-data": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", - "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", - "dev": true, - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" - } - }, - "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.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz", - "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==", - "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==" - }, - "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 - }, - "getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0" - } - }, - "glob": { - "version": "5.0.15", - "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", - "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=", - "dev": true, - "requires": { - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "2 || 3", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "glob-parent": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", - "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", - "dev": true, - "requires": { - "is-glob": "^4.0.1" - } - }, - "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.6", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.6.tgz", - "integrity": "sha512-1f2BACcBfiwAfStCKZNrUCgqNZkGsAT7UM3kkYtXuLo0KnaVfjKOyf7PRzB6++aK9STyT1Pd2ZCPe3EGOXleXA==", - "dev": true, - "requires": { - "minimist": "^1.2.5", - "neo-async": "^2.6.0", - "source-map": "^0.6.1", - "uglify-js": "^3.1.4", - "wordwrap": "^1.0.0" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } - } - }, - "har-schema": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", - "dev": true - }, - "har-validator": { - "version": "5.1.5", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", - "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", - "dev": true, - "requires": { - "ajv": "^6.12.3", - "har-schema": "^2.0.0" - } - }, - "has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "requires": { - "function-bind": "^1.1.1" - } - }, - "has-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", - "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=", - "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==" - }, - "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 - }, - "http-signature": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0", - "jsprim": "^1.2.2", - "sshpk": "^1.7.0" - } - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true - }, - "ioredis": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-4.18.0.tgz", - "integrity": "sha512-wXlB60wD+ayJxbD7t+RFBanXinhHyYpfKUxTEEXNOpd0wb+nC8GLH2r7SaZ6sSBOxr8x6jDfBiuMaiK3bPYABw==", - "dev": true, - "requires": { - "cluster-key-slot": "^1.1.0", - "debug": "^4.1.1", - "denque": "^1.1.0", - "lodash.defaults": "^4.2.0", - "lodash.flatten": "^4.4.0", - "redis-commands": "1.6.0", - "redis-errors": "^1.2.0", - "redis-parser": "^3.0.0", - "standard-as-callback": "^2.0.1" - } - }, - "is-arguments": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.0.4.tgz", - "integrity": "sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA==", - "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.2.2", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.2.tgz", - "integrity": "sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA==" - }, - "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==" - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "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 - }, - "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-map": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.1.tgz", - "integrity": "sha512-T/S49scO8plUiAOA2DBTBG3JHpn1yiw0kRp6dgiZ0v2/6twi5eiB0rHtHFH9ZIrvlWc6+4O+m4zg5+Z833aXgw==", - "dev": true - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "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-regex": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz", - "integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==", - "requires": { - "has-symbols": "^1.0.1" - } - }, - "is-set": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.1.tgz", - "integrity": "sha512-eJEzOtVyenDs1TMzSQ3kU3K+E0GUS9sno+F0OBT97xsgcJsF9nXMBtkT9/kut5JEpM7oL7X/0qxR17K3mcwIAA==", - "dev": true - }, - "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==", - "requires": { - "has-symbols": "^1.0.1" - } - }, - "is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", - "dev": true - }, - "isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "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 - }, - "isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", - "dev": true - }, - "istanbul": { - "version": "0.4.5", - "resolved": "https://registry.npmjs.org/istanbul/-/istanbul-0.4.5.tgz", - "integrity": "sha1-ZcfXPUxNqE1POsMQuRj7C4Azczs=", - "dev": true, - "requires": { - "abbrev": "1.0.x", - "async": "1.x", - "escodegen": "1.8.x", - "esprima": "2.7.x", - "glob": "^5.0.15", - "handlebars": "^4.0.1", - "js-yaml": "3.x", - "mkdirp": "0.5.x", - "nopt": "3.x", - "once": "1.x", - "resolve": "1.1.x", - "supports-color": "^3.1.0", - "which": "^1.1.1", - "wordwrap": "^1.0.0" - }, - "dependencies": { - "esprima": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", - "integrity": "sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE=", - "dev": true - } - } - }, - "iterate-iterator": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/iterate-iterator/-/iterate-iterator-1.0.1.tgz", - "integrity": "sha512-3Q6tudGN05kbkDQDI4CqjaBf4qf85w6W6GnuZDtUVYwKgtC1q8yxYX7CZed7N+tLzQqS6roujWvszf13T+n9aw==", - "dev": true - }, - "iterate-value": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/iterate-value/-/iterate-value-1.0.2.tgz", - "integrity": "sha512-A6fMAio4D2ot2r/TYzr4yUWrmwNdsN5xL7+HUiyACE4DXm+q8HtPcnFTp+NnW3k4N05tZ7FVYFFb2CR13NxyHQ==", - "dev": true, - "requires": { - "es-get-iterator": "^1.0.2", - "iterate-iterator": "^1.0.1" - } - }, - "js-yaml": { - "version": "3.14.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.0.tgz", - "integrity": "sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==", - "dev": true, - "requires": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - } - }, - "jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", - "dev": true - }, - "json-schema": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", - "dev": true - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "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 - }, - "jsprim": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", - "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", - "dev": true, - "requires": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.2.3", - "verror": "1.10.0" - } - }, - "lcov-parse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/lcov-parse/-/lcov-parse-1.0.0.tgz", - "integrity": "sha1-6w1GtUER68VhrLTECO+TY73I9+A=", - "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" - } - }, - "locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "requires": { - "p-locate": "^5.0.0" - } - }, - "lodash.defaults": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", - "integrity": "sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw=", - "dev": true - }, - "lodash.flatten": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", - "integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8=", - "dev": true - }, - "log-driver": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/log-driver/-/log-driver-1.2.7.tgz", - "integrity": "sha512-U7KCmLdqsGHBLeWqYlFA0V0Sl6P08EE1ZrmA9cxjUE0WVqT9qnyVDPz1kzpFEP0jdJuFnasWIfSd7fsaNXkpbg==", - "dev": true - }, - "log-symbols": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.0.0.tgz", - "integrity": "sha512-FN8JBzLx6CzeMrB0tg6pqlGU1wCrXW+ZXGH481kfsBqer0hToTIiHdjH4Mq8xJUbvATujKCvaREGWpGUionraA==", - "dev": true, - "requires": { - "chalk": "^4.0.0" - } - }, - "mime-db": { - "version": "1.44.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz", - "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==", - "dev": true - }, - "mime-types": { - "version": "2.1.27", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz", - "integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==", - "dev": true, - "requires": { - "mime-db": "1.44.0" - } - }, - "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": "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.5", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", - "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", - "dev": true, - "requires": { - "minimist": "^1.2.5" - } - }, - "mocha": { - "version": "8.1.3", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-8.1.3.tgz", - "integrity": "sha512-ZbaYib4hT4PpF4bdSO2DohooKXIn4lDeiYqB+vTmCdr6l2woW0b6H3pf5x4sM5nwQMru9RvjjHYWVGltR50ZBw==", - "dev": true, - "requires": { - "ansi-colors": "4.1.1", - "browser-stdout": "1.3.1", - "chokidar": "3.4.2", - "debug": "4.1.1", - "diff": "4.0.2", - "escape-string-regexp": "4.0.0", - "find-up": "5.0.0", - "glob": "7.1.6", - "growl": "1.10.5", - "he": "1.2.0", - "js-yaml": "3.14.0", - "log-symbols": "4.0.0", - "minimatch": "3.0.4", - "ms": "2.1.2", - "object.assign": "4.1.0", - "promise.allsettled": "1.0.2", - "serialize-javascript": "4.0.0", - "strip-json-comments": "3.0.1", - "supports-color": "7.1.0", - "which": "2.0.2", - "wide-align": "1.1.3", - "workerpool": "6.0.0", - "yargs": "13.3.2", - "yargs-parser": "13.1.2", - "yargs-unparser": "1.6.1" - }, - "dependencies": { - "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" - } - }, - "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" - } - }, - "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" - } - }, - "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" - } - } - } - }, - "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 - }, - "neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "dev": true - }, - "nopt": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", - "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", - "dev": true, - "requires": { - "abbrev": "1" - } - }, - "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 - }, - "oauth-sign": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", - "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", - "dev": true - }, - "object-inspect": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.8.0.tgz", - "integrity": "sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==" - }, - "object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" - }, - "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" - } - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, - "requires": { - "wrappy": "1" - } - }, - "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" - } - }, - "p-limit": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.0.2.tgz", - "integrity": "sha512-iwqZSOoWIW+Ew4kAGUlN16J4M7OB3ysMLSZtnhmqx7njIHFPlxWBX8xo3lVTyFVq6mI/lL9qt2IsN1sHwaxJkg==", - "dev": true, - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "requires": { - "p-limit": "^3.0.2" - } - }, - "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 - }, - "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 - }, - "pathval": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", - "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=", - "dev": true - }, - "performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", - "dev": true - }, - "picomatch": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", - "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==", - "dev": true - }, - "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 - }, - "promise.allsettled": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/promise.allsettled/-/promise.allsettled-1.0.2.tgz", - "integrity": "sha512-UpcYW5S1RaNKT6pd+s9jp9K9rlQge1UXKskec0j6Mmuq7UJCvlS2J2/s/yuPN8ehftf9HXMxWlKiPbGGUzpoRg==", - "dev": true, - "requires": { - "array.prototype.map": "^1.0.1", - "define-properties": "^1.1.3", - "es-abstract": "^1.17.0-next.1", - "function-bind": "^1.1.1", - "iterate-value": "^1.0.0" - } - }, - "psl": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", - "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==", - "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 - }, - "qs": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", - "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", - "dev": true - }, - "randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dev": true, - "requires": { - "safe-buffer": "^5.1.0" - } - }, - "readdirp": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.4.0.tgz", - "integrity": "sha512-0xe001vZBnJEK+uKcj8qOhyAKPzIT+gStxWr3LCB0DwcXR5NZJ3IaC+yGnHCYzB/S7ov3m3EEbZI2zeNvX+hGQ==", - "dev": true, - "requires": { - "picomatch": "^2.2.1" - } - }, - "redis": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/redis/-/redis-3.0.2.tgz", - "integrity": "sha512-PNhLCrjU6vKVuMOyFu7oSP296mwBkcE6lrAjruBYG5LgdSqtRBoVQIylrMyVZD/lkF24RSNNatzvYag6HRBHjQ==", - "dev": true, - "requires": { - "denque": "^1.4.1", - "redis-commands": "^1.5.0", - "redis-errors": "^1.2.0", - "redis-parser": "^3.0.0" - } - }, - "redis-commands": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.6.0.tgz", - "integrity": "sha512-2jnZ0IkjZxvguITjFTrGiLyzQZcTvaw8DAaCXxZq/dsHXz7KfMQ3OUJy7Tz9vnRtZRVz6VRCPDvruvU8Ts44wQ==", - "dev": true - }, - "redis-errors": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", - "integrity": "sha1-62LSrbFeTq9GEMBK/hUpOEJQq60=", - "dev": true - }, - "redis-parser": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", - "integrity": "sha1-tm2CjNyv5rS4pCin3vTGvKwxyLQ=", - "dev": true, - "requires": { - "redis-errors": "^1.0.0" - } - }, - "request": { - "version": "2.88.2", - "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", - "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", - "dev": true, - "requires": { - "aws-sign2": "~0.7.0", - "aws4": "^1.8.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "form-data": "~2.3.2", - "har-validator": "~5.1.3", - "http-signature": "~1.2.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.19", - "oauth-sign": "~0.9.0", - "performance-now": "^2.1.0", - "qs": "~6.5.2", - "safe-buffer": "^5.1.2", - "tough-cookie": "~2.5.0", - "tunnel-agent": "^0.6.0", - "uuid": "^3.3.2" - } - }, - "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.1.7", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", - "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=", - "dev": true - }, - "safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "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 - }, - "serialize-javascript": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz", - "integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==", - "dev": true, - "requires": { - "randombytes": "^2.1.0" - } - }, - "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 - }, - "source-map": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.2.0.tgz", - "integrity": "sha1-2rc/vPwrqBm03gO9b26qSBZLP50=", - "dev": true, - "optional": true, - "requires": { - "amdefine": ">=0.0.4" - } - }, - "sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", - "dev": true - }, - "sshpk": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", - "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", - "dev": true, - "requires": { - "asn1": "~0.2.3", - "assert-plus": "^1.0.0", - "bcrypt-pbkdf": "^1.0.0", - "dashdash": "^1.12.0", - "ecc-jsbn": "~0.1.1", - "getpass": "^0.1.1", - "jsbn": "~0.1.0", - "safer-buffer": "^2.0.2", - "tweetnacl": "~0.14.0" - } - }, - "standard-as-callback": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.0.1.tgz", - "integrity": "sha512-NQOxSeB8gOI5WjSaxjBgog2QFw55FV8TkS6Y07BiB3VJ8xNTvUYm0wl0s8ObgQ5NhdpnNfigMIKjgPESzgr4tg==", - "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" - } - }, - "string.prototype.trimend": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz", - "integrity": "sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g==", - "requires": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.5" - } - }, - "string.prototype.trimstart": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz", - "integrity": "sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw==", - "requires": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.5" - } - }, - "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" - } - }, - "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": "3.2.3", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", - "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", - "dev": true, - "requires": { - "has-flag": "^1.0.0" - } - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - }, - "tough-cookie": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", - "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", - "dev": true, - "requires": { - "psl": "^1.1.28", - "punycode": "^2.1.1" - } - }, - "tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", - "dev": true, - "requires": { - "safe-buffer": "^5.0.1" - } - }, - "tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", - "dev": true - }, - "type-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "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 - }, - "uglify-js": { - "version": "3.11.1", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.11.1.tgz", - "integrity": "sha512-OApPSuJcxcnewwjSGGfWOjx3oix5XpmrK9Z2j0fTRlHGoZ49IU6kExfZTM0++fCArOOCet+vIfWwFHbvWqwp6g==", - "dev": true, - "optional": true - }, - "uri-js": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.0.tgz", - "integrity": "sha512-B0yRTzYdUCCn9n+F4+Gh4yIDtMQcaJsmYBDsTSG8g/OejKBodLQ2IHfN3bM7jUsRXndopT7OIXWdYqc1fjmV6g==", - "dev": true, - "requires": { - "punycode": "^2.1.0" - } - }, - "uuid": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", - "dev": true - }, - "verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - } - }, - "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 - }, - "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" - } - }, - "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": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", - "dev": true - }, - "workerpool": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.0.0.tgz", - "integrity": "sha512-fU2OcNA/GVAJLLyKUoHkAgIhKb0JoCpSjLC/G2vYKxUjVmQwGbRVeoPJ1a8U4pnVofz4AQV5Y/NEw8oKqxEBtA==", - "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": { - "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 - }, - "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" - } - }, - "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 - }, - "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" - } - }, - "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" - } - } - } - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "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 - }, - "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": { - "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 - }, - "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" - } - }, - "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.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "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" - } - }, - "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 - }, - "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" - } - }, - "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" - } - } - } - }, - "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-unparser": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-1.6.1.tgz", - "integrity": "sha512-qZV14lK9MWsGCmcr7u5oXGH0dbGqZAIxTDrWXZDo5zUr6b6iUmelNKO6x6R1dQT24AH3LgRxJpr8meWy2unolA==", - "dev": true, - "requires": { - "camelcase": "^5.3.1", - "decamelize": "^1.2.0", - "flat": "^4.1.0", - "is-plain-obj": "^1.1.0", - "yargs": "^14.2.3" - }, - "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 - }, - "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" - } - }, - "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.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "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" - } - }, - "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 - }, - "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" - } - }, - "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" - } - }, - "yargs": { - "version": "14.2.3", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-14.2.3.tgz", - "integrity": "sha512-ZbotRWhF+lkjijC/VhmOT9wSgyBQ7+zr13+YLkhfsSiTriYsMzkTUFP18pFhWwBeMa5gUc1MzbhrO6/VB7c9Xg==", - "dev": true, - "requires": { - "cliui": "^5.0.0", - "decamelize": "^1.2.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": "^15.0.1" - } - }, - "yargs-parser": { - "version": "15.0.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-15.0.1.tgz", - "integrity": "sha512-0OAMV2mAZQrs3FkNpDQcBk1x5HXb8X4twADss4S0Iuk+2dGnLOE/fRHrsYm542GduMveyA77OF4wrNJuanRCWw==", - "dev": true, - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } - } - } - } - } -} diff --git a/package.json b/package.json index 1e7f001..eef1eed 100644 --- a/package.json +++ b/package.json @@ -1,49 +1,64 @@ { "name": "redlock", - "version": "4.2.0", + "version": "5.0.0-alpha.0", "description": "A node.js redlock implementation for distributed redis locks", - "main": "redlock.js", - "scripts": { - "test": "istanbul cover mocha", - "test-ci": "istanbul cover _mocha --report lcovonly -- -R spec && cat ./coverage/lcov.info | coveralls" - }, - "repository": { - "type": "git", - "url": "https://github.com/mike-marcacci/node-redlock.git" + "license": "MIT", + "author": { + "name": "Mike Marcacci", + "email": "mike.marcacci@gmail.com" }, + "repository": "https://github.com/mike-marcacci/node-redlock.git", + "homepage": "https://github.com/mike-marcacci/node-redlock#readme", + "bugs": "https://github.com/mike-marcacci/node-redlock/issues", + "main": "dist/index.js", "keywords": [ "nodejs", - "iojs", "redlock", "distributed", "lock", "redis" ], - "author": "Mike Marcacci", - "license": "MIT", - "bugs": { - "url": "https://github.com/mike-marcacci/node-redlock/issues" + "files": [ + "dist/index.d.ts", + "dist/index.js", + "dist/index.js.map" + ], + "engines": { + "node": ">=12" + }, + "browserslist": "node >= 12", + "ava": { + "nodeArguments": [ + "--experimental-specifier-resolution=node" + ] }, - "homepage": "https://github.com/mike-marcacci/node-redlock", "devDependencies": { - "chai": "^4.2.0", - "coveralls": "^3.1.0", - "ioredis": "^4.18.0", - "istanbul": "^0.4.2", - "mocha": "^8.1.3", - "redis": "^3.0.2" + "@types/ioredis": "^4.26.6", + "@types/node": "^16.4.2", + "@typescript-eslint/eslint-plugin": "^4.28.4", + "@typescript-eslint/parser": "^4.28.4", + "ava": "^3.13.0", + "eslint": "^7.31.0", + "eslint-config-prettier": "^8.3.0", + "eslint-plugin-prettier": "^3.2.0", + "ioredis": "^4.19.2", + "nodemon": "^2.0.6", + "prettier": "^2.2.1", + "typescript": "^4.1.2" }, - "dependencies": { - "bluebird": "^3.7.2" + "scripts": { + "format": "prettier --list-different --write '**/*.{json,yml,md,ts}'", + "lint": "prettier -c '**/*.{json,yml,md,ts}' && eslint src --ext ts", + "build": "rm -rf dist && tsc", + "build:development": "rm -rf dist && tsc --watch", + "test": "ava --verbose dist/*.test.js", + "test:development": "ava --verbose --watch dist/*.test.js", + "prepare": "yarn build", + "prepublishOnly": "yarn install && yarn lint && yarn build && yarn test" }, - "engines": { - "node": ">=8.0.0" + "dependencies": { + "node-abort-controller": "^2.0.0" }, - "config": { - "blanket": { - "pattern": [ - "redlock.js" - ] - } - } + "type": "module", + "exports": "./dist/index.js" } diff --git a/redlock.js b/redlock.js deleted file mode 100644 index 6bb24f3..0000000 --- a/redlock.js +++ /dev/null @@ -1,470 +0,0 @@ -'use strict'; - -const util = require('util'); -const crypto = require('crypto'); -const Promise = require('bluebird'); -const EventEmitter = require('events'); - -// constants -const lockScript = ` - -- Return 0 if an entry already exists. - for i, key in ipairs(KEYS) do - if redis.call("exists", key) == 1 then - return 0 - end - end - - -- Create an entry for each provided key. - for i, key in ipairs(KEYS) do - redis.call("set", key, ARGV[1], "PX", ARGV[2]) - end - - -- Return the number of entries added. - return #KEYS -`; - -const unlockScript = ` - local count = 0 - for i, key in ipairs(KEYS) do - -- Only remove entries for *this* lock value. - if redis.call("get", key) == ARGV[1] then - redis.pcall("del", key) - count = count + 1 - end - end - - -- Return the number of entries removed. - return count -`; - -const extendScript = ` - -- Return 0 if an entry exists with a *different* lock value. - for i, key in ipairs(KEYS) do - if redis.call("get", key) ~= ARGV[1] then - return 0 - end - end - - -- Update the entry for each provided key. - for i, key in ipairs(KEYS) do - redis.call("set", key, ARGV[1], "PX", ARGV[2]) - end - - -- Return the number of entries updated. - return #KEYS -`; - -// defaults -const defaults = { - driftFactor: 0.01, - retryCount: 10, - retryDelay: 200, - retryJitter: 100 -}; - - - - - -// LockError -// --------- -// This error is returned when there is an error locking a resource. -function LockError(message, attempts) { - Error.call(this); - Error.captureStackTrace(this, LockError); - this.name = 'LockError'; - this.message = message || 'Failed to lock the resource.'; - this.attempts = attempts; -} - -util.inherits(LockError, Error); - - - - - - -// Lock -// ---- -// An object of this type is returned when a resource is successfully locked. It contains -// convenience methods `unlock` and `extend` which perform the associated Redlock method on -// itself. -function Lock(redlock, resource, value, expiration, attempts, attemptsRemaining) { - this.redlock = redlock; - this.resource = resource; - this.value = value; - this.expiration = expiration; - this.attempts = attempts; - this.attemptsRemaining = attemptsRemaining; -} - -Lock.prototype.unlock = function unlock(callback) { - return this.redlock.unlock(this, callback); -}; - -Lock.prototype.extend = function extend(ttl, callback) { - return this.redlock.extend(this, ttl, callback); -}; - -// Attach a reference to Lock, which allows the application to use instanceof -// to ensure type. -Redlock.Lock = Lock; - - -// Redlock -// ------- -// A redlock object is instantiated with an array of at least one redis client and an optional -// `options` object. Properties of the Redlock object should NOT be changed after it is first -// used, as doing so could have unintended consequences for live locks. -function Redlock(clients, options) { - // set default options - options = options || {}; - this.driftFactor = typeof options.driftFactor === 'number' ? options.driftFactor : defaults.driftFactor; - this.retryCount = typeof options.retryCount === 'number' ? options.retryCount : defaults.retryCount; - this.retryDelay = typeof options.retryDelay === 'number' ? options.retryDelay : defaults.retryDelay; - this.retryJitter = typeof options.retryJitter === 'number' ? options.retryJitter : defaults.retryJitter; - this.lockScript = typeof options.lockScript === 'function' ? options.lockScript(lockScript) : lockScript; - this.unlockScript = typeof options.unlockScript === 'function' ? options.unlockScript(unlockScript) : unlockScript; - this.extendScript = typeof options.extendScript === 'function' ? options.extendScript(extendScript) : extendScript; - // set the redis servers from additional arguments - this.servers = clients; - if(this.servers.length === 0) - throw new Error('Redlock must be instantiated with at least one redis server.'); - - this.scripts = { - lockScript: { value: this.lockScript, hash: this._hashScript(this.lockScript) }, - unlockScript: { value: this.unlockScript, hash: this._hashScript(this.unlockScript) }, - extendScript: { value: this.extendScript, hash: this._hashScript(this.extendScript) }, - }; -} - -// Inherit all the EventEmitter methods, like `on`, and `off` -util.inherits(Redlock, EventEmitter); - - -// Attach a reference to LockError per issue #7, which allows the application to use instanceof -// to destinguish between error types. -Redlock.LockError = LockError; - -// quit -// ---- -// This method runs `.quit()` on all client connections. - -Redlock.prototype.quit = function quit(callback) { - - // quit all clients - return Promise.map(this.servers, function(client) { - return client.quit(); - }) - - // optionally run callback - .nodeify(callback); -}; - - -// lock -// ---- -// This method locks a resource using the redlock algorithm. -// -// ```js -// redlock.lock( -// 'some-resource', // the resource to lock -// 2000, // ttl in ms -// function(err, lock) { // callback function (optional) -// ... -// } -// ) -// ``` -Redlock.prototype.acquire = -Redlock.prototype.lock = function lock(resource, ttl, callback) { - return this._lock(resource, null, ttl, {}, callback); -}; - -// lockWithOptions -// --------------- -// This method locks a resource and overwrites some of the options -// ```js -// redlock.lockWithOptions( -// 'some-resource', // the resource to lock -// 2000, // ttl in ms -// { retryCount: 1, retryDelay: 100 }, // additional options -// function(err, lock) { // callback function (optional) -// ... -// } -// ) -// ``` -Redlock.prototype.acquireWithOptions = -Redlock.prototype.lockWithOptions = function lock(resource, ttl, options, callback) { - return this._lock(resource, null, ttl, options, callback); -}; - -// lock -// ---- -// This method locks a resource using the redlock algorithm, -// and returns a bluebird disposer. -// -// ```js -// using( -// redlock.disposer( -// 'some-resource', // the resource to lock -// 2000 // ttl in ms -// ), -// function(lock) { -// ... -// } -// ); -// ``` -Redlock.prototype.disposer = function disposer(resource, ttl, errorHandler) { - errorHandler = errorHandler || function(err) {}; - return this._lock(resource, null, ttl, {}).disposer(function(lock){ - return lock.unlock().catch(errorHandler); - }); -}; - - -// unlock -// ------ -// This method unlocks the provided lock from all servers still persisting it. It will fail -// with an error if it is unable to release the lock on a quorum of nodes, but will make no -// attempt to restore the lock on nodes that failed to release. It is safe to re-attempt an -// unlock or to ignore the error, as the lock will automatically expire after its timeout. -Redlock.prototype.release = -Redlock.prototype.unlock = function unlock(lock, callback) { - const self = this; - - // array of locked resources - const resource = Array.isArray(lock.resource) - ? lock.resource - : [lock.resource]; - - // immediately invalidate the lock - lock.expiration = 0; - - return new Promise(function(resolve, reject) { - - // the number of votes needed for consensus - const quorum = Math.floor(self.servers.length / 2) + 1; - - // the number of servers which have agreed to release this lock - let votes = 0; - - // the number of async redis calls still waiting to finish - let waiting = self.servers.length; - - // release the lock on each server - self.servers.forEach(function(server){ - return self._executeScript(server, 'unlockScript', [ - resource.length, - ...resource, - lock.value - ], loop); - }); - - function loop(err, response) { - if(err) self.emit('clientError', err); - - // - If the response is less than the resource length, than one or - // more resources failed to unlock: - // - It may have been re-acquired by another process; - // - It may hava already been manually released; - // - It may have expired; - - if(response === resource.length || response === '' + resource.length) - votes++; - - if(waiting-- > 1) return; - - // SUCCESS: there is concensus and the lock is released - if(votes >= quorum) - return resolve(); - - // FAILURE: the lock could not be released - return reject(new LockError('Unable to fully release the lock on resource "' + lock.resource + '".')); - } - }) - - // optionally run callback - .nodeify(callback); -}; - - -// extend -// ------ -// This method extends a valid lock by the provided `ttl`. -Redlock.prototype.extend = function extend(lock, ttl, callback) { - const self = this; - - // the lock has expired - if(lock.expiration < Date.now()) - return Promise.reject(new LockError('Cannot extend lock on resource "' + lock.resource + '" because the lock has already expired.', 0)).nodeify(callback); - - // extend the lock - return self._lock(lock.resource, lock.value, ttl, {}) - - // modify and return the original lock object - .then(function(extension){ - lock.value = extension.value; - lock.expiration = extension.expiration; - return lock; - }) - - // optionally run callback - .nodeify(callback); -}; - - -// _lock -// ----- -// This method locks a resource using the redlock algorithm. -// -// ###Creating New Locks: -// -// ```js -// redlock._lock( -// 'some-resource', // the resource to lock -// null, // no original lock value -// 2000, // ttl in ms -// {}, // option overrides {retryCount, retryDelay} -// function(err, lock) { // callback function (optional) -// ... -// } -// ) -// ``` -// -// ###Extending Existing Locks: -// -// ```js -// redlock._lock( -// 'some-resource', // the resource to lock -// 'dkkk18g4gy39dx6r', // the value of the original lock -// 2000, // ttl in ms -// {}, // option overrides {retryCount, retryDelay} -// function(err, lock) { // callback function (optional) -// ... -// } -// ) -// ``` -Redlock.prototype._lock = function _lock(resource, value, ttl, options, callback) { - const self = this; - - // backwards compatibility with previous method signature: _lock(resource, value, ttl, callback) - if (typeof options === 'function' && typeof callback === 'undefined') { - callback = options; - options = {}; - } - - // array of locked resources - resource = Array.isArray(resource) ? resource : [resource]; - - return new Promise(function(resolve, reject) { - let request; - - // the number of times we have attempted this lock - let attempts = 0; - - // create a new lock - if(value === null) { - value = self._random(); - request = function(server, loop){ - return self._executeScript(server, 'lockScript', [ - resource.length, - ...resource, - value, - ttl - ], loop); - }; - } - - // extend an existing lock - else { - request = function(server, loop){ - return self._executeScript(server, 'extendScript', [ - resource.length, - ...resource, - value, - ttl - ], loop); - }; - } - - function attempt(){ - attempts++; - - let retryCount = options.retryCount || self.retryCount; - let retryDelay = options.retryDelay || self.retryDelay; - - // the time when this attempt started - const start = Date.now(); - - // the number of votes needed for consensus - const quorum = Math.floor(self.servers.length / 2) + 1; - - // the number of servers which have agreed to this lock - let votes = 0; - - // the number of async redis calls still waiting to finish - let waiting = self.servers.length; - - function loop(err, response) { - if(err) self.emit('clientError', err); - if(response === resource.length || response === '' + resource.length) votes++; - if(waiting-- > 1) return; - - // Add 2 milliseconds to the drift to account for Redis expires precision, which is 1 ms, - // plus the configured allowable drift factor - const drift = Math.round(self.driftFactor * ttl) + 2; - const lock = new Lock(self, resource, value, start + ttl - drift, attempts, retryCount - attempts); - - // SUCCESS: there is concensus and the lock is not expired - if(votes >= quorum && lock.expiration > Date.now()) - return resolve(lock); - - - // remove this lock from servers that voted for it - return lock.unlock(function(){ - - // RETRY - if(retryCount === -1 || attempts <= retryCount) - return setTimeout(attempt, Math.max(0, retryDelay + Math.floor((Math.random() * 2 - 1) * self.retryJitter))); - - // FAILED - return reject(new LockError('Exceeded ' + retryCount + ' attempts to lock the resource "' + resource + '".', attempts)); - }); - } - - return self.servers.forEach(function(server){ - return request(server, loop); - }); - } - - return attempt(); - }) - - // optionally run callback - .nodeify(callback); -}; - - -Redlock.prototype._random = function _random(){ - return crypto.randomBytes(16).toString('hex'); -}; - -Redlock.prototype._executeScript = function(server, name, args, callback) { - const script = this.scripts[name]; - - return server.evalsha(script.hash, args, (err, result) => { - if(err !== null && err.message.startsWith("NOSCRIPT")) { - // Script is not loaded yet, call eval and it will populate it in redis lua scripts cache - args.unshift(script.value); - return server.eval(args, callback); - } - - return callback(err, result); - }); -} - -Redlock.prototype._hashScript = function(value) { - return crypto.createHash('sha1').update(value).digest('hex'); -} - -module.exports = Redlock; diff --git a/scripts/await.sh b/scripts/await.sh new file mode 100755 index 0000000..f525e87 --- /dev/null +++ b/scripts/await.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +while [ ! -d "$1" ] && [ ! -f "$1" ]; do + +echo "Waiting for '$1' to exist..." +sleep 1 + +done + +eval "${*:2}" \ No newline at end of file diff --git a/src/index.test.ts b/src/index.test.ts new file mode 100644 index 0000000..ed17a3d --- /dev/null +++ b/src/index.test.ts @@ -0,0 +1,535 @@ +import test from "ava"; +import Client from "ioredis"; +import Redlock, { ExecutionError, ResourceLockedError } from "./index.js"; + +const redis = new Client({ host: "redis-single-instance" }); +test.before(async () => { + await redis + .keys("*") + .then((keys) => (keys?.length ? redisA.del(keys) : null)); +}); + +test("acquires, extends, and releases a single lock", async (t) => { + const redlock = new Redlock([redis]); + + const duration = Math.floor(Number.MAX_SAFE_INTEGER / 10); + + // Acquire a lock. + let lock = await redlock.acquire(["a"], duration); + t.is(await redis.get("a"), lock.value, "The lock value was incorrect."); + t.is( + Math.floor((await redis.pttl("a")) / 100), + Math.floor(duration / 100), + "The lock expiration was off by more than 100ms" + ); + + // Extend the lock. + lock = await lock.extend(3 * duration); + t.is(await redis.get("a"), lock.value, "The lock value was incorrect."); + t.is( + Math.floor((await redis.pttl("a")) / 100), + Math.floor((3 * duration) / 100), + "The lock expiration was off by more than 100ms" + ); + + // Release the lock. + await lock.release(); + t.is(await redis.get("a"), null); +}); + +test("acquires, extends, and releases a multi-resource lock", async (t) => { + const redlock = new Redlock([redis]); + + const duration = Math.floor(Number.MAX_SAFE_INTEGER / 10); + + // Acquire a lock. + let lock = await redlock.acquire(["a1", "a2"], duration); + t.is(await redis.get("a1"), lock.value, "The lock value was incorrect."); + t.is(await redis.get("a2"), lock.value, "The lock value was incorrect."); + t.is( + Math.floor((await redis.pttl("a1")) / 100), + Math.floor(duration / 100), + "The lock expiration was off by more than 100ms" + ); + t.is( + Math.floor((await redis.pttl("a2")) / 100), + Math.floor(duration / 100), + "The lock expiration was off by more than 100ms" + ); + + // Extend the lock. + lock = await lock.extend(3 * duration); + t.is(await redis.get("a1"), lock.value, "The lock value was incorrect."); + t.is(await redis.get("a2"), lock.value, "The lock value was incorrect."); + t.is( + Math.floor((await redis.pttl("a1")) / 100), + Math.floor((3 * duration) / 100), + "The lock expiration was off by more than 100ms" + ); + t.is( + Math.floor((await redis.pttl("a2")) / 100), + Math.floor((3 * duration) / 100), + "The lock expiration was off by more than 100ms" + ); + + // Release the lock. + await lock.release(); + t.is(await redis.get("a1"), null); + t.is(await redis.get("a2"), null); +}); + +test("locks fail when redis is unreachable", async (t) => { + const redis = new Client({ + host: "127.0.0.1", + maxRetriesPerRequest: 0, + autoResendUnfulfilledCommands: false, + autoResubscribe: false, + retryStrategy: () => null, + reconnectOnError: () => false, + }); + + redis.on("error", () => { + // ignore redis-generated errors + }); + + const redlock = new Redlock([redis]); + + const duration = Math.floor(Number.MAX_SAFE_INTEGER / 10); + try { + await redlock.acquire(["b"], duration); + throw new Error("This lock should not be acquired."); + } catch (error) { + if (!(error instanceof ExecutionError)) { + throw error; + } + + t.is( + error.attempts.length, + 11, + "A failed acquisition must have the configured number of retries." + ); + + for (const e of await Promise.allSettled(error.attempts)) { + t.is(e.status, "fulfilled"); + if (e.status === "fulfilled") { + for (const v of e.value?.votesAgainst?.values()) { + t.is(v.message, "Connection is closed."); + } + } + } + } +}); + +test("locks automatically expire", async (t) => { + const redlock = new Redlock([redis]); + + const duration = 200; + + // Acquire a lock. + const lock = await redlock.acquire(["d"], duration); + t.is(await redis.get("d"), lock.value, "The lock value was incorrect."); + + // Wait until the lock expires. + await new Promise((resolve) => setTimeout(resolve, 300, undefined)); + + // Attempt to acquire another lock on the same resource. + const lock2 = await redlock.acquire(["d"], duration); + t.is(await redis.get("d"), lock2.value, "The lock value was incorrect."); + + // Release the lock. + await lock2.release(); + t.is(await redis.get("d"), null); +}); + +test("individual locks are exclusive", async (t) => { + const redlock = new Redlock([redis]); + + const duration = Math.floor(Number.MAX_SAFE_INTEGER / 10); + + // Acquire a lock. + const lock = await redlock.acquire(["c"], duration); + t.is(await redis.get("c"), lock.value, "The lock value was incorrect."); + t.is( + Math.floor((await redis.pttl("c")) / 100), + Math.floor(duration / 100), + "The lock expiration was off by more than 100ms" + ); + + // Attempt to acquire another lock on the same resource. + try { + await redlock.acquire(["c"], duration); + throw new Error("This lock should not be acquired."); + } catch (error) { + if (!(error instanceof ExecutionError)) { + throw error; + } + + t.is( + error.attempts.length, + 11, + "A failed acquisition must have the configured number of retries." + ); + + for (const e of await Promise.allSettled(error.attempts)) { + t.is(e.status, "fulfilled"); + if (e.status === "fulfilled") { + for (const v of e.value?.votesAgainst?.values()) { + t.assert( + v instanceof ResourceLockedError, + "The error must be a ResourceLockedError." + ); + } + } + } + } + + // Release the lock. + await lock.release(); + t.is(await redis.get("c"), null); +}); + +test("overlapping multi-locks are exclusive", async (t) => { + const redlock = new Redlock([redis]); + + const duration = Math.floor(Number.MAX_SAFE_INTEGER / 10); + + // Acquire a lock. + const lock = await redlock.acquire(["c1", "c2"], duration); + t.is(await redis.get("c1"), lock.value, "The lock value was incorrect."); + t.is(await redis.get("c2"), lock.value, "The lock value was incorrect."); + t.is( + Math.floor((await redis.pttl("c1")) / 100), + Math.floor(duration / 100), + "The lock expiration was off by more than 100ms" + ); + t.is( + Math.floor((await redis.pttl("c2")) / 100), + Math.floor(duration / 100), + "The lock expiration was off by more than 100ms" + ); + + // Attempt to acquire another lock with overlapping resources + try { + await redlock.acquire(["c2", "c3"], duration); + throw new Error("This lock should not be acquired."); + } catch (error) { + if (!(error instanceof ExecutionError)) { + throw error; + } + + t.is( + await redis.get("c1"), + lock.value, + "The original lock value must not be changed." + ); + t.is( + await redis.get("c2"), + lock.value, + "The original lock value must not be changed." + ); + t.is(await redis.get("c3"), null, "The new resource must remain unlocked."); + + t.is( + error.attempts.length, + 11, + "A failed acquisition must have the configured number of retries." + ); + + for (const e of await Promise.allSettled(error.attempts)) { + t.is(e.status, "fulfilled"); + if (e.status === "fulfilled") { + for (const v of e.value?.votesAgainst?.values()) { + t.assert( + v instanceof ResourceLockedError, + "The error must be a ResourceLockedError." + ); + } + } + } + } + + // Release the lock. + await lock.release(); + t.is(await redis.get("c1"), null); + t.is(await redis.get("c2"), null); + t.is(await redis.get("c3"), null); +}); + +test("the `using` helper acquires, extends, and releases locks", async (t) => { + const redlock = new Redlock([redis]); + + const duration = 300; + + await redlock.using( + ["x"], + duration, + { + automaticExtensionThreshold: 100, + }, + async (signal) => { + const lockValue = await redis.get("x"); + t.assert( + typeof lockValue === "string", + "The lock value was not correctly acquired." + ); + + // Wait to ensure that the lock is extended + await new Promise((resolve) => setTimeout(resolve, 400, undefined)); + + t.is(signal.aborted, false, "The signal must not be aborted."); + t.is(signal.error, undefined, "The signal must not have an error."); + + t.is( + await redis.get("x"), + lockValue, + "The lock value should not have changed." + ); + + return lockValue; + } + ); + + t.is(await redis.get("x"), null, "The lock was not released."); +}); + +test("the `using` helper is exclusive", async (t) => { + const redlock = new Redlock([redis]); + + const duration = 300; + + let locked = false; + const [lock1, lock2] = await Promise.all([ + await redlock.using( + ["y"], + duration, + { + automaticExtensionThreshold: 100, + }, + async (signal) => { + t.is(locked, false, "The resource must not already be locked."); + locked = true; + + const lockValue = await redis.get("y"); + t.assert( + typeof lockValue === "string", + "The lock value was not correctly acquired." + ); + + // Wait to ensure that the lock is extended + await new Promise((resolve) => setTimeout(resolve, 400, undefined)); + + t.is(signal.error, undefined, "The signal must not have an error."); + t.is(signal.aborted, false, "The signal must not be aborted."); + + t.is( + await redis.get("y"), + lockValue, + "The lock value should not have changed." + ); + + locked = false; + return lockValue; + } + ), + await redlock.using( + ["y"], + duration, + { + automaticExtensionThreshold: 100, + }, + async (signal) => { + t.is(locked, false, "The resource must not already be locked."); + locked = true; + + const lockValue = await redis.get("y"); + t.assert( + typeof lockValue === "string", + "The lock value was not correctly acquired." + ); + + // Wait to ensure that the lock is extended + await new Promise((resolve) => setTimeout(resolve, 400, undefined)); + + t.is(signal.error, undefined, "The signal must not have an error."); + t.is(signal.aborted, false, "The signal must not be aborted."); + + t.is( + await redis.get("y"), + lockValue, + "The lock value should not have changed." + ); + + locked = false; + return lockValue; + } + ), + ]); + + t.not(lock1, lock2, "The locks must be different."); + + t.is(await redis.get("y"), null, "The lock was not released."); +}); + +const redisA = new Client({ host: "redis-multi-instance-a" }); +const redisB = new Client({ host: "redis-multi-instance-b" }); +const redisC = new Client({ host: "redis-multi-instance-c" }); +test.before(async () => { + await Promise.all([ + redisA.keys("*").then((keys) => (keys?.length ? redisA.del(keys) : null)), + redisB.keys("*").then((keys) => (keys?.length ? redisB.del(keys) : null)), + redisC.keys("*").then((keys) => (keys?.length ? redisC.del(keys) : null)), + ]); +}); + +test("multi - acquires, extends, and releases a single lock", async (t) => { + const redlock = new Redlock([redisA, redisB, redisC]); + + const duration = Math.floor(Number.MAX_SAFE_INTEGER / 10); + + // Acquire a lock. + let lock = await redlock.acquire(["a"], duration); + t.is(await redisA.get("a"), lock.value, "The lock value was incorrect."); + t.is(await redisB.get("a"), lock.value, "The lock value was incorrect."); + t.is(await redisC.get("a"), lock.value, "The lock value was incorrect."); + t.is( + Math.floor((await redisA.pttl("a")) / 100), + Math.floor(duration / 100), + "The lock expiration was off by more than 100ms" + ); + t.is( + Math.floor((await redisB.pttl("a")) / 100), + Math.floor(duration / 100), + "The lock expiration was off by more than 100ms" + ); + t.is( + Math.floor((await redisC.pttl("a")) / 100), + Math.floor(duration / 100), + "The lock expiration was off by more than 100ms" + ); + + // Extend the lock. + lock = await lock.extend(3 * duration); + t.is(await redisA.get("a"), lock.value, "The lock value was incorrect."); + t.is(await redisB.get("a"), lock.value, "The lock value was incorrect."); + t.is(await redisC.get("a"), lock.value, "The lock value was incorrect."); + t.is( + Math.floor((await redisA.pttl("a")) / 100), + Math.floor((3 * duration) / 100), + "The lock expiration was off by more than 100ms" + ); + t.is( + Math.floor((await redisB.pttl("a")) / 100), + Math.floor((3 * duration) / 100), + "The lock expiration was off by more than 100ms" + ); + t.is( + Math.floor((await redisC.pttl("a")) / 100), + Math.floor((3 * duration) / 100), + "The lock expiration was off by more than 100ms" + ); + + // Release the lock. + await lock.release(); + t.is(await redisA.get("a"), null); + t.is(await redisB.get("a"), null); + t.is(await redisC.get("a"), null); +}); + +test("multi - succeeds when a minority of clients fail", async (t) => { + const redlock = new Redlock([redisA, redisB, redisC]); + + const duration = Math.floor(Number.MAX_SAFE_INTEGER / 10); + + // Set a value on redisC so that lock acquisition fails. + await redisC.set("b", "other"); + + // Acquire a lock. + let lock = await redlock.acquire(["b"], duration); + t.is(await redisA.get("b"), lock.value, "The lock value was incorrect."); + t.is(await redisB.get("b"), lock.value, "The lock value was incorrect."); + t.is(await redisC.get("b"), "other", "The lock value was changed."); + t.is( + Math.floor((await redisA.pttl("b")) / 100), + Math.floor(duration / 100), + "The lock expiration was off by more than 100ms" + ); + t.is( + Math.floor((await redisB.pttl("b")) / 100), + Math.floor(duration / 100), + "The lock expiration was off by more than 100ms" + ); + t.is(await redisC.pttl("b"), -1, "The lock expiration was changed"); + + // Extend the lock. + lock = await lock.extend(3 * duration); + t.is(await redisA.get("b"), lock.value, "The lock value was incorrect."); + t.is(await redisB.get("b"), lock.value, "The lock value was incorrect."); + t.is(await redisC.get("b"), "other", "The lock value was changed."); + t.is( + Math.floor((await redisA.pttl("b")) / 100), + Math.floor((3 * duration) / 100), + "The lock expiration was off by more than 100ms" + ); + t.is( + Math.floor((await redisB.pttl("b")) / 100), + Math.floor((3 * duration) / 100), + "The lock expiration was off by more than 100ms" + ); + t.is(await redisC.pttl("b"), -1, "The lock expiration was changed"); + + // Release the lock. + await lock.release(); + t.is(await redisA.get("b"), null); + t.is(await redisB.get("b"), null); + t.is(await redisC.get("b"), "other"); + await redisC.del("b"); +}); + +test("multi - fails when a majority of clients fail", async (t) => { + const redlock = new Redlock([redisA, redisB, redisC]); + + const duration = Math.floor(Number.MAX_SAFE_INTEGER / 10); + + // Set a value on redisB and redisC so that lock acquisition fails. + await redisB.set("c", "other1"); + await redisC.set("c", "other2"); + + // Acquire a lock. + try { + await redlock.acquire(["c"], duration); + throw new Error("This lock should not be acquired."); + } catch (error) { + if (!(error instanceof ExecutionError)) { + throw error; + } + + t.is( + error.attempts.length, + 11, + "A failed acquisition must have the configured number of retries." + ); + + t.is(await redisA.get("c"), null); + t.is(await redisB.get("c"), "other1"); + t.is(await redisC.get("c"), "other2"); + + for (const e of await Promise.allSettled(error.attempts)) { + t.is(e.status, "fulfilled"); + if (e.status === "fulfilled") { + for (const v of e.value?.votesAgainst?.values()) { + t.assert( + v instanceof ResourceLockedError, + "The error was of the wrong type." + ); + t.is( + v.message, + "The operation was applied to: 0 of the 1 requested resources." + ); + } + } + } + } + + await redisB.del("c"); + await redisC.del("c"); +}); diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..d0dd1c9 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,743 @@ +import { randomBytes, createHash } from "crypto"; +import { EventEmitter } from "events"; + +// AbortController became available as a global in node version 16. Once version +// 14 reaches its end-of-life, this can be removed. +import PolyfillAbortController from "node-abort-controller"; + +import { Redis as IORedisClient } from "ioredis"; +type Client = IORedisClient; + +// Define script constants. +const ACQUIRE_SCRIPT = ` + -- Return 0 if an entry already exists. + for i, key in ipairs(KEYS) do + if redis.call("exists", key) == 1 then + return 0 + end + end + + -- Create an entry for each provided key. + for i, key in ipairs(KEYS) do + redis.call("set", key, ARGV[1], "PX", ARGV[2]) + end + + -- Return the number of entries added. + return #KEYS +`; + +const EXTEND_SCRIPT = ` + -- Return 0 if an entry exists with a *different* lock value. + for i, key in ipairs(KEYS) do + if redis.call("get", key) ~= ARGV[1] then + return 0 + end + end + + -- Update the entry for each provided key. + for i, key in ipairs(KEYS) do + redis.call("set", key, ARGV[1], "PX", ARGV[2]) + end + + -- Return the number of entries updated. + return #KEYS +`; + +const RELEASE_SCRIPT = ` + local count = 0 + for i, key in ipairs(KEYS) do + -- Only remove entries for *this* lock value. + if redis.call("get", key) == ARGV[1] then + redis.pcall("del", key) + count = count + 1 + end + end + + -- Return the number of entries removed. + return count +`; + +export type ClientExecutionResult = + | { + client: Client; + vote: "for"; + value: number; + } + | { + client: Client; + vote: "against"; + error: Error; + }; + +/* + * This object contains a summary of results. + */ +export type ExecutionStats = { + readonly membershipSize: number; + readonly quorumSize: number; + readonly votesFor: Set; + readonly votesAgainst: Map; +}; + +/* + * This object contains a summary of results. Because the result of an attempt + * can sometimes be determined before all requests are finished, each attempt + * contains a Promise that will resolve ExecutionStats once all requests are + * finished. A rejection of these promises should be considered undefined + * behavior and should cause a crash. + */ +export type ExecutionResult = { + attempts: ReadonlyArray>; +}; + +/** + * + */ +export interface Settings { + readonly driftFactor: number; + readonly retryCount: number; + readonly retryDelay: number; + readonly retryJitter: number; + readonly automaticExtensionThreshold: number; +} + +// Define default settings. +const defaultSettings: Readonly = { + driftFactor: 0.01, + retryCount: 10, + retryDelay: 200, + retryJitter: 100, + automaticExtensionThreshold: 500, +}; + +// Modifyng this object is forbidden. +Object.freeze(defaultSettings); + +/* + * This error indicates a failure due to the existence of another lock for one + * or more of the requested resources. + */ +export class ResourceLockedError extends Error { + constructor(public readonly message: string) { + super(); + this.name = "ResourceLockedError"; + } +} + +/* + * This error indicates a failure of an operation to pass with a quorum. + */ +export class ExecutionError extends Error { + constructor( + public readonly message: string, + public readonly attempts: ReadonlyArray> + ) { + super(); + this.name = "ExecutionError"; + } +} + +/* + * An object of this type is returned when a resource is successfully locked. It + * contains convenience methods `release` and `extend` which perform the + * associated Redlock method on itself. + */ +export class Lock { + constructor( + public readonly redlock: Redlock, + public readonly resources: string[], + public readonly value: string, + public readonly attempts: ReadonlyArray>, + public expiration: number + ) {} + + async release(): Promise { + return this.redlock.release(this); + } + + async extend(duration: number): Promise { + return this.redlock.extend(this, duration); + } +} + +type RedlockAbortSignal = AbortSignal & { error?: Error }; + +/** + * A redlock object is instantiated with an array of at least one redis client + * and an optional `options` object. Properties of the Redlock object should NOT + * be changed after it is first used, as doing so could have unintended + * consequences for live locks. + */ +export default class Redlock extends EventEmitter { + public readonly clients: Set; + public readonly settings: Settings; + public readonly scripts: { + readonly acquireScript: { value: string; hash: string }; + readonly extendScript: { value: string; hash: string }; + readonly releaseScript: { value: string; hash: string }; + }; + + public constructor( + clients: Iterable, + settings: Partial = {}, + scripts: { + readonly acquireScript?: string | ((script: string) => string); + readonly extendScript?: string | ((script: string) => string); + readonly releaseScript?: string | ((script: string) => string); + } = {} + ) { + super(); + + // Prevent crashes on error events. + this.on("error", () => { + // Because redlock is designed for high availability, it does not care if + // a minority of redis instances/clusters fail at an operation. + // + // However, it can be helpful to monitor and log such cases. Redlock emits + // an "error" event whenever it encounters an error, even if the error is + // ignored in its normal operation. + // + // This function serves to prevent node's default behavior of crashing + // when an "error" event is emitted in the absence of listeners. + }); + + // Create a new array of client, to ensure no accidental mutation. + this.clients = new Set(clients); + if (this.clients.size === 0) { + throw new Error( + "Redlock must be instantiated with at least one redis client." + ); + } + + // Customize the settings for this instance. + this.settings = { + driftFactor: + typeof settings.driftFactor === "number" + ? settings.driftFactor + : defaultSettings.driftFactor, + retryCount: + typeof settings.retryCount === "number" + ? settings.retryCount + : defaultSettings.retryCount, + retryDelay: + typeof settings.retryDelay === "number" + ? settings.retryDelay + : defaultSettings.retryDelay, + retryJitter: + typeof settings.retryJitter === "number" + ? settings.retryJitter + : defaultSettings.retryJitter, + automaticExtensionThreshold: + typeof settings.automaticExtensionThreshold === "number" + ? settings.automaticExtensionThreshold + : defaultSettings.automaticExtensionThreshold, + }; + + // Use custom scripts and script modifiers. + const acquireScript = + typeof scripts.acquireScript === "function" + ? scripts.acquireScript(ACQUIRE_SCRIPT) + : ACQUIRE_SCRIPT; + const extendScript = + typeof scripts.extendScript === "function" + ? scripts.extendScript(EXTEND_SCRIPT) + : EXTEND_SCRIPT; + const releaseScript = + typeof scripts.releaseScript === "function" + ? scripts.releaseScript(RELEASE_SCRIPT) + : RELEASE_SCRIPT; + + this.scripts = { + acquireScript: { + value: acquireScript, + hash: this._hash(acquireScript), + }, + extendScript: { + value: extendScript, + hash: this._hash(extendScript), + }, + releaseScript: { + value: releaseScript, + hash: this._hash(releaseScript), + }, + }; + } + + /** + * Generate a sha1 hash compatible with redis evalsha. + */ + private _hash(value: string): string { + return createHash("sha1").update(value).digest("hex"); + } + + /** + * Generate a cryptographically random string. + */ + private _random(): string { + return randomBytes(16).toString("hex"); + } + + /** + * This method runs `.quit()` on all client connections. + */ + public async quit(): Promise { + const results = []; + for (const client of this.clients) { + results.push(client.quit()); + } + + await Promise.all(results); + } + + /** + * This method acquires a locks on the resources for the duration specified by + * the `duration`. + */ + public async acquire( + resources: string[], + duration: number, + settings?: Settings + ): Promise { + const start = Date.now(); + const value = this._random(); + + try { + const { attempts } = await this._execute( + this.scripts.acquireScript, + resources, + [value, duration], + settings + ); + + // Add 2 milliseconds to the drift to account for Redis expires precision, + // which is 1 ms, plus the configured allowable drift factor. + const drift = + Math.round( + (settings?.driftFactor ?? this.settings.driftFactor) * duration + ) + 2; + + return new Lock( + this, + resources, + value, + attempts, + start + duration - drift + ); + } catch (error) { + // If there was an error acquiring the lock, release any partial lock + // state that may exist on a minority of clients. + await this._execute(this.scripts.releaseScript, resources, [value], { + retryCount: 0, + }).catch(() => { + // Any error here will be ignored. + }); + + throw error; + } + } + + /** + * This method unlocks the provided lock from all servers still persisting it. + * It will fail with an error if it is unable to release the lock on a quorum + * of nodes, but will make no attempt to restore the lock in the case of a + * failure to release. It is safe to re-attempt a release or to ignore the + * error, as the lock will automatically expire after its timeout. + */ + public async release( + lock: Lock, + settings?: Partial + ): Promise { + // Immediately invalidate the lock. + lock.expiration = 0; + + // Attempt to release the lock. + return this._execute( + this.scripts.releaseScript, + lock.resources, + [lock.value], + settings + ); + } + + /** + * This method extends a valid lock by the provided `duration`. + */ + public async extend( + existing: Lock, + duration: number, + settings?: Partial + ): Promise { + const start = Date.now(); + + // The lock has already expired. + if (existing.expiration < Date.now()) { + throw new ExecutionError("Cannot extend an already-expired lock.", []); + } + + const { attempts } = await this._execute( + this.scripts.extendScript, + existing.resources, + [existing.value, duration], + settings + ); + + // Invalidate the existing lock. + existing.expiration = 0; + + // Add 2 milliseconds to the drift to account for Redis expires precision, + // which is 1 ms, plus the configured allowable drift factor. + const drift = + Math.round( + (settings?.driftFactor ?? this.settings.driftFactor) * duration + ) + 2; + + const replacement = new Lock( + this, + existing.resources, + existing.value, + attempts, + start + duration - drift + ); + + return replacement; + } + + /** + * Execute a script on all clients. The resulting promise is resolved or + * rejected as soon as this quorum is reached; the resolution or rejection + * will contains a `stats` property that is resolved once all votes are in. + */ + private async _execute( + script: { value: string; hash: string }, + keys: string[], + args: (string | number)[], + _settings?: Partial + ): Promise { + const settings = _settings + ? { + ...this.settings, + ..._settings, + } + : this.settings; + + const maxAttempts = settings.retryCount + 1; + const attempts: Promise[] = []; + + while (true) { + const { vote, stats } = await this._attemptOperation(script, keys, args); + + attempts.push(stats); + + // The operation acheived a quorum in favor. + if (vote === "for") { + return { attempts }; + } + + // Wait before reattempting. + if (attempts.length < maxAttempts) { + await new Promise((resolve) => { + setTimeout( + resolve, + Math.max( + 0, + settings.retryDelay + + Math.floor((Math.random() * 2 - 1) * settings.retryJitter) + ), + undefined + ); + }); + } else { + throw new ExecutionError( + "The operation was unable to acheive a quorum during its retry window.", + attempts + ); + } + } + } + + private async _attemptOperation( + script: { value: string; hash: string }, + keys: string[], + args: (string | number)[] + ): Promise< + | { vote: "for"; stats: Promise } + | { vote: "against"; stats: Promise } + > { + return await new Promise((resolve) => { + const clientResults = []; + for (const client of this.clients) { + clientResults.push( + this._attemptOperationOnClient(client, script, keys, args) + ); + } + + const stats: ExecutionStats = { + membershipSize: clientResults.length, + quorumSize: Math.floor(clientResults.length / 2) + 1, + votesFor: new Set(), + votesAgainst: new Map(), + }; + + let done: () => void; + const statsPromise = new Promise((resolve) => { + done = () => resolve(stats); + }); + + // This is the expected flow for all successful and unsuccessful requests. + const onResultResolve = (clientResult: ClientExecutionResult): void => { + switch (clientResult.vote) { + case "for": + stats.votesFor.add(clientResult.client); + break; + case "against": + stats.votesAgainst.set(clientResult.client, clientResult.error); + break; + } + + // A quorum has determined a success. + if (stats.votesFor.size === stats.quorumSize) { + resolve({ + vote: "for", + stats: statsPromise, + }); + } + + // A quorum has determined a failure. + if (stats.votesAgainst.size === stats.quorumSize) { + resolve({ + vote: "against", + stats: statsPromise, + }); + } + + // All votes are in. + if ( + stats.votesFor.size + stats.votesAgainst.size === + stats.membershipSize + ) { + done(); + } + }; + + // This is unexpected and should crash to prevent undefined behavior. + const onResultReject = (error: Error): void => { + throw error; + }; + + for (const result of clientResults) { + result.then(onResultResolve, onResultReject); + } + }); + } + + private async _attemptOperationOnClient( + client: Client, + script: { value: string; hash: string }, + keys: string[], + args: (string | number)[] + ): Promise { + try { + let result: number; + try { + // Attempt to evaluate the script by its hash. + const shaResult = (await client.evalsha(script.hash, keys.length, [ + ...keys, + ...args, + ])) as unknown; + + if (typeof shaResult !== "number") { + throw new Error( + `Unexpected result of type ${typeof shaResult} returned from redis.` + ); + } + + result = shaResult; + } catch (error) { + // If the redis server does not already have the script cached, + // reattempt the request with the script's raw text. + if ( + !(error instanceof Error) || + !error.message.startsWith("NOSCRIPT") + ) { + throw error; + } + const rawResult = (await client.eval(script.value, keys.length, [ + ...keys, + ...args, + ])) as unknown; + + if (typeof rawResult !== "number") { + throw new Error( + `Unexpected result of type ${typeof rawResult} returned from redis.` + ); + } + + result = rawResult; + } + + // One or more of the resources was already locked. + if (result !== keys.length) { + throw new ResourceLockedError( + `The operation was applied to: ${result} of the ${keys.length} requested resources.` + ); + } + + return { + vote: "for", + client, + value: result, + }; + } catch (error) { + if (!(error instanceof Error)) { + throw new Error( + `Unexpected type ${typeof error} thrown with value: ${error}` + ); + } + + // Emit the error on the redlock instance for observability. + this.emit("error", error); + + return { + vote: "against", + client, + error, + }; + } + } + + /** + * Wrap and execute a routine in the context of an auto-extending lock, + * returning a promise of the routine's value. In the case that auto-extension + * fails, an AbortSignal will be updated to indicate that abortion of the + * routine is in order, and to pass along the encountered error. + * + * @example + * ```ts + * await redlock.using([senderId, recipientId], 5000, { retryCount: 5 }, async (signal) => { + * const senderBalance = await getBalance(senderId); + * const recipientBalance = await getBalance(recipientId); + * + * if (senderBalance < amountToSend) { + * throw new Error("Insufficient balance."); + * } + * + * // The abort signal will be true if: + * // 1. the above took long enough that the lock needed to be extended + * // 2. redlock was unable to extend the lock + * // + * // In such a case, exclusivity can no longer be guaranteed for further + * // operations, and should be handled as an exceptional case. + * if (signal.aborted) { + * throw signal.error; + * } + * + * await setBalances([ + * {id: senderId, balance: senderBalance - amountToSend}, + * {id: recipientId, balance: recipientBalance + amountToSend}, + * ]); + * }); + * ``` + */ + + public async using( + resources: string[], + duration: number, + settings: Partial, + routine?: (signal: RedlockAbortSignal) => T + ): Promise; + + public async using( + resources: string[], + duration: number, + routine: (signal: RedlockAbortSignal) => T + ): Promise; + + public async using( + resources: string[], + duration: number, + settingsOrRoutine: + | undefined + | Partial + | ((signal: RedlockAbortSignal) => T), + optionalRoutine?: (signal: RedlockAbortSignal) => T + ): Promise { + const settings = + settingsOrRoutine && typeof settingsOrRoutine !== "function" + ? { + ...this.settings, + ...settingsOrRoutine, + } + : this.settings; + + const routine = optionalRoutine ?? settingsOrRoutine; + if (typeof routine !== "function") { + throw new Error("INVARIANT: routine is not a function."); + } + + if (settings.automaticExtensionThreshold > duration - 100) { + throw new Error( + "A lock `duration` must be at least 100ms greater than the `automaticExtensionThreshold` setting." + ); + } + + // The AbortController/AbortSignal pattern allows the routine to be notified + // of a failure to extend the lock, and subsequent expiration. In the event + // of an abort, the error object will be made available at `signal.error`. + const controller = + typeof AbortController === "undefined" + ? new PolyfillAbortController() + : new AbortController(); + + const signal = controller.signal as RedlockAbortSignal; + + function queue(): void { + timeout = setTimeout( + () => (extension = extend()), + lock.expiration - Date.now() - settings.automaticExtensionThreshold + ); + } + + async function extend(): Promise { + timeout = undefined; + + try { + lock = await lock.extend(duration); + queue(); + } catch (error) { + if (lock.expiration > Date.now()) { + return (extension = extend()); + } + + signal.error = error; + controller.abort(); + } + } + + let timeout: undefined | NodeJS.Timeout; + let extension: undefined | Promise; + let lock = await this.acquire(resources, duration, settings); + queue(); + + try { + return await routine(signal); + } finally { + // Clean up the timer. + if (timeout) { + clearTimeout(timeout); + timeout = undefined; + } + + // Wait for an in-flight extension to finish. + if (extension) { + await extension.catch(() => { + // An error here doesn't matter at all, because the routine has + // already completed, and a release will be attempted regardless. The + // only reason for waiting here is to prevent possible contention + // between the extension and release. + }); + } + + await lock.release(); + } + } +} diff --git a/test.js b/test.js deleted file mode 100644 index a82738d..0000000 --- a/test.js +++ /dev/null @@ -1,893 +0,0 @@ -'use strict'; - -var assert = require('chai').assert; -var Promise = require('bluebird'); -var Redlock = require('./redlock'); - -test('single-server: https://www.npmjs.com/package/redis', [require('redis').createClient()]); -test('single-server: https://www.npmjs.com/package/redis (string_numbers=true)', [require('redis').createClient({string_numbers: true})]); -test('single-server: https://www.npmjs.com/package/ioredis', [new (require('ioredis'))()]); -test('multi-server: https://www.npmjs.com/package/ioredis', [new (require('ioredis'))({db: 1}), new (require('ioredis'))({db: 2}), new (require('ioredis'))({db: 3})]); - -/* istanbul ignore next */ -function test(name, clients){ - var redlock = new Redlock(clients, { - retryCount: 2, - retryDelay: 150, - retryJitter: 50 - }); - - var resourceString = 'Redlock:test:resource'; - var resourceArray = ['Redlock:test:resource1','Redlock:test:resource2']; - var error = 'Redlock:test:error'; - - describe('Redlock: ' + name, function(){ - - before(function(done) { - var err; - var l = clients.length; function cb(e){ if(e) err = e; l--; if(l === 0) done(err); } - for (var i = clients.length - 1; i >= 0; i--) { - clients[i].sadd(error, 'having a set here should cause a failure', cb); - } - }); - - before(function(done) { - var err; - var l = clients.length; function cb(e){ if(e) err = e; l--; if(l === 0) done(err); } - for (var i = clients.length - 1; i >= 0; i--) { - clients[i].script('flush', cb); - } - }) - - it('should throw an error if not passed any clients', function(){ - assert.throws(function(){ - new Redlock([], { - retryCount: 2, - retryDelay: 150, - retryJitter: 0 - }); - }); - }); - - it('emits a clientError event when a client error occurs', function(done){ - var emitted = 0; - function test(err) { - assert.isNotNull(err); - emitted++; - } - redlock.on('clientError', test); - redlock.lock(error, 200, function(err, lock){ - redlock.removeListener('clientError', test); - assert.isNotNull(err); - assert.equal(emitted, 3 * redlock.servers.length); - done(); - }); - }); - - it('supports custom script functions in options', function(){ - var opts = { - lockScript: function(lockScript) { return lockScript + 'and 1'; }, - unlockScript: function(unlockScript) { return unlockScript + 'and 2'; }, - extendScript: function(extendScript) { return extendScript + 'and 3'; }, - }; - var customRedlock = new Redlock(clients, opts); - var i = 1; - assert.equal(customRedlock.lockScript, redlock.lockScript + 'and ' + i++); - assert.equal(customRedlock.unlockScript, redlock.unlockScript + 'and ' + i++); - assert.equal(customRedlock.extendScript, redlock.extendScript + 'and ' + i++); - }); - - describe('callbacks', function(){ - before(function(done) { - var err; - var l = clients.length; function cb(e){ if(e) err = e; l--; if(l === 0) done(err); } - for (var i = clients.length - 1; i >= 0; i--) { - clients[i].del(resourceString, cb); - } - }); - - var one; - it('should lock a resource', function(done) { - redlock.lock(resourceString, 200, function(err, lock){ - if(err) throw err; - assert.isObject(lock); - assert.instanceOf(lock, Redlock.Lock); - assert.isAbove(lock.expiration, Date.now()-1); - assert.equal(lock.attempts, 1); - one = lock; - done(); - }); - }); - - var two; - var two_expiration; - it('should wait until a lock expires before issuing another lock', function(done) { - assert(one, 'Could not run because a required previous test failed.'); - redlock.lock(resourceString, 800, function(err, lock){ - if(err) throw err; - assert.isObject(lock); - assert.isAbove(lock.expiration, Date.now()-1); - assert.isAbove(Date.now()+1, one.expiration); - assert.isAbove(lock.attempts, 1); - two = lock; - two_expiration = lock.expiration; - done(); - }); - }); - - it('should unlock a resource', function(done) { - assert(two, 'Could not run because a required previous test failed.'); - two.unlock(done); - assert.equal(two.expiration, 0, 'Failed to immediately invalidate the lock.'); - }); - - it('should unlock an already-unlocked resource', function(done) { - assert(two, 'Could not run because a required previous test failed.'); - two.unlock(function(err) { - assert.isNotNull(err) - done(); - }); - }); - - it('should error when unable to fully release a resource', function(done) { - assert(two, 'Could not run because a required previous test failed.'); - var failingTwo = Object.create(two); - failingTwo.resource = error; - failingTwo.unlock(function(err) { - assert.isNotNull(err); - done(); - }); - }); - - it('should fail to extend a lock on an already-unlocked resource', function(done) { - assert(two, 'Could not run because a required previous test failed.'); - two.extend(200, function(err, lock){ - assert.isNotNull(err); - assert.instanceOf(err, Redlock.LockError); - assert.equal(err.attempts, 0); - done(); - }); - }); - - var three; - it('should issue another lock immediately after a resource is unlocked', function(done) { - assert(two_expiration, 'Could not run because a required previous test failed.'); - redlock.lock(resourceString, 800, function(err, lock){ - if(err) throw err; - assert.isObject(lock); - assert.isAbove(lock.expiration, Date.now()-1); - assert.isBelow(Date.now()-1, two_expiration); - assert.equal(lock.attempts, 1); - three = lock; - done(); - }); - }); - - var four; - it('should extend an unexpired lock', function(done) { - assert(three, 'Could not run because a required previous test failed.'); - three.extend(800, function(err, lock){ - if(err) throw err; - assert.isObject(lock); - assert.isAbove(lock.expiration, Date.now()-1); - assert.isAbove(lock.expiration, three.expiration-1); - assert.equal(lock.attempts, 1); - assert.equal(three, lock); - four = lock; - done(); - }); - }); - - it('should fail after the maximum retry count is exceeded', function(done) { - assert(four, 'Could not run because a required previous test failed.'); - redlock.lock(resourceString, 200, function(err, lock){ - assert.isNotNull(err); - assert.instanceOf(err, Redlock.LockError); - assert.equal(err.attempts, 3); - done(); - }); - }); - - it('should fail to extend an expired lock', function(done) { - assert(four, 'Could not run because a required previous test failed.'); - setTimeout(function(){ - three.extend(800, function(err, lock){ - assert.isNotNull(err); - assert.instanceOf(err, Redlock.LockError); - assert.equal(err.attempts, 0); - done(); - }); - }, four.expiration - Date.now() + 100); - }); - - it('should issue another lock immediately after a resource is expired', function(done) { - assert(four, 'Could not run because a required previous test failed.'); - redlock.lock(resourceString, 800, function(err, lock){ - if(err) throw err; - assert.isObject(lock); - assert.isAbove(lock.expiration, Date.now()-1); - assert.equal(lock.attempts, 1); - lock.unlock(done); - }); - }); - - describe('lockWithOptions', function() { - it('should lock a resource with additional options', function(done) { - redlock.lockWithOptions(resourceString, 200, {retryCount:10,retryDelay:1}, function(err, lock){ - if(err) throw err; - assert.isObject(lock); - assert.instanceOf(lock, Redlock.Lock); - assert.isAbove(lock.expiration, Date.now()-1); - assert.equal(lock.attempts, 1); - assert.equal(lock.attemptsRemaining, 9); - lock.unlock(done); - }); - }); - it('should be backwards compatible', function(done) { - redlock._lock(resourceString, null, 200, function(err, lock){ - if(err) throw err; - assert.isObject(lock); - assert.instanceOf(lock, Redlock.Lock); - assert.isAbove(lock.expiration, Date.now()-1); - assert.equal(lock.attempts, 1); - assert.equal(lock.attemptsRemaining, 1); - lock.unlock(done); - }); - }); - }); - - after(function(done) { - var err; - var l = clients.length; function cb(e){ if(e) err = e; l--; if(l === 0) done(err); } - for (var i = clients.length - 1; i >= 0; i--) { - clients[i].del(resourceString, cb); - } - }); - }); - - describe('promises', function(){ - before(function(done) { - var err; - var l = clients.length; function cb(e){ if(e) err = e; l--; if(l === 0) done(err); } - for (var i = clients.length - 1; i >= 0; i--) { - clients[i].del(resourceString, cb); - } - }); - - var one; - it('should lock a resource', function(done) { - redlock.lock(resourceString, 200) - .done(function(lock){ - assert.isObject(lock); - assert.isAbove(lock.expiration, Date.now()-1); - assert.equal(lock.attempts, 1); - one = lock; - done(); - }, done); - }); - - var two; - var two_expiration; - it('should wait until a lock expires before issuing another lock', function(done) { - assert(one, 'Could not run because a required previous test failed.'); - redlock.lock(resourceString, 800) - .done(function(lock){ - assert.isObject(lock); - assert.isAbove(lock.expiration, Date.now()-1); - assert.isAbove(Date.now()+1, one.expiration); - assert.isAbove(lock.attempts, 1); - two = lock; - two_expiration = lock.expiration; - done(); - }, done); - }); - - it('should unlock a resource', function(done) { - assert(two, 'Could not run because a required previous test failed.'); - two.unlock().done(done, done); - }); - - it('should unlock an already-unlocked resource', function(done) { - assert(two, 'Could not run because a required previous test failed.'); - two.unlock().done(function(result) { - done(new Error('Expected an error.')); - }, function(err) { - done(); - }); - }); - - it('should error when unable to fully release a resource', function(done) { - assert(two, 'Could not run because a required previous test failed.'); - var failingTwo = Object.create(two); - failingTwo.resource = error; - failingTwo.unlock().done(done, function(err) { - assert.isNotNull(err); - done(); - }); - }); - - it('should fail to extend a lock on an already-unlocked resource', function(done) { - assert(two, 'Could not run because a required previous test failed.'); - two.extend(200) - .done(function(){ - done(new Error('Should have failed with a LockError')); - }, function(err){ - assert.instanceOf(err, Redlock.LockError); - assert.equal(err.attempts, 0); - done(); - }); - }); - - var three; - it('should issue another lock immediately after a resource is unlocked', function(done) { - assert(two_expiration, 'Could not run because a required previous test failed.'); - redlock.lock(resourceString, 800) - .done(function(lock){ - assert.isObject(lock); - assert.isAbove(lock.expiration, Date.now()-1); - assert.isBelow(Date.now()-1, two_expiration); - assert.equal(lock.attempts, 1); - three = lock; - done(); - }, done); - }); - - var four; - it('should extend an unexpired lock', function(done) { - assert(three, 'Could not run because a required previous test failed.'); - three.extend(800) - .done(function(lock){ - assert.isObject(lock); - assert.isAbove(lock.expiration, Date.now()-1); - assert.isAbove(lock.expiration, three.expiration-1); - assert.equal(lock.attempts, 1); - assert.equal(three, lock); - four = lock; - done(); - }, done); - }); - - it('should fail after the maximum retry count is exceeded', function(done) { - assert(four, 'Could not run because a required previous test failed.'); - redlock.lock(resourceString, 200) - .done(function(){ - done(new Error('Should have failed with a LockError')); - }, function(err){ - assert.instanceOf(err, Redlock.LockError); - assert.equal(err.attempts, 3); - done(); - }); - }); - - it('should fail to extend an expired lock', function(done) { - assert(four, 'Could not run because a required previous test failed.'); - setTimeout(function(){ - three.extend(800) - .done(function(){ - done(new Error('Should have failed with a LockError')); - }, function(err){ - assert.instanceOf(err, Redlock.LockError); - assert.equal(err.attempts, 0); - done(); - }); - }, four.expiration - Date.now() + 100); - }); - - after(function(done) { - var err; - var l = clients.length; function cb(e){ if(e) err = e; l--; if(l === 0) done(err); } - for (var i = clients.length - 1; i >= 0; i--) { - clients[i].del(resourceString, cb); - } - }); - }); - - describe('disposer', function(){ - before(function(done) { - var err; - var l = clients.length; function cb(e){ if(e) err = e; l--; if(l === 0) done(err); } - for (var i = clients.length - 1; i >= 0; i--) { - clients[i].del(resourceString, cb); - } - }); - - var one; - var one_expiration; - it('should automatically release a lock after the using block', function(done) { - Promise.using( - redlock.disposer(resourceString, 200), - function(lock){ - assert.isObject(lock); - assert.isAbove(lock.expiration, Date.now()-1); - assert.equal(lock.attempts, 1); - one = lock; - one_expiration = lock.expiration; - } - ).done(done, done); - }); - - var two; - var two_expiration; - it('should issue another lock immediately after a resource is unlocked', function(done) { - assert(one_expiration, 'Could not run because a required previous test failed.'); - Promise.using( - redlock.disposer(resourceString, 800), - function(lock){ - assert.isObject(lock); - assert.isAbove(lock.expiration, Date.now()-1); - assert.isBelow(Date.now()-1, one_expiration); - assert.equal(lock.attempts, 1); - two = lock; - two_expiration = lock.expiration; - } - ).done(done, done); - }); - - it('should call unlockErrorHandler when unable to fully release a resource', function(done) { - assert(two, 'Could not run because a required previous test failed.'); - var errs = 0; - var lock; - Promise.using( - redlock.disposer(resourceString, 800, function(err) { - errs++; - }), - function(l){ - lock = l; - lock.resource = error; - } - ).done(function() { - assert.equal(errs, 1); - lock.resource = resourceString; - lock.unlock().done(done, done); - }, done); - }); - - var three_original, three_extended; - var three_original_expiration; - var three_extended_expiration; - it('should automatically release an extended lock', function(done) { - assert(two_expiration, 'Could not run because a required previous test failed.'); - Promise.using( - redlock.disposer(resourceString, 200), - function(lock){ - assert.isObject(lock); - assert.isAbove(lock.expiration, Date.now()-1); - assert.isBelow(Date.now()-1, two_expiration); - three_original = lock; - three_original_expiration = lock.expiration; - - return Promise.delay(100) - .then(function(){ return lock.extend(200); }) - .then(function(extended) { - assert.isObject(extended); - assert.isAbove(extended.expiration, Date.now()-1); - assert.isBelow(Date.now()-1, three_original_expiration); - assert.isAbove(extended.expiration, three_original_expiration); - assert.equal(lock.attempts, 1); - assert.equal(extended, lock); - three_extended = extended; - three_extended_expiration = extended.expiration; - }); - } - ) - .then(function(){ - assert.equal(three_original.expiration, 0); - assert.equal(three_extended.expiration, 0); - }).done(done, done); - }); - - after(function(done) { - var err; - var l = clients.length; function cb(e){ if(e) err = e; l--; if(l === 0) done(err); } - for (var i = clients.length - 1; i >= 0; i--) { - clients[i].del(resourceString, cb); - } - }); - }); - - describe('callbacks - multi', function(){ - before(function(done) { - var err; - var l = clients.length; function cb(e){ if(e) err = e; l--; if(l === 0) done(err); } - for (var i = clients.length - 1; i >= 0; i--) { - for (var j = resourceArray.length - 1; j >= 0; j--) { - clients[i].del(resourceArray[j], cb); - } - } - }); - - var one; - it('should lock a multivalue resource', function(done) { - redlock.lock(resourceArray, 200, function(err, lock){ - if(err) throw err; - assert.isObject(lock); - assert.instanceOf(lock, Redlock.Lock); - assert.isAbove(lock.expiration, Date.now()-1); - assert.equal(lock.attempts, 1); - one = lock; - done(); - }); - }); - - var two; - var two_expiration; - it('should wait until a lock expires before issuing another lock', function(done) { - assert(one, 'Could not run because a required previous test failed.'); - redlock.lock(resourceArray, 800, function(err, lock){ - if(err) throw err; - assert.isObject(lock); - assert.isAbove(lock.expiration, Date.now()-1); - assert.isAbove(Date.now()+1, one.expiration); - assert.isAbove(lock.attempts, 1); - two = lock; - two_expiration = lock.expiration; - done(); - }); - }); - - it('should unlock a multivalue resource', function(done) { - assert(two, 'Could not run because a required previous test failed.'); - two.unlock(done); - assert.equal(two.expiration, 0, 'Failed to immediately invalidate the lock.'); - }); - - it('should unlock an already-unlocked multivalue resource', function(done) { - assert(two, 'Could not run because a required previous test failed.'); - two.unlock(function(err) { - assert.isNotNull(err) - done(); - }); - }); - - it('should error when unable to fully release a multivalue resource', function(done) { - assert(two, 'Could not run because a required previous test failed.'); - var failingTwo = Object.create(two); - failingTwo.resource = error; - failingTwo.unlock(function(err) { - assert.isNotNull(err); - done(); - }); - }); - - it('should fail to extend a lock on an already-unlocked multivalue resource', function(done) { - assert(two, 'Could not run because a required previous test failed.'); - two.extend(200, function(err, lock){ - assert.isNotNull(err); - assert.instanceOf(err, Redlock.LockError); - assert.equal(err.attempts, 0); - done(); - }); - }); - - var three; - it('should issue another lock immediately after a multivalue resource is unlocked', function(done) { - assert(two_expiration, 'Could not run because a required previous test failed.'); - redlock.lock(resourceArray, 800, function(err, lock){ - if(err) throw err; - assert.isObject(lock); - assert.isAbove(lock.expiration, Date.now()-1); - assert.isBelow(Date.now()-1, two_expiration); - assert.equal(lock.attempts, 1); - three = lock; - done(); - }); - }); - - var four; - it('should extend an unexpired multivalue lock', function(done) { - assert(three, 'Could not run because a required previous test failed.'); - three.extend(800, function(err, lock){ - if(err) throw err; - assert.isObject(lock); - assert.isAbove(lock.expiration, Date.now()-1); - assert.isAbove(lock.expiration, three.expiration-1); - assert.equal(lock.attempts, 1); - assert.equal(three, lock); - four = lock; - done(); - }); - }); - - it('should fail after the maximum retry count is exceeded', function(done) { - assert(four, 'Could not run because a required previous test failed.'); - redlock.lock(resourceArray, 200, function(err, lock){ - assert.isNotNull(err); - assert.instanceOf(err, Redlock.LockError); - assert.equal(err.attempts, 3); - done(); - }); - }); - - it('should fail to extend an expired lock', function(done) { - assert(four, 'Could not run because a required previous test failed.'); - setTimeout(function(){ - three.extend(800, function(err, lock){ - assert.isNotNull(err); - assert.instanceOf(err, Redlock.LockError); - assert.equal(err.attempts, 0); - done(); - }); - }, four.expiration - Date.now() + 100); - }); - - it('should issue another lock immediately after a resource is expired', function(done) { - assert(four, 'Could not run because a required previous test failed.'); - redlock.lock(resourceArray, 800, function(err, lock){ - if(err) throw err; - assert.isObject(lock); - assert.isAbove(lock.expiration, Date.now()-1); - assert.equal(lock.attempts, 1); - done(); - }); - }); - - after(function(done) { - var err; - var l = clients.length; function cb(e){ if(e) err = e; l--; if(l === 0) done(err); } - for (var i = clients.length - 1; i >= 0; i--) { - for (var j = resourceArray.length - 1; j >= 0; j--) { - clients[i].del(resourceArray[j], cb); - } - } - }); - }); - - describe('promises - multi', function(){ - before(function(done) { - var err; - var l = clients.length; function cb(e){ if(e) err = e; l--; if(l === 0) done(err); } - for (var i = clients.length - 1; i >= 0; i--) { - for (var j = resourceArray.length - 1; j >= 0; j--) { - clients[i].del(resourceArray[j], cb); - } - } - }); - - var one; - it('should lock a multivalue resource', function(done) { - redlock.lock(resourceArray, 200) - .done(function(lock){ - assert.isObject(lock); - assert.isAbove(lock.expiration, Date.now()-1); - assert.equal(lock.attempts, 1); - one = lock; - done(); - }, done); - }); - - var two; - var two_expiration; - it('should wait until a multivalue lock expires before issuing another lock', function(done) { - assert(one, 'Could not run because a required previous test failed.'); - redlock.lock(resourceArray, 800) - .done(function(lock){ - assert.isObject(lock); - assert.isAbove(lock.expiration, Date.now()-1); - assert.isAbove(Date.now()+1, one.expiration); - assert.isAbove(lock.attempts, 1); - two = lock; - two_expiration = lock.expiration; - done(); - }, done); - }); - - it('should unlock a multivalue resource', function(done) { - assert(two, 'Could not run because a required previous test failed.'); - two.unlock().done(done, done); - }); - - it('should unlock an already-unlocked multivalue resource', function(done) { - assert(two, 'Could not run because a required previous test failed.'); - two.unlock().done(function(result) { - done(new Error('Expected an error.')); - }, function(err) { - done(); - }); - }); - - it('should error when unable to fully release a multivalue resource', function(done) { - assert(two, 'Could not run because a required previous test failed.'); - var failingTwo = Object.create(two); - failingTwo.resource = error; - failingTwo.unlock().done(done, function(err) { - assert.isNotNull(err); - done(); - }); - }); - - it('should fail to extend a lock on an already-unlocked multivalue resource', function(done) { - assert(two, 'Could not run because a required previous test failed.'); - two.extend(200) - .done(function(){ - done(new Error('Should have failed with a LockError')); - }, function(err){ - assert.instanceOf(err, Redlock.LockError); - assert.equal(err.attempts, 0); - done(); - }); - }); - - var three; - it('should issue another lock immediately after a multivalue resource is unlocked', function(done) { - assert(two_expiration, 'Could not run because a required previous test failed.'); - redlock.lock(resourceArray, 800) - .done(function(lock){ - assert.isObject(lock); - assert.isAbove(lock.expiration, Date.now()-1); - assert.isBelow(Date.now()-1, two_expiration); - assert.equal(lock.attempts, 1); - three = lock; - done(); - }, done); - }); - - var four; - it('should extend an unexpired lock', function(done) { - assert(three, 'Could not run because a required previous test failed.'); - three.extend(800) - .done(function(lock){ - assert.isObject(lock); - assert.isAbove(lock.expiration, Date.now()-1); - assert.isAbove(lock.expiration, three.expiration-1); - assert.equal(lock.attempts, 1); - assert.equal(three, lock); - four = lock; - done(); - }, done); - }); - - it('should fail after the maximum retry count is exceeded', function(done) { - assert(four, 'Could not run because a required previous test failed.'); - redlock.lock(resourceArray, 200) - .done(function(){ - done(new Error('Should have failed with a LockError')); - }, function(err){ - assert.instanceOf(err, Redlock.LockError); - assert.equal(err.attempts, 3); - done(); - }); - }); - - it('should fail to extend an expired lock', function(done) { - assert(four, 'Could not run because a required previous test failed.'); - setTimeout(function(){ - three.extend(800) - .done(function(){ - done(new Error('Should have failed with a LockError')); - }, function(err){ - assert.instanceOf(err, Redlock.LockError); - assert.equal(err.attempts, 0); - done(); - }); - }, four.expiration - Date.now() + 100); - }); - - after(function(done) { - var err; - var l = clients.length; function cb(e){ if(e) err = e; l--; if(l === 0) done(err); } - for (var i = clients.length - 1; i >= 0; i--) { - for (var j = resourceArray.length - 1; j >= 0; j--) { - clients[i].del(resourceArray[j], cb); - } - } - }); - }); - - describe('disposer - multi', function(){ - before(function(done) { - var err; - var l = clients.length; function cb(e){ if(e) err = e; l--; if(l === 0) done(err); } - for (var i = clients.length - 1; i >= 0; i--) { - for (var j = resourceArray.length - 1; j >= 0; j--) { - clients[i].del(resourceArray[j], cb); - } - } - }); - - var one; - var one_expiration; - it('should automatically release a lock after the using block', function(done) { - Promise.using( - redlock.disposer(resourceArray, 200), - function(lock){ - assert.isObject(lock); - assert.isAbove(lock.expiration, Date.now()-1); - assert.equal(lock.attempts, 1); - one = lock; - one_expiration = lock.expiration; - } - ).done(done, done); - }); - - var two; - var two_expiration; - it('should issue another lock immediately after a resource is unlocked', function(done) { - assert(one_expiration, 'Could not run because a required previous test failed.'); - Promise.using( - redlock.disposer(resourceArray, 800), - function(lock){ - assert.isObject(lock); - assert.isAbove(lock.expiration, Date.now()-1); - assert.isBelow(Date.now()-1, one_expiration); - assert.equal(lock.attempts, 1); - two = lock; - two_expiration = lock.expiration; - } - ).done(done, done); - }); - - it('should call unlockErrorHandler when unable to fully release a resource', function(done) { - assert(two, 'Could not run because a required previous test failed.'); - var errs = 0; - var lock; - Promise.using( - redlock.disposer(resourceArray, 800, function(err) { - errs++; - }), - function(l){ - lock = l; - lock.resource = error; - } - ).done(function() { - assert.equal(errs, 1); - lock.resource = resourceArray; - lock.unlock().done(done, done); - }, done); - }); - - var three_original, three_extended; - var three_original_expiration; - var three_extended_expiration; - it('should automatically release an extended lock', function(done) { - assert(two_expiration, 'Could not run because a required previous test failed.'); - Promise.using( - redlock.disposer(resourceArray, 200), - function(lock){ - assert.isObject(lock); - assert.isAbove(lock.expiration, Date.now()-1); - assert.isBelow(Date.now()-1, two_expiration); - three_original = lock; - three_original_expiration = lock.expiration; - return Promise.delay(100) - .then(function(){ return lock.extend(200); }) - .then(function(extended) { - assert.isObject(extended); - assert.isAbove(extended.expiration, Date.now()-1); - assert.isBelow(Date.now()-1, three_original_expiration); - assert.isAbove(extended.expiration, three_original_expiration); - assert.equal(lock.attempts, 1); - assert.equal(extended, lock); - three_extended = extended; - three_extended_expiration = extended.expiration; - }); - } - ) - .then(function(){ - assert.equal(three_original.expiration, 0); - assert.equal(three_extended.expiration, 0); - }).done(done, done); - }); - - after(function(done) { - var err; - var l = clients.length; function cb(e){ if(e) err = e; l--; if(l === 0) done(err); } - for (var i = clients.length - 1; i >= 0; i--) { - for (var j = resourceArray.length - 1; j >= 0; j--) { - clients[i].del(resourceArray[j], cb); - } - } - }); - }); - - describe('quit', function() { - it('should quit all clients', function(done){ - redlock.quit() - .done(function(results) { - assert.isArray(results); - done(); - }, done); - }); - }) - - }); -} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..f5ec5ab --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "lib": ["es2020"], + "target": "es2018", + "module": "es2020", + "outDir": "./dist", + "declaration": true, + "declarationDir": "./dist", + "moduleResolution": "node", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": false, + "sourceMap": true + }, + "include": ["./src/**/*"] +}