├── .eslintrc.json ├── .github ├── CONTRIBUTING.md ├── FUNDING.yml ├── stale.yml └── workflows │ └── test.yml ├── .gitignore ├── .npmrc ├── .prettierignore ├── .releaserc.json ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── benchmarks ├── autopipelining-cluster.ts ├── autopipelining-single.ts ├── dropBuffer.ts ├── errorStack.ts └── fixtures │ ├── generate.ts │ └── insert.ts ├── bin ├── argumentTypes.js ├── index.js ├── overrides.js ├── returnTypes.js ├── sortArguments.js ├── template.ts └── typeMaps.js ├── docs ├── .nojekyll ├── assets │ ├── highlight.css │ ├── icons.css │ ├── icons.png │ ├── icons@2x.png │ ├── main.js │ ├── search.js │ ├── style.css │ ├── widgets.png │ └── widgets@2x.png ├── classes │ ├── Cluster.html │ └── Redis.html ├── index.html └── interfaces │ ├── ChainableCommander.html │ ├── ClusterOptions.html │ ├── CommonRedisOptions.html │ ├── NatMap.html │ ├── SentinelAddress.html │ └── SentinelConnectionOptions.html ├── examples ├── basic_operations.js ├── custom_connector.js ├── express │ ├── README.md │ ├── app.js │ ├── bin │ │ └── www │ ├── package-lock.json │ ├── package.json │ ├── public │ │ └── stylesheets │ │ │ └── style.css │ ├── redis.js │ ├── routes │ │ ├── index.js │ │ └── users.js │ └── views │ │ ├── error.jade │ │ ├── index.jade │ │ ├── layout.jade │ │ └── users.jade ├── hash.js ├── list.js ├── module.js ├── redis_streams.js ├── set.js ├── stream.js ├── string.js ├── ttl.js ├── typescript │ ├── package-lock.json │ ├── package.json │ └── scripts.ts └── zset.js ├── lib ├── Command.ts ├── DataHandler.ts ├── Pipeline.ts ├── Redis.ts ├── ScanStream.ts ├── Script.ts ├── SubscriptionSet.ts ├── autoPipelining.ts ├── cluster │ ├── ClusterOptions.ts │ ├── ClusterSubscriber.ts │ ├── ConnectionPool.ts │ ├── DelayQueue.ts │ ├── index.ts │ └── util.ts ├── connectors │ ├── AbstractConnector.ts │ ├── ConnectorConstructor.ts │ ├── SentinelConnector │ │ ├── FailoverDetector.ts │ │ ├── SentinelIterator.ts │ │ ├── index.ts │ │ └── types.ts │ ├── StandaloneConnector.ts │ └── index.ts ├── errors │ ├── ClusterAllFailedError.ts │ ├── MaxRetriesPerRequestError.ts │ └── index.ts ├── index.ts ├── redis │ ├── RedisOptions.ts │ └── event_handler.ts ├── transaction.ts ├── types.ts └── utils │ ├── Commander.ts │ ├── RedisCommander.ts │ ├── applyMixin.ts │ ├── debug.ts │ ├── index.ts │ └── lodash.ts ├── package-lock.json ├── package.json ├── resources ├── medis.png ├── redis-tryfree.png ├── ts-screenshot.png └── upstash.png ├── test ├── cluster │ ├── basic.ts │ └── docker │ │ ├── Dockerfile │ │ └── main.sh ├── functional │ ├── auth.ts │ ├── autopipelining.ts │ ├── cluster │ │ ├── ClusterSubscriber.ts │ │ ├── ConnectionPool.ts │ │ ├── ask.ts │ │ ├── autopipelining.ts │ │ ├── clusterdown.ts │ │ ├── connect.ts │ │ ├── disconnection.ts │ │ ├── dnsLookup.ts │ │ ├── duplicate.ts │ │ ├── index.ts │ │ ├── maxRedirections.ts │ │ ├── moved.ts │ │ ├── nat.ts │ │ ├── pipeline.ts │ │ ├── pub_sub.ts │ │ ├── quit.ts │ │ ├── resolveSrv.ts │ │ ├── scripting.ts │ │ ├── spub_ssub.ts │ │ ├── tls.ts │ │ ├── transaction.ts │ │ └── tryagain.ts │ ├── commandTimeout.ts │ ├── connection.ts │ ├── disconnection.ts │ ├── duplicate.ts │ ├── elasticache.ts │ ├── exports.ts │ ├── fatal_error.ts │ ├── hgetall.ts │ ├── lazy_connect.ts │ ├── maxRetriesPerRequest.ts │ ├── monitor.ts │ ├── pipeline.ts │ ├── pub_sub.ts │ ├── ready_check.ts │ ├── reconnect_on_error.ts │ ├── scan_stream.ts │ ├── scripting.ts │ ├── select.ts │ ├── send_command.ts │ ├── sentinel.ts │ ├── sentinel_nat.ts │ ├── show_friendly_error_stack.ts │ ├── socketTimeout.ts │ ├── spub_ssub.ts │ ├── string_numbers.ts │ ├── tls.ts │ ├── transaction.ts │ ├── transformer.ts │ └── watch-exec.ts ├── helpers │ ├── global.ts │ ├── mock_server.ts │ └── util.ts ├── typing │ ├── commands.test-d.ts │ ├── events.test-.ts │ ├── options.test-d.ts │ ├── pipeline.test-d.ts │ └── transformers.test-d.ts └── unit │ ├── DataHandler.ts │ ├── autoPipelining.ts │ ├── clusters │ ├── ConnectionPool.ts │ └── index.ts │ ├── command.ts │ ├── commander.ts │ ├── connectors │ ├── SentinelConnector │ │ └── SentinelIterator.ts │ └── connector.ts │ ├── debug.ts │ ├── index.ts │ ├── pipeline.ts │ ├── redis.ts │ └── utils.ts └── tsconfig.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "eslint:recommended", 4 | "plugin:@typescript-eslint/recommended", 5 | "prettier" 6 | ], 7 | "ignorePatterns": ["bin/generateRedisCommander/template.ts"], 8 | "parser": "@typescript-eslint/parser", 9 | "plugins": ["@typescript-eslint"], 10 | "env": { "node": true }, 11 | "rules": { 12 | "prefer-rest-params": 0, 13 | "no-var": 0, 14 | "no-prototype-builtins": 0, 15 | "prefer-spread": 0, 16 | "@typescript-eslint/no-var-requires": 0, 17 | "@typescript-eslint/no-explicit-any": 0, 18 | "@typescript-eslint/no-this-alias": 0, 19 | "@typescript-eslint/ban-ts-ignore": 0, 20 | "@typescript-eslint/ban-ts-comment": 0, 21 | "@typescript-eslint/adjacent-overload-signatures": 0, 22 | "@typescript-eslint/ban-types": 0, 23 | "@typescript-eslint/member-ordering": [ 24 | 1, 25 | { 26 | "default": { 27 | "memberTypes": [ 28 | "public-static-field", 29 | "protected-static-field", 30 | "private-static-field", 31 | 32 | "public-static-method", 33 | "protected-static-method", 34 | "private-static-method", 35 | 36 | "public-instance-field", 37 | "protected-instance-field", 38 | "private-instance-field", 39 | 40 | "public-constructor", 41 | "private-constructor", 42 | "protected-constructor", 43 | 44 | "public-instance-method", 45 | "protected-instance-method", 46 | "private-instance-method" 47 | ] 48 | } 49 | } 50 | ], 51 | "@typescript-eslint/explicit-member-accessibility": [ 52 | 1, 53 | { "accessibility": "no-public" } 54 | ], 55 | "@typescript-eslint/no-empty-interface": 0, 56 | "@typescript-eslint/no-empty-function": 0, 57 | "@typescript-eslint/no-unused-vars": [ 58 | "warn", 59 | { 60 | "args": "none" 61 | } 62 | ] 63 | }, 64 | "overrides": [ 65 | { 66 | "files": ["test/cluster/*", "test/unit/*", "test/functional/*"], 67 | "env": { 68 | "mocha": true 69 | }, 70 | "rules": { "prefer-const": 0 } 71 | } 72 | ] 73 | } 74 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to this repository 2 | 3 | Thanks for contributing to ioredis! 👏 4 | 5 | The goal of ioredis is to become a Redis client that is delightful to work with. It should have a full feature set, easy-to-use APIs, and high performance. 6 | 7 | Nowadays, it is one of the most popular Redis clients, and more and more people are using it. That's why we are welcoming more people to contribute to ioredis to make it even better! 👍 8 | 9 | 10 | ## User Roles 11 | 12 | There are two user roles: contributors and collaborators. Everyone becomes a contributor when they are creating issues, pull requests, or helping to review code. 13 | 14 | In the meantime, there is a group of collaborators of ioredis who can not only contribute code but also approve and merge others' pull requests. 15 | 16 | ## Note to collaborators 17 | 18 | Thank you for being a collaborator (which means you have already contributed amazing code so thanks again)! As a collaborator, you have the following permissions: 19 | 20 | 1. Approve pull requests. 21 | 2. Merge pull requests. 22 | 23 | Considering ioredis has been used in a great many serious codebases, we must be careful with code changes. In this repository, at least one approval is required for each pull request to be merged. 24 | 25 | ioredis uses [semantic-release](https://github.com/semantic-release/semantic-release). Every commit to the master branch will trigger a release automatically. To get a helpful changelog and make sure the version is correct, we adopt AngularJS's convention for commit message format. Please read more here: https://github.com/semantic-release/semantic-release#commit-message-format 26 | 27 | We prefer a linear Git history, so when merging a pull request, we should always go with squash, and update the commit message to fit our convention (simply put, prefix with `feat: ` for features, `fix: ` for fixes, `refactor: ` for refactors, and `docs: ` for documentation changes). 28 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: luin 4 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | daysUntilStale: 365 2 | daysUntilClose: 7 3 | exemptLabels: 4 | - pinned 5 | - security 6 | - bug 7 | - discussion 8 | staleLabel: wontfix 9 | markComment: > 10 | This issue has been automatically marked as stale because it has not had 11 | recent activity. It will be closed after 7 days if no further activity occurs, 12 | but feel free to re-open a closed issue if needed. 13 | closeComment: false 14 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | push: 5 | paths-ignore: 6 | - 'docs/**' 7 | - '*.md' 8 | pull_request: 9 | paths-ignore: 10 | - 'docs/**' 11 | - '*.md' 12 | 13 | jobs: 14 | test-redis: 15 | runs-on: ubuntu-latest 16 | strategy: 17 | fail-fast: true 18 | matrix: 19 | node: [18.x, 20.x, 22.x] 20 | steps: 21 | - name: Git checkout 22 | uses: actions/checkout@v2 23 | 24 | - name: Use Node.js ${{ matrix.node }} 25 | uses: actions/setup-node@v1 26 | with: 27 | node-version: ${{ matrix.node }} 28 | 29 | - name: Start Redis 30 | uses: supercharge/redis-github-action@1.4.0 31 | with: 32 | redis-version: latest 33 | 34 | - run: npm install 35 | - run: npm run lint 36 | - run: npm run build 37 | - run: npm run test:tsd 38 | - run: npm run test:cov || npm run test:cov || npm run test:cov 39 | 40 | test-cluster: 41 | runs-on: ubuntu-latest 42 | steps: 43 | - uses: actions/checkout@v2 44 | - name: Build and test cluster 45 | run: bash test/cluster/docker/main.sh 46 | 47 | test-valkey: 48 | runs-on: ubuntu-latest 49 | strategy: 50 | fail-fast: true 51 | matrix: 52 | node: [18.x, 20.x, 22.x] 53 | 54 | services: 55 | valkey: 56 | image: valkey/valkey:latest 57 | ports: 58 | # Opens tcp port 6379 on the host and service container 59 | - 6379:6379 60 | 61 | steps: 62 | - name: Git checkout 63 | uses: actions/checkout@v2 64 | 65 | - name: Use Node.js ${{ matrix.node }} 66 | uses: actions/setup-node@v1 67 | with: 68 | node-version: ${{ matrix.node }} 69 | 70 | - run: npm install 71 | - run: npm run lint 72 | - run: npm run build 73 | - run: npm run test:tsd 74 | - run: npm run test:cov || npm run test:cov || npm run test:cov 75 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.cpuprofile 3 | /test.* 4 | /.idea 5 | built 6 | 7 | .nyc_output 8 | coverage 9 | 10 | .vscode 11 | benchmarks/fixtures/*.txt 12 | 13 | *.rdb 14 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=true 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | package*.json 2 | built/ 3 | node_modules/ 4 | coverage/ 5 | .vscode/ 6 | -------------------------------------------------------------------------------- /.releaserc.json: -------------------------------------------------------------------------------- 1 | { 2 | "branches": ["main"], 3 | "plugins": [ 4 | [ 5 | "@semantic-release/commit-analyzer", 6 | { 7 | "preset": "angular", 8 | "parserOpts": { 9 | "noteKeywords": ["BREAKING CHANGE", "BREAKING CHANGES", "BREAKING"] 10 | } 11 | } 12 | ], 13 | "@semantic-release/release-notes-generator", 14 | ["@semantic-release/changelog", { "changelogFile": "CHANGELOG.md" }], 15 | "@semantic-release/npm", 16 | [ 17 | "@semantic-release/git", 18 | { 19 | "assets": [ 20 | "package.json", 21 | "package-lock.json", 22 | "CHANGELOG.md", 23 | "docs/**/*" 24 | ] 25 | } 26 | ], 27 | "@semantic-release/github" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at i@zihua.li. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2024 Matteo Collina 4 | Copyright (c) 2015-2022 Zihua Li 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /benchmarks/autopipelining-cluster.ts: -------------------------------------------------------------------------------- 1 | import { cronometro } from "cronometro"; 2 | import { readFileSync } from "fs"; 3 | import { join } from "path"; 4 | import Cluster from "../lib/cluster"; 5 | 6 | const numNodes = parseInt(process.env.NODES || "3", 10); 7 | const iterations = parseInt(process.env.ITERATIONS || "10000", 10); 8 | const batchSize = parseInt(process.env.BATCH_SIZE || "1000", 10); 9 | const keys = readFileSync( 10 | join(__dirname, `fixtures/cluster-${numNodes}.txt`), 11 | "utf-8" 12 | ).split("\n"); 13 | const configuration = Array.from(Array(numNodes), (_, i) => ({ 14 | host: "127.0.0.1", 15 | port: 30000 + i + 1, 16 | })); 17 | let cluster; 18 | 19 | function command(): string { 20 | const choice = Math.random(); 21 | 22 | if (choice < 0.3) { 23 | return "ttl"; 24 | } else if (choice < 0.6) { 25 | return "exists"; 26 | } 27 | 28 | return "get"; 29 | } 30 | 31 | function test() { 32 | const index = Math.floor(Math.random() * keys.length); 33 | 34 | return Promise.all( 35 | Array.from(Array(batchSize)).map(() => cluster[command()](keys[index])) 36 | ); 37 | } 38 | 39 | function after(cb) { 40 | cluster.quit(); 41 | cb(); 42 | } 43 | 44 | cronometro( 45 | { 46 | default: { 47 | test, 48 | before(cb) { 49 | cluster = new Cluster(configuration); 50 | 51 | cb(); 52 | }, 53 | after, 54 | }, 55 | "enableAutoPipelining=true": { 56 | test, 57 | before(cb) { 58 | cluster = new Cluster(configuration, { enableAutoPipelining: true }); 59 | cb(); 60 | }, 61 | after, 62 | }, 63 | }, 64 | { 65 | iterations, 66 | print: { compare: true }, 67 | } 68 | ); 69 | -------------------------------------------------------------------------------- /benchmarks/autopipelining-single.ts: -------------------------------------------------------------------------------- 1 | import { cronometro } from "cronometro"; 2 | import { readFileSync } from "fs"; 3 | import { join } from "path"; 4 | import Redis from "../lib/Redis"; 5 | 6 | const iterations = parseInt(process.env.ITERATIONS || "10000", 10); 7 | const batchSize = parseInt(process.env.BATCH_SIZE || "1000", 10); 8 | const keys = readFileSync( 9 | join(__dirname, "fixtures/cluster-3.txt"), 10 | "utf-8" 11 | ).split("\n"); 12 | let redis; 13 | 14 | function command(): string { 15 | const choice = Math.random(); 16 | 17 | if (choice < 0.3) { 18 | return "ttl"; 19 | } else if (choice < 0.6) { 20 | return "exists"; 21 | } 22 | 23 | return "get"; 24 | } 25 | 26 | function test() { 27 | const index = Math.floor(Math.random() * keys.length); 28 | 29 | return Promise.all( 30 | Array.from(Array(batchSize)).map(() => redis[command()](keys[index])) 31 | ); 32 | } 33 | 34 | function after(cb) { 35 | redis.quit(); 36 | cb(); 37 | } 38 | 39 | cronometro( 40 | { 41 | default: { 42 | test, 43 | before(cb) { 44 | redis = new Redis(); 45 | 46 | cb(); 47 | }, 48 | after, 49 | }, 50 | "enableAutoPipelining=true": { 51 | test, 52 | before(cb) { 53 | redis = new Redis({ enableAutoPipelining: true }); 54 | cb(); 55 | }, 56 | after, 57 | }, 58 | }, 59 | { 60 | iterations, 61 | print: { compare: true }, 62 | } 63 | ); 64 | -------------------------------------------------------------------------------- /benchmarks/dropBuffer.ts: -------------------------------------------------------------------------------- 1 | import { cronometro } from "cronometro"; 2 | import Redis from "../lib/Redis"; 3 | 4 | let redis; 5 | 6 | cronometro( 7 | { 8 | default: { 9 | test() { 10 | return redis.set("foo", "bar"); 11 | }, 12 | before(cb) { 13 | redis = new Redis(); 14 | cb(); 15 | }, 16 | after(cb) { 17 | redis.quit(); 18 | cb(); 19 | }, 20 | }, 21 | "dropBufferSupport=true": { 22 | test() { 23 | return redis.set("foo", "bar"); 24 | }, 25 | before(cb) { 26 | redis = new Redis({ dropBufferSupport: true }); 27 | cb(); 28 | }, 29 | after(cb) { 30 | redis.quit(); 31 | cb(); 32 | }, 33 | }, 34 | }, 35 | { 36 | print: { compare: true }, 37 | } 38 | ); 39 | -------------------------------------------------------------------------------- /benchmarks/errorStack.ts: -------------------------------------------------------------------------------- 1 | import { cronometro } from "cronometro"; 2 | import Redis from "../lib/Redis"; 3 | 4 | let redis; 5 | 6 | cronometro( 7 | { 8 | default: { 9 | test() { 10 | return redis.set("foo", "bar"); 11 | }, 12 | before(cb) { 13 | redis = new Redis(); 14 | cb(); 15 | }, 16 | after(cb) { 17 | redis.quit(); 18 | cb(); 19 | }, 20 | }, 21 | "showFriendlyErrorStack=true": { 22 | test() { 23 | return redis.set("foo", "bar"); 24 | }, 25 | before(cb) { 26 | redis = new Redis({ showFriendlyErrorStack: true }); 27 | cb(); 28 | }, 29 | after(cb) { 30 | redis.quit(); 31 | cb(); 32 | }, 33 | }, 34 | }, 35 | { 36 | print: { compare: true }, 37 | } 38 | ); 39 | -------------------------------------------------------------------------------- /benchmarks/fixtures/generate.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const start = process.hrtime.bigint(); 4 | 5 | import * as calculateSlot from "cluster-key-slot"; 6 | import { writeFileSync } from "fs"; 7 | import { join } from "path"; 8 | import { v4 as uuid } from "uuid"; 9 | 10 | // Input parameters 11 | const numKeys = parseInt(process.env.KEYS || "1000000", 10); 12 | const numNodes = parseInt(process.env.NODES || "3", 10); 13 | 14 | // Prepare topology 15 | const maxSlot = 16384; 16 | const destination = join(__dirname, `cluster-${numNodes}.txt`); 17 | const counts = Array.from(Array(numNodes), () => 0); 18 | const keys = []; 19 | 20 | /* 21 | This algorithm is taken and adapted from Redis source code 22 | See: https://github.com/redis/redis/blob/d9f970d8d3f0b694f1e8915cab6d4eab9cfb2ef1/src/redis-cli.c#L5453 23 | */ 24 | const nodes = []; // This only holds starting slot, since the ending slot can be computed out of the next one 25 | let first = 0; 26 | let cursor = 0; 27 | const slotsPerNode = maxSlot / numNodes; 28 | 29 | for (let i = 0; i < numNodes; i++) { 30 | let last = Math.round(cursor + slotsPerNode - 1); 31 | 32 | if (last > maxSlot || i === numNodes - 1) { 33 | last = maxSlot - 1; 34 | } 35 | 36 | if (last < first) { 37 | last = first; 38 | } 39 | 40 | nodes.push(first); 41 | first = last + 1; 42 | cursor += slotsPerNode; 43 | } 44 | 45 | // Generate keys and also track slot allocations 46 | for (let i = 0; i < numKeys; i++) { 47 | const key = uuid(); 48 | const slot = calculateSlot(key); 49 | const node = nodes.findIndex( 50 | (start, i) => i === numNodes - 1 || (slot >= start && slot < nodes[i + 1]) 51 | ); 52 | 53 | counts[node]++; 54 | keys.push(key); 55 | } 56 | 57 | // Save keys 58 | writeFileSync(destination, keys.join("\n")); 59 | 60 | // Print summary 61 | console.log( 62 | `Generated ${numKeys} keys in ${( 63 | Number(process.hrtime.bigint() - start) / 1e6 64 | ).toFixed(2)} ms ` 65 | ); 66 | 67 | for (let i = 0; i < numNodes; i++) { 68 | const from = nodes[i]; 69 | const to = (i === numNodes - 1 ? maxSlot : nodes[i + 1]) - 1; 70 | console.log( 71 | ` - Generated ${ 72 | counts[i] 73 | } keys for node(s) serving slots ${from}-${to} (${( 74 | (counts[i] * 100) / 75 | numKeys 76 | ).toFixed(2)} %)` 77 | ); 78 | } 79 | -------------------------------------------------------------------------------- /benchmarks/fixtures/insert.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync } from "fs"; 2 | import { join } from "path"; 3 | import Redis from "../../lib"; 4 | import Cluster from "../../lib/cluster"; 5 | 6 | async function main() { 7 | const numNodes = parseInt(process.env.NODES || "3", 10); 8 | let redis; 9 | 10 | if (process.env.CLUSTER === "true") { 11 | const configuration = Array.from(Array(numNodes), (_, i) => ({ 12 | host: "127.0.0.1", 13 | port: 30000 + i + 1, 14 | })); 15 | redis = new Cluster(configuration); 16 | console.log("Inserting fixtures keys in the cluster ..."); 17 | } else { 18 | redis = new Redis(); 19 | console.log("Inserting fixtures keys in the server ..."); 20 | } 21 | 22 | // Use Redis to set the keys 23 | const start = process.hrtime.bigint(); 24 | const keys = readFileSync( 25 | join(__dirname, `cluster-${numNodes}.txt`), 26 | "utf-8" 27 | ).split("\n"); 28 | const keysCount = keys.length; 29 | 30 | while (keys.length) { 31 | const promises = []; 32 | 33 | for (const key of keys.splice(0, 1000)) { 34 | promises.push(redis.set(key, key)); 35 | } 36 | 37 | await Promise.all(promises); 38 | } 39 | 40 | console.log( 41 | `Inserted ${keysCount} keys in ${( 42 | Number(process.hrtime.bigint() - start) / 1e6 43 | ).toFixed(2)} ms.` 44 | ); 45 | process.exit(0); 46 | } 47 | 48 | main().catch(console.error); 49 | -------------------------------------------------------------------------------- /bin/argumentTypes.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const typeMaps = require("./typeMaps"); 4 | 5 | module.exports = { 6 | debug: [ 7 | [{ name: "subcommand", type: "string" }], 8 | [ 9 | { name: "subcommand", type: "string" }, 10 | { name: "args", type: typeMaps.string("args"), multiple: true }, 11 | ], 12 | ], 13 | }; 14 | -------------------------------------------------------------------------------- /bin/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const returnTypes = require("./returnTypes"); 4 | const argumentTypes = require("./argumentTypes"); 5 | const sortArguments = require("./sortArguments"); 6 | const typeMaps = require("./typeMaps"); 7 | const overrides = require("./overrides"); 8 | const { getCommanderInterface } = require("@iovalkey/interface-generator"); 9 | 10 | const HEADER = `/** 11 | * This file is generated by @ioredis/interface-generator. 12 | * Don't edit it manually. Instead, run \`npm run generate\` to update 13 | * this file. 14 | */ 15 | 16 | `; 17 | 18 | const ignoredCommands = ["monitor", "multi", "function"]; 19 | const commands = require("@iovalkey/commands") 20 | .list.filter((name) => !ignoredCommands.includes(name)) 21 | .sort(); 22 | 23 | const fs = require("fs"); 24 | const path = require("path"); 25 | 26 | const template = fs.readFileSync(path.join(__dirname, "/template.ts"), "utf8"); 27 | 28 | async function main() { 29 | const i = await getCommanderInterface({ 30 | commands, 31 | complexityLimit: 50, 32 | redisOpts: { 33 | port: process.env.REDIS_PORT, 34 | }, 35 | overrides, 36 | returnTypes, 37 | argumentTypes, 38 | sortArguments, 39 | typeMaps: typeMaps, 40 | ignoredBufferVariant: [ 41 | "incrbyfloat", 42 | "type", 43 | "info", 44 | "latency", 45 | "lolwut", 46 | "memory", 47 | "cluster", 48 | "geopos", 49 | ], 50 | }); 51 | 52 | fs.writeFileSync( 53 | path.join(__dirname, "..", "lib/utils/RedisCommander.ts"), 54 | HEADER + template.replace("////", () => i) 55 | ); 56 | } 57 | 58 | main() 59 | .catch(console.error) 60 | .then(() => process.exit(0)); 61 | -------------------------------------------------------------------------------- /bin/overrides.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const msetOverrides = { 4 | overwrite: false, 5 | defs: [ 6 | "$1(object: object, callback?: Callback<'OK'>): Result<'OK', Context>", 7 | "$1(map: Map, callback?: Callback<'OK'>): Result<'OK', Context>", 8 | ], 9 | }; 10 | 11 | module.exports = { 12 | hgetall: { 13 | overwrite: true, 14 | defs: [ 15 | "$1(key: RedisKey, callback?: Callback>): Result, Context>", 16 | "$1Buffer(key: RedisKey, callback?: Callback>): Result, Context>", 17 | ], 18 | }, 19 | mset: msetOverrides, 20 | msetnx: msetOverrides, 21 | hset: { 22 | overwrite: false, 23 | defs: [ 24 | "$1(key: RedisKey, object: object, callback?: Callback): Result", 25 | "$1(key: RedisKey, map: Map, callback?: Callback): Result", 26 | ], 27 | }, 28 | hmset: { 29 | overwrite: false, 30 | defs: [ 31 | "$1(key: RedisKey, object: object, callback?: Callback<'OK'>): Result<'OK', Context>", 32 | "$1(key: RedisKey, map: Map, callback?: Callback<'OK'>): Result<'OK', Context>", 33 | ], 34 | }, 35 | exec: { 36 | overwrite: true, 37 | defs: [ 38 | "exec(callback?: Callback<[error: Error | null, result: unknown][] | null>): Promise<[error: Error | null, result: unknown][] | null>;", 39 | ], 40 | }, 41 | }; 42 | -------------------------------------------------------------------------------- /bin/sortArguments.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | set: (args) => { 3 | const sorted = args.sort((a, b) => { 4 | const order = ["key", "value", "expiration", "condition", "get"]; 5 | const indexA = order.indexOf(a.name); 6 | const indexB = order.indexOf(b.name); 7 | if (indexA === -1) { 8 | throw new Error('Invalid argument name: "' + a.name + '"'); 9 | } 10 | if (indexB === -1) { 11 | throw new Error('Invalid argument name: "' + b.name + '"'); 12 | } 13 | return indexA - indexB; 14 | }); 15 | return sorted; 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /bin/template.ts: -------------------------------------------------------------------------------- 1 | import { Callback } from "../types"; 2 | 3 | export type RedisKey = string | Buffer; 4 | export type RedisValue = string | Buffer | number; 5 | 6 | // Inspired by https://github.com/mmkal/handy-redis/blob/main/src/generated/interface.ts. 7 | // Should be fixed with https://github.com/Microsoft/TypeScript/issues/1213 8 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 9 | export interface ResultTypes { 10 | default: Promise; 11 | pipeline: ChainableCommander; 12 | } 13 | 14 | export interface ChainableCommander 15 | extends RedisCommander<{ type: "pipeline" }> { 16 | length: number; 17 | } 18 | 19 | export type ClientContext = { type: keyof ResultTypes }; 20 | export type Result = 21 | // prettier-break 22 | ResultTypes[Context["type"]]; 23 | 24 | interface RedisCommander { 25 | /** 26 | * Call arbitrary commands. 27 | * 28 | * `redis.call('set', 'foo', 'bar')` is the same as `redis.set('foo', 'bar')`, 29 | * so the only case you need to use this method is when the command is not 30 | * supported by ioredis. 31 | * 32 | * ```ts 33 | * redis.call('set', 'foo', 'bar'); 34 | * redis.call('get', 'foo', (err, value) => { 35 | * // value === 'bar' 36 | * }); 37 | * ``` 38 | */ 39 | call(command: string, callback?: Callback): Result; 40 | call( 41 | command: string, 42 | args: (string | Buffer | number)[], 43 | callback?: Callback 44 | ): Result; 45 | call( 46 | ...args: [ 47 | command: string, 48 | ...args: (string | Buffer | number)[], 49 | callback: Callback 50 | ] 51 | ): Result; 52 | call( 53 | ...args: [command: string, ...args: (string | Buffer | number)[]] 54 | ): Result; 55 | callBuffer( 56 | command: string, 57 | callback?: Callback 58 | ): Result; 59 | callBuffer( 60 | command: string, 61 | args: (string | Buffer | number)[], 62 | callback?: Callback 63 | ): Result; 64 | callBuffer( 65 | ...args: [ 66 | command: string, 67 | ...args: (string | Buffer | number)[], 68 | callback: Callback 69 | ] 70 | ): Result; 71 | callBuffer( 72 | ...args: [command: string, ...args: (string | Buffer | number)[]] 73 | ): Result; 74 | 75 | //// 76 | } 77 | 78 | export default RedisCommander; 79 | -------------------------------------------------------------------------------- /bin/typeMaps.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | key: "RedisKey", 3 | string: (name) => 4 | [ 5 | "value", 6 | "member", 7 | "element", 8 | "arg", 9 | "id", 10 | "pivot", 11 | "threshold", 12 | "start", 13 | "end", 14 | "max", 15 | "min", 16 | ].some((pattern) => name.toLowerCase().includes(pattern)) 17 | ? "string | Buffer | number" 18 | : "string | Buffer", 19 | pattern: "string", 20 | number: () => "number | string", 21 | }; 22 | -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- 1 | TypeDoc added this file to prevent GitHub Pages from using Jekyll. You can turn off this behavior by setting the `githubPages` option to false. -------------------------------------------------------------------------------- /docs/assets/highlight.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --light-hl-0: #001080; 3 | --dark-hl-0: #9CDCFE; 4 | --light-hl-1: #000000; 5 | --dark-hl-1: #D4D4D4; 6 | --light-hl-2: #795E26; 7 | --dark-hl-2: #DCDCAA; 8 | --light-hl-3: #A31515; 9 | --dark-hl-3: #CE9178; 10 | --light-hl-4: #0000FF; 11 | --dark-hl-4: #569CD6; 12 | --light-hl-5: #008000; 13 | --dark-hl-5: #6A9955; 14 | --light-hl-6: #0070C1; 15 | --dark-hl-6: #4FC1FF; 16 | --light-hl-7: #AF00DB; 17 | --dark-hl-7: #C586C0; 18 | --light-hl-8: #098658; 19 | --dark-hl-8: #B5CEA8; 20 | --light-code-background: #F5F5F5; 21 | --dark-code-background: #1E1E1E; 22 | } 23 | 24 | @media (prefers-color-scheme: light) { :root { 25 | --hl-0: var(--light-hl-0); 26 | --hl-1: var(--light-hl-1); 27 | --hl-2: var(--light-hl-2); 28 | --hl-3: var(--light-hl-3); 29 | --hl-4: var(--light-hl-4); 30 | --hl-5: var(--light-hl-5); 31 | --hl-6: var(--light-hl-6); 32 | --hl-7: var(--light-hl-7); 33 | --hl-8: var(--light-hl-8); 34 | --code-background: var(--light-code-background); 35 | } } 36 | 37 | @media (prefers-color-scheme: dark) { :root { 38 | --hl-0: var(--dark-hl-0); 39 | --hl-1: var(--dark-hl-1); 40 | --hl-2: var(--dark-hl-2); 41 | --hl-3: var(--dark-hl-3); 42 | --hl-4: var(--dark-hl-4); 43 | --hl-5: var(--dark-hl-5); 44 | --hl-6: var(--dark-hl-6); 45 | --hl-7: var(--dark-hl-7); 46 | --hl-8: var(--dark-hl-8); 47 | --code-background: var(--dark-code-background); 48 | } } 49 | 50 | body.light { 51 | --hl-0: var(--light-hl-0); 52 | --hl-1: var(--light-hl-1); 53 | --hl-2: var(--light-hl-2); 54 | --hl-3: var(--light-hl-3); 55 | --hl-4: var(--light-hl-4); 56 | --hl-5: var(--light-hl-5); 57 | --hl-6: var(--light-hl-6); 58 | --hl-7: var(--light-hl-7); 59 | --hl-8: var(--light-hl-8); 60 | --code-background: var(--light-code-background); 61 | } 62 | 63 | body.dark { 64 | --hl-0: var(--dark-hl-0); 65 | --hl-1: var(--dark-hl-1); 66 | --hl-2: var(--dark-hl-2); 67 | --hl-3: var(--dark-hl-3); 68 | --hl-4: var(--dark-hl-4); 69 | --hl-5: var(--dark-hl-5); 70 | --hl-6: var(--dark-hl-6); 71 | --hl-7: var(--dark-hl-7); 72 | --hl-8: var(--dark-hl-8); 73 | --code-background: var(--dark-code-background); 74 | } 75 | 76 | .hl-0 { color: var(--hl-0); } 77 | .hl-1 { color: var(--hl-1); } 78 | .hl-2 { color: var(--hl-2); } 79 | .hl-3 { color: var(--hl-3); } 80 | .hl-4 { color: var(--hl-4); } 81 | .hl-5 { color: var(--hl-5); } 82 | .hl-6 { color: var(--hl-6); } 83 | .hl-7 { color: var(--hl-7); } 84 | .hl-8 { color: var(--hl-8); } 85 | pre, code { background: var(--code-background); } 86 | -------------------------------------------------------------------------------- /docs/assets/icons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/valkey-io/iovalkey/f75fdc0247172901149cac6f49179f97da28c9b2/docs/assets/icons.png -------------------------------------------------------------------------------- /docs/assets/icons@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/valkey-io/iovalkey/f75fdc0247172901149cac6f49179f97da28c9b2/docs/assets/icons@2x.png -------------------------------------------------------------------------------- /docs/assets/widgets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/valkey-io/iovalkey/f75fdc0247172901149cac6f49179f97da28c9b2/docs/assets/widgets.png -------------------------------------------------------------------------------- /docs/assets/widgets@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/valkey-io/iovalkey/f75fdc0247172901149cac6f49179f97da28c9b2/docs/assets/widgets@2x.png -------------------------------------------------------------------------------- /docs/interfaces/NatMap.html: -------------------------------------------------------------------------------- 1 | NatMap | ioredis
Options
All
  • Public
  • Public/Protected
  • All
