1
0
mirror of https://github.com/redis/node-redis.git synced 2025-08-06 02:15:48 +03:00
* init v4

* add .gitignore to benchmark

* spawn redis-servers for tests,
add some tests,
fix client auth on connect

* add tests coverage report

* add tests workflow, replace nyc text reporter with text-summary

* run tests with node 16.x & redis 6.x only (for now)

* add socket events on client,
stop reconnectiong when manually calling disconnect,
remove abort signal listener when a command is written on the socket

* add isOpen boolean getter on client, add maxLength option to command queue, add test for client.multi

* move to use CommonJS

* add MULTI and EXEC commands to when executing multi command, make client.multi return type innerit the module commands, clean some tests, exclute spec files from coverage report

* missing file from commit 61edd4f1b5

* exclude spec files from coverage report

* add support for options in a command function (.get, .set, ...), add support for the SELECT command, implement a couple of commands, fix client socket reconnection strategy, add support for using replicas (RO) in cluster, and more..

* fix client.blPop test

* use which to find redis-server path

* change command options to work with Symbol rather then WeakSet

* implement more commands

* Add support for lua scripts in client & muilti, fix client socket initiator, implement simple cluster nodes discovery strategy

* replace `callbackify` with `legacyMode`

* add the SCAN command and client.scanIterator

* rename scanIterator

* init benchmark workflow

* fix benchmark workflow

* fix benchmark workflow

* fix benchmark workflow

* push coverage report to Coveralls

* fix Coveralls

* generator lcov (for Coveralls)

* fix .nycrc.json

* PubSub

* add support for all set commands (including sScanIterator)

* support pipeline

* fix KEEPTTL in SET

* remove console.log

* add HyperLogLog commands

* update README.md (thanks to @guyroyse)

* add support for most of the "keys commands"

* fix EXPIREAT.spec.ts

* add support for date in both EXPIREAT & EXPIRE

* add tests

* better cluster nodes discorvery strategy after MOVED error, add PubSub test

* fix PubSub UNSUBSCRIBE/PUNSUBSCRIBE without channel and/or listener

* fix PubSub

* add release-it to dev dependencies

* Release 4.0.0-next.0

* fix .npmignore

* Release 4.0.0-next.1

* fix links in README.md

* fix .npmignore

* Release 4.0.0-next.2

* add support for all sorted set commands

* add support for most stream commands

* add missing file from commit 53de279afe

* lots of todo commends

* make PubSub test more stable

* clean ZPOPMAX

* add support for lua scripts and modules in cluster, spawn cluster for tests, add some cluster tests, fix pubsub listener arguments

* GET.spec.ts

* add support for List commands, fix some Sorted Set commands, add some cluster commands, spawn cluster for testing, add support for command options in cluster, and more

* add missing file from commit faab94fab2

* clean ZRANK and ZREVRANK

* add XREAD and XREADGROUP commands

* remove unused files

* implement a couple of more commands, make cluster random iterator be per node (instead of per slot)

* Release 4.0.0-next.3

* app spec files to npmignore

* fix some code analyzers (LGTM, deepsource, codeclimate) issues

* fix CLUSTER_NODES, add some tests

* add HSCAN, clean some commands, add tests for generic transformers

* add missing files from 0feb35a1fb

* update README.md (thanks to @guyroyse)

* handle ASK errors, add some commands and tests

* Release 4.0.0-next.4

* replace "modern" with "v4"

* remove unused imports

* add all ACL subcommands, all MODULE subcommands, and some other commands

* remove 2 unused imports

* fix BITFIELD command

* fix XTRIM spec file

* clean code

* fix package.json types field

* better modules support, fix some bugs in legacy mode, add some tests

* remove unused function

* add test for hScanIterator

* change node mimimum version to 12 (latest LTS)

* update tsconfig.json to support node 12, run tests on Redis 5 & 6 and on all node live versions

* remove future node releases :P

* remove "lib" from ts compiler options

* Update tsconfig.json

* fix build

* run some tests only on supported redis versions, use coveralls parallel mode

* fix tests

* Do not use "timers/promises", fix "isRedisVersionGreaterThan"

* skip AbortController tests when not available

* use 'fs'.promises instead of 'fs/promises'

* add some missing commands

* run GETDEL tests only if the redis version is greater than 6.2

* implement some GEO commands, improve scan generic transformer, expose RPUSHX

* fix GEOSEARCH & GEOSEARCHSTORE

* use socket.setNoDelay and queueMicrotask to improve latency

