You've already forked node-redis
mirror of
https://github.com/redis/node-redis.git
synced 2025-08-06 02:15:48 +03:00
V4 (#1624)
* 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 commit61edd4f1b5
* 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 commit53de279afe
* 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 commitfaab94fab2
* 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 from0feb35a1fb
* 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 fore7bf09644b
* 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 forf1bf0beebf
- 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:
@@ -1,9 +0,0 @@
|
||||
version = 1
|
||||
exclude_patterns = ["examples/**"]
|
||||
|
||||
[[analyzers]]
|
||||
name = "javascript"
|
||||
enabled = true
|
||||
|
||||
[analyzers.meta]
|
||||
environment = ["nodejs"]
|
@@ -1,4 +0,0 @@
|
||||
node_modules/**
|
||||
coverage/**
|
||||
**.md
|
||||
**.log
|
109
.eslintrc
109
.eslintrc
@@ -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
1
.github/FUNDING.yml
vendored
@@ -1 +0,0 @@
|
||||
open_collective: node-redis
|
19
.github/ISSUE_TEMPLATE.md
vendored
19
.github/ISSUE_TEMPLATE.md
vendored
@@ -5,30 +5,17 @@ labels: needs-triage
|
||||
|
||||
### 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
|
||||
|
||||
|
||||
---
|
||||
|
||||
### Environment
|
||||
|
||||
<!-- e.g. "node --version" -->
|
||||
- **Node.js Version**: `VERSION_HERE`
|
||||
|
||||
|
||||
<!-- e.g. "redis-server --version" -->
|
||||
- **Redis Version**: `VERSION_HERE`
|
||||
|
||||
- **Redis Server Version**: `VERSION_HERE`
|
||||
|
||||
<!-- e.g. Windows 10, Mac OSX 10.15.2 -->
|
||||
- **Platform**: `PLATFORM_HERE`
|
||||
|
7
.github/PULL_REQUEST_TEMPLATE.md
vendored
7
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -1,13 +1,10 @@
|
||||
<!-- please ensure you have read the `./CONTRIBUTING.md` guide -->
|
||||
|
||||
### Description
|
||||
|
||||
<!-- 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. -->
|
||||
|
||||
> Description your pull request here
|
||||
|
||||
> Describe your pull request here
|
||||
|
||||
---
|
||||
|
||||
|
37
.github/workflows/benchmark.yml
vendored
37
.github/workflows/benchmark.yml
vendored
@@ -1,6 +1,9 @@
|
||||
name: Benchmarking
|
||||
name: Benchmark
|
||||
|
||||
on: [pull_request]
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- v4
|
||||
|
||||
jobs:
|
||||
benchmark:
|
||||
@@ -9,8 +12,8 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
node-version: [10.x, 12.x, 14.x, 15.x]
|
||||
redis-version: [5.x, 6.x]
|
||||
node-version: [16.x]
|
||||
redis-version: [6.x]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2.3.4
|
||||
@@ -18,21 +21,25 @@ jobs:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v2.1.5
|
||||
uses: actions/setup-node@v2.3.0
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
|
||||
- name: Setup Redis
|
||||
uses: shogo82148/actions-setup-redis@v1.9.7
|
||||
uses: shogo82148/actions-setup-redis@v1.12.0
|
||||
with:
|
||||
redis-version: ${{ matrix.redis-version }}
|
||||
auto-start: "true"
|
||||
|
||||
- run: npm i --no-audit --prefer-offline
|
||||
- name: Run Benchmark
|
||||
run: npm run benchmark > benchmark-output.txt && cat benchmark-output.txt
|
||||
- name: Upload Benchmark Result
|
||||
uses: actions/upload-artifact@v2.2.2
|
||||
with:
|
||||
name: benchmark-output.txt
|
||||
path: benchmark-output.txt
|
||||
- name: Install Packages
|
||||
run: npm ci
|
||||
|
||||
- name: Build
|
||||
run: npm run build
|
||||
|
||||
- name: Install Benchmark Packages
|
||||
run: npm ci
|
||||
working-directory: ./benchmark
|
||||
|
||||
- name: Benchmark
|
||||
run: npm run start
|
||||
working-directory: ./benchmark
|
||||
|
67
.github/workflows/codeql-analysis.yml
vendored
67
.github/workflows/codeql-analysis.yml
vendored
@@ -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
31
.github/workflows/documentation.yml
vendored
Normal 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 }}
|
31
.github/workflows/linting.yml
vendored
31
.github/workflows/linting.yml
vendored
@@ -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
|
51
.github/workflows/tests.yml
vendored
51
.github/workflows/tests.yml
vendored
@@ -1,16 +1,18 @@
|
||||
name: Tests
|
||||
|
||||
on: [push]
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- v4
|
||||
|
||||
jobs:
|
||||
testing:
|
||||
name: Test
|
||||
tests:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
node-version: [10.x, 12.x, 14.x, 15.x]
|
||||
redis-version: [4.x, 5.x, 6.x]
|
||||
node-version: [12.x, 14.x, 16.x]
|
||||
redis-version: [5.x, 6.x]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2.3.4
|
||||
@@ -18,35 +20,38 @@ jobs:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v2.1.5
|
||||
uses: actions/setup-node@v2.3.0
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
|
||||
- name: Setup Redis
|
||||
uses: shogo82148/actions-setup-redis@v1.9.7
|
||||
uses: shogo82148/actions-setup-redis@v1.12.0
|
||||
with:
|
||||
redis-version: ${{ matrix.redis-version }}
|
||||
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
|
||||
run: npm i --no-audit --prefer-offline
|
||||
run: npm ci
|
||||
|
||||
- name: Run Tests
|
||||
run: npm test
|
||||
run: npm run test
|
||||
|
||||
- name: Submit Coverage
|
||||
run: npm run coveralls
|
||||
env:
|
||||
COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_TOKEN }}
|
||||
- name: Generate lcov
|
||||
run: ./node_modules/.bin/nyc report -r lcov
|
||||
|
||||
- name: Upload Coverage Report
|
||||
uses: actions/upload-artifact@v2.2.2
|
||||
- name: Coveralls
|
||||
uses: coverallsapp/github-action@1.1.3
|
||||
with:
|
||||
name: coverage
|
||||
path: coverage
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
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
|
||||
|
49
.github/workflows/tests_windows.yml
vendored
49
.github/workflows/tests_windows.yml
vendored
@@ -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
25
.gitignore
vendored
@@ -1,17 +1,8 @@
|
||||
node_modules
|
||||
.tern-port
|
||||
.nyc_output
|
||||
coverage
|
||||
*.log
|
||||
*.rdb
|
||||
stunnel.conf
|
||||
stunnel.pid
|
||||
*.out
|
||||
package-lock.json
|
||||
|
||||
# IntelliJ IDEs
|
||||
.idea
|
||||
# VisualStudioCode IDEs
|
||||
.vscode
|
||||
.vs
|
||||
eslint-report.json
|
||||
.vscode/
|
||||
.idea/
|
||||
node_modules/
|
||||
dist/
|
||||
.nyc_output/
|
||||
coverage/
|
||||
dump.rdb
|
||||
documentation/
|
||||
|
36
.npmignore
36
.npmignore
@@ -1,23 +1,17 @@
|
||||
examples/
|
||||
benchmarks/
|
||||
test/
|
||||
.nyc_output/
|
||||
.vscode/
|
||||
.idea/
|
||||
node_modules/
|
||||
.nyc_output
|
||||
coverage/
|
||||
.github/
|
||||
.eslintignore
|
||||
.eslintrc
|
||||
.tern-port
|
||||
*.log
|
||||
*.rdb
|
||||
*.out
|
||||
*.yml
|
||||
.vscode
|
||||
.idea
|
||||
dump.rdb
|
||||
documentation/
|
||||
CONTRIBUTING.md
|
||||
CODE_OF_CONDUCT.md
|
||||
.travis.yml
|
||||
appveyor.yml
|
||||
package-lock.json
|
||||
.prettierrc
|
||||
eslint-report.json
|
||||
.deepsource.toml
|
||||
tsconfig.json
|
||||
.nycrc.json
|
||||
benchmark/
|
||||
.github/
|
||||
scripts/
|
||||
lib/
|
||||
index.ts
|
||||
*.spec.*
|
||||
dist/lib/test-utils.*
|
||||
|
4
.nycrc.json
Normal file
4
.nycrc.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "@istanbuljs/nyc-config-typescript",
|
||||
"exclude": ["**/*.spec.ts", "lib/test-utils.ts"]
|
||||
}
|
11
.prettierrc
11
.prettierrc
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"arrowParens": "avoid",
|
||||
"trailingComma": "all",
|
||||
"useTabs": false,
|
||||
"semi": true,
|
||||
"singleQuote": false,
|
||||
"bracketSpacing": true,
|
||||
"jsxBracketSameLine": false,
|
||||
"tabWidth": 2,
|
||||
"printWidth": 100
|
||||
}
|
23
CHANGELOG.md
23
CHANGELOG.md
@@ -1,5 +1,28 @@
|
||||
# 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
|
||||
|
||||
This version is mainly a release to distribute all the unreleased changes on master since 2017 and additionally removes
|
||||
|
@@ -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/
|
@@ -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! 😊
|
||||
|
||||
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)
|
||||
- **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.
|
||||
- **Donations**: we welcome financial contributions in full transparency on our [open collective](https://opencollective.com/node-redis).
|
||||
- **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
|
||||
- **Code**: take a look at the open issues and help triage them
|
||||
|
||||
---
|
||||
|
||||
## Project Guidelines
|
||||
|
||||
As maintainers of this project, we want to ensure that the project lives and continues to grow. Not blocked by any
|
||||
singular person's time.
|
||||
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.
|
||||
|
||||
One of the simplest ways of doing this is by encouraging a larger set of shallow contributors. Through this we hope to
|
||||
mitigate the problems of a project that needs updates but there is no-one who has the power to do so.
|
||||
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.
|
||||
|
||||
### 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
|
||||
project's direction can be difficult.
|
||||
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.
|
||||
|
||||
We try to help contributors cross that barrier by offering good first step issues (labelled `good-first-issue`). These
|
||||
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.
|
||||
### Expectations of Contributors
|
||||
|
||||
Additionally issues labelled `needs-triage` or `help-wanted` can also be picked up, 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 users incorrect
|
||||
setup of the library or project.
|
||||
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; we’re here to get things done in an enjoyable way. :trophy:
|
||||
|
||||
We aim to keep all project discussion inside GitHub issues. This is to make sure valuable discussion is accessible via
|
||||
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.
|
||||
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.
|
||||
|
||||
### 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
|
||||
the ship shipping and to take some of the load from others. It is non-obligatory; we’re here to get things done in an
|
||||
enjoyable way. :trophy:
|
||||
#### Special Thanks
|
||||
|
||||
We only ask that you follow the conduct guidelines set out in our [Code of Conduct](/CODE_OF_CONDUCT.md) throughout your
|
||||
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.
|
||||
A huge thank you to the original author of Node Redis, [Matthew Ranney](https://github.com/mranney).
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
|
||||
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
|
||||
testing and open the generated `./coverage/index.html` in your browser.
|
||||
To run the tests, run `npm install` to install dependencies, then run `npm test`.
|
||||
|
||||
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)
|
||||
- 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
|
||||
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.
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
### 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
|
||||
to be merged.
|
||||
Pull Requests to the protected branches require peer-review approvals and passing status checks to be able 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?
|
||||
- Is the testing coverage ok and not worse than previously?
|
||||
- Is the test coverage at the same level as before (preferably more!)?
|
||||
|
2
LICENSE
2
LICENSE
@@ -1,4 +1,4 @@
|
||||
MIT License
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2016-present Node Redis contributors.
|
||||
|
||||
|
1
benchmark/.gitignore
vendored
Normal file
1
benchmark/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
node_modules
|
81
benchmark/index.js
Normal file
81
benchmark/index.js
Normal 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
926
benchmark/package-lock.json
generated
Normal 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
17
benchmark/package.json
Normal 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:../"
|
||||
}
|
||||
}
|
@@ -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));
|
@@ -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
13
docs/FAQ.md
Normal 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.
|
30
docs/client-configuration.md
Normal file
30
docs/client-configuration.md
Normal 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.
|
67
docs/isolated-execution.md
Normal file
67
docs/isolated-execution.md
Normal 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
35
docs/v3-to-v4.md
Normal 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.
|
@@ -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'
|
||||
});
|
@@ -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();
|
@@ -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);
|
||||
});
|
@@ -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();
|
||||
});
|
@@ -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();
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
@@ -1,7 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
var client = require('redis').createClient();
|
||||
|
||||
client.mget(['sessions started', 'sessions started', 'foo'], function (err, res) {
|
||||
console.dir(res);
|
||||
});
|
@@ -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));
|
||||
});
|
@@ -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();
|
||||
});
|
@@ -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());
|
||||
});
|
@@ -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*');
|
@@ -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
|
||||
});
|
@@ -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();
|
@@ -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.');
|
||||
});
|
@@ -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
|
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
});
|
@@ -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();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
@@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
@@ -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);
|
@@ -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);
|
10
index.ts
Normal file
10
index.ts
Normal 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
562
lib/client.spec.ts
Normal 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
468
lib/client.ts
Normal 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
221
lib/cluster-slots.ts
Normal 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
115
lib/cluster.spec.ts
Normal 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
202
lib/cluster.ts
Normal 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
14
lib/command-options.ts
Normal 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;
|
||||
}
|
@@ -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
28
lib/commander.spec.ts
Normal 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
109
lib/commander.ts
Normal 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
333
lib/commands-queue.ts
Normal 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);
|
||||
}
|
||||
}
|
105
lib/commands.js
105
lib/commands.js
@@ -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;
|
23
lib/commands/ACL_CAT.spec.ts
Normal file
23
lib/commands/ACL_CAT.spec.ts
Normal 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
13
lib/commands/ACL_CAT.ts
Normal 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;
|
30
lib/commands/ACL_DELUSER.spec.ts
Normal file
30
lib/commands/ACL_DELUSER.spec.ts
Normal 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
|
||||
);
|
||||
});
|
||||
});
|
7
lib/commands/ACL_DELUSER.ts
Normal file
7
lib/commands/ACL_DELUSER.ts
Normal 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;
|
23
lib/commands/ACL_GENPASS.spec.ts
Normal file
23
lib/commands/ACL_GENPASS.spec.ts
Normal 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']
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
13
lib/commands/ACL_GENPASS.ts
Normal file
13
lib/commands/ACL_GENPASS.ts
Normal 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;
|
27
lib/commands/ACL_GETUSER.spec.ts
Normal file
27
lib/commands/ACL_GETUSER.spec.ts
Normal 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: ['*']
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
34
lib/commands/ACL_GETUSER.ts
Normal file
34
lib/commands/ACL_GETUSER.ts
Normal 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]
|
||||
};
|
||||
}
|
14
lib/commands/ACL_LIST.spec.ts
Normal file
14
lib/commands/ACL_LIST.spec.ts
Normal 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
7
lib/commands/ACL_LIST.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { transformReplyStringArray } from './generic-transformers';
|
||||
|
||||
export function transformArguments(): Array<string> {
|
||||
return ['ACL', 'LIST'];
|
||||
}
|
||||
|
||||
export const transformReply = transformReplyStringArray;
|
14
lib/commands/ACL_LOAD.spec.ts
Normal file
14
lib/commands/ACL_LOAD.spec.ts
Normal 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
7
lib/commands/ACL_LOAD.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { transformReplyString } from './generic-transformers';
|
||||
|
||||
export function transformArguments(): Array<string> {
|
||||
return ['ACL', 'LOAD'];
|
||||
}
|
||||
|
||||
export const transformReply = transformReplyString;
|
53
lib/commands/ACL_LOG.spec.ts
Normal file
53
lib/commands/ACL_LOG.spec.ts
Normal 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
48
lib/commands/ACL_LOG.ts
Normal 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]
|
||||
}));
|
||||
}
|
14
lib/commands/ACL_LOG_RESET.spec.ts
Normal file
14
lib/commands/ACL_LOG_RESET.spec.ts
Normal 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']
|
||||
);
|
||||
});
|
||||
});
|
7
lib/commands/ACL_LOG_RESET.ts
Normal file
7
lib/commands/ACL_LOG_RESET.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { transformReplyString } from './generic-transformers';
|
||||
|
||||
export function transformArguments(): Array<string> {
|
||||
return ['ACL', 'LOG', 'RESET'];
|
||||
}
|
||||
|
||||
export const transformReply = transformReplyString;
|
14
lib/commands/ACL_SAVE.spec.ts
Normal file
14
lib/commands/ACL_SAVE.spec.ts
Normal 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
7
lib/commands/ACL_SAVE.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { transformReplyString } from './generic-transformers';
|
||||
|
||||
export function transformArguments(): Array<string> {
|
||||
return ['ACL', 'SAVE'];
|
||||
}
|
||||
|
||||
export const transformReply = transformReplyString;
|
23
lib/commands/ACL_SETUSER.spec.ts
Normal file
23
lib/commands/ACL_SETUSER.spec.ts
Normal 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']
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
7
lib/commands/ACL_SETUSER.ts
Normal file
7
lib/commands/ACL_SETUSER.ts
Normal 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;
|
14
lib/commands/ACL_USERS.spec.ts
Normal file
14
lib/commands/ACL_USERS.spec.ts
Normal 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']
|
||||
);
|
||||
});
|
||||
});
|
7
lib/commands/ACL_USERS.ts
Normal file
7
lib/commands/ACL_USERS.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { transformReplyStringArray } from './generic-transformers';
|
||||
|
||||
export function transformArguments(): Array<string> {
|
||||
return ['ACL', 'USERS'];
|
||||
}
|
||||
|
||||
export const transformReply = transformReplyStringArray;
|
14
lib/commands/ACL_WHOAMI.spec.ts
Normal file
14
lib/commands/ACL_WHOAMI.spec.ts
Normal 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']
|
||||
);
|
||||
});
|
||||
});
|
7
lib/commands/ACL_WHOAMI.ts
Normal file
7
lib/commands/ACL_WHOAMI.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { transformReplyString } from './generic-transformers';
|
||||
|
||||
export function transformArguments(): Array<string> {
|
||||
return ['ACL', 'WHOAMI'];
|
||||
}
|
||||
|
||||
export const transformReply = transformReplyString;
|
11
lib/commands/APPEND.spec.ts
Normal file
11
lib/commands/APPEND.spec.ts
Normal 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
9
lib/commands/APPEND.ts
Normal 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;
|
11
lib/commands/ASKING.spec.ts
Normal file
11
lib/commands/ASKING.spec.ts
Normal 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
7
lib/commands/ASKING.ts
Normal 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
25
lib/commands/AUTH.spec.ts
Normal 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
16
lib/commands/AUTH.ts
Normal 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;
|
11
lib/commands/BGREWRITEAOF.spec.ts
Normal file
11
lib/commands/BGREWRITEAOF.spec.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { strict as assert } from 'assert';
|
||||
import { transformArguments } from './BGREWRITEAOF';
|
||||
|
||||
describe('BGREWRITEAOF', () => {
|
||||
it('transformArguments', () => {
|
||||
assert.deepEqual(
|
||||
transformArguments(),
|
||||
['BGREWRITEAOF']
|
||||
);
|
||||
});
|
||||
});
|
7
lib/commands/BGREWRITEAOF.ts
Normal file
7
lib/commands/BGREWRITEAOF.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { transformReplyString } from './generic-transformers';
|
||||
|
||||
export function transformArguments(): Array<string> {
|
||||
return ['BGREWRITEAOF'];
|
||||
}
|
||||
|
||||
export const transformReply = transformReplyString;
|
23
lib/commands/BGSAVE.spec.ts
Normal file
23
lib/commands/BGSAVE.spec.ts
Normal 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
17
lib/commands/BGSAVE.ts
Normal 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;
|
31
lib/commands/BITCOUNT.spec.ts
Normal file
31
lib/commands/BITCOUNT.spec.ts
Normal 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
25
lib/commands/BITCOUNT.ts
Normal 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;
|
42
lib/commands/BITFIELD.spec.ts
Normal file
42
lib/commands/BITFIELD.spec.ts
Normal 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
Reference in New Issue
Block a user