Menu

Interface NatMap

Hierarchy

  • NatMap

Indexable

[key: string]: { host: string; port: number }
  • host: string
  • port: number

Legend

  • Constructor
  • Property
  • Method
  • Property
  • Method
  • Static property
  • Static method
  • Inherited method

Settings

Theme

Generated using TypeDoc

-------------------------------------------------------------------------------- /examples/basic_operations.js: -------------------------------------------------------------------------------- 1 | const Redis = require("ioredis"); 2 | const redis = new Redis({ 3 | port: Number(process.env.redisPort || 6379), 4 | host: process.env.redisEndpoint, 5 | username: process.env.redisUsername, 6 | password: process.env.redisPW, 7 | }); 8 | 9 | // ioredis supports all Redis commands: 10 | redis.set("foo", "bar"); // returns promise which resolves to string, "OK" 11 | 12 | // the format is: redis[SOME_REDIS_COMMAND_IN_LOWERCASE](ARGUMENTS_ARE_JOINED_INTO_COMMAND_STRING) 13 | // the js: ` redis.set("mykey", "Hello") ` is equivalent to the cli: ` redis> SET mykey "Hello" ` 14 | 15 | // ioredis supports the node.js callback style 16 | redis.get("foo", (err, result) => { 17 | if (err) { 18 | console.error(err); 19 | } else { 20 | console.log(result); // Promise resolves to "bar" 21 | } 22 | }); 23 | 24 | // Or ioredis returns a promise if the last argument isn't a function 25 | redis.get("foo").then((result) => { 26 | console.log(result); 27 | }); 28 | 29 | redis.del("foo"); 30 | 31 | // Arguments to commands are flattened, so the following are the same: 32 | redis.sadd("set", 1, 3, 5, 7); 33 | redis.sadd("set", [1, 3, 5, 7]); 34 | redis.spop("set"); // Promise resolves to "5" or another item in the set 35 | 36 | // Most responses are strings, or arrays of strings 37 | redis.zadd("sortedSet", 1, "one", 2, "dos", 4, "quatro", 3, "three"); 38 | redis.zrange("sortedSet", 0, 2, "WITHSCORES").then((res) => console.log(res)); // Promise resolves to ["one", "1", "dos", "2", "three", "3"] as if the command was ` redis> ZRANGE sortedSet 0 2 WITHSCORES ` 39 | 40 | // Some responses have transformers to JS values 41 | redis.hset("myhash", "field1", "Hello"); 42 | redis.hgetall("myhash").then((res) => console.log(res)); // Promise resolves to Object {field1: "Hello"} rather than a string, or array of strings 43 | 44 | // All arguments are passed directly to the redis server: 45 | redis.set("key", 100, "EX", 10); // set's key to value 100 and expires it after 10 seconds 46 | 47 | // Change the server configuration 48 | redis.config("SET", "notify-keyspace-events", "KEA"); 49 | -------------------------------------------------------------------------------- /examples/custom_connector.js: -------------------------------------------------------------------------------- 1 | const Redis = require("ioredis"); 2 | const MyService = require("path/to/my/service"); 3 | 4 | // Create a custom connector that fetches sentinels from an external call 5 | class AsyncSentinelConnector extends Redis.SentinelConnector { 6 | constructor(options = {}) { 7 | // Placeholder 8 | options.sentinels = options.sentinels || [ 9 | { host: "localhost", port: 6379 }, 10 | ]; 11 | 12 | // SentinelConnector saves options as its property 13 | super(options); 14 | } 15 | 16 | connect(eventEmitter) { 17 | return MyService.getSentinels().then((sentinels) => { 18 | this.options.sentinels = sentinels; 19 | this.sentinelIterator = new Redis.SentinelIterator(sentinels); 20 | return Redis.SentinelConnector.prototype.connect.call(this, eventEmitter); 21 | }); 22 | } 23 | } 24 | 25 | const redis = new Redis({ 26 | Connector: AsyncSentinelConnector, 27 | }); 28 | 29 | // ioredis supports all Redis commands: 30 | redis.set("foo", "bar"); 31 | redis.get("foo", function (err, result) { 32 | if (err) { 33 | console.error(err); 34 | } else { 35 | console.log(result); 36 | } 37 | }); 38 | redis.del("foo"); 39 | 40 | // Or using a promise if the last argument isn't a function 41 | redis.get("foo").then(function (result) { 42 | console.log(result); 43 | }); 44 | 45 | // Arguments to commands are flattened, so the following are the same: 46 | redis.sadd("set", 1, 3, 5, 7); 47 | redis.sadd("set", [1, 3, 5, 7]); 48 | 49 | // All arguments are passed directly to the redis server: 50 | redis.set("key", 100, "EX", 10); 51 | 52 | // Change the server configuration 53 | redis.config("set", "notify-keyspace-events", "KEA"); 54 | -------------------------------------------------------------------------------- /examples/express/README.md: -------------------------------------------------------------------------------- 1 | ## Express Example 2 | 3 | This example demonstrates how to use ioredis in a web application. 4 | 5 | The idea is to create a shared Redis instance for the entire application, 6 | intead of using a connection pool or creating a new Redis instance for every 7 | file or even for every request. 8 | 9 | ### Install 10 | 11 | ``` 12 | npm install 13 | ``` 14 | 15 | ### Start 16 | 17 | ``` 18 | npm start 19 | ``` 20 | 21 | Then visit http://localhost:3000/ -------------------------------------------------------------------------------- /examples/express/app.js: -------------------------------------------------------------------------------- 1 | const createError = require("http-errors"); 2 | const express = require("express"); 3 | const path = require("path"); 4 | const cookieParser = require("cookie-parser"); 5 | const logger = require("morgan"); 6 | 7 | const indexRouter = require("./routes/index"); 8 | const usersRouter = require("./routes/users"); 9 | 10 | const app = express(); 11 | 12 | // view engine setup 13 | app.set("views", path.join(__dirname, "views")); 14 | app.set("view engine", "jade"); 15 | 16 | app.use(logger("dev")); 17 | app.use(express.json()); 18 | app.use(express.urlencoded({ extended: false })); 19 | app.use(cookieParser()); 20 | app.use(express.static(path.join(__dirname, "public"))); 21 | 22 | app.use("/", indexRouter); 23 | app.use("/users", usersRouter); 24 | 25 | // catch 404 and forward to error handler 26 | app.use((req, res, next) => { 27 | next(createError(404)); 28 | }); 29 | 30 | // error handler 31 | app.use((err, req, res, next) => { 32 | // set locals, only providing error in development 33 | res.locals.message = err.message; 34 | res.locals.error = req.app.get("env") === "development" ? err : {}; 35 | 36 | // render the error page 37 | res.status(err.status || 500); 38 | res.render("error"); 39 | }); 40 | 41 | module.exports = app; 42 | -------------------------------------------------------------------------------- /examples/express/bin/www: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | var app = require('../app'); 8 | var debug = require('debug')('express:server'); 9 | var http = require('http'); 10 | 11 | /** 12 | * Get port from environment and store in Express. 13 | */ 14 | 15 | var port = normalizePort(process.env.PORT || '3000'); 16 | app.set('port', port); 17 | 18 | /** 19 | * Create HTTP server. 20 | */ 21 | 22 | var server = http.createServer(app); 23 | 24 | /** 25 | * Listen on provided port, on all network interfaces. 26 | */ 27 | 28 | server.listen(port); 29 | server.on('error', onError); 30 | server.on('listening', onListening); 31 | 32 | /** 33 | * Normalize a port into a number, string, or false. 34 | */ 35 | 36 | function normalizePort(val) { 37 | var port = parseInt(val, 10); 38 | 39 | if (isNaN(port)) { 40 | // named pipe 41 | return val; 42 | } 43 | 44 | if (port >= 0) { 45 | // port number 46 | return port; 47 | } 48 | 49 | return false; 50 | } 51 | 52 | /** 53 | * Event listener for HTTP server "error" event. 54 | */ 55 | 56 | function onError(error) { 57 | if (error.syscall !== 'listen') { 58 | throw error; 59 | } 60 | 61 | var bind = typeof port === 'string' 62 | ? 'Pipe ' + port 63 | : 'Port ' + port; 64 | 65 | // handle specific listen errors with friendly messages 66 | switch (error.code) { 67 | case 'EACCES': 68 | console.error(bind + ' requires elevated privileges'); 69 | process.exit(1); 70 | break; 71 | case 'EADDRINUSE': 72 | console.error(bind + ' is already in use'); 73 | process.exit(1); 74 | break; 75 | default: 76 | throw error; 77 | } 78 | } 79 | 80 | /** 81 | * Event listener for HTTP server "listening" event. 82 | */ 83 | 84 | function onListening() { 85 | var addr = server.address(); 86 | var bind = typeof addr === 'string' 87 | ? 'pipe ' + addr 88 | : 'port ' + addr.port; 89 | debug('Listening on ' + bind); 90 | } 91 | -------------------------------------------------------------------------------- /examples/express/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "express", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "node ./bin/www" 7 | }, 8 | "dependencies": { 9 | "cookie-parser": "~1.4.4", 10 | "debug": "^4.3.3", 11 | "express": "^4.17.3", 12 | "http-errors": "~1.6.3", 13 | "ioredis": "^5.0.0", 14 | "jade": "~1.11.0", 15 | "morgan": "~1.9.1" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/express/public/stylesheets/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 50px; 3 | font: 14px "Lucida Grande", Helvetica, Arial, sans-serif; 4 | } 5 | 6 | a { 7 | color: #00B7FF; 8 | } 9 | -------------------------------------------------------------------------------- /examples/express/redis.js: -------------------------------------------------------------------------------- 1 | const Redis = require("ioredis"); 2 | const redis = new Redis(); 3 | 4 | // Create a shared Redis instance for the entire application. 5 | // Redis is single-thread so you don't need to create multiple instances 6 | // or use a connection pool. 7 | module.exports = redis; 8 | -------------------------------------------------------------------------------- /examples/express/routes/index.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const redis = require("../redis"); 3 | const router = express.Router(); 4 | 5 | /* GET home page. */ 6 | router.get("/", async (req, res) => { 7 | const count = await redis.llen("my-users"); 8 | res.render("index", { count }); 9 | }); 10 | 11 | module.exports = router; 12 | -------------------------------------------------------------------------------- /examples/express/routes/users.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const redis = require("../redis"); 3 | const router = express.Router(); 4 | 5 | /* GET users listing. */ 6 | router.get("/", async (req, res) => { 7 | const users = await redis.lrange("my-users", 0, -1); 8 | res.render("users", { users }); 9 | }); 10 | 11 | /* POST create a user. */ 12 | router.post("/", async (req, res) => { 13 | await redis.lpush("my-users", req.body.name); 14 | res.redirect("/"); 15 | }); 16 | 17 | module.exports = router; 18 | -------------------------------------------------------------------------------- /examples/express/views/error.jade: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block content 4 | h1= message 5 | h2= error.status 6 | pre #{error.stack} 7 | -------------------------------------------------------------------------------- /examples/express/views/index.jade: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block content 4 | h1= ioredis 5 | p We have  6 | a(href="/users") #{count} users 7 | 8 | form(method="post", action="/users") 9 | input(type="text", placeholder="New user", name="name") 10 | button(type="submit") Add User 11 | -------------------------------------------------------------------------------- /examples/express/views/layout.jade: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | title= title 5 | link(rel='stylesheet', href='/stylesheets/style.css') 6 | body 7 | block content 8 | -------------------------------------------------------------------------------- /examples/express/views/users.jade: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block content 4 | h1= ioredis 5 | ul 6 | each user in users 7 | li= user 8 | -------------------------------------------------------------------------------- /examples/hash.js: -------------------------------------------------------------------------------- 1 | const Redis = require("ioredis"); 2 | const redis = new Redis(); 3 | 4 | async function main() { 5 | const user = { 6 | name: "Bob", 7 | // The field of a Redis Hash key can only be a string. 8 | // We can write `age: 20` here but ioredis will convert it to a string anyway. 9 | age: "20", 10 | description: "I am a programmer", 11 | }; 12 | 13 | await redis.hset("user-hash", user); 14 | 15 | const name = await redis.hget("user-hash", "name"); 16 | console.log(name); // "Bob" 17 | 18 | const age = await redis.hget("user-hash", "age"); 19 | console.log(age); // "20" 20 | 21 | const all = await redis.hgetall("user-hash"); 22 | console.log(all); // { age: '20', name: 'Bob', description: 'I am a programmer' } 23 | 24 | // or `await redis.hdel("user-hash", "name", "description")`; 25 | await redis.hdel("user-hash", ["name", "description"]); 26 | 27 | const exists = await redis.hexists("user-hash", "name"); 28 | console.log(exists); // 0 (means false, and if it's 1, it means true) 29 | 30 | await redis.hincrby("user-hash", "age", 1); 31 | const newAge = await redis.hget("user-hash", "age"); 32 | console.log(newAge); // 21 33 | 34 | await redis.hsetnx("user-hash", "age", 23); 35 | console.log(await redis.hget("user-hash", "age")); // 21, as the field "age" already exists. 36 | } 37 | 38 | main(); 39 | -------------------------------------------------------------------------------- /examples/list.js: -------------------------------------------------------------------------------- 1 | const Redis = require("ioredis"); 2 | const redis = new Redis(); 3 | 4 | async function main() { 5 | const numbers = [1, 3, 5, 7, 9]; 6 | await redis.lpush("user-list", numbers); 7 | 8 | const popped = await redis.lpop("user-list"); 9 | console.log(popped); // 9 10 | 11 | const all = await redis.lrange("user-list", 0, -1); 12 | console.log(all); // [ '7', '5', '3', '1' ] 13 | 14 | const position = await redis.lpos("user-list", 5); 15 | console.log(position); // 1 16 | 17 | setTimeout(() => { 18 | // `redis` is in the block mode due to `redis.blpop()`, 19 | // so we duplicate a new connection to invoke LPUSH command. 20 | redis.duplicate().lpush("block-list", "hello"); 21 | }, 1200); 22 | const blockPopped = await redis.blpop("block-list", 0); // Resolved after 1200ms. 23 | console.log(blockPopped); // [ 'block-list', 'hello' ] 24 | } 25 | 26 | main(); 27 | -------------------------------------------------------------------------------- /examples/module.js: -------------------------------------------------------------------------------- 1 | const Redis = require("ioredis"); 2 | const redis = new Redis(); 3 | 4 | async function main() { 5 | // Redis#call() can be used to call arbitrary Redis commands. 6 | // The first parameter is the command name, the rest are arguments. 7 | await redis.call("JSON.SET", "doc", "$", '{"f1": {"a":1}, "f2":{"a":2}}'); 8 | const json = await redis.call("JSON.GET", "doc", "$..f1"); 9 | console.log(json); // [{"a":1}] 10 | } 11 | 12 | main(); 13 | -------------------------------------------------------------------------------- /examples/redis_streams.js: -------------------------------------------------------------------------------- 1 | const Redis = require("ioredis"); 2 | const redis = new Redis(); 3 | 4 | // you may find this read https://redis.io/topics/streams-intro 5 | // very helpfull as a starter to understand the usescases and the parameters used 6 | 7 | async function main() { 8 | const channel = "ioredis_channel"; 9 | // specify the channel. you want to know how many messages 10 | // have been written in this channel 11 | let messageCount = await redis.xlen(channel); 12 | console.log( 13 | `current message count in channel ${channel} is ${messageCount} messages` 14 | ); 15 | 16 | // specify channel to write a message into, 17 | // messages are key value 18 | const myMessage = "hello world"; 19 | await redis.xadd(channel, "*", myMessage, "message"); 20 | 21 | messageCount = await redis.xlen(channel); 22 | console.log( 23 | `current message count in channel ${channel} is ${messageCount} messages` 24 | ); 25 | // now you can see we have one new message 26 | 27 | // use xread to read all messages in channel 28 | let messages = await redis.xread(["STREAMS", channel, 0]); 29 | messages = messages[0][1]; 30 | console.log( 31 | `reading messages from channel ${channel}, found ${messages.length} messages` 32 | ); 33 | for (let i = 0; i < messages.length; i++) { 34 | let msg = messages[i]; 35 | msg = msg[1][0].toString(); 36 | console.log("reading message:", msg); 37 | } 38 | process.exit(0); 39 | } 40 | 41 | main(); 42 | -------------------------------------------------------------------------------- /examples/set.js: -------------------------------------------------------------------------------- 1 | const Redis = require("ioredis"); 2 | const redis = new Redis(); 3 | 4 | async function main() { 5 | const numbers = [1, 3, 5, 7, 9]; 6 | await redis.sadd("user-set", numbers); 7 | 8 | const elementCount = await redis.scard("user-set"); 9 | console.log(elementCount); // 5 10 | 11 | await redis.sadd("user-set", "1"); 12 | const newElementCount = await redis.scard("user-set"); 13 | console.log(newElementCount); // 5 14 | 15 | const isMember = await redis.sismember("user-set", 3); 16 | console.log(isMember); // 1 (means true, and if it's 0, it means false) 17 | } 18 | 19 | main(); 20 | -------------------------------------------------------------------------------- /examples/stream.js: -------------------------------------------------------------------------------- 1 | const Redis = require("ioredis"); 2 | const redis = new Redis(); 3 | const sub = new Redis(); 4 | const pub = new Redis(); 5 | 6 | // Usage 1: As message hub 7 | const processMessage = (message) => { 8 | console.log("Id: %s. Data: %O", message[0], message[1]); 9 | }; 10 | 11 | async function listenForMessage(lastId = "$") { 12 | // `results` is an array, each element of which corresponds to a key. 13 | // Because we only listen to one key (mystream) here, `results` only contains 14 | // a single element. See more: https://redis.io/commands/xread#return-value 15 | const results = await sub.xread("BLOCK", 0, "STREAMS", "user-stream", lastId); 16 | const [key, messages] = results[0]; // `key` equals to "user-stream" 17 | 18 | messages.forEach(processMessage); 19 | 20 | // Pass the last id of the results to the next round. 21 | await listenForMessage(messages[messages.length - 1][0]); 22 | } 23 | 24 | listenForMessage(); 25 | 26 | setInterval(() => { 27 | // `redis` is in the block mode due to `redis.xread('BLOCK', ....)`, 28 | // so we use another connection to publish messages. 29 | pub.xadd("user-stream", "*", "name", "John", "age", "20"); 30 | }, 1000); 31 | 32 | // Usage 2: As a list 33 | async function main() { 34 | redis 35 | .pipeline() 36 | .xadd("list-stream", "*", "id", "item1") 37 | .xadd("list-stream", "*", "id", "item2") 38 | .xadd("list-stream", "*", "id", "item3") 39 | .exec(); 40 | 41 | const items = await redis.xrange("list-stream", "-", "+", "COUNT", 2); 42 | console.log(items); 43 | // [ 44 | // [ '1647321710097-0', [ 'id', 'item1' ] ], 45 | // [ '1647321710098-0', [ 'id', 'item2' ] ] 46 | // ] 47 | } 48 | 49 | main(); 50 | -------------------------------------------------------------------------------- /examples/string.js: -------------------------------------------------------------------------------- 1 | const Redis = require("ioredis"); 2 | const redis = new Redis(); 3 | 4 | async function main() { 5 | const user = { 6 | name: "Bob", 7 | // The value of a Redis key can not be a number. 8 | // We can write `age: 20` here but ioredis will convert it to a string anyway. 9 | age: "20", 10 | description: "I am a programmer", 11 | }; 12 | 13 | await redis.mset(user); 14 | 15 | const name = await redis.get("name"); 16 | console.log(name); // "Bob" 17 | 18 | const age = await redis.get("age"); 19 | console.log(age); // "20" 20 | 21 | const all = await redis.mget("name", "age", "description"); 22 | console.log(all); // [ 'Bob', '20', 'I am a programmer' ] 23 | 24 | // or `await redis.del("name", "description")`; 25 | await redis.del(["name", "description"]); 26 | 27 | const exists = await redis.exists("name"); 28 | console.log(exists); // 0 (means false, and if it's 1, it means true) 29 | 30 | await redis.incrby("age", 1); 31 | const newAge = await redis.get("age"); 32 | console.log(newAge); // 21 33 | 34 | await redis.set("key_with_ttl", "hey", "EX", 1000); 35 | const ttl = await redis.ttl("key_with_ttl"); 36 | console.log(ttl); // a number smaller or equal to 1000 37 | } 38 | 39 | main(); 40 | -------------------------------------------------------------------------------- /examples/ttl.js: -------------------------------------------------------------------------------- 1 | const Redis = require("ioredis"); 2 | const redis = new Redis(); 3 | 4 | async function main() { 5 | await redis.set("foo", "bar"); 6 | await redis.expire("foo", 10); // 10 seconds 7 | console.log(await redis.ttl("foo")); // a number smaller or equal to 10 8 | 9 | await redis.set("foo", "bar", "EX", 20); 10 | console.log(await redis.ttl("foo")); // a number smaller or equal to 20 11 | 12 | // expireat accepts unix time in seconds. 13 | await redis.expireat("foo", Math.round(Date.now() / 1000) + 30); 14 | console.log(await redis.ttl("foo")); // a number smaller or equal to 30 15 | 16 | // "XX" and other options are available since Redis 7.0. 17 | await redis.expireat("foo", Math.round(Date.now() / 1000) + 40, "XX"); 18 | console.log(await redis.ttl("foo")); // a number smaller or equal to 40 19 | 20 | // expiretime is available since Redis 7.0. 21 | console.log(new Date((await redis.expiretime("foo")) * 1000)); 22 | 23 | await redis.pexpire("foo", 10 * 1000); // unit is millisecond for pexpire. 24 | console.log(await redis.ttl("foo")); // a number smaller or equal to 10 25 | 26 | await redis.persist("foo"); // Remove the existing timeout on key "foo" 27 | console.log(await redis.ttl("foo")); // -1 28 | } 29 | 30 | main(); 31 | -------------------------------------------------------------------------------- /examples/typescript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typescript", 3 | "version": "0.0.0", 4 | "description": "", 5 | "private": true, 6 | "main": "index.js", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "devDependencies": { 11 | "typescript": "^4.6.2" 12 | }, 13 | "dependencies": { 14 | "ioredis": "^5.0.0" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/typescript/scripts.ts: -------------------------------------------------------------------------------- 1 | import Redis, { Result, Callback } from "ioredis"; 2 | const redis = new Redis(); 3 | 4 | /** 5 | * Define our command 6 | */ 7 | redis.defineCommand("myecho", { 8 | numberOfKeys: 1, 9 | lua: "return KEYS[1] .. ARGV[1]", 10 | }); 11 | 12 | // Add declarations 13 | declare module "ioredis" { 14 | interface RedisCommander { 15 | myecho( 16 | key: string, 17 | argv: string, 18 | callback?: Callback 19 | ): Result; 20 | } 21 | } 22 | 23 | // Works with callbacks 24 | redis.myecho("key", "argv", (err, result) => { 25 | console.log("callback", result); 26 | }); 27 | 28 | // Works with Promises 29 | (async () => { 30 | console.log("promise", await redis.myecho("key", "argv")); 31 | })(); 32 | 33 | // Works with pipelining 34 | redis 35 | .pipeline() 36 | .myecho("key", "argv") 37 | .exec((err, result) => { 38 | console.log("pipeline", result); 39 | }); 40 | -------------------------------------------------------------------------------- /examples/zset.js: -------------------------------------------------------------------------------- 1 | const Redis = require("ioredis"); 2 | const redis = new Redis(); 3 | 4 | async function main() { 5 | const scores = [ 6 | { name: "Bob", score: 80 }, 7 | { name: "Jeff", score: 59.5 }, 8 | { name: "Tom", score: 100 }, 9 | { name: "Alex", score: 99.5 }, 10 | ]; 11 | await redis.zadd( 12 | "user-zset", 13 | ...scores.map(({ name, score }) => [score, name]) 14 | ); 15 | 16 | console.log(await redis.zrange("user-zset", 2, 3)); // [ 'Alex', 'Tom' ] 17 | console.log(await redis.zrange("user-zset", 2, 3, "WITHSCORES")); // [ 'Alex', '99.5', 'Tom', '100' ] 18 | console.log(await redis.zrange("user-zset", 2, 3, "REV")); // [ 'Bob', 'Jeff' ] 19 | console.log(await redis.zrange("user-zset", 80, 100, "BYSCORE")); // [ 'Bob', 'Alex', 'Tom' ] 20 | console.log(await redis.zrange("user-zset", 2, 3)); // [ 'Alex', 'Tom' ] 21 | } 22 | 23 | main(); 24 | -------------------------------------------------------------------------------- /lib/ScanStream.ts: -------------------------------------------------------------------------------- 1 | import { Readable, ReadableOptions } from "stream"; 2 | 3 | interface Options extends ReadableOptions { 4 | key?: string; 5 | match?: string; 6 | type?: string; 7 | noscores?: boolean; 8 | command: string; 9 | redis: any; 10 | count?: string | number; 11 | } 12 | 13 | /** 14 | * Convenient class to convert the process of scanning keys to a readable stream. 15 | */ 16 | export default class ScanStream extends Readable { 17 | private _redisCursor = "0"; 18 | private _redisDrained = false; 19 | 20 | constructor(private opt: Options) { 21 | super(opt); 22 | } 23 | 24 | _read() { 25 | if (this._redisDrained) { 26 | this.push(null); 27 | return; 28 | } 29 | 30 | const args: string[] = [this._redisCursor]; 31 | if (this.opt.key) { 32 | args.unshift(this.opt.key); 33 | } 34 | if (this.opt.match) { 35 | args.push("MATCH", this.opt.match); 36 | } 37 | if (this.opt.type) { 38 | args.push("TYPE", this.opt.type); 39 | } 40 | if (this.opt.count) { 41 | args.push("COUNT", String(this.opt.count)); 42 | } 43 | if (this.opt.noscores) { 44 | args.push("noscores"); 45 | } 46 | 47 | this.opt.redis[this.opt.command](args, (err, res) => { 48 | if (err) { 49 | this.emit("error", err); 50 | return; 51 | } 52 | this._redisCursor = res[0] instanceof Buffer ? res[0].toString() : res[0]; 53 | if (this._redisCursor === "0") { 54 | this._redisDrained = true; 55 | } 56 | this.push(res[1]); 57 | }); 58 | } 59 | 60 | close() { 61 | this._redisDrained = true; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /lib/Script.ts: -------------------------------------------------------------------------------- 1 | import { createHash } from "crypto"; 2 | import Command from "./Command"; 3 | import asCallback from "standard-as-callback"; 4 | import { Callback } from "./types"; 5 | export default class Script { 6 | private sha: string; 7 | private Command: new (...args: any[]) => Command; 8 | 9 | constructor( 10 | private lua: string, 11 | private numberOfKeys: number | null = null, 12 | private keyPrefix: string = "", 13 | private readOnly: boolean = false 14 | ) { 15 | this.sha = createHash("sha1").update(lua).digest("hex"); 16 | 17 | const sha = this.sha; 18 | const socketHasScriptLoaded = new WeakSet(); 19 | this.Command = class CustomScriptCommand extends Command { 20 | toWritable(socket: object): string | Buffer { 21 | const origReject = this.reject; 22 | this.reject = (err) => { 23 | if (err.message.indexOf("NOSCRIPT") !== -1) { 24 | socketHasScriptLoaded.delete(socket); 25 | } 26 | origReject.call(this, err); 27 | }; 28 | 29 | if (!socketHasScriptLoaded.has(socket)) { 30 | socketHasScriptLoaded.add(socket); 31 | this.name = "eval"; 32 | this.args[0] = lua; 33 | } else if (this.name === "eval") { 34 | this.name = "evalsha"; 35 | this.args[0] = sha; 36 | } 37 | return super.toWritable(socket); 38 | } 39 | }; 40 | } 41 | 42 | execute(container: any, args: any[], options: any, callback?: Callback) { 43 | if (typeof this.numberOfKeys === "number") { 44 | args.unshift(this.numberOfKeys); 45 | } 46 | if (this.keyPrefix) { 47 | options.keyPrefix = this.keyPrefix; 48 | } 49 | if (this.readOnly) { 50 | options.readOnly = true; 51 | } 52 | 53 | const evalsha = new this.Command("evalsha", [this.sha, ...args], options); 54 | 55 | evalsha.promise = evalsha.promise.catch((err: Error) => { 56 | if (err.message.indexOf("NOSCRIPT") === -1) { 57 | throw err; 58 | } 59 | 60 | // Resend the same custom evalsha command that gets transformed 61 | // to an eval in case it's not loaded yet on the connection. 62 | const resend = new this.Command("evalsha", [this.sha, ...args], options); 63 | 64 | const client = container.isPipeline ? container.redis : container; 65 | return client.sendCommand(resend); 66 | }); 67 | 68 | asCallback(evalsha.promise, callback); 69 | return container.sendCommand(evalsha); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /lib/SubscriptionSet.ts: -------------------------------------------------------------------------------- 1 | import { CommandNameFlags } from "./Command"; 2 | 3 | type AddSet = CommandNameFlags["ENTER_SUBSCRIBER_MODE"][number]; 4 | type DelSet = CommandNameFlags["EXIT_SUBSCRIBER_MODE"][number]; 5 | 6 | /** 7 | * Tiny class to simplify dealing with subscription set 8 | */ 9 | export default class SubscriptionSet { 10 | private set: { [key: string]: { [channel: string]: boolean } } = { 11 | subscribe: {}, 12 | psubscribe: {}, 13 | ssubscribe: {}, 14 | }; 15 | 16 | add(set: AddSet, channel: string) { 17 | this.set[mapSet(set)][channel] = true; 18 | } 19 | 20 | del(set: DelSet, channel: string) { 21 | delete this.set[mapSet(set)][channel]; 22 | } 23 | 24 | channels(set: AddSet | DelSet): string[] { 25 | return Object.keys(this.set[mapSet(set)]); 26 | } 27 | 28 | isEmpty(): boolean { 29 | return ( 30 | this.channels("subscribe").length === 0 && 31 | this.channels("psubscribe").length === 0 && 32 | this.channels("ssubscribe").length === 0 33 | ); 34 | } 35 | } 36 | 37 | function mapSet(set: AddSet | DelSet): AddSet { 38 | if (set === "unsubscribe") { 39 | return "subscribe"; 40 | } 41 | if (set === "punsubscribe") { 42 | return "psubscribe"; 43 | } 44 | if (set === "sunsubscribe") { 45 | return "ssubscribe"; 46 | } 47 | return set; 48 | } 49 | -------------------------------------------------------------------------------- /lib/cluster/DelayQueue.ts: -------------------------------------------------------------------------------- 1 | import { Debug } from "../utils"; 2 | import Deque = require("denque"); 3 | 4 | const debug = Debug("delayqueue"); 5 | 6 | export interface DelayQueueOptions { 7 | callback?: Function; 8 | timeout: number; 9 | } 10 | 11 | /** 12 | * Queue that runs items after specified duration 13 | */ 14 | export default class DelayQueue { 15 | private queues: { [key: string]: Deque } = {}; 16 | private timeouts: { [key: string]: NodeJS.Timer } = {}; 17 | 18 | /** 19 | * Add a new item to the queue 20 | * 21 | * @param bucket bucket name 22 | * @param item function that will run later 23 | * @param options 24 | */ 25 | push(bucket: string, item: Function, options: DelayQueueOptions): void { 26 | const callback = options.callback || process.nextTick; 27 | if (!this.queues[bucket]) { 28 | this.queues[bucket] = new Deque(); 29 | } 30 | 31 | const queue = this.queues[bucket]; 32 | queue.push(item); 33 | 34 | if (!this.timeouts[bucket]) { 35 | this.timeouts[bucket] = setTimeout(() => { 36 | callback(() => { 37 | this.timeouts[bucket] = null; 38 | this.execute(bucket); 39 | }); 40 | }, options.timeout); 41 | } 42 | } 43 | 44 | private execute(bucket: string): void { 45 | const queue = this.queues[bucket]; 46 | if (!queue) { 47 | return; 48 | } 49 | const { length } = queue; 50 | if (!length) { 51 | return; 52 | } 53 | debug("send %d commands in %s queue", length, bucket); 54 | 55 | this.queues[bucket] = null; 56 | while (queue.length > 0) { 57 | queue.shift()(); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /lib/cluster/util.ts: -------------------------------------------------------------------------------- 1 | import { parseURL } from "../utils"; 2 | import { isIP } from "net"; 3 | import { SrvRecord } from "dns"; 4 | 5 | export type NodeKey = string; 6 | export type NodeRole = "master" | "slave" | "all"; 7 | 8 | export interface RedisOptions { 9 | port: number; 10 | host: string; 11 | username?: string; 12 | password?: string; 13 | [key: string]: any; 14 | } 15 | 16 | export interface SrvRecordsGroup { 17 | totalWeight: number; 18 | records: SrvRecord[]; 19 | } 20 | 21 | export interface GroupedSrvRecords { 22 | [key: number]: SrvRecordsGroup; 23 | } 24 | 25 | export function getNodeKey(node: RedisOptions): NodeKey { 26 | node.port = node.port || 6379; 27 | node.host = node.host || "127.0.0.1"; 28 | return node.host + ":" + node.port; 29 | } 30 | 31 | export function nodeKeyToRedisOptions(nodeKey: NodeKey): RedisOptions { 32 | const portIndex = nodeKey.lastIndexOf(":"); 33 | if (portIndex === -1) { 34 | throw new Error(`Invalid node key ${nodeKey}`); 35 | } 36 | return { 37 | host: nodeKey.slice(0, portIndex), 38 | port: Number(nodeKey.slice(portIndex + 1)), 39 | }; 40 | } 41 | 42 | export function normalizeNodeOptions( 43 | nodes: Array 44 | ): RedisOptions[] { 45 | return nodes.map((node) => { 46 | const options: any = {}; 47 | if (typeof node === "object") { 48 | Object.assign(options, node); 49 | } else if (typeof node === "string") { 50 | Object.assign(options, parseURL(node)); 51 | } else if (typeof node === "number") { 52 | options.port = node; 53 | } else { 54 | throw new Error("Invalid argument " + node); 55 | } 56 | if (typeof options.port === "string") { 57 | options.port = parseInt(options.port, 10); 58 | } 59 | 60 | // Cluster mode only support db 0 61 | delete options.db; 62 | 63 | if (!options.port) { 64 | options.port = 6379; 65 | } 66 | if (!options.host) { 67 | options.host = "127.0.0.1"; 68 | } 69 | 70 | return options; 71 | }); 72 | } 73 | 74 | export function getUniqueHostnamesFromOptions(nodes: RedisOptions[]): string[] { 75 | const uniqueHostsMap = {}; 76 | nodes.forEach((node) => { 77 | uniqueHostsMap[node.host] = true; 78 | }); 79 | 80 | return Object.keys(uniqueHostsMap).filter((host) => !isIP(host)); 81 | } 82 | 83 | export function groupSrvRecords(records: SrvRecord[]): GroupedSrvRecords { 84 | const recordsByPriority = {}; 85 | for (const record of records) { 86 | if (!recordsByPriority.hasOwnProperty(record.priority)) { 87 | recordsByPriority[record.priority] = { 88 | totalWeight: record.weight, 89 | records: [record], 90 | }; 91 | } else { 92 | recordsByPriority[record.priority].totalWeight += record.weight; 93 | recordsByPriority[record.priority].records.push(record); 94 | } 95 | } 96 | 97 | return recordsByPriority; 98 | } 99 | 100 | export function weightSrvRecords(recordsGroup: SrvRecordsGroup): SrvRecord { 101 | if (recordsGroup.records.length === 1) { 102 | recordsGroup.totalWeight = 0; 103 | return recordsGroup.records.shift(); 104 | } 105 | 106 | // + `recordsGroup.records.length` to support `weight` 0 107 | const random = Math.floor( 108 | Math.random() * (recordsGroup.totalWeight + recordsGroup.records.length) 109 | ); 110 | let total = 0; 111 | for (const [i, record] of recordsGroup.records.entries()) { 112 | total += 1 + record.weight; 113 | if (total > random) { 114 | recordsGroup.totalWeight -= record.weight; 115 | recordsGroup.records.splice(i, 1); 116 | return record; 117 | } 118 | } 119 | } 120 | 121 | export function getConnectionName(component, nodeConnectionName) { 122 | const prefix = `ioredis-cluster(${component})`; 123 | return nodeConnectionName ? `${prefix}:${nodeConnectionName}` : prefix; 124 | } 125 | -------------------------------------------------------------------------------- /lib/connectors/AbstractConnector.ts: -------------------------------------------------------------------------------- 1 | import { NetStream } from "../types"; 2 | import { Debug } from "../utils"; 3 | 4 | const debug = Debug("AbstractConnector"); 5 | 6 | export type ErrorEmitter = (type: string, err: Error) => void; 7 | 8 | export default abstract class AbstractConnector { 9 | firstError?: Error; 10 | protected connecting = false; 11 | protected stream: NetStream; 12 | private disconnectTimeout: number; 13 | 14 | constructor(disconnectTimeout: number) { 15 | this.disconnectTimeout = disconnectTimeout; 16 | } 17 | 18 | check(info: any): boolean { 19 | return true; 20 | } 21 | 22 | disconnect(): void { 23 | this.connecting = false; 24 | 25 | if (this.stream) { 26 | const stream = this.stream; // Make sure callbacks refer to the same instance 27 | 28 | const timeout = setTimeout(() => { 29 | debug( 30 | "stream %s:%s still open, destroying it", 31 | stream.remoteAddress, 32 | stream.remotePort 33 | ); 34 | 35 | stream.destroy(); 36 | }, this.disconnectTimeout); 37 | 38 | stream.on("close", () => clearTimeout(timeout)); 39 | stream.end(); 40 | } 41 | } 42 | 43 | abstract connect(_: ErrorEmitter): Promise; 44 | } 45 | -------------------------------------------------------------------------------- /lib/connectors/ConnectorConstructor.ts: -------------------------------------------------------------------------------- 1 | import AbstractConnector from "./AbstractConnector"; 2 | 3 | interface ConnectorConstructor { 4 | new (options: unknown): AbstractConnector; 5 | } 6 | 7 | export default ConnectorConstructor; 8 | -------------------------------------------------------------------------------- /lib/connectors/SentinelConnector/FailoverDetector.ts: -------------------------------------------------------------------------------- 1 | import { Debug } from "../../utils"; 2 | import SentinelConnector from "./index"; 3 | import { Sentinel } from "./types"; 4 | 5 | const debug = Debug("FailoverDetector"); 6 | 7 | const CHANNEL_NAME = "+switch-master"; 8 | 9 | export class FailoverDetector { 10 | private connector: SentinelConnector; 11 | private sentinels: Sentinel[]; 12 | private isDisconnected = false; 13 | 14 | // sentinels can't be used for regular commands after this 15 | constructor(connector: SentinelConnector, sentinels: Sentinel[]) { 16 | this.connector = connector; 17 | this.sentinels = sentinels; 18 | } 19 | 20 | cleanup() { 21 | this.isDisconnected = true; 22 | 23 | for (const sentinel of this.sentinels) { 24 | sentinel.client.disconnect(); 25 | } 26 | } 27 | 28 | async subscribe() { 29 | debug("Starting FailoverDetector"); 30 | 31 | const promises: Promise[] = []; 32 | 33 | for (const sentinel of this.sentinels) { 34 | const promise = sentinel.client.subscribe(CHANNEL_NAME).catch((err) => { 35 | debug( 36 | "Failed to subscribe to failover messages on sentinel %s:%s (%s)", 37 | sentinel.address.host || "127.0.0.1", 38 | sentinel.address.port || 26739, 39 | err.message 40 | ); 41 | }); 42 | 43 | promises.push(promise); 44 | 45 | sentinel.client.on("message", (channel: string) => { 46 | if (!this.isDisconnected && channel === CHANNEL_NAME) { 47 | this.disconnect(); 48 | } 49 | }); 50 | } 51 | 52 | await Promise.all(promises); 53 | } 54 | 55 | private disconnect() { 56 | // Avoid disconnecting more than once per failover. 57 | // A new FailoverDetector will be created after reconnecting. 58 | this.isDisconnected = true; 59 | 60 | debug("Failover detected, disconnecting"); 61 | 62 | // Will call this.cleanup() 63 | this.connector.disconnect(); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /lib/connectors/SentinelConnector/SentinelIterator.ts: -------------------------------------------------------------------------------- 1 | import { SentinelAddress } from "./types"; 2 | 3 | function isSentinelEql( 4 | a: Partial, 5 | b: Partial 6 | ): boolean { 7 | return ( 8 | (a.host || "127.0.0.1") === (b.host || "127.0.0.1") && 9 | (a.port || 26379) === (b.port || 26379) 10 | ); 11 | } 12 | 13 | export default class SentinelIterator 14 | implements Iterator> 15 | { 16 | private cursor = 0; 17 | private sentinels: Array>; 18 | 19 | constructor(sentinels: Array>) { 20 | this.sentinels = sentinels.slice(0); 21 | } 22 | 23 | next() { 24 | const done = this.cursor >= this.sentinels.length; 25 | return { done, value: done ? undefined : this.sentinels[this.cursor++] }; 26 | } 27 | 28 | reset(moveCurrentEndpointToFirst: boolean): void { 29 | if ( 30 | moveCurrentEndpointToFirst && 31 | this.sentinels.length > 1 && 32 | this.cursor !== 1 33 | ) { 34 | this.sentinels.unshift(...this.sentinels.splice(this.cursor - 1)); 35 | } 36 | this.cursor = 0; 37 | } 38 | 39 | add(sentinel: SentinelAddress): boolean { 40 | for (let i = 0; i < this.sentinels.length; i++) { 41 | if (isSentinelEql(sentinel, this.sentinels[i])) { 42 | return false; 43 | } 44 | } 45 | 46 | this.sentinels.push(sentinel); 47 | return true; 48 | } 49 | 50 | toString(): string { 51 | return `${JSON.stringify(this.sentinels)} @${this.cursor}`; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /lib/connectors/SentinelConnector/types.ts: -------------------------------------------------------------------------------- 1 | import { RedisOptions } from "../../redis/RedisOptions"; 2 | 3 | export interface SentinelAddress { 4 | port: number; 5 | host: string; 6 | family?: number; 7 | } 8 | 9 | // TODO: A proper typedef. This one only declares a small subset of all the members. 10 | export interface RedisClient { 11 | options: RedisOptions; 12 | sentinel(subcommand: "sentinels", name: string): Promise; 13 | sentinel( 14 | subcommand: "get-master-addr-by-name", 15 | name: string 16 | ): Promise; 17 | sentinel(subcommand: "slaves", name: string): Promise; 18 | subscribe(...channelNames: string[]): Promise; 19 | on( 20 | event: "message", 21 | callback: (channel: string, message: string) => void 22 | ): void; 23 | on(event: "error", callback: (error: Error) => void): void; 24 | on(event: "reconnecting", callback: () => void): void; 25 | disconnect(): void; 26 | } 27 | 28 | export interface Sentinel { 29 | address: Partial; 30 | client: RedisClient; 31 | } 32 | -------------------------------------------------------------------------------- /lib/connectors/StandaloneConnector.ts: -------------------------------------------------------------------------------- 1 | import { createConnection, IpcNetConnectOpts, TcpNetConnectOpts } from "net"; 2 | import { connect as createTLSConnection, ConnectionOptions } from "tls"; 3 | import { NetStream } from "../types"; 4 | import { CONNECTION_CLOSED_ERROR_MSG } from "../utils"; 5 | import AbstractConnector, { ErrorEmitter } from "./AbstractConnector"; 6 | 7 | type TcpOptions = Pick; 8 | type IpcOptions = Pick; 9 | 10 | export type StandaloneConnectionOptions = Partial & { 11 | disconnectTimeout?: number; 12 | tls?: ConnectionOptions; 13 | }; 14 | 15 | export default class StandaloneConnector extends AbstractConnector { 16 | constructor(protected options: StandaloneConnectionOptions) { 17 | super(options.disconnectTimeout); 18 | } 19 | 20 | connect(_: ErrorEmitter) { 21 | const { options } = this; 22 | this.connecting = true; 23 | 24 | let connectionOptions: TcpOptions | IpcOptions; 25 | if ("path" in options && options.path) { 26 | connectionOptions = { 27 | path: options.path, 28 | } as IpcOptions; 29 | } else { 30 | connectionOptions = {} as TcpOptions; 31 | if ("port" in options && options.port != null) { 32 | connectionOptions.port = options.port; 33 | } 34 | if ("host" in options && options.host != null) { 35 | connectionOptions.host = options.host; 36 | } 37 | if ("family" in options && options.family != null) { 38 | connectionOptions.family = options.family; 39 | } 40 | } 41 | 42 | if (options.tls) { 43 | Object.assign(connectionOptions, options.tls); 44 | } 45 | 46 | // TODO: 47 | // We use native Promise here since other Promise 48 | // implementation may use different schedulers that 49 | // cause issue when the stream is resolved in the 50 | // next tick. 51 | // Should use the provided promise in the next major 52 | // version and do not connect before resolved. 53 | return new Promise((resolve, reject) => { 54 | process.nextTick(() => { 55 | if (!this.connecting) { 56 | reject(new Error(CONNECTION_CLOSED_ERROR_MSG)); 57 | return; 58 | } 59 | 60 | try { 61 | if (options.tls) { 62 | this.stream = createTLSConnection(connectionOptions); 63 | } else { 64 | this.stream = createConnection(connectionOptions); 65 | } 66 | } catch (err) { 67 | reject(err); 68 | return; 69 | } 70 | 71 | this.stream.once("error", (err) => { 72 | this.firstError = err; 73 | }); 74 | 75 | resolve(this.stream); 76 | }); 77 | }); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /lib/connectors/index.ts: -------------------------------------------------------------------------------- 1 | import StandaloneConnector from "./StandaloneConnector"; 2 | import SentinelConnector from "./SentinelConnector"; 3 | 4 | export { StandaloneConnector, SentinelConnector }; 5 | -------------------------------------------------------------------------------- /lib/errors/ClusterAllFailedError.ts: -------------------------------------------------------------------------------- 1 | import { RedisError } from "redis-errors"; 2 | 3 | export default class ClusterAllFailedError extends RedisError { 4 | static defaultMessage = "Failed to refresh slots cache."; 5 | 6 | constructor(message, public lastNodeError: RedisError) { 7 | super(message); 8 | Error.captureStackTrace(this, this.constructor); 9 | } 10 | 11 | get name(): string { 12 | return this.constructor.name; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /lib/errors/MaxRetriesPerRequestError.ts: -------------------------------------------------------------------------------- 1 | import { AbortError } from "redis-errors"; 2 | 3 | export default class MaxRetriesPerRequestError extends AbortError { 4 | constructor(maxRetriesPerRequest: number) { 5 | const message = `Reached the max retries per request limit (which is ${maxRetriesPerRequest}). Refer to "maxRetriesPerRequest" option for details.`; 6 | 7 | super(message); 8 | Error.captureStackTrace(this, this.constructor); 9 | } 10 | 11 | get name(): string { 12 | return this.constructor.name; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /lib/errors/index.ts: -------------------------------------------------------------------------------- 1 | import MaxRetriesPerRequestError from "./MaxRetriesPerRequestError"; 2 | 3 | export { MaxRetriesPerRequestError }; 4 | -------------------------------------------------------------------------------- /lib/index.ts: -------------------------------------------------------------------------------- 1 | exports = module.exports = require("./Redis").default; 2 | 3 | export { default } from "./Redis"; 4 | export { default as Redis } from "./Redis"; 5 | export { default as Cluster } from "./cluster"; 6 | 7 | /** 8 | * @ignore 9 | */ 10 | export { default as Command } from "./Command"; 11 | 12 | /** 13 | * @ignore 14 | */ 15 | export { 16 | default as RedisCommander, 17 | Result, 18 | ClientContext, 19 | } from "./utils/RedisCommander"; 20 | 21 | /** 22 | * @ignore 23 | */ 24 | export { default as ScanStream } from "./ScanStream"; 25 | 26 | /** 27 | * @ignore 28 | */ 29 | export { default as Pipeline } from "./Pipeline"; 30 | 31 | /** 32 | * @ignore 33 | */ 34 | export { default as AbstractConnector } from "./connectors/AbstractConnector"; 35 | 36 | /** 37 | * @ignore 38 | */ 39 | export { 40 | default as SentinelConnector, 41 | SentinelIterator, 42 | } from "./connectors/SentinelConnector"; 43 | 44 | /** 45 | * @ignore 46 | */ 47 | export { Callback } from "./types"; 48 | 49 | // Type Exports 50 | export { 51 | SentinelAddress, 52 | SentinelConnectionOptions, 53 | } from "./connectors/SentinelConnector"; 54 | export { StandaloneConnectionOptions } from "./connectors/StandaloneConnector"; 55 | export { RedisOptions, CommonRedisOptions } from "./redis/RedisOptions"; 56 | export { ClusterNode } from "./cluster"; 57 | export { 58 | ClusterOptions, 59 | DNSLookupFunction, 60 | DNSResolveSrvFunction, 61 | NatMap, 62 | } from "./cluster/ClusterOptions"; 63 | export { NodeRole } from "./cluster/util"; 64 | export type { 65 | RedisKey, 66 | RedisValue, 67 | ChainableCommander, 68 | } from "./utils/RedisCommander"; 69 | 70 | // No TS typings 71 | export const ReplyError = require("redis-errors").ReplyError; 72 | 73 | /** 74 | * @ignore 75 | */ 76 | Object.defineProperty(exports, "Promise", { 77 | get() { 78 | console.warn( 79 | "ioredis v5 does not support plugging third-party Promise library anymore. Native Promise will be used." 80 | ); 81 | return Promise; 82 | }, 83 | set(_lib: unknown) { 84 | console.warn( 85 | "ioredis v5 does not support plugging third-party Promise library anymore. Native Promise will be used." 86 | ); 87 | }, 88 | }); 89 | 90 | /** 91 | * @ignore 92 | */ 93 | export function print(err: Error | null, reply?: any) { 94 | if (err) { 95 | console.log("Error: " + err); 96 | } else { 97 | console.log("Reply: " + reply); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /lib/transaction.ts: -------------------------------------------------------------------------------- 1 | import { wrapMultiResult, noop } from "./utils"; 2 | import asCallback from "standard-as-callback"; 3 | import Pipeline from "./Pipeline"; 4 | import { Callback } from "./types"; 5 | import { ChainableCommander } from "./utils/RedisCommander"; 6 | 7 | export interface Transaction { 8 | pipeline(commands?: unknown[][]): ChainableCommander; 9 | multi(options: { pipeline: false }): Promise<"OK">; 10 | multi(): ChainableCommander; 11 | multi(options: { pipeline: true }): ChainableCommander; 12 | multi(commands?: unknown[][]): ChainableCommander; 13 | } 14 | 15 | export function addTransactionSupport(redis) { 16 | redis.pipeline = function (commands) { 17 | const pipeline = new Pipeline(this); 18 | if (Array.isArray(commands)) { 19 | pipeline.addBatch(commands); 20 | } 21 | return pipeline; 22 | }; 23 | 24 | const { multi } = redis; 25 | redis.multi = function (commands, options) { 26 | if (typeof options === "undefined" && !Array.isArray(commands)) { 27 | options = commands; 28 | commands = null; 29 | } 30 | if (options && options.pipeline === false) { 31 | return multi.call(this); 32 | } 33 | const pipeline = new Pipeline(this); 34 | // @ts-expect-error 35 | pipeline.multi(); 36 | if (Array.isArray(commands)) { 37 | pipeline.addBatch(commands); 38 | } 39 | const exec = pipeline.exec; 40 | pipeline.exec = function (callback: Callback) { 41 | // Wait for the cluster to be connected, since we need nodes information before continuing 42 | if (this.isCluster && !this.redis.slots.length) { 43 | if (this.redis.status === "wait") this.redis.connect().catch(noop); 44 | return asCallback( 45 | new Promise((resolve, reject) => { 46 | this.redis.delayUntilReady((err) => { 47 | if (err) { 48 | reject(err); 49 | return; 50 | } 51 | 52 | this.exec(pipeline).then(resolve, reject); 53 | }); 54 | }), 55 | callback 56 | ); 57 | } 58 | 59 | if (this._transactions > 0) { 60 | exec.call(pipeline); 61 | } 62 | 63 | // Returns directly when the pipeline 64 | // has been called multiple times (retries). 65 | if (this.nodeifiedPromise) { 66 | return exec.call(pipeline); 67 | } 68 | const promise = exec.call(pipeline); 69 | return asCallback( 70 | promise.then(function (result: any[]): any[] | null { 71 | const execResult = result[result.length - 1]; 72 | if (typeof execResult === "undefined") { 73 | throw new Error( 74 | "Pipeline cannot be used to send any commands when the `exec()` has been called on it." 75 | ); 76 | } 77 | if (execResult[0]) { 78 | execResult[0].previousErrors = []; 79 | for (let i = 0; i < result.length - 1; ++i) { 80 | if (result[i][0]) { 81 | execResult[0].previousErrors.push(result[i][0]); 82 | } 83 | } 84 | throw execResult[0]; 85 | } 86 | return wrapMultiResult(execResult[1]); 87 | }), 88 | callback 89 | ); 90 | }; 91 | 92 | // @ts-expect-error 93 | const { execBuffer } = pipeline; 94 | // @ts-expect-error 95 | pipeline.execBuffer = function (callback: Callback) { 96 | if (this._transactions > 0) { 97 | execBuffer.call(pipeline); 98 | } 99 | return pipeline.exec(callback); 100 | }; 101 | return pipeline; 102 | }; 103 | 104 | const { exec } = redis; 105 | redis.exec = function (callback: Callback): Promise { 106 | return asCallback( 107 | exec.call(this).then(function (results: any[] | null) { 108 | if (Array.isArray(results)) { 109 | results = wrapMultiResult(results); 110 | } 111 | return results; 112 | }), 113 | callback 114 | ); 115 | }; 116 | } 117 | -------------------------------------------------------------------------------- /lib/types.ts: -------------------------------------------------------------------------------- 1 | import { Socket } from "net"; 2 | import { TLSSocket } from "tls"; 3 | 4 | export type Callback = (err?: Error | null, result?: T) => void; 5 | export type NetStream = Socket | TLSSocket; 6 | 7 | export type CommandParameter = string | Buffer | number | any[]; 8 | export interface Respondable { 9 | name: string; 10 | args: CommandParameter[]; 11 | resolve(result: any): void; 12 | reject(error: Error): void; 13 | } 14 | 15 | export interface PipelineWriteableStream { 16 | isPipeline: true; 17 | write(data: string | Buffer): unknown; 18 | destination: { redis: { stream: NetStream } }; 19 | } 20 | 21 | export type WriteableStream = NetStream | PipelineWriteableStream; 22 | 23 | export interface CommandItem { 24 | command: Respondable; 25 | stream: WriteableStream; 26 | select: number; 27 | } 28 | 29 | export interface ScanStreamOptions { 30 | match?: string; 31 | type?: string; 32 | count?: number; 33 | noscores?: boolean; 34 | } 35 | -------------------------------------------------------------------------------- /lib/utils/applyMixin.ts: -------------------------------------------------------------------------------- 1 | type Constructor = new (...args: any[]) => void; 2 | function applyMixin( 3 | derivedConstructor: Constructor, 4 | mixinConstructor: Constructor 5 | ) { 6 | Object.getOwnPropertyNames(mixinConstructor.prototype).forEach((name) => { 7 | Object.defineProperty( 8 | derivedConstructor.prototype, 9 | name, 10 | Object.getOwnPropertyDescriptor(mixinConstructor.prototype, name) 11 | ); 12 | }); 13 | } 14 | 15 | export default applyMixin; 16 | -------------------------------------------------------------------------------- /lib/utils/debug.ts: -------------------------------------------------------------------------------- 1 | import debug from "debug"; 2 | 3 | const MAX_ARGUMENT_LENGTH = 200; 4 | const NAMESPACE_PREFIX = "ioredis"; 5 | 6 | /** 7 | * helper function that tried to get a string value for 8 | * arbitrary "debug" arg 9 | */ 10 | function getStringValue(v: any): string | void { 11 | if (v === null) { 12 | return; 13 | } 14 | 15 | switch (typeof v) { 16 | case "boolean": 17 | return; 18 | case "number": 19 | return; 20 | 21 | case "object": 22 | if (Buffer.isBuffer(v)) { 23 | return v.toString("hex"); 24 | } 25 | if (Array.isArray(v)) { 26 | return v.join(","); 27 | } 28 | 29 | try { 30 | return JSON.stringify(v); 31 | } catch (e) { 32 | return; 33 | } 34 | 35 | case "string": 36 | return v; 37 | } 38 | } 39 | 40 | /** 41 | * helper function that redacts a string representation of a "debug" arg 42 | */ 43 | function genRedactedString(str: string, maxLen: number): string { 44 | const { length } = str; 45 | 46 | return length <= maxLen 47 | ? str 48 | : str.slice(0, maxLen) + ' ... '; 49 | } 50 | 51 | /** 52 | * a wrapper for the `debug` module, used to generate 53 | * "debug functions" that trim the values in their output 54 | */ 55 | export default function genDebugFunction( 56 | namespace: string 57 | ): (...args: any[]) => void { 58 | const fn = debug(`${NAMESPACE_PREFIX}:${namespace}`); 59 | 60 | function wrappedDebug(...args: any[]): void { 61 | if (!fn.enabled) { 62 | return; // no-op 63 | } 64 | 65 | // we skip the first arg because that is the message 66 | for (let i = 1; i < args.length; i++) { 67 | const str = getStringValue(args[i]); 68 | if (typeof str === "string" && str.length > MAX_ARGUMENT_LENGTH) { 69 | args[i] = genRedactedString(str, MAX_ARGUMENT_LENGTH); 70 | } 71 | } 72 | 73 | return fn.apply(null, args); 74 | } 75 | 76 | Object.defineProperties(wrappedDebug, { 77 | namespace: { 78 | get() { 79 | return fn.namespace; 80 | }, 81 | }, 82 | enabled: { 83 | get() { 84 | return fn.enabled; 85 | }, 86 | }, 87 | destroy: { 88 | get() { 89 | return fn.destroy; 90 | }, 91 | }, 92 | log: { 93 | get() { 94 | return fn.log; 95 | }, 96 | set(l) { 97 | fn.log = l; 98 | }, 99 | }, 100 | }); 101 | return wrappedDebug; 102 | } 103 | 104 | // TODO: remove these 105 | // expose private stuff for unit-testing 106 | export { MAX_ARGUMENT_LENGTH, getStringValue, genRedactedString }; 107 | -------------------------------------------------------------------------------- /lib/utils/lodash.ts: -------------------------------------------------------------------------------- 1 | import defaults = require("lodash.defaults"); 2 | import isArguments = require("lodash.isarguments"); 3 | 4 | export function noop() {} 5 | 6 | export { defaults, isArguments }; 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "iovalkey", 3 | "version": "0.3.2", 4 | "description": "A robust, performance-focused and full-featured Valkey/Redis client for Node.js.", 5 | "main": "./built/index.js", 6 | "types": "./built/index.d.ts", 7 | "files": [ 8 | "built/" 9 | ], 10 | "scripts": { 11 | "test:tsd": "npm run build && tsd", 12 | "test:js": "TS_NODE_TRANSPILE_ONLY=true NODE_ENV=test mocha \"test/helpers/*.ts\" \"test/unit/**/*.ts\" \"test/functional/**/*.ts\"", 13 | "test:cov": "nyc npm run test:js", 14 | "test:js:cluster": "TS_NODE_TRANSPILE_ONLY=true NODE_ENV=test mocha \"test/cluster/**/*.ts\"", 15 | "test": "npm run test:js && npm run test:tsd", 16 | "lint": "eslint --ext .js,.ts ./lib", 17 | "docs": "npx typedoc --logLevel Error --excludeExternals --excludeProtected --excludePrivate --readme none lib/index.ts", 18 | "format": "prettier --write \"{,!(node_modules)/**/}*.{js,ts}\"", 19 | "format-check": "prettier --check \"{,!(node_modules)/**/}*.{js,ts}\"", 20 | "build": "rm -rf built && tsc", 21 | "prepublishOnly": "npm run build", 22 | "start-valkey": "docker run -p 6379:6379 valkey/valkey" 23 | }, 24 | "repository": { 25 | "type": "git", 26 | "url": "git://github.com/valkey-io/iovalkey.git" 27 | }, 28 | "keywords": [ 29 | "valkey", 30 | "redis", 31 | "cluster", 32 | "sentinel", 33 | "pipelining" 34 | ], 35 | "tsd": { 36 | "directory": "test/typing" 37 | }, 38 | "author": "Matteo Collina ", 39 | "license": "MIT", 40 | "dependencies": { 41 | "@iovalkey/commands": "^0.1.0", 42 | "cluster-key-slot": "^1.1.0", 43 | "debug": "^4.3.4", 44 | "denque": "^2.1.0", 45 | "lodash.defaults": "^4.2.0", 46 | "lodash.isarguments": "^3.1.0", 47 | "redis-errors": "^1.2.0", 48 | "redis-parser": "^3.0.0", 49 | "standard-as-callback": "^2.1.0" 50 | }, 51 | "devDependencies": { 52 | "@iovalkey/interface-generator": "^0.1.0", 53 | "@types/chai": "^4.3.0", 54 | "@types/chai-as-promised": "^7.1.5", 55 | "@types/debug": "^4.1.5", 56 | "@types/lodash.defaults": "^4.2.7", 57 | "@types/lodash.isarguments": "^3.1.7", 58 | "@types/mocha": "^9.1.0", 59 | "@types/node": "^14.18.12", 60 | "@types/redis-errors": "^1.2.1", 61 | "@types/sinon": "^10.0.11", 62 | "@typescript-eslint/eslint-plugin": "^5.48.1", 63 | "@typescript-eslint/parser": "^5.48.1", 64 | "chai": "^4.3.6", 65 | "chai-as-promised": "^7.1.1", 66 | "eslint": "^8.31.0", 67 | "eslint-config-prettier": "^8.6.0", 68 | "mocha": "^9.2.1", 69 | "nyc": "^15.1.0", 70 | "prettier": "^2.6.1", 71 | "server-destroy": "^1.0.1", 72 | "sinon": "^13.0.1", 73 | "ts-node": "^10.4.0", 74 | "tsd": "^0.19.1", 75 | "typedoc": "^0.22.18", 76 | "typescript": "^4.6.3", 77 | "uuid": "^9.0.0" 78 | }, 79 | "nyc": { 80 | "reporter": [ 81 | "lcov" 82 | ] 83 | }, 84 | "engines": { 85 | "node": ">=18.12.0" 86 | }, 87 | "mocha": { 88 | "exit": true, 89 | "timeout": 8000, 90 | "recursive": true, 91 | "require": "ts-node/register" 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /resources/medis.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/valkey-io/iovalkey/f75fdc0247172901149cac6f49179f97da28c9b2/resources/medis.png -------------------------------------------------------------------------------- /resources/redis-tryfree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/valkey-io/iovalkey/f75fdc0247172901149cac6f49179f97da28c9b2/resources/redis-tryfree.png -------------------------------------------------------------------------------- /resources/ts-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/valkey-io/iovalkey/f75fdc0247172901149cac6f49179f97da28c9b2/resources/ts-screenshot.png -------------------------------------------------------------------------------- /resources/upstash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/valkey-io/iovalkey/f75fdc0247172901149cac6f49179f97da28c9b2/resources/upstash.png -------------------------------------------------------------------------------- /test/cluster/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM grokzen/redis-cluster 2 | 3 | RUN apt-get --allow-releaseinfo-change update 4 | 5 | RUN apt-get update -y && apt-get install -y redis-server && apt-get install -y curl 6 | RUN touch /etc/apt/apt.conf.d/99verify-peer.conf \ 7 | && echo >>/etc/apt/apt.conf.d/99verify-peer.conf "Acquire { https::Verify-Peer false }" 8 | RUN echo insecure >> $HOME/.curlrc 9 | 10 | RUN curl --insecure -fsSL https://deb.nodesource.com/setup_14.x | bash - 11 | RUN apt-get install -y nodejs 12 | RUN apt-get clean 13 | RUN mkdir /code 14 | WORKDIR /code 15 | ADD package.json package-lock.json ./ 16 | # Install npm dependencies without converting the lockfile version in npm 7, 17 | # and remove temporary files to save space when developing locally. 18 | RUN npm install --no-save && npm cache clean --force 19 | 20 | -------------------------------------------------------------------------------- /test/cluster/docker/main.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euo pipefail 4 | 5 | docker run --rm --name redis-cluster-ioredis-test -e "INITIAL_PORT=30000" -e "IP=0.0.0.0" -p 30000-30005:30000-30005 grokzen/redis-cluster:latest & 6 | trap 'docker stop redis-cluster-ioredis-test' EXIT 7 | 8 | npm install 9 | 10 | sleep 15 11 | 12 | for port in {30000..30005}; do 13 | docker exec redis-cluster-ioredis-test /bin/bash -c "redis-cli -p $port CONFIG SET protected-mode no" 14 | done 15 | 16 | npm run test:js:cluster || npm run test:js:cluster || npm run test:js:cluster 17 | -------------------------------------------------------------------------------- /test/functional/cluster/ClusterSubscriber.ts: -------------------------------------------------------------------------------- 1 | import ConnectionPool from "../../../lib/cluster/ConnectionPool"; 2 | import ClusterSubscriber from "../../../lib/cluster/ClusterSubscriber"; 3 | import { EventEmitter } from "events"; 4 | import MockServer from "../../helpers/mock_server"; 5 | import { expect } from "chai"; 6 | 7 | describe("ClusterSubscriber", () => { 8 | it("cleans up subscribers when selecting a new one", async () => { 9 | const pool = new ConnectionPool({}); 10 | const subscriber = new ClusterSubscriber(pool, new EventEmitter()); 11 | 12 | let rejectSubscribes = false; 13 | const server = new MockServer(30000, (argv) => { 14 | if (rejectSubscribes && argv[0] === "subscribe") { 15 | return new Error("Failed to subscribe"); 16 | } 17 | return "OK"; 18 | }); 19 | 20 | pool.findOrCreate({ host: "127.0.0.1", port: 30000 }); 21 | 22 | subscriber.start(); 23 | await subscriber.getInstance().subscribe("foo"); 24 | rejectSubscribes = true; 25 | 26 | subscriber.start(); 27 | await subscriber.getInstance().echo("hello"); 28 | 29 | subscriber.start(); 30 | await subscriber.getInstance().echo("hello"); 31 | 32 | expect(server.getAllClients()).to.have.lengthOf(1); 33 | subscriber.stop(); 34 | pool.reset([]); 35 | }); 36 | 37 | it("sets correct connection name when connectionName is set", async () => { 38 | const pool = new ConnectionPool({ connectionName: "test" }); 39 | const subscriber = new ClusterSubscriber(pool, new EventEmitter()); 40 | 41 | const clientNames = []; 42 | new MockServer(30000, (argv) => { 43 | if (argv[0] === "client" && argv[1] === "setname") { 44 | clientNames.push(argv[2]); 45 | } 46 | }); 47 | 48 | pool.findOrCreate({ host: "127.0.0.1", port: 30000 }); 49 | 50 | subscriber.start(); 51 | await subscriber.getInstance().subscribe("foo"); 52 | subscriber.stop(); 53 | pool.reset([]); 54 | 55 | expect(clientNames).to.eql(["ioredis-cluster(subscriber):test"]); 56 | }); 57 | 58 | it("sets correct connection name when connectionName is absent", async () => { 59 | const pool = new ConnectionPool({}); 60 | const subscriber = new ClusterSubscriber(pool, new EventEmitter()); 61 | 62 | const clientNames = []; 63 | new MockServer(30000, (argv) => { 64 | if (argv[0] === "client" && argv[1] === "setname") { 65 | clientNames.push(argv[2]); 66 | } 67 | }); 68 | 69 | pool.findOrCreate({ host: "127.0.0.1", port: 30000 }); 70 | 71 | subscriber.start(); 72 | await subscriber.getInstance().subscribe("foo"); 73 | subscriber.stop(); 74 | pool.reset([]); 75 | 76 | expect(clientNames).to.eql(["ioredis-cluster(subscriber)"]); 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /test/functional/cluster/ConnectionPool.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | 3 | import ConnectionPool from "../../../lib/cluster/ConnectionPool"; 4 | 5 | describe("The cluster connection pool", () => { 6 | describe("when not connected", () => { 7 | it("does not throw when fetching a sample node", () => { 8 | expect(new ConnectionPool({}).getSampleInstance("all")).to.be.undefined; 9 | expect(new ConnectionPool({}).getNodes("all")).to.be.eql([]); 10 | }); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /test/functional/cluster/ask.ts: -------------------------------------------------------------------------------- 1 | import MockServer from "../../helpers/mock_server"; 2 | import * as calculateSlot from "cluster-key-slot"; 3 | import { expect } from "chai"; 4 | import { Cluster } from "../../../lib"; 5 | 6 | describe("cluster:ASK", () => { 7 | it("should support ASK", (done) => { 8 | let asked = false; 9 | let times = 0; 10 | const slotTable = [ 11 | [0, 1, ["127.0.0.1", 30001]], 12 | [2, 16383, ["127.0.0.1", 30002]], 13 | ]; 14 | new MockServer(30001, (argv) => { 15 | if (argv[0] === "cluster" && argv[1] === "SLOTS") { 16 | return slotTable; 17 | } 18 | if (argv[0] === "get" && argv[1] === "foo") { 19 | expect(asked).to.eql(true); 20 | } else if (argv[0] === "asking") { 21 | asked = true; 22 | } 23 | }); 24 | new MockServer(30002, (argv) => { 25 | if (argv[0] === "cluster" && argv[1] === "SLOTS") { 26 | return slotTable; 27 | } 28 | if (argv[0] === "get" && argv[1] === "foo") { 29 | if (++times === 2) { 30 | process.nextTick(() => { 31 | cluster.disconnect(); 32 | done(); 33 | }); 34 | } else { 35 | return new Error("ASK " + calculateSlot("foo") + " 127.0.0.1:30001"); 36 | } 37 | } 38 | }); 39 | 40 | var cluster = new Cluster([{ host: "127.0.0.1", port: "30001" }], { 41 | lazyConnect: false, 42 | }); 43 | cluster.get("foo", () => { 44 | cluster.get("foo"); 45 | }); 46 | }); 47 | 48 | it("should be able to redirect a command to a unknown node", (done) => { 49 | let asked = false; 50 | const slotTable = [[0, 16383, ["127.0.0.1", 30002]]]; 51 | new MockServer(30001, (argv) => { 52 | if (argv[0] === "get" && argv[1] === "foo") { 53 | expect(asked).to.eql(true); 54 | return "bar"; 55 | } else if (argv[0] === "asking") { 56 | asked = true; 57 | } 58 | }); 59 | new MockServer(30002, (argv) => { 60 | if (argv[0] === "cluster" && argv[1] === "SLOTS") { 61 | return slotTable; 62 | } 63 | if (argv[0] === "get" && argv[1] === "foo") { 64 | return new Error("ASK " + calculateSlot("foo") + " 127.0.0.1:30001"); 65 | } 66 | }); 67 | 68 | const cluster = new Cluster([{ host: "127.0.0.1", port: "30002" }]); 69 | cluster.get("foo", function (err, res) { 70 | expect(res).to.eql("bar"); 71 | cluster.disconnect(); 72 | done(); 73 | }); 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /test/functional/cluster/clusterdown.ts: -------------------------------------------------------------------------------- 1 | import MockServer from "../../helpers/mock_server"; 2 | import { expect } from "chai"; 3 | import { Cluster } from "../../../lib"; 4 | 5 | describe("cluster:CLUSTERDOWN", () => { 6 | it("should redirect the command to a random node", (done) => { 7 | const slotTable = [ 8 | [0, 1, ["127.0.0.1", 30001]], 9 | [2, 16383, ["127.0.0.1", 30002]], 10 | ]; 11 | new MockServer(30001, (argv) => { 12 | if (argv[0] === "cluster" && argv[1] === "SLOTS") { 13 | return slotTable; 14 | } 15 | if (argv[0] === "get" && argv[1] === "foo") { 16 | return "bar"; 17 | } 18 | }); 19 | new MockServer(30002, (argv) => { 20 | if (argv[0] === "cluster" && argv[1] === "SLOTS") { 21 | return slotTable; 22 | } 23 | if (argv[0] === "get" && argv[1] === "foo") { 24 | return new Error("CLUSTERDOWN"); 25 | } 26 | }); 27 | 28 | const cluster = new Cluster([{ host: "127.0.0.1", port: "30001" }], { 29 | lazyConnect: false, 30 | retryDelayOnClusterDown: 1, 31 | }); 32 | cluster.get("foo", function (_, res) { 33 | expect(res).to.eql("bar"); 34 | cluster.disconnect(); 35 | done(); 36 | }); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /test/functional/cluster/disconnection.ts: -------------------------------------------------------------------------------- 1 | import * as sinon from "sinon"; 2 | import { expect } from "chai"; 3 | import { Cluster } from "../../../lib"; 4 | import MockServer from "../../helpers/mock_server"; 5 | 6 | describe("disconnection", () => { 7 | afterEach(() => { 8 | sinon.restore(); 9 | }); 10 | 11 | it("should clear all timers on disconnect", (done) => { 12 | const slotTable = [[0, 16383, ["127.0.0.1", 30000]]]; 13 | const server = new MockServer(30000, (argv) => { 14 | if (argv[0] === "cluster" && argv[1] === "SLOTS") { 15 | return slotTable; 16 | } 17 | }); 18 | 19 | const setIntervalCalls = sinon.spy(global, "setInterval"); 20 | const clearIntervalCalls = sinon.spy(global, "clearInterval"); 21 | 22 | const cluster = new Cluster([{ host: "127.0.0.1", port: "30000" }]); 23 | cluster.on("connect", () => { 24 | cluster.disconnect(); 25 | }); 26 | 27 | cluster.on("end", () => { 28 | setTimeout(() => { 29 | // wait for disconnect with refresher. 30 | expect(setIntervalCalls.callCount).to.equal( 31 | clearIntervalCalls.callCount 32 | ); 33 | server.disconnect(); 34 | done(); 35 | }, 500); 36 | }); 37 | }); 38 | 39 | it("should clear all timers on server exits", (done) => { 40 | const server = new MockServer(30000); 41 | 42 | const setIntervalCalls = sinon.spy(global, "setInterval"); 43 | const clearIntervalCalls = sinon.spy(global, "clearInterval"); 44 | 45 | const cluster = new Cluster([{ host: "127.0.0.1", port: "30000" }], { 46 | clusterRetryStrategy: null, 47 | }); 48 | cluster.on("end", () => { 49 | expect(setIntervalCalls.callCount).to.equal(clearIntervalCalls.callCount); 50 | done(); 51 | }); 52 | 53 | server.disconnect(); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /test/functional/cluster/dnsLookup.ts: -------------------------------------------------------------------------------- 1 | import MockServer from "../../helpers/mock_server"; 2 | import { Cluster } from "../../../lib"; 3 | import { expect } from "chai"; 4 | 5 | describe("cluster:dnsLookup", () => { 6 | it("resolve hostnames to IPs", (done) => { 7 | const slotTable = [ 8 | [0, 1000, ["127.0.0.1", 30001]], 9 | [1001, 16383, ["127.0.0.1", 30002]], 10 | ]; 11 | new MockServer(30001, () => {}, slotTable); 12 | new MockServer(30002, () => {}, slotTable); 13 | 14 | const cluster = new Cluster([{ host: "localhost", port: "30001" }]); 15 | cluster.on("ready", () => { 16 | const nodes = cluster.nodes("master"); 17 | expect(nodes.length).to.eql(2); 18 | expect(nodes[0].options.host).to.eql("127.0.0.1"); 19 | expect(nodes[1].options.host).to.eql("127.0.0.1"); 20 | cluster.disconnect(); 21 | done(); 22 | }); 23 | }); 24 | 25 | it("support customize dnsLookup function", (done) => { 26 | let dnsLookupCalledCount = 0; 27 | const slotTable = [ 28 | [0, 1000, ["127.0.0.1", 30001]], 29 | [1001, 16383, ["127.0.0.1", 30002]], 30 | ]; 31 | new MockServer(30001, (argv, c) => {}, slotTable); 32 | new MockServer(30002, (argv, c) => {}, slotTable); 33 | 34 | const cluster = new Cluster([{ host: "a.com", port: "30001" }], { 35 | dnsLookup(hostname, callback) { 36 | dnsLookupCalledCount += 1; 37 | if (hostname === "a.com") { 38 | callback(null, "127.0.0.1"); 39 | } else { 40 | callback(new Error("Unknown hostname")); 41 | } 42 | }, 43 | }); 44 | cluster.on("ready", () => { 45 | const nodes = cluster.nodes("master"); 46 | expect(nodes.length).to.eql(2); 47 | expect(nodes[0].options.host).to.eql("127.0.0.1"); 48 | expect(nodes[1].options.host).to.eql("127.0.0.1"); 49 | expect(dnsLookupCalledCount).to.eql(1); 50 | cluster.disconnect(); 51 | done(); 52 | }); 53 | }); 54 | 55 | it("reconnects when dns lookup fails", (done) => { 56 | const slotTable = [ 57 | [0, 1000, ["127.0.0.1", 30001]], 58 | [1001, 16383, ["127.0.0.1", 30002]], 59 | ]; 60 | new MockServer(30001, (argv, c) => {}, slotTable); 61 | new MockServer(30002, (argv, c) => {}, slotTable); 62 | 63 | let retried = false; 64 | const cluster = new Cluster([{ host: "localhost", port: "30001" }], { 65 | dnsLookup(_, callback) { 66 | if (retried) { 67 | callback(null, "127.0.0.1"); 68 | } else { 69 | callback(new Error("Random Exception")); 70 | } 71 | }, 72 | clusterRetryStrategy: function (_, reason) { 73 | expect(reason.message).to.eql("Random Exception"); 74 | expect(retried).to.eql(false); 75 | retried = true; 76 | return 0; 77 | }, 78 | }); 79 | cluster.on("ready", () => { 80 | cluster.disconnect(); 81 | done(); 82 | }); 83 | }); 84 | 85 | it("reconnects when dns lookup thrown an error", (done) => { 86 | const slotTable = [ 87 | [0, 1000, ["127.0.0.1", 30001]], 88 | [1001, 16383, ["127.0.0.1", 30002]], 89 | ]; 90 | new MockServer(30001, (argv, c) => {}, slotTable); 91 | new MockServer(30002, (argv, c) => {}, slotTable); 92 | 93 | let retried = false; 94 | const cluster = new Cluster([{ host: "localhost", port: "30001" }], { 95 | dnsLookup(_, callback) { 96 | if (retried) { 97 | callback(null, "127.0.0.1"); 98 | } else { 99 | throw new Error("Random Exception"); 100 | } 101 | }, 102 | clusterRetryStrategy: function (_, reason) { 103 | expect(reason.message).to.eql("Random Exception"); 104 | expect(retried).to.eql(false); 105 | retried = true; 106 | return 0; 107 | }, 108 | }); 109 | cluster.on("ready", () => { 110 | cluster.disconnect(); 111 | done(); 112 | }); 113 | }); 114 | }); 115 | -------------------------------------------------------------------------------- /test/functional/cluster/duplicate.ts: -------------------------------------------------------------------------------- 1 | import MockServer from "../../helpers/mock_server"; 2 | import { Cluster } from "../../../lib"; 3 | import { expect } from "chai"; 4 | 5 | describe("cluster:duplicate", () => { 6 | it("clone the options", (done) => { 7 | const node = new MockServer(30001); 8 | const cluster = new Cluster([]); 9 | const duplicatedCluster = cluster.duplicate([ 10 | { host: "127.0.0.1", port: "30001" }, 11 | ]); 12 | 13 | node.once("connect", () => { 14 | expect(duplicatedCluster.nodes()).to.have.lengthOf(1); 15 | expect(duplicatedCluster.nodes()[0].options.port).to.eql(30001); 16 | cluster.disconnect(); 17 | duplicatedCluster.disconnect(); 18 | done(); 19 | }); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /test/functional/cluster/maxRedirections.ts: -------------------------------------------------------------------------------- 1 | import * as calculateSlot from "cluster-key-slot"; 2 | import MockServer from "../../helpers/mock_server"; 3 | import { expect } from "chai"; 4 | import { Cluster } from "../../../lib"; 5 | 6 | describe("cluster:maxRedirections", () => { 7 | it("should return error when reached max redirection", (done) => { 8 | let redirectTimes = 0; 9 | const argvHandler = function (argv) { 10 | if (argv[0] === "cluster" && argv[1] === "SLOTS") { 11 | return [ 12 | [0, 1, ["127.0.0.1", 30001]], 13 | [2, 16383, ["127.0.0.1", 30002]], 14 | ]; 15 | } else if (argv[0] === "get" && argv[1] === "foo") { 16 | redirectTimes += 1; 17 | return new Error("ASK " + calculateSlot("foo") + " 127.0.0.1:30001"); 18 | } 19 | }; 20 | new MockServer(30001, argvHandler); 21 | new MockServer(30002, argvHandler); 22 | 23 | const cluster = new Cluster([{ host: "127.0.0.1", port: "30001" }], { 24 | maxRedirections: 5, 25 | }); 26 | cluster.get("foo", function (err) { 27 | expect(redirectTimes).to.eql(6); 28 | expect(err.message).to.match(/Too many Cluster redirections/); 29 | cluster.disconnect(); 30 | done(); 31 | }); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /test/functional/cluster/pub_sub.ts: -------------------------------------------------------------------------------- 1 | import MockServer, { getConnectionName } from "../../helpers/mock_server"; 2 | import { expect } from "chai"; 3 | import { Cluster } from "../../../lib"; 4 | import * as sinon from "sinon"; 5 | import Redis from "../../../lib/Redis"; 6 | import { noop } from "../../../lib/utils"; 7 | 8 | describe("cluster:pub/sub", function () { 9 | it("should receive messages", (done) => { 10 | const handler = function (argv) { 11 | if (argv[0] === "cluster" && argv[1] === "SLOTS") { 12 | return [ 13 | [0, 1, ["127.0.0.1", 30001]], 14 | [2, 16383, ["127.0.0.1", 30002]], 15 | ]; 16 | } 17 | }; 18 | const node1 = new MockServer(30001, handler); 19 | new MockServer(30002, handler); 20 | 21 | const options = [{ host: "127.0.0.1", port: "30001" }]; 22 | const sub = new Cluster(options); 23 | 24 | sub.subscribe("test cluster", function () { 25 | node1.write(node1.findClientByName("ioredis-cluster(subscriber)"), [ 26 | "message", 27 | "test channel", 28 | "hi", 29 | ]); 30 | }); 31 | sub.on("message", function (channel, message) { 32 | expect(channel).to.eql("test channel"); 33 | expect(message).to.eql("hi"); 34 | sub.disconnect(); 35 | done(); 36 | }); 37 | }); 38 | 39 | it("should works when sending regular commands", (done) => { 40 | const handler = function (argv) { 41 | if (argv[0] === "cluster" && argv[1] === "SLOTS") { 42 | return [[0, 16383, ["127.0.0.1", 30001]]]; 43 | } 44 | }; 45 | new MockServer(30001, handler); 46 | 47 | const sub = new Cluster([{ port: "30001" }]); 48 | 49 | sub.subscribe("test cluster", function () { 50 | sub.set("foo", "bar").then((res) => { 51 | expect(res).to.eql("OK"); 52 | sub.disconnect(); 53 | done(); 54 | }); 55 | }); 56 | }); 57 | 58 | it("supports password", (done) => { 59 | const handler = function (argv, c) { 60 | if (argv[0] === "auth") { 61 | c.password = argv[1]; 62 | return; 63 | } 64 | if (argv[0] === "subscribe") { 65 | expect(c.password).to.eql("abc"); 66 | expect(getConnectionName(c)).to.eql("ioredis-cluster(subscriber)"); 67 | } 68 | if (argv[0] === "cluster" && argv[1] === "SLOTS") { 69 | return [[0, 16383, ["127.0.0.1", 30001]]]; 70 | } 71 | }; 72 | new MockServer(30001, handler); 73 | 74 | const sub = new Cluster([{ port: "30001", password: "abc" }]); 75 | 76 | sub.subscribe("test cluster", function () { 77 | sub.disconnect(); 78 | done(); 79 | }); 80 | }); 81 | 82 | it("should re-subscribe after reconnection", (done) => { 83 | new MockServer(30001, function (argv) { 84 | if (argv[0] === "cluster" && argv[1] === "SLOTS") { 85 | return [[0, 16383, ["127.0.0.1", 30001]]]; 86 | } else if (argv[0] === "subscribe" || argv[0] === "psubscribe") { 87 | return [argv[0], argv[1]]; 88 | } 89 | }); 90 | const client = new Cluster([{ host: "127.0.0.1", port: "30001" }]); 91 | 92 | client.subscribe("test cluster", function () { 93 | const stub = sinon 94 | .stub(Redis.prototype, "subscribe") 95 | .callsFake((channels) => { 96 | expect(channels).to.eql(["test cluster"]); 97 | stub.restore(); 98 | client.disconnect(); 99 | done(); 100 | return Redis.prototype.subscribe.apply(this, arguments); 101 | }); 102 | client.once("end", function () { 103 | client.connect().catch(noop); 104 | }); 105 | client.disconnect(); 106 | }); 107 | }); 108 | 109 | it("should re-psubscribe after reconnection", (done) => { 110 | new MockServer(30001, function (argv) { 111 | if (argv[0] === "cluster" && argv[1] === "SLOTS") { 112 | return [[0, 16383, ["127.0.0.1", 30001]]]; 113 | } else if (argv[0] === "subscribe" || argv[0] === "psubscribe") { 114 | return [argv[0], argv[1]]; 115 | } 116 | }); 117 | const client = new Cluster([{ host: "127.0.0.1", port: "30001" }]); 118 | 119 | client.psubscribe("test?", function () { 120 | const stub = sinon 121 | .stub(Redis.prototype, "psubscribe") 122 | .callsFake((channels) => { 123 | expect(channels).to.eql(["test?"]); 124 | stub.restore(); 125 | client.disconnect(); 126 | done(); 127 | return Redis.prototype.psubscribe.apply(this, arguments); 128 | }); 129 | client.once("end", function () { 130 | client.connect().catch(noop); 131 | }); 132 | client.disconnect(); 133 | }); 134 | }); 135 | }); 136 | -------------------------------------------------------------------------------- /test/functional/cluster/quit.ts: -------------------------------------------------------------------------------- 1 | import MockServer from "../../helpers/mock_server"; 2 | import { expect } from "chai"; 3 | import { Cluster } from "../../../lib"; 4 | 5 | describe("cluster:quit", () => { 6 | it("quit successfully when server is disconnecting", (done) => { 7 | const slotTable = [ 8 | [0, 1000, ["127.0.0.1", 30001]], 9 | [1001, 16383, ["127.0.0.1", 30002]], 10 | ]; 11 | const server = new MockServer( 12 | 30001, 13 | (argv, c) => { 14 | if (argv[0] === "quit") { 15 | c.destroy(); 16 | } 17 | }, 18 | slotTable 19 | ); 20 | new MockServer( 21 | 30002, 22 | (argv, c) => { 23 | if (argv[0] === "quit") { 24 | c.destroy(); 25 | } 26 | }, 27 | slotTable 28 | ); 29 | 30 | const cluster = new Cluster([{ host: "127.0.0.1", port: "30001" }]); 31 | cluster.on("ready", () => { 32 | server.disconnect(); 33 | cluster.quit((err, res) => { 34 | expect(err).to.eql(null); 35 | expect(res).to.eql("OK"); 36 | cluster.disconnect(); 37 | done(); 38 | }); 39 | }); 40 | }); 41 | 42 | it("failed when quit returns error", (done) => { 43 | const ERROR_MESSAGE = "quit random error"; 44 | const slotTable = [ 45 | [0, 16381, ["127.0.0.1", 30001]], 46 | [16382, 16383, ["127.0.0.1", 30002]], 47 | ]; 48 | new MockServer( 49 | 30001, 50 | function (argv, c) { 51 | if (argv[0] === "quit") { 52 | return new Error(ERROR_MESSAGE); 53 | } 54 | }, 55 | slotTable 56 | ); 57 | new MockServer( 58 | 30002, 59 | function (argv, c) { 60 | if (argv[0] === "quit") { 61 | c.destroy(); 62 | } 63 | }, 64 | slotTable 65 | ); 66 | 67 | const cluster = new Cluster([{ host: "127.0.0.1", port: "30001" }]); 68 | cluster.get("foo", () => { 69 | cluster.quit((err) => { 70 | expect(err.message).to.eql(ERROR_MESSAGE); 71 | cluster.disconnect(); 72 | done(); 73 | }); 74 | }); 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /test/functional/cluster/resolveSrv.ts: -------------------------------------------------------------------------------- 1 | import MockServer from "../../helpers/mock_server"; 2 | import { Cluster } from "../../../lib"; 3 | import { expect } from "chai"; 4 | 5 | describe("cluster:resolveSrv", () => { 6 | it("support customize resolveSrv function", (done) => { 7 | let resolveSrvCalledCount = 0; 8 | new MockServer(30001, (argv, c) => {}, [[0, 1000, ["127.0.0.1", 30001]]]); 9 | 10 | const cluster = new Cluster([{ host: "a.com" }], { 11 | useSRVRecords: true, 12 | resolveSrv(hostname, callback) { 13 | resolveSrvCalledCount++; 14 | if (hostname === "a.com") { 15 | callback(null, [ 16 | { 17 | priority: 1, 18 | weight: 1, 19 | port: 30001, 20 | name: "127.0.0.1", 21 | }, 22 | ]); 23 | } else { 24 | callback(new Error("Unknown hostname")); 25 | } 26 | }, 27 | }); 28 | cluster.on("ready", () => { 29 | const nodes = cluster.nodes("master"); 30 | expect(nodes.length).to.eql(1); 31 | expect(nodes[0].options.host).to.eql("127.0.0.1"); 32 | expect(resolveSrvCalledCount).to.eql(1); 33 | cluster.disconnect(); 34 | done(); 35 | }); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /test/functional/cluster/scripting.ts: -------------------------------------------------------------------------------- 1 | import MockServer from "../../helpers/mock_server"; 2 | import { expect } from "chai"; 3 | import { Cluster } from "../../../lib"; 4 | 5 | describe("cluster:scripting", () => { 6 | it("should throw when not all keys in a pipeline command belong to the same slot", async () => { 7 | const lua = "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}"; 8 | const handler = (argv) => { 9 | if (argv[0] === "cluster" && argv[1] === "SLOTS") { 10 | return [ 11 | [0, 12181, ["127.0.0.1", 30001]], 12 | [12182, 16383, ["127.0.0.1", 30002]], 13 | ]; 14 | } 15 | if (argv[0] === "eval" && argv[1] === lua && argv[2] === "2") { 16 | return argv.slice(3); 17 | } 18 | }; 19 | new MockServer(30001, handler); 20 | new MockServer(30002, handler); 21 | 22 | const cluster = new Cluster([{ host: "127.0.0.1", port: "30001" }], { 23 | scripts: { test: { lua, numberOfKeys: 2 }, testDynamic: { lua } }, 24 | }); 25 | 26 | // @ts-expect-error 27 | expect(await cluster.test("{foo}1", "{foo}2", "argv1", "argv2")).to.eql([ 28 | "{foo}1", 29 | "{foo}2", 30 | "argv1", 31 | "argv2", 32 | ]); 33 | 34 | expect( 35 | // @ts-expect-error 36 | await cluster.testDynamic(2, "{foo}1", "{foo}2", "argv1", "argv2") 37 | ).to.eql(["{foo}1", "{foo}2", "argv1", "argv2"]); 38 | 39 | cluster.disconnect(); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /test/functional/cluster/spub_ssub.ts: -------------------------------------------------------------------------------- 1 | import MockServer, { getConnectionName } from "../../helpers/mock_server"; 2 | import { expect } from "chai"; 3 | import { Cluster } from "../../../lib"; 4 | import * as sinon from "sinon"; 5 | import Redis from "../../../lib/Redis"; 6 | import { noop } from "../../../lib/utils"; 7 | 8 | describe("cluster:spub/ssub", function () { 9 | it("should receive messages", (done) => { 10 | const handler = function (argv) { 11 | if (argv[0] === "cluster" && argv[1] === "SLOTS") { 12 | return [ 13 | [0, 1, ["127.0.0.1", 30001]], 14 | [2, 16383, ["127.0.0.1", 30002]], 15 | ]; 16 | } 17 | }; 18 | const node1 = new MockServer(30001, handler); 19 | new MockServer(30002, handler); 20 | 21 | const options = [{ host: "127.0.0.1", port: "30001" }]; 22 | const ssub = new Cluster(options); 23 | 24 | ssub.ssubscribe("test cluster", function () { 25 | node1.write(node1.findClientByName("ioredis-cluster(subscriber)"), [ 26 | "smessage", 27 | "test shard channel", 28 | "hi", 29 | ]); 30 | }); 31 | ssub.on("smessage", function (channel, message) { 32 | expect(channel).to.eql("test shard channel"); 33 | expect(message).to.eql("hi"); 34 | ssub.disconnect(); 35 | done(); 36 | }); 37 | }); 38 | 39 | it("should works when sending regular commands", (done) => { 40 | const handler = function (argv) { 41 | if (argv[0] === "cluster" && argv[1] === "SLOTS") { 42 | return [[0, 16383, ["127.0.0.1", 30001]]]; 43 | } 44 | }; 45 | new MockServer(30001, handler); 46 | 47 | const ssub = new Cluster([{ port: "30001" }]); 48 | 49 | ssub.ssubscribe("test cluster", function () { 50 | ssub.set("foo", "bar").then((res) => { 51 | expect(res).to.eql("OK"); 52 | ssub.disconnect(); 53 | done(); 54 | }); 55 | }); 56 | }); 57 | 58 | it("supports password", (done) => { 59 | const handler = function (argv, c) { 60 | if (argv[0] === "auth") { 61 | c.password = argv[1]; 62 | return; 63 | } 64 | if (argv[0] === "ssubscribe") { 65 | expect(c.password).to.eql("abc"); 66 | expect(getConnectionName(c)).to.eql("ioredis-cluster(subscriber)"); 67 | } 68 | if (argv[0] === "cluster" && argv[1] === "SLOTS") { 69 | return [[0, 16383, ["127.0.0.1", 30001]]]; 70 | } 71 | }; 72 | new MockServer(30001, handler); 73 | 74 | const ssub = new Redis.Cluster([{ port: "30001", password: "abc" }]); 75 | 76 | ssub.ssubscribe("test cluster", function () { 77 | ssub.disconnect(); 78 | done(); 79 | }); 80 | }); 81 | 82 | it("should re-ssubscribe after reconnection", (done) => { 83 | new MockServer(30001, function (argv) { 84 | if (argv[0] === "cluster" && argv[1] === "SLOTS") { 85 | return [[0, 16383, ["127.0.0.1", 30001]]]; 86 | } else if (argv[0] === "ssubscribe" || argv[0] === "psubscribe") { 87 | return [argv[0], argv[1]]; 88 | } 89 | }); 90 | const client = new Cluster([{ host: "127.0.0.1", port: "30001" }]); 91 | 92 | client.ssubscribe("test cluster", function () { 93 | const stub = sinon 94 | .stub(Redis.prototype, "ssubscribe") 95 | .callsFake((channels) => { 96 | expect(channels).to.eql(["test cluster"]); 97 | stub.restore(); 98 | client.disconnect(); 99 | done(); 100 | return Redis.prototype.ssubscribe.apply(this, arguments); 101 | }); 102 | client.once("end", function () { 103 | client.connect().catch(noop); 104 | }); 105 | client.disconnect(); 106 | }); 107 | }); 108 | }); 109 | -------------------------------------------------------------------------------- /test/functional/cluster/tls.ts: -------------------------------------------------------------------------------- 1 | import MockServer from "../../helpers/mock_server"; 2 | import { expect } from "chai"; 3 | import { Cluster } from "../../../lib"; 4 | import * as sinon from "sinon"; 5 | import * as tls from "tls"; 6 | import * as net from "net"; 7 | 8 | describe("cluster:tls option", () => { 9 | it("supports tls", (done) => { 10 | const slotTable = [ 11 | [0, 5460, ["127.0.0.1", 30001]], 12 | [5461, 10922, ["127.0.0.1", 30002]], 13 | [10923, 16383, ["127.0.0.1", 30003]], 14 | ]; 15 | const argvHandler = function (argv) { 16 | if (argv[0] === "cluster" && argv[1] === "SLOTS") { 17 | return slotTable; 18 | } 19 | }; 20 | 21 | new MockServer(30001, argvHandler); 22 | new MockServer(30002, argvHandler); 23 | new MockServer(30003, argvHandler); 24 | 25 | // @ts-expect-error 26 | const stub = sinon.stub(tls, "connect").callsFake((op) => { 27 | // @ts-expect-error 28 | expect(op.ca).to.eql("123"); 29 | // @ts-expect-error 30 | expect(op.port).to.be.oneOf([30001, 30003, 30003]); 31 | const stream = net.createConnection(op); 32 | stream.on("connect", (data) => { 33 | stream.emit("secureConnect", data); 34 | }); 35 | return stream; 36 | }); 37 | 38 | const cluster = new Cluster( 39 | [ 40 | { host: "127.0.0.1", port: "30001" }, 41 | { host: "127.0.0.1", port: "30002" }, 42 | { host: "127.0.0.1", port: "30003" }, 43 | ], 44 | { 45 | redisOptions: { tls: { ca: "123" } }, 46 | } 47 | ); 48 | 49 | cluster.on("ready", () => { 50 | expect(cluster.subscriber.subscriber.options.tls).to.deep.equal({ 51 | ca: "123", 52 | }); 53 | 54 | cluster.disconnect(); 55 | stub.restore(); 56 | cluster.on("end", () => done()); 57 | }); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /test/functional/cluster/transaction.ts: -------------------------------------------------------------------------------- 1 | import * as calculateSlot from "cluster-key-slot"; 2 | import MockServer from "../../helpers/mock_server"; 3 | import { expect } from "chai"; 4 | import { Cluster } from "../../../lib"; 5 | 6 | describe("cluster:transaction", () => { 7 | it("should auto redirect commands on MOVED", (done) => { 8 | let moved = false; 9 | const slotTable = [ 10 | [0, 12181, ["127.0.0.1", 30001]], 11 | [12182, 16383, ["127.0.0.1", 30002]], 12 | ]; 13 | new MockServer(30001, (argv) => { 14 | if (argv[0] === "cluster" && argv[1] === "SLOTS") { 15 | return slotTable; 16 | } 17 | if (argv[1] === "foo") { 18 | return "QUEUED"; 19 | } 20 | if (argv[0] === "exec") { 21 | expect(moved).to.eql(true); 22 | return ["bar", "OK"]; 23 | } 24 | }); 25 | new MockServer(30002, (argv) => { 26 | if (argv[0] === "cluster" && argv[1] === "SLOTS") { 27 | return slotTable; 28 | } 29 | if (argv[0] === "get" && argv[1] === "foo") { 30 | moved = true; 31 | return new Error("MOVED " + calculateSlot("foo") + " 127.0.0.1:30001"); 32 | } 33 | if (argv[0] === "exec") { 34 | return new Error( 35 | "EXECABORT Transaction discarded because of previous errors." 36 | ); 37 | } 38 | }); 39 | 40 | const cluster = new Cluster([{ host: "127.0.0.1", port: "30001" }]); 41 | 42 | cluster 43 | .multi() 44 | .get("foo") 45 | .set("foo", "bar") 46 | .exec(function (err, result) { 47 | expect(err).to.eql(null); 48 | expect(result[0]).to.eql([null, "bar"]); 49 | expect(result[1]).to.eql([null, "OK"]); 50 | cluster.disconnect(); 51 | done(); 52 | }); 53 | }); 54 | 55 | it("should auto redirect commands on ASK", (done) => { 56 | let asked = false; 57 | const slotTable = [ 58 | [0, 12181, ["127.0.0.1", 30001]], 59 | [12182, 16383, ["127.0.0.1", 30002]], 60 | ]; 61 | new MockServer(30001, (argv) => { 62 | if (argv[0] === "cluster" && argv[1] === "SLOTS") { 63 | return slotTable; 64 | } 65 | if (argv[0] === "asking") { 66 | asked = true; 67 | } 68 | if (argv[0] === "multi") { 69 | expect(asked).to.eql(true); 70 | } 71 | if (argv[0] === "get" && argv[1] === "foo") { 72 | expect(asked).to.eql(false); 73 | return "bar"; 74 | } 75 | if (argv[0] === "exec") { 76 | expect(asked).to.eql(false); 77 | return ["bar", "OK"]; 78 | } 79 | if (argv[0] !== "asking") { 80 | asked = false; 81 | } 82 | }); 83 | new MockServer(30002, (argv) => { 84 | if (argv[0] === "cluster" && argv[1] === "SLOTS") { 85 | return slotTable; 86 | } 87 | if (argv[0] === "get" && argv[1] === "foo") { 88 | return new Error("ASK " + calculateSlot("foo") + " 127.0.0.1:30001"); 89 | } 90 | if (argv[0] === "exec") { 91 | return new Error( 92 | "EXECABORT Transaction discarded because of previous errors." 93 | ); 94 | } 95 | }); 96 | 97 | const cluster = new Cluster([{ host: "127.0.0.1", port: "30001" }]); 98 | cluster 99 | .multi() 100 | .get("foo") 101 | .set("foo", "bar") 102 | .exec(function (err, result) { 103 | expect(err).to.eql(null); 104 | expect(result[0]).to.eql([null, "bar"]); 105 | expect(result[1]).to.eql([null, "OK"]); 106 | cluster.disconnect(); 107 | done(); 108 | }); 109 | }); 110 | 111 | it("should not print unhandled warnings", (done) => { 112 | const errorMessage = "Connection is closed."; 113 | const slotTable = [[0, 16383, ["127.0.0.1", 30001]]]; 114 | new MockServer( 115 | 30001, 116 | function (argv) { 117 | if (argv[0] === "exec" || argv[1] === "foo") { 118 | return new Error(errorMessage); 119 | } 120 | }, 121 | slotTable 122 | ); 123 | 124 | const cluster = new Cluster([{ host: "127.0.0.1", port: "30001" }], { 125 | maxRedirections: 3, 126 | }); 127 | 128 | let isDoneCalled = false; 129 | const wrapDone = function (error?: Error) { 130 | if (isDoneCalled) { 131 | return; 132 | } 133 | isDoneCalled = true; 134 | process.removeAllListeners("unhandledRejection"); 135 | done(error); 136 | }; 137 | 138 | process.on("unhandledRejection", (err) => { 139 | wrapDone(new Error("got unhandledRejection: " + (err as Error).message)); 140 | }); 141 | cluster 142 | .multi() 143 | .get("foo") 144 | .set("foo", "bar") 145 | .exec(function (err) { 146 | expect(err).to.have.property("message", errorMessage); 147 | cluster.on("end", () => { 148 | // Wait for the end event to ensure the transaction 149 | // promise has been resolved. 150 | wrapDone(); 151 | }); 152 | cluster.disconnect(); 153 | }); 154 | }); 155 | }); 156 | -------------------------------------------------------------------------------- /test/functional/cluster/tryagain.ts: -------------------------------------------------------------------------------- 1 | import MockServer from "../../helpers/mock_server"; 2 | import { Cluster } from "../../../lib"; 3 | 4 | describe("cluster:TRYAGAIN", () => { 5 | it("should retry the command", (done) => { 6 | let cluster; 7 | let times = 0; 8 | const slotTable = [[0, 16383, ["127.0.0.1", 30001]]]; 9 | new MockServer(30001, (argv) => { 10 | if (argv[0] === "cluster" && argv[1] === "SLOTS") { 11 | return slotTable; 12 | } 13 | if (argv[0] === "get" && argv[1] === "foo") { 14 | if (times++ === 1) { 15 | process.nextTick(() => { 16 | cluster.disconnect(); 17 | done(); 18 | }); 19 | } else { 20 | return new Error( 21 | "TRYAGAIN Multiple keys request during rehashing of slot" 22 | ); 23 | } 24 | } 25 | }); 26 | 27 | cluster = new Cluster([{ host: "127.0.0.1", port: "30001" }], { 28 | retryDelayOnTryAgain: 1, 29 | }); 30 | cluster.get("foo"); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /test/functional/commandTimeout.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import * as sinon from "sinon"; 3 | import Redis from "../../lib/Redis"; 4 | import MockServer from "../helpers/mock_server"; 5 | 6 | describe("commandTimeout", () => { 7 | it("rejects if command timed out", (done) => { 8 | const server = new MockServer(30001, (argv, socket, flags) => { 9 | if (argv[0] === "hget") { 10 | flags.hang = true; 11 | return; 12 | } 13 | }); 14 | 15 | const redis = new Redis({ port: 30001, commandTimeout: 1000 }); 16 | const clock = sinon.useFakeTimers(); 17 | redis.hget("foo", (err) => { 18 | expect(err.message).to.eql("Command timed out"); 19 | clock.restore(); 20 | redis.disconnect(); 21 | server.disconnect(() => done()); 22 | }); 23 | clock.tick(1000); 24 | }); 25 | 26 | it("does not leak timers for commands in offline queue", async () => { 27 | const server = new MockServer(30001); 28 | 29 | const redis = new Redis({ port: 30001, commandTimeout: 1000 }); 30 | const clock = sinon.useFakeTimers(); 31 | await redis.hget("foo"); 32 | expect(clock.countTimers()).to.eql(0); 33 | clock.restore(); 34 | redis.disconnect(); 35 | await server.disconnectPromise(); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /test/functional/disconnection.ts: -------------------------------------------------------------------------------- 1 | import Redis from "../../lib/Redis"; 2 | import * as sinon from "sinon"; 3 | import { expect } from "chai"; 4 | import MockServer from "../helpers/mock_server"; 5 | 6 | describe("disconnection", () => { 7 | afterEach(() => { 8 | sinon.restore(); 9 | }); 10 | 11 | it("should clear all timers on disconnect", (done) => { 12 | const server = new MockServer(30000); 13 | 14 | const setIntervalCalls = sinon.spy(global, "setInterval"); 15 | const clearIntervalCalls = sinon.spy(global, "clearInterval"); 16 | 17 | const redis = new Redis({}); 18 | redis.on("connect", () => { 19 | redis.disconnect(); 20 | }); 21 | 22 | redis.on("end", () => { 23 | expect(setIntervalCalls.callCount).to.equal(clearIntervalCalls.callCount); 24 | server.disconnect(); 25 | done(); 26 | }); 27 | }); 28 | 29 | it("should clear all timers on server exits", (done) => { 30 | const server = new MockServer(30000); 31 | 32 | const setIntervalCalls = sinon.spy(global, "setInterval"); 33 | const clearIntervalCalls = sinon.spy(global, "clearInterval"); 34 | 35 | const redis = new Redis({ 36 | port: 30000, 37 | retryStrategy: null, 38 | }); 39 | redis.on("end", () => { 40 | expect(setIntervalCalls.callCount).to.equal(clearIntervalCalls.callCount); 41 | done(); 42 | }); 43 | 44 | server.disconnect(); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /test/functional/duplicate.ts: -------------------------------------------------------------------------------- 1 | import Redis from "../../lib/Redis"; 2 | import { expect } from "chai"; 3 | 4 | describe("duplicate", () => { 5 | it("clone the options", () => { 6 | const redis = new Redis(); 7 | const duplicatedRedis = redis.duplicate(); 8 | redis.options.port = 1234; 9 | expect(duplicatedRedis.options.port).to.eql(6379); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /test/functional/elasticache.ts: -------------------------------------------------------------------------------- 1 | import Redis from "../../lib/Redis"; 2 | import { expect } from "chai"; 3 | import MockServer from "../helpers/mock_server"; 4 | 5 | // AWS Elasticache closes the connection immediately when it encounters a READONLY error 6 | function simulateElasticache(options: { 7 | reconnectOnErrorValue: boolean | number; 8 | }) { 9 | let inTransaction = false; 10 | const mockServer = new MockServer(30000, (argv, socket, flags) => { 11 | switch (argv[0]) { 12 | case "multi": 13 | inTransaction = true; 14 | return MockServer.raw("+OK\r\n"); 15 | case "del": 16 | flags.disconnect = true; 17 | return new Error( 18 | "READONLY You can't write against a read only replica." 19 | ); 20 | case "get": 21 | return inTransaction ? MockServer.raw("+QUEUED\r\n") : argv[1]; 22 | case "exec": 23 | inTransaction = false; 24 | return []; 25 | } 26 | }); 27 | 28 | return new Redis({ 29 | port: 30000, 30 | reconnectOnError(err: Error): boolean | number { 31 | // bring the mock server back up 32 | mockServer.connect(); 33 | return options.reconnectOnErrorValue; 34 | }, 35 | }); 36 | } 37 | 38 | function expectReplyError(err) { 39 | expect(err).to.exist; 40 | expect(err.name).to.eql("ReplyError"); 41 | } 42 | 43 | function expectAbortError(err) { 44 | expect(err).to.exist; 45 | expect(err.name).to.eql("AbortError"); 46 | expect(err.message).to.eql("Command aborted due to connection close"); 47 | } 48 | 49 | describe("elasticache", () => { 50 | it("should abort a failed transaction when connection is lost", (done) => { 51 | const redis = simulateElasticache({ reconnectOnErrorValue: true }); 52 | 53 | redis 54 | .multi() 55 | .del("foo") 56 | .del("bar") 57 | .exec((err) => { 58 | expectAbortError(err); 59 | expect(err.command).to.eql({ 60 | name: "exec", 61 | args: [], 62 | }); 63 | expect(err.previousErrors).to.have.lengthOf(2); 64 | expectReplyError(err.previousErrors[0]); 65 | expect(err.previousErrors[0].command).to.eql({ 66 | name: "del", 67 | args: ["foo"], 68 | }); 69 | expectAbortError(err.previousErrors[1]); 70 | expect(err.previousErrors[1].command).to.eql({ 71 | name: "del", 72 | args: ["bar"], 73 | }); 74 | 75 | // ensure we've recovered into a healthy state 76 | redis.get("foo", (err, res) => { 77 | expect(res).to.eql("foo"); 78 | done(); 79 | }); 80 | }); 81 | }); 82 | 83 | it("should not resend failed transaction commands", (done) => { 84 | const redis = simulateElasticache({ reconnectOnErrorValue: 2 }); 85 | redis 86 | .multi() 87 | .del("foo") 88 | .get("bar") 89 | .exec((err) => { 90 | expectAbortError(err); 91 | expect(err.command).to.eql({ 92 | name: "exec", 93 | args: [], 94 | }); 95 | expect(err.previousErrors).to.have.lengthOf(2); 96 | expectAbortError(err.previousErrors[0]); 97 | expect(err.previousErrors[0].command).to.eql({ 98 | name: "del", 99 | args: ["foo"], 100 | }); 101 | expectAbortError(err.previousErrors[1]); 102 | expect(err.previousErrors[1].command).to.eql({ 103 | name: "get", 104 | args: ["bar"], 105 | }); 106 | 107 | // ensure we've recovered into a healthy state 108 | redis.get("foo", (err, res) => { 109 | expect(res).to.eql("foo"); 110 | done(); 111 | }); 112 | }); 113 | }); 114 | 115 | it("should resend intact pipelines", (done) => { 116 | const redis = simulateElasticache({ reconnectOnErrorValue: true }); 117 | 118 | let p1Result; 119 | redis 120 | .pipeline() 121 | .del("foo") 122 | .get("bar") 123 | .exec((err, result) => (p1Result = result)); 124 | 125 | redis 126 | .pipeline() 127 | .get("baz") 128 | .get("qux") 129 | .exec((err, p2Result) => { 130 | // First pipeline should have been aborted 131 | expect(p1Result).to.have.lengthOf(2); 132 | expect(p1Result[0]).to.have.lengthOf(1); 133 | expect(p1Result[1]).to.have.lengthOf(1); 134 | expectReplyError(p1Result[0][0]); 135 | expect(p1Result[0][0].command).to.eql({ 136 | name: "del", 137 | args: ["foo"], 138 | }); 139 | expectAbortError(p1Result[1][0]); 140 | expect(p1Result[1][0].command).to.eql({ 141 | name: "get", 142 | args: ["bar"], 143 | }); 144 | 145 | // Second pipeline was intact and should have been retried successfully 146 | expect(p2Result).to.have.lengthOf(2); 147 | expect(p2Result[0]).to.eql([null, "baz"]); 148 | expect(p2Result[1]).to.eql([null, "qux"]); 149 | 150 | done(); 151 | }); 152 | }); 153 | }); 154 | -------------------------------------------------------------------------------- /test/functional/exports.ts: -------------------------------------------------------------------------------- 1 | import { Command, Cluster, ReplyError } from "../../lib"; 2 | import { expect } from "chai"; 3 | 4 | describe("exports", () => { 5 | describe(".Command", () => { 6 | it("should be `Command`", () => { 7 | expect(Command).to.eql(require("../../lib/Command").default); 8 | }); 9 | }); 10 | 11 | describe(".Cluster", () => { 12 | it("should be `Cluster`", () => { 13 | expect(Cluster).to.eql(require("../../lib/cluster").default); 14 | }); 15 | }); 16 | 17 | describe(".ReplyError", () => { 18 | it("should be `ReplyError`", () => { 19 | expect(ReplyError).to.eql(require("redis-errors").ReplyError); 20 | }); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /test/functional/fatal_error.ts: -------------------------------------------------------------------------------- 1 | import Redis from "../../lib/Redis"; 2 | import { expect } from "chai"; 3 | import MockServer from "../helpers/mock_server"; 4 | 5 | describe("fatal_error", () => { 6 | it("should handle fatal error of parser", (done) => { 7 | let recovered = false; 8 | new MockServer(30000, (argv) => { 9 | if (recovered) { 10 | return; 11 | } 12 | if (argv[0] === "get") { 13 | return MockServer.raw("&"); 14 | } 15 | }); 16 | const redis = new Redis(30000); 17 | redis.get("foo", function (err) { 18 | expect(err.message).to.match(/Protocol error/); 19 | 20 | recovered = true; 21 | redis.get("bar", function (err) { 22 | expect(err).to.eql(null); 23 | done(); 24 | }); 25 | }); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /test/functional/hgetall.ts: -------------------------------------------------------------------------------- 1 | import Redis from "../../lib/Redis"; 2 | import { expect } from "chai"; 3 | 4 | const CUSTOM_PROPERTY = "_myCustomProperty"; 5 | 6 | describe("hgetall", () => { 7 | beforeEach(() => { 8 | Object.defineProperty(Object.prototype, CUSTOM_PROPERTY, { 9 | value: false, 10 | configurable: true, 11 | enumerable: false, 12 | writable: false, 13 | }); 14 | }); 15 | 16 | afterEach(() => { 17 | delete (Object.prototype as any)[CUSTOM_PROPERTY]; 18 | }); 19 | 20 | it("should handle special field names", async () => { 21 | const redis = new Redis(); 22 | await redis.hmset( 23 | "test_key", 24 | "__proto__", 25 | "hello", 26 | CUSTOM_PROPERTY, 27 | "world" 28 | ); 29 | const ret = await redis.hgetall("test_key"); 30 | expect(ret.__proto__).to.eql("hello"); 31 | expect(ret[CUSTOM_PROPERTY]).to.eql("world"); 32 | expect(Object.keys(ret).sort()).to.eql( 33 | ["__proto__", CUSTOM_PROPERTY].sort() 34 | ); 35 | expect(Object.getPrototypeOf(ret)).to.eql(Object.prototype); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /test/functional/lazy_connect.ts: -------------------------------------------------------------------------------- 1 | import Redis from "../../lib/Redis"; 2 | import { expect } from "chai"; 3 | import * as sinon from "sinon"; 4 | import { Cluster } from "../../lib"; 5 | import Pipeline from "../../lib/Pipeline"; 6 | 7 | describe("lazy connect", () => { 8 | it("should not call `connect` when init", () => { 9 | // TODO: use spy 10 | const stub = sinon 11 | .stub(Redis.prototype, "connect") 12 | .throws(new Error("`connect` should not be called")); 13 | new Redis({ lazyConnect: true }); 14 | 15 | stub.restore(); 16 | }); 17 | 18 | it("should connect when calling a command", (done) => { 19 | const redis = new Redis({ lazyConnect: true }); 20 | redis.set("foo", "bar"); 21 | redis.get("foo", function (err, result) { 22 | expect(result).to.eql("bar"); 23 | done(); 24 | }); 25 | }); 26 | 27 | it("should not try to reconnect when disconnected manually", (done) => { 28 | const redis = new Redis({ lazyConnect: true }); 29 | redis.get("foo", () => { 30 | redis.disconnect(); 31 | redis.get("foo", function (err) { 32 | expect(err.message).to.match(/Connection is closed/); 33 | done(); 34 | }); 35 | }); 36 | }); 37 | 38 | it("should be able to disconnect", (done) => { 39 | const redis = new Redis({ lazyConnect: true }); 40 | redis.on("end", () => { 41 | done(); 42 | }); 43 | redis.disconnect(); 44 | }); 45 | 46 | describe("Cluster", () => { 47 | it("should not call `connect` when init", () => { 48 | const stub = sinon 49 | .stub(Cluster.prototype, "connect") 50 | .throws(new Error("`connect` should not be called")); 51 | new Cluster([], { lazyConnect: true }); 52 | stub.restore(); 53 | }); 54 | 55 | it("should call connect when pipeline exec", (done) => { 56 | const stub = sinon.stub(Cluster.prototype, "connect").callsFake(() => { 57 | stub.restore(); 58 | done(); 59 | }); 60 | const cluster = new Cluster([], { lazyConnect: true }); 61 | const pipline = new Pipeline(cluster); 62 | pipline.get("fool1").exec(() => {}); 63 | }); 64 | 65 | it("should call connect when transction exec", (done) => { 66 | const stub = sinon.stub(Cluster.prototype, "connect").callsFake(() => { 67 | stub.restore(); 68 | done(); 69 | }); 70 | const cluster = new Cluster([], { lazyConnect: true }); 71 | cluster 72 | .multi() 73 | .get("fool1") 74 | .exec(() => {}); 75 | }); 76 | 77 | it('should quit before "close" being emited', (done) => { 78 | const stub = sinon 79 | .stub(Cluster.prototype, "connect") 80 | .throws(new Error("`connect` should not be called")); 81 | const cluster = new Cluster([], { lazyConnect: true }); 82 | cluster.quit(() => { 83 | cluster.once("close", () => { 84 | cluster.once("end", () => { 85 | stub.restore(); 86 | done(); 87 | }); 88 | }); 89 | }); 90 | }); 91 | 92 | it('should disconnect before "close" being emited', (done) => { 93 | const stub = sinon 94 | .stub(Cluster.prototype, "connect") 95 | .throws(new Error("`connect` should not be called")); 96 | const cluster = new Cluster([], { lazyConnect: true }); 97 | cluster.disconnect(); 98 | cluster.once("close", () => { 99 | cluster.once("end", () => { 100 | stub.restore(); 101 | done(); 102 | }); 103 | }); 104 | }); 105 | 106 | it("should support disconnecting with reconnect", (done) => { 107 | let stub = sinon 108 | .stub(Cluster.prototype, "connect") 109 | .throws(new Error("`connect` should not be called")); 110 | const cluster = new Cluster([], { 111 | lazyConnect: true, 112 | clusterRetryStrategy: () => { 113 | return 1; 114 | }, 115 | }); 116 | cluster.disconnect(true); 117 | cluster.once("close", () => { 118 | stub.restore(); 119 | stub = sinon.stub(Cluster.prototype, "connect").callsFake(() => { 120 | stub.restore(); 121 | done(); 122 | return Promise.resolve(); 123 | }); 124 | }); 125 | }); 126 | }); 127 | }); 128 | -------------------------------------------------------------------------------- /test/functional/maxRetriesPerRequest.ts: -------------------------------------------------------------------------------- 1 | import Redis from "../../lib/Redis"; 2 | import { expect } from "chai"; 3 | import { MaxRetriesPerRequestError } from "../../lib/errors"; 4 | 5 | describe("maxRetriesPerRequest", () => { 6 | it("throw the correct error when reached the limit", (done) => { 7 | const redis = new Redis(9999, { 8 | connectTimeout: 1, 9 | retryStrategy() { 10 | return 1; 11 | }, 12 | }); 13 | redis.get("foo", (err) => { 14 | expect(err).instanceOf(MaxRetriesPerRequestError); 15 | redis.disconnect(); 16 | done(); 17 | }); 18 | }); 19 | 20 | it("defaults to max 20 retries", (done) => { 21 | const redis = new Redis(9999, { 22 | connectTimeout: 1, 23 | retryStrategy() { 24 | return 1; 25 | }, 26 | }); 27 | redis.get("foo", () => { 28 | expect(redis.retryAttempts).to.eql(21); 29 | redis.get("foo", () => { 30 | expect(redis.retryAttempts).to.eql(42); 31 | redis.disconnect(); 32 | done(); 33 | }); 34 | }); 35 | }); 36 | 37 | it("can be changed", (done) => { 38 | const redis = new Redis(9999, { 39 | maxRetriesPerRequest: 1, 40 | retryStrategy() { 41 | return 1; 42 | }, 43 | }); 44 | redis.get("foo", () => { 45 | expect(redis.retryAttempts).to.eql(2); 46 | redis.get("foo", () => { 47 | expect(redis.retryAttempts).to.eql(4); 48 | redis.disconnect(); 49 | done(); 50 | }); 51 | }); 52 | }); 53 | 54 | it("allows 0", (done) => { 55 | const redis = new Redis(9999, { 56 | maxRetriesPerRequest: 0, 57 | retryStrategy() { 58 | return 1; 59 | }, 60 | }); 61 | redis.get("foo", () => { 62 | expect(redis.retryAttempts).to.eql(1); 63 | redis.get("foo", () => { 64 | expect(redis.retryAttempts).to.eql(2); 65 | redis.disconnect(); 66 | done(); 67 | }); 68 | }); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /test/functional/monitor.ts: -------------------------------------------------------------------------------- 1 | import Redis from "../../lib/Redis"; 2 | import { expect, use } from "chai"; 3 | import * as sinon from "sinon"; 4 | import { waitForMonitorReady } from "../helpers/util"; 5 | 6 | use(require("chai-as-promised")); 7 | 8 | describe("monitor", () => { 9 | it("should receive commands", (done) => { 10 | const redis = new Redis(); 11 | redis.on("ready", () => { 12 | redis.monitor(async (err, monitor) => { 13 | if (err) { 14 | done(err); 15 | return; 16 | } 17 | monitor.on("monitor", function (time, args) { 18 | expect(args[0]).to.match(/get/i); 19 | expect(args[1]).to.eql("foo"); 20 | redis.disconnect(); 21 | monitor.disconnect(); 22 | done(); 23 | }); 24 | 25 | await waitForMonitorReady(); 26 | redis.get("foo"); 27 | }); 28 | }); 29 | }); 30 | 31 | it("should reject processing commands", (done) => { 32 | const redis = new Redis(); 33 | redis.monitor(async (err, monitor) => { 34 | await waitForMonitorReady(); 35 | monitor.get("foo", function (err) { 36 | expect(err.message).to.match(/Connection is in monitoring mode/); 37 | redis.disconnect(); 38 | monitor.disconnect(); 39 | done(); 40 | }); 41 | }); 42 | }); 43 | 44 | it("should report being in 'monitor' mode", (done) => { 45 | const redis = new Redis(); 46 | redis.monitor(async (err, monitor) => { 47 | await waitForMonitorReady(); 48 | expect(redis.mode).to.equal("normal"); 49 | expect(monitor.mode).to.equal("monitor"); 50 | redis.disconnect(); 51 | monitor.disconnect(); 52 | done(); 53 | }); 54 | }); 55 | 56 | it("should continue monitoring after reconnection", (done) => { 57 | const redis = new Redis(); 58 | redis.monitor((err, monitor) => { 59 | if (err) { 60 | done(err); 61 | return; 62 | } 63 | monitor.on("monitor", (_time, args) => { 64 | if (args[0] === "set" || args[0] === "SET") { 65 | redis.disconnect(); 66 | monitor.disconnect(); 67 | done(); 68 | } 69 | }); 70 | monitor.disconnect(true); 71 | monitor.on("ready", async () => { 72 | await waitForMonitorReady(); 73 | redis.set("foo", "bar"); 74 | }); 75 | }); 76 | }); 77 | 78 | it("should wait for the ready event before monitoring", (done) => { 79 | const redis = new Redis(); 80 | redis.on("ready", () => { 81 | // @ts-expect-error 82 | const readyCheck = sinon.spy(Redis.prototype, "_readyCheck"); 83 | redis.monitor((err, monitor) => { 84 | expect(readyCheck.callCount).to.eql(1); 85 | redis.disconnect(); 86 | monitor.disconnect(); 87 | done(); 88 | }); 89 | }); 90 | }); 91 | 92 | it("rejects when monitor is disabled", async () => { 93 | const redis = new Redis(); 94 | await redis.acl("SETUSER", "nomonitor", "reset", "+info", ">123456", "on"); 95 | 96 | await expect( 97 | new Redis({ username: "nomonitor", password: "123456" }).monitor() 98 | ).to.eventually.be.rejectedWith(/NOPERM/); 99 | }); 100 | }); 101 | -------------------------------------------------------------------------------- /test/functional/ready_check.ts: -------------------------------------------------------------------------------- 1 | import Redis from "../../lib/Redis"; 2 | import { noop } from "../../lib/utils"; 3 | import * as sinon from "sinon"; 4 | import { expect } from "chai"; 5 | 6 | const stubInfo = ( 7 | redis: Redis, 8 | response: [Error | null, string | undefined] 9 | ) => { 10 | sinon.stub(redis, "info").callsFake((section, callback) => { 11 | const cb = typeof section === "function" ? section : callback; 12 | const [error, info] = response; 13 | cb(error, info); 14 | return error ? Promise.reject(error) : Promise.resolve(info); 15 | }); 16 | }; 17 | 18 | describe("ready_check", () => { 19 | it("should retry when redis is not ready", (done) => { 20 | const redis = new Redis({ lazyConnect: true }); 21 | 22 | stubInfo(redis, [null, "loading:1\r\nloading_eta_seconds:7"]); 23 | 24 | // @ts-expect-error 25 | sinon.stub(global, "setTimeout").callsFake((_body, ms) => { 26 | if (ms === 7000) { 27 | done(); 28 | } 29 | }); 30 | redis.connect().catch(noop); 31 | }); 32 | 33 | it("should reconnect when info return a error", (done) => { 34 | const redis = new Redis({ 35 | lazyConnect: true, 36 | retryStrategy: () => { 37 | done(); 38 | return; 39 | }, 40 | }); 41 | 42 | stubInfo(redis, [new Error("info error"), undefined]); 43 | 44 | redis.connect().catch(noop); 45 | }); 46 | 47 | it("warns for NOPERM error", async () => { 48 | const redis = new Redis({ 49 | lazyConnect: true, 50 | }); 51 | 52 | const warn = sinon.stub(console, "warn"); 53 | stubInfo(redis, [ 54 | new Error( 55 | "NOPERM this user has no permissions to run the 'info' command" 56 | ), 57 | undefined, 58 | ]); 59 | 60 | await redis.connect(); 61 | expect(warn.calledOnce).to.eql(true); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /test/functional/reconnect_on_error.ts: -------------------------------------------------------------------------------- 1 | import Redis from "../../lib/Redis"; 2 | import { expect } from "chai"; 3 | import * as sinon from "sinon"; 4 | 5 | describe("reconnectOnError", () => { 6 | it("should pass the error as the first param", (done) => { 7 | let pending = 2; 8 | function assert(err) { 9 | expect(err.name).to.eql("ReplyError"); 10 | expect(err.command.name).to.eql("set"); 11 | expect(err.command.args).to.eql(["foo"]); 12 | if (!--pending) { 13 | done(); 14 | } 15 | } 16 | const redis = new Redis({ 17 | reconnectOnError: function (err) { 18 | assert(err); 19 | return 1; 20 | }, 21 | }); 22 | 23 | redis.set("foo", function (err) { 24 | assert(err); 25 | }); 26 | }); 27 | 28 | it("should not reconnect if reconnectOnError returns false", (done) => { 29 | const redis = new Redis({ 30 | reconnectOnError: function (err) { 31 | return false; 32 | }, 33 | }); 34 | 35 | redis.disconnect = () => { 36 | throw new Error("should not disconnect"); 37 | }; 38 | 39 | redis.set("foo", function (err) { 40 | done(); 41 | }); 42 | }); 43 | 44 | it("should reconnect if reconnectOnError returns true or 1", (done) => { 45 | const redis = new Redis({ 46 | reconnectOnError: () => { 47 | return true; 48 | }, 49 | }); 50 | 51 | redis.set("foo", () => { 52 | redis.on("ready", () => { 53 | done(); 54 | }); 55 | }); 56 | }); 57 | 58 | it("should reconnect and retry the command if reconnectOnError returns 2", (done) => { 59 | const redis = new Redis({ 60 | reconnectOnError: () => { 61 | redis.del("foo"); 62 | return 2; 63 | }, 64 | }); 65 | 66 | redis.set("foo", "bar"); 67 | redis.sadd("foo", "a", function (err, res) { 68 | expect(res).to.eql(1); 69 | done(); 70 | }); 71 | }); 72 | 73 | it("should select the currect database", (done) => { 74 | const redis = new Redis({ 75 | reconnectOnError: () => { 76 | redis.select(3); 77 | redis.del("foo"); 78 | redis.select(0); 79 | return 2; 80 | }, 81 | }); 82 | 83 | redis.select(3); 84 | redis.set("foo", "bar"); 85 | redis.sadd("foo", "a", function (err, res) { 86 | expect(res).to.eql(1); 87 | redis.select(3); 88 | redis.type("foo", function (err, type) { 89 | expect(type).to.eql("set"); 90 | done(); 91 | }); 92 | }); 93 | }); 94 | 95 | it("should work with pipeline", (done) => { 96 | const redis = new Redis({ 97 | reconnectOnError: () => { 98 | redis.del("foo"); 99 | return 2; 100 | }, 101 | }); 102 | 103 | redis.set("foo", "bar"); 104 | redis 105 | .pipeline() 106 | .get("foo") 107 | .sadd("foo", "a") 108 | .exec(function (err, res) { 109 | expect(res).to.eql([ 110 | [null, "bar"], 111 | [null, 1], 112 | ]); 113 | done(); 114 | }); 115 | }); 116 | 117 | it("should work with pipelined multi", (done) => { 118 | const redis = new Redis({ 119 | reconnectOnError: () => { 120 | // deleting foo allows sadd below to succeed on the second try 121 | redis.del("foo"); 122 | return 2; 123 | }, 124 | }); 125 | const delSpy = sinon.spy(redis, "del"); 126 | 127 | redis.set("foo", "bar"); 128 | redis.set("i", 1); 129 | redis 130 | .pipeline() 131 | .sadd("foo", "a") // trigger a WRONGTYPE error 132 | .multi() 133 | .get("foo") 134 | .incr("i") 135 | .exec() 136 | .exec(function (err, res) { 137 | expect(delSpy.calledOnce).to.eql(true); 138 | expect(delSpy.firstCall.args[0]).to.eql("foo"); 139 | expect(err).to.be.null; 140 | expect(res).to.eql([ 141 | [null, 1], 142 | [null, "OK"], 143 | [null, "QUEUED"], 144 | [null, "QUEUED"], 145 | [null, ["bar", 2]], 146 | ]); 147 | done(); 148 | }); 149 | }); 150 | }); 151 | -------------------------------------------------------------------------------- /test/functional/select.ts: -------------------------------------------------------------------------------- 1 | import Redis from "../../lib/Redis"; 2 | import { expect } from "chai"; 3 | 4 | describe("select", function () { 5 | it("should support auto select", (done) => { 6 | const redis = new Redis({ db: 2 }); 7 | redis.set("foo", "2"); 8 | redis.select("2"); 9 | redis.get("foo", function (err, res) { 10 | expect(res).to.eql("2"); 11 | redis.disconnect(); 12 | done(); 13 | }); 14 | }); 15 | 16 | it("should resend commands to the correct db", (done) => { 17 | const redis = new Redis(); 18 | redis.once("ready", function () { 19 | redis.set("foo", "2", function () { 20 | redis.stream.destroy(); 21 | redis.select("3"); 22 | redis.set("foo", "3"); 23 | redis.select("0"); 24 | redis.get("foo", function (err, res) { 25 | expect(res).to.eql("2"); 26 | redis.select("3"); 27 | redis.get("foo", function (err, res) { 28 | expect(res).to.eql("3"); 29 | redis.disconnect(); 30 | done(); 31 | }); 32 | }); 33 | }); 34 | }); 35 | }); 36 | 37 | it("should re-select the current db when reconnect", (done) => { 38 | const redis = new Redis(); 39 | 40 | redis.once("ready", function () { 41 | redis.set("foo", "bar"); 42 | redis.select(2); 43 | redis.set("foo", "2", function () { 44 | redis.stream.destroy(); 45 | redis.get("foo", function (err, res) { 46 | expect(res).to.eql("2"); 47 | redis.disconnect(); 48 | done(); 49 | }); 50 | }); 51 | }); 52 | }); 53 | 54 | it('should emit "select" event when db changes', (done) => { 55 | const changes = []; 56 | const redis = new Redis(); 57 | redis.on("select", function (db) { 58 | changes.push(db); 59 | }); 60 | redis.select("2", function () { 61 | expect(changes).to.eql([2]); 62 | redis.select("4", function () { 63 | expect(changes).to.eql([2, 4]); 64 | redis.select("4", function () { 65 | expect(changes).to.eql([2, 4]); 66 | redis.disconnect(); 67 | done(); 68 | }); 69 | }); 70 | }); 71 | }); 72 | 73 | it("should be sent on the connect event", (done) => { 74 | const redis = new Redis({ db: 2 }); 75 | const select = redis.select; 76 | redis.select = function () { 77 | return select.apply(redis, arguments).then(function () { 78 | redis.select = select; 79 | redis.disconnect(); 80 | done(); 81 | }); 82 | }; 83 | redis.on("connect", function () { 84 | redis.subscribe("anychannel"); 85 | }); 86 | }); 87 | }); 88 | -------------------------------------------------------------------------------- /test/functional/sentinel_nat.ts: -------------------------------------------------------------------------------- 1 | import Redis from "../../lib/Redis"; 2 | import MockServer from "../helpers/mock_server"; 3 | 4 | describe("sentinel_nat", () => { 5 | it("connects to server as expected", (done) => { 6 | const sentinel = new MockServer(27379, (argv) => { 7 | if (argv[0] === "sentinel" && argv[1] === "get-master-addr-by-name") { 8 | return ["127.0.0.1", "17380"]; 9 | } 10 | }); 11 | 12 | const redis = new Redis({ 13 | sentinels: [{ host: "127.0.0.1", port: 27379 }], 14 | natMap: { 15 | "127.0.0.1:17380": { 16 | host: "localhost", 17 | port: 6379, 18 | }, 19 | }, 20 | name: "master", 21 | lazyConnect: true, 22 | }); 23 | 24 | redis.connect(function (err) { 25 | if (err) { 26 | sentinel.disconnect(() => {}); 27 | return done(err); 28 | } 29 | sentinel.disconnect(done); 30 | }); 31 | }); 32 | 33 | it("rejects connection if host is not defined in map", (done) => { 34 | const sentinel = new MockServer(27379, (argv) => { 35 | if (argv[0] === "sentinel" && argv[1] === "get-master-addr-by-name") { 36 | return ["127.0.0.1", "17380"]; 37 | } 38 | 39 | if ( 40 | argv[0] === "sentinel" && 41 | argv[1] === "sentinels" && 42 | argv[2] === "master" 43 | ) { 44 | return ["127.0.0.1", "27379"]; 45 | } 46 | }); 47 | 48 | const redis = new Redis({ 49 | sentinels: [{ host: "127.0.0.1", port: 27379 }], 50 | natMap: { 51 | "127.0.0.1:17381": { 52 | host: "localhost", 53 | port: 6379, 54 | }, 55 | }, 56 | maxRetriesPerRequest: 1, 57 | name: "master", 58 | lazyConnect: true, 59 | }); 60 | 61 | redis 62 | .connect() 63 | .then(() => { 64 | throw new Error("Should not call"); 65 | }) 66 | .catch(function (err) { 67 | if (err.message === "Connection is closed.") { 68 | return done(null); 69 | } 70 | sentinel.disconnect(done); 71 | }); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /test/functional/show_friendly_error_stack.ts: -------------------------------------------------------------------------------- 1 | import Redis from "../../lib/Redis"; 2 | import { expect } from "chai"; 3 | 4 | const path = require("path"); 5 | const scriptName = path.basename(__filename); 6 | 7 | describe("showFriendlyErrorStack", () => { 8 | it("should show friendly error stack", (done) => { 9 | const redis = new Redis({ showFriendlyErrorStack: true }); 10 | redis.set("foo").catch(function (err) { 11 | const errors = err.stack.split("\n"); 12 | expect(errors[0].indexOf("ReplyError")).not.eql(-1); 13 | expect(errors[1].indexOf(scriptName)).not.eql(-1); 14 | done(); 15 | }); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /test/functional/socketTimeout.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import Redis from "../../lib/Redis"; 3 | 4 | describe("socketTimeout", () => { 5 | const timeoutMs = 500; 6 | 7 | it("should ensure correct startup with password (https://github.com/redis/ioredis/issues/1919)", (done) => { 8 | let timeoutObj: NodeJS.Timeout; 9 | 10 | const redis = new Redis({ 11 | socketTimeout: timeoutMs, 12 | lazyConnect: true, 13 | password: "foobared", 14 | }); 15 | 16 | redis.on("error", (err) => { 17 | clearTimeout(timeoutObj); 18 | done(err.toString()); 19 | }); 20 | 21 | redis.connect(() => { 22 | timeoutObj = setTimeout(() => { 23 | done(); 24 | }, timeoutMs * 2); 25 | }); 26 | }); 27 | 28 | it("should not throw error when socketTimeout is set and no command is sent", (done) => { 29 | let timeoutObj: NodeJS.Timeout; 30 | 31 | const redis = new Redis({ 32 | socketTimeout: timeoutMs, 33 | lazyConnect: true, 34 | }); 35 | 36 | redis.on("error", (err) => { 37 | clearTimeout(timeoutObj); 38 | done(err.toString()); 39 | }); 40 | 41 | redis.connect(() => { 42 | timeoutObj = setTimeout(() => { 43 | done(); 44 | }, timeoutMs * 2); 45 | }); 46 | }); 47 | 48 | it("should throw if socket timeout is reached", (done) => { 49 | const redis = new Redis({ 50 | socketTimeout: timeoutMs, 51 | lazyConnect: true, 52 | }); 53 | 54 | redis.on("error", (err) => { 55 | expect(err.message).to.include("Socket timeout"); 56 | done(); 57 | }); 58 | 59 | redis.connect(() => { 60 | redis.stream.removeAllListeners("data"); 61 | redis.ping(); 62 | }); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /test/functional/spub_ssub.ts: -------------------------------------------------------------------------------- 1 | import Redis from "../../lib/Redis"; 2 | import { expect } from "chai"; 3 | 4 | describe("spub/ssub", function () { 5 | it("should invoke the callback when subscribe successfully", (done) => { 6 | const redis = new Redis(); 7 | let pending = 1; 8 | redis.ssubscribe("foo", "bar", function (err, count) { 9 | expect(count).to.eql(2); 10 | pending -= 1; 11 | }); 12 | redis.ssubscribe("foo", "zoo", function (err, count) { 13 | expect(count).to.eql(3); 14 | expect(pending).to.eql(0); 15 | redis.disconnect(); 16 | done(); 17 | }); 18 | }); 19 | 20 | it("should reject when issue a command in the subscriber mode", (done) => { 21 | const redis = new Redis(); 22 | redis.ssubscribe("foo", function () { 23 | redis.set("foo", "bar", function (err) { 24 | expect(err instanceof Error); 25 | expect(err.message).to.match(/subscriber mode/); 26 | redis.disconnect(); 27 | done(); 28 | }); 29 | }); 30 | }); 31 | 32 | it("should report being in 'subscriber' mode when subscribed", (done) => { 33 | const redis = new Redis(); 34 | redis.ssubscribe("foo", function () { 35 | expect(redis.mode).to.equal("subscriber"); 36 | redis.disconnect(); 37 | done(); 38 | }); 39 | }); 40 | 41 | it("should exit subscriber mode using sunsubscribe", (done) => { 42 | const redis = new Redis(); 43 | redis.ssubscribe("foo", "bar", function () { 44 | redis.sunsubscribe("foo", "bar", function (err, count) { 45 | expect(count).to.eql(0); 46 | redis.set("foo", "bar", function (err) { 47 | expect(err).to.eql(null); 48 | 49 | redis.ssubscribe("zoo", "foo", function () { 50 | redis.sunsubscribe(function (err, count) { 51 | expect(count).to.eql(0); 52 | redis.set("foo", "bar", function (err) { 53 | expect(err).to.eql(null); 54 | redis.disconnect(); 55 | done(); 56 | }); 57 | }); 58 | }); 59 | }); 60 | }); 61 | }); 62 | }); 63 | 64 | it("should report being in 'normal' mode after sunsubscribing", (done) => { 65 | const redis = new Redis(); 66 | redis.ssubscribe("foo", "bar", function () { 67 | redis.sunsubscribe("foo", "bar", function (err, count) { 68 | expect(redis.mode).to.equal("normal"); 69 | redis.disconnect(); 70 | done(); 71 | }); 72 | }); 73 | }); 74 | 75 | it("should receive messages when subscribe a shard channel", (done) => { 76 | const redis = new Redis(); 77 | const pub = new Redis(); 78 | let pending = 2; 79 | redis.ssubscribe("foo", function () { 80 | pub.spublish("foo", "bar"); 81 | }); 82 | redis.on("smessage", function (channel, message) { 83 | expect(channel).to.eql("foo"); 84 | expect(message).to.eql("bar"); 85 | if (!--pending) { 86 | redis.disconnect(); 87 | done(); 88 | } 89 | }); 90 | redis.on("smessageBuffer", function (channel, message) { 91 | expect(channel).to.be.instanceof(Buffer); 92 | expect(channel.toString()).to.eql("foo"); 93 | expect(message).to.be.instanceof(Buffer); 94 | expect(message.toString()).to.eql("bar"); 95 | if (!--pending) { 96 | redis.disconnect(); 97 | done(); 98 | } 99 | }); 100 | }); 101 | 102 | it("should be able to send quit command in the subscriber mode", (done) => { 103 | const redis = new Redis(); 104 | let pending = 1; 105 | redis.ssubscribe("foo", function () { 106 | redis.quit(function () { 107 | pending -= 1; 108 | }); 109 | }); 110 | redis.on("end", function () { 111 | expect(pending).to.eql(0); 112 | redis.disconnect(); 113 | done(); 114 | }); 115 | }); 116 | 117 | // TODO ready reconnect in redis stand 118 | it("should restore subscription after reconnecting(ssubscribe)", (done) => { 119 | const redis = new Redis({ port: 6379, host: "127.0.0.1" }); 120 | const pub = new Redis({ port: 6379, host: "127.0.0.1" }); 121 | // redis.ping(function (err, result) { 122 | // // redis.on("message", function (channel, message) { 123 | // console.log(`${err}-${result}`); 124 | // // }); 125 | // }); 126 | redis.ssubscribe("foo", "bar", function () { 127 | redis.on("ready", function () { 128 | // Execute a random command to make sure that `subscribe` 129 | // is sent 130 | redis.ping(function () { 131 | let pending = 2; 132 | redis.on("smessage", function (channel, message) { 133 | if (!--pending) { 134 | redis.disconnect(); 135 | pub.disconnect(); 136 | done(); 137 | } 138 | }); 139 | pub.spublish("foo", "hi1"); 140 | pub.spublish("bar", "hi2"); 141 | }); 142 | }); 143 | redis.disconnect(true); 144 | }); 145 | }); 146 | }); 147 | -------------------------------------------------------------------------------- /test/functional/string_numbers.ts: -------------------------------------------------------------------------------- 1 | import Redis from "../../lib/Redis"; 2 | import { expect } from "chai"; 3 | 4 | const MAX_NUMBER = 9007199254740991; // Number.MAX_SAFE_INTEGER 5 | 6 | describe("stringNumbers", () => { 7 | context("enabled", () => { 8 | it("returns numbers as strings", async () => { 9 | const redis = new Redis({ 10 | stringNumbers: true, 11 | }); 12 | 13 | await redis.set("foo", MAX_NUMBER); 14 | expect(await redis.incr("foo")).to.equal("9007199254740992"); 15 | expect(await redis.incr("foo")).to.equal("9007199254740993"); 16 | expect(await redis.incr("foo")).to.equal("9007199254740994"); 17 | 18 | // also works for small interger 19 | await redis.set("foo", 123); 20 | expect(await redis.incr("foo")).to.equal("124"); 21 | 22 | // and floats 23 | await redis.set("foo", 123.23); 24 | expect(Number(await redis.incrbyfloat("foo", 1.2))).to.be.within( 25 | 124.42999, 26 | 124.430001 27 | ); 28 | 29 | redis.disconnect(); 30 | }); 31 | }); 32 | 33 | context("disabled", () => { 34 | it("returns numbers", (done) => { 35 | const redis = new Redis(); 36 | 37 | redis.set("foo", "123"); 38 | redis.incr("foo", function (err, res) { 39 | expect(res).to.eql(124); 40 | redis.disconnect(); 41 | done(); 42 | }); 43 | }); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /test/functional/tls.ts: -------------------------------------------------------------------------------- 1 | import * as tls from "tls"; 2 | import * as net from "net"; 3 | import Redis from "../../lib/Redis"; 4 | import { expect } from "chai"; 5 | import * as sinon from "sinon"; 6 | import MockServer from "../helpers/mock_server"; 7 | 8 | describe("tls option", () => { 9 | describe("Standalone", () => { 10 | it("supports tls", (done) => { 11 | let redis; 12 | 13 | // @ts-expect-error 14 | const stub = sinon.stub(tls, "connect").callsFake((op) => { 15 | // @ts-expect-error 16 | expect(op.ca).to.eql("123"); 17 | // @ts-expect-error 18 | expect(op.servername).to.eql("localhost"); 19 | // @ts-expect-error 20 | expect(op.rejectUnauthorized).to.eql(false); 21 | // @ts-expect-error 22 | expect(op.port).to.eql(6379); 23 | const stream = net.createConnection(op); 24 | stream.on("connect", (data) => { 25 | stream.emit("secureConnect", data); 26 | }); 27 | return stream; 28 | }); 29 | 30 | redis = new Redis({ 31 | tls: { ca: "123", servername: "localhost", rejectUnauthorized: false }, 32 | }); 33 | redis.on("ready", () => { 34 | redis.disconnect(); 35 | stub.restore(); 36 | redis.on("end", () => done()); 37 | }); 38 | }); 39 | }); 40 | 41 | describe("Sentinel", () => { 42 | it("does not use tls option by default", (done) => { 43 | new MockServer(27379, (argv) => { 44 | if (argv[0] === "sentinel" && argv[1] === "get-master-addr-by-name") { 45 | return ["127.0.0.1", "6379"]; 46 | } 47 | }); 48 | 49 | const stub = sinon.stub(tls, "connect").callsFake(() => { 50 | throw new Error("called"); 51 | }); 52 | 53 | const redis = new Redis({ 54 | sentinels: [{ port: 27379 }], 55 | name: "my", 56 | tls: { ca: "123" }, 57 | }); 58 | redis.on("ready", () => { 59 | redis.disconnect(); 60 | stub.restore(); 61 | done(); 62 | }); 63 | }); 64 | 65 | it("can be enabled by `enableTLSForSentinelMode`", (done) => { 66 | new MockServer(27379, (argv) => { 67 | if (argv[0] === "sentinel" && argv[1] === "get-master-addr-by-name") { 68 | return ["127.0.0.1", "6379"]; 69 | } 70 | }); 71 | 72 | let redis; 73 | 74 | const stub = sinon.stub(tls, "connect").callsFake((op) => { 75 | // @ts-expect-error 76 | expect(op.ca).to.eql("123"); 77 | // @ts-expect-error 78 | expect(op.servername).to.eql("localhost"); 79 | // @ts-expect-error 80 | expect(op.rejectUnauthorized).to.eql(false); 81 | redis.disconnect(); 82 | stub.restore(); 83 | process.nextTick(done); 84 | return tls.connect(op); 85 | }); 86 | 87 | redis = new Redis({ 88 | sentinels: [{ port: 27379 }], 89 | name: "my", 90 | tls: { ca: "123", servername: "localhost", rejectUnauthorized: false }, 91 | enableTLSForSentinelMode: true, 92 | }); 93 | }); 94 | 95 | it("supports sentinelTLS", (done) => { 96 | new MockServer(27379, (argv) => { 97 | if (argv[0] === "sentinel" && argv[1] === "get-master-addr-by-name") { 98 | return ["127.0.0.1", "6379"]; 99 | } 100 | }); 101 | 102 | let redis; 103 | 104 | // @ts-expect-error 105 | const stub = sinon.stub(tls, "connect").callsFake((op) => { 106 | // @ts-expect-error 107 | expect(op.ca).to.eql("123"); 108 | // @ts-expect-error 109 | expect(op.servername).to.eql("localhost"); 110 | // @ts-expect-error 111 | expect(op.rejectUnauthorized).to.eql(false); 112 | // @ts-expect-error 113 | expect(op.port).to.eql(27379); 114 | const stream = net.createConnection(op); 115 | stream.on("connect", (data) => { 116 | stream.emit("secureConnect", data); 117 | }); 118 | return stream; 119 | }); 120 | 121 | redis = new Redis({ 122 | sentinels: [{ port: 27379 }], 123 | name: "my", 124 | sentinelTLS: { 125 | ca: "123", 126 | servername: "localhost", 127 | rejectUnauthorized: false, 128 | }, 129 | }); 130 | redis.on("ready", () => { 131 | redis.disconnect(); 132 | stub.restore(); 133 | redis.on("end", () => done()); 134 | }); 135 | }); 136 | }); 137 | }); 138 | -------------------------------------------------------------------------------- /test/functional/watch-exec.ts: -------------------------------------------------------------------------------- 1 | import Redis from "../../lib/Redis"; 2 | import { expect } from "chai"; 3 | 4 | describe("watch-exec", () => { 5 | it("should support watch/exec transactions", () => { 6 | const redis1 = new Redis(); 7 | return redis1 8 | .watch("watchkey") 9 | .then(() => { 10 | return redis1.multi().set("watchkey", "1").exec(); 11 | }) 12 | .then(function (result) { 13 | expect(result.length).to.eql(1); 14 | expect(result[0]).to.eql([null, "OK"]); 15 | }); 16 | }); 17 | 18 | it("should support watch/exec transaction rollback", () => { 19 | const redis1 = new Redis(); 20 | const redis2 = new Redis(); 21 | return redis1 22 | .watch("watchkey") 23 | .then(() => { 24 | return redis2.set("watchkey", "2"); 25 | }) 26 | .then(() => { 27 | return redis1.multi().set("watchkey", "1").exec(); 28 | }) 29 | .then(function (result) { 30 | expect(result).to.be.null; 31 | }); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /test/helpers/global.ts: -------------------------------------------------------------------------------- 1 | import * as sinon from "sinon"; 2 | import Redis from "../../lib/Redis"; 3 | 4 | afterEach((done) => { 5 | sinon.restore(); 6 | new Redis() 7 | .pipeline() 8 | .flushall() 9 | .script("FLUSH") 10 | .client("KILL", "normal") 11 | .exec(done); 12 | }); 13 | 14 | process.on("unhandledRejection", (reason) => { 15 | console.log("mocha test saw unexpected unhandledRejection", reason); 16 | throw new Error("mocha test saw unexpected unhandledRejection: " + reason); 17 | }); 18 | 19 | // Suppress "Unhandled error event" logs from edge cases that are deliberately being tested. 20 | // Don't suppress other error types, such as errors in typescript files. 21 | const error = console.error; 22 | console.error = function (...args) { 23 | if ( 24 | typeof args[0] === "string" && 25 | args[0].indexOf("[ioredis] Unhandled error event") === 0 26 | ) { 27 | return; 28 | } 29 | error.call(console, ...args); 30 | }; 31 | -------------------------------------------------------------------------------- /test/helpers/mock_server.ts: -------------------------------------------------------------------------------- 1 | import { createServer, Server, Socket } from "net"; 2 | import { EventEmitter } from "events"; 3 | import { convertBufferToString } from "../../lib/utils"; 4 | import enableDestroy = require("server-destroy"); 5 | import Parser = require("redis-parser"); 6 | 7 | let createdMockServers: MockServer[] = []; 8 | const RAW_DATA_KEY = "___IOREDIS_MOCK_ROW_DATA___"; 9 | 10 | afterEach((done) => { 11 | if (createdMockServers.length === 0) { 12 | done(); 13 | return; 14 | } 15 | let pending = 0; 16 | for (const server of createdMockServers) { 17 | pending += 1; 18 | server.disconnect(check); 19 | } 20 | 21 | function check() { 22 | if (!--pending) { 23 | createdMockServers = []; 24 | done(); 25 | } 26 | } 27 | }); 28 | 29 | const connectionNameMap: WeakMap = new WeakMap(); 30 | 31 | export function getConnectionName(socket: Socket): string | undefined { 32 | return connectionNameMap.get(socket); 33 | } 34 | 35 | interface Flags { 36 | disconnect?: boolean; 37 | hang?: boolean; 38 | } 39 | export type MockServerHandler = ( 40 | reply: any, 41 | socket: Socket, 42 | flags: Flags 43 | ) => any; 44 | 45 | export default class MockServer extends EventEmitter { 46 | static REDIS_OK = "+OK"; 47 | 48 | static raw(data: T): { [RAW_DATA_KEY]: T } { 49 | return { 50 | [RAW_DATA_KEY]: data, 51 | }; 52 | } 53 | 54 | private clients: Socket[] = []; 55 | private socket?: Server; 56 | 57 | constructor( 58 | private port: number, 59 | public handler?: MockServerHandler, 60 | private slotTable?: any 61 | ) { 62 | super(); 63 | this.connect(); 64 | createdMockServers.push(this); 65 | } 66 | 67 | connect() { 68 | this.socket = createServer((c) => { 69 | const clientIndex = this.clients.push(c) - 1; 70 | process.nextTick(() => { 71 | this.emit("connect", c); 72 | }); 73 | 74 | const parser = new Parser({ 75 | returnBuffers: true, 76 | stringNumbers: false, 77 | returnReply: (reply: any) => { 78 | reply = convertBufferToString(reply, "utf8"); 79 | if ( 80 | reply.length === 3 && 81 | reply[0].toLowerCase() === "client" && 82 | reply[1].toLowerCase() === "setname" 83 | ) { 84 | connectionNameMap.set(c, reply[2]); 85 | } 86 | if ( 87 | this.slotTable && 88 | reply.length === 2 && 89 | reply[0].toLowerCase() === "cluster" && 90 | reply[1].toLowerCase() === "slots" 91 | ) { 92 | this.write(c, this.slotTable); 93 | return; 94 | } 95 | const flags: Flags = {}; 96 | const handlerResult = this.handler && this.handler(reply, c, flags); 97 | if (!flags.hang) { 98 | this.write(c, handlerResult); 99 | } 100 | if (flags.disconnect) { 101 | this.disconnect(); 102 | } 103 | }, 104 | returnError: () => {}, 105 | }); 106 | 107 | c.on("end", () => { 108 | this.clients[clientIndex] = null; 109 | this.emit("disconnect", c); 110 | }); 111 | 112 | c.on("data", (data) => { 113 | parser.execute(data); 114 | }); 115 | }); 116 | 117 | this.socket.listen(this.port); 118 | enableDestroy(this.socket); 119 | } 120 | 121 | disconnect(callback?: () => void) { 122 | // @ts-expect-error 123 | this.socket.destroy(callback); 124 | } 125 | 126 | disconnectPromise() { 127 | return new Promise((resolve) => this.disconnect(resolve)); 128 | } 129 | 130 | broadcast(data: any) { 131 | this.clients 132 | .filter((c) => c) 133 | .forEach((client) => { 134 | this.write(client, data); 135 | }); 136 | } 137 | 138 | write(c: Socket, data: any) { 139 | if (c.writable) { 140 | c.write(convert("", data)); 141 | } 142 | 143 | function convert(str: string, data: any) { 144 | let result: string; 145 | if (typeof data === "undefined") { 146 | data = MockServer.REDIS_OK; 147 | } 148 | if (data === MockServer.REDIS_OK) { 149 | result = "+OK\r\n"; 150 | } else if (data instanceof Error) { 151 | result = "-" + data.message + "\r\n"; 152 | } else if (Array.isArray(data)) { 153 | result = "*" + data.length + "\r\n"; 154 | data.forEach(function (item) { 155 | result += convert(str, item); 156 | }); 157 | } else if (typeof data === "number") { 158 | result = ":" + data + "\r\n"; 159 | } else if (data === null) { 160 | result = "$-1\r\n"; 161 | } else if (typeof data === "object" && data[RAW_DATA_KEY]) { 162 | result = data[RAW_DATA_KEY]; 163 | } else { 164 | data = data.toString(); 165 | result = "$" + data.length + "\r\n"; 166 | result += data + "\r\n"; 167 | } 168 | return str + result; 169 | } 170 | } 171 | 172 | findClientByName(name: string): Socket | undefined { 173 | return this.clients 174 | .filter((c) => c) 175 | .find((client) => { 176 | return getConnectionName(client) === name; 177 | }); 178 | } 179 | 180 | getAllClients(): Socket[] { 181 | return this.clients.filter(Boolean); 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /test/helpers/util.ts: -------------------------------------------------------------------------------- 1 | export function waitForMonitorReady() { 2 | // It takes a while for the monitor to be ready. 3 | // This is a hack to wait for it because the monitor command 4 | // does not have a response 5 | return new Promise((resolve) => setTimeout(resolve, 150)); 6 | } 7 | 8 | export async function getCommandsFromMonitor( 9 | redis: any, 10 | count: number, 11 | exec: Function 12 | ): Promise<[any]> { 13 | const arr: string[] = []; 14 | const monitor = await redis.monitor(); 15 | await waitForMonitorReady(); 16 | const promise = new Promise((resolve, reject) => { 17 | setTimeout(reject, 1000, new Error("Monitor timed out")); 18 | monitor.on("monitor", (_, command) => { 19 | if (arr.length !== count) arr.push(command); 20 | if (arr.length === count) { 21 | resolve(arr); 22 | monitor.disconnect(); 23 | } 24 | }); 25 | }); 26 | 27 | const [commands] = await Promise.all<[any]>([promise, exec()]); 28 | return commands; 29 | } 30 | -------------------------------------------------------------------------------- /test/typing/commands.test-d.ts: -------------------------------------------------------------------------------- 1 | import { expectError, expectType } from "tsd"; 2 | import { Redis } from "../../built"; 3 | 4 | const redis = new Redis(); 5 | 6 | // call 7 | expectType>(redis.call("info")); 8 | expectType>(redis.call("set", "foo", "bar")); 9 | expectType>(redis.call("set", ["foo", "bar"])); 10 | expectType>(redis.callBuffer("set", ["foo", "bar"])); 11 | expectType>( 12 | redis.call("set", ["foo", "bar"], (err, value) => { 13 | expectType(err); 14 | expectType(value); 15 | }) 16 | ); 17 | 18 | expectType>( 19 | redis.call("get", "foo", (err, value) => { 20 | expectType(err); 21 | expectType(value); 22 | }) 23 | ); 24 | 25 | expectType>( 26 | redis.call("info", (err, value) => { 27 | expectType(err); 28 | expectType(value); 29 | }) 30 | ); 31 | 32 | // GET 33 | expectType>(redis.get("key")); 34 | expectType>(redis.getBuffer("key")); 35 | expectError(redis.get("key", "bar")); 36 | 37 | // SET 38 | expectType>(redis.set("key", "bar")); 39 | expectType>(redis.set("key", "bar", "EX", 100)); 40 | expectType>(redis.set("key", "bar", "EX", 100, "NX")); // NX can fail thus `null` is returned 41 | expectType>(redis.set("key", "bar", "GET")); 42 | 43 | // DEL 44 | expectType>(redis.del("key")); 45 | expectType>(redis.del(["key1", "key2"])); 46 | expectType>(redis.del("key1", "key2")); 47 | 48 | // INCR 49 | expectType>(redis.incr("key")); 50 | expectType>(redis.incrby("key", 42)); 51 | expectType>(redis.incrby("key", "42")); 52 | expectType>(redis.incrbyfloat("key", "42")); 53 | 54 | // MGET 55 | expectType>(redis.mget("key", "bar")); 56 | expectType>(redis.mget(["key", "bar"])); 57 | 58 | // HGETALL 59 | expectType>>(redis.hgetall("key")); 60 | expectType>>(redis.hgetallBuffer("key")); 61 | 62 | // LPOP 63 | expectType>(redis.lpop("key")); 64 | expectType>(redis.lpopBuffer("key")); 65 | expectType>(redis.lpop("key", 17)); 66 | expectType>(redis.lpopBuffer("key", 17)); 67 | 68 | // LPOS 69 | expectType>(redis.lpos("key", "element")); 70 | expectType>( 71 | redis.lpos("key", "element", "RANK", -1, "COUNT", 2) 72 | ); 73 | 74 | // SRANDMEMBER 75 | expectType>(redis.srandmember("key")); 76 | expectType>(redis.srandmemberBuffer("key")); 77 | expectType>(redis.srandmember("key", 10)); 78 | expectType>(redis.srandmemberBuffer("key", 10)); 79 | 80 | // LMISMEMBER 81 | expectType>(redis.smismember("key", "e1", "e2")); 82 | 83 | // ZADD 84 | expectType>(redis.zadd("key", 1, "member")); 85 | expectType>(redis.zadd("key", "CH", 1, "member")); 86 | 87 | // ZRANDMEMBER 88 | expectType>(redis.zrandmember("key")); 89 | expectType>(redis.zrandmember("key", 20)); 90 | 91 | // ZSCORE 92 | expectType>(redis.zscore("key", "member")); 93 | expectType>(redis.zscoreBuffer("key", "member")); 94 | 95 | // GETRANGE 96 | expectType>(redis.getrangeBuffer("foo", 0, 1)); 97 | 98 | // Callbacks 99 | redis.getBuffer("foo", (err, res) => { 100 | expectType(err); 101 | expectType(res); 102 | }); 103 | 104 | redis.set("foo", "bar", (err, res) => { 105 | expectType(err); 106 | expectType<"OK" | undefined>(res); 107 | }); 108 | 109 | redis.set("foo", "bar", "GET", (err, res) => { 110 | expectType(err); 111 | expectType(res); 112 | }); 113 | 114 | redis.del("key1", "key2", (err, res) => { 115 | expectType(err); 116 | expectType(res); 117 | }); 118 | 119 | redis.del(["key1", "key2"], (err, res) => { 120 | expectType(err); 121 | expectType(res); 122 | }); 123 | -------------------------------------------------------------------------------- /test/typing/events.test-.ts: -------------------------------------------------------------------------------- 1 | import { expectType } from "tsd"; 2 | import { Redis } from "../../built"; 3 | 4 | const redis = new Redis(); 5 | 6 | expectType(redis.on("connect", () => {})); 7 | expectType(redis.on("ready", () => {})); 8 | expectType(redis.on("close", () => {})); 9 | expectType(redis.on("end", () => {})); 10 | expectType( 11 | redis.on("error", (error) => { 12 | expectType(error); 13 | }) 14 | ); 15 | 16 | expectType(redis.once("connect", () => {})); 17 | expectType(redis.once("ready", () => {})); 18 | expectType(redis.once("close", () => {})); 19 | expectType(redis.once("end", () => {})); 20 | expectType( 21 | redis.once("error", (error) => { 22 | expectType(error); 23 | }) 24 | ); 25 | 26 | redis.on("message", (channel, message) => { 27 | expectType(channel); 28 | expectType(message); 29 | }); 30 | 31 | redis.on("messageBuffer", (channel, message) => { 32 | expectType(channel); 33 | expectType(message); 34 | }); 35 | 36 | redis.on("pmessage", (pattern, channel, message) => { 37 | expectType(pattern); 38 | expectType(channel); 39 | expectType(message); 40 | }); 41 | 42 | redis.on("pmessageBuffer", (pattern, channel, message) => { 43 | expectType(pattern); 44 | expectType(channel); 45 | expectType(message); 46 | }); 47 | 48 | redis.once("message", (channel, message) => { 49 | expectType(channel); 50 | expectType(message); 51 | }); 52 | 53 | redis.once("messageBuffer", (channel, message) => { 54 | expectType(channel); 55 | expectType(message); 56 | }); 57 | 58 | redis.once("pmessage", (pattern, channel, message) => { 59 | expectType(pattern); 60 | expectType(channel); 61 | expectType(message); 62 | }); 63 | 64 | redis.once("pmessageBuffer", (pattern, channel, message) => { 65 | expectType(pattern); 66 | expectType(channel); 67 | expectType(message); 68 | }); 69 | -------------------------------------------------------------------------------- /test/typing/options.test-d.ts: -------------------------------------------------------------------------------- 1 | import { expectAssignable, expectType } from "tsd"; 2 | import { Redis, Cluster, NatMap, DNSLookupFunction } from "../../built"; 3 | 4 | expectType(new Redis()); 5 | 6 | // TCP 7 | expectType(new Redis()); 8 | expectType(new Redis(6379)); 9 | expectType(new Redis({ port: 6379 })); 10 | expectType(new Redis({ host: "localhost" })); 11 | expectType(new Redis({ host: "localhost", port: 6379 })); 12 | expectType(new Redis({ host: "localhost", port: 6379, family: 4 })); 13 | expectType(new Redis({ host: "localhost", port: 6379, family: 4 })); 14 | expectType(new Redis(6379, "localhost", { password: "password" })); 15 | 16 | // Socket 17 | expectType(new Redis("/tmp/redis.sock")); 18 | expectType(new Redis("/tmp/redis.sock", { password: "password" })); 19 | 20 | // TLS 21 | expectType(new Redis({ tls: {} })); 22 | expectType(new Redis({ tls: { ca: "myca" } })); 23 | 24 | // Sentinels 25 | expectType( 26 | new Redis({ 27 | sentinels: [{ host: "localhost", port: 16379 }], 28 | sentinelPassword: "password", 29 | }) 30 | ); 31 | 32 | // Cluster 33 | expectType(new Cluster([30001, 30002])); 34 | expectType(new Redis.Cluster([30001, 30002])); 35 | expectType(new Redis.Cluster([30001, "localhost"])); 36 | expectType(new Redis.Cluster([30001, "localhost", { port: 30002 }])); 37 | expectType( 38 | new Redis.Cluster([30001, 30002], { 39 | enableAutoPipelining: true, 40 | }) 41 | ); 42 | 43 | expectAssignable({ 44 | "10.0.1.230:30001": { host: "203.0.113.73", port: 30001 }, 45 | "10.0.1.231:30001": { host: "203.0.113.73", port: 30002 }, 46 | "10.0.1.232:30001": { host: "203.0.113.73", port: 30003 }, 47 | }); 48 | 49 | expectAssignable((address, callback) => 50 | callback(null, address) 51 | ); 52 | -------------------------------------------------------------------------------- /test/typing/pipeline.test-d.ts: -------------------------------------------------------------------------------- 1 | import { expectType } from "tsd"; 2 | import { Redis, Pipeline } from "../../built"; 3 | 4 | const redis = new Redis(); 5 | 6 | type RETURN_TYPE = Promise<[Error | null, unknown][] | null>; 7 | 8 | expectType(redis.pipeline().set("foo", "bar").get("foo").exec()); 9 | 10 | expectType( 11 | redis 12 | .pipeline([ 13 | ["set", "foo", "bar"], 14 | ["get", "foo"], 15 | ]) 16 | .exec() 17 | ); 18 | 19 | expectType( 20 | redis 21 | .pipeline([ 22 | ["set", Buffer.from("foo"), "bar"], 23 | ["incrby", "foo", 42], 24 | ]) 25 | .exec() 26 | ); 27 | 28 | expectType( 29 | redis.pipeline([ 30 | ["set", Buffer.from("foo"), "bar"], 31 | ["incrby", "foo", 42], 32 | ]).length 33 | ); 34 | 35 | expectType(({} as unknown as Pipeline).length); 36 | -------------------------------------------------------------------------------- /test/typing/transformers.test-d.ts: -------------------------------------------------------------------------------- 1 | import { expectType } from "tsd"; 2 | import { Redis } from "../../built"; 3 | 4 | interface User { 5 | name: string; 6 | title: string; 7 | } 8 | 9 | const user: User = { name: "Bob", title: "Engineer" }; 10 | const stringMap = new Map([["key", "value"]]); 11 | const numberMap = new Map([[42, "value"]]); 12 | const bufferMap = new Map([[Buffer.from([0xff]), "value"]]); 13 | const mixedMap = new Map([ 14 | [Buffer.from([0xff]), "value"], 15 | [42, "value"], 16 | ["field", "value"], 17 | ]); 18 | 19 | const redis = new Redis(); 20 | 21 | // mset 22 | expectType>(redis.mset("key1", "value1", "key2", "value2")); 23 | expectType>(redis.mset(user)); 24 | expectType>(redis.mset(stringMap)); 25 | expectType>(redis.mset(numberMap)); 26 | expectType>(redis.mset(bufferMap)); 27 | expectType>(redis.mset(mixedMap)); 28 | 29 | // msetnx 30 | expectType>(redis.msetnx(user)); 31 | expectType>(redis.msetnx(stringMap)); 32 | expectType>(redis.msetnx(numberMap)); 33 | expectType>(redis.msetnx(bufferMap)); 34 | expectType>(redis.msetnx(mixedMap)); 35 | 36 | // hmset 37 | expectType>(redis.hmset("key", user)); 38 | expectType>(redis.hmset("key", stringMap)); 39 | expectType>(redis.hmset("key", numberMap)); 40 | expectType>(redis.hmset("key", bufferMap)); 41 | expectType>(redis.hmset("key", mixedMap)); 42 | 43 | // hset 44 | expectType>(redis.hset("key", user)); 45 | expectType>(redis.hset("key", stringMap)); 46 | expectType>(redis.hset("key", numberMap)); 47 | expectType>(redis.hset("key", bufferMap)); 48 | expectType>(redis.hset("key", mixedMap)); 49 | 50 | // hgetall 51 | expectType>>(redis.hgetall("key")); 52 | -------------------------------------------------------------------------------- /test/unit/DataHandler.ts: -------------------------------------------------------------------------------- 1 | import * as sinon from "sinon"; 2 | import { expect } from "chai"; 3 | import DataHandler from "../../lib/DataHandler"; 4 | 5 | describe("DataHandler", () => { 6 | afterEach(() => { 7 | sinon.restore(); 8 | }); 9 | 10 | describe("constructor()", () => { 11 | it("should add a data handler to the redis stream properly", () => { 12 | const dataHandledable = { 13 | stream: { 14 | prependListener: sinon.spy(), 15 | resume: sinon.spy(), 16 | }, 17 | }; 18 | new DataHandler(dataHandledable, {}); 19 | 20 | expect(dataHandledable.stream.prependListener.calledOnce).to.eql(true); 21 | expect(dataHandledable.stream.resume.calledOnce).to.eql(true); 22 | 23 | expect( 24 | dataHandledable.stream.resume.calledAfter( 25 | dataHandledable.stream.prependListener 26 | ) 27 | ).to.eql(true); 28 | }); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /test/unit/autoPipelining.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { getFirstValueInFlattenedArray } from "../../lib/autoPipelining"; 3 | 4 | describe("autoPipelining", function () { 5 | const expectGetFirstValueIs = (values, expected) => { 6 | expect(getFirstValueInFlattenedArray(values)).to.eql(expected); 7 | }; 8 | 9 | it("should be able to efficiently get array args", function () { 10 | expectGetFirstValueIs([], undefined); 11 | expectGetFirstValueIs([null, "key"], null); 12 | expectGetFirstValueIs(["key", "value"], "key"); 13 | expectGetFirstValueIs([[], "key"], "key"); 14 | expectGetFirstValueIs([["key"]], "key"); 15 | expectGetFirstValueIs([[["key"]]], ["key"]); 16 | expectGetFirstValueIs([0, 1, 2, 3, 4], 0); 17 | expectGetFirstValueIs([[true]], true); 18 | expectGetFirstValueIs([Buffer.from("test")], Buffer.from("test")); 19 | expectGetFirstValueIs([{}], {}); 20 | // lodash.isArguments is true for this legacy js way to get argument lists 21 | const createArguments = function () { 22 | return arguments; 23 | }; 24 | // @ts-expect-error 25 | expectGetFirstValueIs([createArguments(), createArguments("key")], "key"); 26 | // @ts-expect-error 27 | expectGetFirstValueIs([createArguments("")], ""); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /test/unit/clusters/ConnectionPool.ts: -------------------------------------------------------------------------------- 1 | import * as sinon from "sinon"; 2 | import { expect } from "chai"; 3 | import ConnectionPool from "../../../lib/cluster/ConnectionPool"; 4 | 5 | describe("ConnectionPool", () => { 6 | describe("#reset", () => { 7 | it("prefers to master if there are two same node for a slot", () => { 8 | const pool = new ConnectionPool({}); 9 | const stub = sinon.stub(pool, "findOrCreate"); 10 | 11 | pool.reset([ 12 | { host: "127.0.0.1", port: 30001, readOnly: true }, 13 | { host: "127.0.0.1", port: 30001, readOnly: false }, 14 | ]); 15 | 16 | expect(stub.callCount).to.eql(1); 17 | expect(stub.firstCall.args[1]).to.eql(false); 18 | 19 | pool.reset([ 20 | { host: "127.0.0.1", port: 30001, readOnly: false }, 21 | { host: "127.0.0.1", port: 30001, readOnly: true }, 22 | ]); 23 | 24 | expect(stub.callCount).to.eql(2); 25 | expect(stub.firstCall.args[1]).to.eql(false); 26 | }); 27 | 28 | it("remove the node immediately instead of waiting for 'end' event", () => { 29 | const pool = new ConnectionPool({}); 30 | pool.reset([{ host: "127.0.0.1", port: 300001 }]); 31 | expect(pool.getNodes().length).to.eql(1); 32 | 33 | pool.reset([]); 34 | expect(pool.getNodes().length).to.eql(0); 35 | }); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /test/unit/clusters/index.ts: -------------------------------------------------------------------------------- 1 | import { nodeKeyToRedisOptions } from "../../../lib/cluster/util"; 2 | import { Cluster } from "../../../lib"; 3 | import * as sinon from "sinon"; 4 | import { expect } from "chai"; 5 | 6 | describe("cluster", () => { 7 | let stub: sinon.SinonStub | undefined; 8 | beforeEach(() => { 9 | stub = sinon.stub(Cluster.prototype, "connect"); 10 | stub.callsFake(() => Promise.resolve()); 11 | }); 12 | 13 | afterEach(() => { 14 | if (stub) stub.restore(); 15 | }); 16 | 17 | it("should support frozen options", () => { 18 | const options = Object.freeze({ maxRedirections: 1000 }); 19 | const cluster = new Cluster([{ port: 7777 }], options); 20 | expect(cluster.options).to.have.property("maxRedirections", 1000); 21 | expect(cluster.options).to.have.property("scaleReads", "master"); 22 | }); 23 | 24 | it("should allow overriding Commander options", () => { 25 | const cluster = new Cluster([{ port: 7777 }], { 26 | showFriendlyErrorStack: true, 27 | }); 28 | expect(cluster.options).to.have.property("showFriendlyErrorStack", true); 29 | }); 30 | 31 | it("should support passing keyPrefix via redisOptions", () => { 32 | const cluster = new Cluster([{ port: 7777 }], { 33 | redisOptions: { keyPrefix: "prefix:" }, 34 | }); 35 | expect(cluster.options).to.have.property("keyPrefix", "prefix:"); 36 | }); 37 | 38 | it("throws when scaleReads is invalid", () => { 39 | expect(() => { 40 | // @ts-expect-error 41 | new Cluster([{}], { scaleReads: "invalid" }); 42 | }).to.throw(/Invalid option scaleReads/); 43 | }); 44 | 45 | it("disables slotsRefreshTimeout by default", () => { 46 | const cluster = new Cluster([{}]); 47 | expect(cluster.options.slotsRefreshInterval).to.eql(undefined); 48 | }); 49 | 50 | describe("#nodes()", () => { 51 | it("throws when role is invalid", () => { 52 | const cluster = new Cluster([{}]); 53 | expect(() => { 54 | // @ts-expect-error 55 | cluster.nodes("invalid"); 56 | }).to.throw(/Invalid role/); 57 | }); 58 | }); 59 | 60 | 61 | describe("natMapper", () => { 62 | it("returns the original nodeKey if no NAT mapping is provided", () => { 63 | const cluster = new Cluster([]); 64 | const nodeKey = { host: "127.0.0.1", port: 6379 }; 65 | const result = cluster["natMapper"](nodeKey); 66 | 67 | expect(result).to.eql(nodeKey); 68 | }); 69 | 70 | it("maps external IP to internal IP using NAT mapping object", () => { 71 | const natMap = { "203.0.113.1:6379": { host: "127.0.0.1", port: 30000 } }; 72 | const cluster = new Cluster([], { natMap }); 73 | const nodeKey = "203.0.113.1:6379"; 74 | const result = cluster["natMapper"](nodeKey); 75 | expect(result).to.eql({ host: "127.0.0.1", port: 30000 }); 76 | }); 77 | 78 | it("maps external IP to internal IP using NAT mapping function", () => { 79 | const natMap = (key) => { 80 | if (key === "203.0.113.1:6379") { 81 | return { host: "127.0.0.1", port: 30000 }; 82 | } 83 | return null; 84 | }; 85 | const cluster = new Cluster([], { natMap }); 86 | const nodeKey = "203.0.113.1:6379"; 87 | const result = cluster["natMapper"](nodeKey); 88 | expect(result).to.eql({ host: "127.0.0.1", port: 30000 }); 89 | }); 90 | 91 | it("returns the original nodeKey if NAT mapping is invalid", () => { 92 | const natMap = { "invalid:key": { host: "127.0.0.1", port: 30000 } }; 93 | const cluster = new Cluster([], { natMap }); 94 | const nodeKey = "203.0.113.1:6379"; 95 | const result = cluster["natMapper"](nodeKey); 96 | expect(result).to.eql({ host: "203.0.113.1", port: 6379 }); 97 | }); 98 | }); 99 | 100 | }); 101 | 102 | describe("nodeKeyToRedisOptions()", () => { 103 | it("returns correct result", () => { 104 | expect(nodeKeyToRedisOptions("127.0.0.1:6379")).to.eql({ 105 | port: 6379, 106 | host: "127.0.0.1", 107 | }); 108 | expect(nodeKeyToRedisOptions("192.168.1.1:30001")).to.eql({ 109 | port: 30001, 110 | host: "192.168.1.1", 111 | }); 112 | expect(nodeKeyToRedisOptions("::0:6379")).to.eql({ 113 | port: 6379, 114 | host: "::0", 115 | }); 116 | expect(nodeKeyToRedisOptions("0:0:6379")).to.eql({ 117 | port: 6379, 118 | host: "0:0", 119 | }); 120 | }); 121 | }); 122 | -------------------------------------------------------------------------------- /test/unit/commander.ts: -------------------------------------------------------------------------------- 1 | import * as sinon from "sinon"; 2 | import { expect } from "chai"; 3 | import Commander from "../../lib/utils/Commander"; 4 | import Command from "../../lib/Command"; 5 | 6 | describe("Commander", () => { 7 | describe("#getBuiltinCommands()", () => { 8 | it("returns a new copy of commands", () => { 9 | const c = new Commander(); 10 | const commands = c.getBuiltinCommands(); 11 | commands.unshift("abc"); 12 | const commandsNew = c.getBuiltinCommands(); 13 | expect(commands.slice(1)).to.eql(commandsNew); 14 | }); 15 | }); 16 | 17 | describe("#addBuiltinCommand()", () => { 18 | beforeEach(() => sinon.stub(Commander.prototype, "sendCommand")); 19 | afterEach(() => sinon.restore()); 20 | it("adds string command", () => { 21 | const c = new Commander(); 22 | c.addBuiltinCommand("someCommand"); 23 | c.someCommand(); 24 | const command = Commander.prototype.sendCommand.getCall(0).args[0]; 25 | expect(command.name).to.eql("someCommand"); 26 | expect(command.replyEncoding).to.eql("utf8"); 27 | }); 28 | 29 | it("adds buffer command", () => { 30 | const c = new Commander(); 31 | c.addBuiltinCommand("someCommand"); 32 | c.someCommandBuffer(); 33 | const command = Commander.prototype.sendCommand.getCall(0).args[0]; 34 | expect(command.name).to.eql("someCommand"); 35 | expect(command.replyEncoding).to.eql(null); 36 | }); 37 | }); 38 | 39 | it("should pass the correct arguments", () => { 40 | sinon.stub(Commander.prototype, "sendCommand").callsFake((command) => { 41 | return command; 42 | }); 43 | 44 | let command; 45 | 46 | const c = new Commander(); 47 | command = c.call("set", "foo", "bar"); 48 | expect(command.name).to.eql("set"); 49 | expect(command.args[0]).to.eql("foo"); 50 | expect(command.args[1]).to.eql("bar"); 51 | 52 | command = c.callBuffer("set", ["foo", "bar"]); 53 | expect(command.name).to.eql("set"); 54 | expect(command.args[0]).to.eql("foo"); 55 | expect(command.args[1]).to.eql("bar"); 56 | 57 | command = c.call("set", "foo", "bar", () => {}); 58 | expect(command.name).to.eql("set"); 59 | expect(command.args.length).to.eql(2); 60 | 61 | command = c.callBuffer("set", "foo", "bar", () => {}); 62 | expect(command.name).to.eql("set"); 63 | expect(command.args.length).to.eql(2); 64 | 65 | Commander.prototype.sendCommand.restore(); 66 | }); 67 | 68 | describe("#zscan", () => { 69 | it("should pass noscores option", async (done) => { 70 | const args: any[] = [ 71 | "key", 72 | "0", 73 | "MATCH", 74 | "pattern", 75 | "COUNT", 76 | "10", 77 | "noscores", 78 | ]; 79 | sinon.stub(Commander.prototype, "sendCommand").callsFake((command) => { 80 | if (command.args.every((arg, index) => arg === args[index])) { 81 | return done(); 82 | } 83 | return done(new Error(`args should be ${args.join(", ")}`)); 84 | }); 85 | const c = new Commander(); 86 | 87 | await c.zscan( 88 | args[0], 89 | args[1], 90 | args[2], 91 | args[3], 92 | args[4], 93 | args[5], 94 | args[6] 95 | ); 96 | (Commander.prototype.sendCommand as any).restore(); 97 | }); 98 | }); 99 | }); 100 | -------------------------------------------------------------------------------- /test/unit/connectors/SentinelConnector/SentinelIterator.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import SentinelIterator from "../../../../lib/connectors/SentinelConnector/SentinelIterator"; 3 | 4 | describe("SentinelIterator", () => { 5 | it("keep the options immutable", () => { 6 | function getSentinels() { 7 | return [{ host: "127.0.0.1", port: 30001 }]; 8 | } 9 | const sentinels = getSentinels(); 10 | 11 | const iter = new SentinelIterator(sentinels); 12 | iter.add({ host: "127.0..0.1", port: 30002 }); 13 | 14 | expect(sentinels).to.eql(getSentinels()); 15 | expect(iter.next().value.port).to.eql(30001); 16 | expect(iter.next().value.port).to.eql(30002); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /test/unit/connectors/connector.ts: -------------------------------------------------------------------------------- 1 | import * as net from "net"; 2 | import * as tls from "tls"; 3 | import { StandaloneConnector } from "../../../lib/connectors"; 4 | import * as sinon from "sinon"; 5 | import { expect } from "chai"; 6 | 7 | describe("StandaloneConnector", () => { 8 | describe("connect()", () => { 9 | it("first tries path", async () => { 10 | const spy = sinon.spy(net, "createConnection"); 11 | const connector = new StandaloneConnector({ port: 6379, path: "/tmp" }); 12 | try { 13 | const stream = await connector.connect(() => {}); 14 | stream.on("error", () => {}); 15 | } catch (err) { 16 | // ignore errors 17 | } 18 | expect(spy.calledOnce).to.eql(true); 19 | connector.disconnect(); 20 | }); 21 | 22 | it("ignore path when port is set and path is null", async () => { 23 | const spy = sinon.spy(net, "createConnection"); 24 | const connector = new StandaloneConnector({ port: 6379, path: null }); 25 | await connector.connect(() => {}); 26 | expect(spy.calledOnce).to.eql(true); 27 | expect(spy.firstCall.args[0]).to.eql({ port: 6379 }); 28 | connector.disconnect(); 29 | }); 30 | 31 | it("supports tls", async () => { 32 | const spy = sinon.spy(tls, "connect"); 33 | const connector = new StandaloneConnector({ 34 | port: 6379, 35 | tls: { ca: "on", servername: "localhost", rejectUnauthorized: false }, 36 | }); 37 | await connector.connect(() => {}); 38 | expect(spy.calledOnce).to.eql(true); 39 | expect(spy.firstCall.args[0]).to.eql({ 40 | port: 6379, 41 | ca: "on", 42 | servername: "localhost", 43 | rejectUnauthorized: false, 44 | }); 45 | connector.disconnect(); 46 | }); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /test/unit/debug.ts: -------------------------------------------------------------------------------- 1 | import rDebug = require("debug"); 2 | import * as sinon from "sinon"; 3 | import { expect } from "chai"; 4 | import debug, { 5 | getStringValue, 6 | MAX_ARGUMENT_LENGTH, 7 | } from "../../lib/utils/debug"; 8 | 9 | describe("utils/debug", () => { 10 | afterEach(() => { 11 | rDebug.enable(process.env.DEBUG || ""); 12 | }); 13 | 14 | describe(".exports.getStringValue", () => { 15 | it("should return a string or undefined", () => { 16 | expect(getStringValue(true)).to.be.undefined; 17 | expect(getStringValue(undefined)).to.be.undefined; 18 | expect(getStringValue(null)).to.be.undefined; 19 | expect(getStringValue(false)).to.be.undefined; 20 | expect(getStringValue(1)).to.be.undefined; 21 | expect(getStringValue(1.1)).to.be.undefined; 22 | expect(getStringValue(-1)).to.be.undefined; 23 | expect(getStringValue(-1.1)).to.be.undefined; 24 | 25 | expect(getStringValue("abc")).to.be.a("string"); 26 | expect( 27 | getStringValue(Buffer.from ? Buffer.from("abc") : Buffer.from("abc")) 28 | ).to.be.a("string"); 29 | expect(getStringValue(new Date())).to.be.a("string"); 30 | expect(getStringValue({ foo: { bar: "qux" } })).to.be.a("string"); 31 | }); 32 | }); 33 | 34 | describe(".exports", () => { 35 | it("should return a function", () => { 36 | expect(debug("test")).to.be.a("function"); 37 | }); 38 | 39 | it("should output to console if DEBUG is set", () => { 40 | const dbgNS = "ioredis:debugtest"; 41 | 42 | rDebug.enable(dbgNS); 43 | 44 | const logspy = sinon.spy(); 45 | const fn = debug("debugtest"); 46 | 47 | // @ts-expect-error 48 | fn.log = logspy; 49 | 50 | // @ts-expect-error 51 | expect(fn.enabled).to.equal(true); 52 | // @ts-expect-error 53 | expect(fn.namespace).to.equal(dbgNS); 54 | 55 | let data = [], 56 | i = 0; 57 | 58 | while (i < 1000) { 59 | data.push(String(i)); 60 | i += 1; 61 | } 62 | 63 | const datastr = JSON.stringify(data); 64 | 65 | fn("my message %s", { json: data }); 66 | expect(logspy.called).to.equal(true); 67 | 68 | const args = logspy.getCall(0).args; 69 | 70 | const wantedArglen = 71 | 30 + // " ... " 72 | MAX_ARGUMENT_LENGTH + // max-length of redacted string 73 | datastr.length.toString().length; // length of string of string length (inception much?) 74 | 75 | expect(args.length).to.be.above(1); 76 | expect(args[1]).to.be.a("string"); 77 | expect(args[1].length).to.equal(wantedArglen); 78 | }); 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /test/unit/index.ts: -------------------------------------------------------------------------------- 1 | import * as sinon from "sinon"; 2 | import { expect } from "chai"; 3 | import { print } from "../../lib"; 4 | 5 | describe("index", () => { 6 | describe("print()", () => { 7 | it("prints logs", () => { 8 | const stub = sinon.stub(console, "log"); 9 | print(new Error("err")); 10 | print(null, "success"); 11 | expect(stub.calledTwice).to.eql(true); 12 | stub.restore(); 13 | }); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /test/unit/pipeline.ts: -------------------------------------------------------------------------------- 1 | import * as sinon from "sinon"; 2 | import { expect } from "chai"; 3 | import Pipeline from "../../lib/Pipeline"; 4 | import Commander from "../../lib/utils/Commander"; 5 | import Redis from "../../lib/Redis"; 6 | 7 | describe("Pipeline", () => { 8 | beforeEach(() => { 9 | sinon.stub(Redis.prototype, "connect").resolves(); 10 | sinon.stub(Commander.prototype, "sendCommand").callsFake((command) => { 11 | return command; 12 | }); 13 | }); 14 | 15 | afterEach(() => { 16 | sinon.restore(); 17 | }); 18 | 19 | it("should properly mark commands as transactions", () => { 20 | const redis = new Redis(); 21 | const p = new Pipeline(redis); 22 | let i = 0; 23 | 24 | function validate(name, inTransaction) { 25 | const command = p._queue[i++]; 26 | expect(command.name).to.eql(name); 27 | expect(command.inTransaction).to.eql(inTransaction); 28 | } 29 | 30 | p.get(); 31 | p.multi(); 32 | p.get(); 33 | p.multi(); 34 | p.exec(); 35 | p.exec(); 36 | p.get(); 37 | 38 | validate("get", false); 39 | validate("multi", true); 40 | validate("get", true); 41 | validate("multi", true); 42 | validate("exec", true); 43 | validate("exec", false); 44 | validate("get", false); 45 | }); 46 | 47 | it("should properly set pipelineIndex on commands", () => { 48 | const redis = new Redis(); 49 | const p = new Pipeline(redis); 50 | let i = 0; 51 | 52 | function validate(name) { 53 | const command = p._queue[i]; 54 | expect(command.name).to.eql(name); 55 | expect(command.pipelineIndex).to.eql(i); 56 | i++; 57 | } 58 | 59 | p.get(); 60 | p.set(); 61 | p.del(); 62 | p.ping(); 63 | 64 | validate("get"); 65 | validate("set"); 66 | validate("del"); 67 | validate("ping"); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2019", 4 | "declaration": true, 5 | "lib": [ 6 | "es2019", 7 | "es2020.bigint", 8 | "es2020.string", 9 | "es2020.symbol.wellknown" 10 | ], 11 | "moduleResolution": "node", 12 | "module": "commonjs", 13 | "outDir": "./built" 14 | }, 15 | "include": ["./lib/**/*"] 16 | } 17 | --------------------------------------------------------------------------------