* commands-queue.ts: String length / byte length counting issue (#1630)

* Update commands-queue.ts

Hopefully fixing #1628

* Reverted 2fa5ea6, and implemented test for byte length check

* Changed back to Buffer.byteLength, due to issue author input. Updated test to look for 4 bytes.

* Fixed. There were two places that length was calculated.

* Removed redundant string assignment

* add 2 bytes test as well

Co-authored-by: Leibale Eidelman <leibale1998@gmail.com>

* fix scripts in multi

* do not hide bugs in redis

* fix for e7bf09644b

* remove unused import

* implement WATCH command, fix ZRANGESTORE & GEOSEARCHSTORE tests

* update README.md

Co-authored-by: @GuyRoyse

* use typedoc to auto generate documentation

* run "npm install" before "npm run documentation"

* clean documentation workflow

* fix WATCH spec file

* increase "CLUSTER_NODE_TIMEOUT" to 5000ms to avoid "CLUSTERDOWN" errors in tests

* pull cluster state every 100 ms

* await meetPromises before pulling the cluster state

* enhance the way commanders (client/multi/cluster) get extended with modules and scripts

* add test for socket retry strategy

* implement more commands

* set GETEX minimum version to 6.2

* remove unused imports

* add support for multi in cluster

* upgrade dependencies

* Release 4.0.0-next.5

* remove unused imports

* improve benchmarking

* use the same Multi with duplicated clients

* exclude some files from the documentation, add some exports, clean code

* fix #1636 - handle null in multi.exec

* remove unused import

* add supoprt for tuples in HSET

* add FIRST_KEY_INDEX to HSET

* add a bunch of missing commands, fix MSET and HELLO, add some tests

* add FIRST_KEY_INDEX to MSET and MSETNX

* upgrade actions

* fix coverallsapp/github-action version

* Update documentation.yml

* Update documentation.yml

* clean code

* remove unused imports

* use "npm ci" instead of "npm install"

* fix `self` binding on client modules, use connection pool for `duplicateConnection`

* add client.executeIsolated, rename "duplicateConnection" to "isolated", update README.md (thanks to @GuyRoyse and @SimonPrickett)

* update README (thanks to @GuyRoyse), add some tests

* try to fix "cluster is down" errors in tests

* try to fix "cluster is down" errors in tests

* upgrade dependencies

* update package-lock

* Release 4.0.0-next.6

* fix #1636 - fix WatchError

* fix for f1bf0beebf - remove .only from multi tests

* Release 4.0.0-next.7

* update README and other markdown files

Co-authored-by: @GuyRoyse & @SimonPrickett

* Doc updates. (#1640)

* update docs, upgrade dependencies

* fix README

* Release 4.0.0-rc.0

* Update README.md

* update docs, add `connectTimeout` options, fix tls

Co-authored-by: Guy Royse <guy@guyroyse.com>

* npm update, "fix" some tests, clean code

* fix AssertionError import

* fix #1642 - fix XREAD, XREADGROUP and XTRIM

* fix #1644 - add the QUIT command

* add socket.noDelay and socket.keepAlive configurations

* Update README.md (#1645)

* Update README.md

Fixed issue with how connection string was specified.
Now you can have user@host without having to specify a password, which just makes more sense

* Update client-configuration.md as well

Co-authored-by: Leibale Eidelman <leibale1998@gmail.com>

* update socket.reconnectStrategy description

* fix borken link in v3-to-v4.md

* increase test coverage, fix bug in cluster redirection strategy, implement CLIENT_ID, remove unused EXEC command

Co-authored-by: Nova <novaw@warrenservices.co.uk>
Co-authored-by: Simon Prickett <simon@crudworks.org>
Co-authored-by: Guy Royse <guy@guyroyse.com>
This commit is contained in:
Leibale Eidelman
2021-09-02 10:04:48 -04:00
committed by GitHub
parent 4f85030e42
commit 4e6d018d77
661 changed files with 28847 additions and 14559 deletions

View File

@@ -1,9 +0,0 @@
version = 1
exclude_patterns = ["examples/**"]
[[analyzers]]
name = "javascript"
enabled = true
[analyzers.meta]
environment = ["nodejs"]

View File

@@ -1,4 +0,0 @@
node_modules/**
coverage/**
**.md
**.log

109
.eslintrc
View File

@@ -1,109 +0,0 @@
env:
node: true
es6: false
rules:
# Possible Errors
# http://eslint.org/docs/rules/#possible-errors
comma-dangle: [2, "only-multiline"]
no-constant-condition: 2
no-control-regex: 2
no-debugger: 2
no-dupe-args: 2
no-dupe-keys: 2
no-duplicate-case: 2
no-empty: 2
no-empty-character-class: 2
no-ex-assign: 2
no-extra-boolean-cast : 2
no-extra-parens: [2, "functions"]
no-extra-semi: 2
no-func-assign: 2
no-invalid-regexp: 2
no-irregular-whitespace: 2
no-negated-in-lhs: 2
no-obj-calls: 2
no-regex-spaces: 2
no-sparse-arrays: 2
no-inner-declarations: 2
no-unexpected-multiline: 2
no-unreachable: 2
use-isnan: 2
valid-typeof: 2
# Best Practices
# http://eslint.org/docs/rules/#best-practices
array-callback-return: 2
block-scoped-var: 2
dot-notation: 2
eqeqeq: 2
no-else-return: 2
no-extend-native: 2
no-floating-decimal: 2
no-extra-bind: 2
no-fallthrough: 2
no-labels: 2
no-lone-blocks: 2
no-loop-func: 2
no-multi-spaces: 2
no-multi-str: 2
no-native-reassign: 2
no-new-wrappers: 2
no-octal: 2
no-proto: 2
no-redeclare: 2
no-return-assign: 2
no-self-assign: 2
no-self-compare: 2
no-sequences: 2
no-throw-literal: 2
no-useless-call: 2
no-useless-concat: 2
no-useless-escape: 2
no-void: 2
no-unmodified-loop-condition: 2
yoda: 2
# Strict Mode
# http://eslint.org/docs/rules/#strict-mode
strict: [2, "global"]
# Variables
# http://eslint.org/docs/rules/#variables
no-delete-var: 2
no-shadow-restricted-names: 2
no-undef: 2
no-unused-vars: [2, {"args": "none"}]
# http://eslint.org/docs/rules/#nodejs-and-commonjs
no-mixed-requires: 2
no-new-require: 2
no-path-concat: 2
# Stylistic Issues
# http://eslint.org/docs/rules/#stylistic-issues
comma-spacing: 2
eol-last: 2
indent: [2, 4, {SwitchCase: 2}]
keyword-spacing: 2
max-len: [2, 200, 2]
new-parens: 2
no-mixed-spaces-and-tabs: 2
no-multiple-empty-lines: [2, {max: 2}]
no-trailing-spaces: 2
quotes: [2, "single", "avoid-escape"]
semi: 2
space-before-blocks: [2, "always"]
space-before-function-paren: [2, "always"]
space-in-parens: [2, "never"]
space-infix-ops: 2
space-unary-ops: 2
globals:
it: true
describe: true
xdescribe: true
before: true
after: true
beforeEach: true
afterEach: true

1
.github/FUNDING.yml vendored
View File

@@ -1 +0,0 @@
open_collective: node-redis

View File

@@ -5,21 +5,8 @@ labels: needs-triage
### Issue ### Issue
<!--
Thanks for wanting to report an issue you've found in node_redis. Please delete
this text and fill in the template below. Please note that the issue tracker is only
for bug reports or feature requests.
If you have a question, please ask it on Stack Overflow using the `node-redis` tag.
Note that it will be much easier to fix the issue if a test case that reproduces
the problem is provided. It is of course not always possible to reduce your code
with a small test case, but it's highly appreciated to have as much data as possible.
-->
> Describe your issue here > Describe your issue here
--- ---
### Environment ### Environment
@@ -28,7 +15,7 @@ with a small test case, but it's highly appreciated to have as much data as poss
- **Node.js Version**: `VERSION_HERE` - **Node.js Version**: `VERSION_HERE`
<!-- e.g. "redis-server --version" --> <!-- e.g. "redis-server --version" -->
- **Redis Version**: `VERSION_HERE` - **Redis Server Version**: `VERSION_HERE`
<!-- e.g. Windows 10, Mac OSX 10.15.2 --> <!-- e.g. Windows 10, Mac OSX 10.15.2 -->
- **Platform**: `PLATFORM_HERE` - **Platform**: `PLATFORM_HERE`

View File

@@ -1,13 +1,10 @@
<!-- please ensure you have read the `./CONTRIBUTING.md` guide -->
### Description ### Description
<!-- Please provide a description of the change below, e.g What was the purpose? --> <!-- Please provide a description of the change below, e.g What was the purpose? -->
<!-- Why does it matter to you? What problem are you trying to solve? --> <!-- Why does it matter to you? What problem are you trying to solve? -->
<!-- Tag in any linked issues. --> <!-- Tag in any linked issues. -->
> Description your pull request here > Describe your pull request here
--- ---

View File

@@ -1,6 +1,9 @@
name: Benchmarking name: Benchmark
on: [pull_request] on:
push:
branches:
- v4
jobs: jobs:
benchmark: benchmark:
@@ -9,8 +12,8 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
node-version: [10.x, 12.x, 14.x, 15.x] node-version: [16.x]
redis-version: [5.x, 6.x] redis-version: [6.x]
steps: steps:
- uses: actions/checkout@v2.3.4 - uses: actions/checkout@v2.3.4
@@ -18,21 +21,25 @@ jobs:
fetch-depth: 1 fetch-depth: 1
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v2.1.5 uses: actions/setup-node@v2.3.0
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
- name: Setup Redis - name: Setup Redis
uses: shogo82148/actions-setup-redis@v1.9.7 uses: shogo82148/actions-setup-redis@v1.12.0
with: with:
redis-version: ${{ matrix.redis-version }} redis-version: ${{ matrix.redis-version }}
auto-start: "true"
- run: npm i --no-audit --prefer-offline - name: Install Packages
- name: Run Benchmark run: npm ci
run: npm run benchmark > benchmark-output.txt && cat benchmark-output.txt
- name: Upload Benchmark Result - name: Build
uses: actions/upload-artifact@v2.2.2 run: npm run build
with:
name: benchmark-output.txt - name: Install Benchmark Packages
path: benchmark-output.txt run: npm ci
working-directory: ./benchmark
- name: Benchmark
run: npm run start
working-directory: ./benchmark

View File

@@ -1,67 +0,0 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [ master ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ master ]
schedule:
- cron: '35 0 * * 4'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
language: [ 'javascript' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
# Learn more:
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
steps:
- name: Checkout repository
uses: actions/checkout@v2
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1

31
.github/workflows/documentation.yml vendored Normal file
View File

@@ -0,0 +1,31 @@
name: Documentation
on:
push:
branches:
- v4
jobs:
documentation:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2.3.4
with:
fetch-depth: 1
- name: Use Node.js
uses: actions/setup-node@v2.3.0
- name: Install Packages
run: npm ci
- name: Generate Documentation
run: npm run documentation
- name: Upload Documentation to Wiki
uses: SwiftDocOrg/github-wiki-publish-action@v1
with:
path: documentation
env:
GH_PERSONAL_ACCESS_TOKEN: ${{ secrets.BOT_PERSONAL_ACCESS_TOKEN }}

View File

@@ -1,31 +0,0 @@
name: Linting
on: [pull_request]
jobs:
eslint:
name: ESLint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2.3.4
with:
fetch-depth: 1
- uses: actions/setup-node@v2.1.5
with:
node-version: 12
- run: npm i --no-audit --prefer-offline
- name: Test Code Linting
run: npm run lint
- name: Save Code Linting Report JSON
run: npm run lint:report
continue-on-error: true
- name: Annotate Code Linting Results
uses: ataylorme/eslint-annotate-action@1.1.2
with:
repo-token: "${{ secrets.GITHUB_TOKEN }}"
report-json: "eslint-report.json"
- name: Upload ESLint report
uses: actions/upload-artifact@v2.2.2
with:
name: eslint-report.json
path: eslint-report.json

View File

@@ -1,16 +1,18 @@
name: Tests name: Tests
on: [push] on:
push:
branches:
- v4
jobs: jobs:
testing: tests:
name: Test
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
node-version: [10.x, 12.x, 14.x, 15.x] node-version: [12.x, 14.x, 16.x]
redis-version: [4.x, 5.x, 6.x] redis-version: [5.x, 6.x]
steps: steps:
- uses: actions/checkout@v2.3.4 - uses: actions/checkout@v2.3.4
@@ -18,35 +20,38 @@ jobs:
fetch-depth: 1 fetch-depth: 1
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v2.1.5 uses: actions/setup-node@v2.3.0
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
- name: Setup Redis - name: Setup Redis
uses: shogo82148/actions-setup-redis@v1.9.7 uses: shogo82148/actions-setup-redis@v1.12.0
with: with:
redis-version: ${{ matrix.redis-version }} redis-version: ${{ matrix.redis-version }}
auto-start: "false" auto-start: "false"
- name: Disable IPv6
run: sudo sh -c 'echo 0 > /proc/sys/net/ipv6/conf/all/disable_ipv6';
- name: Setup Stunnel
run: sudo apt-get install stunnel4
- name: Install Packages - name: Install Packages
run: npm i --no-audit --prefer-offline run: npm ci
- name: Run Tests - name: Run Tests
run: npm test run: npm run test
- name: Submit Coverage - name: Generate lcov
run: npm run coveralls run: ./node_modules/.bin/nyc report -r lcov
env:
COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_TOKEN }}
- name: Upload Coverage Report - name: Coveralls
uses: actions/upload-artifact@v2.2.2 uses: coverallsapp/github-action@1.1.3
with: with:
name: coverage github-token: ${{ secrets.GITHUB_TOKEN }}
path: coverage flag-name: Node ${{ matrix.node-version }} Redis ${{ matrix.redis-version }}
parallel: true
finish:
needs: tests
runs-on: ubuntu-latest
steps:
- name: Coveralls Finished
uses: coverallsapp/github-action@1.1.3
with:
github-token: ${{ secrets.github_token }}
parallel-finished: true

View File

@@ -1,49 +0,0 @@
name: Tests Windows
on: [push]
jobs:
testing-windows:
name: Test Windows
runs-on: windows-latest
strategy:
fail-fast: false
matrix:
node-version: [10.x, 12.x, 14.x, 15.x]
steps:
- uses: actions/checkout@v2.3.4
with:
fetch-depth: 1
- name: Install Redis
uses: crazy-max/ghaction-chocolatey@v1.4.0
with:
args: install redis-64 --version=3.0.503 --no-progress
- name: Start Redis
run: |
redis-server --service-install
redis-server --service-start
redis-cli config set stop-writes-on-bgsave-error no
- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v2.1.5
with:
node-version: ${{ matrix.node-version }}
- name: Install Packages
run: npm i --no-audit --prefer-offline
- name: Run Tests
run: npm test
- name: Submit Coverage
run: npm run coveralls
env:
COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_TOKEN }}
- name: Upload Coverage Report
uses: actions/upload-artifact@v2.2.2
with:
name: coverage
path: coverage

25
.gitignore vendored
View File

@@ -1,17 +1,8 @@
node_modules .vscode/
.tern-port .idea/
.nyc_output node_modules/
coverage dist/
*.log .nyc_output/
*.rdb coverage/
stunnel.conf dump.rdb
stunnel.pid documentation/
*.out
package-lock.json
# IntelliJ IDEs
.idea
# VisualStudioCode IDEs
.vscode
.vs
eslint-report.json

View File

@@ -1,23 +1,17 @@
examples/ .vscode/
benchmarks/ .idea/
test/ node_modules/
.nyc_output/ .nyc_output
coverage/ coverage/
.github/ dump.rdb
.eslintignore documentation/
.eslintrc
.tern-port
*.log
*.rdb
*.out
*.yml
.vscode
.idea
CONTRIBUTING.md CONTRIBUTING.md
CODE_OF_CONDUCT.md tsconfig.json
.travis.yml .nycrc.json
appveyor.yml benchmark/
package-lock.json .github/
.prettierrc scripts/
eslint-report.json lib/
.deepsource.toml index.ts
*.spec.*
dist/lib/test-utils.*

4
.nycrc.json Normal file
View File

@@ -0,0 +1,4 @@
{
"extends": "@istanbuljs/nyc-config-typescript",
"exclude": ["**/*.spec.ts", "lib/test-utils.ts"]
}

View File

@@ -1,11 +0,0 @@
{
"arrowParens": "avoid",
"trailingComma": "all",
"useTabs": false,
"semi": true,
"singleQuote": false,
"bracketSpacing": true,
"jsxBracketSameLine": false,
"tabWidth": 2,
"printWidth": 100
}

View File

@@ -1,5 +1,28 @@
# Changelog # Changelog
## v4.0.0
This version is a major change and refactor, adding modern JavaScript capabilities and multiple breaking changes. See the [migration guide](./docs/v3-to-v4.md) for tips on how to upgrade.
### Breaking Changes
- All functions return Promises by default
- Dropped support for Node.js 10.x, the minimum supported Node.js version is now 12.x
- `createClient` takes new and different arguments
- The `prefix`, `rename_commands` configuration options to `createClient` have been removed
- The `enable_offline_queue` configuration option is removed, executing commands on a closed client (without calling `.connect()` or after calling `.disconnect()`) will reject immediately
- Login credentials are no longer saved when using `.auth()` directly
### Features
- Added support for Promises
- Added built-in TypeScript declaration files enabling code completion
- Added support for [clustering](./README.md#cluster)
- Added idiomatic arguments and responses to [Redis commands](./README.md#redis-commands)
- Added full support for [Lua Scripts](./README.md#lua-scripts)
- Added support for [SCAN iterators](./README.md#scan-iterator)
- Added the ability to extend Node Redis with Redis Module commands
## v3.0.0 - 09 Feb, 2020 ## v3.0.0 - 09 Feb, 2020
This version is mainly a release to distribute all the unreleased changes on master since 2017 and additionally removes This version is mainly a release to distribute all the unreleased changes on master since 2017 and additionally removes

View File

@@ -1,60 +0,0 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making
participation in our project and our community a harassment-free experience for everyone, regardless of age, body size,
disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race,
religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment include:
- Using welcoming and inclusive language
- Being respectful of differing viewpoints and experiences
- Gracefully accepting constructive criticism
- Focusing on what is best for the community
- Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
- The use of sexualized language or imagery and unwelcome sexual attention or advances
- Trolling, insulting/derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or electronic address, without explicit permission
- Other conduct which could reasonably be considered inappropriate in a professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take
appropriate and fair corrective action in response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits,
issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any
contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the
project or its community. Examples of representing a project or community include using an official project e-mail address,
posting via an official social media account, or acting as an appointed representative at an online or offline event.
Representation of a project may be further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at
`redis[AT]invertase.io`. The project team will review and investigate all complaints, and will respond in a way that it
deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the
reporter of an incident. Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent
repercussions as determined by other members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at
[http://contributor-covenant.org/version/1/4][version]
[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/

View File

@@ -2,110 +2,70 @@
First, thank you for considering contributing to Node Redis! It's people like you that make the open source community such a great community! 😊 First, thank you for considering contributing to Node Redis! It's people like you that make the open source community such a great community! 😊
We welcome any type of contribution, not just code. You can help with; We welcome any type of contribution, not just code. You can help with:
- **QA**: file bug reports, the more details you can give the better (e.g. platform versions, screenshots sdk versions & logs) - **QA**: file bug reports, the more details you can give the better (e.g. platform versions, screenshots, SDK versions, logs)
- **Docs**: improve reference coverage, add more examples, fix typos or anything else you can spot. - **Docs**: improve reference coverage, add more examples, fix typos or anything else you can spot
- **Code**: take a look at the open issues and help triage them. - **Code**: take a look at the open issues and help triage them
- **Donations**: we welcome financial contributions in full transparency on our [open collective](https://opencollective.com/node-redis).
--- ---
## Project Guidelines ## Project Guidelines
As maintainers of this project, we want to ensure that the project lives and continues to grow. Not blocked by any As maintainers of this project, we want to ensure that the project lives and continues to grow. Progress should not be blocked by any one person's availability.
singular person's time.
One of the simplest ways of doing this is by encouraging a larger set of shallow contributors. Through this we hope to One of the simplest ways of doing this is by encouraging a larger set of contributors. Using this approach we hope to mitigate the challenges of maintaining a project that needs regular updates.
mitigate the problems of a project that needs updates but there is no-one who has the power to do so.
### Continuous Deployment ### Getting Comfortable Contributing
<!-- TODO(Salakar) --> It is normal for your first pull request to be a potential fix for a problem but moving on from there to helping the project's direction can be difficult.
Coming soon. We try to help contributors cross that barrier by identifying good first step issues (labelled `good-first-issue`). These issues are considered appropriate for first time contributors. Generally, these should be non-critical issues that are well defined. Established contributors will not work on these, to make space for others.
### How can we help you get comfortable contributing? New contributors may consider picking up issues labelled `needs-triage` or `help-wanted`. These may not necessarily require code changes but rather help with debugging and finding the cause of the issue whether it's a bug or a user's incorrect setup of the library or project.
It is normal for a first pull request to be a potential fix for a problem but moving on from there to helping the We keep all project discussion inside GitHub issues. This ensures that valuable information can be searched easily. GitHub issues are the go to tool for questions about how to use the library, or how the project is run.
project's direction can be difficult.
We try to help contributors cross that barrier by offering good first step issues (labelled `good-first-issue`). These ### Expectations of Contributors
issues can be fixed without feeling like you are stepping on toes. Generally, these should be non-critical issues that
are well defined. They will be purposely avoided by mature contributors to the project, to make space for others.
Additionally issues labelled `needs-triage` or `help-wanted` can also be picked up, these may not necessarily require You shouldn't feel bad for not contributing to open source. We want contributors like yourself to provide ideas, keep the ship shipping and to take some of the load from others. It is non-obligatory; were here to get things done in an enjoyable way. :trophy:
code changes but rather help with debugging and finding the cause of the issue whether it's a bug or a users incorrect
setup of the library or project.
We aim to keep all project discussion inside GitHub issues. This is to make sure valuable discussion is accessible via We only ask that you follow the conduct guidelines set out in our [Code of Conduct](https://redis.com/community/community-guidelines-code-of-conduct/) throughout your contribution journey.
search. If you have questions about how to use the library, or how the project is running - GitHub issues are the goto
tool for this project.
### Our expectations on you as a contributor
You shouldn't feel bad for not contributing to open source. We want contributors like yourself to provide ideas, keep #### Special Thanks
the ship shipping and to take some of the load from others. It is non-obligatory; were here to get things done in an
enjoyable way. :trophy:
We only ask that you follow the conduct guidelines set out in our [Code of Conduct](/CODE_OF_CONDUCT.md) throughout your A huge thank you to the original author of Node Redis, [Matthew Ranney](https://github.com/mranney).
contribution journey.
### What about if you have problems that cannot be discussed in public?
You can reach out to us directly via email (`redis[AT]invertase.io`) or direct message us on
[Twitter](https://twitter.com/NodeRedis) if you'd like to discuss something privately.
#### Project Maintainers
- Mike Diarmid ([Salakar](https://github.com/Salakar)) @ [Invertase](https://github.com/invertase)
- Twitter: [@mikediarmid](https://twitter.com/mikediarmid)
- Elliot Hesp ([Ehesp](https://github.com/Ehesp)) @ [Invertase](https://github.com/invertase)
- Twitter: [@elliothesp](https://twitter.com/elliothesp)
- Ruben Bridgewater ([BridgeAR](https://github.com/BridgeAR))
- Twitter: [@BridgeAR](https://twitter.com/BridgeAR)
Huge thanks to the original author of Node Redis, [Matthew Ranney](https://github.com/mranney) and also to
[Ruben Bridgewater](https://github.com/BridgeAR) for handing over this project over to new maintainers so it could be
continuously maintained.
--- ---
## Code Guidelines ## Code Guidelines
### Your First Contribution
Working on your first Pull Request? You can learn how from this _free_ series,
[How to Contribute to an Open Source Project on GitHub](https://egghead.io/series/how-to-contribute-to-an-open-source-project-on-github).
### Testing Code ### Testing Code
Node Redis has a full test suite with coverage setup. Node Redis has a full test suite with coverage setup.
To run the tests, run `npm install` to install all dependencies, and then run `npm test`. To check detailed coverage locally run the `npm run coverage` command after To run the tests, run `npm install` to install dependencies, then run `npm test`.
testing and open the generated `./coverage/index.html` in your browser.
Note that the test suite assumes that a few tools are installed in your environment, such as: Note that the test suite assumes that a few tools are installed in your environment, such as:
- redis (make sure redis-server is not running when starting the tests, it's part of the test-suite to start it and you'll end up with a "port already in use" error) - redis (make sure redis-server is not running when starting the tests, it's part of the test-suite to start it and you'll end up with a "port already in use" error)
- stunnel (for TLS tests) - stunnel (for TLS tests)
### Submitting code for review ### Submitting Code for Review
The bigger the pull request, the longer it will take to review and merge. Where possible try to break down large pull The bigger the pull request, the longer it will take to review and merge. Where possible try to break down large pull requests into smaller chunks that are easier to review and merge. It is also always helpful to have some context for your pull request. What was the purpose? Why does it matter to you? What problem are you trying to solve? Tag in any relevant issues.
requests into smaller chunks that are easier to review and merge. It is also always helpful to have some context for
your pull request. What was the purpose? Why does it matter to you? What problem are you trying to solve? Tag in any linked issues.
To aid review we also ask that you fill out the pull request template as much as possible. To assist reviewers, we ask that you fill out the pull request template as much as possible.
> Use a `draft` pull request if your pull request is not complete or ready for review. > Use a `draft` pull request if your pull request is not complete or ready for review.
### Code review process ### Code Review Process
Pull Requests to the protected branches require two or more peer-review approvals and passing status checks to be able Pull Requests to the protected branches require peer-review approvals and passing status checks to be able to be merged.
to be merged.
When reviewing a Pull Request please check the following steps on top of the existing automated checks: When reviewing a Pull Request please check the following steps as well as the existing automated checks:
- Does the it provide or update the docs if docs changes are required? - Does your Pull Request provide or update the docs if docs changes are required?
- Have the tests been updated or new tests been added to test any newly implemented or changed functionality? - Have the tests been updated or new tests been added to test any newly implemented or changed functionality?
- Is the testing coverage ok and not worse than previously? - Is the test coverage at the same level as before (preferably more!)?

View File

@@ -1,4 +1,4 @@
MIT License MIT License
Copyright (c) 2016-present Node Redis contributors. Copyright (c) 2016-present Node Redis contributors.

1092
README.md

File diff suppressed because it is too large Load Diff

1
benchmark/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
node_modules

81
benchmark/index.js Normal file
View File

@@ -0,0 +1,81 @@
import { add, suite, cycle, complete } from 'benny';
import v4 from 'v4';
import v3 from 'v3';
import { once } from 'events';
const v4Client = v4.createClient(),
v4LegacyClient = v4.createClient({
legacyMode: true
}),
v3Client = v3.createClient();
await Promise.all([
v4Client.connect(),
v4LegacyClient.connect(),
once(v3Client, 'connect')
]);
const key = random(100),
value = random(100);
function random(size) {
const result = [];
for (let i = 0; i < size; i++) {
result.push(Math.floor(Math.random() * 10));
}
return result.join('');
}
suite(
'SET GET',
add('v4', async () => {
await Promise.all([
v4Client.set(key, value),
v4Client.get(key)
]);
}),
add('v4 - legacy mode', () => {
return new Promise((resolve, reject) => {
v4LegacyClient.set(key, value);
v4LegacyClient.get(key, (err, reply) => {
if (err) {
reject(err);
} else {
resolve(reply);
}
});
});
}),
add('v3', () => {
return new Promise((resolve, reject) => {
v3Client.set(key, value);
v3Client.get(key, (err, reply) => {
if (err) {
reject(err);
} else {
resolve(reply);
}
});
});
}),
cycle(),
complete(),
complete(() => {
return Promise.all([
v4Client.disconnect(),
v4LegacyClient.disconnect(),
new Promise((resolve, reject) => {
v3Client.quit((err) => {
if (err) {
reject(err);
} else {
resolve(err);
}
});
})
]);
})
);

926
benchmark/package-lock.json generated Normal file
View File

@@ -0,0 +1,926 @@
{
"name": "benchmark",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"license": "ISC",
"dependencies": {
"@probe.gl/bench": "^3.4.0",
"benny": "^3.6.15",
"v3": "npm:redis@3.1.2",
"v4": "file:../"
}
},
"..": {
"name": "redis",
"version": "4.0.0-next.5",
"license": "MIT",
"dependencies": {
"cluster-key-slot": "1.1.0",
"redis-parser": "3.0.0",
"yallist": "4.0.0"
},
"devDependencies": {
"@istanbuljs/nyc-config-typescript": "^1.0.1",
"@types/mocha": "^9.0.0",
"@types/node": "^16.4.5",
"@types/sinon": "^10.0.2",
"@types/which": "^2.0.1",
"@types/yallist": "^4.0.1",
"mocha": "^9.0.3",
"nyc": "^15.1.0",
"release-it": "^14.10.1",
"sinon": "^11.1.2",
"source-map-support": "^0.5.19",
"ts-node": "^10.1.0",
"typedoc": "^0.21.4",
"typedoc-github-wiki-theme": "^0.5.1",
"typedoc-plugin-markdown": "^3.10.4",
"typescript": "^4.3.5",
"which": "^2.0.2"
},
"engines": {
"node": ">=12"
}
},
"node_modules/@arrows/array": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/@arrows/array/-/array-1.4.1.tgz",
"integrity": "sha512-MGYS8xi3c4tTy1ivhrVntFvufoNzje0PchjEz6G/SsWRgUKxL4tKwS6iPdO8vsaJYldagAeWMd5KRD0aX3Q39g==",
"dependencies": {
"@arrows/composition": "^1.2.2"
}
},
"node_modules/@arrows/composition": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@arrows/composition/-/composition-1.2.2.tgz",
"integrity": "sha512-9fh1yHwrx32lundiB3SlZ/VwuStPB4QakPsSLrGJFH6rCXvdrd060ivAZ7/2vlqPnEjBkPRRXOcG1YOu19p2GQ=="
},
"node_modules/@arrows/dispatch": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@arrows/dispatch/-/dispatch-1.0.3.tgz",
"integrity": "sha512-v/HwvrFonitYZM2PmBlAlCqVqxrkIIoiEuy5bQgn0BdfvlL0ooSBzcPzTMrtzY8eYktPyYcHg8fLbSgyybXEqw==",
"dependencies": {
"@arrows/composition": "^1.2.2"
}
},
"node_modules/@arrows/error": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@arrows/error/-/error-1.0.2.tgz",
"integrity": "sha512-yvkiv1ay4Z3+Z6oQsUkedsQm5aFdyPpkBUQs8vejazU/RmANABx6bMMcBPPHI4aW43VPQmXFfBzr/4FExwWTEA=="
},
"node_modules/@arrows/multimethod": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/@arrows/multimethod/-/multimethod-1.1.7.tgz",
"integrity": "sha512-EjHD3XuGAV4G28rm7mu8k7zQJh/EOizh104/p9i2ofGcnL5mgKONFH/Bq6H3SJjM+WDAlKcR9WBpNhaAKCnH2g==",
"dependencies": {
"@arrows/array": "^1.4.0",
"@arrows/composition": "^1.2.2",
"@arrows/error": "^1.0.2",
"fast-deep-equal": "^3.1.1"
}
},
"node_modules/@babel/runtime": {
"version": "7.14.8",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.14.8.tgz",
"integrity": "sha512-twj3L8Og5SaCRCErB4x4ajbvBIVV77CGeFglHpeg5WC5FF8TZzBWXtTJ4MqaD9QszLYTtr+IsaAL2rEUevb+eg==",
"dependencies": {
"regenerator-runtime": "^0.13.4"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@probe.gl/bench": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@probe.gl/bench/-/bench-3.4.0.tgz",
"integrity": "sha512-S7iNPz5G3zEfEP0S4SAMvtj+dwP7EWfVBaA8Cy5CVIgM1lnpUbXvqoAJxlVEedNC32Icxwq65XQheufy1Zzmug==",
"dependencies": {
"@babel/runtime": "^7.0.0",
"probe.gl": "3.4.0"
}
},
"node_modules/@probe.gl/stats": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@probe.gl/stats/-/stats-3.4.0.tgz",
"integrity": "sha512-Gl37r9qGuiKadIvTZdSZvzCNOttJYw6RcY1oT0oDuB8r2uhuZAdSMQRQTy9FTinp6MY6O9wngGnV6EpQ8wSBAw==",
"dependencies": {
"@babel/runtime": "^7.0.0"
}
},
"node_modules/ansi-escapes": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
"integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==",
"dependencies": {
"type-fest": "^0.21.3"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/ansi-regex": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
"integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
"engines": {
"node": ">=8"
}
},
"node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dependencies": {
"color-convert": "^2.0.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/astral-regex": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz",
"integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==",
"engines": {
"node": ">=8"
}
},
"node_modules/at-least-node": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz",
"integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==",
"engines": {
"node": ">= 4.0.0"
}
},
"node_modules/benchmark": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/benchmark/-/benchmark-2.1.4.tgz",
"integrity": "sha1-CfPeMckWQl1JjMLuVloOvzwqVik=",
"dependencies": {
"lodash": "^4.17.4",
"platform": "^1.3.3"
}
},
"node_modules/benny": {
"version": "3.6.15",
"resolved": "https://registry.npmjs.org/benny/-/benny-3.6.15.tgz",
"integrity": "sha512-kq6XVGGYVou3Y8KNPs3SEF881vi5fJ8sIf9w69D2rreiNfRicWVWK6u6/mObMw6BiexoHHumtipn5gcu0Tngng==",
"dependencies": {
"@arrows/composition": "^1.0.0",
"@arrows/dispatch": "^1.0.2",
"@arrows/multimethod": "^1.1.6",
"benchmark": "^2.1.4",
"fs-extra": "^9.0.1",
"json2csv": "^5.0.4",
"kleur": "^4.1.3",
"log-update": "^4.0.0",
"prettier": "^2.1.2",
"stats-median": "^1.0.1"
}
},
"node_modules/cli-cursor": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
"integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==",
"dependencies": {
"restore-cursor": "^3.1.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/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==",
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/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=="
},
"node_modules/commander": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz",
"integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==",
"engines": {
"node": ">= 6"
}
},
"node_modules/denque": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/denque/-/denque-1.5.0.tgz",
"integrity": "sha512-CYiCSgIF1p6EUByQPlGkKnP1M9g0ZV3qMIrqMqZqdwazygIA/YP2vrbcyl1h/WppKJTdl1F85cXIle+394iDAQ==",
"engines": {
"node": ">=0.10"
}
},
"node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
},
"node_modules/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=="
},
"node_modules/fs-extra": {
"version": "9.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
"integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==",
"dependencies": {
"at-least-node": "^1.0.0",
"graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1",
"universalify": "^2.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/graceful-fs": {
"version": "4.2.6",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz",
"integrity": "sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ=="
},
"node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"engines": {
"node": ">=8"
}
},
"node_modules/json2csv": {
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/json2csv/-/json2csv-5.0.6.tgz",
"integrity": "sha512-0/4Lv6IenJV0qj2oBdgPIAmFiKKnh8qh7bmLFJ+/ZZHLjSeiL3fKKGX3UryvKPbxFbhV+JcYo9KUC19GJ/Z/4A==",
"dependencies": {
"commander": "^6.1.0",
"jsonparse": "^1.3.1",
"lodash.get": "^4.4.2"
},
"bin": {
"json2csv": "bin/json2csv.js"
},
"engines": {
"node": ">= 10",
"npm": ">= 6.13.0"
}
},
"node_modules/jsonfile": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
"integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
"dependencies": {
"universalify": "^2.0.0"
},
"optionalDependencies": {
"graceful-fs": "^4.1.6"
}
},
"node_modules/jsonparse": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz",
"integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=",
"engines": [
"node >= 0.2.0"
]
},
"node_modules/kleur": {
"version": "4.1.4",
"resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.4.tgz",
"integrity": "sha512-8QADVssbrFjivHWQU7KkMgptGTl6WAcSdlbBPY4uNF+mWr6DGcKrvY2w4FQJoXch7+fKMjj0dRrL75vk3k23OA==",
"engines": {
"node": ">=6"
}
},
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"node_modules/lodash.get": {
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
"integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk="
},
"node_modules/log-update": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz",
"integrity": "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==",
"dependencies": {
"ansi-escapes": "^4.3.0",
"cli-cursor": "^3.1.0",
"slice-ansi": "^4.0.0",
"wrap-ansi": "^6.2.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/mimic-fn": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
"integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
"engines": {
"node": ">=6"
}
},
"node_modules/onetime": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
"integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
"dependencies": {
"mimic-fn": "^2.1.0"
},
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/platform": {
"version": "1.3.6",
"resolved": "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz",
"integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg=="
},
"node_modules/prettier": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.3.2.tgz",
"integrity": "sha512-lnJzDfJ66zkMy58OL5/NY5zp70S7Nz6KqcKkXYzn2tMVrNxvbqaBpg7H3qHaLxCJ5lNMsGuM8+ohS7cZrthdLQ==",
"bin": {
"prettier": "bin-prettier.js"
},
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/probe.gl": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/probe.gl/-/probe.gl-3.4.0.tgz",
"integrity": "sha512-9CLByZATuhuG/Viq3ckfWU+dAhb7dMmjzsyCy4s7ds9ueTejcVRENxL197/XacOK/AN61YrEERB0QnouB0Qc0Q==",
"dependencies": {
"@babel/runtime": "^7.0.0",
"@probe.gl/stats": "3.4.0"
}
},
"node_modules/redis-commands": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.7.0.tgz",
"integrity": "sha512-nJWqw3bTFy21hX/CPKHth6sfhZbdiHP6bTawSgQBlKOVRG7EZkfHbbHwQJnrE4vsQf0CMNE+3gJ4Fmm16vdVlQ=="
},
"node_modules/redis-errors": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz",
"integrity": "sha1-62LSrbFeTq9GEMBK/hUpOEJQq60=",
"engines": {
"node": ">=4"
}
},
"node_modules/redis-parser": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz",
"integrity": "sha1-tm2CjNyv5rS4pCin3vTGvKwxyLQ=",
"dependencies": {
"redis-errors": "^1.0.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/regenerator-runtime": {
"version": "0.13.9",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz",
"integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA=="
},
"node_modules/restore-cursor": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz",
"integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==",
"dependencies": {
"onetime": "^5.1.0",
"signal-exit": "^3.0.2"
},
"engines": {
"node": ">=8"
}
},
"node_modules/signal-exit": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz",
"integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA=="
},
"node_modules/slice-ansi": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz",
"integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==",
"dependencies": {
"ansi-styles": "^4.0.0",
"astral-regex": "^2.0.0",
"is-fullwidth-code-point": "^3.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/slice-ansi?sponsor=1"
}
},
"node_modules/stats-median": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/stats-median/-/stats-median-1.0.1.tgz",
"integrity": "sha512-IYsheLg6dasD3zT/w9+8Iq9tcIQqqu91ZIpJOnIEM25C3X/g4Tl8mhXwW2ZQpbrsJISr9+wizEYgsibN5/b32Q=="
},
"node_modules/string-width": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz",
"integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/strip-ansi": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
"integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==",
"dependencies": {
"ansi-regex": "^5.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/type-fest": {
"version": "0.21.3",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz",
"integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/universalify": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
"integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==",
"engines": {
"node": ">= 10.0.0"
}
},
"node_modules/v3": {
"name": "redis",
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/redis/-/redis-3.1.2.tgz",
"integrity": "sha512-grn5KoZLr/qrRQVwoSkmzdbw6pwF+/rwODtrOr6vuBRiR/f3rjSTGupbF90Zpqm2oenix8Do6RV7pYEkGwlKkw==",
"dependencies": {
"denque": "^1.5.0",
"redis-commands": "^1.7.0",
"redis-errors": "^1.2.0",
"redis-parser": "^3.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/node-redis"
}
},
"node_modules/v4": {
"resolved": "..",
"link": true
},
"node_modules/wrap-ansi": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
},
"engines": {
"node": ">=8"
}
}
},
"dependencies": {
"@arrows/array": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/@arrows/array/-/array-1.4.1.tgz",
"integrity": "sha512-MGYS8xi3c4tTy1ivhrVntFvufoNzje0PchjEz6G/SsWRgUKxL4tKwS6iPdO8vsaJYldagAeWMd5KRD0aX3Q39g==",
"requires": {
"@arrows/composition": "^1.2.2"
}
},
"@arrows/composition": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@arrows/composition/-/composition-1.2.2.tgz",
"integrity": "sha512-9fh1yHwrx32lundiB3SlZ/VwuStPB4QakPsSLrGJFH6rCXvdrd060ivAZ7/2vlqPnEjBkPRRXOcG1YOu19p2GQ=="
},
"@arrows/dispatch": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@arrows/dispatch/-/dispatch-1.0.3.tgz",
"integrity": "sha512-v/HwvrFonitYZM2PmBlAlCqVqxrkIIoiEuy5bQgn0BdfvlL0ooSBzcPzTMrtzY8eYktPyYcHg8fLbSgyybXEqw==",
"requires": {
"@arrows/composition": "^1.2.2"
}
},
"@arrows/error": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@arrows/error/-/error-1.0.2.tgz",
"integrity": "sha512-yvkiv1ay4Z3+Z6oQsUkedsQm5aFdyPpkBUQs8vejazU/RmANABx6bMMcBPPHI4aW43VPQmXFfBzr/4FExwWTEA=="
},
"@arrows/multimethod": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/@arrows/multimethod/-/multimethod-1.1.7.tgz",
"integrity": "sha512-EjHD3XuGAV4G28rm7mu8k7zQJh/EOizh104/p9i2ofGcnL5mgKONFH/Bq6H3SJjM+WDAlKcR9WBpNhaAKCnH2g==",
"requires": {
"@arrows/array": "^1.4.0",
"@arrows/composition": "^1.2.2",
"@arrows/error": "^1.0.2",
"fast-deep-equal": "^3.1.1"
}
},
"@babel/runtime": {
"version": "7.14.8",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.14.8.tgz",
"integrity": "sha512-twj3L8Og5SaCRCErB4x4ajbvBIVV77CGeFglHpeg5WC5FF8TZzBWXtTJ4MqaD9QszLYTtr+IsaAL2rEUevb+eg==",
"requires": {
"regenerator-runtime": "^0.13.4"
}
},
"@probe.gl/bench": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@probe.gl/bench/-/bench-3.4.0.tgz",
"integrity": "sha512-S7iNPz5G3zEfEP0S4SAMvtj+dwP7EWfVBaA8Cy5CVIgM1lnpUbXvqoAJxlVEedNC32Icxwq65XQheufy1Zzmug==",
"requires": {
"@babel/runtime": "^7.0.0",
"probe.gl": "3.4.0"
}
},
"@probe.gl/stats": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@probe.gl/stats/-/stats-3.4.0.tgz",
"integrity": "sha512-Gl37r9qGuiKadIvTZdSZvzCNOttJYw6RcY1oT0oDuB8r2uhuZAdSMQRQTy9FTinp6MY6O9wngGnV6EpQ8wSBAw==",
"requires": {
"@babel/runtime": "^7.0.0"
}
},
"ansi-escapes": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
"integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==",
"requires": {
"type-fest": "^0.21.3"
}
},
"ansi-regex": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
"integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg=="
},
"ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"requires": {
"color-convert": "^2.0.1"
}
},
"astral-regex": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz",
"integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ=="
},
"at-least-node": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz",
"integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg=="
},
"benchmark": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/benchmark/-/benchmark-2.1.4.tgz",
"integrity": "sha1-CfPeMckWQl1JjMLuVloOvzwqVik=",
"requires": {
"lodash": "^4.17.4",
"platform": "^1.3.3"
}
},
"benny": {
"version": "3.6.15",
"resolved": "https://registry.npmjs.org/benny/-/benny-3.6.15.tgz",
"integrity": "sha512-kq6XVGGYVou3Y8KNPs3SEF881vi5fJ8sIf9w69D2rreiNfRicWVWK6u6/mObMw6BiexoHHumtipn5gcu0Tngng==",
"requires": {
"@arrows/composition": "^1.0.0",
"@arrows/dispatch": "^1.0.2",
"@arrows/multimethod": "^1.1.6",
"benchmark": "^2.1.4",
"fs-extra": "^9.0.1",
"json2csv": "^5.0.4",
"kleur": "^4.1.3",
"log-update": "^4.0.0",
"prettier": "^2.1.2",
"stats-median": "^1.0.1"
}
},
"cli-cursor": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
"integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==",
"requires": {
"restore-cursor": "^3.1.0"
}
},
"color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"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=="
},
"commander": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz",
"integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA=="
},
"denque": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/denque/-/denque-1.5.0.tgz",
"integrity": "sha512-CYiCSgIF1p6EUByQPlGkKnP1M9g0ZV3qMIrqMqZqdwazygIA/YP2vrbcyl1h/WppKJTdl1F85cXIle+394iDAQ=="
},
"emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
},
"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=="
},
"fs-extra": {
"version": "9.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
"integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==",
"requires": {
"at-least-node": "^1.0.0",
"graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1",
"universalify": "^2.0.0"
}
},
"graceful-fs": {
"version": "4.2.6",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz",
"integrity": "sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ=="
},
"is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="
},
"json2csv": {
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/json2csv/-/json2csv-5.0.6.tgz",
"integrity": "sha512-0/4Lv6IenJV0qj2oBdgPIAmFiKKnh8qh7bmLFJ+/ZZHLjSeiL3fKKGX3UryvKPbxFbhV+JcYo9KUC19GJ/Z/4A==",
"requires": {
"commander": "^6.1.0",
"jsonparse": "^1.3.1",
"lodash.get": "^4.4.2"
}
},
"jsonfile": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
"integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
"requires": {
"graceful-fs": "^4.1.6",
"universalify": "^2.0.0"
}
},
"jsonparse": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz",
"integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA="
},
"kleur": {
"version": "4.1.4",
"resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.4.tgz",
"integrity": "sha512-8QADVssbrFjivHWQU7KkMgptGTl6WAcSdlbBPY4uNF+mWr6DGcKrvY2w4FQJoXch7+fKMjj0dRrL75vk3k23OA=="
},
"lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"lodash.get": {
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
"integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk="
},
"log-update": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz",
"integrity": "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==",
"requires": {
"ansi-escapes": "^4.3.0",
"cli-cursor": "^3.1.0",
"slice-ansi": "^4.0.0",
"wrap-ansi": "^6.2.0"
}
},
"mimic-fn": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
"integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="
},
"onetime": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
"integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
"requires": {
"mimic-fn": "^2.1.0"
}
},
"platform": {
"version": "1.3.6",
"resolved": "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz",
"integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg=="
},
"prettier": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.3.2.tgz",
"integrity": "sha512-lnJzDfJ66zkMy58OL5/NY5zp70S7Nz6KqcKkXYzn2tMVrNxvbqaBpg7H3qHaLxCJ5lNMsGuM8+ohS7cZrthdLQ=="
},
"probe.gl": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/probe.gl/-/probe.gl-3.4.0.tgz",
"integrity": "sha512-9CLByZATuhuG/Viq3ckfWU+dAhb7dMmjzsyCy4s7ds9ueTejcVRENxL197/XacOK/AN61YrEERB0QnouB0Qc0Q==",
"requires": {
"@babel/runtime": "^7.0.0",
"@probe.gl/stats": "3.4.0"
}
},
"redis-commands": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.7.0.tgz",
"integrity": "sha512-nJWqw3bTFy21hX/CPKHth6sfhZbdiHP6bTawSgQBlKOVRG7EZkfHbbHwQJnrE4vsQf0CMNE+3gJ4Fmm16vdVlQ=="
},
"redis-errors": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz",
"integrity": "sha1-62LSrbFeTq9GEMBK/hUpOEJQq60="
},
"redis-parser": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz",
"integrity": "sha1-tm2CjNyv5rS4pCin3vTGvKwxyLQ=",
"requires": {
"redis-errors": "^1.0.0"
}
},
"regenerator-runtime": {
"version": "0.13.9",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz",
"integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA=="
},
"restore-cursor": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz",
"integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==",
"requires": {
"onetime": "^5.1.0",
"signal-exit": "^3.0.2"
}
},
"signal-exit": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz",
"integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA=="
},
"slice-ansi": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz",
"integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==",
"requires": {
"ansi-styles": "^4.0.0",
"astral-regex": "^2.0.0",
"is-fullwidth-code-point": "^3.0.0"
}
},
"stats-median": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/stats-median/-/stats-median-1.0.1.tgz",
"integrity": "sha512-IYsheLg6dasD3zT/w9+8Iq9tcIQqqu91ZIpJOnIEM25C3X/g4Tl8mhXwW2ZQpbrsJISr9+wizEYgsibN5/b32Q=="
},
"string-width": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz",
"integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==",
"requires": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.0"
}
},
"strip-ansi": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
"integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==",
"requires": {
"ansi-regex": "^5.0.0"
}
},
"type-fest": {
"version": "0.21.3",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz",
"integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="
},
"universalify": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
"integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ=="
},
"v3": {
"version": "npm:redis@3.1.2",
"resolved": "https://registry.npmjs.org/redis/-/redis-3.1.2.tgz",
"integrity": "sha512-grn5KoZLr/qrRQVwoSkmzdbw6pwF+/rwODtrOr6vuBRiR/f3rjSTGupbF90Zpqm2oenix8Do6RV7pYEkGwlKkw==",
"requires": {
"denque": "^1.5.0",
"redis-commands": "^1.7.0",
"redis-errors": "^1.2.0",
"redis-parser": "^3.0.0"
}
},
"v4": {
"version": "file:..",
"requires": {
"@istanbuljs/nyc-config-typescript": "^1.0.1",
"@types/mocha": "^9.0.0",
"@types/node": "^16.4.5",
"@types/sinon": "^10.0.2",
"@types/which": "^2.0.1",
"@types/yallist": "^4.0.1",
"cluster-key-slot": "1.1.0",
"mocha": "^9.0.3",
"nyc": "^15.1.0",
"redis-parser": "3.0.0",
"release-it": "^14.10.1",
"sinon": "^11.1.2",
"source-map-support": "^0.5.19",
"ts-node": "^10.1.0",
"typedoc": "^0.21.4",
"typedoc-github-wiki-theme": "^0.5.1",
"typedoc-plugin-markdown": "^3.10.4",
"typescript": "^4.3.5",
"which": "^2.0.2",
"yallist": "4.0.0"
}
},
"wrap-ansi": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
"requires": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
}
}
}
}

17
benchmark/package.json Normal file
View File

@@ -0,0 +1,17 @@
{
"name": "benchmark",
"private": true,
"description": "",
"main": "index.js",
"type": "module",
"scripts": {
"start": "node ./"
},
"author": "",
"license": "ISC",
"dependencies": {
"benny": "^3.6.15",
"v3": "npm:redis@3.1.2",
"v4": "file:../"
}
}

View File

@@ -1,95 +0,0 @@
'use strict';
var fs = require('fs');
var metrics = require('metrics');
// `node diff_multi_bench_output.js beforeBench.txt afterBench.txt`
var file1 = process.argv[2];
var file2 = process.argv[3];
if (!file1 || !file2) {
console.log('Please supply two file arguments:');
var n = __filename;
n = n.substring(n.lastIndexOf('/', n.length));
console.log(' node .' + n + ' benchBefore.txt benchAfter.txt\n');
console.log('To generate the benchmark files, run');
console.log(' npm run benchmark > benchBefore.txt\n');
console.log('Thank you for benchmarking responsibly.');
return;
}
var before_lines = fs.readFileSync(file1, 'utf8').split('\n');
var after_lines = fs.readFileSync(file2, 'utf8').split('\n');
var total_ops = new metrics.Histogram.createUniformHistogram();
console.log('Comparing before,', file1, '(', before_lines.length, 'lines)', 'to after,', file2, '(', after_lines.length, 'lines)');
function is_whitespace (s) {
return !!s.trim();
}
function pad (input, len, chr, right) {
var str = input.toString();
chr = chr || ' ';
if (right) {
while (str.length < len) {
str += chr;
}
} else {
while (str.length < len) {
str = chr + str;
}
}
return str;
}
// green if greater than 0, red otherwise
function humanize_diff (num, unit, toFixed) {
unit = unit || '';
if (num > 0) {
return ' +' + pad(num.toFixed(toFixed || 0) + unit, 7);
}
return ' -' + pad(Math.abs(num).toFixed(toFixed || 0) + unit, 7);
}
function command_name (words) {
var line = words.join(' ');
return line.substr(0, line.indexOf(','));
}
before_lines.forEach(function (b, i) {
var a = after_lines[i];
if (!a || !b || !b.trim() || !a.trim()) {
// console.log('#ignored#', '>'+a+'<', '>'+b+'<');
return;
}
var b_words = b.split(' ').filter(is_whitespace);
var a_words = a.split(' ').filter(is_whitespace);
var ops = [b_words, a_words].map(function (words) {
// console.log(words);
return words.slice(-2, -1) | 0;
}).filter(function (num) {
var isNaN = !num && num !== 0;
return !isNaN;
});
if (ops.length !== 2) {
return;
}
var delta = ops[1] - ops[0];
var pct = +((delta / ops[0]) * 100);
ops[0] = pad(ops[0], 6);
ops[1] = pad(ops[1], 6);
total_ops.update(delta);
delta = humanize_diff(delta);
var small_delta = pct < 3 && pct > -3;
// Let's mark differences above 20% bold
var big_delta = pct > 20 || pct < -20 ? ';1' : '';
pct = humanize_diff(pct, '', 2) + '%';
var str = pad((command_name(a_words) === command_name(b_words) ? command_name(a_words) + ':' : '404:'), 14, false, true) +
(pad(ops.join(' -> '), 15) + ' ops/sec (∆' + delta + pct + ')');
str = (small_delta ? '' : (/-[^>]/.test(str) ? '\x1b[31' : '\x1b[32') + big_delta + 'm') + str + '\x1b[0m';
console.log(str);
});
console.log('Mean difference in ops/sec:', humanize_diff(total_ops.mean(), '', 1));

View File

@@ -1,291 +0,0 @@
'use strict';
var path = require('path');
var RedisProcess = require('../test/lib/redis-process');
var rp;
var client_nr = 0;
var redis = require('../index');
var totalTime = 0;
var metrics = require('metrics');
var tests = [];
// var bluebird = require('bluebird');
// bluebird.promisifyAll(redis.RedisClient.prototype);
// bluebird.promisifyAll(redis.Multi.prototype);
function returnArg (name, def) {
var matches = process.argv.filter(function (entry) {
return entry.indexOf(name + '=') === 0;
});
if (matches.length) {
return matches[0].substr(name.length + 1);
}
return def;
}
var num_clients = returnArg('clients', 1);
var run_time = returnArg('time', 2500); // ms
var pipeline = returnArg('pipeline', 1); // number of concurrent commands
var versions_logged = false;
var client_options = {
parser: returnArg('parser', 'javascript'),
path: returnArg('socket') // '/tmp/redis.sock'
};
var small_str, large_str, small_buf, large_buf, very_large_str, very_large_buf;
function lpad (input, len, chr) {
var str = input.toString();
chr = chr || ' ';
while (str.length < len) {
str = chr + str;
}
return str;
}
metrics.Histogram.prototype.print_line = function () {
var obj = this.printObj();
return lpad((obj.mean / 1e6).toFixed(2), 6) + '/' + lpad((obj.max / 1e6).toFixed(2), 6);
};
function Test (args) {
this.args = args;
this.args.pipeline = +pipeline;
this.callback = null;
this.clients = [];
this.clients_ready = 0;
this.commands_sent = 0;
this.commands_completed = 0;
this.max_pipeline = +pipeline;
this.batch_pipeline = this.args.batch || 0;
this.client_options = args.client_options || {};
this.client_options.parser = client_options.parser;
this.client_options.connect_timeout = 1000;
if (client_options.path) {
this.client_options.path = client_options.path;
}
this.connect_latency = new metrics.Histogram();
this.ready_latency = new metrics.Histogram();
this.command_latency = new metrics.Histogram();
}
Test.prototype.run = function (callback) {
var i;
this.callback = callback;
for (i = 0; i < num_clients ; i++) {
this.new_client(i);
}
};
Test.prototype.new_client = function (id) {
var self = this, new_client;
new_client = redis.createClient(this.client_options);
new_client.create_time = Date.now();
new_client.on('connect', function () {
self.connect_latency.update(Date.now() - new_client.create_time);
});
new_client.on('ready', function () {
if (!versions_logged) {
console.log(
'clients: ' + num_clients +
', NodeJS: ' + process.versions.node +
', Redis: ' + new_client.server_info.redis_version +
', parser: ' + client_options.parser +
', connected by: ' + (client_options.path ? 'socket' : 'tcp')
);
versions_logged = true;
}
self.ready_latency.update(Date.now() - new_client.create_time);
self.clients_ready++;
if (self.clients_ready === self.clients.length) {
self.on_clients_ready();
}
});
// If no redis server is running, start one
new_client.on('error', function (err) {
if (err.code === 'CONNECTION_BROKEN') {
throw err;
}
if (rp) {
return;
}
rp = true;
var conf = '../test/conf/redis.conf';
RedisProcess.start(function (err, _rp) {
if (err) {
throw err;
}
rp = _rp;
}, path.resolve(__dirname, conf));
});
self.clients[id] = new_client;
};
Test.prototype.on_clients_ready = function () {
process.stdout.write(lpad(this.args.descr, 13) + ', ' + (this.args.batch ? lpad('batch ' + this.args.batch, 9) : lpad(this.args.pipeline, 9)) + '/' + this.clients_ready + ' ');
this.test_start = Date.now();
this.fill_pipeline();
};
Test.prototype.fill_pipeline = function () {
var pipeline = this.commands_sent - this.commands_completed;
if (this.test_start < Date.now() - run_time) {
if (this.ended) {
return;
}
this.ended = true;
this.print_stats();
this.stop_clients();
return;
}
if (this.batch_pipeline) {
this.batch();
} else {
while (pipeline < this.max_pipeline) {
this.commands_sent++;
pipeline++;
this.send_next();
}
}
};
Test.prototype.batch = function () {
var self = this,
cur_client = client_nr++ % this.clients.length,
start = process.hrtime(),
i = 0,
batch = this.clients[cur_client].batch();
while (i++ < this.batch_pipeline) {
this.commands_sent++;
batch[this.args.command](this.args.args);
}
batch.exec(function (err, res) {
if (err) {
throw err;
}
self.commands_completed += res.length;
self.command_latency.update(process.hrtime(start)[1]);
self.fill_pipeline();
});
};
Test.prototype.stop_clients = function () {
var self = this;
this.clients.forEach(function (client, pos) {
if (pos === self.clients.length - 1) {
client.quit(function (err, res) {
self.callback();
});
} else {
client.quit();
}
});
};
Test.prototype.send_next = function () {
var self = this,
cur_client = this.commands_sent % this.clients.length,
start = process.hrtime();
this.clients[cur_client][this.args.command](this.args.args, function (err, res) {
if (err) {
throw err;
}
self.commands_completed++;
self.command_latency.update(process.hrtime(start)[1]);
self.fill_pipeline();
});
};
Test.prototype.print_stats = function () {
var duration = Date.now() - this.test_start;
totalTime += duration;
console.log('avg/max: ' + this.command_latency.print_line() + lpad(duration, 5) + 'ms total, ' +
lpad(Math.round(this.commands_completed / (duration / 1000)), 7) + ' ops/sec');
};
small_str = '1234';
small_buf = Buffer.from(small_str);
large_str = (new Array(4096 + 1).join('-'));
large_buf = Buffer.from(large_str);
very_large_str = (new Array((4 * 1024 * 1024) + 1).join('-'));
very_large_buf = Buffer.from(very_large_str);
tests.push(new Test({descr: 'PING', command: 'ping', args: []}));
tests.push(new Test({descr: 'PING', command: 'ping', args: [], batch: 50}));
tests.push(new Test({descr: 'SET 4B str', command: 'set', args: ['foo_rand000000000000', small_str]}));
tests.push(new Test({descr: 'SET 4B str', command: 'set', args: ['foo_rand000000000000', small_str], batch: 50}));
tests.push(new Test({descr: 'SET 4B buf', command: 'set', args: ['foo_rand000000000000', small_buf]}));
tests.push(new Test({descr: 'SET 4B buf', command: 'set', args: ['foo_rand000000000000', small_buf], batch: 50}));
tests.push(new Test({descr: 'GET 4B str', command: 'get', args: ['foo_rand000000000000']}));
tests.push(new Test({descr: 'GET 4B str', command: 'get', args: ['foo_rand000000000000'], batch: 50}));
tests.push(new Test({descr: 'GET 4B buf', command: 'get', args: ['foo_rand000000000000'], client_options: { return_buffers: true} }));
tests.push(new Test({descr: 'GET 4B buf', command: 'get', args: ['foo_rand000000000000'], batch: 50, client_options: { return_buffers: true} }));
tests.push(new Test({descr: 'SET 4KiB str', command: 'set', args: ['foo_rand000000000001', large_str]}));
tests.push(new Test({descr: 'SET 4KiB str', command: 'set', args: ['foo_rand000000000001', large_str], batch: 50}));
tests.push(new Test({descr: 'SET 4KiB buf', command: 'set', args: ['foo_rand000000000001', large_buf]}));
tests.push(new Test({descr: 'SET 4KiB buf', command: 'set', args: ['foo_rand000000000001', large_buf], batch: 50}));
tests.push(new Test({descr: 'GET 4KiB str', command: 'get', args: ['foo_rand000000000001']}));
tests.push(new Test({descr: 'GET 4KiB str', command: 'get', args: ['foo_rand000000000001'], batch: 50}));
tests.push(new Test({descr: 'GET 4KiB buf', command: 'get', args: ['foo_rand000000000001'], client_options: { return_buffers: true} }));
tests.push(new Test({descr: 'GET 4KiB buf', command: 'get', args: ['foo_rand000000000001'], batch: 50, client_options: { return_buffers: true} }));
tests.push(new Test({descr: 'INCR', command: 'incr', args: ['counter_rand000000000000']}));
tests.push(new Test({descr: 'INCR', command: 'incr', args: ['counter_rand000000000000'], batch: 50}));
tests.push(new Test({descr: 'LPUSH', command: 'lpush', args: ['mylist', small_str]}));
tests.push(new Test({descr: 'LPUSH', command: 'lpush', args: ['mylist', small_str], batch: 50}));
tests.push(new Test({descr: 'LRANGE 10', command: 'lrange', args: ['mylist', '0', '9']}));
tests.push(new Test({descr: 'LRANGE 10', command: 'lrange', args: ['mylist', '0', '9'], batch: 50}));
tests.push(new Test({descr: 'LRANGE 100', command: 'lrange', args: ['mylist', '0', '99']}));
tests.push(new Test({descr: 'LRANGE 100', command: 'lrange', args: ['mylist', '0', '99'], batch: 50}));
tests.push(new Test({descr: 'SET 4MiB str', command: 'set', args: ['foo_rand000000000002', very_large_str]}));
tests.push(new Test({descr: 'SET 4MiB str', command: 'set', args: ['foo_rand000000000002', very_large_str], batch: 20}));
tests.push(new Test({descr: 'SET 4MiB buf', command: 'set', args: ['foo_rand000000000002', very_large_buf]}));
tests.push(new Test({descr: 'SET 4MiB buf', command: 'set', args: ['foo_rand000000000002', very_large_buf], batch: 20}));
tests.push(new Test({descr: 'GET 4MiB str', command: 'get', args: ['foo_rand000000000002']}));
tests.push(new Test({descr: 'GET 4MiB str', command: 'get', args: ['foo_rand000000000002'], batch: 20}));
tests.push(new Test({descr: 'GET 4MiB buf', command: 'get', args: ['foo_rand000000000002'], client_options: { return_buffers: true} }));
tests.push(new Test({descr: 'GET 4MiB buf', command: 'get', args: ['foo_rand000000000002'], batch: 20, client_options: { return_buffers: true} }));
function next () {
var test = tests.shift();
if (test) {
test.run(function () {
next();
});
} else if (rp) {
// Stop the redis process if started by the benchmark
rp.stop(function () {
rp = undefined;
next();
});
} else {
console.log('End of tests. Total time elapsed:', totalTime, 'ms');
process.exit(0);
}
}
next();

13
docs/FAQ.md Normal file
View File

@@ -0,0 +1,13 @@
# F.A.Q.
Nobody has *actually* asked these questions. But, we needed somewhere to put all the important bits and bobs that didn't fit anywhere else. So, here you go!
## What happens when the network goes down?
When a socket closed unexpectedly, all the commands that were already sent will reject as they might have been executed on the server. The rest will remain queued in memory until a new socket is established. If the client is closed—either by returning an error from [`reconnectStrategy`](./client-configuration.md#reconnect-strategy) or by manually calling `.disconnect()`—they will be rejected.
## How are commands batched?
Commands are pipelined using [`queueMicrotask`](https://nodejs.org/api/globals.html#globals_queuemicrotask_callback). Commands from the same "tick" will be sent in batches and respect the [`writableHighWaterMark`](https://nodejs.org/api/stream.html#stream_new_stream_writable_options).
If `socket.write()` returns `false`—meaning that ["all or part of the data was queued in user memory"](https://nodejs.org/api/net.html#net_socket_write_data_encoding_callback:~:text=all%20or%20part%20of%20the%20data%20was%20queued%20in%20user%20memory)—the commands will stack in memory until the [`drain`](https://nodejs.org/api/net.html#net_event_drain) event is fired.

View File

@@ -0,0 +1,30 @@
# `createClient` configuration
| Property | Default | Description |
|--------------------------|------------------------------------------|------------------------------------------------------------------------------------------------------------------------------|
| socket | | Object defining socket connection properties |
| socket.url | | `[redis[s]:]//[[username][:password]@][host][:port]` |
| socket.host | `'localhost'` | Hostname to connect to |
| socket.port | `6379` | Port to connect to |
| socket.username | | ACL username ([see ACL guide](https://redis.io/topics/acl)) |
| socket.password | | ACL password or the old "--requirepass" password |
| socket.connectTimeout | `5000` | The timeout for connecting to the Redis Server (in milliseconds) |
| socket.noDelay | `true` | Enable/disable the use of [`Nagle's algorithm`](https://nodejs.org/api/net.html#net_socket_setnodelay_nodelay) |
| socket.keepAlive | `5000` | Enable/disable the [`keep-alive`](https://nodejs.org/api/net.html#net_socket_setkeepalive_enable_initialdelay) functionality |
| socket.tls | | Set to `true` to enable [TLS Configuration](https://nodejs.org/api/tls.html#tls_tls_connect_options_callback) |
| socket.reconnectStrategy | `retries => Math.min(retries * 50, 500)` | A function containing the [Reconnect Strategy](#reconnect-strategy) logic |
| modules | | Object defining which [Redis Modules](https://redis.io/modules) to include (TODO - document) |
| scripts | | Object defining Lua scripts to use with this client. See [Lua Scripts](../README.md#lua-scripts) |
| commandsQueueMaxLength | | Maximum length of the client's internal command queue |
| readonly | `false` | Connect in [`READONLY`](https://redis.io/commands/readonly) mode |
| legacyMode | `false` | Maintain some backwards compatibility (see the [Migration Guide](v3-to-v4.md)) |
| isolationPoolOptions | | See the [Isolated Execution Guide](./isolated-execution.md) |
## Reconnect Strategy
You can implement a custom reconnect strategy as a function that should:
- Receives the number of retries attempted so far.
- Should return `number | Error`:
- `number`: the time in milliseconds to wait before trying to reconnect again.
- `Error`: close the client and flush the commands queue.

View File

@@ -0,0 +1,67 @@
# Isolated Execution
Sometimes you want to run your commands on an exclusive connection. There are a few reasons to do this:
- You're using [transactions]() and need to `WATCH` a key or keys for changes.
- You want to run a blocking command that will take over the connection, such as `BLPOP` or `BLMOVE`.
- You're using the `MONITOR` command which also takes over a connection.
Below are several examples of how to use isolated execution.
> NOTE: Behind the scences we're using [`generic-pool`](https://www.npmjs.com/package/generic-pool) to provide a pool of connections that can be isolated. Go there to learn more.
## The Simple Secnario
This just isolates execution on a single connection. Do what you want with that connection:
```typescript
await client.executeIsolated(async isolatedClient => {
await isolatedClient.set('key', 'value');
await isolatedClient.get('key');
});
```
## Transactions
Things get a little more complex with transactions. Here we are `.watch()`ing some keys. If the keys change during the transaction, a `WatchError` is thrown when `.exec()` is called:
```typescript
try {
await client.executeIsolated(async isolatedClient => {
await isolatedClient.watch('key');
const multi = isolatedClient.multi()
.ping()
.get('key');
if (Math.random() > 0.5) {
await isolatedClient.watch('another-key');
multi.set('another-key', await isolatedClient.get('another-key') / 2);
}
return multi.exec();
});
} catch (err) {
if (err instanceof WatchError) {
// the transaction aborted
}
}
```
## Blocking Commands
For blocking commands, you can execute a tidy little one-liner:
```typescript
await client.executeIsolated(isolatedClient => isolatedClient.blPop('key'));
```
Or, you can just run the command directly, and provide the `isolated` option:
```typescript
await client.blPop(
commandOptions({ isolated: true }),
'key'
);
```

35
docs/v3-to-v4.md Normal file
View File

@@ -0,0 +1,35 @@
# v3 to v4 Migration Guide
Version 4 of Node Redis is a major refactor. While we have tried to maintain backwards compatibility where possible, several interfaces have changed. Read this guide to understand the differences and how to implement version 4 in your application.
## Breaking Changes
See the [Change Log](../CHANGELOG.md).
## Promises
Node Redis now uses native [Promises](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) by default for all functions.
## Legacy Mode
Use legacy mode to preserve the backwards compatibility of commands while still getting access to the updated experience:
```typescript
const client = createClient({
legacyMode: true
});
// legacy mode
client.set('key', 'value', 'NX', (err, reply) => {
// ...
});
// version 4 interface is still accessible
await client.v4.set('key', 'value', {
NX: true
});
```
## `createClient`
The configuration object passed to `createClient` has changed significantly with this release. See the [client configuration guide](./client-configuration.md) for details.

View File

@@ -1,7 +0,0 @@
'use strict';
var redis = require('redis');
// The client stashes the password and will reauthenticate on every connect.
redis.createClient({
password: 'somepass'
});

View File

@@ -1,34 +0,0 @@
'use strict';
var redis = require('../index');
var client = redis.createClient();
var remaining_ops = 100000;
var paused = false;
function op () {
if (remaining_ops <= 0) {
console.error('Finished.');
process.exit(0);
}
remaining_ops--;
client.hset('test hash', 'val ' + remaining_ops, remaining_ops);
if (client.should_buffer === true) {
console.log('Pausing at ' + remaining_ops);
paused = true;
} else {
setTimeout(op, 1);
}
}
client.on('drain', function () {
if (paused) {
console.log('Resuming at ' + remaining_ops);
paused = false;
process.nextTick(op);
} else {
console.log('Got drain while not paused at ' + remaining_ops);
}
});
op();

View File

@@ -1,14 +0,0 @@
'use strict';
var redis = require('../index');
var client = redis.createClient();
client.eval('return 100.5', 0, function (err, res) {
console.dir(err);
console.dir(res);
});
client.eval([ 'return 100.5', 0 ], function (err, res) {
console.dir(err);
console.dir(res);
});

View File

@@ -1,26 +0,0 @@
'use strict';
var redis = require('redis');
var client = redis.createClient();
// Extend the RedisClient prototype to add a custom method
// This one converts the results from 'INFO' into a JavaScript Object
redis.RedisClient.prototype.parse_info = function (callback) {
this.info(function (err, res) {
var lines = res.toString().split('\r\n').sort();
var obj = {};
lines.forEach(function (line) {
var parts = line.split(':');
if (parts[1]) {
obj[parts[0]] = parts[1];
}
});
callback(obj);
});
};
client.parse_info(function (info) {
console.dir(info);
client.quit();
});

View File

@@ -1,38 +0,0 @@
'use strict';
// Read a file from disk, store it in Redis, then read it back from Redis.
var redis = require('redis');
var client = redis.createClient({
return_buffers: true
});
var fs = require('fs');
var assert = require('assert');
var filename = 'grumpyCat.jpg';
// Get the file I use for testing like this:
// curl http://media4.popsugar-assets.com/files/2014/08/08/878/n/1922507/caef16ec354ca23b_thumb_temp_cover_file32304521407524949.xxxlarge/i/Funny-Cat-GIFs.jpg -o grumpyCat.jpg
// or just use your own file.
// Read a file from fs, store it in Redis, get it back from Redis, write it back to fs.
fs.readFile(filename, function (err, data) {
if (err) throw err;
console.log('Read ' + data.length + ' bytes from filesystem.');
client.set(filename, data, redis.print); // set entire file
client.get(filename, function (err, reply) { // get entire file
if (err) {
console.log('Get error: ' + err);
} else {
assert.strictEqual(data.inspect(), reply.inspect());
fs.writeFile('duplicate_' + filename, reply, function (err) {
if (err) {
console.log('Error on write: ' + err);
} else {
console.log('File written.');
}
client.end();
});
}
});
});

View File

@@ -1,7 +0,0 @@
'use strict';
var client = require('redis').createClient();
client.mget(['sessions started', 'sessions started', 'foo'], function (err, res) {
console.dir(res);
});

View File

@@ -1,12 +0,0 @@
'use strict';
var client = require('../index').createClient();
var util = require('util');
client.monitor(function (err, res) {
console.log('Entering monitoring mode.');
});
client.on('monitor', function (time, args) {
console.log(time + ': ' + util.inspect(args));
});

View File

@@ -1,49 +0,0 @@
'use strict';
var redis = require('redis');
var client = redis.createClient();
var set_size = 20;
client.sadd('bigset', 'a member');
client.sadd('bigset', 'another member');
while (set_size > 0) {
client.sadd('bigset', 'member ' + set_size);
set_size -= 1;
}
// multi chain with an individual callback
client.multi()
.scard('bigset')
.smembers('bigset')
.keys('*', function (err, replies) {
client.mget(replies, redis.print);
})
.dbsize()
.exec(function (err, replies) {
console.log('MULTI got ' + replies.length + ' replies');
replies.forEach(function (reply, index) {
console.log('Reply ' + index + ': ' + reply.toString());
});
});
client.mset('incr thing', 100, 'incr other thing', 1, redis.print);
// start a separate multi command queue
var multi = client.multi();
multi.incr('incr thing', redis.print);
multi.incr('incr other thing', redis.print);
// runs immediately
client.get('incr thing', redis.print); // 100
// drains multi queue and runs atomically
multi.exec(function (err, replies) {
console.log(replies); // 101, 2
});
// you can re-run the same transaction if you like
multi.exec(function (err, replies) {
console.log(replies); // 102, 3
client.quit();
});

View File

@@ -1,31 +0,0 @@
'use strict';
var redis = require('redis');
var client = redis.createClient();
// start a separate command queue for multi
var multi = client.multi();
multi.incr('incr thing', redis.print);
multi.incr('incr other thing', redis.print);
// runs immediately
client.mset('incr thing', 100, 'incr other thing', 1, redis.print);
// drains multi queue and runs atomically
multi.exec(function (err, replies) {
console.log(replies); // 101, 2
});
// you can re-run the same transaction if you like
multi.exec(function (err, replies) {
console.log(replies); // 102, 3
client.quit();
});
client.multi([
['mget', 'multifoo', 'multibar', redis.print],
['incr', 'multifoo'],
['incr', 'multibar']
]).exec(function (err, replies) {
console.log(replies.toString());
});

View File

@@ -1,33 +0,0 @@
'use strict';
var redis = require('redis');
var client1 = redis.createClient();
var client2 = redis.createClient();
var client3 = redis.createClient();
var client4 = redis.createClient();
var msg_count = 0;
client1.on('psubscribe', function (pattern, count) {
console.log('client1 psubscribed to ' + pattern + ', ' + count + ' total subscriptions');
client2.publish('channeltwo', 'Me!');
client3.publish('channelthree', 'Me too!');
client4.publish('channelfour', 'And me too!');
});
client1.on('punsubscribe', function (pattern, count) {
console.log('client1 punsubscribed from ' + pattern + ', ' + count + ' total subscriptions');
client4.end();
client3.end();
client2.end();
client1.end();
});
client1.on('pmessage', function (pattern, channel, message) {
console.log('(' + pattern + ') client1 received message on ' + channel + ': ' + message);
msg_count += 1;
if (msg_count === 3) {
client1.punsubscribe();
}
});
client1.psubscribe('channel*');

View File

@@ -1,42 +0,0 @@
'use strict';
var redis = require('redis');
var client1 = redis.createClient();
var msg_count = 0;
var client2 = redis.createClient();
// Most clients probably don't do much on 'subscribe'. This example uses it to coordinate things within one program.
client1.on('subscribe', function (channel, count) {
console.log('client1 subscribed to ' + channel + ', ' + count + ' total subscriptions');
if (count === 2) {
client2.publish('a nice channel', 'I am sending a message.');
client2.publish('another one', 'I am sending a second message.');
client2.publish('a nice channel', 'I am sending my last message.');
}
});
client1.on('unsubscribe', function (channel, count) {
console.log('client1 unsubscribed from ' + channel + ', ' + count + ' total subscriptions');
if (count === 0) {
client2.end();
client1.end();
}
});
client1.on('message', function (channel, message) {
console.log('client1 channel ' + channel + ': ' + message);
msg_count += 1;
if (msg_count === 3) {
client1.unsubscribe();
}
});
client1.on('ready', function () {
// if you need auth, do it here
client1.incr('did a thing');
client1.subscribe('a nice channel', 'another one');
});
client2.on('ready', function () {
// if you need auth, do it here
});

View File

@@ -1,51 +0,0 @@
'use strict';
var redis = require('redis');
var client = redis.createClient();
var cursor = '0';
function scan () {
client.scan(
cursor,
'MATCH', 'q:job:*',
'COUNT', '10',
function (err, res) {
if (err) throw err;
// Update the cursor position for the next scan
cursor = res[0];
// get the SCAN result for this iteration
var keys = res[1];
// Remember: more or less than COUNT or no keys may be returned
// See http://redis.io/commands/scan#the-count-option
// Also, SCAN may return the same key multiple times
// See http://redis.io/commands/scan#scan-guarantees
// Additionally, you should always have the code that uses the keys
// before the code checking the cursor.
if (keys.length > 0) {
console.log('Array of matching keys', keys);
}
// It's important to note that the cursor and returned keys
// vary independently. The scan is never complete until redis
// returns a non-zero cursor. However, with MATCH and large
// collections, most iterations will return an empty keys array.
// Still, a cursor of zero DOES NOT mean that there are no keys.
// A zero cursor just means that the SCAN is complete, but there
// might be one last batch of results to process.
// From <http://redis.io/commands/scan>:
// 'An iteration starts when the cursor is set to 0,
// and terminates when the cursor returned by the server is 0.'
if (cursor === '0') {
return console.log('Iteration complete');
}
return scan();
}
);
}
scan();

View File

@@ -1,26 +0,0 @@
'use strict';
var redis = require('redis');
var client = redis.createClient();
client.on('error', function (err) {
console.log('error event - ' + client.host + ':' + client.port + ' - ' + err);
});
client.set('string key', 'string val', redis.print);
client.hset('hash key', 'hashtest 1', 'some value', redis.print);
client.hset(['hash key', 'hashtest 2', 'some other value'], redis.print);
client.hkeys('hash key', function (err, replies) {
if (err) {
return console.error('error response - ' + err);
}
console.log(replies.length + ' replies:');
replies.forEach(function (reply, i) {
console.log(' ' + i + ': ' + reply);
});
});
client.quit(function (err, res) {
console.log('Exiting from quit command.');
});

View File

@@ -1,19 +0,0 @@
'use strict';
var redis = require('redis');
var client = redis.createClient();
client.sadd('mylist', 1);
client.sadd('mylist', 2);
client.sadd('mylist', 3);
client.set('weight_1', 5);
client.set('weight_2', 500);
client.set('weight_3', 1);
client.set('object_1', 'foo');
client.set('object_2', 'bar');
client.set('object_3', 'qux');
client.sort('mylist', 'by', 'weight_*', 'get', 'object_*', redis.print);
// Prints Reply: qux,foo,bar

View File

@@ -1,47 +0,0 @@
'use strict';
var redis = require('redis');
var client1 = redis.createClient();
var client2 = redis.createClient();
var client3 = redis.createClient();
client1.xadd('mystream', '*', 'field1', 'm1', function (err) {
if (err) {
return console.error(err);
}
client1.xgroup('CREATE', 'mystream', 'mygroup', '$', function (err) {
if (err) {
return console.error(err);
}
});
client2.xreadgroup('GROUP', 'mygroup', 'consumer', 'Block', 1000, 'NOACK',
'STREAMS', 'mystream', '>', function (err, stream) {
if (err) {
return console.error(err);
}
console.log('client2 ' + stream);
});
client3.xreadgroup('GROUP', 'mygroup', 'consumer', 'Block', 1000, 'NOACK',
'STREAMS', 'mystream', '>', function (err, stream) {
if (err) {
return console.error(err);
}
console.log('client3 ' + stream);
});
client1.xadd('mystream', '*', 'field1', 'm2', function (err) {
if (err) {
return console.error(err);
}
});
client1.xadd('mystream', '*', 'field1', 'm3', function (err) {
if (err) {
return console.error(err);
}
});
});

View File

@@ -1,17 +0,0 @@
'use strict';
// Sending commands in response to other commands.
// This example runs 'type' against every key in the database
//
var client = require('redis').createClient();
client.keys('*', function (err, keys) {
keys.forEach(function (key, pos) {
client.type(key, function (err, keytype) {
console.log(key + ' is ' + keytype);
if (pos === (keys.length - 1)) {
client.quit();
}
});
});
});

View File

@@ -1,17 +0,0 @@
'use strict';
var client = require('redis').createClient();
// build a map of all keys and their types
client.keys('*', function (err, all_keys) {
var key_types = {};
all_keys.forEach(function (key, pos) { // use second arg of forEach to get pos
client.type(key, function (err, type) {
key_types[key] = type;
if (pos === all_keys.length - 1) { // callbacks all run in order
console.dir(key_types);
}
});
});
});

View File

@@ -1,32 +0,0 @@
'use strict';
var redis = require('redis');
var client = redis.createClient('/tmp/redis.sock');
var profiler = require('v8-profiler');
client.on('connect', function () {
console.log('Got Unix socket connection.');
});
client.on('error', function (err) {
console.log(err.message);
});
client.set('space chars', 'space value');
setInterval(function () {
client.get('space chars');
}, 100);
function done () {
client.info(function (err, reply) {
console.log(reply.toString());
client.quit();
});
}
setTimeout(function () {
console.log('Taking snapshot.');
profiler.takeSnapshot();
done();
}, 5000);

View File

@@ -1,33 +0,0 @@
'use strict';
// A simple web server that generates dyanmic content based on responses from Redis
var http = require('http');
var redis_client = require('redis').createClient();
http.createServer(function (request, response) { // The server
response.writeHead(200, {
'Content-Type': 'text/plain'
});
var redis_info, total_requests;
redis_client.info(function (err, reply) {
redis_info = reply; // stash response in outer scope
});
redis_client.incr('requests', function (err, reply) {
total_requests = reply; // stash response in outer scope
});
redis_client.hincrby('ip', request.connection.remoteAddress, 1);
redis_client.hgetall('ip', function (err, reply) {
// This is the last reply, so all of the previous replies must have completed already
response.write('This page was generated after talking to redis.\n\n' +
'Redis info:\n' + redis_info + '\n' +
'Total requests: ' + total_requests + '\n\n' +
'IP count: \n');
Object.keys(reply).forEach(function (ip) {
response.write(' ' + ip + ': ' + reply[ip] + '\n');
});
response.end();
});
}).listen(80);

1039
index.js

File diff suppressed because it is too large Load Diff

10
index.ts Normal file
View File

@@ -0,0 +1,10 @@
import RedisClient from './lib/client';
import RedisCluster from './lib/cluster';
export const createClient = RedisClient.create;
export const commandOptions = RedisClient.commandOptions;
export const createCluster = RedisCluster.create;
export { defineScript } from './lib/lua-script';

562
lib/client.spec.ts Normal file
View File

@@ -0,0 +1,562 @@
import { strict as assert, AssertionError } from 'assert';
import { once } from 'events';
import { itWithClient, TEST_REDIS_SERVERS, TestRedisServers, waitTillBeenCalled, isRedisVersionGreaterThan } from './test-utils';
import RedisClient from './client';
import { AbortError, ClientClosedError, ConnectionTimeoutError, WatchError } from './errors';
import { defineScript } from './lua-script';
import { spy } from 'sinon';
export const SQUARE_SCRIPT = defineScript({
NUMBER_OF_KEYS: 0,
SCRIPT: 'return ARGV[1] * ARGV[1];',
transformArguments(number: number): Array<string> {
return [number.toString()];
},
transformReply(reply: number): number {
return reply;
}
});
describe('Client', () => {
describe('authentication', () => {
itWithClient(TestRedisServers.PASSWORD, 'Client should be authenticated', async client => {
assert.equal(
await client.ping(),
'PONG'
);
});
it('should not retry connecting if failed due to wrong auth', async () => {
const client = RedisClient.create({
socket: {
...TEST_REDIS_SERVERS[TestRedisServers.PASSWORD],
password: 'wrongpassword'
}
});
await assert.rejects(
client.connect(),
{
message: isRedisVersionGreaterThan([6]) ?
'WRONGPASS invalid username-password pair or user is disabled.' :
'ERR invalid password'
}
);
assert.equal(client.isOpen, false);
});
});
describe('legacyMode', () => {
const client = RedisClient.create({
socket: TEST_REDIS_SERVERS[TestRedisServers.OPEN],
scripts: {
square: SQUARE_SCRIPT
},
legacyMode: true
});
before(() => client.connect());
afterEach(() => client.v4.flushAll());
after(() => client.disconnect());
it('client.sendCommand should call the callback', done => {
(client as any).sendCommand('PING', (err?: Error, reply?: string) => {
if (err) {
return done(err);
}
try {
assert.equal(reply, 'PONG');
done();
} catch (err) {
done(err);
}
});
});
it('client.sendCommand should work without callback', async () => {
(client as any).sendCommand('PING');
await client.v4.ping(); // make sure the first command was replied
});
it('client.v4.sendCommand should return a promise', async () => {
assert.equal(
await client.v4.sendCommand(['PING']),
'PONG'
);
});
it('client.{command} should accept vardict arguments', done => {
(client as any).set('a', 'b', (err?: Error, reply?: string) => {
if (err) {
return done(err);
}
try {
assert.equal(reply, 'OK');
done();
} catch (err) {
done(err);
}
});
});
it('client.{command} should accept arguments array', done => {
(client as any).set(['a', 'b'], (err?: Error, reply?: string) => {
if (err) {
return done(err);
}
try {
assert.equal(reply, 'OK');
done();
} catch (err) {
done(err);
}
});
});
it('client.{command} should accept mix of strings and array of strings', done => {
(client as any).set(['a'], 'b', ['XX'], (err?: Error, reply?: string) => {
if (err) {
return done(err);
}
try {
assert.equal(reply, null);
done();
} catch (err) {
done(err);
}
});
});
it('client.multi.ping.exec should call the callback', done => {
(client as any).multi()
.ping()
.exec((err?: Error, reply?: string) => {
if (err) {
return done(err);
}
try {
assert.deepEqual(reply, ['PONG']);
done();
} catch (err) {
done(err);
}
});
});
it('client.multi.ping.exec should work without callback', async () => {
(client as any).multi()
.ping()
.exec();
await client.v4.ping(); // make sure the first command was replied
});
it('client.multi.ping.v4.ping.v4.exec should return a promise', async () => {
assert.deepEqual(
await ((client as any).multi()
.ping()
.v4.ping()
.v4.exec()),
['PONG', 'PONG']
);
});
it('client.{script} should return a promise', async () => {
assert.equal(await client.square(2), 4);
});
});
describe('events', () => {
it('connect, ready, end', async () => {
const client = RedisClient.create({
socket: TEST_REDIS_SERVERS[TestRedisServers.OPEN]
});
await Promise.all([
client.connect(),
once(client, 'connect'),
once(client, 'ready')
]);
await Promise.all([
client.disconnect(),
once(client, 'end')
]);
});
});
describe('sendCommand', () => {
itWithClient(TestRedisServers.OPEN, 'PING', async client => {
assert.equal(await client.sendCommand(['PING']), 'PONG');
});
describe('AbortController', () => {
before(function () {
if (!global.AbortController) {
this.skip();
}
});
itWithClient(TestRedisServers.OPEN, 'success', async client => {
await client.sendCommand(['PING'], {
signal: new AbortController().signal
});
});
itWithClient(TestRedisServers.OPEN, 'AbortError', client => {
const controller = new AbortController();
controller.abort();
return assert.rejects(
client.sendCommand(['PING'], {
signal: controller.signal
}),
AbortError
);
});
});
});
describe('multi', () => {
itWithClient(TestRedisServers.OPEN, 'simple', async client => {
assert.deepEqual(
await client.multi()
.ping()
.set('key', 'value')
.get('key')
.exec(),
['PONG', 'OK', 'value']
);
});
itWithClient(TestRedisServers.OPEN, 'should reject the whole chain on error', client => {
client.on('error', () => {
// ignore errors
});
return assert.rejects(
client.multi()
.ping()
.addCommand(['DEBUG', 'RESTART'])
.ping()
.exec()
);
});
it('with script', async () => {
const client = RedisClient.create({
scripts: {
square: SQUARE_SCRIPT
}
});
await client.connect();
try {
assert.deepEqual(
await client.multi()
.square(2)
.exec(),
[4]
);
} finally {
await client.disconnect();
}
});
itWithClient(TestRedisServers.OPEN, 'WatchError', async client => {
await client.watch('key');
await client.set(
RedisClient.commandOptions({
isolated: true
}),
'key',
'1'
);
await assert.rejects(
client.multi()
.decr('key')
.exec(),
WatchError
);
});
});
it('scripts', async () => {
const client = RedisClient.create({
scripts: {
square: SQUARE_SCRIPT
}
});
await client.connect();
try {
assert.equal(
await client.square(2),
4
);
} finally {
await client.disconnect();
}
});
it('modules', async () => {
const client = RedisClient.create({
modules: {
module: {
echo: {
transformArguments(message: string): Array<string> {
return ['ECHO', message];
},
transformReply(reply: string): string {
return reply;
}
}
}
}
});
await client.connect();
try {
assert.equal(
await client.module.echo('message'),
'message'
);
} finally {
await client.disconnect();
}
});
itWithClient(TestRedisServers.OPEN, 'executeIsolated', async client => {
await client.sendCommand(['CLIENT', 'SETNAME', 'client']);
assert.equal(
await client.executeIsolated(isolatedClient =>
isolatedClient.sendCommand(['CLIENT', 'GETNAME'])
),
null
);
});
itWithClient(TestRedisServers.OPEN, 'should reconnect after DEBUG RESTART', async client => {
client.on('error', () => {
// ignore errors
});
await client.sendCommand(['CLIENT', 'SETNAME', 'client']);
await assert.rejects(client.sendCommand(['DEBUG', 'RESTART']));
assert.ok(await client.sendCommand(['CLIENT', 'GETNAME']) === null);
});
itWithClient(TestRedisServers.OPEN, 'should SELECT db after reconnection', async client => {
client.on('error', () => {
// ignore errors
});
await client.select(1);
await assert.rejects(client.sendCommand(['DEBUG', 'RESTART']));
assert.equal(
(await client.clientInfo()).db,
1
);
}, {
// because of CLIENT INFO
minimumRedisVersion: [6, 2]
});
itWithClient(TestRedisServers.OPEN, 'scanIterator', async client => {
const promises = [],
keys = new Set();
for (let i = 0; i < 100; i++) {
const key = i.toString();
keys.add(key);
promises.push(client.set(key, ''));
}
await Promise.all(promises);
const results = new Set();
for await (const key of client.scanIterator()) {
results.add(key);
}
assert.deepEqual(keys, results);
});
itWithClient(TestRedisServers.OPEN, 'hScanIterator', async client => {
const hash: Record<string, string> = {};
for (let i = 0; i < 100; i++) {
hash[i.toString()] = i.toString();
}
await client.hSet('key', hash);
const results: Record<string, string> = {};
for await (const { field, value } of client.hScanIterator('key')) {
results[field] = value;
}
assert.deepEqual(hash, results);
});
itWithClient(TestRedisServers.OPEN, 'sScanIterator', async client => {
const members = new Set<string>();
for (let i = 0; i < 100; i++) {
members.add(i.toString());
}
await client.sAdd('key', Array.from(members));
const results = new Set<string>();
for await (const key of client.sScanIterator('key')) {
results.add(key);
}
assert.deepEqual(members, results);
});
itWithClient(TestRedisServers.OPEN, 'zScanIterator', async client => {
const members = [];
for (let i = 0; i < 100; i++) {
members.push({
score: 1,
value: i.toString()
});
}
await client.zAdd('key', members);
const map = new Map();
for await (const member of client.zScanIterator('key')) {
map.set(member.value, member.score);
}
type MemberTuple = [string, number];
function sort(a: MemberTuple, b: MemberTuple) {
return Number(b[0]) - Number(a[0]);
}
assert.deepEqual(
[...map.entries()].sort(sort),
members.map<MemberTuple>(member => [member.value, member.score]).sort(sort)
);
});
itWithClient(TestRedisServers.OPEN, 'PubSub', async publisher => {
const subscriber = publisher.duplicate();
await subscriber.connect();
try {
const channelListener1 = spy(),
channelListener2 = spy(),
patternListener = spy();
await Promise.all([
subscriber.subscribe('channel', channelListener1),
subscriber.subscribe('channel', channelListener2),
subscriber.pSubscribe('channel*', patternListener)
]);
await Promise.all([
waitTillBeenCalled(channelListener1),
waitTillBeenCalled(channelListener2),
waitTillBeenCalled(patternListener),
publisher.publish('channel', 'message')
]);
assert.ok(channelListener1.calledOnceWithExactly('message', 'channel'));
assert.ok(channelListener2.calledOnceWithExactly('message', 'channel'));
assert.ok(patternListener.calledOnceWithExactly('message', 'channel'));
await subscriber.unsubscribe('channel', channelListener1);
await Promise.all([
waitTillBeenCalled(channelListener2),
waitTillBeenCalled(patternListener),
publisher.publish('channel', 'message')
]);
assert.ok(channelListener1.calledOnce);
assert.ok(channelListener2.calledTwice);
assert.ok(channelListener2.secondCall.calledWithExactly('message', 'channel'));
assert.ok(patternListener.calledTwice);
assert.ok(patternListener.secondCall.calledWithExactly('message', 'channel'));
await subscriber.unsubscribe('channel');
await Promise.all([
waitTillBeenCalled(patternListener),
publisher.publish('channel', 'message')
]);
assert.ok(channelListener1.calledOnce);
assert.ok(channelListener2.calledTwice);
assert.ok(patternListener.calledThrice);
assert.ok(patternListener.thirdCall.calledWithExactly('message', 'channel'));
await subscriber.pUnsubscribe();
await publisher.publish('channel', 'message');
assert.ok(channelListener1.calledOnce);
assert.ok(channelListener2.calledTwice);
assert.ok(patternListener.calledThrice);
} finally {
await subscriber.disconnect();
}
});
it('ConnectionTimeoutError', async () => {
const client = RedisClient.create({
socket: {
...TEST_REDIS_SERVERS[TestRedisServers.OPEN],
connectTimeout: 1
}
});
try {
const promise = assert.rejects(client.connect(), ConnectionTimeoutError),
start = process.hrtime.bigint();
// block the event loop for 1ms, to make sure the connection will timeout
while (process.hrtime.bigint() - start < 1_000_000) {}
await promise;
} catch (err) {
if (err instanceof AssertionError) {
await client.disconnect();
}
throw err;
}
});
it('client.quit', async () => {
const client = RedisClient.create({
socket: TEST_REDIS_SERVERS[TestRedisServers.OPEN]
});
await client.connect();
try {
const quitPromise = client.quit();
assert.equal(client.isOpen, false);
await Promise.all([
quitPromise,
assert.rejects(client.ping(), ClientClosedError)
]);
} finally {
if (client.isOpen) {
await client.disconnect();
}
}
});
});

468
lib/client.ts Normal file
View File

@@ -0,0 +1,468 @@
import RedisSocket, { RedisSocketOptions } from './socket';
import RedisCommandsQueue, { PubSubListener, PubSubSubscribeCommands, PubSubUnsubscribeCommands, QueueCommandOptions } from './commands-queue';
import COMMANDS from './commands';
import { RedisCommand, RedisModules, RedisReply } from './commands';
import RedisMultiCommand, { MultiQueuedCommand, RedisMultiCommandType } from './multi-command';
import EventEmitter from 'events';
import { CommandOptions, commandOptions, isCommandOptions } from './command-options';
import { RedisLuaScript, RedisLuaScripts } from './lua-script';
import { ScanOptions, ZMember } from './commands/generic-transformers';
import { ScanCommandOptions } from './commands/SCAN';
import { HScanTuple } from './commands/HSCAN';
import { encodeCommand, extendWithDefaultCommands, extendWithModulesAndScripts, transformCommandArguments } from './commander';
import { Pool, Options as PoolOptions, createPool } from 'generic-pool';
import { ClientClosedError } from './errors';
export interface RedisClientOptions<M = RedisModules, S = RedisLuaScripts> {
socket?: RedisSocketOptions;
modules?: M;
scripts?: S;
commandsQueueMaxLength?: number;
readonly?: boolean;
legacyMode?: boolean;
isolationPoolOptions?: PoolOptions;
}
export type RedisCommandSignature<C extends RedisCommand> =
(...args: Parameters<C['transformArguments']> | [options: CommandOptions<ClientCommandOptions>, ...rest: Parameters<C['transformArguments']>]) => Promise<ReturnType<C['transformReply']>>;
type WithCommands = {
[P in keyof typeof COMMANDS]: RedisCommandSignature<(typeof COMMANDS)[P]>;
};
type WithModules<M extends RedisModules> = {
[P in keyof M]: {
[C in keyof M[P]]: RedisCommandSignature<M[P][C]>;
};
};
type WithScripts<S extends RedisLuaScripts> = {
[P in keyof S]: RedisCommandSignature<S[P]>;
};
export type WithPlugins<M extends RedisModules, S extends RedisLuaScripts> =
WithCommands & WithModules<M> & WithScripts<S>;
export type RedisClientType<M extends RedisModules, S extends RedisLuaScripts> =
WithPlugins<M, S> & RedisClient<M, S>;
export interface ClientCommandOptions extends QueueCommandOptions {
isolated?: boolean;
}
export default class RedisClient<M extends RedisModules = RedisModules, S extends RedisLuaScripts = RedisLuaScripts> extends EventEmitter {
static commandOptions(options: ClientCommandOptions): CommandOptions<ClientCommandOptions> {
return commandOptions(options);
}
static async commandsExecutor(
this: RedisClient,
command: RedisCommand,
args: Array<unknown>
): Promise<ReturnType<typeof command['transformReply']>> {
const { args: redisArgs, options } = transformCommandArguments<ClientCommandOptions>(command, args);
const reply = command.transformReply(
await this.#sendCommand(redisArgs, options),
redisArgs.preserve
);
return reply;
}
static async #scriptsExecutor(
this: RedisClient,
script: RedisLuaScript,
args: Array<unknown>
): Promise<typeof script['transformArguments']> {
const { args: redisArgs, options } = transformCommandArguments<ClientCommandOptions>(script, args);
const reply = script.transformReply(
await this.executeScript(script, redisArgs, options),
redisArgs.preserve
);
return reply;
}
static create<M extends RedisModules, S extends RedisLuaScripts>(options?: RedisClientOptions<M, S>): RedisClientType<M, S> {
const Client = (<any>extendWithModulesAndScripts({
BaseClass: RedisClient,
modules: options?.modules,
modulesCommandsExecutor: RedisClient.commandsExecutor,
scripts: options?.scripts,
scriptsExecutor: RedisClient.#scriptsExecutor
}));
if (Client !== RedisClient) {
Client.prototype.Multi = RedisMultiCommand.extend(options);
}
return new Client(options);
}
readonly #options?: RedisClientOptions<M, S>;
readonly #socket: RedisSocket;
readonly #queue: RedisCommandsQueue;
readonly #isolationPool: Pool<RedisClientType<M, S>>;
readonly #v4: Record<string, any> = {};
#selectedDB = 0;
get options(): RedisClientOptions<M> | null | undefined {
return this.#options;
}
get isOpen(): boolean {
return this.#socket.isOpen;
}
get v4(): Record<string, any> {
if (!this.#options?.legacyMode) {
throw new Error('the client is not in "legacy mode"');
}
return this.#v4;
}
constructor(options?: RedisClientOptions<M, S>) {
super();
this.#options = options;
this.#socket = this.#initiateSocket();
this.#queue = this.#initiateQueue();
this.#isolationPool = createPool({
create: async () => {
const duplicate = this.duplicate();
await duplicate.connect();
return duplicate;
},
destroy: client => client.disconnect()
}, options?.isolationPoolOptions);
this.#legacyMode();
}
#initiateSocket(): RedisSocket {
const socketInitiator = async (): Promise<void> => {
const v4Commands = this.#options?.legacyMode ? this.#v4 : this,
promises = [];
if (this.#selectedDB !== 0) {
promises.push(v4Commands.select(RedisClient.commandOptions({ asap: true }), this.#selectedDB));
}
if (this.#options?.readonly) {
promises.push(v4Commands.readonly(RedisClient.commandOptions({ asap: true })));
}
if (this.#options?.socket?.username || this.#options?.socket?.password) {
promises.push(v4Commands.auth(RedisClient.commandOptions({ asap: true }), this.#options.socket));
}
const resubscribePromise = this.#queue.resubscribe();
if (resubscribePromise) {
promises.push(resubscribePromise);
this.#tick();
}
await Promise.all(promises);
};
return new RedisSocket(socketInitiator, this.#options?.socket)
.on('data', data => this.#queue.parseResponse(data))
.on('error', err => {
this.emit('error', err);
this.#queue.flushWaitingForReply(err);
})
.on('connect', () => this.emit('connect'))
.on('ready', () => {
this.emit('ready');
this.#tick();
})
.on('reconnecting', () => this.emit('reconnecting'))
.on('end', () => this.emit('end'));
}
#initiateQueue(): RedisCommandsQueue {
return new RedisCommandsQueue(
this.#options?.commandsQueueMaxLength,
(encodedCommands: string) => this.#socket.write(encodedCommands)
);
}
#legacyMode(): void {
if (!this.#options?.legacyMode) return;
(this as any).#v4.sendCommand = this.#sendCommand.bind(this);
(this as any).sendCommand = (...args: Array<unknown>): void => {
const callback = typeof args[args.length - 1] === 'function' ? args[args.length - 1] as Function : undefined,
actualArgs = !callback ? args : args.slice(0, -1);
this.#sendCommand(actualArgs.flat() as Array<string>)
.then((reply: unknown) => {
if (!callback) return;
// https://github.com/NodeRedis/node-redis#commands:~:text=minimal%20parsing
callback(null, reply);
})
.catch((err: Error) => {
if (!callback) {
this.emit('error', err);
return;
}
callback(err);
});
};
for (const name of Object.keys(COMMANDS)) {
this.#defineLegacyCommand(name);
}
// hard coded commands
this.#defineLegacyCommand('SELECT');
this.#defineLegacyCommand('select');
this.#defineLegacyCommand('SUBSCRIBE');
this.#defineLegacyCommand('subscribe');
this.#defineLegacyCommand('PSUBSCRIBE');
this.#defineLegacyCommand('pSubscribe');
this.#defineLegacyCommand('UNSUBSCRIBE');
this.#defineLegacyCommand('unsubscribe');
this.#defineLegacyCommand('PUNSUBSCRIBE');
this.#defineLegacyCommand('pUnsubscribe');
this.#defineLegacyCommand('QUIT');
this.#defineLegacyCommand('quit');
}
#defineLegacyCommand(name: string): void {
(this as any).#v4[name] = (this as any)[name].bind(this);
(this as any)[name] = (...args: Array<unknown>): void => {
(this as any).sendCommand(name, ...args);
};
}
duplicate(): RedisClientType<M, S> {
return new (Object.getPrototypeOf(this).constructor)(this.#options);
}
async connect(): Promise<void> {
await this.#socket.connect();
}
async SELECT(db: number): Promise<void>;
async SELECT(options: CommandOptions<ClientCommandOptions>, db: number): Promise<void>;
async SELECT(options?: any, db?: any): Promise<void> {
if (!isCommandOptions(options)) {
db = options;
options = null;
}
await this.#sendCommand(['SELECT', db.toString()], options);
this.#selectedDB = db;
}
select = this.SELECT;
SUBSCRIBE(channels: string | Array<string>, listener: PubSubListener): Promise<void> {
return this.#subscribe(PubSubSubscribeCommands.SUBSCRIBE, channels, listener);
}
subscribe = this.SUBSCRIBE;
PSUBSCRIBE(patterns: string | Array<string>, listener: PubSubListener): Promise<void> {
return this.#subscribe(PubSubSubscribeCommands.PSUBSCRIBE, patterns, listener);
}
pSubscribe = this.PSUBSCRIBE;
#subscribe(command: PubSubSubscribeCommands, channels: string | Array<string>, listener: PubSubListener): Promise<void> {
const promise = this.#queue.subscribe(command, channels, listener);
this.#tick();
return promise;
}
UNSUBSCRIBE(channels?: string | Array<string>, listener?: PubSubListener): Promise<void> {
return this.#unsubscribe(PubSubUnsubscribeCommands.UNSUBSCRIBE, channels, listener);
}
unsubscribe = this.UNSUBSCRIBE;
PUNSUBSCRIBE(patterns?: string | Array<string>, listener?: PubSubListener): Promise<void> {
return this.#unsubscribe(PubSubUnsubscribeCommands.PUNSUBSCRIBE, patterns, listener);
}
pUnsubscribe = this.PUNSUBSCRIBE;
#unsubscribe(command: PubSubUnsubscribeCommands, channels?: string | Array<string>, listener?: PubSubListener): Promise<void> {
const promise = this.#queue.unsubscribe(command, channels, listener);
this.#tick();
return promise;
}
QUIT(): Promise<void> {
return this.#socket.quit(async () => {
this.#queue.addEncodedCommand(encodeCommand(['QUIT']));
this.#tick();
});
}
quit = this.QUIT;
sendCommand<T = unknown>(args: Array<string>, options?: ClientCommandOptions): Promise<T> {
return this.#sendCommand(args, options);
}
// using `#sendCommand` cause `sendCommand` is overwritten in legacy mode
#sendCommand<T = RedisReply>(args: Array<string>, options?: ClientCommandOptions): Promise<T> {
return this.sendEncodedCommand(encodeCommand(args), options);
}
async sendEncodedCommand<T = RedisReply>(encodedCommand: string, options?: ClientCommandOptions): Promise<T> {
if (!this.#socket.isOpen) {
throw new ClientClosedError();
}
if (options?.isolated) {
return this.executeIsolated(isolatedClient =>
isolatedClient.sendEncodedCommand(encodedCommand, {
...options,
isolated: false
})
);
}
const promise = this.#queue.addEncodedCommand<T>(encodedCommand, options);
this.#tick();
return await promise;
}
executeIsolated<T>(fn: (client: RedisClientType<M, S>) => T | Promise<T>): Promise<T> {
return this.#isolationPool.use(fn);
}
async executeScript(script: RedisLuaScript, args: Array<string>, options?: ClientCommandOptions): Promise<ReturnType<typeof script['transformReply']>> {
try {
return await this.#sendCommand([
'EVALSHA',
script.SHA1,
script.NUMBER_OF_KEYS.toString(),
...args
], options);
} catch (err: any) {
if (!err?.message?.startsWith?.('NOSCRIPT')) {
throw err;
}
return await this.#sendCommand([
'EVAL',
script.SCRIPT,
script.NUMBER_OF_KEYS.toString(),
...args
], options);
}
}
#multiExecutor(commands: Array<MultiQueuedCommand>, chainId?: symbol): Promise<Array<RedisReply>> {
const promise = Promise.all(
commands.map(({encodedCommand}) => {
return this.#queue.addEncodedCommand(encodedCommand, RedisClient.commandOptions({
chainId
}));
})
);
this.#tick();
return promise;
}
multi(): RedisMultiCommandType<M, S> {
return new (this as any).Multi(
this.#multiExecutor.bind(this),
this.#options
);
}
async* scanIterator(options?: ScanCommandOptions): AsyncIterable<string> {
let cursor = 0;
do {
const reply = await (this as any).scan(cursor, options);
cursor = reply.cursor;
for (const key of reply.keys) {
yield key;
}
} while (cursor !== 0)
}
async* hScanIterator(key: string, options?: ScanOptions): AsyncIterable<HScanTuple> {
let cursor = 0;
do {
const reply = await (this as any).hScan(key, cursor, options);
cursor = reply.cursor;
for (const tuple of reply.tuples) {
yield tuple;
}
} while (cursor !== 0)
}
async* sScanIterator(key: string, options?: ScanOptions): AsyncIterable<string> {
let cursor = 0;
do {
const reply = await (this as any).sScan(key, cursor, options);
cursor = reply.cursor;
for (const member of reply.members) {
yield member;
}
} while (cursor !== 0)
}
async* zScanIterator(key: string, options?: ScanOptions): AsyncIterable<ZMember> {
let cursor = 0;
do {
const reply = await (this as any).zScan(key, cursor, options);
cursor = reply.cursor;
for (const member of reply.members) {
yield member;
}
} while (cursor !== 0)
}
async disconnect(): Promise<void> {
this.#queue.flushAll(new Error('Disconnecting'));
await Promise.all([
this.#socket.disconnect(),
this.#destroyIsolationPool()
]);
}
async #destroyIsolationPool(): Promise<void> {
await this.#isolationPool.drain();
await this.#isolationPool.clear();
}
#isTickQueued = false;
#tick(): void {
const {chunkRecommendedSize} = this.#socket;
if (!chunkRecommendedSize) {
return;
}
if (!this.#isTickQueued && this.#queue.waitingToBeSentCommandsLength < chunkRecommendedSize) {
queueMicrotask(() => this.#tick());
this.#isTickQueued = true;
return;
}
const isBuffering = this.#queue.executeChunk(chunkRecommendedSize);
if (isBuffering === true) {
this.#socket.once('drain', () => this.#tick());
} else if (isBuffering === false) {
this.#tick();
return;
}
this.#isTickQueued = false;
}
}
extendWithDefaultCommands(RedisClient, RedisClient.commandsExecutor);
(RedisClient.prototype as any).Multi = RedisMultiCommand.extend();

221
lib/cluster-slots.ts Normal file
View File

@@ -0,0 +1,221 @@
import calculateSlot from 'cluster-key-slot';
import RedisClient, { RedisClientType } from './client';
import { RedisSocketOptions } from './socket';
import { RedisClusterMasterNode, RedisClusterReplicaNode } from './commands/CLUSTER_NODES';
import { RedisClusterOptions } from './cluster';
import { RedisModules } from './commands';
import { RedisLuaScripts } from './lua-script';
export interface ClusterNode<M extends RedisModules, S extends RedisLuaScripts> {
id: string;
client: RedisClientType<M, S>;
}
interface SlotNodes<M extends RedisModules, S extends RedisLuaScripts> {
master: ClusterNode<M, S>;
replicas: Array<ClusterNode<M, S>>;
clientIterator: IterableIterator<RedisClientType<M, S>> | undefined;
}
export default class RedisClusterSlots<M extends RedisModules, S extends RedisLuaScripts> {
readonly #options: RedisClusterOptions;
readonly #nodeByUrl = new Map<string, ClusterNode<M, S>>();
readonly #slots: Array<SlotNodes<M, S>> = [];
constructor(options: RedisClusterOptions) {
this.#options = options;
}
async connect(): Promise<void> {
for (const rootNode of this.#options.rootNodes) {
try {
await this.#discoverNodes(rootNode);
return;
} catch (err) {
console.error(err);
// this.emit('error', err);
}
}
throw new Error('None of the root nodes is available');
}
async discover(startWith: RedisClientType<M, S>): Promise<void> {
try {
await this.#discoverNodes(startWith.options?.socket);
return;
} catch (err) {
console.error(err);
// this.emit('error', err);
}
for (const { client } of this.#nodeByUrl.values()) {
if (client === startWith) continue;
try {
await this.#discoverNodes(client.options?.socket);
return;
} catch (err) {
console.error(err);
// this.emit('error', err);
}
}
throw new Error('None of the cluster nodes is available');
}
async #discoverNodes(socketOptions?: RedisSocketOptions): Promise<void> {
const client = RedisClient.create({
socket: socketOptions
});
await client.connect();
try {
await this.#reset(await client.clusterNodes());
} finally {
await client.disconnect(); // TODO: catch error from disconnect?
}
}
async #reset(masters: Array<RedisClusterMasterNode>): Promise<void> {
// Override this.#slots and add not existing clients to this.#clientByKey
const promises: Array<Promise<void>> = [],
clientsInUse = new Set<string>();
for (const master of masters) {
const slot = {
master: this.#initiateClientForNode(master, false, clientsInUse, promises),
replicas: this.#options.useReplicas ?
master.replicas.map(replica => this.#initiateClientForNode(replica, true, clientsInUse, promises)) :
[],
clientIterator: undefined // will be initiated in use
};
for (const { from, to } of master.slots) {
for (let i = from; i < to; i++) {
this.#slots[i] = slot;
}
}
}
// Remove unused clients from this.#clientBykey using clientsInUse
for (const [url, { client }] of this.#nodeByUrl.entries()) {
if (clientsInUse.has(url)) continue;
// TODO: ignore error from `.disconnect`?
promises.push(client.disconnect());
this.#nodeByUrl.delete(url);
}
await Promise.all(promises);
}
#initiateClientForNode(nodeData: RedisClusterMasterNode | RedisClusterReplicaNode, readonly: boolean, clientsInUse: Set<string>, promises: Array<Promise<void>>): ClusterNode<M, S> {
const url = `${nodeData.host}:${nodeData.port}`;
clientsInUse.add(url);
let node = this.#nodeByUrl.get(url);
if (!node) {
node = {
id: nodeData.id,
client: RedisClient.create({
socket: {
host: nodeData.host,
port: nodeData.port
},
readonly
})
};
promises.push(node.client.connect());
this.#nodeByUrl.set(url, node);
}
return node;
}
getSlotMaster(slot: number): ClusterNode<M, S> {
return this.#slots[slot].master;
}
*#slotClientIterator(slotNumber: number): IterableIterator<RedisClientType<M, S>> {
const slot = this.#slots[slotNumber];
yield slot.master.client;
for (const replica of slot.replicas) {
yield replica.client;
}
}
#getSlotClient(slotNumber: number): RedisClientType<M, S> {
const slot = this.#slots[slotNumber];
if (!slot.clientIterator) {
slot.clientIterator = this.#slotClientIterator(slotNumber);
}
const {done, value} = slot.clientIterator.next();
if (done) {
slot.clientIterator = undefined;
return this.#getSlotClient(slotNumber);
}
return value;
}
#randomClientIterator?: IterableIterator<ClusterNode<M, S>>;
#getRandomClient(): RedisClientType<M, S> {
if (!this.#nodeByUrl.size) {
throw new Error('Cluster is not connected');
}
if (!this.#randomClientIterator) {
this.#randomClientIterator = this.#nodeByUrl.values();
}
const {done, value} = this.#randomClientIterator.next();
if (done) {
this.#randomClientIterator = undefined;
return this.#getRandomClient();
}
return value.client;
}
getClient(firstKey?: string, isReadonly?: boolean): RedisClientType<M, S> {
if (!firstKey) {
return this.#getRandomClient();
}
const slot = calculateSlot(firstKey);
if (!isReadonly || !this.#options.useReplicas) {
return this.getSlotMaster(slot).client;
}
return this.#getSlotClient(slot);
}
getMasters(): Array<ClusterNode<M, S>> {
const masters = [];
for (const node of this.#nodeByUrl.values()) {
if (node.client.options?.readonly) continue;
masters.push(node);
}
return masters;
}
getNodeByUrl(url: string): ClusterNode<M, S> | undefined {
return this.#nodeByUrl.get(url);
}
async disconnect(): Promise<void> {
await Promise.all(
[...this.#nodeByUrl.values()].map(({ client }) => client.disconnect())
);
this.#nodeByUrl.clear();
this.#slots.splice(0);
}
}

115
lib/cluster.spec.ts Normal file
View File

@@ -0,0 +1,115 @@
import { strict as assert } from 'assert';
import RedisCluster from './cluster';
import { defineScript } from './lua-script';
import { itWithCluster, itWithDedicatedCluster, TestRedisClusters, TEST_REDIS_CLUSTERES } from './test-utils';
import calculateSlot from 'cluster-key-slot';
import { ClusterSlotStates } from './commands/CLUSTER_SETSLOT';
describe('Cluster', () => {
it('sendCommand', async () => {
const cluster = RedisCluster.create({
rootNodes: TEST_REDIS_CLUSTERES[TestRedisClusters.OPEN],
useReplicas: true
});
await cluster.connect();
try {
await cluster.ping();
await cluster.set('a', 'b');
await cluster.set('a{a}', 'bb');
await cluster.set('aa', 'bb');
await cluster.get('aa');
await cluster.get('aa');
await cluster.get('aa');
await cluster.get('aa');
} finally {
await cluster.disconnect();
}
});
itWithCluster(TestRedisClusters.OPEN, 'multi', async cluster => {
const key = 'key';
assert.deepEqual(
await cluster.multi(key)
.ping()
.set(key, 'value')
.get(key)
.exec(),
['PONG', 'OK', 'value']
);
});
it('scripts', async () => {
const cluster = RedisCluster.create({
rootNodes: TEST_REDIS_CLUSTERES[TestRedisClusters.OPEN],
scripts: {
add: defineScript({
NUMBER_OF_KEYS: 0,
SCRIPT: 'return ARGV[1] + 1;',
transformArguments(number: number): Array<string> {
assert.equal(number, 1);
return [number.toString()];
},
transformReply(reply: number): number {
assert.equal(reply, 2);
return reply;
}
})
}
});
await cluster.connect();
try {
assert.equal(
await cluster.add(1),
2
);
} finally {
await cluster.disconnect();
}
});
itWithDedicatedCluster('should handle live resharding', async cluster => {
const key = 'key',
value = 'value';
await cluster.set(key, value);
const slot = calculateSlot(key),
from = cluster.getSlotMaster(slot),
to = cluster.getMasters().find(node => node.id !== from.id);
await to!.client.clusterSetSlot(slot, ClusterSlotStates.IMPORTING, from.id);
// should be able to get the key from the original node before it was migrated
assert.equal(
await cluster.get(key),
value
);
await from.client.clusterSetSlot(slot, ClusterSlotStates.MIGRATING, to!.id);
// should be able to get the key from the original node using the "ASKING" command
assert.equal(
await cluster.get(key),
value
);
const { port: toPort } = <any>to!.client.options!.socket;
await from.client.migrate(
'127.0.0.1',
toPort,
key,
0,
10
);
// should be able to get the key from the new node
assert.equal(
await cluster.get(key),
value
);
});
});

202
lib/cluster.ts Normal file
View File

@@ -0,0 +1,202 @@
import { RedisCommand, RedisModules } from './commands';
import RedisClient, { ClientCommandOptions, RedisClientType, WithPlugins } from './client';
import { RedisSocketOptions } from './socket';
import RedisClusterSlots, { ClusterNode } from './cluster-slots';
import { RedisLuaScript, RedisLuaScripts } from './lua-script';
import { extendWithModulesAndScripts, extendWithDefaultCommands, transformCommandArguments } from './commander';
import RedisMultiCommand, { MultiQueuedCommand, RedisMultiCommandType } from './multi-command';
export interface RedisClusterOptions<M = RedisModules, S = RedisLuaScripts> {
rootNodes: Array<RedisSocketOptions>;
modules?: M;
scripts?: S;
useReplicas?: boolean;
maxCommandRedirections?: number;
}
export type RedisClusterType<M extends RedisModules, S extends RedisLuaScripts> =
WithPlugins<M, S> & RedisCluster;
export default class RedisCluster<M extends RedisModules = RedisModules, S extends RedisLuaScripts = RedisLuaScripts> {
static #extractFirstKey(command: RedisCommand, originalArgs: Array<unknown>, redisArgs: Array<string>): string | undefined {
if (command.FIRST_KEY_INDEX === undefined) {
return undefined;
} else if (typeof command.FIRST_KEY_INDEX === 'number') {
return redisArgs[command.FIRST_KEY_INDEX];
}
return command.FIRST_KEY_INDEX(...originalArgs);
}
static async commandsExecutor(
this: RedisCluster,
command: RedisCommand,
args: Array<unknown>
): Promise<ReturnType<typeof command['transformReply']>> {
const { args: redisArgs, options } = transformCommandArguments<ClientCommandOptions>(command, args);
const reply = command.transformReply(
await this.sendCommand(
RedisCluster.#extractFirstKey(command, args, redisArgs),
command.IS_READ_ONLY,
redisArgs,
options
),
redisArgs.preserve
);
return reply;
}
static async #scriptsExecutor(
this: RedisCluster,
script: RedisLuaScript,
args: Array<unknown>
): Promise<typeof script['transformArguments']> {
const { args: redisArgs, options } = transformCommandArguments<ClientCommandOptions>(script, args);
const reply = script.transformReply(
await this.executeScript(
script,
args,
redisArgs,
options
),
redisArgs.preserve
);
return reply;
}
static create<M extends RedisModules, S extends RedisLuaScripts>(options?: RedisClusterOptions<M, S>): RedisClusterType<M, S> {
return new (<any>extendWithModulesAndScripts({
BaseClass: RedisCluster,
modules: options?.modules,
modulesCommandsExecutor: RedisCluster.commandsExecutor,
scripts: options?.scripts,
scriptsExecutor: RedisCluster.#scriptsExecutor
}))(options);
}
readonly #options: RedisClusterOptions;
readonly #slots: RedisClusterSlots<M, S>;
readonly #Multi: new (...args: ConstructorParameters<typeof RedisMultiCommand>) => RedisMultiCommandType<M, S>;
constructor(options: RedisClusterOptions<M, S>) {
this.#options = options;
this.#slots = new RedisClusterSlots(options);
this.#Multi = RedisMultiCommand.extend(options);
}
async connect(): Promise<void> {
return this.#slots.connect();
}
async sendCommand<C extends RedisCommand>(
firstKey: string | undefined,
isReadonly: boolean | undefined,
args: Array<string>,
options?: ClientCommandOptions,
redirections = 0
): Promise<ReturnType<C['transformReply']>> {
const client = this.#slots.getClient(firstKey, isReadonly);
try {
return await client.sendCommand(args, options);
} catch (err: any) {
const shouldRetry = await this.#handleCommandError(err, client, redirections);
if (shouldRetry === true) {
return this.sendCommand(firstKey, isReadonly, args, options, redirections + 1);
} else if (shouldRetry) {
return shouldRetry.sendCommand(args, options);
}
throw err;
}
}
async executeScript(
script: RedisLuaScript,
originalArgs: Array<unknown>,
redisArgs: Array<string>,
options?: ClientCommandOptions,
redirections = 0
): Promise<ReturnType<typeof script['transformReply']>> {
const client = this.#slots.getClient(
RedisCluster.#extractFirstKey(script, originalArgs, redisArgs),
script.IS_READ_ONLY
);
try {
return await client.executeScript(script, redisArgs, options);
} catch (err: any) {
const shouldRetry = await this.#handleCommandError(err, client, redirections);
if (shouldRetry === true) {
return this.executeScript(script, originalArgs, redisArgs, options, redirections + 1);
} else if (shouldRetry) {
return shouldRetry.executeScript(script, redisArgs, options);
}
throw err;
}
}
async #handleCommandError(err: Error, client: RedisClientType<M, S>, redirections: number): Promise<boolean | RedisClientType<M, S>> {
if (redirections > (this.#options.maxCommandRedirections ?? 16)) {
throw err;
}
if (err.message.startsWith('ASK')) {
const url = err.message.substring(err.message.lastIndexOf(' ') + 1);
let node = this.#slots.getNodeByUrl(url);
if (!node) {
await this.#slots.discover(client);
node = this.#slots.getNodeByUrl(url);
if (!node) {
throw new Error(`Cannot find node ${url}`);
}
}
await node.client.asking();
return node.client;
} else if (err.message.startsWith('MOVED')) {
await this.#slots.discover(client);
return true;
}
throw err;
}
multi(routing: string): RedisMultiCommandType<M, S> {
return new this.#Multi(
async (commands: Array<MultiQueuedCommand>, chainId?: symbol) => {
const client = this.#slots.getClient(routing);
return Promise.all(
commands.map(({encodedCommand}) => {
return client.sendEncodedCommand(encodedCommand, RedisClient.commandOptions({
chainId
}));
})
);
},
this.#options
);
}
getMasters(): Array<ClusterNode<M, S>> {
return this.#slots.getMasters();
}
getSlotMaster(slot: number): ClusterNode<M, S> {
return this.#slots.getSlotMaster(slot);
}
disconnect(): Promise<void> {
return this.#slots.disconnect();
}
}
extendWithDefaultCommands(RedisCluster, RedisCluster.commandsExecutor);

14
lib/command-options.ts Normal file
View File

@@ -0,0 +1,14 @@
const symbol = Symbol('Command Options');
export type CommandOptions<T> = T & {
readonly [symbol]: true;
};
export function commandOptions<T>(options: T): CommandOptions<T> {
(options as any)[symbol] = true;
return options as CommandOptions<T>;
}
export function isCommandOptions<T>(options: any): options is CommandOptions<T> {
return options && options[symbol] === true;
}

View File

@@ -1,16 +0,0 @@
'use strict';
var betterStackTraces = /development/i.test(process.env.NODE_ENV) || /\bredis\b/i.test(process.env.NODE_DEBUG);
function Command (command, args, callback, call_on_write) {
this.command = command;
this.args = args;
this.buffer_args = false;
this.callback = callback;
this.call_on_write = call_on_write;
if (betterStackTraces) {
this.error = new Error();
}
}
module.exports = Command;

28
lib/commander.spec.ts Normal file
View File

@@ -0,0 +1,28 @@
import { strict as assert } from 'assert';
import { describe } from 'mocha';
import { encodeCommand } from './commander';
describe('Commander', () => {
describe('encodeCommand (see #1628)', () => {
it('1 byte', () => {
assert.equal(
encodeCommand(['a', 'z']),
'*2\r\n$1\r\na\r\n$1\r\nz\r\n'
);
});
it('2 bytes', () => {
assert.equal(
encodeCommand(['א', 'ת']),
'*2\r\n$2\r\nא\r\n$2\r\nת\r\n'
);
});
it('4 bytes', () => {
assert.equal(
encodeCommand(['🐣', '🐤']),
'*2\r\n$4\r\n🐣\r\n$4\r\n🐤\r\n'
);
});
});
});

109
lib/commander.ts Normal file
View File

@@ -0,0 +1,109 @@
import COMMANDS, { RedisCommand, RedisModules, TransformArgumentsReply } from './commands';
import { RedisLuaScript, RedisLuaScripts } from './lua-script';
import { CommandOptions, isCommandOptions } from './command-options';
type Instantiable<T = any> = new(...args: Array<any>) => T;
type CommandExecutor<T extends Instantiable = Instantiable> = (this: InstanceType<T>, command: RedisCommand, args: Array<unknown>) => unknown;
export function extendWithDefaultCommands<T extends Instantiable>(BaseClass: T, executor: CommandExecutor<T>): void {
for (const [name, command] of Object.entries(COMMANDS)) {
BaseClass.prototype[name] = function (...args: Array<unknown>): unknown {
return executor.call(this, command, args);
};
}
}
interface ExtendWithModulesAndScriptsConfig<
T extends Instantiable,
M extends RedisModules,
S extends RedisLuaScripts
> {
BaseClass: T;
modules: M | undefined;
modulesCommandsExecutor: CommandExecutor<T>;
scripts: S | undefined;
scriptsExecutor(this: InstanceType<T>, script: RedisLuaScript, args: Array<unknown>): unknown;
}
export function extendWithModulesAndScripts<
T extends Instantiable,
M extends RedisModules,
S extends RedisLuaScripts,
>(config: ExtendWithModulesAndScriptsConfig<T, M, S>): T {
let Commander: T | undefined;
if (config.modules) {
Commander = class extends config.BaseClass {
constructor(...args: Array<any>) {
super(...args);
for (const module of Object.keys(config.modules as RedisModules)) {
this[module] = new this[module](this);
}
}
};
for (const [moduleName, module] of Object.entries(config.modules)) {
Commander.prototype[moduleName] = class {
readonly self: T;
constructor(self: InstanceType<T>) {
this.self = self;
}
};
for (const [commandName, command] of Object.entries(module)) {
Commander.prototype[moduleName].prototype[commandName] = function (...args: Array<unknown>): unknown {
return config.modulesCommandsExecutor.call(this.self, command, args);
};
}
}
}
if (config.scripts) {
Commander ??= class extends config.BaseClass {};
for (const [name, script] of Object.entries(config.scripts)) {
Commander.prototype[name] = function (...args: Array<unknown>): unknown {
return config.scriptsExecutor.call(this, script, args);
};
}
}
return (Commander ?? config.BaseClass) as any;
}
export function transformCommandArguments<T = unknown>(
command: RedisCommand,
args: Array<unknown>
): {
args: TransformArgumentsReply;
options: CommandOptions<T> | undefined;
} {
let options;
if (isCommandOptions<T>(args[0])) {
options = args[0];
args = args.slice(1);
}
return {
args: command.transformArguments(...args),
options
};
}
export function encodeCommand(args: Array<string>): string {
const encoded = [
`*${args.length}`,
`$${Buffer.byteLength(args[0])}`,
args[0]
];
for (let i = 1; i < args.length; i++) {
encoded.push(`$${Buffer.byteLength(args[i])}`, args[i]);
}
return encoded.join('\r\n') + '\r\n';
}

333
lib/commands-queue.ts Normal file
View File

@@ -0,0 +1,333 @@
import LinkedList from 'yallist';
import RedisParser from 'redis-parser';
import { AbortError } from './errors';
import { RedisReply } from './commands';
import { encodeCommand } from './commander';
export interface QueueCommandOptions {
asap?: boolean;
signal?: any; // TODO: `AbortSignal` type is incorrect
chainId?: symbol;
}
interface CommandWaitingToBeSent extends CommandWaitingForReply {
encodedCommand: string;
chainId?: symbol;
abort?: {
signal: any; // TODO: `AbortSignal` type is incorrect
listener(): void;
};
}
interface CommandWaitingForReply {
resolve(reply?: any): void;
reject(err: Error): void;
channelsCounter?: number;
}
export type CommandsQueueExecutor = (encodedCommands: string) => boolean | undefined;
export enum PubSubSubscribeCommands {
SUBSCRIBE = 'SUBSCRIBE',
PSUBSCRIBE = 'PSUBSCRIBE'
}
export enum PubSubUnsubscribeCommands {
UNSUBSCRIBE = 'UNSUBSCRIBE',
PUNSUBSCRIBE = 'PUNSUBSCRIBE'
}
export type PubSubListener = (message: string, channel: string) => unknown;
export type PubSubListenersMap = Map<string, Set<PubSubListener>>;
export default class RedisCommandsQueue {
static #flushQueue<T extends CommandWaitingForReply>(queue: LinkedList<T>, err: Error): void {
while (queue.length) {
queue.shift()!.reject(err);
}
}
static #emitPubSubMessage(listeners: Set<PubSubListener>, message: string, channel: string): void {
for (const listener of listeners) {
listener(message, channel);
}
}
readonly #maxLength: number | null | undefined;
readonly #executor: CommandsQueueExecutor;
readonly #waitingToBeSent = new LinkedList<CommandWaitingToBeSent>();
#waitingToBeSentCommandsLength = 0;
get waitingToBeSentCommandsLength() {
return this.#waitingToBeSentCommandsLength;
}
readonly #waitingForReply = new LinkedList<CommandWaitingForReply>();
readonly #pubSubState = {
subscribing: 0,
subscribed: 0,
unsubscribing: 0
};
readonly #pubSubListeners = {
channels: <PubSubListenersMap>new Map(),
patterns: <PubSubListenersMap>new Map()
};
readonly #parser = new RedisParser({
returnReply: (reply: unknown) => {
if ((this.#pubSubState.subscribing || this.#pubSubState.subscribed) && Array.isArray(reply)) {
switch (reply[0]) {
case 'message':
return RedisCommandsQueue.#emitPubSubMessage(
this.#pubSubListeners.channels.get(reply[1])!,
reply[2],
reply[1]
);
case 'pmessage':
return RedisCommandsQueue.#emitPubSubMessage(
this.#pubSubListeners.patterns.get(reply[1])!,
reply[3],
reply[2]
);
case 'subscribe':
case 'psubscribe':
if (--this.#waitingForReply.head!.value.channelsCounter! === 0) {
this.#shiftWaitingForReply().resolve();
}
return;
}
}
this.#shiftWaitingForReply().resolve(reply);
},
returnError: (err: Error) => this.#shiftWaitingForReply().reject(err)
});
#chainInExecution: symbol | undefined;
constructor(maxLength: number | null | undefined, executor: CommandsQueueExecutor) {
this.#maxLength = maxLength;
this.#executor = executor;
}
addEncodedCommand<T = RedisReply>(encodedCommand: string, options?: QueueCommandOptions): Promise<T> {
if (this.#pubSubState.subscribing || this.#pubSubState.subscribed) {
return Promise.reject(new Error('Cannot send commands in PubSub mode'));
} else if (this.#maxLength && this.#waitingToBeSent.length + this.#waitingForReply.length >= this.#maxLength) {
return Promise.reject(new Error('The queue is full'));
} else if (options?.signal?.aborted) {
return Promise.reject(new AbortError());
}
return new Promise((resolve, reject) => {
const node = new LinkedList.Node<CommandWaitingToBeSent>({
encodedCommand,
chainId: options?.chainId,
resolve,
reject
});
if (options?.signal) {
const listener = () => {
this.#waitingToBeSent.removeNode(node);
node.value.reject(new AbortError());
};
node.value.abort = {
signal: options.signal,
listener
};
options.signal.addEventListener('abort', listener, {
once: true
});
}
if (options?.asap) {
this.#waitingToBeSent.unshiftNode(node);
} else {
this.#waitingToBeSent.pushNode(node);
}
this.#waitingToBeSentCommandsLength += encodedCommand.length;
});
}
subscribe(command: PubSubSubscribeCommands, channels: string | Array<string>, listener: PubSubListener): Promise<void> {
const channelsToSubscribe: Array<string> = [],
listeners = command === PubSubSubscribeCommands.SUBSCRIBE ? this.#pubSubListeners.channels : this.#pubSubListeners.patterns;
for (const channel of (Array.isArray(channels) ? channels : [channels])) {
if (listeners.has(channel)) {
listeners.get(channel)!.add(listener);
continue;
}
listeners.set(channel, new Set([listener]));
channelsToSubscribe.push(channel);
}
if (!channelsToSubscribe.length) {
return Promise.resolve();
}
return this.#pushPubSubCommand(command, channelsToSubscribe);
}
unsubscribe(command: PubSubUnsubscribeCommands, channels?: string | Array<string>, listener?: PubSubListener): Promise<void> {
const listeners = command === PubSubUnsubscribeCommands.UNSUBSCRIBE ? this.#pubSubListeners.channels : this.#pubSubListeners.patterns;
if (!channels) {
listeners.clear();
return this.#pushPubSubCommand(command);
}
const channelsToUnsubscribe = [];
for (const channel of (Array.isArray(channels) ? channels : [channels])) {
const set = listeners.get(channel);
if (!set) continue;
let shouldUnsubscribe = !listener;
if (listener) {
set.delete(listener);
shouldUnsubscribe = set.size === 0;
}
if (shouldUnsubscribe) {
channelsToUnsubscribe.push(channel);
listeners.delete(channel);
}
}
if (!channelsToUnsubscribe.length) {
return Promise.resolve();
}
return this.#pushPubSubCommand(command, channelsToUnsubscribe);
}
#pushPubSubCommand(command: PubSubSubscribeCommands | PubSubUnsubscribeCommands, channels?: Array<string>): Promise<void> {
return new Promise((resolve, reject) => {
const isSubscribe = command === PubSubSubscribeCommands.SUBSCRIBE || command === PubSubSubscribeCommands.PSUBSCRIBE,
inProgressKey = isSubscribe ? 'subscribing' : 'unsubscribing',
commandArgs: Array<string> = [command];
let channelsCounter: number;
if (channels?.length) {
commandArgs.push(...channels);
channelsCounter = channels.length;
} else {
// unsubscribe only
channelsCounter = (
command[0] === 'P' ?
this.#pubSubListeners.patterns :
this.#pubSubListeners.channels
).size;
}
this.#pubSubState[inProgressKey] += channelsCounter;
this.#waitingToBeSent.push({
encodedCommand: encodeCommand(commandArgs),
channelsCounter,
resolve: () => {
this.#pubSubState[inProgressKey] -= channelsCounter;
this.#pubSubState.subscribed += channelsCounter * (isSubscribe ? 1 : -1);
resolve();
},
reject: () => {
this.#pubSubState[inProgressKey] -= channelsCounter;
reject();
}
});
});
}
resubscribe(): Promise<any> | undefined {
if (!this.#pubSubState.subscribed && !this.#pubSubState.subscribing) {
return;
}
this.#pubSubState.subscribed = this.#pubSubState.subscribing = 0;
// TODO: acl error on one channel/pattern will reject the whole command
return Promise.all([
this.#pushPubSubCommand(PubSubSubscribeCommands.SUBSCRIBE, [...this.#pubSubListeners.channels.keys()]),
this.#pushPubSubCommand(PubSubSubscribeCommands.PSUBSCRIBE, [...this.#pubSubListeners.patterns.keys()])
]);
}
executeChunk(recommendedSize: number): boolean | undefined {
if (!this.#waitingToBeSent.length) return;
const encoded: Array<string> = [];
let size = 0,
lastCommandChainId: symbol | undefined;
for (const command of this.#waitingToBeSent) {
encoded.push(command.encodedCommand);
size += command.encodedCommand.length;
if (size > recommendedSize) {
lastCommandChainId = command.chainId;
break;
}
}
if (!lastCommandChainId && encoded.length === this.#waitingToBeSent.length) {
lastCommandChainId = this.#waitingToBeSent.tail!.value.chainId;
}
lastCommandChainId ??= this.#waitingToBeSent.tail?.value.chainId;
this.#executor(encoded.join(''));
for (let i = 0; i < encoded.length; i++) {
const waitingToBeSent = this.#waitingToBeSent.shift()!;
if (waitingToBeSent.abort) {
waitingToBeSent.abort.signal.removeEventListener('abort', waitingToBeSent.abort.listener);
}
this.#waitingForReply.push({
resolve: waitingToBeSent.resolve,
reject: waitingToBeSent.reject,
channelsCounter: waitingToBeSent.channelsCounter
});
}
this.#chainInExecution = lastCommandChainId;
this.#waitingToBeSentCommandsLength -= size;
}
parseResponse(data: Buffer): void {
this.#parser.execute(data);
}
#shiftWaitingForReply(): CommandWaitingForReply {
if (!this.#waitingForReply.length) {
throw new Error('Got an unexpected reply from Redis');
}
return this.#waitingForReply.shift()!;
}
flushWaitingForReply(err: Error): void {
RedisCommandsQueue.#flushQueue(this.#waitingForReply, err);
if (!this.#chainInExecution) {
return;
}
while (this.#waitingToBeSent.head?.value.chainId === this.#chainInExecution) {
this.#waitingToBeSent.shift();
}
this.#chainInExecution = undefined;
}
flushAll(err: Error): void {
RedisCommandsQueue.#flushQueue(this.#waitingForReply, err);
RedisCommandsQueue.#flushQueue(this.#waitingToBeSent, err);
}
}

View File

@@ -1,105 +0,0 @@
'use strict';
var commands = require('redis-commands');
var Multi = require('./multi');
var RedisClient = require('../').RedisClient;
var Command = require('./command');
var addCommand = function (command) {
// Some rare Redis commands use special characters in their command name
// Convert those to a underscore to prevent using invalid function names
var commandName = command.replace(/(?:^([0-9])|[^a-zA-Z0-9_$])/g, '_$1');
// Do not override existing functions
if (!RedisClient.prototype[command]) {
RedisClient.prototype[command.toUpperCase()] = RedisClient.prototype[command] = function () {
var arr;
var len = arguments.length;
var callback;
var i = 0;
if (Array.isArray(arguments[0])) {
arr = arguments[0];
if (len === 2) {
callback = arguments[1];
}
} else if (len > 1 && Array.isArray(arguments[1])) {
if (len === 3) {
callback = arguments[2];
}
len = arguments[1].length;
arr = new Array(len + 1);
arr[0] = arguments[0];
for (; i < len; i += 1) {
arr[i + 1] = arguments[1][i];
}
} else {
// The later should not be the average use case
if (len !== 0 && (typeof arguments[len - 1] === 'function' || typeof arguments[len - 1] === 'undefined')) {
len--;
callback = arguments[len];
}
arr = new Array(len);
for (; i < len; i += 1) {
arr[i] = arguments[i];
}
}
return this.internal_send_command(new Command(command, arr, callback));
};
// Alias special function names (e.g. NR.RUN becomes NR_RUN and nr_run)
if (commandName !== command) {
RedisClient.prototype[commandName.toUpperCase()] = RedisClient.prototype[commandName] = RedisClient.prototype[command];
}
Object.defineProperty(RedisClient.prototype[command], 'name', {
value: commandName
});
}
// Do not override existing functions
if (!Multi.prototype[command]) {
Multi.prototype[command.toUpperCase()] = Multi.prototype[command] = function () {
var arr;
var len = arguments.length;
var callback;
var i = 0;
if (Array.isArray(arguments[0])) {
arr = arguments[0];
if (len === 2) {
callback = arguments[1];
}
} else if (len > 1 && Array.isArray(arguments[1])) {
if (len === 3) {
callback = arguments[2];
}
len = arguments[1].length;
arr = new Array(len + 1);
arr[0] = arguments[0];
for (; i < len; i += 1) {
arr[i + 1] = arguments[1][i];
}
} else {
// The later should not be the average use case
if (len !== 0 && (typeof arguments[len - 1] === 'function' || typeof arguments[len - 1] === 'undefined')) {
len--;
callback = arguments[len];
}
arr = new Array(len);
for (; i < len; i += 1) {
arr[i] = arguments[i];
}
}
this.queue.push(new Command(command, arr, callback));
return this;
};
// Alias special function names (e.g. NR.RUN becomes NR_RUN and nr_run)
if (commandName !== command) {
Multi.prototype[commandName.toUpperCase()] = Multi.prototype[commandName] = Multi.prototype[command];
}
Object.defineProperty(Multi.prototype[command], 'name', {
value: commandName
});
}
};
commands.list.forEach(addCommand);
module.exports = addCommand;

View File

@@ -0,0 +1,23 @@
import { strict as assert } from 'assert';
import { describeHandleMinimumRedisVersion } from '../test-utils';
import { transformArguments } from './ACL_CAT';
describe('ACL CAT', () => {
describeHandleMinimumRedisVersion([6]);
describe('transformArguments', () => {
it('simple', () => {
assert.deepEqual(
transformArguments(),
['ACL', 'CAT']
);
});
it('with categoryName', () => {
assert.deepEqual(
transformArguments('dangerous'),
['ACL', 'CAT', 'dangerous']
);
});
});
});

13
lib/commands/ACL_CAT.ts Normal file
View File

@@ -0,0 +1,13 @@
import { transformReplyStringArray } from './generic-transformers';
export function transformArguments(categoryName?: string): Array<string> {
const args = ['ACL', 'CAT'];
if (categoryName) {
args.push(categoryName);
}
return args;
}
export const transformReply = transformReplyStringArray;

View File

@@ -0,0 +1,30 @@
import { strict as assert } from 'assert';
import { describeHandleMinimumRedisVersion, itWithClient, TestRedisServers } from '../test-utils';
import { transformArguments } from './ACL_DELUSER';
describe('ACL DELUSER', () => {
describeHandleMinimumRedisVersion([6]);
describe('transformArguments', () => {
it('string', () => {
assert.deepEqual(
transformArguments('username'),
['ACL', 'DELUSER', 'username']
);
});
it('array', () => {
assert.deepEqual(
transformArguments(['1', '2']),
['ACL', 'DELUSER', '1', '2']
);
});
});
itWithClient(TestRedisServers.OPEN, 'client.aclDelUser', async client => {
assert.equal(
await client.aclDelUser('dosenotexists'),
0
);
});
});

View File

@@ -0,0 +1,7 @@
import { pushVerdictArguments, transformReplyNumber } from './generic-transformers';
export function transformArguments(username: string | Array<string>): Array<string> {
return pushVerdictArguments(['ACL', 'DELUSER'], username);
}
export const transformReply = transformReplyNumber;

View File

@@ -0,0 +1,23 @@
import { strict as assert } from 'assert';
import { describeHandleMinimumRedisVersion } from '../test-utils';
import { transformArguments } from './ACL_GENPASS';
describe('ACL GENPASS', () => {
describeHandleMinimumRedisVersion([6]);
describe('transformArguments', () => {
it('simple', () => {
assert.deepEqual(
transformArguments(),
['ACL', 'GENPASS']
);
});
it('with bits', () => {
assert.deepEqual(
transformArguments(128),
['ACL', 'GENPASS', '128']
);
});
});
});

View File

@@ -0,0 +1,13 @@
import { transformReplyString } from './generic-transformers';
export function transformArguments(bits?: number): Array<string> {
const args = ['ACL', 'GENPASS'];
if (bits) {
args.push(bits.toString());
}
return args;
}
export const transformReply = transformReplyString;

View File

@@ -0,0 +1,27 @@
import { strict as assert } from 'assert';
import { describeHandleMinimumRedisVersion, itWithClient, TestRedisServers } from '../test-utils';
import { transformArguments } from './ACL_GETUSER';
describe('ACL GETUSER', () => {
describeHandleMinimumRedisVersion([6]);
it('transformArguments', () => {
assert.deepEqual(
transformArguments('username'),
['ACL', 'GETUSER', 'username']
);
});
itWithClient(TestRedisServers.OPEN, 'client.aclGetUser', async client => {
assert.deepEqual(
await client.aclGetUser('default'),
{
flags: ['on', 'allkeys', 'allchannels', 'allcommands', 'nopass'],
passwords: [],
commands: '+@all',
keys: ['*'],
channels: ['*']
}
);
});
});

View File

@@ -0,0 +1,34 @@
export function transformArguments(username: string): Array<string> {
return ['ACL', 'GETUSER', username];
}
type AclGetUserRawReply = [
_: string,
flags: Array<string>,
_: string,
passwords: Array<string>,
_: string,
commands: string,
_: string,
keys: Array<string>,
_: string,
channels: Array<string>
];
interface AclUser {
flags: Array<string>;
passwords: Array<string>;
commands: string;
keys: Array<string>;
channels: Array<string>
}
export function transformReply(reply: AclGetUserRawReply): AclUser {
return {
flags: reply[1],
passwords: reply[3],
commands: reply[5],
keys: reply[7],
channels: reply[9]
};
}

View File

@@ -0,0 +1,14 @@
import { strict as assert } from 'assert';
import { describeHandleMinimumRedisVersion } from '../test-utils';
import { transformArguments } from './ACL_LIST';
describe('ACL LIST', () => {
describeHandleMinimumRedisVersion([6]);
it('transformArguments', () => {
assert.deepEqual(
transformArguments(),
['ACL', 'LIST']
);
});
});

7
lib/commands/ACL_LIST.ts Normal file
View File

@@ -0,0 +1,7 @@
import { transformReplyStringArray } from './generic-transformers';
export function transformArguments(): Array<string> {
return ['ACL', 'LIST'];
}
export const transformReply = transformReplyStringArray;

View File

@@ -0,0 +1,14 @@
import { strict as assert } from 'assert';
import { describeHandleMinimumRedisVersion } from '../test-utils';
import { transformArguments } from './ACL_SAVE';
describe('ACL SAVE', () => {
describeHandleMinimumRedisVersion([6]);
it('transformArguments', () => {
assert.deepEqual(
transformArguments(),
['ACL', 'SAVE']
);
});
});

7
lib/commands/ACL_LOAD.ts Normal file
View File

@@ -0,0 +1,7 @@
import { transformReplyString } from './generic-transformers';
export function transformArguments(): Array<string> {
return ['ACL', 'LOAD'];
}
export const transformReply = transformReplyString;

View File

@@ -0,0 +1,53 @@
import { strict as assert } from 'assert';
import { describeHandleMinimumRedisVersion } from '../test-utils';
import { transformArguments, transformReply } from './ACL_LOG';
describe('ACL LOG', () => {
describeHandleMinimumRedisVersion([6]);
describe('transformArguments', () => {
it('simple', () => {
assert.deepEqual(
transformArguments(),
['ACL', 'LOG']
);
});
it('with count', () => {
assert.deepEqual(
transformArguments(10),
['ACL', 'LOG', '10']
);
});
});
it('transformReply', () => {
assert.deepEqual(
transformReply([[
'count',
1,
'reason',
'auth',
'context',
'toplevel',
'object',
'AUTH',
'username',
'someuser',
'age-seconds',
'4.096',
'client-info',
'id=6 addr=127.0.0.1:63026 fd=8 name= age=9 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=48 qbuf-free=32720 obl=0 oll=0 omem=0 events=r cmd=auth user=default'
]]),
[{
count: 1,
reason: 'auth',
context: 'toplevel',
object: 'AUTH',
username: 'someuser',
ageSeconds: 4.096,
clientInfo: 'id=6 addr=127.0.0.1:63026 fd=8 name= age=9 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=48 qbuf-free=32720 obl=0 oll=0 omem=0 events=r cmd=auth user=default'
}]
);
});
});

48
lib/commands/ACL_LOG.ts Normal file
View File

@@ -0,0 +1,48 @@
export function transformArguments(count?: number): Array<string> {
const args = ['ACL', 'LOG'];
if (count) {
args.push(count.toString());
}
return args;
}
type AclLogRawReply = [
_: string,
count: number,
_: string,
reason: string,
_: string,
context: string,
_: string,
object: string,
_: string,
username: string,
_: string,
ageSeconds: string,
_: string,
clientInfo: string
];
interface AclLog {
count: number;
reason: string;
context: string;
object: string;
username: string;
ageSeconds: number;
clientInfo: string;
}
export function transformReply(reply: Array<AclLogRawReply>): Array<AclLog> {
return reply.map(log => ({
count: log[1],
reason: log[3],
context: log[5],
object: log[7],
username: log[9],
ageSeconds: Number(log[11]),
clientInfo: log[13]
}));
}

View File

@@ -0,0 +1,14 @@
import { strict as assert } from 'assert';
import { describeHandleMinimumRedisVersion } from '../test-utils';
import { transformArguments } from './ACL_LOG_RESET';
describe('ACL LOG RESET', () => {
describeHandleMinimumRedisVersion([6]);
it('transformArguments', () => {
assert.deepEqual(
transformArguments(),
['ACL', 'LOG', 'RESET']
);
});
});

View File

@@ -0,0 +1,7 @@
import { transformReplyString } from './generic-transformers';
export function transformArguments(): Array<string> {
return ['ACL', 'LOG', 'RESET'];
}
export const transformReply = transformReplyString;

View File

@@ -0,0 +1,14 @@
import { strict as assert } from 'assert';
import { describeHandleMinimumRedisVersion } from '../test-utils';
import { transformArguments } from './ACL_LOAD';
describe('ACL LOAD', () => {
describeHandleMinimumRedisVersion([6]);
it('transformArguments', () => {
assert.deepEqual(
transformArguments(),
['ACL', 'LOAD']
);
});
});

7
lib/commands/ACL_SAVE.ts Normal file
View File

@@ -0,0 +1,7 @@
import { transformReplyString } from './generic-transformers';
export function transformArguments(): Array<string> {
return ['ACL', 'SAVE'];
}
export const transformReply = transformReplyString;

View File

@@ -0,0 +1,23 @@
import { strict as assert } from 'assert';
import { describeHandleMinimumRedisVersion } from '../test-utils';
import { transformArguments } from './ACL_SETUSER';
describe('ACL SETUSER', () => {
describeHandleMinimumRedisVersion([6]);
describe('transformArguments', () => {
it('string', () => {
assert.deepEqual(
transformArguments('username', 'allkeys'),
['ACL', 'SETUSER', 'username', 'allkeys']
);
});
it('array', () => {
assert.deepEqual(
transformArguments('username', ['allkeys', 'allchannels']),
['ACL', 'SETUSER', 'username', 'allkeys', 'allchannels']
);
});
});
});

View File

@@ -0,0 +1,7 @@
import { pushVerdictArguments, transformReplyString } from './generic-transformers';
export function transformArguments(username: string, rule: string | Array<string>): Array<string> {
return pushVerdictArguments(['ACL', 'SETUSER', username], rule);
}
export const transformReply = transformReplyString;

View File

@@ -0,0 +1,14 @@
import { strict as assert } from 'assert';
import { describeHandleMinimumRedisVersion } from '../test-utils';
import { transformArguments } from './ACL_USERS';
describe('ACL USERS', () => {
describeHandleMinimumRedisVersion([6]);
it('transformArguments', () => {
assert.deepEqual(
transformArguments(),
['ACL', 'USERS']
);
});
});

View File

@@ -0,0 +1,7 @@
import { transformReplyStringArray } from './generic-transformers';
export function transformArguments(): Array<string> {
return ['ACL', 'USERS'];
}
export const transformReply = transformReplyStringArray;

View File

@@ -0,0 +1,14 @@
import { strict as assert } from 'assert';
import { describeHandleMinimumRedisVersion } from '../test-utils';
import { transformArguments } from './ACL_WHOAMI';
describe('ACL WHOAMI', () => {
describeHandleMinimumRedisVersion([6]);
it('transformArguments', () => {
assert.deepEqual(
transformArguments(),
['ACL', 'WHOAMI']
);
});
});

View File

@@ -0,0 +1,7 @@
import { transformReplyString } from './generic-transformers';
export function transformArguments(): Array<string> {
return ['ACL', 'WHOAMI'];
}
export const transformReply = transformReplyString;

View File

@@ -0,0 +1,11 @@
import { strict as assert } from 'assert';
import { transformArguments } from './APPEND';
describe('AUTH', () => {
it('transformArguments', () => {
assert.deepEqual(
transformArguments('key', 'value'),
['APPEND', 'key', 'value']
);
});
});

9
lib/commands/APPEND.ts Normal file
View File

@@ -0,0 +1,9 @@
import { transformReplyString } from './generic-transformers';
export const FIRST_KEY_INDEX = 1;
export function transformArguments(key: string, value: string): Array<string> {
return ['APPEND', key, value];
}
export const transformReply = transformReplyString;

View File

@@ -0,0 +1,11 @@
import { strict as assert } from 'assert';
import { transformArguments } from './ASKING';
describe('ASKING', () => {
it('transformArguments', () => {
assert.deepEqual(
transformArguments(),
['ASKING']
);
});
});

7
lib/commands/ASKING.ts Normal file
View File

@@ -0,0 +1,7 @@
import { transformReplyString } from './generic-transformers';
export function transformArguments(): Array<string> {
return ['ASKING'];
}
export const transformReply = transformReplyString;

25
lib/commands/AUTH.spec.ts Normal file
View File

@@ -0,0 +1,25 @@
import { strict as assert } from 'assert';
import { transformArguments } from './AUTH';
describe('AUTH', () => {
describe('transformArguments', () => {
it('password only', () => {
assert.deepEqual(
transformArguments({
password: 'password'
}),
['AUTH', 'password']
);
});
it('username & password', () => {
assert.deepEqual(
transformArguments({
username: 'username',
password: 'password'
}),
['AUTH', 'username', 'password']
);
});
});
});

16
lib/commands/AUTH.ts Normal file
View File

@@ -0,0 +1,16 @@
import { transformReplyString } from './generic-transformers';
export interface AuthOptions {
username?: string;
password: string;
}
export function transformArguments({username, password}: AuthOptions): Array<string> {
if (!username) {
return ['AUTH', password];
}
return ['AUTH', username, password];
}
export const transformReply = transformReplyString;

View File

@@ -0,0 +1,11 @@
import { strict as assert } from 'assert';
import { transformArguments } from './BGREWRITEAOF';
describe('BGREWRITEAOF', () => {
it('transformArguments', () => {
assert.deepEqual(
transformArguments(),
['BGREWRITEAOF']
);
});
});

View File

@@ -0,0 +1,7 @@
import { transformReplyString } from './generic-transformers';
export function transformArguments(): Array<string> {
return ['BGREWRITEAOF'];
}
export const transformReply = transformReplyString;

View File

@@ -0,0 +1,23 @@
import { strict as assert } from 'assert';
import { describe } from 'mocha';
import { transformArguments } from './BGSAVE';
describe('BGSAVE', () => {
describe('transformArguments', () => {
it('simple', () => {
assert.deepEqual(
transformArguments(),
['BGSAVE']
);
});
it('with SCHEDULE', () => {
assert.deepEqual(
transformArguments({
SCHEDULE: true
}),
['BGSAVE', 'SCHEDULE']
);
});
});
});

17
lib/commands/BGSAVE.ts Normal file
View File

@@ -0,0 +1,17 @@
import { transformReplyString } from './generic-transformers';
interface BgSaveOptions {
SCHEDULE?: true;
}
export function transformArguments(options?: BgSaveOptions): Array<string> {
const args = ['BGSAVE'];
if (options?.SCHEDULE) {
args.push('SCHEDULE');
}
return args;
}
export const transformReply = transformReplyString;

View File

@@ -0,0 +1,31 @@
import { strict as assert } from 'assert';
import { TestRedisServers, itWithClient } from '../test-utils';
import { transformArguments } from './BITCOUNT';
describe('BITCOUNT', () => {
describe('transformArguments', () => {
it('simple', () => {
assert.deepEqual(
transformArguments('key'),
['BITCOUNT', 'key']
);
});
it('with range', () => {
assert.deepEqual(
transformArguments('key', {
start: 0,
end: 1
}),
['BITCOUNT', 'key', '0', '1']
);
});
});
itWithClient(TestRedisServers.OPEN, 'client.bitCount', async client => {
assert.equal(
await client.bitCount('key'),
0
);
});
});

25
lib/commands/BITCOUNT.ts Normal file
View File

@@ -0,0 +1,25 @@
import { transformReplyNumber } from './generic-transformers';
export const FIRST_KEY_INDEX = 1;
export const IS_READ_ONLY = true;
interface BitCountRange {
start: number;
end: number;
}
export function transformArguments(key: string, range?: BitCountRange): Array<string> {
const args = ['BITCOUNT', key];
if (range) {
args.push(
range.start.toString(),
range.end.toString()
);
}
return args;
}
export const transformReply = transformReplyNumber;

View File

@@ -0,0 +1,42 @@
import { strict as assert } from 'assert';
import { TestRedisServers, itWithClient } from '../test-utils';
import { transformArguments } from './BITFIELD';
describe('BITFIELD', () => {
it('transformArguments', () => {
assert.deepEqual(
transformArguments('key', [{
operation: 'OVERFLOW',
behavior: 'WRAP'
}, {
operation: 'GET',
type: 'i8',
offset: 0
}, {
operation: 'OVERFLOW',
behavior: 'SAT'
}, {
operation: 'SET',
type: 'i16',
offset: 1,
value: 0
}, {
operation: 'OVERFLOW',
behavior: 'FAIL'
}, {
operation: 'INCRBY',
type: 'i32',
offset: 2,
increment: 1
}]),
['BITFIELD', 'key', 'OVERFLOW', 'WRAP', 'GET', 'i8', '0', 'OVERFLOW', 'SAT', 'SET', 'i16', '1', '0', 'OVERFLOW', 'FAIL', 'INCRBY', 'i32', '2', '1']
);
});
itWithClient(TestRedisServers.OPEN, 'client.bitField', async client => {
assert.deepEqual(
await client.bitField('key', []),
[]
);
});
});

Some files were not shown because too many files have changed in this diff Show More