You've already forked node-redis
mirror of
https://github.com/redis/node-redis.git
synced 2025-08-04 15:02:09 +03:00
Update doctest client with latest v4 release (#2844)
This commit is contained in:
39
.github/ISSUE_TEMPLATE/BUG-REPORT.yml
vendored
Normal file
39
.github/ISSUE_TEMPLATE/BUG-REPORT.yml
vendored
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
name: Bug Report
|
||||||
|
description: Tell us about something that isn't working as expected
|
||||||
|
labels: [Bug]
|
||||||
|
body:
|
||||||
|
- type: textarea
|
||||||
|
id: description
|
||||||
|
attributes:
|
||||||
|
label: Description
|
||||||
|
description: Please enter a detailed description of your issue. If possible, please provide example code to reproduce the issue.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: input
|
||||||
|
id: node-js-version
|
||||||
|
attributes:
|
||||||
|
label: Node.js Version
|
||||||
|
description: Please enter your Node.js version `node --version`
|
||||||
|
- type: input
|
||||||
|
id: redis-server-version
|
||||||
|
attributes:
|
||||||
|
label: Redis Server Version
|
||||||
|
description: Please enter your Redis server version ([`INFO server`](https://redis.io/commands/info/))
|
||||||
|
- type: input
|
||||||
|
id: node-redis-version
|
||||||
|
attributes:
|
||||||
|
label: Node Redis Version
|
||||||
|
description: Please enter your node redis version `npm ls redis`
|
||||||
|
- type: input
|
||||||
|
id: platform
|
||||||
|
attributes:
|
||||||
|
label: Platform
|
||||||
|
description: Please enter the platform you are using e.g. Linux, macOS, Windows
|
||||||
|
- type: textarea
|
||||||
|
id: logs
|
||||||
|
attributes:
|
||||||
|
label: Logs
|
||||||
|
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
|
||||||
|
render: bash
|
||||||
|
validations:
|
||||||
|
required: false
|
11
.github/ISSUE_TEMPLATE/DOCUMENTATION.yml
vendored
Normal file
11
.github/ISSUE_TEMPLATE/DOCUMENTATION.yml
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
name: Documentation
|
||||||
|
description: Any questions or issues relating to the project documentation.
|
||||||
|
labels: [Documentation]
|
||||||
|
body:
|
||||||
|
- type: textarea
|
||||||
|
id: description
|
||||||
|
attributes:
|
||||||
|
label: Description
|
||||||
|
description: Ask your question or describe your issue here.
|
||||||
|
validations:
|
||||||
|
required: true
|
19
.github/ISSUE_TEMPLATE/FEATURE-REQUEST.yml
vendored
Normal file
19
.github/ISSUE_TEMPLATE/FEATURE-REQUEST.yml
vendored
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
name: Feature Request
|
||||||
|
description: Suggest an idea for this project
|
||||||
|
labels: [Feature]
|
||||||
|
body:
|
||||||
|
- type: textarea
|
||||||
|
id: motivation
|
||||||
|
attributes:
|
||||||
|
label: Motivation
|
||||||
|
description: How would Node Redis users benefit from this feature?
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: basic-code-example
|
||||||
|
attributes:
|
||||||
|
label: Basic Code Example
|
||||||
|
description: Provide examples of how you imagine the API for this feature might be implemented. This will be automatically formatted into code, so no need for backticks.
|
||||||
|
render: JavaScript
|
||||||
|
validations:
|
||||||
|
required: false
|
15
.github/ISSUE_TEMPLATE/bug-report.md
vendored
15
.github/ISSUE_TEMPLATE/bug-report.md
vendored
@@ -1,15 +0,0 @@
|
|||||||
---
|
|
||||||
name: Bug report
|
|
||||||
about: Create a report to help us improve
|
|
||||||
title: ''
|
|
||||||
labels: Bug
|
|
||||||
assignees: ''
|
|
||||||
---
|
|
||||||
|
|
||||||
<!-- Describe your issue here -->
|
|
||||||
|
|
||||||
**Environment:**
|
|
||||||
- **Node.js Version**: <!-- e.g. "node --version" -->
|
|
||||||
- **Redis Server Version**: <!-- e.g. "redis-server --version" -->
|
|
||||||
- **Node Redis Version**: <!-- e.g. "npm ls redis" -->
|
|
||||||
- **Platform**: <!-- e.g. Ubuntu 20.04.3, Windows 10, Mac OS 11.6 -->
|
|
7
.github/ISSUE_TEMPLATE/feature-request.md
vendored
7
.github/ISSUE_TEMPLATE/feature-request.md
vendored
@@ -1,7 +0,0 @@
|
|||||||
---
|
|
||||||
name: Feature request
|
|
||||||
about: Suggest an idea for this project
|
|
||||||
title: ''
|
|
||||||
labels: Bug
|
|
||||||
assignees: ''
|
|
||||||
---
|
|
50
.github/release-drafter/bloom-config.yml
vendored
Normal file
50
.github/release-drafter/bloom-config.yml
vendored
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
name-template: 'bloom@$NEXT_PATCH_VERSION'
|
||||||
|
tag-template: 'bloom@$NEXT_PATCH_VERSION'
|
||||||
|
autolabeler:
|
||||||
|
- label: 'chore'
|
||||||
|
files:
|
||||||
|
- '*.md'
|
||||||
|
- '.github/*'
|
||||||
|
- label: 'bug'
|
||||||
|
branch:
|
||||||
|
- '/bug-.+'
|
||||||
|
- label: 'chore'
|
||||||
|
branch:
|
||||||
|
- '/chore-.+'
|
||||||
|
- label: 'feature'
|
||||||
|
branch:
|
||||||
|
- '/feature-.+'
|
||||||
|
categories:
|
||||||
|
- title: 'Breaking Changes'
|
||||||
|
labels:
|
||||||
|
- 'breakingchange'
|
||||||
|
- title: '🚀 New Features'
|
||||||
|
labels:
|
||||||
|
- 'feature'
|
||||||
|
- 'enhancement'
|
||||||
|
- title: '🐛 Bug Fixes'
|
||||||
|
labels:
|
||||||
|
- 'fix'
|
||||||
|
- 'bugfix'
|
||||||
|
- 'bug'
|
||||||
|
- title: '🧰 Maintenance'
|
||||||
|
label:
|
||||||
|
- 'chore'
|
||||||
|
- 'maintenance'
|
||||||
|
- 'documentation'
|
||||||
|
- 'docs'
|
||||||
|
|
||||||
|
change-template: '- $TITLE (#$NUMBER)'
|
||||||
|
include-paths:
|
||||||
|
- 'packages/bloom'
|
||||||
|
exclude-labels:
|
||||||
|
- 'skip-changelog'
|
||||||
|
template: |
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
$CHANGES
|
||||||
|
|
||||||
|
## Contributors
|
||||||
|
We'd like to thank all the contributors who worked on this release!
|
||||||
|
|
||||||
|
$CONTRIBUTORS
|
49
.github/release-drafter/graph-config.yml
vendored
Normal file
49
.github/release-drafter/graph-config.yml
vendored
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
name-template: 'graph@$NEXT_PATCH_VERSION'
|
||||||
|
tag-template: 'graph@$NEXT_PATCH_VERSION'
|
||||||
|
autolabeler:
|
||||||
|
- label: 'chore'
|
||||||
|
files:
|
||||||
|
- '*.md'
|
||||||
|
- '.github/*'
|
||||||
|
- label: 'bug'
|
||||||
|
branch:
|
||||||
|
- '/bug-.+'
|
||||||
|
- label: 'chore'
|
||||||
|
branch:
|
||||||
|
- '/chore-.+'
|
||||||
|
- label: 'feature'
|
||||||
|
branch:
|
||||||
|
- '/feature-.+'
|
||||||
|
categories:
|
||||||
|
- title: 'Breaking Changes'
|
||||||
|
labels:
|
||||||
|
- 'breakingchange'
|
||||||
|
- title: '🚀 New Features'
|
||||||
|
labels:
|
||||||
|
- 'feature'
|
||||||
|
- 'enhancement'
|
||||||
|
- title: '🐛 Bug Fixes'
|
||||||
|
labels:
|
||||||
|
- 'fix'
|
||||||
|
- 'bugfix'
|
||||||
|
- 'bug'
|
||||||
|
- title: '🧰 Maintenance'
|
||||||
|
label:
|
||||||
|
- 'chore'
|
||||||
|
- 'maintenance'
|
||||||
|
- 'documentation'
|
||||||
|
- 'docs'
|
||||||
|
change-template: '- $TITLE (#$NUMBER)'
|
||||||
|
include-paths:
|
||||||
|
- 'packages/graph'
|
||||||
|
exclude-labels:
|
||||||
|
- 'skip-changelog'
|
||||||
|
template: |
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
$CHANGES
|
||||||
|
|
||||||
|
## Contributors
|
||||||
|
We'd like to thank all the contributors who worked on this release!
|
||||||
|
|
||||||
|
$CONTRIBUTORS
|
@@ -1,5 +1,5 @@
|
|||||||
name-template: 'Version $NEXT_PATCH_VERSION'
|
name-template: 'json@$NEXT_PATCH_VERSION'
|
||||||
tag-template: 'v$NEXT_PATCH_VERSION'
|
tag-template: 'json@$NEXT_PATCH_VERSION'
|
||||||
autolabeler:
|
autolabeler:
|
||||||
- label: 'chore'
|
- label: 'chore'
|
||||||
files:
|
files:
|
||||||
@@ -28,8 +28,15 @@ categories:
|
|||||||
- 'bugfix'
|
- 'bugfix'
|
||||||
- 'bug'
|
- 'bug'
|
||||||
- title: '🧰 Maintenance'
|
- title: '🧰 Maintenance'
|
||||||
label: 'chore'
|
label:
|
||||||
|
- 'chore'
|
||||||
|
- 'maintenance'
|
||||||
|
- 'documentation'
|
||||||
|
- 'docs'
|
||||||
|
|
||||||
change-template: '- $TITLE (#$NUMBER)'
|
change-template: '- $TITLE (#$NUMBER)'
|
||||||
|
include-paths:
|
||||||
|
- 'packages/json'
|
||||||
exclude-labels:
|
exclude-labels:
|
||||||
- 'skip-changelog'
|
- 'skip-changelog'
|
||||||
template: |
|
template: |
|
50
.github/release-drafter/search-config.yml
vendored
Normal file
50
.github/release-drafter/search-config.yml
vendored
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
name-template: 'search@$NEXT_PATCH_VERSION'
|
||||||
|
tag-template: 'search@$NEXT_PATCH_VERSION'
|
||||||
|
autolabeler:
|
||||||
|
- label: 'chore'
|
||||||
|
files:
|
||||||
|
- '*.md'
|
||||||
|
- '.github/*'
|
||||||
|
- label: 'bug'
|
||||||
|
branch:
|
||||||
|
- '/bug-.+'
|
||||||
|
- label: 'chore'
|
||||||
|
branch:
|
||||||
|
- '/chore-.+'
|
||||||
|
- label: 'feature'
|
||||||
|
branch:
|
||||||
|
- '/feature-.+'
|
||||||
|
categories:
|
||||||
|
- title: 'Breaking Changes'
|
||||||
|
labels:
|
||||||
|
- 'breakingchange'
|
||||||
|
- title: '🚀 New Features'
|
||||||
|
labels:
|
||||||
|
- 'feature'
|
||||||
|
- 'enhancement'
|
||||||
|
- title: '🐛 Bug Fixes'
|
||||||
|
labels:
|
||||||
|
- 'fix'
|
||||||
|
- 'bugfix'
|
||||||
|
- 'bug'
|
||||||
|
- title: '🧰 Maintenance'
|
||||||
|
label:
|
||||||
|
- 'chore'
|
||||||
|
- 'maintenance'
|
||||||
|
- 'documentation'
|
||||||
|
- 'docs'
|
||||||
|
|
||||||
|
change-template: '- $TITLE (#$NUMBER)'
|
||||||
|
include-paths:
|
||||||
|
- 'packages/search'
|
||||||
|
exclude-labels:
|
||||||
|
- 'skip-changelog'
|
||||||
|
template: |
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
$CHANGES
|
||||||
|
|
||||||
|
## Contributors
|
||||||
|
We'd like to thank all the contributors who worked on this release!
|
||||||
|
|
||||||
|
$CONTRIBUTORS
|
49
.github/release-drafter/time-series-config.yml
vendored
Normal file
49
.github/release-drafter/time-series-config.yml
vendored
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
name-template: 'time-series@$NEXT_PATCH_VERSION'
|
||||||
|
tag-template: 'time-series@$NEXT_PATCH_VERSION'
|
||||||
|
autolabeler:
|
||||||
|
- label: 'chore'
|
||||||
|
files:
|
||||||
|
- '*.md'
|
||||||
|
- '.github/*'
|
||||||
|
- label: 'bug'
|
||||||
|
branch:
|
||||||
|
- '/bug-.+'
|
||||||
|
- label: 'chore'
|
||||||
|
branch:
|
||||||
|
- '/chore-.+'
|
||||||
|
- label: 'feature'
|
||||||
|
branch:
|
||||||
|
- '/feature-.+'
|
||||||
|
categories:
|
||||||
|
- title: 'Breaking Changes'
|
||||||
|
labels:
|
||||||
|
- 'breakingchange'
|
||||||
|
- title: '🚀 New Features'
|
||||||
|
labels:
|
||||||
|
- 'feature'
|
||||||
|
- 'enhancement'
|
||||||
|
- title: '🐛 Bug Fixes'
|
||||||
|
labels:
|
||||||
|
- 'fix'
|
||||||
|
- 'bugfix'
|
||||||
|
- 'bug'
|
||||||
|
- title: '🧰 Maintenance'
|
||||||
|
label:
|
||||||
|
- 'chore'
|
||||||
|
- 'maintenance'
|
||||||
|
- 'documentation'
|
||||||
|
- 'docs'
|
||||||
|
change-template: '- $TITLE (#$NUMBER)'
|
||||||
|
include-paths:
|
||||||
|
- 'packages/time-series'
|
||||||
|
exclude-labels:
|
||||||
|
- 'skip-changelog'
|
||||||
|
template: |
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
$CHANGES
|
||||||
|
|
||||||
|
## Contributors
|
||||||
|
We'd like to thank all the contributors who worked on this release!
|
||||||
|
|
||||||
|
$CONTRIBUTORS
|
2
.github/workflows/codeql.yml
vendored
2
.github/workflows/codeql.yml
vendored
@@ -38,7 +38,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
|
4
.github/workflows/documentation.yml
vendored
4
.github/workflows/documentation.yml
vendored
@@ -10,11 +10,11 @@ jobs:
|
|||||||
documentation:
|
documentation:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2.3.4
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
- name: Use Node.js
|
- name: Use Node.js
|
||||||
uses: actions/setup-node@v2.3.0
|
uses: actions/setup-node@v3
|
||||||
- name: Install Packages
|
- name: Install Packages
|
||||||
run: npm ci
|
run: npm ci
|
||||||
- name: Build tests tools
|
- name: Build tests tools
|
||||||
|
24
.github/workflows/release-drafter-bloom.yml
vendored
Normal file
24
.github/workflows/release-drafter-bloom.yml
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
name: Release Drafter
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
# branches to consider in the event; optional, defaults to all
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
|
||||||
|
update_release_draft:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
pull-requests: write
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
# Drafts your next Release notes as Pull Requests are merged into "master"
|
||||||
|
- uses: release-drafter/release-drafter@v5
|
||||||
|
with:
|
||||||
|
# (Optional) specify config name to use, relative to .github/. Default: release-drafter.yml
|
||||||
|
config-name: release-drafter/bloom-config.yml
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
24
.github/workflows/release-drafter-graph.yml
vendored
Normal file
24
.github/workflows/release-drafter-graph.yml
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
name: Release Drafter
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
# branches to consider in the event; optional, defaults to all
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
|
||||||
|
update_release_draft:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
pull-requests: write
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
# Drafts your next Release notes as Pull Requests are merged into "master"
|
||||||
|
- uses: release-drafter/release-drafter@v5
|
||||||
|
with:
|
||||||
|
# (Optional) specify config name to use, relative to .github/. Default: release-drafter.yml
|
||||||
|
config-name: release-drafter/graph-config.yml
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
@@ -7,13 +7,18 @@ on:
|
|||||||
- master
|
- master
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|
||||||
update_release_draft:
|
update_release_draft:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
pull-requests: write
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
# Drafts your next Release notes as Pull Requests are merged into "master"
|
# Drafts your next Release notes as Pull Requests are merged into "master"
|
||||||
- uses: release-drafter/release-drafter@v5
|
- uses: release-drafter/release-drafter@v5
|
||||||
with:
|
with:
|
||||||
# (Optional) specify config name to use, relative to .github/. Default: release-drafter.yml
|
# (Optional) specify config name to use, relative to .github/. Default: release-drafter.yml
|
||||||
config-name: release-drafter-config.yml
|
config-name: release-drafter/json-config.yml
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
24
.github/workflows/release-drafter-search.yml
vendored
Normal file
24
.github/workflows/release-drafter-search.yml
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
name: Release Drafter
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
# branches to consider in the event; optional, defaults to all
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
|
||||||
|
update_release_draft:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
pull-requests: write
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
# Drafts your next Release notes as Pull Requests are merged into "master"
|
||||||
|
- uses: release-drafter/release-drafter@v5
|
||||||
|
with:
|
||||||
|
# (Optional) specify config name to use, relative to .github/. Default: release-drafter.yml
|
||||||
|
config-name: release-drafter/search-config.yml
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
24
.github/workflows/release-drafter-time-series.yml
vendored
Normal file
24
.github/workflows/release-drafter-time-series.yml
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
name: Release Drafter
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
# branches to consider in the event; optional, defaults to all
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
|
||||||
|
update_release_draft:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
pull-requests: write
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
# Drafts your next Release notes as Pull Requests are merged into "master"
|
||||||
|
- uses: release-drafter/release-drafter@v5
|
||||||
|
with:
|
||||||
|
# (Optional) specify config name to use, relative to .github/. Default: release-drafter.yml
|
||||||
|
config-name: release-drafter/time-series-config.yml
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
8
.github/workflows/tests.yml
vendored
8
.github/workflows/tests.yml
vendored
@@ -16,14 +16,14 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
node-version: ['14', '16', '18', '19']
|
node-version: ['18', '20']
|
||||||
redis-version: ['5', '6.0', '6.2', '7.0']
|
redis-version: ['5', '6.0', '6.2', '7.0', '7.2', '7.4-rc2']
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2.3.4
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
- name: Use Node.js ${{ matrix.node-version }}
|
- name: Use Node.js ${{ matrix.node-version }}
|
||||||
uses: actions/setup-node@v2.3.0
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
node-version: ${{ matrix.node-version }}
|
||||||
- name: Update npm
|
- name: Update npm
|
||||||
|
35
LICENSE
35
LICENSE
@@ -1,24 +1,21 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2016-present Node Redis contributors.
|
Copyright (c) 2022-2023, Redis, inc.
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
obtaining a copy of this software and associated documentation
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
files (the "Software"), to deal in the Software without
|
in the Software without restriction, including without limitation the rights
|
||||||
restriction, including without limitation the rights to use,
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
copies of the Software, and to permit persons to whom the
|
furnished to do so, subject to the following conditions:
|
||||||
Software is furnished to do so, subject to the following
|
|
||||||
conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be
|
The above copyright notice and this permission notice shall be included in all
|
||||||
included in all copies or substantial portions of the Software.
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
SOFTWARE.
|
||||||
OTHER DEALINGS IN THE SOFTWARE.
|
|
||||||
|
82
README.md
82
README.md
@@ -11,6 +11,19 @@
|
|||||||
|
|
||||||
node-redis is a modern, high performance [Redis](https://redis.io) client for Node.js.
|
node-redis is a modern, high performance [Redis](https://redis.io) client for Node.js.
|
||||||
|
|
||||||
|
## How do I Redis?
|
||||||
|
|
||||||
|
[Learn for free at Redis University](https://university.redis.com/)
|
||||||
|
|
||||||
|
[Build faster with the Redis Launchpad](https://launchpad.redis.com/)
|
||||||
|
|
||||||
|
[Try the Redis Cloud](https://redis.com/try-free/)
|
||||||
|
|
||||||
|
[Dive in developer tutorials](https://developer.redis.com/)
|
||||||
|
|
||||||
|
[Join the Redis community](https://redis.com/community/)
|
||||||
|
|
||||||
|
[Work at Redis](https://redis.com/company/careers/jobs/)
|
||||||
|
|
||||||
## Packages
|
## Packages
|
||||||
|
|
||||||
@@ -51,11 +64,9 @@ Looking for a high-level library to handle object mapping? See [redis-om-node](h
|
|||||||
```typescript
|
```typescript
|
||||||
import { createClient } from 'redis';
|
import { createClient } from 'redis';
|
||||||
|
|
||||||
const client = createClient();
|
const client = await createClient()
|
||||||
|
.on('error', err => console.log('Redis Client Error', err))
|
||||||
client.on('error', (err) => console.log('Redis Client Error', err));
|
.connect();
|
||||||
|
|
||||||
await client.connect();
|
|
||||||
|
|
||||||
await client.set('key', 'value');
|
await client.set('key', 'value');
|
||||||
const value = await client.get('key');
|
const value = await client.get('key');
|
||||||
@@ -166,47 +177,7 @@ To learn more about isolated execution, check out the [guide](./docs/isolated-ex
|
|||||||
|
|
||||||
### Pub/Sub
|
### Pub/Sub
|
||||||
|
|
||||||
Subscribing to a channel requires a dedicated stand-alone connection. You can easily get one by `.duplicate()`ing an existing Redis connection.
|
See the [Pub/Sub overview](./docs/pub-sub.md).
|
||||||
|
|
||||||
```typescript
|
|
||||||
const subscriber = client.duplicate();
|
|
||||||
|
|
||||||
await subscriber.connect();
|
|
||||||
```
|
|
||||||
|
|
||||||
Once you have one, simply subscribe and unsubscribe as needed:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
await subscriber.subscribe('channel', (message) => {
|
|
||||||
console.log(message); // 'message'
|
|
||||||
});
|
|
||||||
|
|
||||||
await subscriber.pSubscribe('channe*', (message, channel) => {
|
|
||||||
console.log(message, channel); // 'message', 'channel'
|
|
||||||
});
|
|
||||||
|
|
||||||
await subscriber.unsubscribe('channel');
|
|
||||||
|
|
||||||
await subscriber.pUnsubscribe('channe*');
|
|
||||||
```
|
|
||||||
|
|
||||||
Publish a message on a channel:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
await publisher.publish('channel', 'message');
|
|
||||||
```
|
|
||||||
|
|
||||||
There is support for buffers as well:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
await subscriber.subscribe('channel', (message) => {
|
|
||||||
console.log(message); // <Buffer 6d 65 73 73 61 67 65>
|
|
||||||
}, true);
|
|
||||||
|
|
||||||
await subscriber.pSubscribe('channe*', (message, channel) => {
|
|
||||||
console.log(message, channel); // <Buffer 6d 65 73 73 61 67 65>, <Buffer 63 68 61 6e 6e 65 6c>
|
|
||||||
}, true);
|
|
||||||
```
|
|
||||||
|
|
||||||
### Scan Iterator
|
### Scan Iterator
|
||||||
|
|
||||||
@@ -373,15 +344,18 @@ Check out the [Clustering Guide](./docs/clustering.md) when using Node Redis to
|
|||||||
|
|
||||||
The Node Redis client class is an Nodejs EventEmitter and it emits an event each time the network status changes:
|
The Node Redis client class is an Nodejs EventEmitter and it emits an event each time the network status changes:
|
||||||
|
|
||||||
| Event name | Scenes | Arguments to be passed to the listener |
|
| Name | When | Listener arguments |
|
||||||
|----------------|-------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------|
|
|-------------------------|------------------------------------------------------------------------------------|------------------------------------------------------------|
|
||||||
| `connect` | The client is initiating a connection to the server. | _No argument_ |
|
| `connect` | Initiating a connection to the server | *No arguments* |
|
||||||
| `ready` | The client successfully initiated the connection to the server. | _No argument_ |
|
| `ready` | Client is ready to use | *No arguments* |
|
||||||
| `end` | The client disconnected the connection to the server via `.quit()` or `.disconnect()`. | _No argument_ |
|
| `end` | Connection has been closed (via `.quit()` or `.disconnect()`) | *No arguments* |
|
||||||
| `error` | When a network error has occurred, such as unable to connect to the server or the connection closed unexpectedly. | 1 argument: The error object, such as `SocketClosedUnexpectedlyError: Socket closed unexpectedly` or `Error: connect ECONNREFUSED [IP]:[PORT]` |
|
| `error` | An error has occurred—usually a network issue such as "Socket closed unexpectedly" | `(error: Error)` |
|
||||||
| `reconnecting` | The client is trying to reconnect to the server. | _No argument_ |
|
| `reconnecting` | Client is trying to reconnect to the server | *No arguments* |
|
||||||
|
| `sharded-channel-moved` | See [here](./docs/pub-sub.md#sharded-channel-moved-event) | See [here](./docs/pub-sub.md#sharded-channel-moved-event) |
|
||||||
|
|
||||||
The client will not emit [any other events](./docs/v3-to-v4.md#all-the-removed-events) beyond those listed above.
|
> :warning: You **MUST** listen to `error` events. If a client doesn't have at least one `error` listener registered and an `error` occurs, that error will be thrown and the Node.js process will exit. See the [`EventEmitter` docs](https://nodejs.org/api/events.html#events_error_events) for more details.
|
||||||
|
|
||||||
|
> The client will not emit [any other events](./docs/v3-to-v4.md#all-the-removed-events) beyond those listed above.
|
||||||
|
|
||||||
## Supported Redis versions
|
## Supported Redis versions
|
||||||
|
|
||||||
|
383
benchmark/package-lock.json
generated
383
benchmark/package-lock.json
generated
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@redis/client-benchmark",
|
"name": "@redis/client-benchmark",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
@@ -8,39 +8,9 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@redis/client": "../packages/client",
|
"@redis/client": "../packages/client",
|
||||||
"hdr-histogram-js": "3.0.0",
|
"hdr-histogram-js": "3.0.0",
|
||||||
"ioredis": "5.2.2",
|
"ioredis": "5.3.2",
|
||||||
"redis-v3": "npm:redis@3.1.2",
|
"redis-v3": "npm:redis@3.1.2",
|
||||||
"yargs": "17.5.1"
|
"yargs": "17.7.2"
|
||||||
}
|
|
||||||
},
|
|
||||||
"../packages/client": {
|
|
||||||
"name": "@redis/client",
|
|
||||||
"version": "1.2.0",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"cluster-key-slot": "1.1.0",
|
|
||||||
"generic-pool": "3.8.2",
|
|
||||||
"yallist": "4.0.0"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@istanbuljs/nyc-config-typescript": "^1.0.2",
|
|
||||||
"@redis/test-utils": "*",
|
|
||||||
"@types/node": "^18.7.10",
|
|
||||||
"@types/sinon": "^10.0.13",
|
|
||||||
"@types/yallist": "^4.0.1",
|
|
||||||
"@typescript-eslint/eslint-plugin": "^5.34.0",
|
|
||||||
"@typescript-eslint/parser": "^5.34.0",
|
|
||||||
"eslint": "^8.22.0",
|
|
||||||
"nyc": "^15.1.0",
|
|
||||||
"release-it": "^15.3.0",
|
|
||||||
"sinon": "^14.0.0",
|
|
||||||
"source-map-support": "^0.5.21",
|
|
||||||
"ts-node": "^10.9.1",
|
|
||||||
"typedoc": "^0.23.10",
|
|
||||||
"typescript": "^4.7.4"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=14"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@assemblyscript/loader": {
|
"node_modules/@assemblyscript/loader": {
|
||||||
@@ -49,13 +19,22 @@
|
|||||||
"integrity": "sha512-ulkCYfFbYj01ie1MDOyxv2F6SpRN1TOj7fQxbP07D6HmeR+gr2JLSmINKjga2emB+b1L2KGrFKBTc+e00p54nw=="
|
"integrity": "sha512-ulkCYfFbYj01ie1MDOyxv2F6SpRN1TOj7fQxbP07D6HmeR+gr2JLSmINKjga2emB+b1L2KGrFKBTc+e00p54nw=="
|
||||||
},
|
},
|
||||||
"node_modules/@ioredis/commands": {
|
"node_modules/@ioredis/commands": {
|
||||||
"version": "1.1.1",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz",
|
||||||
"integrity": "sha512-fsR4P/ROllzf/7lXYyElUJCheWdTJVJvOTps8v9IWKFATxR61ANOlnoPqhH099xYLrJGpc2ZQ28B3rMeUt5VQg=="
|
"integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg=="
|
||||||
},
|
},
|
||||||
"node_modules/@redis/client": {
|
"node_modules/@redis/client": {
|
||||||
"resolved": "../packages/client",
|
"version": "1.5.7",
|
||||||
"link": true
|
"resolved": "file:../packages/client",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"cluster-key-slot": "1.1.2",
|
||||||
|
"generic-pool": "3.9.0",
|
||||||
|
"yallist": "4.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ansi-regex": {
|
"node_modules/ansi-regex": {
|
||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
@@ -99,19 +78,22 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/cliui": {
|
"node_modules/cliui": {
|
||||||
"version": "7.0.4",
|
"version": "8.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
|
||||||
"integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==",
|
"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"string-width": "^4.2.0",
|
"string-width": "^4.2.0",
|
||||||
"strip-ansi": "^6.0.0",
|
"strip-ansi": "^6.0.1",
|
||||||
"wrap-ansi": "^7.0.0"
|
"wrap-ansi": "^7.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/cluster-key-slot": {
|
"node_modules/cluster-key-slot": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz",
|
||||||
"integrity": "sha512-2Nii8p3RwAPiFwsnZvukotvow2rIHM+yQ6ZcBXGHdniadkYGZYiGmkHJIbZPIV9nfv7m/U1IPMVVcAhoWFeklw==",
|
"integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
@@ -149,9 +131,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/denque": {
|
"node_modules/denque": {
|
||||||
"version": "2.0.1",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/denque/-/denque-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
|
||||||
"integrity": "sha512-tfiWc6BQLXNLpNiR5iGd0Ocu3P3VpxfzFiqubLgMfhfOw9WyvgJBd46CClNn9k3qfbjvT//0cf7AlYRX/OslMQ==",
|
"integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10"
|
"node": ">=0.10"
|
||||||
}
|
}
|
||||||
@@ -169,6 +151,14 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/generic-pool": {
|
||||||
|
"version": "3.9.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.9.0.tgz",
|
||||||
|
"integrity": "sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/get-caller-file": {
|
"node_modules/get-caller-file": {
|
||||||
"version": "2.0.5",
|
"version": "2.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
||||||
@@ -191,14 +181,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ioredis": {
|
"node_modules/ioredis": {
|
||||||
"version": "5.2.2",
|
"version": "5.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.3.2.tgz",
|
||||||
"integrity": "sha512-wryKc1ur8PcCmNwfcGkw5evouzpbDXxxkMkzPK8wl4xQfQf7lHe11Jotell5ikMVAtikXJEu/OJVaoV51BggRQ==",
|
"integrity": "sha512-1DKMMzlIHM02eBBVOFQ1+AolGjs6+xEcM4PDL7NqOS6szq7H9jSaEkIUH6/a5Hl241LzW6JLSiAbNvTQjUupUA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ioredis/commands": "^1.1.1",
|
"@ioredis/commands": "^1.1.1",
|
||||||
"cluster-key-slot": "^1.1.0",
|
"cluster-key-slot": "^1.1.0",
|
||||||
"debug": "^4.3.4",
|
"debug": "^4.3.4",
|
||||||
"denque": "^2.0.1",
|
"denque": "^2.1.0",
|
||||||
"lodash.defaults": "^4.2.0",
|
"lodash.defaults": "^4.2.0",
|
||||||
"lodash.isarguments": "^3.1.0",
|
"lodash.isarguments": "^3.1.0",
|
||||||
"redis-errors": "^1.2.0",
|
"redis-errors": "^1.2.0",
|
||||||
@@ -224,12 +214,12 @@
|
|||||||
"node_modules/lodash.defaults": {
|
"node_modules/lodash.defaults": {
|
||||||
"version": "4.2.0",
|
"version": "4.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
|
||||||
"integrity": "sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw="
|
"integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ=="
|
||||||
},
|
},
|
||||||
"node_modules/lodash.isarguments": {
|
"node_modules/lodash.isarguments": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz",
|
||||||
"integrity": "sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo="
|
"integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg=="
|
||||||
},
|
},
|
||||||
"node_modules/ms": {
|
"node_modules/ms": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
@@ -249,7 +239,7 @@
|
|||||||
"node_modules/redis-errors": {
|
"node_modules/redis-errors": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz",
|
||||||
"integrity": "sha1-62LSrbFeTq9GEMBK/hUpOEJQq60=",
|
"integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=4"
|
"node": ">=4"
|
||||||
}
|
}
|
||||||
@@ -257,7 +247,7 @@
|
|||||||
"node_modules/redis-parser": {
|
"node_modules/redis-parser": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz",
|
||||||
"integrity": "sha1-tm2CjNyv5rS4pCin3vTGvKwxyLQ=",
|
"integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"redis-errors": "^1.0.0"
|
"redis-errors": "^1.0.0"
|
||||||
},
|
},
|
||||||
@@ -295,7 +285,7 @@
|
|||||||
"node_modules/require-directory": {
|
"node_modules/require-directory": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||||
"integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=",
|
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
@@ -353,288 +343,35 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/yallist": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
|
||||||
|
},
|
||||||
"node_modules/yargs": {
|
"node_modules/yargs": {
|
||||||
"version": "17.5.1",
|
"version": "17.7.2",
|
||||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
|
||||||
"integrity": "sha512-t6YAJcxDkNX7NFYiVtKvWUz8l+PaKTLiL63mJYWR2GnHq2gjEWISzsLp9wg3aY36dY1j+gfIEL3pIF+XlJJfbA==",
|
"integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cliui": "^7.0.2",
|
"cliui": "^8.0.1",
|
||||||
"escalade": "^3.1.1",
|
"escalade": "^3.1.1",
|
||||||
"get-caller-file": "^2.0.5",
|
"get-caller-file": "^2.0.5",
|
||||||
"require-directory": "^2.1.1",
|
"require-directory": "^2.1.1",
|
||||||
"string-width": "^4.2.3",
|
"string-width": "^4.2.3",
|
||||||
"y18n": "^5.0.5",
|
"y18n": "^5.0.5",
|
||||||
"yargs-parser": "^21.0.0"
|
"yargs-parser": "^21.1.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/yargs-parser": {
|
"node_modules/yargs-parser": {
|
||||||
"version": "21.0.1",
|
"version": "21.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
|
||||||
"integrity": "sha512-9BK1jFpLzJROCI5TzwZL/TU4gqjK5xiHV/RfWLOahrjAko/e4DJkRDZQXfvqAsiZzzYhgAzbgz6lg48jcm4GLg==",
|
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@assemblyscript/loader": {
|
|
||||||
"version": "0.19.23",
|
|
||||||
"resolved": "https://registry.npmjs.org/@assemblyscript/loader/-/loader-0.19.23.tgz",
|
|
||||||
"integrity": "sha512-ulkCYfFbYj01ie1MDOyxv2F6SpRN1TOj7fQxbP07D6HmeR+gr2JLSmINKjga2emB+b1L2KGrFKBTc+e00p54nw=="
|
|
||||||
},
|
|
||||||
"@ioredis/commands": {
|
|
||||||
"version": "1.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.1.1.tgz",
|
|
||||||
"integrity": "sha512-fsR4P/ROllzf/7lXYyElUJCheWdTJVJvOTps8v9IWKFATxR61ANOlnoPqhH099xYLrJGpc2ZQ28B3rMeUt5VQg=="
|
|
||||||
},
|
|
||||||
"@redis/client": {
|
|
||||||
"version": "file:../packages/client",
|
|
||||||
"requires": {
|
|
||||||
"@istanbuljs/nyc-config-typescript": "^1.0.2",
|
|
||||||
"@redis/test-utils": "*",
|
|
||||||
"@types/node": "^18.7.10",
|
|
||||||
"@types/sinon": "^10.0.13",
|
|
||||||
"@types/yallist": "^4.0.1",
|
|
||||||
"@typescript-eslint/eslint-plugin": "^5.34.0",
|
|
||||||
"@typescript-eslint/parser": "^5.34.0",
|
|
||||||
"cluster-key-slot": "1.1.0",
|
|
||||||
"eslint": "^8.22.0",
|
|
||||||
"generic-pool": "3.8.2",
|
|
||||||
"nyc": "^15.1.0",
|
|
||||||
"release-it": "^15.3.0",
|
|
||||||
"sinon": "^14.0.0",
|
|
||||||
"source-map-support": "^0.5.21",
|
|
||||||
"ts-node": "^10.9.1",
|
|
||||||
"typedoc": "^0.23.10",
|
|
||||||
"typescript": "^4.7.4",
|
|
||||||
"yallist": "4.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ansi-regex": {
|
|
||||||
"version": "5.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
|
||||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="
|
|
||||||
},
|
|
||||||
"ansi-styles": {
|
|
||||||
"version": "4.3.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
|
||||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
|
||||||
"requires": {
|
|
||||||
"color-convert": "^2.0.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"base64-js": {
|
|
||||||
"version": "1.5.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
|
||||||
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="
|
|
||||||
},
|
|
||||||
"cliui": {
|
|
||||||
"version": "7.0.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
|
|
||||||
"integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==",
|
|
||||||
"requires": {
|
|
||||||
"string-width": "^4.2.0",
|
|
||||||
"strip-ansi": "^6.0.0",
|
|
||||||
"wrap-ansi": "^7.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"cluster-key-slot": {
|
|
||||||
"version": "1.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.0.tgz",
|
|
||||||
"integrity": "sha512-2Nii8p3RwAPiFwsnZvukotvow2rIHM+yQ6ZcBXGHdniadkYGZYiGmkHJIbZPIV9nfv7m/U1IPMVVcAhoWFeklw=="
|
|
||||||
},
|
|
||||||
"color-convert": {
|
|
||||||
"version": "2.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
|
||||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
|
||||||
"requires": {
|
|
||||||
"color-name": "~1.1.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"color-name": {
|
|
||||||
"version": "1.1.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
|
||||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
|
|
||||||
},
|
|
||||||
"debug": {
|
|
||||||
"version": "4.3.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
|
||||||
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
|
|
||||||
"requires": {
|
|
||||||
"ms": "2.1.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"denque": {
|
|
||||||
"version": "2.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/denque/-/denque-2.0.1.tgz",
|
|
||||||
"integrity": "sha512-tfiWc6BQLXNLpNiR5iGd0Ocu3P3VpxfzFiqubLgMfhfOw9WyvgJBd46CClNn9k3qfbjvT//0cf7AlYRX/OslMQ=="
|
|
||||||
},
|
|
||||||
"emoji-regex": {
|
|
||||||
"version": "8.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
|
||||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
|
|
||||||
},
|
|
||||||
"escalade": {
|
|
||||||
"version": "3.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
|
|
||||||
"integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw=="
|
|
||||||
},
|
|
||||||
"get-caller-file": {
|
|
||||||
"version": "2.0.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
|
||||||
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="
|
|
||||||
},
|
|
||||||
"hdr-histogram-js": {
|
|
||||||
"version": "3.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/hdr-histogram-js/-/hdr-histogram-js-3.0.0.tgz",
|
|
||||||
"integrity": "sha512-/EpvQI2/Z98mNFYEnlqJ8Ogful8OpArLG/6Tf2bPnkutBVLIeMVNHjk1ZDfshF2BUweipzbk+dB1hgSB7SIakw==",
|
|
||||||
"requires": {
|
|
||||||
"@assemblyscript/loader": "^0.19.21",
|
|
||||||
"base64-js": "^1.2.0",
|
|
||||||
"pako": "^1.0.3"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ioredis": {
|
|
||||||
"version": "5.2.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.2.2.tgz",
|
|
||||||
"integrity": "sha512-wryKc1ur8PcCmNwfcGkw5evouzpbDXxxkMkzPK8wl4xQfQf7lHe11Jotell5ikMVAtikXJEu/OJVaoV51BggRQ==",
|
|
||||||
"requires": {
|
|
||||||
"@ioredis/commands": "^1.1.1",
|
|
||||||
"cluster-key-slot": "^1.1.0",
|
|
||||||
"debug": "^4.3.4",
|
|
||||||
"denque": "^2.0.1",
|
|
||||||
"lodash.defaults": "^4.2.0",
|
|
||||||
"lodash.isarguments": "^3.1.0",
|
|
||||||
"redis-errors": "^1.2.0",
|
|
||||||
"redis-parser": "^3.0.0",
|
|
||||||
"standard-as-callback": "^2.1.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"is-fullwidth-code-point": {
|
|
||||||
"version": "3.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
|
||||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="
|
|
||||||
},
|
|
||||||
"lodash.defaults": {
|
|
||||||
"version": "4.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
|
|
||||||
"integrity": "sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw="
|
|
||||||
},
|
|
||||||
"lodash.isarguments": {
|
|
||||||
"version": "3.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz",
|
|
||||||
"integrity": "sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo="
|
|
||||||
},
|
|
||||||
"ms": {
|
|
||||||
"version": "2.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
|
||||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
|
||||||
},
|
|
||||||
"pako": {
|
|
||||||
"version": "1.0.11",
|
|
||||||
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
|
|
||||||
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="
|
|
||||||
},
|
|
||||||
"redis-commands": {
|
|
||||||
"version": "1.7.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.7.0.tgz",
|
|
||||||
"integrity": "sha512-nJWqw3bTFy21hX/CPKHth6sfhZbdiHP6bTawSgQBlKOVRG7EZkfHbbHwQJnrE4vsQf0CMNE+3gJ4Fmm16vdVlQ=="
|
|
||||||
},
|
|
||||||
"redis-errors": {
|
|
||||||
"version": "1.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz",
|
|
||||||
"integrity": "sha1-62LSrbFeTq9GEMBK/hUpOEJQq60="
|
|
||||||
},
|
|
||||||
"redis-parser": {
|
|
||||||
"version": "3.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz",
|
|
||||||
"integrity": "sha1-tm2CjNyv5rS4pCin3vTGvKwxyLQ=",
|
|
||||||
"requires": {
|
|
||||||
"redis-errors": "^1.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"redis-v3": {
|
|
||||||
"version": "npm:redis@3.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/redis/-/redis-3.1.2.tgz",
|
|
||||||
"integrity": "sha512-grn5KoZLr/qrRQVwoSkmzdbw6pwF+/rwODtrOr6vuBRiR/f3rjSTGupbF90Zpqm2oenix8Do6RV7pYEkGwlKkw==",
|
|
||||||
"requires": {
|
|
||||||
"denque": "^1.5.0",
|
|
||||||
"redis-commands": "^1.7.0",
|
|
||||||
"redis-errors": "^1.2.0",
|
|
||||||
"redis-parser": "^3.0.0"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"denque": {
|
|
||||||
"version": "1.5.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/denque/-/denque-1.5.1.tgz",
|
|
||||||
"integrity": "sha512-XwE+iZ4D6ZUB7mfYRMb5wByE8L74HCn30FBN7sWnXksWc1LO1bPDl67pBR9o/kC4z/xSNAwkMYcGgqDV3BE3Hw=="
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"require-directory": {
|
|
||||||
"version": "2.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
|
||||||
"integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I="
|
|
||||||
},
|
|
||||||
"standard-as-callback": {
|
|
||||||
"version": "2.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz",
|
|
||||||
"integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A=="
|
|
||||||
},
|
|
||||||
"string-width": {
|
|
||||||
"version": "4.2.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
|
||||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
|
||||||
"requires": {
|
|
||||||
"emoji-regex": "^8.0.0",
|
|
||||||
"is-fullwidth-code-point": "^3.0.0",
|
|
||||||
"strip-ansi": "^6.0.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"strip-ansi": {
|
|
||||||
"version": "6.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
|
||||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
|
||||||
"requires": {
|
|
||||||
"ansi-regex": "^5.0.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"wrap-ansi": {
|
|
||||||
"version": "7.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
|
||||||
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
|
|
||||||
"requires": {
|
|
||||||
"ansi-styles": "^4.0.0",
|
|
||||||
"string-width": "^4.1.0",
|
|
||||||
"strip-ansi": "^6.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"y18n": {
|
|
||||||
"version": "5.0.8",
|
|
||||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
|
||||||
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="
|
|
||||||
},
|
|
||||||
"yargs": {
|
|
||||||
"version": "17.5.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.5.1.tgz",
|
|
||||||
"integrity": "sha512-t6YAJcxDkNX7NFYiVtKvWUz8l+PaKTLiL63mJYWR2GnHq2gjEWISzsLp9wg3aY36dY1j+gfIEL3pIF+XlJJfbA==",
|
|
||||||
"requires": {
|
|
||||||
"cliui": "^7.0.2",
|
|
||||||
"escalade": "^3.1.1",
|
|
||||||
"get-caller-file": "^2.0.5",
|
|
||||||
"require-directory": "^2.1.1",
|
|
||||||
"string-width": "^4.2.3",
|
|
||||||
"y18n": "^5.0.5",
|
|
||||||
"yargs-parser": "^21.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"yargs-parser": {
|
|
||||||
"version": "21.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.0.1.tgz",
|
|
||||||
"integrity": "sha512-9BK1jFpLzJROCI5TzwZL/TU4gqjK5xiHV/RfWLOahrjAko/e4DJkRDZQXfvqAsiZzzYhgAzbgz6lg48jcm4GLg=="
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -9,8 +9,8 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@redis/client": "../packages/client",
|
"@redis/client": "../packages/client",
|
||||||
"hdr-histogram-js": "3.0.0",
|
"hdr-histogram-js": "3.0.0",
|
||||||
"ioredis": "5.2.2",
|
"ioredis": "5.3.2",
|
||||||
"redis-v3": "npm:redis@3.1.2",
|
"redis-v3": "npm:redis@3.1.2",
|
||||||
"yargs": "17.5.1"
|
"yargs": "17.7.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -10,7 +10,7 @@ If don't want to queue commands in memory until a new socket is established, set
|
|||||||
|
|
||||||
## How are commands batched?
|
## How are commands batched?
|
||||||
|
|
||||||
Commands are pipelined using [`queueMicrotask`](https://nodejs.org/api/globals.html#globals_queuemicrotask_callback).
|
Commands are pipelined using [`setImmediate`](https://nodejs.org/api/timers.html#setimmediatecallback-args).
|
||||||
|
|
||||||
If `socket.write()` returns `false`—meaning that ["all or part of the data was queued in user memory"](https://nodejs.org/api/net.html#net_socket_write_data_encoding_callback:~:text=all%20or%20part%20of%20the%20data%20was%20queued%20in%20user%20memory)—the commands will stack in memory until the [`drain`](https://nodejs.org/api/net.html#net_event_drain) event is fired.
|
If `socket.write()` returns `false`—meaning that ["all or part of the data was queued in user memory"](https://nodejs.org/api/net.html#net_socket_write_data_encoding_callback:~:text=all%20or%20part%20of%20the%20data%20was%20queued%20in%20user%20memory)—the commands will stack in memory until the [`drain`](https://nodejs.org/api/net.html#net_event_drain) event is fired.
|
||||||
|
|
||||||
|
@@ -15,7 +15,7 @@
|
|||||||
| socket.reconnectStrategy | `retries => Math.min(retries * 50, 500)` | A function containing the [Reconnect Strategy](#reconnect-strategy) logic |
|
| socket.reconnectStrategy | `retries => Math.min(retries * 50, 500)` | A function containing the [Reconnect Strategy](#reconnect-strategy) logic |
|
||||||
| username | | ACL username ([see ACL guide](https://redis.io/topics/acl)) |
|
| username | | ACL username ([see ACL guide](https://redis.io/topics/acl)) |
|
||||||
| password | | ACL password or the old "--requirepass" password |
|
| password | | ACL password or the old "--requirepass" password |
|
||||||
| name | | Connection name ([see `CLIENT SETNAME`](https://redis.io/commands/client-setname)) |
|
| name | | Client name ([see `CLIENT SETNAME`](https://redis.io/commands/client-setname)) |
|
||||||
| database | | Redis database number (see [`SELECT`](https://redis.io/commands/select) command) |
|
| database | | Redis database number (see [`SELECT`](https://redis.io/commands/select) command) |
|
||||||
| modules | | Included [Redis Modules](../README.md#packages) |
|
| modules | | Included [Redis Modules](../README.md#packages) |
|
||||||
| scripts | | Script definitions (see [Lua Scripts](../README.md#lua-scripts)) |
|
| scripts | | Script definitions (see [Lua Scripts](../README.md#lua-scripts)) |
|
||||||
@@ -25,16 +25,24 @@
|
|||||||
| readonly | `false` | Connect in [`READONLY`](https://redis.io/commands/readonly) mode |
|
| readonly | `false` | Connect in [`READONLY`](https://redis.io/commands/readonly) mode |
|
||||||
| legacyMode | `false` | Maintain some backwards compatibility (see the [Migration Guide](./v3-to-v4.md)) |
|
| legacyMode | `false` | Maintain some backwards compatibility (see the [Migration Guide](./v3-to-v4.md)) |
|
||||||
| isolationPoolOptions | | See the [Isolated Execution Guide](./isolated-execution.md) |
|
| isolationPoolOptions | | See the [Isolated Execution Guide](./isolated-execution.md) |
|
||||||
| pingInterval | | Send `PING` command at interval (in ms). Useful with "[Azure Cache for Redis](https://learn.microsoft.com/en-us/azure/azure-cache-for-redis/cache-best-practices-connection#idle-timeout)" |
|
| pingInterval | | Send `PING` command at interval (in ms). Useful with ["Azure Cache for Redis"](https://learn.microsoft.com/en-us/azure/azure-cache-for-redis/cache-best-practices-connection#idle-timeout) |
|
||||||
|
|
||||||
## Reconnect Strategy
|
## Reconnect Strategy
|
||||||
|
|
||||||
You can implement a custom reconnect strategy as a function:
|
When the socket closes unexpectedly (without calling `.quit()`/`.disconnect()`), the client uses `reconnectStrategy` to decide what to do. The following values are supported:
|
||||||
|
1. `false` -> do not reconnect, close the client and flush the command queue.
|
||||||
|
2. `number` -> wait for `X` milliseconds before reconnecting.
|
||||||
|
3. `(retries: number, cause: Error) => false | number | Error` -> `number` is the same as configuring a `number` directly, `Error` is the same as `false`, but with a custom error.
|
||||||
|
|
||||||
- Receives the number of retries attempted so far.
|
By default the strategy is `Math.min(retries * 50, 500)`, but it can be overwritten like so:
|
||||||
- Returns `number | Error`:
|
|
||||||
- `number`: wait time in milliseconds prior to attempting a reconnect.
|
```javascript
|
||||||
- `Error`: closes the client and flushes internal command queues.
|
createClient({
|
||||||
|
socket: {
|
||||||
|
reconnectStrategy: retries => Math.min(retries * 50, 1000)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
## TLS
|
## TLS
|
||||||
|
|
||||||
@@ -44,7 +52,7 @@ To enable TLS, set `socket.tls` to `true`. Below are some basic examples.
|
|||||||
|
|
||||||
### Create a SSL client
|
### Create a SSL client
|
||||||
|
|
||||||
```typescript
|
```javascript
|
||||||
createClient({
|
createClient({
|
||||||
socket: {
|
socket: {
|
||||||
tls: true,
|
tls: true,
|
||||||
@@ -56,7 +64,7 @@ createClient({
|
|||||||
|
|
||||||
### Create a SSL client using a self-signed certificate
|
### Create a SSL client using a self-signed certificate
|
||||||
|
|
||||||
```typescript
|
```javascript
|
||||||
createClient({
|
createClient({
|
||||||
socket: {
|
socket: {
|
||||||
tls: true,
|
tls: true,
|
||||||
|
@@ -35,35 +35,70 @@ const value = await cluster.get('key');
|
|||||||
| rootNodes | | An array of root nodes that are part of the cluster, which will be used to get the cluster topology. Each element in the array is a client configuration object. There is no need to specify every node in the cluster, 3 should be enough to reliably connect and obtain the cluster configuration from the server |
|
| rootNodes | | An array of root nodes that are part of the cluster, which will be used to get the cluster topology. Each element in the array is a client configuration object. There is no need to specify every node in the cluster, 3 should be enough to reliably connect and obtain the cluster configuration from the server |
|
||||||
| defaults | | The default configuration values for every client in the cluster. Use this for example when specifying an ACL user to connect with |
|
| defaults | | The default configuration values for every client in the cluster. Use this for example when specifying an ACL user to connect with |
|
||||||
| useReplicas | `false` | When `true`, distribute load by executing readonly commands (such as `GET`, `GEOSEARCH`, etc.) across all cluster nodes. When `false`, only use master nodes |
|
| useReplicas | `false` | When `true`, distribute load by executing readonly commands (such as `GET`, `GEOSEARCH`, etc.) across all cluster nodes. When `false`, only use master nodes |
|
||||||
|
| minimizeConnections | `false` | When `true`, `.connect()` will only discover the cluster topology, without actually connecting to all the nodes. Useful for short-term or Pub/Sub-only connections. |
|
||||||
| maxCommandRedirections | `16` | The maximum number of times a command will be redirected due to `MOVED` or `ASK` errors |
|
| maxCommandRedirections | `16` | The maximum number of times a command will be redirected due to `MOVED` or `ASK` errors |
|
||||||
| nodeAddressMap | | Defines the [node address mapping](#node-address-map) |
|
| nodeAddressMap | | Defines the [node address mapping](#node-address-map) |
|
||||||
| modules | | Included [Redis Modules](../README.md#packages) |
|
| modules | | Included [Redis Modules](../README.md#packages) |
|
||||||
| scripts | | Script definitions (see [Lua Scripts](../README.md#lua-scripts)) |
|
| scripts | | Script definitions (see [Lua Scripts](../README.md#lua-scripts)) |
|
||||||
| functions | | Function definitions (see [Functions](../README.md#functions)) |
|
| functions | | Function definitions (see [Functions](../README.md#functions)) |
|
||||||
|
## Auth with password and username
|
||||||
|
|
||||||
## Node Address Map
|
Specifying the password in the URL or a root node will only affect the connection to that specific node. In case you want to set the password for all the connections being created from a cluster instance, use the `defaults` option.
|
||||||
|
|
||||||
A node address map is required when a Redis cluster is configured with addresses that are inaccessible by the machine running the Redis client.
|
|
||||||
This is a mapping of addresses and ports, with the values being the accessible address/port combination. Example:
|
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
createCluster({
|
createCluster({
|
||||||
rootNodes: [{
|
rootNodes: [{
|
||||||
url: 'external-host-1.io:30001'
|
url: 'redis://10.0.0.1:30001'
|
||||||
}, {
|
}, {
|
||||||
url: 'external-host-2.io:30002'
|
url: 'redis://10.0.0.2:30002'
|
||||||
}],
|
}],
|
||||||
|
defaults: {
|
||||||
|
username: 'username',
|
||||||
|
password: 'password'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Node Address Map
|
||||||
|
|
||||||
|
A mapping between the addresses in the cluster (see `CLUSTER SHARDS`) and the addresses the client should connect to.
|
||||||
|
Useful when the cluster is running on a different network to the client.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const rootNodes = [{
|
||||||
|
url: 'external-host-1.io:30001'
|
||||||
|
}, {
|
||||||
|
url: 'external-host-2.io:30002'
|
||||||
|
}];
|
||||||
|
|
||||||
|
// Use either a static mapping:
|
||||||
|
createCluster({
|
||||||
|
rootNodes,
|
||||||
nodeAddressMap: {
|
nodeAddressMap: {
|
||||||
'10.0.0.1:30001': {
|
'10.0.0.1:30001': {
|
||||||
host: 'external-host-1.io',
|
host: 'external-host.io',
|
||||||
port: 30001
|
port: 30001
|
||||||
},
|
},
|
||||||
'10.0.0.2:30002': {
|
'10.0.0.2:30002': {
|
||||||
host: 'external-host-2.io',
|
host: 'external-host.io',
|
||||||
port: 30002
|
port: 30002
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// or create the mapping dynamically, as a function:
|
||||||
|
createCluster({
|
||||||
|
rootNodes,
|
||||||
|
nodeAddressMap(address) {
|
||||||
|
const indexOfDash = address.lastIndexOf('-'),
|
||||||
|
indexOfDot = address.indexOf('.', indexOfDash),
|
||||||
|
indexOfColons = address.indexOf(':', indexOfDot);
|
||||||
|
|
||||||
|
return {
|
||||||
|
host: `external-host-${address.substring(indexOfDash + 1, indexOfDot)}.io`,
|
||||||
|
port: Number(address.substring(indexOfColons + 1))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
> This is a common problem when using ElastiCache. See [Accessing ElastiCache from outside AWS](https://docs.aws.amazon.com/AmazonElastiCache/latest/red-ug/accessing-elasticache.html) for more information on that.
|
> This is a common problem when using ElastiCache. See [Accessing ElastiCache from outside AWS](https://docs.aws.amazon.com/AmazonElastiCache/latest/red-ug/accessing-elasticache.html) for more information on that.
|
||||||
|
88
docs/pub-sub.md
Normal file
88
docs/pub-sub.md
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
# Pub/Sub
|
||||||
|
|
||||||
|
The Pub/Sub API is implemented by `RedisClient` and `RedisCluster`.
|
||||||
|
|
||||||
|
## Pub/Sub with `RedisClient`
|
||||||
|
|
||||||
|
Pub/Sub requires a dedicated stand-alone client. You can easily get one by `.duplicate()`ing an existing `RedisClient`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const subscriber = client.duplicate();
|
||||||
|
subscriber.on('error', err => console.error(err));
|
||||||
|
await subscriber.connect();
|
||||||
|
```
|
||||||
|
|
||||||
|
When working with a `RedisCluster`, this is handled automatically for you.
|
||||||
|
|
||||||
|
### `sharded-channel-moved` event
|
||||||
|
|
||||||
|
`RedisClient` emits the `sharded-channel-moved` event when the ["cluster slot"](https://redis.io/docs/reference/cluster-spec/#key-distribution-model) of a subscribed [Sharded Pub/Sub](https://redis.io/docs/manual/pubsub/#sharded-pubsub) channel has been moved to another shard.
|
||||||
|
|
||||||
|
The event listener signature is as follows:
|
||||||
|
```typescript
|
||||||
|
(
|
||||||
|
channel: string,
|
||||||
|
listeners: {
|
||||||
|
buffers: Set<Listener>;
|
||||||
|
strings: Set<Listener>;
|
||||||
|
}
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Subscribing
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const listener = (message, channel) => console.log(message, channel);
|
||||||
|
await client.subscribe('channel', listener);
|
||||||
|
await client.pSubscribe('channe*', listener);
|
||||||
|
// Use sSubscribe for sharded Pub/Sub:
|
||||||
|
await client.sSubscribe('channel', listener);
|
||||||
|
```
|
||||||
|
|
||||||
|
> ⚠️ Subscribing to the same channel more than once will create multiple listeners which will each be called when a message is recieved.
|
||||||
|
|
||||||
|
## Publishing
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
await client.publish('channel', 'message');
|
||||||
|
// Use sPublish for sharded Pub/Sub:
|
||||||
|
await client.sPublish('channel', 'message');
|
||||||
|
```
|
||||||
|
|
||||||
|
## Unsubscribing
|
||||||
|
|
||||||
|
The code below unsubscribes all listeners from all channels.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
await client.unsubscribe();
|
||||||
|
await client.pUnsubscribe();
|
||||||
|
// Use sUnsubscribe for sharded Pub/Sub:
|
||||||
|
await client.sUnsubscribe();
|
||||||
|
```
|
||||||
|
|
||||||
|
To unsubscribe from specific channels:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
await client.unsubscribe('channel');
|
||||||
|
await client.unsubscribe(['1', '2']);
|
||||||
|
```
|
||||||
|
|
||||||
|
To unsubscribe a specific listener:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
await client.unsubscribe('channel', listener);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Buffers
|
||||||
|
|
||||||
|
Publishing and subscribing using `Buffer`s is also supported:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
await subscriber.subscribe('channel', message => {
|
||||||
|
console.log(message); // <Buffer 6d 65 73 73 61 67 65>
|
||||||
|
}, true); // true = subscribe in `Buffer` mode.
|
||||||
|
|
||||||
|
await subscriber.publish(Buffer.from('channel'), Buffer.from('message'));
|
||||||
|
```
|
||||||
|
|
||||||
|
> NOTE: Buffers and strings are supported both for the channel name and the message. You can mix and match these as desired.
|
@@ -3,7 +3,7 @@
|
|||||||
This folder contains example scripts showing how to use Node Redis in different scenarios.
|
This folder contains example scripts showing how to use Node Redis in different scenarios.
|
||||||
|
|
||||||
| File Name | Description |
|
| File Name | Description |
|
||||||
| ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| `blocking-list-pop.js` | Block until an element is pushed to a list. |
|
| `blocking-list-pop.js` | Block until an element is pushed to a list. |
|
||||||
| `bloom-filter.js` | Space efficient set membership checks with a [Bloom Filter](https://en.wikipedia.org/wiki/Bloom_filter) using [RedisBloom](https://redisbloom.io). |
|
| `bloom-filter.js` | Space efficient set membership checks with a [Bloom Filter](https://en.wikipedia.org/wiki/Bloom_filter) using [RedisBloom](https://redisbloom.io). |
|
||||||
| `check-connection-status.js` | Check the client's connection status. |
|
| `check-connection-status.js` | Check the client's connection status. |
|
||||||
@@ -12,6 +12,7 @@ This folder contains example scripts showing how to use Node Redis in different
|
|||||||
| `connect-to-cluster.js` | Connect to a Redis cluster. |
|
| `connect-to-cluster.js` | Connect to a Redis cluster. |
|
||||||
| `count-min-sketch.js` | Estimate the frequency of a given event using the [RedisBloom](https://redisbloom.io) Count-Min Sketch. |
|
| `count-min-sketch.js` | Estimate the frequency of a given event using the [RedisBloom](https://redisbloom.io) Count-Min Sketch. |
|
||||||
| `cuckoo-filter.js` | Space efficient set membership checks with a [Cuckoo Filter](https://en.wikipedia.org/wiki/Cuckoo_filter) using [RedisBloom](https://redisbloom.io). |
|
| `cuckoo-filter.js` | Space efficient set membership checks with a [Cuckoo Filter](https://en.wikipedia.org/wiki/Cuckoo_filter) using [RedisBloom](https://redisbloom.io). |
|
||||||
|
| `dump-and-restore.js` | Demonstrates the use of the [`DUMP`](https://redis.io/commands/dump/) and [`RESTORE`](https://redis.io/commands/restore/) commands |
|
||||||
| `get-server-time.js` | Get the time from the Redis server. |
|
| `get-server-time.js` | Get the time from the Redis server. |
|
||||||
| `hyperloglog.js` | Showing use of Hyperloglog commands [PFADD, PFCOUNT and PFMERGE](https://redis.io/commands/?group=hyperloglog). |
|
| `hyperloglog.js` | Showing use of Hyperloglog commands [PFADD, PFCOUNT and PFMERGE](https://redis.io/commands/?group=hyperloglog). |
|
||||||
| `lua-multi-incr.js` | Define a custom lua script that allows you to perform INCRBY on multiple keys. |
|
| `lua-multi-incr.js` | Define a custom lua script that allows you to perform INCRBY on multiple keys. |
|
||||||
|
22
examples/dump-and-restore.js
Normal file
22
examples/dump-and-restore.js
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
// This example demonstrates the use of the DUMP and RESTORE commands
|
||||||
|
|
||||||
|
import { commandOptions, createClient } from 'redis';
|
||||||
|
|
||||||
|
const client = createClient();
|
||||||
|
await client.connect();
|
||||||
|
|
||||||
|
// DUMP a specific key into a local variable
|
||||||
|
const dump = await client.dump(
|
||||||
|
commandOptions({ returnBuffers: true }),
|
||||||
|
'source'
|
||||||
|
);
|
||||||
|
|
||||||
|
// RESTORE into a new key
|
||||||
|
await client.restore('destination', 0, dump);
|
||||||
|
|
||||||
|
// RESTORE and REPLACE an existing key
|
||||||
|
await client.restore('destination', 0, dump, {
|
||||||
|
REPLACE: true
|
||||||
|
});
|
||||||
|
|
||||||
|
await client.quit();
|
@@ -13,7 +13,7 @@ try {
|
|||||||
await client.ft.create('idx:animals', {
|
await client.ft.create('idx:animals', {
|
||||||
name: {
|
name: {
|
||||||
type: SchemaFieldTypes.TEXT,
|
type: SchemaFieldTypes.TEXT,
|
||||||
sortable: true
|
SORTABLE: true
|
||||||
},
|
},
|
||||||
species: SchemaFieldTypes.TAG,
|
species: SchemaFieldTypes.TAG,
|
||||||
age: SchemaFieldTypes.NUMERIC
|
age: SchemaFieldTypes.NUMERIC
|
||||||
|
@@ -15,7 +15,7 @@ try {
|
|||||||
await client.ft.create('idx:users', {
|
await client.ft.create('idx:users', {
|
||||||
'$.name': {
|
'$.name': {
|
||||||
type: SchemaFieldTypes.TEXT,
|
type: SchemaFieldTypes.TEXT,
|
||||||
SORTABLE: 'UNF'
|
SORTABLE: true
|
||||||
},
|
},
|
||||||
'$.age': {
|
'$.age': {
|
||||||
type: SchemaFieldTypes.NUMERIC,
|
type: SchemaFieldTypes.NUMERIC,
|
||||||
|
10969
package-lock.json
generated
10969
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
27
package.json
27
package.json
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "redis",
|
"name": "redis",
|
||||||
"description": "A modern, high performance Redis client",
|
"description": "A modern, high performance Redis client",
|
||||||
"version": "4.5.1",
|
"version": "4.7.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
"types": "./dist/index.d.ts",
|
"types": "./dist/index.d.ts",
|
||||||
@@ -23,18 +23,18 @@
|
|||||||
"gh-pages": "gh-pages -d ./documentation -e ./documentation -u 'documentation-bot <documentation@bot>'"
|
"gh-pages": "gh-pages -d ./documentation -e ./documentation -u 'documentation-bot <documentation@bot>'"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@redis/bloom": "1.1.0",
|
"@redis/bloom": "1.2.0",
|
||||||
"@redis/client": "1.4.2",
|
"@redis/client": "1.6.0",
|
||||||
"@redis/graph": "1.1.0",
|
"@redis/graph": "1.1.1",
|
||||||
"@redis/json": "1.0.4",
|
"@redis/json": "1.0.7",
|
||||||
"@redis/search": "1.1.0",
|
"@redis/search": "1.2.0",
|
||||||
"@redis/time-series": "1.0.4"
|
"@redis/time-series": "1.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tsconfig/node14": "^1.0.3",
|
"@tsconfig/node14": "^14.1.0",
|
||||||
"gh-pages": "^4.0.0",
|
"gh-pages": "^6.0.0",
|
||||||
"release-it": "^15.3.0",
|
"release-it": "^16.1.5",
|
||||||
"typescript": "^4.7.4"
|
"typescript": "^5.2.2"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -43,5 +43,8 @@
|
|||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/redis/node-redis/issues"
|
"url": "https://github.com/redis/node-redis/issues"
|
||||||
},
|
},
|
||||||
"homepage": "https://github.com/redis/node-redis"
|
"homepage": "https://github.com/redis/node-redis",
|
||||||
|
"keywords": [
|
||||||
|
"redis"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@redis/bloom",
|
"name": "@redis/bloom",
|
||||||
"version": "1.1.0",
|
"version": "1.2.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
"types": "./dist/index.d.ts",
|
"types": "./dist/index.d.ts",
|
||||||
@@ -18,12 +18,24 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@istanbuljs/nyc-config-typescript": "^1.0.2",
|
"@istanbuljs/nyc-config-typescript": "^1.0.2",
|
||||||
"@redis/test-utils": "*",
|
"@redis/test-utils": "*",
|
||||||
"@types/node": "^18.11.6",
|
"@types/node": "^20.6.2",
|
||||||
"nyc": "^15.1.0",
|
"nyc": "^15.1.0",
|
||||||
"release-it": "^15.5.0",
|
"release-it": "^16.1.5",
|
||||||
"source-map-support": "^0.5.21",
|
"source-map-support": "^0.5.21",
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.1",
|
||||||
"typedoc": "^0.23.18",
|
"typedoc": "^0.25.1",
|
||||||
"typescript": "^4.8.4"
|
"typescript": "^5.2.2"
|
||||||
}
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git://github.com/redis/node-redis.git"
|
||||||
|
},
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/redis/node-redis/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/redis/node-redis/tree/master/packages/bloom",
|
||||||
|
"keywords": [
|
||||||
|
"redis",
|
||||||
|
"RedisBloom"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
@@ -15,8 +15,10 @@ export const createCluster = RedisCluster.create;
|
|||||||
|
|
||||||
export { defineScript } from './lib/lua-script';
|
export { defineScript } from './lib/lua-script';
|
||||||
|
|
||||||
export { GeoReplyWith } from './lib/commands/generic-transformers';
|
|
||||||
|
|
||||||
export * from './lib/errors';
|
export * from './lib/errors';
|
||||||
|
|
||||||
export { SetOptions } from "./lib/commands/SET";
|
export { GeoReplyWith } from './lib/commands/generic-transformers';
|
||||||
|
|
||||||
|
export { SetOptions } from './lib/commands/SET';
|
||||||
|
|
||||||
|
export { RedisFlushModes } from './lib/commands/FLUSHALL';
|
||||||
|
@@ -203,9 +203,9 @@ export default class RESP2Decoder {
|
|||||||
this.arrayItemType = undefined;
|
this.arrayItemType = undefined;
|
||||||
|
|
||||||
if (length === -1) {
|
if (length === -1) {
|
||||||
return this.returnArrayReply(null, arraysToKeep);
|
return this.returnArrayReply(null, arraysToKeep, chunk);
|
||||||
} else if (length === 0) {
|
} else if (length === 0) {
|
||||||
return this.returnArrayReply([], arraysToKeep);
|
return this.returnArrayReply([], arraysToKeep, chunk);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.arraysInProcess.push({
|
this.arraysInProcess.push({
|
||||||
@@ -235,20 +235,23 @@ export default class RESP2Decoder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private returnArrayReply(reply: ArrayReply, arraysToKeep: number): ArrayReply | undefined {
|
private returnArrayReply(reply: ArrayReply, arraysToKeep: number, chunk?: Buffer): ArrayReply | undefined {
|
||||||
if (this.arraysInProcess.length <= arraysToKeep) return reply;
|
if (this.arraysInProcess.length <= arraysToKeep) return reply;
|
||||||
|
|
||||||
return this.pushArrayItem(reply, arraysToKeep);
|
return this.pushArrayItem(reply, arraysToKeep, chunk);
|
||||||
}
|
}
|
||||||
|
|
||||||
private pushArrayItem(item: Reply, arraysToKeep: number): ArrayReply | undefined {
|
private pushArrayItem(item: Reply, arraysToKeep: number, chunk?: Buffer): ArrayReply | undefined {
|
||||||
const to = this.arraysInProcess[this.arraysInProcess.length - 1]!;
|
const to = this.arraysInProcess[this.arraysInProcess.length - 1]!;
|
||||||
to.array[to.pushCounter] = item;
|
to.array[to.pushCounter] = item;
|
||||||
if (++to.pushCounter === to.array.length) {
|
if (++to.pushCounter === to.array.length) {
|
||||||
return this.returnArrayReply(
|
return this.returnArrayReply(
|
||||||
this.arraysInProcess.pop()!.array,
|
this.arraysInProcess.pop()!.array,
|
||||||
arraysToKeep
|
arraysToKeep,
|
||||||
|
chunk
|
||||||
);
|
);
|
||||||
|
} else if (chunk && chunk.length > this.cursor) {
|
||||||
|
return this.parseArray(chunk, arraysToKeep);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,18 +1,18 @@
|
|||||||
import * as LinkedList from 'yallist';
|
import * as LinkedList from 'yallist';
|
||||||
import { AbortError, ErrorReply } from '../errors';
|
import { AbortError, ErrorReply } from '../errors';
|
||||||
import { RedisCommandArgument, RedisCommandArguments, RedisCommandRawReply } from '../commands';
|
import { RedisCommandArguments, RedisCommandRawReply } from '../commands';
|
||||||
import RESP2Decoder from './RESP2/decoder';
|
import RESP2Decoder from './RESP2/decoder';
|
||||||
import encodeCommand from './RESP2/encoder';
|
import encodeCommand from './RESP2/encoder';
|
||||||
|
import { ChannelListeners, PubSub, PubSubCommand, PubSubListener, PubSubType, PubSubTypeListeners } from './pub-sub';
|
||||||
|
|
||||||
export interface QueueCommandOptions {
|
export interface QueueCommandOptions {
|
||||||
asap?: boolean;
|
asap?: boolean;
|
||||||
chainId?: symbol;
|
chainId?: symbol;
|
||||||
signal?: AbortSignal;
|
signal?: AbortSignal;
|
||||||
returnBuffers?: boolean;
|
returnBuffers?: boolean;
|
||||||
ignorePubSubMode?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CommandWaitingToBeSent extends CommandWaitingForReply {
|
export interface CommandWaitingToBeSent extends CommandWaitingForReply {
|
||||||
args: RedisCommandArguments;
|
args: RedisCommandArguments;
|
||||||
chainId?: symbol;
|
chainId?: symbol;
|
||||||
abort?: {
|
abort?: {
|
||||||
@@ -28,27 +28,9 @@ interface CommandWaitingForReply {
|
|||||||
returnBuffers?: boolean;
|
returnBuffers?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum PubSubSubscribeCommands {
|
const PONG = Buffer.from('pong');
|
||||||
SUBSCRIBE = 'SUBSCRIBE',
|
|
||||||
PSUBSCRIBE = 'PSUBSCRIBE'
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum PubSubUnsubscribeCommands {
|
export type OnShardedChannelMoved = (channel: string, listeners: ChannelListeners) => void;
|
||||||
UNSUBSCRIBE = 'UNSUBSCRIBE',
|
|
||||||
PUNSUBSCRIBE = 'PUNSUBSCRIBE'
|
|
||||||
}
|
|
||||||
|
|
||||||
export type PubSubListener<
|
|
||||||
RETURN_BUFFERS extends boolean = false,
|
|
||||||
T = RETURN_BUFFERS extends true ? Buffer : string
|
|
||||||
> = (message: T, channel: T) => unknown;
|
|
||||||
|
|
||||||
interface PubSubListeners {
|
|
||||||
buffers: Set<PubSubListener<true>>;
|
|
||||||
strings: Set<PubSubListener<false>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
type PubSubListenersMap = Map<string, PubSubListeners>;
|
|
||||||
|
|
||||||
export default class RedisCommandsQueue {
|
export default class RedisCommandsQueue {
|
||||||
static #flushQueue<T extends CommandWaitingForReply>(queue: LinkedList<T>, err: Error): void {
|
static #flushQueue<T extends CommandWaitingForReply>(queue: LinkedList<T>, err: Error): void {
|
||||||
@@ -57,67 +39,54 @@ export default class RedisCommandsQueue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static #emitPubSubMessage(listenersMap: PubSubListenersMap, message: Buffer, channel: Buffer, pattern?: Buffer): void {
|
|
||||||
const keyString = (pattern ?? channel).toString(),
|
|
||||||
listeners = listenersMap.get(keyString);
|
|
||||||
|
|
||||||
if (!listeners) return;
|
|
||||||
|
|
||||||
for (const listener of listeners.buffers) {
|
|
||||||
listener(message, channel);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!listeners.strings.size) return;
|
|
||||||
|
|
||||||
const channelString = pattern ? channel.toString() : keyString,
|
|
||||||
messageString = channelString === '__redis__:invalidate' ?
|
|
||||||
// https://github.com/redis/redis/pull/7469
|
|
||||||
// https://github.com/redis/redis/issues/7463
|
|
||||||
(message === null ? null : (message as any as Array<Buffer>).map(x => x.toString())) as any :
|
|
||||||
message.toString();
|
|
||||||
for (const listener of listeners.strings) {
|
|
||||||
listener(messageString, channelString);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
readonly #maxLength: number | null | undefined;
|
readonly #maxLength: number | null | undefined;
|
||||||
readonly #waitingToBeSent = new LinkedList<CommandWaitingToBeSent>();
|
readonly #waitingToBeSent = new LinkedList<CommandWaitingToBeSent>();
|
||||||
readonly #waitingForReply = new LinkedList<CommandWaitingForReply>();
|
readonly #waitingForReply = new LinkedList<CommandWaitingForReply>();
|
||||||
|
readonly #onShardedChannelMoved: OnShardedChannelMoved;
|
||||||
|
|
||||||
readonly #pubSubState = {
|
readonly #pubSub = new PubSub();
|
||||||
isActive: false,
|
|
||||||
subscribing: 0,
|
|
||||||
subscribed: 0,
|
|
||||||
unsubscribing: 0,
|
|
||||||
listeners: {
|
|
||||||
channels: new Map(),
|
|
||||||
patterns: new Map()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
static readonly #PUB_SUB_MESSAGES = {
|
get isPubSubActive() {
|
||||||
message: Buffer.from('message'),
|
return this.#pubSub.isActive;
|
||||||
pMessage: Buffer.from('pmessage'),
|
}
|
||||||
subscribe: Buffer.from('subscribe'),
|
|
||||||
pSubscribe: Buffer.from('psubscribe'),
|
|
||||||
unsubscribe: Buffer.from('unsubscribe'),
|
|
||||||
pUnsubscribe: Buffer.from('punsubscribe')
|
|
||||||
};
|
|
||||||
|
|
||||||
#chainInExecution: symbol | undefined;
|
#chainInExecution: symbol | undefined;
|
||||||
|
|
||||||
#decoder = new RESP2Decoder({
|
#decoder = new RESP2Decoder({
|
||||||
returnStringsAsBuffers: () => {
|
returnStringsAsBuffers: () => {
|
||||||
return !!this.#waitingForReply.head?.value.returnBuffers ||
|
return !!this.#waitingForReply.head?.value.returnBuffers ||
|
||||||
this.#pubSubState.isActive;
|
this.#pubSub.isActive;
|
||||||
},
|
},
|
||||||
onReply: reply => {
|
onReply: reply => {
|
||||||
if (this.#handlePubSubReply(reply)) {
|
if (this.#pubSub.isActive && Array.isArray(reply)) {
|
||||||
return;
|
if (this.#pubSub.handleMessageReply(reply as Array<Buffer>)) return;
|
||||||
} else if (!this.#waitingForReply.length) {
|
|
||||||
throw new Error('Got an unexpected reply from Redis');
|
const isShardedUnsubscribe = PubSub.isShardedUnsubscribe(reply as Array<Buffer>);
|
||||||
|
if (isShardedUnsubscribe && !this.#waitingForReply.length) {
|
||||||
|
const channel = (reply[1] as Buffer).toString();
|
||||||
|
this.#onShardedChannelMoved(
|
||||||
|
channel,
|
||||||
|
this.#pubSub.removeShardedListeners(channel)
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
} else if (isShardedUnsubscribe || PubSub.isStatusReply(reply as Array<Buffer>)) {
|
||||||
|
const head = this.#waitingForReply.head!.value;
|
||||||
|
if (
|
||||||
|
(Number.isNaN(head.channelsCounter!) && reply[2] === 0) ||
|
||||||
|
--head.channelsCounter! === 0
|
||||||
|
) {
|
||||||
|
this.#waitingForReply.shift()!.resolve();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (PONG.equals(reply[0] as Buffer)) {
|
||||||
|
const { resolve, returnBuffers } = this.#waitingForReply.shift()!,
|
||||||
|
buffer = ((reply[1] as Buffer).length === 0 ? reply[0] : reply[1]) as Buffer;
|
||||||
|
resolve(returnBuffers ? buffer : buffer.toString());
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const { resolve, reject } = this.#waitingForReply.shift()!;
|
const { resolve, reject } = this.#waitingForReply.shift()!;
|
||||||
if (reply instanceof ErrorReply) {
|
if (reply instanceof ErrorReply) {
|
||||||
reject(reply);
|
reject(reply);
|
||||||
@@ -127,14 +96,16 @@ export default class RedisCommandsQueue {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
constructor(maxLength: number | null | undefined) {
|
constructor(
|
||||||
|
maxLength: number | null | undefined,
|
||||||
|
onShardedChannelMoved: OnShardedChannelMoved
|
||||||
|
) {
|
||||||
this.#maxLength = maxLength;
|
this.#maxLength = maxLength;
|
||||||
|
this.#onShardedChannelMoved = onShardedChannelMoved;
|
||||||
}
|
}
|
||||||
|
|
||||||
addCommand<T = RedisCommandRawReply>(args: RedisCommandArguments, options?: QueueCommandOptions): Promise<T> {
|
addCommand<T = RedisCommandRawReply>(args: RedisCommandArguments, options?: QueueCommandOptions): Promise<T> {
|
||||||
if (this.#pubSubState.isActive && !options?.ignorePubSubMode) {
|
if (this.#maxLength && this.#waitingToBeSent.length + this.#waitingForReply.length >= this.#maxLength) {
|
||||||
return Promise.reject(new Error('Cannot send commands in PubSub mode'));
|
|
||||||
} else if (this.#maxLength && this.#waitingToBeSent.length + this.#waitingForReply.length >= this.#maxLength) {
|
|
||||||
return Promise.reject(new Error('The queue is full'));
|
return Promise.reject(new Error('The queue is full'));
|
||||||
} else if (options?.signal?.aborted) {
|
} else if (options?.signal?.aborted) {
|
||||||
return Promise.reject(new AbortError());
|
return Promise.reject(new AbortError());
|
||||||
@@ -173,158 +144,76 @@ export default class RedisCommandsQueue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
subscribe<T extends boolean>(
|
subscribe<T extends boolean>(
|
||||||
command: PubSubSubscribeCommands,
|
type: PubSubType,
|
||||||
channels: RedisCommandArgument | Array<RedisCommandArgument>,
|
channels: string | Array<string>,
|
||||||
listener: PubSubListener<T>,
|
listener: PubSubListener<T>,
|
||||||
returnBuffers?: T
|
returnBuffers?: T
|
||||||
): Promise<void> {
|
) {
|
||||||
const channelsToSubscribe: Array<RedisCommandArgument> = [],
|
return this.#pushPubSubCommand(
|
||||||
listenersMap = command === PubSubSubscribeCommands.SUBSCRIBE ?
|
this.#pubSub.subscribe(type, channels, listener, returnBuffers)
|
||||||
this.#pubSubState.listeners.channels :
|
);
|
||||||
this.#pubSubState.listeners.patterns;
|
|
||||||
for (const channel of (Array.isArray(channels) ? channels : [channels])) {
|
|
||||||
const channelString = typeof channel === 'string' ? channel : channel.toString();
|
|
||||||
let listeners = listenersMap.get(channelString);
|
|
||||||
if (!listeners) {
|
|
||||||
listeners = {
|
|
||||||
buffers: new Set(),
|
|
||||||
strings: new Set()
|
|
||||||
};
|
|
||||||
listenersMap.set(channelString, listeners);
|
|
||||||
channelsToSubscribe.push(channel);
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://github.com/microsoft/TypeScript/issues/23132
|
|
||||||
(returnBuffers ? listeners.buffers : listeners.strings).add(listener as any);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!channelsToSubscribe.length) {
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.#pushPubSubCommand(command, channelsToSubscribe);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
unsubscribe<T extends boolean>(
|
unsubscribe<T extends boolean>(
|
||||||
command: PubSubUnsubscribeCommands,
|
type: PubSubType,
|
||||||
channels?: string | Array<string>,
|
channels?: string | Array<string>,
|
||||||
listener?: PubSubListener<T>,
|
listener?: PubSubListener<T>,
|
||||||
returnBuffers?: T
|
returnBuffers?: T
|
||||||
): Promise<void> {
|
) {
|
||||||
const listeners = command === PubSubUnsubscribeCommands.UNSUBSCRIBE ?
|
return this.#pushPubSubCommand(
|
||||||
this.#pubSubState.listeners.channels :
|
this.#pubSub.unsubscribe(type, channels, listener, returnBuffers)
|
||||||
this.#pubSubState.listeners.patterns;
|
);
|
||||||
|
|
||||||
if (!channels) {
|
|
||||||
const size = listeners.size;
|
|
||||||
listeners.clear();
|
|
||||||
return this.#pushPubSubCommand(command, size);
|
|
||||||
}
|
|
||||||
|
|
||||||
const channelsToUnsubscribe = [];
|
|
||||||
for (const channel of (Array.isArray(channels) ? channels : [channels])) {
|
|
||||||
const sets = listeners.get(channel);
|
|
||||||
if (!sets) continue;
|
|
||||||
|
|
||||||
let shouldUnsubscribe;
|
|
||||||
if (listener) {
|
|
||||||
// https://github.com/microsoft/TypeScript/issues/23132
|
|
||||||
(returnBuffers ? sets.buffers : sets.strings).delete(listener as any);
|
|
||||||
shouldUnsubscribe = !sets.buffers.size && !sets.strings.size;
|
|
||||||
} else {
|
|
||||||
shouldUnsubscribe = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (shouldUnsubscribe) {
|
|
||||||
channelsToUnsubscribe.push(channel);
|
|
||||||
listeners.delete(channel);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!channelsToUnsubscribe.length) {
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.#pushPubSubCommand(command, channelsToUnsubscribe);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#pushPubSubCommand(command: PubSubSubscribeCommands | PubSubUnsubscribeCommands, channels: number | Array<RedisCommandArgument>): Promise<void> {
|
resubscribe(): Promise<any> | undefined {
|
||||||
return new Promise((resolve, reject) => {
|
const commands = this.#pubSub.resubscribe();
|
||||||
const isSubscribe = command === PubSubSubscribeCommands.SUBSCRIBE || command === PubSubSubscribeCommands.PSUBSCRIBE,
|
if (!commands.length) return;
|
||||||
inProgressKey = isSubscribe ? 'subscribing' : 'unsubscribing',
|
|
||||||
commandArgs: Array<RedisCommandArgument> = [command];
|
|
||||||
|
|
||||||
let channelsCounter: number;
|
return Promise.all(
|
||||||
if (typeof channels === 'number') { // unsubscribe only
|
commands.map(command => this.#pushPubSubCommand(command))
|
||||||
channelsCounter = channels;
|
);
|
||||||
} else {
|
}
|
||||||
commandArgs.push(...channels);
|
|
||||||
channelsCounter = channels.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.#pubSubState.isActive = true;
|
extendPubSubChannelListeners(
|
||||||
this.#pubSubState[inProgressKey] += channelsCounter;
|
type: PubSubType,
|
||||||
|
channel: string,
|
||||||
|
listeners: ChannelListeners
|
||||||
|
) {
|
||||||
|
return this.#pushPubSubCommand(
|
||||||
|
this.#pubSub.extendChannelListeners(type, channel, listeners)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
extendPubSubListeners(type: PubSubType, listeners: PubSubTypeListeners) {
|
||||||
|
return this.#pushPubSubCommand(
|
||||||
|
this.#pubSub.extendTypeListeners(type, listeners)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
getPubSubListeners(type: PubSubType) {
|
||||||
|
return this.#pubSub.getTypeListeners(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
#pushPubSubCommand(command: PubSubCommand) {
|
||||||
|
if (command === undefined) return;
|
||||||
|
|
||||||
|
return new Promise<void>((resolve, reject) => {
|
||||||
this.#waitingToBeSent.push({
|
this.#waitingToBeSent.push({
|
||||||
args: commandArgs,
|
args: command.args,
|
||||||
channelsCounter,
|
channelsCounter: command.channelsCounter,
|
||||||
returnBuffers: true,
|
returnBuffers: true,
|
||||||
resolve: () => {
|
resolve: () => {
|
||||||
this.#pubSubState[inProgressKey] -= channelsCounter;
|
command.resolve();
|
||||||
this.#pubSubState.subscribed += channelsCounter * (isSubscribe ? 1 : -1);
|
|
||||||
this.#updatePubSubActiveState();
|
|
||||||
resolve();
|
resolve();
|
||||||
},
|
},
|
||||||
reject: err => {
|
reject: err => {
|
||||||
this.#pubSubState[inProgressKey] -= channelsCounter * (isSubscribe ? 1 : -1);
|
command.reject?.();
|
||||||
this.#updatePubSubActiveState();
|
|
||||||
reject(err);
|
reject(err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#updatePubSubActiveState(): void {
|
|
||||||
if (
|
|
||||||
!this.#pubSubState.subscribed &&
|
|
||||||
!this.#pubSubState.subscribing &&
|
|
||||||
!this.#pubSubState.subscribed
|
|
||||||
) {
|
|
||||||
this.#pubSubState.isActive = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
resubscribe(): Promise<any> | undefined {
|
|
||||||
this.#pubSubState.subscribed = 0;
|
|
||||||
this.#pubSubState.subscribing = 0;
|
|
||||||
this.#pubSubState.unsubscribing = 0;
|
|
||||||
|
|
||||||
const promises = [],
|
|
||||||
{ channels, patterns } = this.#pubSubState.listeners;
|
|
||||||
|
|
||||||
if (channels.size) {
|
|
||||||
promises.push(
|
|
||||||
this.#pushPubSubCommand(
|
|
||||||
PubSubSubscribeCommands.SUBSCRIBE,
|
|
||||||
[...channels.keys()]
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (patterns.size) {
|
|
||||||
promises.push(
|
|
||||||
this.#pushPubSubCommand(
|
|
||||||
PubSubSubscribeCommands.PSUBSCRIBE,
|
|
||||||
[...patterns.keys()]
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (promises.length) {
|
|
||||||
return Promise.all(promises);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getCommandToSend(): RedisCommandArguments | undefined {
|
getCommandToSend(): RedisCommandArguments | undefined {
|
||||||
const toSend = this.#waitingToBeSent.shift();
|
const toSend = this.#waitingToBeSent.shift();
|
||||||
if (!toSend) return;
|
if (!toSend) return;
|
||||||
@@ -351,39 +240,9 @@ export default class RedisCommandsQueue {
|
|||||||
this.#decoder.write(chunk);
|
this.#decoder.write(chunk);
|
||||||
}
|
}
|
||||||
|
|
||||||
#handlePubSubReply(reply: any): boolean {
|
|
||||||
if (!this.#pubSubState.isActive || !Array.isArray(reply)) return false;
|
|
||||||
|
|
||||||
if (RedisCommandsQueue.#PUB_SUB_MESSAGES.message.equals(reply[0])) {
|
|
||||||
RedisCommandsQueue.#emitPubSubMessage(
|
|
||||||
this.#pubSubState.listeners.channels,
|
|
||||||
reply[2],
|
|
||||||
reply[1]
|
|
||||||
);
|
|
||||||
} else if (RedisCommandsQueue.#PUB_SUB_MESSAGES.pMessage.equals(reply[0])) {
|
|
||||||
RedisCommandsQueue.#emitPubSubMessage(
|
|
||||||
this.#pubSubState.listeners.patterns,
|
|
||||||
reply[3],
|
|
||||||
reply[2],
|
|
||||||
reply[1]
|
|
||||||
);
|
|
||||||
} else if (
|
|
||||||
RedisCommandsQueue.#PUB_SUB_MESSAGES.subscribe.equals(reply[0]) ||
|
|
||||||
RedisCommandsQueue.#PUB_SUB_MESSAGES.pSubscribe.equals(reply[0]) ||
|
|
||||||
RedisCommandsQueue.#PUB_SUB_MESSAGES.unsubscribe.equals(reply[0]) ||
|
|
||||||
RedisCommandsQueue.#PUB_SUB_MESSAGES.pUnsubscribe.equals(reply[0])
|
|
||||||
) {
|
|
||||||
if (--this.#waitingForReply.head!.value.channelsCounter! === 0) {
|
|
||||||
this.#waitingForReply.shift()!.resolve();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
flushWaitingForReply(err: Error): void {
|
flushWaitingForReply(err: Error): void {
|
||||||
this.#decoder.reset();
|
this.#decoder.reset();
|
||||||
this.#pubSubState.isActive = false;
|
this.#pubSub.reset();
|
||||||
RedisCommandsQueue.#flushQueue(this.#waitingForReply, err);
|
RedisCommandsQueue.#flushQueue(this.#waitingForReply, err);
|
||||||
|
|
||||||
if (!this.#chainInExecution) return;
|
if (!this.#chainInExecution) return;
|
||||||
@@ -396,6 +255,8 @@ export default class RedisCommandsQueue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
flushAll(err: Error): void {
|
flushAll(err: Error): void {
|
||||||
|
this.#decoder.reset();
|
||||||
|
this.#pubSub.reset();
|
||||||
RedisCommandsQueue.#flushQueue(this.#waitingForReply, err);
|
RedisCommandsQueue.#flushQueue(this.#waitingForReply, err);
|
||||||
RedisCommandsQueue.#flushQueue(this.#waitingToBeSent, err);
|
RedisCommandsQueue.#flushQueue(this.#waitingToBeSent, err);
|
||||||
}
|
}
|
||||||
|
@@ -21,7 +21,9 @@ import * as CLIENT_GETNAME from '../commands/CLIENT_GETNAME';
|
|||||||
import * as CLIENT_GETREDIR from '../commands/CLIENT_GETREDIR';
|
import * as CLIENT_GETREDIR from '../commands/CLIENT_GETREDIR';
|
||||||
import * as CLIENT_ID from '../commands/CLIENT_ID';
|
import * as CLIENT_ID from '../commands/CLIENT_ID';
|
||||||
import * as CLIENT_KILL from '../commands/CLIENT_KILL';
|
import * as CLIENT_KILL from '../commands/CLIENT_KILL';
|
||||||
|
import * as CLIENT_LIST from '../commands/CLIENT_LIST';
|
||||||
import * as CLIENT_NO_EVICT from '../commands/CLIENT_NO-EVICT';
|
import * as CLIENT_NO_EVICT from '../commands/CLIENT_NO-EVICT';
|
||||||
|
import * as CLIENT_NO_TOUCH from '../commands/CLIENT_NO-TOUCH';
|
||||||
import * as CLIENT_PAUSE from '../commands/CLIENT_PAUSE';
|
import * as CLIENT_PAUSE from '../commands/CLIENT_PAUSE';
|
||||||
import * as CLIENT_SETNAME from '../commands/CLIENT_SETNAME';
|
import * as CLIENT_SETNAME from '../commands/CLIENT_SETNAME';
|
||||||
import * as CLIENT_TRACKING from '../commands/CLIENT_TRACKING';
|
import * as CLIENT_TRACKING from '../commands/CLIENT_TRACKING';
|
||||||
@@ -44,6 +46,7 @@ import * as CLUSTER_KEYSLOT from '../commands/CLUSTER_KEYSLOT';
|
|||||||
import * as CLUSTER_LINKS from '../commands/CLUSTER_LINKS';
|
import * as CLUSTER_LINKS from '../commands/CLUSTER_LINKS';
|
||||||
import * as CLUSTER_MEET from '../commands/CLUSTER_MEET';
|
import * as CLUSTER_MEET from '../commands/CLUSTER_MEET';
|
||||||
import * as CLUSTER_MYID from '../commands/CLUSTER_MYID';
|
import * as CLUSTER_MYID from '../commands/CLUSTER_MYID';
|
||||||
|
import * as CLUSTER_MYSHARDID from '../commands/CLUSTER_MYSHARDID';
|
||||||
import * as CLUSTER_NODES from '../commands/CLUSTER_NODES';
|
import * as CLUSTER_NODES from '../commands/CLUSTER_NODES';
|
||||||
import * as CLUSTER_REPLICAS from '../commands/CLUSTER_REPLICAS';
|
import * as CLUSTER_REPLICAS from '../commands/CLUSTER_REPLICAS';
|
||||||
import * as CLUSTER_REPLICATE from '../commands/CLUSTER_REPLICATE';
|
import * as CLUSTER_REPLICATE from '../commands/CLUSTER_REPLICATE';
|
||||||
@@ -83,6 +86,8 @@ import * as KEYS from '../commands/KEYS';
|
|||||||
import * as LASTSAVE from '../commands/LASTSAVE';
|
import * as LASTSAVE from '../commands/LASTSAVE';
|
||||||
import * as LATENCY_DOCTOR from '../commands/LATENCY_DOCTOR';
|
import * as LATENCY_DOCTOR from '../commands/LATENCY_DOCTOR';
|
||||||
import * as LATENCY_GRAPH from '../commands/LATENCY_GRAPH';
|
import * as LATENCY_GRAPH from '../commands/LATENCY_GRAPH';
|
||||||
|
import * as LATENCY_HISTORY from '../commands/LATENCY_HISTORY';
|
||||||
|
import * as LATENCY_LATEST from '../commands/LATENCY_LATEST';
|
||||||
import * as LOLWUT from '../commands/LOLWUT';
|
import * as LOLWUT from '../commands/LOLWUT';
|
||||||
import * as MEMORY_DOCTOR from '../commands/MEMORY_DOCTOR';
|
import * as MEMORY_DOCTOR from '../commands/MEMORY_DOCTOR';
|
||||||
import * as MEMORY_MALLOC_STATS from '../commands/MEMORY_MALLOC-STATS';
|
import * as MEMORY_MALLOC_STATS from '../commands/MEMORY_MALLOC-STATS';
|
||||||
@@ -97,6 +102,8 @@ import * as PING from '../commands/PING';
|
|||||||
import * as PUBSUB_CHANNELS from '../commands/PUBSUB_CHANNELS';
|
import * as PUBSUB_CHANNELS from '../commands/PUBSUB_CHANNELS';
|
||||||
import * as PUBSUB_NUMPAT from '../commands/PUBSUB_NUMPAT';
|
import * as PUBSUB_NUMPAT from '../commands/PUBSUB_NUMPAT';
|
||||||
import * as PUBSUB_NUMSUB from '../commands/PUBSUB_NUMSUB';
|
import * as PUBSUB_NUMSUB from '../commands/PUBSUB_NUMSUB';
|
||||||
|
import * as PUBSUB_SHARDCHANNELS from '../commands/PUBSUB_SHARDCHANNELS';
|
||||||
|
import * as PUBSUB_SHARDNUMSUB from '../commands/PUBSUB_SHARDNUMSUB';
|
||||||
import * as RANDOMKEY from '../commands/RANDOMKEY';
|
import * as RANDOMKEY from '../commands/RANDOMKEY';
|
||||||
import * as READONLY from '../commands/READONLY';
|
import * as READONLY from '../commands/READONLY';
|
||||||
import * as READWRITE from '../commands/READWRITE';
|
import * as READWRITE from '../commands/READWRITE';
|
||||||
@@ -164,6 +171,10 @@ export default {
|
|||||||
clientKill: CLIENT_KILL,
|
clientKill: CLIENT_KILL,
|
||||||
'CLIENT_NO-EVICT': CLIENT_NO_EVICT,
|
'CLIENT_NO-EVICT': CLIENT_NO_EVICT,
|
||||||
clientNoEvict: CLIENT_NO_EVICT,
|
clientNoEvict: CLIENT_NO_EVICT,
|
||||||
|
'CLIENT_NO-TOUCH': CLIENT_NO_TOUCH,
|
||||||
|
clientNoTouch: CLIENT_NO_TOUCH,
|
||||||
|
CLIENT_LIST,
|
||||||
|
clientList: CLIENT_LIST,
|
||||||
CLIENT_PAUSE,
|
CLIENT_PAUSE,
|
||||||
clientPause: CLIENT_PAUSE,
|
clientPause: CLIENT_PAUSE,
|
||||||
CLIENT_SETNAME,
|
CLIENT_SETNAME,
|
||||||
@@ -208,6 +219,8 @@ export default {
|
|||||||
clusterMeet: CLUSTER_MEET,
|
clusterMeet: CLUSTER_MEET,
|
||||||
CLUSTER_MYID,
|
CLUSTER_MYID,
|
||||||
clusterMyId: CLUSTER_MYID,
|
clusterMyId: CLUSTER_MYID,
|
||||||
|
CLUSTER_MYSHARDID,
|
||||||
|
clusterMyShardId: CLUSTER_MYSHARDID,
|
||||||
CLUSTER_NODES,
|
CLUSTER_NODES,
|
||||||
clusterNodes: CLUSTER_NODES,
|
clusterNodes: CLUSTER_NODES,
|
||||||
CLUSTER_REPLICAS,
|
CLUSTER_REPLICAS,
|
||||||
@@ -286,6 +299,10 @@ export default {
|
|||||||
latencyDoctor: LATENCY_DOCTOR,
|
latencyDoctor: LATENCY_DOCTOR,
|
||||||
LATENCY_GRAPH,
|
LATENCY_GRAPH,
|
||||||
latencyGraph: LATENCY_GRAPH,
|
latencyGraph: LATENCY_GRAPH,
|
||||||
|
LATENCY_HISTORY,
|
||||||
|
latencyHistory: LATENCY_HISTORY,
|
||||||
|
LATENCY_LATEST,
|
||||||
|
latencyLatest: LATENCY_LATEST,
|
||||||
LOLWUT,
|
LOLWUT,
|
||||||
lolwut: LOLWUT,
|
lolwut: LOLWUT,
|
||||||
MEMORY_DOCTOR,
|
MEMORY_DOCTOR,
|
||||||
@@ -314,6 +331,10 @@ export default {
|
|||||||
pubSubNumPat: PUBSUB_NUMPAT,
|
pubSubNumPat: PUBSUB_NUMPAT,
|
||||||
PUBSUB_NUMSUB,
|
PUBSUB_NUMSUB,
|
||||||
pubSubNumSub: PUBSUB_NUMSUB,
|
pubSubNumSub: PUBSUB_NUMSUB,
|
||||||
|
PUBSUB_SHARDCHANNELS,
|
||||||
|
pubSubShardChannels: PUBSUB_SHARDCHANNELS,
|
||||||
|
PUBSUB_SHARDNUMSUB,
|
||||||
|
pubSubShardNumSub: PUBSUB_SHARDNUMSUB,
|
||||||
RANDOMKEY,
|
RANDOMKEY,
|
||||||
randomKey: RANDOMKEY,
|
randomKey: RANDOMKEY,
|
||||||
READONLY,
|
READONLY,
|
||||||
|
@@ -2,14 +2,16 @@ import { strict as assert } from 'assert';
|
|||||||
import testUtils, { GLOBAL, waitTillBeenCalled } from '../test-utils';
|
import testUtils, { GLOBAL, waitTillBeenCalled } from '../test-utils';
|
||||||
import RedisClient, { RedisClientType } from '.';
|
import RedisClient, { RedisClientType } from '.';
|
||||||
import { RedisClientMultiCommandType } from './multi-command';
|
import { RedisClientMultiCommandType } from './multi-command';
|
||||||
import { RedisCommandArguments, RedisCommandRawReply, RedisModules, RedisFunctions, RedisScripts } from '../commands';
|
import { RedisCommandRawReply, RedisModules, RedisFunctions, RedisScripts } from '../commands';
|
||||||
import { AbortError, ClientClosedError, ClientOfflineError, ConnectionTimeoutError, DisconnectsClientError, SocketClosedUnexpectedlyError, WatchError } from '../errors';
|
import { AbortError, ClientClosedError, ClientOfflineError, ConnectionTimeoutError, DisconnectsClientError, ErrorReply, MultiErrorReply, SocketClosedUnexpectedlyError, WatchError } from '../errors';
|
||||||
import { defineScript } from '../lua-script';
|
import { defineScript } from '../lua-script';
|
||||||
import { spy } from 'sinon';
|
import { spy } from 'sinon';
|
||||||
import { once } from 'events';
|
import { once } from 'events';
|
||||||
import { ClientKillFilters } from '../commands/CLIENT_KILL';
|
import { ClientKillFilters } from '../commands/CLIENT_KILL';
|
||||||
import { promisify } from 'util';
|
import { promisify } from 'util';
|
||||||
|
|
||||||
|
import {version} from '../../package.json';
|
||||||
|
|
||||||
export const SQUARE_SCRIPT = defineScript({
|
export const SQUARE_SCRIPT = defineScript({
|
||||||
SCRIPT: 'return ARGV[1] * ARGV[1];',
|
SCRIPT: 'return ARGV[1] * ARGV[1];',
|
||||||
NUMBER_OF_KEYS: 0,
|
NUMBER_OF_KEYS: 0,
|
||||||
@@ -107,6 +109,57 @@ describe('Client', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('connect', () => {
|
||||||
|
testUtils.testWithClient('connect should return the client instance', async client => {
|
||||||
|
try {
|
||||||
|
assert.equal(await client.connect(), client);
|
||||||
|
} finally {
|
||||||
|
if (client.isOpen) await client.disconnect();
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
...GLOBAL.SERVERS.PASSWORD,
|
||||||
|
disableClientSetup: true
|
||||||
|
});
|
||||||
|
|
||||||
|
testUtils.testWithClient('should set default lib name and version', async client => {
|
||||||
|
const clientInfo = await client.clientInfo();
|
||||||
|
|
||||||
|
assert.equal(clientInfo.libName, 'node-redis');
|
||||||
|
assert.equal(clientInfo.libVer, version);
|
||||||
|
}, {
|
||||||
|
...GLOBAL.SERVERS.PASSWORD,
|
||||||
|
minimumDockerVersion: [7, 2]
|
||||||
|
});
|
||||||
|
|
||||||
|
testUtils.testWithClient('disable sending lib name and version', async client => {
|
||||||
|
const clientInfo = await client.clientInfo();
|
||||||
|
|
||||||
|
assert.equal(clientInfo.libName, '');
|
||||||
|
assert.equal(clientInfo.libVer, '');
|
||||||
|
}, {
|
||||||
|
...GLOBAL.SERVERS.PASSWORD,
|
||||||
|
clientOptions: {
|
||||||
|
...GLOBAL.SERVERS.PASSWORD.clientOptions,
|
||||||
|
disableClientInfo: true
|
||||||
|
},
|
||||||
|
minimumDockerVersion: [7, 2]
|
||||||
|
});
|
||||||
|
|
||||||
|
testUtils.testWithClient('send client name tag', async client => {
|
||||||
|
const clientInfo = await client.clientInfo();
|
||||||
|
|
||||||
|
assert.equal(clientInfo.libName, 'node-redis(test)');
|
||||||
|
assert.equal(clientInfo.libVer, version);
|
||||||
|
}, {
|
||||||
|
...GLOBAL.SERVERS.PASSWORD,
|
||||||
|
clientOptions: {
|
||||||
|
...GLOBAL.SERVERS.PASSWORD.clientOptions,
|
||||||
|
clientInfoTag: "test"
|
||||||
|
},
|
||||||
|
minimumDockerVersion: [7, 2]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('authentication', () => {
|
describe('authentication', () => {
|
||||||
testUtils.testWithClient('Client should be authenticated', async client => {
|
testUtils.testWithClient('Client should be authenticated', async client => {
|
||||||
assert.equal(
|
assert.equal(
|
||||||
@@ -165,6 +218,28 @@ describe('Client', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testUtils.testWithClient('client.sendCommand should reply with error', async client => {
|
||||||
|
await assert.rejects(
|
||||||
|
promisify(client.sendCommand).call(client, '1', '2')
|
||||||
|
);
|
||||||
|
}, {
|
||||||
|
...GLOBAL.SERVERS.OPEN,
|
||||||
|
clientOptions: {
|
||||||
|
legacyMode: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
testUtils.testWithClient('client.hGetAll should reply with error', async client => {
|
||||||
|
await assert.rejects(
|
||||||
|
promisify(client.hGetAll).call(client)
|
||||||
|
);
|
||||||
|
}, {
|
||||||
|
...GLOBAL.SERVERS.OPEN,
|
||||||
|
clientOptions: {
|
||||||
|
legacyMode: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
testUtils.testWithClient('client.v4.sendCommand should return a promise', async client => {
|
testUtils.testWithClient('client.v4.sendCommand should return a promise', async client => {
|
||||||
assert.equal(
|
assert.equal(
|
||||||
await client.v4.sendCommand(['PING']),
|
await client.v4.sendCommand(['PING']),
|
||||||
@@ -177,6 +252,18 @@ describe('Client', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testUtils.testWithClient('client.v4.{command} should return a promise', async client => {
|
||||||
|
assert.equal(
|
||||||
|
await client.v4.ping(),
|
||||||
|
'PONG'
|
||||||
|
);
|
||||||
|
}, {
|
||||||
|
...GLOBAL.SERVERS.OPEN,
|
||||||
|
clientOptions: {
|
||||||
|
legacyMode: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
testUtils.testWithClient('client.{command} should accept vardict arguments', async client => {
|
testUtils.testWithClient('client.{command} should accept vardict arguments', async client => {
|
||||||
assert.equal(
|
assert.equal(
|
||||||
await promisify(client.set).call(client, 'a', 'b'),
|
await promisify(client.set).call(client, 'a', 'b'),
|
||||||
@@ -484,14 +571,23 @@ describe('Client', () => {
|
|||||||
);
|
);
|
||||||
}, GLOBAL.SERVERS.OPEN);
|
}, GLOBAL.SERVERS.OPEN);
|
||||||
|
|
||||||
testUtils.testWithClient('execAsPipeline', async client => {
|
describe('execAsPipeline', () => {
|
||||||
assert.deepEqual(
|
testUtils.testWithClient('exec(true)', async client => {
|
||||||
await client.multi()
|
assert.deepEqual(
|
||||||
.ping()
|
await client.multi()
|
||||||
.exec(true),
|
.ping()
|
||||||
['PONG']
|
.exec(true),
|
||||||
);
|
['PONG']
|
||||||
}, GLOBAL.SERVERS.OPEN);
|
);
|
||||||
|
}, GLOBAL.SERVERS.OPEN);
|
||||||
|
|
||||||
|
testUtils.testWithClient('empty execAsPipeline', async client => {
|
||||||
|
assert.deepEqual(
|
||||||
|
await client.multi().execAsPipeline(),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
}, GLOBAL.SERVERS.OPEN);
|
||||||
|
});
|
||||||
|
|
||||||
testUtils.testWithClient('should remember selected db', async client => {
|
testUtils.testWithClient('should remember selected db', async client => {
|
||||||
await client.multi()
|
await client.multi()
|
||||||
@@ -506,6 +602,23 @@ describe('Client', () => {
|
|||||||
...GLOBAL.SERVERS.OPEN,
|
...GLOBAL.SERVERS.OPEN,
|
||||||
minimumDockerVersion: [6, 2] // CLIENT INFO
|
minimumDockerVersion: [6, 2] // CLIENT INFO
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testUtils.testWithClient('should handle error replies (#2665)', async client => {
|
||||||
|
await assert.rejects(
|
||||||
|
client.multi()
|
||||||
|
.set('key', 'value')
|
||||||
|
.hGetAll('key')
|
||||||
|
.exec(),
|
||||||
|
err => {
|
||||||
|
assert.ok(err instanceof MultiErrorReply);
|
||||||
|
assert.equal(err.replies.length, 2);
|
||||||
|
assert.deepEqual(err.errorIndexes, [1]);
|
||||||
|
assert.ok(err.replies[1] instanceof ErrorReply);
|
||||||
|
assert.deepEqual([...err.errors()], [err.replies[1]]);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}, GLOBAL.SERVERS.OPEN);
|
||||||
});
|
});
|
||||||
|
|
||||||
testUtils.testWithClient('scripts', async client => {
|
testUtils.testWithClient('scripts', async client => {
|
||||||
@@ -564,11 +677,41 @@ describe('Client', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
testUtils.testWithClient('executeIsolated', async client => {
|
describe('isolationPool', () => {
|
||||||
const id = await client.clientId(),
|
testUtils.testWithClient('executeIsolated', async client => {
|
||||||
isolatedId = await client.executeIsolated(isolatedClient => isolatedClient.clientId());
|
const id = await client.clientId(),
|
||||||
assert.ok(id !== isolatedId);
|
isolatedId = await client.executeIsolated(isolatedClient => isolatedClient.clientId());
|
||||||
}, GLOBAL.SERVERS.OPEN);
|
assert.ok(id !== isolatedId);
|
||||||
|
}, GLOBAL.SERVERS.OPEN);
|
||||||
|
|
||||||
|
testUtils.testWithClient('should be able to use pool even before connect', async client => {
|
||||||
|
await client.executeIsolated(() => Promise.resolve());
|
||||||
|
// make sure to destroy isolation pool
|
||||||
|
await client.connect();
|
||||||
|
await client.disconnect();
|
||||||
|
}, {
|
||||||
|
...GLOBAL.SERVERS.OPEN,
|
||||||
|
disableClientSetup: true
|
||||||
|
});
|
||||||
|
|
||||||
|
testUtils.testWithClient('should work after reconnect (#2406)', async client => {
|
||||||
|
await client.disconnect();
|
||||||
|
await client.connect();
|
||||||
|
await client.executeIsolated(() => Promise.resolve());
|
||||||
|
}, GLOBAL.SERVERS.OPEN);
|
||||||
|
|
||||||
|
testUtils.testWithClient('should throw ClientClosedError after disconnect', async client => {
|
||||||
|
await client.connect();
|
||||||
|
await client.disconnect();
|
||||||
|
await assert.rejects(
|
||||||
|
client.executeIsolated(() => Promise.resolve()),
|
||||||
|
ClientClosedError
|
||||||
|
);
|
||||||
|
}, {
|
||||||
|
...GLOBAL.SERVERS.OPEN,
|
||||||
|
disableClientSetup: true
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
async function killClient<
|
async function killClient<
|
||||||
M extends RedisModules,
|
M extends RedisModules,
|
||||||
@@ -604,6 +747,9 @@ describe('Client', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
testUtils.testWithClient('should propagated errors from "isolated" clients', client => {
|
testUtils.testWithClient('should propagated errors from "isolated" clients', client => {
|
||||||
|
client.on('error', () => {
|
||||||
|
// ignore errors
|
||||||
|
});
|
||||||
return client.executeIsolated(isolated => killClient(isolated, client));
|
return client.executeIsolated(isolated => killClient(isolated, client));
|
||||||
}, GLOBAL.SERVERS.OPEN);
|
}, GLOBAL.SERVERS.OPEN);
|
||||||
|
|
||||||
@@ -642,6 +788,31 @@ describe('Client', () => {
|
|||||||
assert.deepEqual(hash, results);
|
assert.deepEqual(hash, results);
|
||||||
}, GLOBAL.SERVERS.OPEN);
|
}, GLOBAL.SERVERS.OPEN);
|
||||||
|
|
||||||
|
testUtils.testWithClient('hScanNoValuesIterator', async client => {
|
||||||
|
const hash: Record<string, string> = {};
|
||||||
|
const expectedKeys: Array<string> = [];
|
||||||
|
for (let i = 0; i < 100; i++) {
|
||||||
|
hash[i.toString()] = i.toString();
|
||||||
|
expectedKeys.push(i.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
await client.hSet('key', hash);
|
||||||
|
|
||||||
|
const keys: Array<string> = [];
|
||||||
|
for await (const key of client.hScanNoValuesIterator('key')) {
|
||||||
|
keys.push(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sort(a: string, b: string) {
|
||||||
|
return Number(a) - Number(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.deepEqual(keys.sort(sort), expectedKeys);
|
||||||
|
}, {
|
||||||
|
...GLOBAL.SERVERS.OPEN,
|
||||||
|
minimumDockerVersion: [7, 4]
|
||||||
|
});
|
||||||
|
|
||||||
testUtils.testWithClient('sScanIterator', async client => {
|
testUtils.testWithClient('sScanIterator', async client => {
|
||||||
const members = new Set<string>();
|
const members = new Set<string>();
|
||||||
for (let i = 0; i < 100; i++) {
|
for (let i = 0; i < 100; i++) {
|
||||||
@@ -685,7 +856,7 @@ describe('Client', () => {
|
|||||||
members.map<MemberTuple>(member => [member.value, member.score]).sort(sort)
|
members.map<MemberTuple>(member => [member.value, member.score]).sort(sort)
|
||||||
);
|
);
|
||||||
}, GLOBAL.SERVERS.OPEN);
|
}, GLOBAL.SERVERS.OPEN);
|
||||||
|
|
||||||
describe('PubSub', () => {
|
describe('PubSub', () => {
|
||||||
testUtils.testWithClient('should be able to publish and subscribe to messages', async publisher => {
|
testUtils.testWithClient('should be able to publish and subscribe to messages', async publisher => {
|
||||||
function assertStringListener(message: string, channel: string) {
|
function assertStringListener(message: string, channel: string) {
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import COMMANDS from './commands';
|
import COMMANDS from './commands';
|
||||||
import { RedisCommand, RedisCommandArguments, RedisCommandRawReply, RedisCommandReply, RedisFunctions, RedisModules, RedisExtensions, RedisScript, RedisScripts, RedisCommandSignature, ConvertArgumentType, RedisFunction, ExcludeMappedString, RedisCommands } from '../commands';
|
import { RedisCommand, RedisCommandArgument, RedisCommandArguments, RedisCommandRawReply, RedisCommandReply, RedisFunctions, RedisModules, RedisExtensions, RedisScript, RedisScripts, RedisCommandSignature, ConvertArgumentType, RedisFunction, ExcludeMappedString, RedisCommands } from '../commands';
|
||||||
import RedisSocket, { RedisSocketOptions, RedisTlsSocketOptions } from './socket';
|
import RedisSocket, { RedisSocketOptions, RedisTlsSocketOptions } from './socket';
|
||||||
import RedisCommandsQueue, { PubSubListener, PubSubSubscribeCommands, PubSubUnsubscribeCommands, QueueCommandOptions } from './commands-queue';
|
import RedisCommandsQueue, { QueueCommandOptions } from './commands-queue';
|
||||||
import RedisClientMultiCommand, { RedisClientMultiCommandType } from './multi-command';
|
import RedisClientMultiCommand, { RedisClientMultiCommandType } from './multi-command';
|
||||||
import { RedisMultiQueuedCommand } from '../multi-command';
|
import { RedisMultiQueuedCommand } from '../multi-command';
|
||||||
import { EventEmitter } from 'events';
|
import { EventEmitter } from 'events';
|
||||||
@@ -11,27 +11,71 @@ import { ScanCommandOptions } from '../commands/SCAN';
|
|||||||
import { HScanTuple } from '../commands/HSCAN';
|
import { HScanTuple } from '../commands/HSCAN';
|
||||||
import { attachCommands, attachExtensions, fCallArguments, transformCommandArguments, transformCommandReply, transformLegacyCommandArguments } from '../commander';
|
import { attachCommands, attachExtensions, fCallArguments, transformCommandArguments, transformCommandReply, transformLegacyCommandArguments } from '../commander';
|
||||||
import { Pool, Options as PoolOptions, createPool } from 'generic-pool';
|
import { Pool, Options as PoolOptions, createPool } from 'generic-pool';
|
||||||
import { ClientClosedError, ClientOfflineError, DisconnectsClientError } from '../errors';
|
import { ClientClosedError, ClientOfflineError, DisconnectsClientError, ErrorReply } from '../errors';
|
||||||
import { URL } from 'url';
|
import { URL } from 'url';
|
||||||
import { TcpSocketConnectOpts } from 'net';
|
import { TcpSocketConnectOpts } from 'net';
|
||||||
|
import { PubSubType, PubSubListener, PubSubTypeListeners, ChannelListeners } from './pub-sub';
|
||||||
|
|
||||||
|
import {version} from '../../package.json';
|
||||||
|
|
||||||
export interface RedisClientOptions<
|
export interface RedisClientOptions<
|
||||||
M extends RedisModules = RedisModules,
|
M extends RedisModules = RedisModules,
|
||||||
F extends RedisFunctions = RedisFunctions,
|
F extends RedisFunctions = RedisFunctions,
|
||||||
S extends RedisScripts = RedisScripts
|
S extends RedisScripts = RedisScripts
|
||||||
> extends RedisExtensions<M, F, S> {
|
> extends RedisExtensions<M, F, S> {
|
||||||
|
/**
|
||||||
|
* `redis[s]://[[username][:password]@][host][:port][/db-number]`
|
||||||
|
* See [`redis`](https://www.iana.org/assignments/uri-schemes/prov/redis) and [`rediss`](https://www.iana.org/assignments/uri-schemes/prov/rediss) IANA registration for more details
|
||||||
|
*/
|
||||||
url?: string;
|
url?: string;
|
||||||
|
/**
|
||||||
|
* Socket connection properties
|
||||||
|
*/
|
||||||
socket?: RedisSocketOptions;
|
socket?: RedisSocketOptions;
|
||||||
|
/**
|
||||||
|
* ACL username ([see ACL guide](https://redis.io/topics/acl))
|
||||||
|
*/
|
||||||
username?: string;
|
username?: string;
|
||||||
|
/**
|
||||||
|
* ACL password or the old "--requirepass" password
|
||||||
|
*/
|
||||||
password?: string;
|
password?: string;
|
||||||
|
/**
|
||||||
|
* Client name ([see `CLIENT SETNAME`](https://redis.io/commands/client-setname))
|
||||||
|
*/
|
||||||
name?: string;
|
name?: string;
|
||||||
|
/**
|
||||||
|
* Redis database number (see [`SELECT`](https://redis.io/commands/select) command)
|
||||||
|
*/
|
||||||
database?: number;
|
database?: number;
|
||||||
|
/**
|
||||||
|
* Maximum length of the client's internal command queue
|
||||||
|
*/
|
||||||
commandsQueueMaxLength?: number;
|
commandsQueueMaxLength?: number;
|
||||||
|
/**
|
||||||
|
* When `true`, commands are rejected when the client is reconnecting.
|
||||||
|
* When `false`, commands are queued for execution after reconnection.
|
||||||
|
*/
|
||||||
disableOfflineQueue?: boolean;
|
disableOfflineQueue?: boolean;
|
||||||
|
/**
|
||||||
|
* Connect in [`READONLY`](https://redis.io/commands/readonly) mode
|
||||||
|
*/
|
||||||
readonly?: boolean;
|
readonly?: boolean;
|
||||||
legacyMode?: boolean;
|
legacyMode?: boolean;
|
||||||
isolationPoolOptions?: PoolOptions;
|
isolationPoolOptions?: PoolOptions;
|
||||||
|
/**
|
||||||
|
* Send `PING` command at interval (in ms).
|
||||||
|
* Useful with Redis deployments that do not use TCP Keep-Alive.
|
||||||
|
*/
|
||||||
pingInterval?: number;
|
pingInterval?: number;
|
||||||
|
/**
|
||||||
|
* If set to true, disables sending client identifier (user-agent like message) to the redis server
|
||||||
|
*/
|
||||||
|
disableClientInfo?: boolean;
|
||||||
|
/**
|
||||||
|
* Tag to append to library name that is sent to the Redis server
|
||||||
|
*/
|
||||||
|
clientInfoTag?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
type WithCommands = {
|
type WithCommands = {
|
||||||
@@ -155,7 +199,7 @@ export default class RedisClient<
|
|||||||
readonly #options?: RedisClientOptions<M, F, S>;
|
readonly #options?: RedisClientOptions<M, F, S>;
|
||||||
readonly #socket: RedisSocket;
|
readonly #socket: RedisSocket;
|
||||||
readonly #queue: RedisCommandsQueue;
|
readonly #queue: RedisCommandsQueue;
|
||||||
readonly #isolationPool: Pool<RedisClientType<M, F, S>>;
|
#isolationPool?: Pool<RedisClientType<M, F, S>>;
|
||||||
readonly #v4: Record<string, any> = {};
|
readonly #v4: Record<string, any> = {};
|
||||||
#selectedDB = 0;
|
#selectedDB = 0;
|
||||||
|
|
||||||
@@ -171,6 +215,10 @@ export default class RedisClient<
|
|||||||
return this.#socket.isReady;
|
return this.#socket.isReady;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get isPubSubActive() {
|
||||||
|
return this.#queue.isPubSubActive;
|
||||||
|
}
|
||||||
|
|
||||||
get v4(): Record<string, any> {
|
get v4(): Record<string, any> {
|
||||||
if (!this.#options?.legacyMode) {
|
if (!this.#options?.legacyMode) {
|
||||||
throw new Error('the client is not in "legacy mode"');
|
throw new Error('the client is not in "legacy mode"');
|
||||||
@@ -184,16 +232,9 @@ export default class RedisClient<
|
|||||||
this.#options = this.#initiateOptions(options);
|
this.#options = this.#initiateOptions(options);
|
||||||
this.#queue = this.#initiateQueue();
|
this.#queue = this.#initiateQueue();
|
||||||
this.#socket = this.#initiateSocket();
|
this.#socket = this.#initiateSocket();
|
||||||
this.#isolationPool = createPool({
|
// should be initiated in connect, not here
|
||||||
create: async () => {
|
// TODO: consider breaking in v5
|
||||||
const duplicate = this.duplicate({
|
this.#isolationPool = this.#initiateIsolationPool();
|
||||||
isolationPoolOptions: undefined
|
|
||||||
}).on('error', err => this.emit('error', err));
|
|
||||||
await duplicate.connect();
|
|
||||||
return duplicate;
|
|
||||||
},
|
|
||||||
destroy: client => client.disconnect()
|
|
||||||
}, options?.isolationPoolOptions);
|
|
||||||
this.#legacyMode();
|
this.#legacyMode();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -215,7 +256,10 @@ export default class RedisClient<
|
|||||||
}
|
}
|
||||||
|
|
||||||
#initiateQueue(): RedisCommandsQueue {
|
#initiateQueue(): RedisCommandsQueue {
|
||||||
return new RedisCommandsQueue(this.#options?.commandsQueueMaxLength);
|
return new RedisCommandsQueue(
|
||||||
|
this.#options?.commandsQueueMaxLength,
|
||||||
|
(channel, listeners) => this.emit('sharded-channel-moved', channel, listeners)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#initiateSocket(): RedisSocket {
|
#initiateSocket(): RedisSocket {
|
||||||
@@ -240,6 +284,33 @@ export default class RedisClient<
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!this.#options?.disableClientInfo) {
|
||||||
|
promises.push(
|
||||||
|
this.#queue.addCommand(
|
||||||
|
[ 'CLIENT', 'SETINFO', 'LIB-VER', version],
|
||||||
|
{ asap: true }
|
||||||
|
).catch(err => {
|
||||||
|
if (!(err instanceof ErrorReply)) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
promises.push(
|
||||||
|
this.#queue.addCommand(
|
||||||
|
[
|
||||||
|
'CLIENT', 'SETINFO', 'LIB-NAME',
|
||||||
|
this.#options?.clientInfoTag ? `node-redis(${this.#options.clientInfoTag})` : 'node-redis'
|
||||||
|
],
|
||||||
|
{ asap: true }
|
||||||
|
).catch(err => {
|
||||||
|
if (!(err instanceof ErrorReply)) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (this.#options?.name) {
|
if (this.#options?.name) {
|
||||||
promises.push(
|
promises.push(
|
||||||
this.#queue.addCommand(
|
this.#queue.addCommand(
|
||||||
@@ -295,6 +366,19 @@ export default class RedisClient<
|
|||||||
.on('end', () => this.emit('end'));
|
.on('end', () => this.emit('end'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#initiateIsolationPool() {
|
||||||
|
return createPool({
|
||||||
|
create: async () => {
|
||||||
|
const duplicate = this.duplicate({
|
||||||
|
isolationPoolOptions: undefined
|
||||||
|
}).on('error', err => this.emit('error', err));
|
||||||
|
await duplicate.connect();
|
||||||
|
return duplicate;
|
||||||
|
},
|
||||||
|
destroy: client => client.disconnect()
|
||||||
|
}, this.#options?.isolationPoolOptions);
|
||||||
|
}
|
||||||
|
|
||||||
#legacyMode(): void {
|
#legacyMode(): void {
|
||||||
if (!this.#options?.legacyMode) return;
|
if (!this.#options?.legacyMode) return;
|
||||||
|
|
||||||
@@ -302,13 +386,15 @@ export default class RedisClient<
|
|||||||
(this as any).sendCommand = (...args: Array<any>): void => {
|
(this as any).sendCommand = (...args: Array<any>): void => {
|
||||||
const result = this.#legacySendCommand(...args);
|
const result = this.#legacySendCommand(...args);
|
||||||
if (result) {
|
if (result) {
|
||||||
result.promise.then(reply => result.callback(null, reply));
|
result.promise
|
||||||
|
.then(reply => result.callback(null, reply))
|
||||||
|
.catch(err => result.callback(err));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const [ name, command ] of Object.entries(COMMANDS as RedisCommands)) {
|
for (const [ name, command ] of Object.entries(COMMANDS as RedisCommands)) {
|
||||||
this.#defineLegacyCommand(name, command);
|
this.#defineLegacyCommand(name, command);
|
||||||
(this as any)[name.toLowerCase()] = (this as any)[name];
|
(this as any)[name.toLowerCase()] ??= (this as any)[name];
|
||||||
}
|
}
|
||||||
|
|
||||||
// hard coded commands
|
// hard coded commands
|
||||||
@@ -339,21 +425,21 @@ export default class RedisClient<
|
|||||||
promise.catch(err => this.emit('error', err));
|
promise.catch(err => this.emit('error', err));
|
||||||
}
|
}
|
||||||
|
|
||||||
#defineLegacyCommand(this: any, name: string, command?: RedisCommand): void {
|
#defineLegacyCommand(name: string, command?: RedisCommand): void {
|
||||||
this.#v4[name] = this[name].bind(this);
|
this.#v4[name] = (this as any)[name].bind(this);
|
||||||
this[name] = command && command.TRANSFORM_LEGACY_REPLY && command.transformReply ?
|
(this as any)[name] = command && command.TRANSFORM_LEGACY_REPLY && command.transformReply ?
|
||||||
(...args: Array<unknown>) => {
|
(...args: Array<unknown>) => {
|
||||||
const result = this.#legacySendCommand(name, ...args);
|
const result = this.#legacySendCommand(name, ...args);
|
||||||
if (result) {
|
if (result) {
|
||||||
result.promise.then((reply: any) => {
|
result.promise
|
||||||
result.callback(null, command.transformReply!(reply));
|
.then(reply => result.callback(null, command.transformReply!(reply)))
|
||||||
});
|
.catch(err => result.callback(err));
|
||||||
}
|
}
|
||||||
} :
|
} :
|
||||||
(...args: Array<unknown>) => this.sendCommand(name, ...args);
|
(...args: Array<unknown>) => (this as any).sendCommand(name, ...args);
|
||||||
}
|
}
|
||||||
|
|
||||||
#pingTimer?: NodeJS.Timer;
|
#pingTimer?: NodeJS.Timeout;
|
||||||
|
|
||||||
#setPingTimer(): void {
|
#setPingTimer(): void {
|
||||||
if (!this.#options?.pingInterval || !this.#socket.isReady) return;
|
if (!this.#options?.pingInterval || !this.#socket.isReady) return;
|
||||||
@@ -362,7 +448,8 @@ export default class RedisClient<
|
|||||||
this.#pingTimer = setTimeout(() => {
|
this.#pingTimer = setTimeout(() => {
|
||||||
if (!this.#socket.isReady) return;
|
if (!this.#socket.isReady) return;
|
||||||
|
|
||||||
(this as unknown as RedisClientType<M, F, S>).ping()
|
// using #sendCommand to support legacy mode
|
||||||
|
this.#sendCommand(['PING'])
|
||||||
.then(reply => this.emit('ping-interval', reply))
|
.then(reply => this.emit('ping-interval', reply))
|
||||||
.catch(err => this.emit('error', err))
|
.catch(err => this.emit('error', err))
|
||||||
.finally(() => this.#setPingTimer());
|
.finally(() => this.#setPingTimer());
|
||||||
@@ -376,8 +463,11 @@ export default class RedisClient<
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async connect(): Promise<void> {
|
async connect() {
|
||||||
|
// see comment in constructor
|
||||||
|
this.#isolationPool ??= this.#initiateIsolationPool();
|
||||||
await this.#socket.connect();
|
await this.#socket.connect();
|
||||||
|
return this as unknown as RedisClientType<M, F, S>;
|
||||||
}
|
}
|
||||||
|
|
||||||
async commandsExecutor<C extends RedisCommand>(
|
async commandsExecutor<C extends RedisCommand>(
|
||||||
@@ -415,7 +505,7 @@ export default class RedisClient<
|
|||||||
);
|
);
|
||||||
} else if (!this.#socket.isReady && this.#options?.disableOfflineQueue) {
|
} else if (!this.#socket.isReady && this.#options?.disableOfflineQueue) {
|
||||||
return Promise.reject(new ClientOfflineError());
|
return Promise.reject(new ClientOfflineError());
|
||||||
}
|
}
|
||||||
|
|
||||||
const promise = this.#queue.addCommand<T>(args, options);
|
const promise = this.#queue.addCommand<T>(args, options);
|
||||||
this.#tick();
|
this.#tick();
|
||||||
@@ -499,18 +589,9 @@ export default class RedisClient<
|
|||||||
|
|
||||||
select = this.SELECT;
|
select = this.SELECT;
|
||||||
|
|
||||||
#subscribe<T extends boolean>(
|
#pubSubCommand(promise: Promise<void> | undefined) {
|
||||||
command: PubSubSubscribeCommands,
|
if (promise === undefined) return Promise.resolve();
|
||||||
channels: string | Array<string>,
|
|
||||||
listener: PubSubListener<T>,
|
|
||||||
bufferMode?: T
|
|
||||||
): Promise<void> {
|
|
||||||
const promise = this.#queue.subscribe(
|
|
||||||
command,
|
|
||||||
channels,
|
|
||||||
listener,
|
|
||||||
bufferMode
|
|
||||||
);
|
|
||||||
this.#tick();
|
this.#tick();
|
||||||
return promise;
|
return promise;
|
||||||
}
|
}
|
||||||
@@ -520,77 +601,128 @@ export default class RedisClient<
|
|||||||
listener: PubSubListener<T>,
|
listener: PubSubListener<T>,
|
||||||
bufferMode?: T
|
bufferMode?: T
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
return this.#subscribe(
|
return this.#pubSubCommand(
|
||||||
PubSubSubscribeCommands.SUBSCRIBE,
|
this.#queue.subscribe(
|
||||||
channels,
|
PubSubType.CHANNELS,
|
||||||
listener,
|
channels,
|
||||||
bufferMode
|
listener,
|
||||||
|
bufferMode
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
subscribe = this.SUBSCRIBE;
|
subscribe = this.SUBSCRIBE;
|
||||||
|
|
||||||
PSUBSCRIBE<T extends boolean = false>(
|
|
||||||
patterns: string | Array<string>,
|
|
||||||
listener: PubSubListener<T>,
|
|
||||||
bufferMode?: T
|
|
||||||
): Promise<void> {
|
|
||||||
return this.#subscribe(
|
|
||||||
PubSubSubscribeCommands.PSUBSCRIBE,
|
|
||||||
patterns,
|
|
||||||
listener,
|
|
||||||
bufferMode
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
pSubscribe = this.PSUBSCRIBE;
|
|
||||||
|
|
||||||
#unsubscribe<T extends boolean>(
|
|
||||||
command: PubSubUnsubscribeCommands,
|
|
||||||
channels?: string | Array<string>,
|
|
||||||
listener?: PubSubListener<T>,
|
|
||||||
bufferMode?: T
|
|
||||||
): Promise<void> {
|
|
||||||
const promise = this.#queue.unsubscribe(command, channels, listener, bufferMode);
|
|
||||||
this.#tick();
|
|
||||||
return promise;
|
|
||||||
}
|
|
||||||
|
|
||||||
UNSUBSCRIBE<T extends boolean = false>(
|
UNSUBSCRIBE<T extends boolean = false>(
|
||||||
channels?: string | Array<string>,
|
channels?: string | Array<string>,
|
||||||
listener?: PubSubListener<T>,
|
listener?: PubSubListener<T>,
|
||||||
bufferMode?: T
|
bufferMode?: T
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
return this.#unsubscribe(
|
return this.#pubSubCommand(
|
||||||
PubSubUnsubscribeCommands.UNSUBSCRIBE,
|
this.#queue.unsubscribe(
|
||||||
channels,
|
PubSubType.CHANNELS,
|
||||||
listener,
|
channels,
|
||||||
bufferMode
|
listener,
|
||||||
|
bufferMode
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
unsubscribe = this.UNSUBSCRIBE;
|
unsubscribe = this.UNSUBSCRIBE;
|
||||||
|
|
||||||
|
PSUBSCRIBE<T extends boolean = false>(
|
||||||
|
patterns: string | Array<string>,
|
||||||
|
listener: PubSubListener<T>,
|
||||||
|
bufferMode?: T
|
||||||
|
): Promise<void> {
|
||||||
|
return this.#pubSubCommand(
|
||||||
|
this.#queue.subscribe(
|
||||||
|
PubSubType.PATTERNS,
|
||||||
|
patterns,
|
||||||
|
listener,
|
||||||
|
bufferMode
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pSubscribe = this.PSUBSCRIBE;
|
||||||
|
|
||||||
PUNSUBSCRIBE<T extends boolean = false>(
|
PUNSUBSCRIBE<T extends boolean = false>(
|
||||||
patterns?: string | Array<string>,
|
patterns?: string | Array<string>,
|
||||||
listener?: PubSubListener<T>,
|
listener?: PubSubListener<T>,
|
||||||
bufferMode?: T
|
bufferMode?: T
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
return this.#unsubscribe(
|
return this.#pubSubCommand(
|
||||||
PubSubUnsubscribeCommands.PUNSUBSCRIBE,
|
this.#queue.unsubscribe(
|
||||||
patterns,
|
PubSubType.PATTERNS,
|
||||||
listener,
|
patterns,
|
||||||
bufferMode
|
listener,
|
||||||
|
bufferMode
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
pUnsubscribe = this.PUNSUBSCRIBE;
|
pUnsubscribe = this.PUNSUBSCRIBE;
|
||||||
|
|
||||||
|
SSUBSCRIBE<T extends boolean = false>(
|
||||||
|
channels: string | Array<string>,
|
||||||
|
listener: PubSubListener<T>,
|
||||||
|
bufferMode?: T
|
||||||
|
): Promise<void> {
|
||||||
|
return this.#pubSubCommand(
|
||||||
|
this.#queue.subscribe(
|
||||||
|
PubSubType.SHARDED,
|
||||||
|
channels,
|
||||||
|
listener,
|
||||||
|
bufferMode
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
sSubscribe = this.SSUBSCRIBE;
|
||||||
|
|
||||||
|
SUNSUBSCRIBE<T extends boolean = false>(
|
||||||
|
channels?: string | Array<string>,
|
||||||
|
listener?: PubSubListener<T>,
|
||||||
|
bufferMode?: T
|
||||||
|
): Promise<void> {
|
||||||
|
return this.#pubSubCommand(
|
||||||
|
this.#queue.unsubscribe(
|
||||||
|
PubSubType.SHARDED,
|
||||||
|
channels,
|
||||||
|
listener,
|
||||||
|
bufferMode
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
sUnsubscribe = this.SUNSUBSCRIBE;
|
||||||
|
|
||||||
|
getPubSubListeners(type: PubSubType) {
|
||||||
|
return this.#queue.getPubSubListeners(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
extendPubSubChannelListeners(
|
||||||
|
type: PubSubType,
|
||||||
|
channel: string,
|
||||||
|
listeners: ChannelListeners
|
||||||
|
) {
|
||||||
|
return this.#pubSubCommand(
|
||||||
|
this.#queue.extendPubSubChannelListeners(type, channel, listeners)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
extendPubSubListeners(type: PubSubType, listeners: PubSubTypeListeners) {
|
||||||
|
return this.#pubSubCommand(
|
||||||
|
this.#queue.extendPubSubListeners(type, listeners)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
QUIT(): Promise<string> {
|
QUIT(): Promise<string> {
|
||||||
return this.#socket.quit(async () => {
|
return this.#socket.quit(async () => {
|
||||||
const quitPromise = this.#queue.addCommand<string>(['QUIT'], {
|
if (this.#pingTimer) clearTimeout(this.#pingTimer);
|
||||||
ignorePubSubMode: true
|
const quitPromise = this.#queue.addCommand<string>(['QUIT']);
|
||||||
});
|
|
||||||
this.#tick();
|
this.#tick();
|
||||||
const [reply] = await Promise.all([
|
const [reply] = await Promise.all([
|
||||||
quitPromise,
|
quitPromise,
|
||||||
@@ -618,6 +750,7 @@ export default class RedisClient<
|
|||||||
}
|
}
|
||||||
|
|
||||||
executeIsolated<T>(fn: (client: RedisClientType<M, F, S>) => T | Promise<T>): Promise<T> {
|
executeIsolated<T>(fn: (client: RedisClientType<M, F, S>) => T | Promise<T>): Promise<T> {
|
||||||
|
if (!this.#isolationPool) return Promise.reject(new ClientClosedError());
|
||||||
return this.#isolationPool.use(fn);
|
return this.#isolationPool.use(fn);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -639,11 +772,14 @@ export default class RedisClient<
|
|||||||
return Promise.reject(new ClientClosedError());
|
return Promise.reject(new ClientClosedError());
|
||||||
}
|
}
|
||||||
|
|
||||||
const promise = Promise.all(
|
const promise = chainId ?
|
||||||
commands.map(({ args }) => {
|
// if `chainId` has a value, it's a `MULTI` (and not "pipeline") - need to add the `MULTI` and `EXEC` commands
|
||||||
return this.#queue.addCommand(args, { chainId });
|
Promise.all([
|
||||||
})
|
this.#queue.addCommand(['MULTI'], { chainId }),
|
||||||
);
|
this.#addMultiCommands(commands, chainId),
|
||||||
|
this.#queue.addCommand(['EXEC'], { chainId })
|
||||||
|
]) :
|
||||||
|
this.#addMultiCommands(commands);
|
||||||
|
|
||||||
this.#tick();
|
this.#tick();
|
||||||
|
|
||||||
@@ -656,6 +792,12 @@ export default class RedisClient<
|
|||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#addMultiCommands(commands: Array<RedisMultiQueuedCommand>, chainId?: symbol) {
|
||||||
|
return Promise.all(
|
||||||
|
commands.map(({ args }) => this.#queue.addCommand(args, { chainId }))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async* scanIterator(options?: ScanCommandOptions): AsyncIterable<string> {
|
async* scanIterator(options?: ScanCommandOptions): AsyncIterable<string> {
|
||||||
let cursor = 0;
|
let cursor = 0;
|
||||||
do {
|
do {
|
||||||
@@ -678,6 +820,17 @@ export default class RedisClient<
|
|||||||
} while (cursor !== 0);
|
} while (cursor !== 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async* hScanNoValuesIterator(key: string, options?: ScanOptions): AsyncIterable<ConvertArgumentType<RedisCommandArgument, string>> {
|
||||||
|
let cursor = 0;
|
||||||
|
do {
|
||||||
|
const reply = await (this as any).hScanNoValues(key, cursor, options);
|
||||||
|
cursor = reply.cursor;
|
||||||
|
for (const k of reply.keys) {
|
||||||
|
yield k;
|
||||||
|
}
|
||||||
|
} while (cursor !== 0);
|
||||||
|
}
|
||||||
|
|
||||||
async* sScanIterator(key: string, options?: ScanOptions): AsyncIterable<string> {
|
async* sScanIterator(key: string, options?: ScanOptions): AsyncIterable<string> {
|
||||||
let cursor = 0;
|
let cursor = 0;
|
||||||
do {
|
do {
|
||||||
@@ -701,14 +854,16 @@ export default class RedisClient<
|
|||||||
}
|
}
|
||||||
|
|
||||||
async disconnect(): Promise<void> {
|
async disconnect(): Promise<void> {
|
||||||
|
if (this.#pingTimer) clearTimeout(this.#pingTimer);
|
||||||
this.#queue.flushAll(new DisconnectsClientError());
|
this.#queue.flushAll(new DisconnectsClientError());
|
||||||
this.#socket.disconnect();
|
this.#socket.disconnect();
|
||||||
await this.#destroyIsolationPool();
|
await this.#destroyIsolationPool();
|
||||||
}
|
}
|
||||||
|
|
||||||
async #destroyIsolationPool(): Promise<void> {
|
async #destroyIsolationPool(): Promise<void> {
|
||||||
await this.#isolationPool.drain();
|
await this.#isolationPool!.drain();
|
||||||
await this.#isolationPool.clear();
|
await this.#isolationPool!.clear();
|
||||||
|
this.#isolationPool = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
ref(): void {
|
ref(): void {
|
||||||
|
@@ -119,7 +119,7 @@ export default class RedisClientMultiCommand {
|
|||||||
|
|
||||||
for (const [ name, command ] of Object.entries(COMMANDS as RedisCommands)) {
|
for (const [ name, command ] of Object.entries(COMMANDS as RedisCommands)) {
|
||||||
this.#defineLegacyCommand(name, command);
|
this.#defineLegacyCommand(name, command);
|
||||||
(this as any)[name.toLowerCase()] = (this as any)[name];
|
(this as any)[name.toLowerCase()] ??= (this as any)[name];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,12 +170,9 @@ export default class RedisClientMultiCommand {
|
|||||||
return this.execAsPipeline();
|
return this.execAsPipeline();
|
||||||
}
|
}
|
||||||
|
|
||||||
const commands = this.#multi.exec();
|
|
||||||
if (!commands) return [];
|
|
||||||
|
|
||||||
return this.#multi.handleExecReplies(
|
return this.#multi.handleExecReplies(
|
||||||
await this.#executor(
|
await this.#executor(
|
||||||
commands,
|
this.#multi.queue,
|
||||||
this.#selectedDB,
|
this.#selectedDB,
|
||||||
RedisMultiCommand.generateChainId()
|
RedisMultiCommand.generateChainId()
|
||||||
)
|
)
|
||||||
@@ -185,6 +182,8 @@ export default class RedisClientMultiCommand {
|
|||||||
EXEC = this.exec;
|
EXEC = this.exec;
|
||||||
|
|
||||||
async execAsPipeline(): Promise<Array<RedisCommandRawReply>> {
|
async execAsPipeline(): Promise<Array<RedisCommandRawReply>> {
|
||||||
|
if (this.#multi.queue.length === 0) return [];
|
||||||
|
|
||||||
return this.#multi.transformReplies(
|
return this.#multi.transformReplies(
|
||||||
await this.#executor(
|
await this.#executor(
|
||||||
this.#multi.queue,
|
this.#multi.queue,
|
||||||
|
151
packages/client/lib/client/pub-sub.spec.ts
Normal file
151
packages/client/lib/client/pub-sub.spec.ts
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
import { strict as assert } from 'assert';
|
||||||
|
import { PubSub, PubSubType } from './pub-sub';
|
||||||
|
|
||||||
|
describe('PubSub', () => {
|
||||||
|
const TYPE = PubSubType.CHANNELS,
|
||||||
|
CHANNEL = 'channel',
|
||||||
|
LISTENER = () => {};
|
||||||
|
|
||||||
|
describe('subscribe to new channel', () => {
|
||||||
|
function createAndSubscribe() {
|
||||||
|
const pubSub = new PubSub(),
|
||||||
|
command = pubSub.subscribe(TYPE, CHANNEL, LISTENER);
|
||||||
|
|
||||||
|
assert.equal(pubSub.isActive, true);
|
||||||
|
assert.ok(command);
|
||||||
|
assert.equal(command.channelsCounter, 1);
|
||||||
|
|
||||||
|
return {
|
||||||
|
pubSub,
|
||||||
|
command
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
it('resolve', () => {
|
||||||
|
const { pubSub, command } = createAndSubscribe();
|
||||||
|
|
||||||
|
command.resolve();
|
||||||
|
|
||||||
|
assert.equal(pubSub.isActive, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reject', () => {
|
||||||
|
const { pubSub, command } = createAndSubscribe();
|
||||||
|
|
||||||
|
assert.ok(command.reject);
|
||||||
|
command.reject();
|
||||||
|
|
||||||
|
assert.equal(pubSub.isActive, false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('subscribe to already subscribed channel', () => {
|
||||||
|
const pubSub = new PubSub(),
|
||||||
|
firstSubscribe = pubSub.subscribe(TYPE, CHANNEL, LISTENER);
|
||||||
|
assert.ok(firstSubscribe);
|
||||||
|
|
||||||
|
const secondSubscribe = pubSub.subscribe(TYPE, CHANNEL, LISTENER);
|
||||||
|
assert.ok(secondSubscribe);
|
||||||
|
|
||||||
|
firstSubscribe.resolve();
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
pubSub.subscribe(TYPE, CHANNEL, LISTENER),
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('unsubscribe all', () => {
|
||||||
|
const pubSub = new PubSub();
|
||||||
|
|
||||||
|
const subscribe = pubSub.subscribe(TYPE, CHANNEL, LISTENER);
|
||||||
|
assert.ok(subscribe);
|
||||||
|
subscribe.resolve();
|
||||||
|
assert.equal(pubSub.isActive, true);
|
||||||
|
|
||||||
|
const unsubscribe = pubSub.unsubscribe(TYPE);
|
||||||
|
assert.equal(pubSub.isActive, true);
|
||||||
|
assert.ok(unsubscribe);
|
||||||
|
unsubscribe.resolve();
|
||||||
|
assert.equal(pubSub.isActive, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('unsubscribe from channel', () => {
|
||||||
|
it('when not subscribed', () => {
|
||||||
|
const pubSub = new PubSub(),
|
||||||
|
unsubscribe = pubSub.unsubscribe(TYPE, CHANNEL);
|
||||||
|
assert.ok(unsubscribe);
|
||||||
|
unsubscribe.resolve();
|
||||||
|
assert.equal(pubSub.isActive, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('when already subscribed', () => {
|
||||||
|
const pubSub = new PubSub(),
|
||||||
|
subscribe = pubSub.subscribe(TYPE, CHANNEL, LISTENER);
|
||||||
|
assert.ok(subscribe);
|
||||||
|
subscribe.resolve();
|
||||||
|
assert.equal(pubSub.isActive, true);
|
||||||
|
|
||||||
|
const unsubscribe = pubSub.unsubscribe(TYPE, CHANNEL);
|
||||||
|
assert.equal(pubSub.isActive, true);
|
||||||
|
assert.ok(unsubscribe);
|
||||||
|
unsubscribe.resolve();
|
||||||
|
assert.equal(pubSub.isActive, false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('unsubscribe from listener', () => {
|
||||||
|
it('when it\'s the only listener', () => {
|
||||||
|
const pubSub = new PubSub(),
|
||||||
|
subscribe = pubSub.subscribe(TYPE, CHANNEL, LISTENER);
|
||||||
|
assert.ok(subscribe);
|
||||||
|
subscribe.resolve();
|
||||||
|
assert.equal(pubSub.isActive, true);
|
||||||
|
|
||||||
|
const unsubscribe = pubSub.unsubscribe(TYPE, CHANNEL, LISTENER);
|
||||||
|
assert.ok(unsubscribe);
|
||||||
|
unsubscribe.resolve();
|
||||||
|
assert.equal(pubSub.isActive, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('when there are more listeners', () => {
|
||||||
|
const pubSub = new PubSub(),
|
||||||
|
subscribe = pubSub.subscribe(TYPE, CHANNEL, LISTENER);
|
||||||
|
assert.ok(subscribe);
|
||||||
|
subscribe.resolve();
|
||||||
|
assert.equal(pubSub.isActive, true);
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
pubSub.subscribe(TYPE, CHANNEL, () => {}),
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
pubSub.unsubscribe(TYPE, CHANNEL, LISTENER),
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('non-existing listener', () => {
|
||||||
|
it('on subscribed channel', () => {
|
||||||
|
const pubSub = new PubSub(),
|
||||||
|
subscribe = pubSub.subscribe(TYPE, CHANNEL, LISTENER);
|
||||||
|
assert.ok(subscribe);
|
||||||
|
subscribe.resolve();
|
||||||
|
assert.equal(pubSub.isActive, true);
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
pubSub.unsubscribe(TYPE, CHANNEL, () => {}),
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
assert.equal(pubSub.isActive, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('on unsubscribed channel', () => {
|
||||||
|
const pubSub = new PubSub();
|
||||||
|
assert.ok(pubSub.unsubscribe(TYPE, CHANNEL, () => {}));
|
||||||
|
assert.equal(pubSub.isActive, false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
408
packages/client/lib/client/pub-sub.ts
Normal file
408
packages/client/lib/client/pub-sub.ts
Normal file
@@ -0,0 +1,408 @@
|
|||||||
|
import { RedisCommandArgument } from "../commands";
|
||||||
|
|
||||||
|
export enum PubSubType {
|
||||||
|
CHANNELS = 'CHANNELS',
|
||||||
|
PATTERNS = 'PATTERNS',
|
||||||
|
SHARDED = 'SHARDED'
|
||||||
|
}
|
||||||
|
|
||||||
|
const COMMANDS = {
|
||||||
|
[PubSubType.CHANNELS]: {
|
||||||
|
subscribe: Buffer.from('subscribe'),
|
||||||
|
unsubscribe: Buffer.from('unsubscribe'),
|
||||||
|
message: Buffer.from('message')
|
||||||
|
},
|
||||||
|
[PubSubType.PATTERNS]: {
|
||||||
|
subscribe: Buffer.from('psubscribe'),
|
||||||
|
unsubscribe: Buffer.from('punsubscribe'),
|
||||||
|
message: Buffer.from('pmessage')
|
||||||
|
},
|
||||||
|
[PubSubType.SHARDED]: {
|
||||||
|
subscribe: Buffer.from('ssubscribe'),
|
||||||
|
unsubscribe: Buffer.from('sunsubscribe'),
|
||||||
|
message: Buffer.from('smessage')
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PubSubListener<
|
||||||
|
RETURN_BUFFERS extends boolean = false
|
||||||
|
> = <T extends RETURN_BUFFERS extends true ? Buffer : string>(message: T, channel: T) => unknown;
|
||||||
|
|
||||||
|
export interface ChannelListeners {
|
||||||
|
unsubscribing: boolean;
|
||||||
|
buffers: Set<PubSubListener<true>>;
|
||||||
|
strings: Set<PubSubListener<false>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PubSubTypeListeners = Map<string, ChannelListeners>;
|
||||||
|
|
||||||
|
type Listeners = Record<PubSubType, PubSubTypeListeners>;
|
||||||
|
|
||||||
|
export type PubSubCommand = ReturnType<
|
||||||
|
typeof PubSub.prototype.subscribe |
|
||||||
|
typeof PubSub.prototype.unsubscribe |
|
||||||
|
typeof PubSub.prototype.extendTypeListeners
|
||||||
|
>;
|
||||||
|
|
||||||
|
export class PubSub {
|
||||||
|
static isStatusReply(reply: Array<Buffer>): boolean {
|
||||||
|
return (
|
||||||
|
COMMANDS[PubSubType.CHANNELS].subscribe.equals(reply[0]) ||
|
||||||
|
COMMANDS[PubSubType.CHANNELS].unsubscribe.equals(reply[0]) ||
|
||||||
|
COMMANDS[PubSubType.PATTERNS].subscribe.equals(reply[0]) ||
|
||||||
|
COMMANDS[PubSubType.PATTERNS].unsubscribe.equals(reply[0]) ||
|
||||||
|
COMMANDS[PubSubType.SHARDED].subscribe.equals(reply[0])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static isShardedUnsubscribe(reply: Array<Buffer>): boolean {
|
||||||
|
return COMMANDS[PubSubType.SHARDED].unsubscribe.equals(reply[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
static #channelsArray(channels: string | Array<string>) {
|
||||||
|
return (Array.isArray(channels) ? channels : [channels]);
|
||||||
|
}
|
||||||
|
|
||||||
|
static #listenersSet<T extends boolean>(
|
||||||
|
listeners: ChannelListeners,
|
||||||
|
returnBuffers?: T
|
||||||
|
) {
|
||||||
|
return (returnBuffers ? listeners.buffers : listeners.strings);
|
||||||
|
}
|
||||||
|
|
||||||
|
#subscribing = 0;
|
||||||
|
|
||||||
|
#isActive = false;
|
||||||
|
|
||||||
|
get isActive() {
|
||||||
|
return this.#isActive;
|
||||||
|
}
|
||||||
|
|
||||||
|
#listeners: Listeners = {
|
||||||
|
[PubSubType.CHANNELS]: new Map(),
|
||||||
|
[PubSubType.PATTERNS]: new Map(),
|
||||||
|
[PubSubType.SHARDED]: new Map()
|
||||||
|
};
|
||||||
|
|
||||||
|
subscribe<T extends boolean>(
|
||||||
|
type: PubSubType,
|
||||||
|
channels: string | Array<string>,
|
||||||
|
listener: PubSubListener<T>,
|
||||||
|
returnBuffers?: T
|
||||||
|
) {
|
||||||
|
const args: Array<RedisCommandArgument> = [COMMANDS[type].subscribe],
|
||||||
|
channelsArray = PubSub.#channelsArray(channels);
|
||||||
|
for (const channel of channelsArray) {
|
||||||
|
let channelListeners = this.#listeners[type].get(channel);
|
||||||
|
if (!channelListeners || channelListeners.unsubscribing) {
|
||||||
|
args.push(channel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.length === 1) {
|
||||||
|
// all channels are already subscribed, add listeners without issuing a command
|
||||||
|
for (const channel of channelsArray) {
|
||||||
|
PubSub.#listenersSet(
|
||||||
|
this.#listeners[type].get(channel)!,
|
||||||
|
returnBuffers
|
||||||
|
).add(listener);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#isActive = true;
|
||||||
|
this.#subscribing++;
|
||||||
|
return {
|
||||||
|
args,
|
||||||
|
channelsCounter: args.length - 1,
|
||||||
|
resolve: () => {
|
||||||
|
this.#subscribing--;
|
||||||
|
for (const channel of channelsArray) {
|
||||||
|
let listeners = this.#listeners[type].get(channel);
|
||||||
|
if (!listeners) {
|
||||||
|
listeners = {
|
||||||
|
unsubscribing: false,
|
||||||
|
buffers: new Set(),
|
||||||
|
strings: new Set()
|
||||||
|
};
|
||||||
|
this.#listeners[type].set(channel, listeners);
|
||||||
|
}
|
||||||
|
|
||||||
|
PubSub.#listenersSet(listeners, returnBuffers).add(listener);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
reject: () => {
|
||||||
|
this.#subscribing--;
|
||||||
|
this.#updateIsActive();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
extendChannelListeners(
|
||||||
|
type: PubSubType,
|
||||||
|
channel: string,
|
||||||
|
listeners: ChannelListeners
|
||||||
|
) {
|
||||||
|
if (!this.#extendChannelListeners(type, channel, listeners)) return;
|
||||||
|
|
||||||
|
this.#isActive = true;
|
||||||
|
this.#subscribing++;
|
||||||
|
return {
|
||||||
|
args: [
|
||||||
|
COMMANDS[type].subscribe,
|
||||||
|
channel
|
||||||
|
],
|
||||||
|
channelsCounter: 1,
|
||||||
|
resolve: () => this.#subscribing--,
|
||||||
|
reject: () => {
|
||||||
|
this.#subscribing--;
|
||||||
|
this.#updateIsActive();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#extendChannelListeners(
|
||||||
|
type: PubSubType,
|
||||||
|
channel: string,
|
||||||
|
listeners: ChannelListeners
|
||||||
|
) {
|
||||||
|
const existingListeners = this.#listeners[type].get(channel);
|
||||||
|
if (!existingListeners) {
|
||||||
|
this.#listeners[type].set(channel, listeners);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const listener of listeners.buffers) {
|
||||||
|
existingListeners.buffers.add(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const listener of listeners.strings) {
|
||||||
|
existingListeners.strings.add(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
extendTypeListeners(type: PubSubType, listeners: PubSubTypeListeners) {
|
||||||
|
const args: Array<RedisCommandArgument> = [COMMANDS[type].subscribe];
|
||||||
|
for (const [channel, channelListeners] of listeners) {
|
||||||
|
if (this.#extendChannelListeners(type, channel, channelListeners)) {
|
||||||
|
args.push(channel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.length === 1) return;
|
||||||
|
|
||||||
|
this.#isActive = true;
|
||||||
|
this.#subscribing++;
|
||||||
|
return {
|
||||||
|
args,
|
||||||
|
channelsCounter: args.length - 1,
|
||||||
|
resolve: () => this.#subscribing--,
|
||||||
|
reject: () => {
|
||||||
|
this.#subscribing--;
|
||||||
|
this.#updateIsActive();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
unsubscribe<T extends boolean>(
|
||||||
|
type: PubSubType,
|
||||||
|
channels?: string | Array<string>,
|
||||||
|
listener?: PubSubListener<T>,
|
||||||
|
returnBuffers?: T
|
||||||
|
) {
|
||||||
|
const listeners = this.#listeners[type];
|
||||||
|
if (!channels) {
|
||||||
|
return this.#unsubscribeCommand(
|
||||||
|
[COMMANDS[type].unsubscribe],
|
||||||
|
// cannot use `this.#subscribed` because there might be some `SUBSCRIBE` commands in the queue
|
||||||
|
// cannot use `this.#subscribed + this.#subscribing` because some `SUBSCRIBE` commands might fail
|
||||||
|
NaN,
|
||||||
|
() => listeners.clear()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const channelsArray = PubSub.#channelsArray(channels);
|
||||||
|
if (!listener) {
|
||||||
|
return this.#unsubscribeCommand(
|
||||||
|
[COMMANDS[type].unsubscribe, ...channelsArray],
|
||||||
|
channelsArray.length,
|
||||||
|
() => {
|
||||||
|
for (const channel of channelsArray) {
|
||||||
|
listeners.delete(channel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const args: Array<RedisCommandArgument> = [COMMANDS[type].unsubscribe];
|
||||||
|
for (const channel of channelsArray) {
|
||||||
|
const sets = listeners.get(channel);
|
||||||
|
if (sets) {
|
||||||
|
let current,
|
||||||
|
other;
|
||||||
|
if (returnBuffers) {
|
||||||
|
current = sets.buffers;
|
||||||
|
other = sets.strings;
|
||||||
|
} else {
|
||||||
|
current = sets.strings;
|
||||||
|
other = sets.buffers;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentSize = current.has(listener) ? current.size - 1 : current.size;
|
||||||
|
if (currentSize !== 0 || other.size !== 0) continue;
|
||||||
|
sets.unsubscribing = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
args.push(channel);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.length === 1) {
|
||||||
|
// all channels has other listeners,
|
||||||
|
// delete the listeners without issuing a command
|
||||||
|
for (const channel of channelsArray) {
|
||||||
|
PubSub.#listenersSet(
|
||||||
|
listeners.get(channel)!,
|
||||||
|
returnBuffers
|
||||||
|
).delete(listener);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.#unsubscribeCommand(
|
||||||
|
args,
|
||||||
|
args.length - 1,
|
||||||
|
() => {
|
||||||
|
for (const channel of channelsArray) {
|
||||||
|
const sets = listeners.get(channel);
|
||||||
|
if (!sets) continue;
|
||||||
|
|
||||||
|
(returnBuffers ? sets.buffers : sets.strings).delete(listener);
|
||||||
|
if (sets.buffers.size === 0 && sets.strings.size === 0) {
|
||||||
|
listeners.delete(channel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#unsubscribeCommand(
|
||||||
|
args: Array<RedisCommandArgument>,
|
||||||
|
channelsCounter: number,
|
||||||
|
removeListeners: () => void
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
args,
|
||||||
|
channelsCounter,
|
||||||
|
resolve: () => {
|
||||||
|
removeListeners();
|
||||||
|
this.#updateIsActive();
|
||||||
|
},
|
||||||
|
reject: undefined // use the same structure as `subscribe`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#updateIsActive() {
|
||||||
|
this.#isActive = (
|
||||||
|
this.#listeners[PubSubType.CHANNELS].size !== 0 ||
|
||||||
|
this.#listeners[PubSubType.PATTERNS].size !== 0 ||
|
||||||
|
this.#listeners[PubSubType.SHARDED].size !== 0 ||
|
||||||
|
this.#subscribing !== 0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.#isActive = false;
|
||||||
|
this.#subscribing = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
resubscribe(): Array<PubSubCommand> {
|
||||||
|
const commands = [];
|
||||||
|
for (const [type, listeners] of Object.entries(this.#listeners)) {
|
||||||
|
if (!listeners.size) continue;
|
||||||
|
|
||||||
|
this.#isActive = true;
|
||||||
|
this.#subscribing++;
|
||||||
|
const callback = () => this.#subscribing--;
|
||||||
|
commands.push({
|
||||||
|
args: [
|
||||||
|
COMMANDS[type as PubSubType].subscribe,
|
||||||
|
...listeners.keys()
|
||||||
|
],
|
||||||
|
channelsCounter: listeners.size,
|
||||||
|
resolve: callback,
|
||||||
|
reject: callback
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return commands;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMessageReply(reply: Array<Buffer>): boolean {
|
||||||
|
if (COMMANDS[PubSubType.CHANNELS].message.equals(reply[0])) {
|
||||||
|
this.#emitPubSubMessage(
|
||||||
|
PubSubType.CHANNELS,
|
||||||
|
reply[2],
|
||||||
|
reply[1]
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
} else if (COMMANDS[PubSubType.PATTERNS].message.equals(reply[0])) {
|
||||||
|
this.#emitPubSubMessage(
|
||||||
|
PubSubType.PATTERNS,
|
||||||
|
reply[3],
|
||||||
|
reply[2],
|
||||||
|
reply[1]
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
} else if (COMMANDS[PubSubType.SHARDED].message.equals(reply[0])) {
|
||||||
|
this.#emitPubSubMessage(
|
||||||
|
PubSubType.SHARDED,
|
||||||
|
reply[2],
|
||||||
|
reply[1]
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
removeShardedListeners(channel: string): ChannelListeners {
|
||||||
|
const listeners = this.#listeners[PubSubType.SHARDED].get(channel)!;
|
||||||
|
this.#listeners[PubSubType.SHARDED].delete(channel);
|
||||||
|
this.#updateIsActive();
|
||||||
|
return listeners;
|
||||||
|
}
|
||||||
|
|
||||||
|
#emitPubSubMessage(
|
||||||
|
type: PubSubType,
|
||||||
|
message: Buffer,
|
||||||
|
channel: Buffer,
|
||||||
|
pattern?: Buffer
|
||||||
|
): void {
|
||||||
|
const keyString = (pattern ?? channel).toString(),
|
||||||
|
listeners = this.#listeners[type].get(keyString);
|
||||||
|
|
||||||
|
if (!listeners) return;
|
||||||
|
|
||||||
|
for (const listener of listeners.buffers) {
|
||||||
|
listener(message, channel);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!listeners.strings.size) return;
|
||||||
|
|
||||||
|
const channelString = pattern ? channel.toString() : keyString,
|
||||||
|
messageString = channelString === '__redis__:invalidate' ?
|
||||||
|
// https://github.com/redis/redis/pull/7469
|
||||||
|
// https://github.com/redis/redis/issues/7463
|
||||||
|
(message === null ? null : (message as any as Array<Buffer>).map(x => x.toString())) as any :
|
||||||
|
message.toString();
|
||||||
|
for (const listener of listeners.strings) {
|
||||||
|
listener(messageString, channelString);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getTypeListeners(type: PubSubType): PubSubTypeListeners {
|
||||||
|
return this.#listeners[type];
|
||||||
|
}
|
||||||
|
}
|
@@ -1,5 +1,6 @@
|
|||||||
import { strict as assert } from 'assert';
|
import { strict as assert } from 'assert';
|
||||||
import { spy } from 'sinon';
|
import { spy } from 'sinon';
|
||||||
|
import { once } from 'events';
|
||||||
import RedisSocket, { RedisSocketOptions } from './socket';
|
import RedisSocket, { RedisSocketOptions } from './socket';
|
||||||
|
|
||||||
describe('Socket', () => {
|
describe('Socket', () => {
|
||||||
@@ -17,16 +18,42 @@ describe('Socket', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe('reconnectStrategy', () => {
|
describe('reconnectStrategy', () => {
|
||||||
|
it('false', async () => {
|
||||||
|
const socket = createSocket({
|
||||||
|
host: 'error',
|
||||||
|
connectTimeout: 1,
|
||||||
|
reconnectStrategy: false
|
||||||
|
});
|
||||||
|
|
||||||
|
await assert.rejects(socket.connect());
|
||||||
|
|
||||||
|
assert.equal(socket.isOpen, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('0', async () => {
|
||||||
|
const socket = createSocket({
|
||||||
|
host: 'error',
|
||||||
|
connectTimeout: 1,
|
||||||
|
reconnectStrategy: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.connect();
|
||||||
|
await once(socket, 'error');
|
||||||
|
assert.equal(socket.isOpen, true);
|
||||||
|
assert.equal(socket.isReady, false);
|
||||||
|
socket.disconnect();
|
||||||
|
assert.equal(socket.isOpen, false);
|
||||||
|
});
|
||||||
|
|
||||||
it('custom strategy', async () => {
|
it('custom strategy', async () => {
|
||||||
const numberOfRetries = 10;
|
const numberOfRetries = 3;
|
||||||
|
|
||||||
const reconnectStrategy = spy((retries: number) => {
|
const reconnectStrategy = spy((retries: number) => {
|
||||||
assert.equal(retries + 1, reconnectStrategy.callCount);
|
assert.equal(retries + 1, reconnectStrategy.callCount);
|
||||||
|
|
||||||
if (retries === numberOfRetries) return new Error(`${numberOfRetries}`);
|
if (retries === numberOfRetries) return new Error(`${numberOfRetries}`);
|
||||||
|
|
||||||
const time = retries * 2;
|
return 0;
|
||||||
return time;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const socket = createSocket({
|
const socket = createSocket({
|
||||||
|
@@ -6,10 +6,26 @@ import { ConnectionTimeoutError, ClientClosedError, SocketClosedUnexpectedlyErro
|
|||||||
import { promiseTimeout } from '../utils';
|
import { promiseTimeout } from '../utils';
|
||||||
|
|
||||||
export interface RedisSocketCommonOptions {
|
export interface RedisSocketCommonOptions {
|
||||||
|
/**
|
||||||
|
* Connection Timeout (in milliseconds)
|
||||||
|
*/
|
||||||
connectTimeout?: number;
|
connectTimeout?: number;
|
||||||
|
/**
|
||||||
|
* Toggle [`Nagle's algorithm`](https://nodejs.org/api/net.html#net_socket_setnodelay_nodelay)
|
||||||
|
*/
|
||||||
noDelay?: boolean;
|
noDelay?: boolean;
|
||||||
|
/**
|
||||||
|
* Toggle [`keep-alive`](https://nodejs.org/api/net.html#net_socket_setkeepalive_enable_initialdelay)
|
||||||
|
*/
|
||||||
keepAlive?: number | false;
|
keepAlive?: number | false;
|
||||||
reconnectStrategy?(retries: number): number | Error;
|
/**
|
||||||
|
* When the socket closes unexpectedly (without calling `.quit()`/`.disconnect()`), the client uses `reconnectStrategy` to decide what to do. The following values are supported:
|
||||||
|
* 1. `false` -> do not reconnect, close the client and flush the command queue.
|
||||||
|
* 2. `number` -> wait for `X` milliseconds before reconnecting.
|
||||||
|
* 3. `(retries: number, cause: Error) => false | number | Error` -> `number` is the same as configuring a `number` directly, `Error` is the same as `false`, but with a custom error.
|
||||||
|
* Defaults to `retries => Math.min(retries * 50, 500)`
|
||||||
|
*/
|
||||||
|
reconnectStrategy?: false | number | ((retries: number, cause: Error) => false | Error | number);
|
||||||
}
|
}
|
||||||
|
|
||||||
type RedisNetSocketOptions = Partial<net.SocketConnectOpts> & {
|
type RedisNetSocketOptions = Partial<net.SocketConnectOpts> & {
|
||||||
@@ -83,23 +99,42 @@ export default class RedisSocket extends EventEmitter {
|
|||||||
this.#options = RedisSocket.#initiateOptions(options);
|
this.#options = RedisSocket.#initiateOptions(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
reconnectStrategy(retries: number): number | Error {
|
#reconnectStrategy(retries: number, cause: Error) {
|
||||||
if (this.#options.reconnectStrategy) {
|
if (this.#options.reconnectStrategy === false) {
|
||||||
|
return false;
|
||||||
|
} else if (typeof this.#options.reconnectStrategy === 'number') {
|
||||||
|
return this.#options.reconnectStrategy;
|
||||||
|
} else if (this.#options.reconnectStrategy) {
|
||||||
try {
|
try {
|
||||||
const retryIn = this.#options.reconnectStrategy(retries);
|
const retryIn = this.#options.reconnectStrategy(retries, cause);
|
||||||
if (typeof retryIn !== 'number' && !(retryIn instanceof Error)) {
|
if (retryIn !== false && !(retryIn instanceof Error) && typeof retryIn !== 'number') {
|
||||||
throw new TypeError('Reconnect strategy should return `number | Error`');
|
throw new TypeError(`Reconnect strategy should return \`false | Error | number\`, got ${retryIn} instead`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return retryIn;
|
return retryIn;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.emit('error', err);
|
this.emit('error', err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Math.min(retries * 50, 500);
|
return Math.min(retries * 50, 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#shouldReconnect(retries: number, cause: Error) {
|
||||||
|
const retryIn = this.#reconnectStrategy(retries, cause);
|
||||||
|
if (retryIn === false) {
|
||||||
|
this.#isOpen = false;
|
||||||
|
this.emit('error', cause);
|
||||||
|
return cause;
|
||||||
|
} else if (retryIn instanceof Error) {
|
||||||
|
this.#isOpen = false;
|
||||||
|
this.emit('error', cause);
|
||||||
|
return new ReconnectStrategyError(retryIn, cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
return retryIn;
|
||||||
|
}
|
||||||
|
|
||||||
async connect(): Promise<void> {
|
async connect(): Promise<void> {
|
||||||
if (this.#isOpen) {
|
if (this.#isOpen) {
|
||||||
throw new Error('Socket already opened');
|
throw new Error('Socket already opened');
|
||||||
@@ -109,13 +144,9 @@ export default class RedisSocket extends EventEmitter {
|
|||||||
return this.#connect();
|
return this.#connect();
|
||||||
}
|
}
|
||||||
|
|
||||||
async #connect(hadError?: boolean): Promise<void> {
|
async #connect(): Promise<void> {
|
||||||
let retries = 0;
|
let retries = 0;
|
||||||
do {
|
do {
|
||||||
if (retries > 0 || hadError) {
|
|
||||||
this.emit('reconnecting');
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.#socket = await this.#createSocket();
|
this.#socket = await this.#createSocket();
|
||||||
this.#writableNeedDrain = false;
|
this.#writableNeedDrain = false;
|
||||||
@@ -131,17 +162,15 @@ export default class RedisSocket extends EventEmitter {
|
|||||||
this.#isReady = true;
|
this.#isReady = true;
|
||||||
this.emit('ready');
|
this.emit('ready');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const retryIn = this.reconnectStrategy(retries);
|
const retryIn = this.#shouldReconnect(retries++, err as Error);
|
||||||
if (retryIn instanceof Error) {
|
if (typeof retryIn !== 'number') {
|
||||||
this.#isOpen = false;
|
throw retryIn;
|
||||||
this.emit('error', err);
|
|
||||||
throw new ReconnectStrategyError(retryIn, err);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.emit('error', err);
|
this.emit('error', err);
|
||||||
await promiseTimeout(retryIn);
|
await promiseTimeout(retryIn);
|
||||||
|
this.emit('reconnecting');
|
||||||
}
|
}
|
||||||
retries++;
|
|
||||||
} while (this.#isOpen && !this.#isReady);
|
} while (this.#isOpen && !this.#isReady);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,12 +229,14 @@ export default class RedisSocket extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#onSocketError(err: Error): void {
|
#onSocketError(err: Error): void {
|
||||||
|
const wasReady = this.#isReady;
|
||||||
this.#isReady = false;
|
this.#isReady = false;
|
||||||
this.emit('error', err);
|
this.emit('error', err);
|
||||||
|
|
||||||
if (!this.#isOpen) return;
|
if (!wasReady || !this.#isOpen || typeof this.#shouldReconnect(0, err) !== 'number') return;
|
||||||
|
|
||||||
this.#connect(true).catch(() => {
|
this.emit('reconnecting');
|
||||||
|
this.#connect().catch(() => {
|
||||||
// the error was already emitted, silently ignore it
|
// the error was already emitted, silently ignore it
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -261,7 +292,7 @@ export default class RedisSocket extends EventEmitter {
|
|||||||
this.#socket.cork();
|
this.#socket.cork();
|
||||||
this.#isCorked = true;
|
this.#isCorked = true;
|
||||||
|
|
||||||
queueMicrotask(() => {
|
setImmediate(() => {
|
||||||
this.#socket?.uncork();
|
this.#socket?.uncork();
|
||||||
this.#isCorked = false;
|
this.#isCorked = false;
|
||||||
});
|
});
|
||||||
|
@@ -1,23 +1,17 @@
|
|||||||
import RedisClient, { InstantiableRedisClient, RedisClientType } from '../client';
|
import RedisClient, { InstantiableRedisClient, RedisClientType } from '../client';
|
||||||
import { RedisClusterMasterNode, RedisClusterReplicaNode } from '../commands/CLUSTER_NODES';
|
|
||||||
import { RedisClusterClientOptions, RedisClusterOptions } from '.';
|
import { RedisClusterClientOptions, RedisClusterOptions } from '.';
|
||||||
import { RedisCommandArgument, RedisFunctions, RedisModules, RedisScripts } from '../commands';
|
import { RedisCommandArgument, RedisFunctions, RedisModules, RedisScripts } from '../commands';
|
||||||
import { RootNodesUnavailableError } from '../errors';
|
import { RootNodesUnavailableError } from '../errors';
|
||||||
|
import { ClusterSlotsNode } from '../commands/CLUSTER_SLOTS';
|
||||||
|
import { types } from 'util';
|
||||||
|
import { ChannelListeners, PubSubType, PubSubTypeListeners } from '../client/pub-sub';
|
||||||
|
import { EventEmitter } from 'stream';
|
||||||
|
|
||||||
// We need to use 'require', because it's not possible with Typescript to import
|
// We need to use 'require', because it's not possible with Typescript to import
|
||||||
// function that are exported as 'module.exports = function`, without esModuleInterop
|
// function that are exported as 'module.exports = function`, without esModuleInterop
|
||||||
// set to true.
|
// set to true.
|
||||||
const calculateSlot = require('cluster-key-slot');
|
const calculateSlot = require('cluster-key-slot');
|
||||||
|
|
||||||
export interface ClusterNode<
|
|
||||||
M extends RedisModules,
|
|
||||||
F extends RedisFunctions,
|
|
||||||
S extends RedisScripts
|
|
||||||
> {
|
|
||||||
id: string;
|
|
||||||
client: RedisClientType<M, F, S>;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface NodeAddress {
|
interface NodeAddress {
|
||||||
host: string;
|
host: string;
|
||||||
port: number;
|
port: number;
|
||||||
@@ -27,133 +21,236 @@ export type NodeAddressMap = {
|
|||||||
[address: string]: NodeAddress;
|
[address: string]: NodeAddress;
|
||||||
} | ((address: string) => NodeAddress | undefined);
|
} | ((address: string) => NodeAddress | undefined);
|
||||||
|
|
||||||
interface SlotNodes<
|
type ValueOrPromise<T> = T | Promise<T>;
|
||||||
|
|
||||||
|
type ClientOrPromise<
|
||||||
|
M extends RedisModules,
|
||||||
|
F extends RedisFunctions,
|
||||||
|
S extends RedisScripts
|
||||||
|
> = ValueOrPromise<RedisClientType<M, F, S>>;
|
||||||
|
|
||||||
|
export interface Node<
|
||||||
M extends RedisModules,
|
M extends RedisModules,
|
||||||
F extends RedisFunctions,
|
F extends RedisFunctions,
|
||||||
S extends RedisScripts
|
S extends RedisScripts
|
||||||
> {
|
> {
|
||||||
master: ClusterNode<M, F, S>;
|
address: string;
|
||||||
replicas: Array<ClusterNode<M, F, S>>;
|
client?: ClientOrPromise<M, F, S>;
|
||||||
clientIterator: IterableIterator<RedisClientType<M, F, S>> | undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type OnError = (err: unknown) => void;
|
export interface ShardNode<
|
||||||
|
M extends RedisModules,
|
||||||
|
F extends RedisFunctions,
|
||||||
|
S extends RedisScripts
|
||||||
|
> extends Node<M, F, S> {
|
||||||
|
id: string;
|
||||||
|
host: string;
|
||||||
|
port: number;
|
||||||
|
readonly: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MasterNode<
|
||||||
|
M extends RedisModules,
|
||||||
|
F extends RedisFunctions,
|
||||||
|
S extends RedisScripts
|
||||||
|
> extends ShardNode<M, F, S> {
|
||||||
|
pubSubClient?: ClientOrPromise<M, F, S>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Shard<
|
||||||
|
M extends RedisModules,
|
||||||
|
F extends RedisFunctions,
|
||||||
|
S extends RedisScripts
|
||||||
|
> {
|
||||||
|
master: MasterNode<M, F, S>;
|
||||||
|
replicas?: Array<ShardNode<M, F, S>>;
|
||||||
|
nodesIterator?: IterableIterator<ShardNode<M, F, S>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ShardWithReplicas<
|
||||||
|
M extends RedisModules,
|
||||||
|
F extends RedisFunctions,
|
||||||
|
S extends RedisScripts
|
||||||
|
> = Shard<M, F, S> & Required<Pick<Shard<M, F, S>, 'replicas'>>;
|
||||||
|
|
||||||
|
export type PubSubNode<
|
||||||
|
M extends RedisModules,
|
||||||
|
F extends RedisFunctions,
|
||||||
|
S extends RedisScripts
|
||||||
|
> = Required<Node<M, F, S>>;
|
||||||
|
|
||||||
|
type PubSubToResubscribe = Record<
|
||||||
|
PubSubType.CHANNELS | PubSubType.PATTERNS,
|
||||||
|
PubSubTypeListeners
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type OnShardedChannelMovedError = (
|
||||||
|
err: unknown,
|
||||||
|
channel: string,
|
||||||
|
listeners?: ChannelListeners
|
||||||
|
) => void;
|
||||||
|
|
||||||
export default class RedisClusterSlots<
|
export default class RedisClusterSlots<
|
||||||
M extends RedisModules,
|
M extends RedisModules,
|
||||||
F extends RedisFunctions,
|
F extends RedisFunctions,
|
||||||
S extends RedisScripts
|
S extends RedisScripts
|
||||||
> {
|
> {
|
||||||
|
static #SLOTS = 16384;
|
||||||
|
|
||||||
readonly #options: RedisClusterOptions<M, F, S>;
|
readonly #options: RedisClusterOptions<M, F, S>;
|
||||||
readonly #Client: InstantiableRedisClient<M, F, S>;
|
readonly #Client: InstantiableRedisClient<M, F, S>;
|
||||||
readonly #onError: OnError;
|
readonly #emit: EventEmitter['emit'];
|
||||||
readonly #nodeByAddress = new Map<string, ClusterNode<M, F, S>>();
|
slots = new Array<Shard<M, F, S>>(RedisClusterSlots.#SLOTS);
|
||||||
readonly #slots: Array<SlotNodes<M, F, S>> = [];
|
shards = new Array<Shard<M, F, S>>();
|
||||||
|
masters = new Array<ShardNode<M, F, S>>();
|
||||||
|
replicas = new Array<ShardNode<M, F, S>>();
|
||||||
|
readonly nodeByAddress = new Map<string, MasterNode<M, F, S> | ShardNode<M, F, S>>();
|
||||||
|
pubSubNode?: PubSubNode<M, F, S>;
|
||||||
|
|
||||||
constructor(options: RedisClusterOptions<M, F, S>, onError: OnError) {
|
#isOpen = false;
|
||||||
this.#options = options;
|
|
||||||
this.#Client = RedisClient.extend(options);
|
get isOpen() {
|
||||||
this.#onError = onError;
|
return this.#isOpen;
|
||||||
}
|
}
|
||||||
|
|
||||||
async connect(): Promise<void> {
|
constructor(
|
||||||
for (const rootNode of this.#options.rootNodes) {
|
options: RedisClusterOptions<M, F, S>,
|
||||||
if (await this.#discoverNodes(rootNode)) return;
|
emit: EventEmitter['emit']
|
||||||
|
) {
|
||||||
|
this.#options = options;
|
||||||
|
this.#Client = RedisClient.extend(options);
|
||||||
|
this.#emit = emit;
|
||||||
|
}
|
||||||
|
|
||||||
|
async connect() {
|
||||||
|
if (this.#isOpen) {
|
||||||
|
throw new Error('Cluster already open');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#isOpen = true;
|
||||||
|
try {
|
||||||
|
await this.#discoverWithRootNodes();
|
||||||
|
} catch (err) {
|
||||||
|
this.#isOpen = false;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async #discoverWithRootNodes() {
|
||||||
|
let start = Math.floor(Math.random() * this.#options.rootNodes.length);
|
||||||
|
for (let i = start; i < this.#options.rootNodes.length; i++) {
|
||||||
|
if (await this.#discover(this.#options.rootNodes[i])) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < start; i++) {
|
||||||
|
if (await this.#discover(this.#options.rootNodes[i])) return;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new RootNodesUnavailableError();
|
throw new RootNodesUnavailableError();
|
||||||
}
|
}
|
||||||
|
|
||||||
async #discoverNodes(clientOptions?: RedisClusterClientOptions): Promise<boolean> {
|
#resetSlots() {
|
||||||
const client = this.#initiateClient(clientOptions);
|
this.slots = new Array(RedisClusterSlots.#SLOTS);
|
||||||
|
this.shards = [];
|
||||||
|
this.masters = [];
|
||||||
|
this.replicas = [];
|
||||||
|
this.#randomNodeIterator = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
async #discover(rootNode?: RedisClusterClientOptions) {
|
||||||
|
const addressesInUse = new Set<string>();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const shards = await this.#getShards(rootNode),
|
||||||
|
promises: Array<Promise<unknown>> = [],
|
||||||
|
eagerConnect = this.#options.minimizeConnections !== true;
|
||||||
|
this.#resetSlots();
|
||||||
|
for (const { from, to, master, replicas } of shards) {
|
||||||
|
const shard: Shard<M, F, S> = {
|
||||||
|
master: this.#initiateSlotNode(master, false, eagerConnect, addressesInUse, promises)
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.#options.useReplicas) {
|
||||||
|
shard.replicas = replicas.map(replica =>
|
||||||
|
this.#initiateSlotNode(replica, true, eagerConnect, addressesInUse, promises)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.shards.push(shard);
|
||||||
|
|
||||||
|
for (let i = from; i <= to; i++) {
|
||||||
|
this.slots[i] = shard;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.pubSubNode && !addressesInUse.has(this.pubSubNode.address)) {
|
||||||
|
if (types.isPromise(this.pubSubNode.client)) {
|
||||||
|
promises.push(
|
||||||
|
this.pubSubNode.client.then(client => client.disconnect())
|
||||||
|
);
|
||||||
|
this.pubSubNode = undefined;
|
||||||
|
} else {
|
||||||
|
promises.push(this.pubSubNode.client.disconnect());
|
||||||
|
|
||||||
|
const channelsListeners = this.pubSubNode.client.getPubSubListeners(PubSubType.CHANNELS),
|
||||||
|
patternsListeners = this.pubSubNode.client.getPubSubListeners(PubSubType.PATTERNS);
|
||||||
|
|
||||||
|
if (channelsListeners.size || patternsListeners.size) {
|
||||||
|
promises.push(
|
||||||
|
this.#initiatePubSubClient({
|
||||||
|
[PubSubType.CHANNELS]: channelsListeners,
|
||||||
|
[PubSubType.PATTERNS]: patternsListeners
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [address, node] of this.nodeByAddress.entries()) {
|
||||||
|
if (addressesInUse.has(address)) continue;
|
||||||
|
|
||||||
|
if (node.client) {
|
||||||
|
promises.push(
|
||||||
|
this.#execOnNodeClient(node.client, client => client.disconnect())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { pubSubClient } = node as MasterNode<M, F, S>;
|
||||||
|
if (pubSubClient) {
|
||||||
|
promises.push(
|
||||||
|
this.#execOnNodeClient(pubSubClient, client => client.disconnect())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.nodeByAddress.delete(address);
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(promises);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
this.#emit('error', err);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async #getShards(rootNode?: RedisClusterClientOptions) {
|
||||||
|
const client = new this.#Client(
|
||||||
|
this.#clientOptionsDefaults(rootNode, true)
|
||||||
|
);
|
||||||
|
|
||||||
|
client.on('error', err => this.#emit('error', err));
|
||||||
|
|
||||||
await client.connect();
|
await client.connect();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.#reset(await client.clusterNodes());
|
// using `CLUSTER SLOTS` and not `CLUSTER SHARDS` to support older versions
|
||||||
return true;
|
return await client.clusterSlots();
|
||||||
} catch (err) {
|
|
||||||
this.#onError(err);
|
|
||||||
return false;
|
|
||||||
} finally {
|
} finally {
|
||||||
if (client.isOpen) {
|
await client.disconnect();
|
||||||
await client.disconnect();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#runningRediscoverPromise?: Promise<void>;
|
|
||||||
|
|
||||||
async rediscover(startWith: RedisClientType<M, F, S>): Promise<void> {
|
|
||||||
if (!this.#runningRediscoverPromise) {
|
|
||||||
this.#runningRediscoverPromise = this.#rediscover(startWith)
|
|
||||||
.finally(() => this.#runningRediscoverPromise = undefined);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.#runningRediscoverPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
async #rediscover(startWith: RedisClientType<M, F, S>): Promise<void> {
|
|
||||||
if (await this.#discoverNodes(startWith.options)) return;
|
|
||||||
|
|
||||||
for (const { client } of this.#nodeByAddress.values()) {
|
|
||||||
if (client === startWith) continue;
|
|
||||||
|
|
||||||
if (await this.#discoverNodes(client.options)) return;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error('None of the cluster nodes is available');
|
|
||||||
}
|
|
||||||
|
|
||||||
async #reset(masters: Array<RedisClusterMasterNode>): Promise<void> {
|
|
||||||
// Override this.#slots and add not existing clients to this.#nodeByAddress
|
|
||||||
const promises: Array<Promise<void>> = [],
|
|
||||||
clientsInUse = new Set<string>();
|
|
||||||
for (const master of masters) {
|
|
||||||
const slot = {
|
|
||||||
master: this.#initiateClientForNode(master, false, clientsInUse, promises),
|
|
||||||
replicas: this.#options.useReplicas ?
|
|
||||||
master.replicas.map(replica => this.#initiateClientForNode(replica, true, clientsInUse, promises)) :
|
|
||||||
[],
|
|
||||||
clientIterator: undefined // will be initiated in use
|
|
||||||
};
|
|
||||||
|
|
||||||
for (const { from, to } of master.slots) {
|
|
||||||
for (let i = from; i <= to; i++) {
|
|
||||||
this.#slots[i] = slot;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove unused clients from this.#nodeByAddress using clientsInUse
|
|
||||||
for (const [address, { client }] of this.#nodeByAddress.entries()) {
|
|
||||||
if (clientsInUse.has(address)) continue;
|
|
||||||
|
|
||||||
promises.push(client.disconnect());
|
|
||||||
this.#nodeByAddress.delete(address);
|
|
||||||
}
|
|
||||||
|
|
||||||
await Promise.all(promises);
|
|
||||||
}
|
|
||||||
|
|
||||||
#clientOptionsDefaults(options?: RedisClusterClientOptions): RedisClusterClientOptions | undefined {
|
|
||||||
if (!this.#options.defaults) return options;
|
|
||||||
|
|
||||||
return {
|
|
||||||
...this.#options.defaults,
|
|
||||||
...options,
|
|
||||||
socket: this.#options.defaults.socket && options?.socket ? {
|
|
||||||
...this.#options.defaults.socket,
|
|
||||||
...options.socket
|
|
||||||
} : this.#options.defaults.socket ?? options?.socket
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
#initiateClient(options?: RedisClusterClientOptions): RedisClientType<M, F, S> {
|
|
||||||
return new this.#Client(this.#clientOptionsDefaults(options))
|
|
||||||
.on('error', this.#onError);
|
|
||||||
}
|
|
||||||
|
|
||||||
#getNodeAddress(address: string): NodeAddress | undefined {
|
#getNodeAddress(address: string): NodeAddress | undefined {
|
||||||
switch (typeof this.#options.nodeAddressMap) {
|
switch (typeof this.#options.nodeAddressMap) {
|
||||||
case 'object':
|
case 'object':
|
||||||
@@ -164,111 +261,123 @@ export default class RedisClusterSlots<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#initiateClientForNode(
|
#clientOptionsDefaults(
|
||||||
nodeData: RedisClusterMasterNode | RedisClusterReplicaNode,
|
options?: RedisClusterClientOptions,
|
||||||
readonly: boolean,
|
disableReconnect?: boolean
|
||||||
clientsInUse: Set<string>,
|
): RedisClusterClientOptions | undefined {
|
||||||
promises: Array<Promise<void>>
|
let result: RedisClusterClientOptions | undefined;
|
||||||
): ClusterNode<M, F, S> {
|
if (this.#options.defaults) {
|
||||||
const address = `${nodeData.host}:${nodeData.port}`;
|
let socket;
|
||||||
clientsInUse.add(address);
|
if (this.#options.defaults.socket) {
|
||||||
|
socket = {
|
||||||
|
...this.#options.defaults.socket,
|
||||||
|
...options?.socket
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
socket = options?.socket;
|
||||||
|
}
|
||||||
|
|
||||||
let node = this.#nodeByAddress.get(address);
|
result = {
|
||||||
|
...this.#options.defaults,
|
||||||
|
...options,
|
||||||
|
socket
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
result = options;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (disableReconnect) {
|
||||||
|
result ??= {};
|
||||||
|
result.socket ??= {};
|
||||||
|
result.socket.reconnectStrategy = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
#initiateSlotNode(
|
||||||
|
{ id, ip, port }: ClusterSlotsNode,
|
||||||
|
readonly: boolean,
|
||||||
|
eagerConnent: boolean,
|
||||||
|
addressesInUse: Set<string>,
|
||||||
|
promises: Array<Promise<unknown>>
|
||||||
|
) {
|
||||||
|
const address = `${ip}:${port}`;
|
||||||
|
addressesInUse.add(address);
|
||||||
|
|
||||||
|
let node = this.nodeByAddress.get(address);
|
||||||
if (!node) {
|
if (!node) {
|
||||||
node = {
|
node = {
|
||||||
id: nodeData.id,
|
id,
|
||||||
client: this.#initiateClient({
|
host: ip,
|
||||||
socket: this.#getNodeAddress(address) ?? {
|
port,
|
||||||
host: nodeData.host,
|
address,
|
||||||
port: nodeData.port
|
readonly,
|
||||||
},
|
client: undefined
|
||||||
readonly
|
|
||||||
})
|
|
||||||
};
|
};
|
||||||
promises.push(node.client.connect());
|
|
||||||
this.#nodeByAddress.set(address, node);
|
if (eagerConnent) {
|
||||||
|
promises.push(this.#createNodeClient(node));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.nodeByAddress.set(address, node);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
(readonly ? this.replicas : this.masters).push(node);
|
||||||
|
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
getSlotMaster(slot: number): ClusterNode<M, F, S> {
|
async #createClient(
|
||||||
return this.#slots[slot].master;
|
node: ShardNode<M, F, S>,
|
||||||
}
|
readonly = node.readonly
|
||||||
|
) {
|
||||||
*#slotClientIterator(slotNumber: number): IterableIterator<RedisClientType<M, F, S>> {
|
const client = new this.#Client(
|
||||||
const slot = this.#slots[slotNumber];
|
this.#clientOptionsDefaults({
|
||||||
yield slot.master.client;
|
socket: this.#getNodeAddress(node.address) ?? {
|
||||||
|
host: node.host,
|
||||||
for (const replica of slot.replicas) {
|
port: node.port
|
||||||
yield replica.client;
|
},
|
||||||
}
|
readonly
|
||||||
}
|
})
|
||||||
|
|
||||||
#getSlotClient(slotNumber: number): RedisClientType<M, F, S> {
|
|
||||||
const slot = this.#slots[slotNumber];
|
|
||||||
if (!slot.clientIterator) {
|
|
||||||
slot.clientIterator = this.#slotClientIterator(slotNumber);
|
|
||||||
}
|
|
||||||
|
|
||||||
const {done, value} = slot.clientIterator.next();
|
|
||||||
if (done) {
|
|
||||||
slot.clientIterator = undefined;
|
|
||||||
return this.#getSlotClient(slotNumber);
|
|
||||||
}
|
|
||||||
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
#randomClientIterator?: IterableIterator<ClusterNode<M, F, S>>;
|
|
||||||
|
|
||||||
#getRandomClient(): RedisClientType<M, F, S> {
|
|
||||||
if (!this.#nodeByAddress.size) {
|
|
||||||
throw new Error('Cluster is not connected');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.#randomClientIterator) {
|
|
||||||
this.#randomClientIterator = this.#nodeByAddress.values();
|
|
||||||
}
|
|
||||||
|
|
||||||
const {done, value} = this.#randomClientIterator.next();
|
|
||||||
if (done) {
|
|
||||||
this.#randomClientIterator = undefined;
|
|
||||||
return this.#getRandomClient();
|
|
||||||
}
|
|
||||||
|
|
||||||
return value.client;
|
|
||||||
}
|
|
||||||
|
|
||||||
getClient(firstKey?: RedisCommandArgument, isReadonly?: boolean): RedisClientType<M, F, S> {
|
|
||||||
if (!firstKey) {
|
|
||||||
return this.#getRandomClient();
|
|
||||||
}
|
|
||||||
|
|
||||||
const slot = calculateSlot(firstKey);
|
|
||||||
if (!isReadonly || !this.#options.useReplicas) {
|
|
||||||
return this.getSlotMaster(slot).client;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.#getSlotClient(slot);
|
|
||||||
}
|
|
||||||
|
|
||||||
getMasters(): Array<ClusterNode<M, F, S>> {
|
|
||||||
const masters = [];
|
|
||||||
for (const node of this.#nodeByAddress.values()) {
|
|
||||||
if (node.client.options?.readonly) continue;
|
|
||||||
|
|
||||||
masters.push(node);
|
|
||||||
}
|
|
||||||
|
|
||||||
return masters;
|
|
||||||
}
|
|
||||||
|
|
||||||
getNodeByAddress(address: string): ClusterNode<M, F, S> | undefined {
|
|
||||||
const mappedAddress = this.#getNodeAddress(address);
|
|
||||||
return this.#nodeByAddress.get(
|
|
||||||
mappedAddress ? `${mappedAddress.host}:${mappedAddress.port}` : address
|
|
||||||
);
|
);
|
||||||
|
client.on('error', err => this.#emit('error', err));
|
||||||
|
|
||||||
|
await client.connect();
|
||||||
|
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
|
||||||
|
#createNodeClient(node: ShardNode<M, F, S>) {
|
||||||
|
const promise = this.#createClient(node)
|
||||||
|
.then(client => {
|
||||||
|
node.client = client;
|
||||||
|
return client;
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
node.client = undefined;
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
node.client = promise;
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
nodeClient(node: ShardNode<M, F, S>) {
|
||||||
|
return node.client ?? this.#createNodeClient(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
#runningRediscoverPromise?: Promise<void>;
|
||||||
|
|
||||||
|
async rediscover(startWith: RedisClientType<M, F, S>): Promise<void> {
|
||||||
|
this.#runningRediscoverPromise ??= this.#rediscover(startWith)
|
||||||
|
.finally(() => this.#runningRediscoverPromise = undefined);
|
||||||
|
return this.#runningRediscoverPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
async #rediscover(startWith: RedisClientType<M, F, S>): Promise<void> {
|
||||||
|
if (await this.#discover(startWith.options)) return;
|
||||||
|
|
||||||
|
return this.#discoverWithRootNodes();
|
||||||
}
|
}
|
||||||
|
|
||||||
quit(): Promise<void> {
|
quit(): Promise<void> {
|
||||||
@@ -280,14 +389,233 @@ export default class RedisClusterSlots<
|
|||||||
}
|
}
|
||||||
|
|
||||||
async #destroy(fn: (client: RedisClientType<M, F, S>) => Promise<unknown>): Promise<void> {
|
async #destroy(fn: (client: RedisClientType<M, F, S>) => Promise<unknown>): Promise<void> {
|
||||||
|
this.#isOpen = false;
|
||||||
|
|
||||||
const promises = [];
|
const promises = [];
|
||||||
for (const { client } of this.#nodeByAddress.values()) {
|
for (const { master, replicas } of this.shards) {
|
||||||
promises.push(fn(client));
|
if (master.client) {
|
||||||
|
promises.push(
|
||||||
|
this.#execOnNodeClient(master.client, fn)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (master.pubSubClient) {
|
||||||
|
promises.push(
|
||||||
|
this.#execOnNodeClient(master.pubSubClient, fn)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (replicas) {
|
||||||
|
for (const { client } of replicas) {
|
||||||
|
if (client) {
|
||||||
|
promises.push(
|
||||||
|
this.#execOnNodeClient(client, fn)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await Promise.all(promises);
|
if (this.pubSubNode) {
|
||||||
|
promises.push(this.#execOnNodeClient(this.pubSubNode.client, fn));
|
||||||
|
this.pubSubNode = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
this.#nodeByAddress.clear();
|
this.#resetSlots();
|
||||||
this.#slots.splice(0);
|
this.nodeByAddress.clear();
|
||||||
|
|
||||||
|
await Promise.allSettled(promises);
|
||||||
|
}
|
||||||
|
|
||||||
|
#execOnNodeClient(
|
||||||
|
client: ClientOrPromise<M, F, S>,
|
||||||
|
fn: (client: RedisClientType<M, F, S>) => Promise<unknown>
|
||||||
|
) {
|
||||||
|
return types.isPromise(client) ?
|
||||||
|
client.then(fn) :
|
||||||
|
fn(client);
|
||||||
|
}
|
||||||
|
|
||||||
|
getClient(
|
||||||
|
firstKey: RedisCommandArgument | undefined,
|
||||||
|
isReadonly: boolean | undefined
|
||||||
|
): ClientOrPromise<M, F, S> {
|
||||||
|
if (!firstKey) {
|
||||||
|
return this.nodeClient(this.getRandomNode());
|
||||||
|
}
|
||||||
|
|
||||||
|
const slotNumber = calculateSlot(firstKey);
|
||||||
|
if (!isReadonly) {
|
||||||
|
return this.nodeClient(this.slots[slotNumber].master);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.nodeClient(this.getSlotRandomNode(slotNumber));
|
||||||
|
}
|
||||||
|
|
||||||
|
*#iterateAllNodes() {
|
||||||
|
let i = Math.floor(Math.random() * (this.masters.length + this.replicas.length));
|
||||||
|
if (i < this.masters.length) {
|
||||||
|
do {
|
||||||
|
yield this.masters[i];
|
||||||
|
} while (++i < this.masters.length);
|
||||||
|
|
||||||
|
for (const replica of this.replicas) {
|
||||||
|
yield replica;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
i -= this.masters.length;
|
||||||
|
do {
|
||||||
|
yield this.replicas[i];
|
||||||
|
} while (++i < this.replicas.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
for (const master of this.masters) {
|
||||||
|
yield master;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const replica of this.replicas) {
|
||||||
|
yield replica;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#randomNodeIterator?: IterableIterator<ShardNode<M, F, S>>;
|
||||||
|
|
||||||
|
getRandomNode() {
|
||||||
|
this.#randomNodeIterator ??= this.#iterateAllNodes();
|
||||||
|
return this.#randomNodeIterator.next().value as ShardNode<M, F, S>;
|
||||||
|
}
|
||||||
|
|
||||||
|
*#slotNodesIterator(slot: ShardWithReplicas<M, F, S>) {
|
||||||
|
let i = Math.floor(Math.random() * (1 + slot.replicas.length));
|
||||||
|
if (i < slot.replicas.length) {
|
||||||
|
do {
|
||||||
|
yield slot.replicas[i];
|
||||||
|
} while (++i < slot.replicas.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
yield slot.master;
|
||||||
|
|
||||||
|
for (const replica of slot.replicas) {
|
||||||
|
yield replica;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getSlotRandomNode(slotNumber: number) {
|
||||||
|
const slot = this.slots[slotNumber];
|
||||||
|
if (!slot.replicas?.length) {
|
||||||
|
return slot.master;
|
||||||
|
}
|
||||||
|
|
||||||
|
slot.nodesIterator ??= this.#slotNodesIterator(slot as ShardWithReplicas<M, F, S>);
|
||||||
|
return slot.nodesIterator.next().value as ShardNode<M, F, S>;
|
||||||
|
}
|
||||||
|
|
||||||
|
getMasterByAddress(address: string) {
|
||||||
|
const master = this.nodeByAddress.get(address);
|
||||||
|
if (!master) return;
|
||||||
|
|
||||||
|
return this.nodeClient(master);
|
||||||
|
}
|
||||||
|
|
||||||
|
getPubSubClient() {
|
||||||
|
return this.pubSubNode ?
|
||||||
|
this.pubSubNode.client :
|
||||||
|
this.#initiatePubSubClient();
|
||||||
|
}
|
||||||
|
|
||||||
|
async #initiatePubSubClient(toResubscribe?: PubSubToResubscribe) {
|
||||||
|
const index = Math.floor(Math.random() * (this.masters.length + this.replicas.length)),
|
||||||
|
node = index < this.masters.length ?
|
||||||
|
this.masters[index] :
|
||||||
|
this.replicas[index - this.masters.length];
|
||||||
|
|
||||||
|
this.pubSubNode = {
|
||||||
|
address: node.address,
|
||||||
|
client: this.#createClient(node, true)
|
||||||
|
.then(async client => {
|
||||||
|
if (toResubscribe) {
|
||||||
|
await Promise.all([
|
||||||
|
client.extendPubSubListeners(PubSubType.CHANNELS, toResubscribe[PubSubType.CHANNELS]),
|
||||||
|
client.extendPubSubListeners(PubSubType.PATTERNS, toResubscribe[PubSubType.PATTERNS])
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.pubSubNode!.client = client;
|
||||||
|
return client;
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
this.pubSubNode = undefined;
|
||||||
|
throw err;
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.pubSubNode.client as Promise<RedisClientType<M, F, S>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
async executeUnsubscribeCommand(
|
||||||
|
unsubscribe: (client: RedisClientType<M, F, S>) => Promise<void>
|
||||||
|
): Promise<void> {
|
||||||
|
const client = await this.getPubSubClient();
|
||||||
|
await unsubscribe(client);
|
||||||
|
|
||||||
|
if (!client.isPubSubActive && client.isOpen) {
|
||||||
|
await client.disconnect();
|
||||||
|
this.pubSubNode = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getShardedPubSubClient(channel: string) {
|
||||||
|
const { master } = this.slots[calculateSlot(channel)];
|
||||||
|
return master.pubSubClient ?? this.#initiateShardedPubSubClient(master);
|
||||||
|
}
|
||||||
|
|
||||||
|
#initiateShardedPubSubClient(master: MasterNode<M, F, S>) {
|
||||||
|
const promise = this.#createClient(master, true)
|
||||||
|
.then(client => {
|
||||||
|
client.on('server-sunsubscribe', async (channel, listeners) => {
|
||||||
|
try {
|
||||||
|
await this.rediscover(client);
|
||||||
|
const redirectTo = await this.getShardedPubSubClient(channel);
|
||||||
|
redirectTo.extendPubSubChannelListeners(
|
||||||
|
PubSubType.SHARDED,
|
||||||
|
channel,
|
||||||
|
listeners
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
this.#emit('sharded-shannel-moved-error', err, channel, listeners);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
master.pubSubClient = client;
|
||||||
|
return client;
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
master.pubSubClient = undefined;
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
|
||||||
|
master.pubSubClient = promise;
|
||||||
|
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
async executeShardedUnsubscribeCommand(
|
||||||
|
channel: string,
|
||||||
|
unsubscribe: (client: RedisClientType<M, F, S>) => Promise<void>
|
||||||
|
): Promise<void> {
|
||||||
|
const { master } = this.slots[calculateSlot(channel)];
|
||||||
|
if (!master.pubSubClient) return Promise.resolve();
|
||||||
|
|
||||||
|
const client = await master.pubSubClient;
|
||||||
|
await unsubscribe(client);
|
||||||
|
|
||||||
|
if (!client.isPubSubActive && client.isOpen) {
|
||||||
|
await client.disconnect();
|
||||||
|
master.pubSubClient = undefined;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -53,6 +53,9 @@ import * as GETRANGE from '../commands/GETRANGE';
|
|||||||
import * as GETSET from '../commands/GETSET';
|
import * as GETSET from '../commands/GETSET';
|
||||||
import * as HDEL from '../commands/HDEL';
|
import * as HDEL from '../commands/HDEL';
|
||||||
import * as HEXISTS from '../commands/HEXISTS';
|
import * as HEXISTS from '../commands/HEXISTS';
|
||||||
|
import * as HEXPIRE from '../commands/HEXPIRE';
|
||||||
|
import * as HEXPIREAT from '../commands/HEXPIREAT';
|
||||||
|
import * as HEXPIRETIME from '../commands/HEXPIRETIME';
|
||||||
import * as HGET from '../commands/HGET';
|
import * as HGET from '../commands/HGET';
|
||||||
import * as HGETALL from '../commands/HGETALL';
|
import * as HGETALL from '../commands/HGETALL';
|
||||||
import * as HINCRBY from '../commands/HINCRBY';
|
import * as HINCRBY from '../commands/HINCRBY';
|
||||||
@@ -60,13 +63,20 @@ import * as HINCRBYFLOAT from '../commands/HINCRBYFLOAT';
|
|||||||
import * as HKEYS from '../commands/HKEYS';
|
import * as HKEYS from '../commands/HKEYS';
|
||||||
import * as HLEN from '../commands/HLEN';
|
import * as HLEN from '../commands/HLEN';
|
||||||
import * as HMGET from '../commands/HMGET';
|
import * as HMGET from '../commands/HMGET';
|
||||||
|
import * as HPERSIST from '../commands/HPERSIST';
|
||||||
|
import * as HPEXPIRE from '../commands/HPEXPIRE';
|
||||||
|
import * as HPEXPIREAT from '../commands/HPEXPIREAT';
|
||||||
|
import * as HPEXPIRETIME from '../commands/HPEXPIRETIME';
|
||||||
|
import * as HPTTL from '../commands/HPTTL';
|
||||||
import * as HRANDFIELD_COUNT_WITHVALUES from '../commands/HRANDFIELD_COUNT_WITHVALUES';
|
import * as HRANDFIELD_COUNT_WITHVALUES from '../commands/HRANDFIELD_COUNT_WITHVALUES';
|
||||||
import * as HRANDFIELD_COUNT from '../commands/HRANDFIELD_COUNT';
|
import * as HRANDFIELD_COUNT from '../commands/HRANDFIELD_COUNT';
|
||||||
import * as HRANDFIELD from '../commands/HRANDFIELD';
|
import * as HRANDFIELD from '../commands/HRANDFIELD';
|
||||||
import * as HSCAN from '../commands/HSCAN';
|
import * as HSCAN from '../commands/HSCAN';
|
||||||
|
import * as HSCAN_NOVALUES from '../commands/HSCAN_NOVALUES';
|
||||||
import * as HSET from '../commands/HSET';
|
import * as HSET from '../commands/HSET';
|
||||||
import * as HSETNX from '../commands/HSETNX';
|
import * as HSETNX from '../commands/HSETNX';
|
||||||
import * as HSTRLEN from '../commands/HSTRLEN';
|
import * as HSTRLEN from '../commands/HSTRLEN';
|
||||||
|
import * as HTTL from '../commands/HTTL';
|
||||||
import * as HVALS from '../commands/HVALS';
|
import * as HVALS from '../commands/HVALS';
|
||||||
import * as INCR from '../commands/INCR';
|
import * as INCR from '../commands/INCR';
|
||||||
import * as INCRBY from '../commands/INCRBY';
|
import * as INCRBY from '../commands/INCRBY';
|
||||||
@@ -110,6 +120,7 @@ import * as PTTL from '../commands/PTTL';
|
|||||||
import * as PUBLISH from '../commands/PUBLISH';
|
import * as PUBLISH from '../commands/PUBLISH';
|
||||||
import * as RENAME from '../commands/RENAME';
|
import * as RENAME from '../commands/RENAME';
|
||||||
import * as RENAMENX from '../commands/RENAMENX';
|
import * as RENAMENX from '../commands/RENAMENX';
|
||||||
|
import * as RESTORE from '../commands/RESTORE';
|
||||||
import * as RPOP_COUNT from '../commands/RPOP_COUNT';
|
import * as RPOP_COUNT from '../commands/RPOP_COUNT';
|
||||||
import * as RPOP from '../commands/RPOP';
|
import * as RPOP from '../commands/RPOP';
|
||||||
import * as RPOPLPUSH from '../commands/RPOPLPUSH';
|
import * as RPOPLPUSH from '../commands/RPOPLPUSH';
|
||||||
@@ -135,6 +146,7 @@ import * as SORT_RO from '../commands/SORT_RO';
|
|||||||
import * as SORT_STORE from '../commands/SORT_STORE';
|
import * as SORT_STORE from '../commands/SORT_STORE';
|
||||||
import * as SORT from '../commands/SORT';
|
import * as SORT from '../commands/SORT';
|
||||||
import * as SPOP from '../commands/SPOP';
|
import * as SPOP from '../commands/SPOP';
|
||||||
|
import * as SPUBLISH from '../commands/SPUBLISH';
|
||||||
import * as SRANDMEMBER_COUNT from '../commands/SRANDMEMBER_COUNT';
|
import * as SRANDMEMBER_COUNT from '../commands/SRANDMEMBER_COUNT';
|
||||||
import * as SRANDMEMBER from '../commands/SRANDMEMBER';
|
import * as SRANDMEMBER from '../commands/SRANDMEMBER';
|
||||||
import * as SREM from '../commands/SREM';
|
import * as SREM from '../commands/SREM';
|
||||||
@@ -319,6 +331,12 @@ export default {
|
|||||||
hDel: HDEL,
|
hDel: HDEL,
|
||||||
HEXISTS,
|
HEXISTS,
|
||||||
hExists: HEXISTS,
|
hExists: HEXISTS,
|
||||||
|
HEXPIRE,
|
||||||
|
hExpire: HEXPIRE,
|
||||||
|
HEXPIREAT,
|
||||||
|
hExpireAt: HEXPIREAT,
|
||||||
|
HEXPIRETIME,
|
||||||
|
hExpireTime: HEXPIRETIME,
|
||||||
HGET,
|
HGET,
|
||||||
hGet: HGET,
|
hGet: HGET,
|
||||||
HGETALL,
|
HGETALL,
|
||||||
@@ -333,6 +351,16 @@ export default {
|
|||||||
hLen: HLEN,
|
hLen: HLEN,
|
||||||
HMGET,
|
HMGET,
|
||||||
hmGet: HMGET,
|
hmGet: HMGET,
|
||||||
|
HPERSIST,
|
||||||
|
hPersist: HPERSIST,
|
||||||
|
HPEXPIRE,
|
||||||
|
hpExpire: HPEXPIRE,
|
||||||
|
HPEXPIREAT,
|
||||||
|
hpExpireAt: HPEXPIREAT,
|
||||||
|
HPEXPIRETIME,
|
||||||
|
hpExpireTime: HPEXPIRETIME,
|
||||||
|
HPTTL,
|
||||||
|
hpTTL: HPTTL,
|
||||||
HRANDFIELD_COUNT_WITHVALUES,
|
HRANDFIELD_COUNT_WITHVALUES,
|
||||||
hRandFieldCountWithValues: HRANDFIELD_COUNT_WITHVALUES,
|
hRandFieldCountWithValues: HRANDFIELD_COUNT_WITHVALUES,
|
||||||
HRANDFIELD_COUNT,
|
HRANDFIELD_COUNT,
|
||||||
@@ -341,12 +369,16 @@ export default {
|
|||||||
hRandField: HRANDFIELD,
|
hRandField: HRANDFIELD,
|
||||||
HSCAN,
|
HSCAN,
|
||||||
hScan: HSCAN,
|
hScan: HSCAN,
|
||||||
|
HSCAN_NOVALUES,
|
||||||
|
hScanNoValues: HSCAN_NOVALUES,
|
||||||
HSET,
|
HSET,
|
||||||
hSet: HSET,
|
hSet: HSET,
|
||||||
HSETNX,
|
HSETNX,
|
||||||
hSetNX: HSETNX,
|
hSetNX: HSETNX,
|
||||||
HSTRLEN,
|
HSTRLEN,
|
||||||
hStrLen: HSTRLEN,
|
hStrLen: HSTRLEN,
|
||||||
|
HTTL,
|
||||||
|
hTTL: HTTL,
|
||||||
HVALS,
|
HVALS,
|
||||||
hVals: HVALS,
|
hVals: HVALS,
|
||||||
INCR,
|
INCR,
|
||||||
@@ -433,6 +465,8 @@ export default {
|
|||||||
rename: RENAME,
|
rename: RENAME,
|
||||||
RENAMENX,
|
RENAMENX,
|
||||||
renameNX: RENAMENX,
|
renameNX: RENAMENX,
|
||||||
|
RESTORE,
|
||||||
|
restore: RESTORE,
|
||||||
RPOP_COUNT,
|
RPOP_COUNT,
|
||||||
rPopCount: RPOP_COUNT,
|
rPopCount: RPOP_COUNT,
|
||||||
RPOP,
|
RPOP,
|
||||||
@@ -483,6 +517,8 @@ export default {
|
|||||||
sort: SORT,
|
sort: SORT,
|
||||||
SPOP,
|
SPOP,
|
||||||
sPop: SPOP,
|
sPop: SPOP,
|
||||||
|
SPUBLISH,
|
||||||
|
sPublish: SPUBLISH,
|
||||||
SRANDMEMBER_COUNT,
|
SRANDMEMBER_COUNT,
|
||||||
sRandMemberCount: SRANDMEMBER_COUNT,
|
sRandMemberCount: SRANDMEMBER_COUNT,
|
||||||
SRANDMEMBER,
|
SRANDMEMBER,
|
||||||
|
@@ -1,25 +1,30 @@
|
|||||||
import { strict as assert } from 'assert';
|
import { strict as assert } from 'assert';
|
||||||
import testUtils, { GLOBAL } from '../test-utils';
|
import testUtils, { GLOBAL, waitTillBeenCalled } from '../test-utils';
|
||||||
import RedisCluster from '.';
|
import RedisCluster from '.';
|
||||||
import { ClusterSlotStates } from '../commands/CLUSTER_SETSLOT';
|
import { ClusterSlotStates } from '../commands/CLUSTER_SETSLOT';
|
||||||
|
import { commandOptions } from '../command-options';
|
||||||
import { SQUARE_SCRIPT } from '../client/index.spec';
|
import { SQUARE_SCRIPT } from '../client/index.spec';
|
||||||
import { RootNodesUnavailableError } from '../errors';
|
import { RootNodesUnavailableError } from '../errors';
|
||||||
|
import { spy } from 'sinon';
|
||||||
// We need to use 'require', because it's not possible with Typescript to import
|
import { promiseTimeout } from '../utils';
|
||||||
// function that are exported as 'module.exports = function`, without esModuleInterop
|
import RedisClient from '../client';
|
||||||
// set to true.
|
|
||||||
const calculateSlot = require('cluster-key-slot');
|
|
||||||
|
|
||||||
describe('Cluster', () => {
|
describe('Cluster', () => {
|
||||||
testUtils.testWithCluster('sendCommand', async cluster => {
|
testUtils.testWithCluster('sendCommand', async cluster => {
|
||||||
await cluster.publish('channel', 'message');
|
assert.equal(
|
||||||
await cluster.set('a', 'b');
|
await cluster.sendCommand(undefined, true, ['PING']),
|
||||||
await cluster.set('a{a}', 'bb');
|
'PONG'
|
||||||
await cluster.set('aa', 'bb');
|
);
|
||||||
await cluster.get('aa');
|
}, GLOBAL.CLUSTERS.OPEN);
|
||||||
await cluster.get('aa');
|
|
||||||
await cluster.get('aa');
|
testUtils.testWithCluster('isOpen', async cluster => {
|
||||||
await cluster.get('aa');
|
assert.equal(cluster.isOpen, true);
|
||||||
|
await cluster.disconnect();
|
||||||
|
assert.equal(cluster.isOpen, false);
|
||||||
|
}, GLOBAL.CLUSTERS.OPEN);
|
||||||
|
|
||||||
|
testUtils.testWithCluster('connect should throw if already connected', async cluster => {
|
||||||
|
await assert.rejects(cluster.connect());
|
||||||
}, GLOBAL.CLUSTERS.OPEN);
|
}, GLOBAL.CLUSTERS.OPEN);
|
||||||
|
|
||||||
testUtils.testWithCluster('multi', async cluster => {
|
testUtils.testWithCluster('multi', async cluster => {
|
||||||
@@ -64,54 +69,321 @@ describe('Cluster', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
testUtils.testWithCluster('should handle live resharding', async cluster => {
|
testUtils.testWithCluster('should handle live resharding', async cluster => {
|
||||||
const key = 'key',
|
const slot = 12539,
|
||||||
|
key = 'key',
|
||||||
value = 'value';
|
value = 'value';
|
||||||
await cluster.set(key, value);
|
await cluster.set(key, value);
|
||||||
|
|
||||||
const slot = calculateSlot(key),
|
const importing = cluster.slots[0].master,
|
||||||
source = cluster.getSlotMaster(slot),
|
migrating = cluster.slots[slot].master,
|
||||||
destination = cluster.getMasters().find(node => node.id !== source.id)!;
|
[ importingClient, migratingClient ] = await Promise.all([
|
||||||
|
cluster.nodeClient(importing),
|
||||||
|
cluster.nodeClient(migrating)
|
||||||
|
]);
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
source.client.clusterSetSlot(slot, ClusterSlotStates.MIGRATING, destination.id),
|
importingClient.clusterSetSlot(slot, ClusterSlotStates.IMPORTING, migrating.id),
|
||||||
destination.client.clusterSetSlot(slot, ClusterSlotStates.IMPORTING, destination.id)
|
migratingClient.clusterSetSlot(slot, ClusterSlotStates.MIGRATING, importing.id)
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// should be able to get the key from the source node using "ASKING"
|
// should be able to get the key from the migrating node
|
||||||
|
assert.equal(
|
||||||
|
await cluster.get(key),
|
||||||
|
value
|
||||||
|
);
|
||||||
|
|
||||||
|
await migratingClient.migrate(
|
||||||
|
importing.host,
|
||||||
|
importing.port,
|
||||||
|
key,
|
||||||
|
0,
|
||||||
|
10
|
||||||
|
);
|
||||||
|
|
||||||
|
// should be able to get the key from the importing node using `ASKING`
|
||||||
assert.equal(
|
assert.equal(
|
||||||
await cluster.get(key),
|
await cluster.get(key),
|
||||||
value
|
value
|
||||||
);
|
);
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
source.client.migrate(
|
importingClient.clusterSetSlot(slot, ClusterSlotStates.NODE, importing.id),
|
||||||
'127.0.0.1',
|
migratingClient.clusterSetSlot(slot, ClusterSlotStates.NODE, importing.id),
|
||||||
(<any>destination.client.options).socket.port,
|
|
||||||
key,
|
|
||||||
0,
|
|
||||||
10
|
|
||||||
)
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// should be able to get the key from the destination node using the "ASKING" command
|
// should handle `MOVED` errors
|
||||||
assert.equal(
|
|
||||||
await cluster.get(key),
|
|
||||||
value
|
|
||||||
);
|
|
||||||
|
|
||||||
await Promise.all(
|
|
||||||
cluster.getMasters().map(({ client }) => {
|
|
||||||
return client.clusterSetSlot(slot, ClusterSlotStates.NODE, destination.id);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// should handle "MOVED" errors
|
|
||||||
assert.equal(
|
assert.equal(
|
||||||
await cluster.get(key),
|
await cluster.get(key),
|
||||||
value
|
value
|
||||||
);
|
);
|
||||||
}, {
|
}, {
|
||||||
serverArguments: [],
|
serverArguments: [],
|
||||||
numberOfNodes: 2
|
numberOfMasters: 2
|
||||||
|
});
|
||||||
|
|
||||||
|
testUtils.testWithCluster('getRandomNode should spread the the load evenly', async cluster => {
|
||||||
|
const totalNodes = cluster.masters.length + cluster.replicas.length,
|
||||||
|
ids = new Set<string>();
|
||||||
|
for (let i = 0; i < totalNodes; i++) {
|
||||||
|
ids.add(cluster.getRandomNode().id);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.equal(ids.size, totalNodes);
|
||||||
|
}, GLOBAL.CLUSTERS.WITH_REPLICAS);
|
||||||
|
|
||||||
|
testUtils.testWithCluster('getSlotRandomNode should spread the the load evenly', async cluster => {
|
||||||
|
const totalNodes = 1 + cluster.slots[0].replicas!.length,
|
||||||
|
ids = new Set<string>();
|
||||||
|
for (let i = 0; i < totalNodes; i++) {
|
||||||
|
ids.add(cluster.getSlotRandomNode(0).id);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.equal(ids.size, totalNodes);
|
||||||
|
}, GLOBAL.CLUSTERS.WITH_REPLICAS);
|
||||||
|
|
||||||
|
testUtils.testWithCluster('cluster topology', async cluster => {
|
||||||
|
assert.equal(cluster.slots.length, 16384);
|
||||||
|
const { numberOfMasters, numberOfReplicas } = GLOBAL.CLUSTERS.WITH_REPLICAS;
|
||||||
|
assert.equal(cluster.shards.length, numberOfMasters);
|
||||||
|
assert.equal(cluster.masters.length, numberOfMasters);
|
||||||
|
assert.equal(cluster.replicas.length, numberOfReplicas * numberOfMasters);
|
||||||
|
assert.equal(cluster.nodeByAddress.size, numberOfMasters + numberOfMasters * numberOfReplicas);
|
||||||
|
}, GLOBAL.CLUSTERS.WITH_REPLICAS);
|
||||||
|
|
||||||
|
testUtils.testWithCluster('getMasters should be backwards competiable (without `minimizeConnections`)', async cluster => {
|
||||||
|
const masters = cluster.getMasters();
|
||||||
|
assert.ok(Array.isArray(masters));
|
||||||
|
for (const master of masters) {
|
||||||
|
assert.equal(typeof master.id, 'string');
|
||||||
|
assert.ok(master.client instanceof RedisClient);
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
...GLOBAL.CLUSTERS.OPEN,
|
||||||
|
clusterConfiguration: {
|
||||||
|
minimizeConnections: undefined // reset to default
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
testUtils.testWithCluster('getSlotMaster should be backwards competiable (without `minimizeConnections`)', async cluster => {
|
||||||
|
const master = cluster.getSlotMaster(0);
|
||||||
|
assert.equal(typeof master.id, 'string');
|
||||||
|
assert.ok(master.client instanceof RedisClient);
|
||||||
|
}, {
|
||||||
|
...GLOBAL.CLUSTERS.OPEN,
|
||||||
|
clusterConfiguration: {
|
||||||
|
minimizeConnections: undefined // reset to default
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
testUtils.testWithCluster('should throw CROSSSLOT error', async cluster => {
|
||||||
|
await assert.rejects(cluster.mGet(['a', 'b']));
|
||||||
|
}, GLOBAL.CLUSTERS.OPEN);
|
||||||
|
|
||||||
|
testUtils.testWithCluster('should send commands with commandOptions to correct cluster slot (without redirections)', async cluster => {
|
||||||
|
// 'a' and 'b' hash to different cluster slots (see previous unit test)
|
||||||
|
// -> maxCommandRedirections 0: rejects on MOVED/ASK reply
|
||||||
|
await cluster.set(commandOptions({ isolated: true }), 'a', '1'),
|
||||||
|
await cluster.set(commandOptions({ isolated: true }), 'b', '2'),
|
||||||
|
|
||||||
|
assert.equal(await cluster.get('a'), '1');
|
||||||
|
assert.equal(await cluster.get('b'), '2');
|
||||||
|
}, {
|
||||||
|
...GLOBAL.CLUSTERS.OPEN,
|
||||||
|
clusterConfiguration: {
|
||||||
|
maxCommandRedirections: 0
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('minimizeConnections', () => {
|
||||||
|
testUtils.testWithCluster('false', async cluster => {
|
||||||
|
for (const master of cluster.masters) {
|
||||||
|
assert.ok(master.client instanceof RedisClient);
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
...GLOBAL.CLUSTERS.OPEN,
|
||||||
|
clusterConfiguration: {
|
||||||
|
minimizeConnections: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
testUtils.testWithCluster('true', async cluster => {
|
||||||
|
for (const master of cluster.masters) {
|
||||||
|
assert.equal(master.client, undefined);
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
...GLOBAL.CLUSTERS.OPEN,
|
||||||
|
clusterConfiguration: {
|
||||||
|
minimizeConnections: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('PubSub', () => {
|
||||||
|
testUtils.testWithCluster('subscribe & unsubscribe', async cluster => {
|
||||||
|
const listener = spy();
|
||||||
|
|
||||||
|
await cluster.subscribe('channel', listener);
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
waitTillBeenCalled(listener),
|
||||||
|
cluster.publish('channel', 'message')
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert.ok(listener.calledOnceWithExactly('message', 'channel'));
|
||||||
|
|
||||||
|
await cluster.unsubscribe('channel', listener);
|
||||||
|
|
||||||
|
assert.equal(cluster.pubSubNode, undefined);
|
||||||
|
}, GLOBAL.CLUSTERS.OPEN);
|
||||||
|
|
||||||
|
testUtils.testWithCluster('concurrent UNSUBSCRIBE does not throw an error (#2685)', async cluster => {
|
||||||
|
const listener = spy();
|
||||||
|
await Promise.all([
|
||||||
|
cluster.subscribe('1', listener),
|
||||||
|
cluster.subscribe('2', listener)
|
||||||
|
]);
|
||||||
|
await Promise.all([
|
||||||
|
cluster.unsubscribe('1', listener),
|
||||||
|
cluster.unsubscribe('2', listener)
|
||||||
|
]);
|
||||||
|
}, GLOBAL.CLUSTERS.OPEN);
|
||||||
|
|
||||||
|
testUtils.testWithCluster('psubscribe & punsubscribe', async cluster => {
|
||||||
|
const listener = spy();
|
||||||
|
|
||||||
|
await cluster.pSubscribe('channe*', listener);
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
waitTillBeenCalled(listener),
|
||||||
|
cluster.publish('channel', 'message')
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert.ok(listener.calledOnceWithExactly('message', 'channel'));
|
||||||
|
|
||||||
|
await cluster.pUnsubscribe('channe*', listener);
|
||||||
|
|
||||||
|
assert.equal(cluster.pubSubNode, undefined);
|
||||||
|
}, GLOBAL.CLUSTERS.OPEN);
|
||||||
|
|
||||||
|
testUtils.testWithCluster('should move listeners when PubSub node disconnects from the cluster', async cluster => {
|
||||||
|
const listener = spy();
|
||||||
|
await cluster.subscribe('channel', listener);
|
||||||
|
|
||||||
|
assert.ok(cluster.pubSubNode);
|
||||||
|
const [ migrating, importing ] = cluster.masters[0].address === cluster.pubSubNode.address ?
|
||||||
|
cluster.masters :
|
||||||
|
[cluster.masters[1], cluster.masters[0]],
|
||||||
|
[ migratingClient, importingClient ] = await Promise.all([
|
||||||
|
cluster.nodeClient(migrating),
|
||||||
|
cluster.nodeClient(importing)
|
||||||
|
]);
|
||||||
|
|
||||||
|
const range = cluster.slots[0].master === migrating ? {
|
||||||
|
key: 'bar', // 5061
|
||||||
|
start: 0,
|
||||||
|
end: 8191
|
||||||
|
} : {
|
||||||
|
key: 'foo', // 12182
|
||||||
|
start: 8192,
|
||||||
|
end: 16383
|
||||||
|
};
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
migratingClient.clusterDelSlotsRange(range),
|
||||||
|
importingClient.clusterDelSlotsRange(range),
|
||||||
|
importingClient.clusterAddSlotsRange(range)
|
||||||
|
]);
|
||||||
|
|
||||||
|
// wait for migrating node to be notified about the new topology
|
||||||
|
while ((await migratingClient.clusterInfo()).state !== 'ok') {
|
||||||
|
await promiseTimeout(50);
|
||||||
|
}
|
||||||
|
|
||||||
|
// make sure to cause `MOVED` error
|
||||||
|
await cluster.get(range.key);
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
cluster.publish('channel', 'message'),
|
||||||
|
waitTillBeenCalled(listener)
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert.ok(listener.calledOnceWithExactly('message', 'channel'));
|
||||||
|
}, {
|
||||||
|
serverArguments: [],
|
||||||
|
numberOfMasters: 2,
|
||||||
|
minimumDockerVersion: [7]
|
||||||
|
});
|
||||||
|
|
||||||
|
testUtils.testWithCluster('ssubscribe & sunsubscribe', async cluster => {
|
||||||
|
const listener = spy();
|
||||||
|
|
||||||
|
await cluster.sSubscribe('channel', listener);
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
waitTillBeenCalled(listener),
|
||||||
|
cluster.sPublish('channel', 'message')
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert.ok(listener.calledOnceWithExactly('message', 'channel'));
|
||||||
|
|
||||||
|
await cluster.sUnsubscribe('channel', listener);
|
||||||
|
|
||||||
|
// 10328 is the slot of `channel`
|
||||||
|
assert.equal(cluster.slots[10328].master.pubSubClient, undefined);
|
||||||
|
}, {
|
||||||
|
...GLOBAL.CLUSTERS.OPEN,
|
||||||
|
minimumDockerVersion: [7]
|
||||||
|
});
|
||||||
|
|
||||||
|
testUtils.testWithCluster('concurrent SUNSUBCRIBE does not throw an error (#2685)', async cluster => {
|
||||||
|
const listener = spy();
|
||||||
|
await Promise.all([
|
||||||
|
await cluster.sSubscribe('1', listener),
|
||||||
|
await cluster.sSubscribe('2', listener)
|
||||||
|
]);
|
||||||
|
await Promise.all([
|
||||||
|
cluster.sUnsubscribe('1', listener),
|
||||||
|
cluster.sUnsubscribe('2', listener)
|
||||||
|
]);
|
||||||
|
}, {
|
||||||
|
...GLOBAL.CLUSTERS.OPEN,
|
||||||
|
minimumDockerVersion: [7]
|
||||||
|
});
|
||||||
|
|
||||||
|
testUtils.testWithCluster('should handle sharded-channel-moved events', async cluster => {
|
||||||
|
const SLOT = 10328,
|
||||||
|
migrating = cluster.slots[SLOT].master,
|
||||||
|
importing = cluster.masters.find(master => master !== migrating)!,
|
||||||
|
[ migratingClient, importingClient ] = await Promise.all([
|
||||||
|
cluster.nodeClient(migrating),
|
||||||
|
cluster.nodeClient(importing)
|
||||||
|
]);
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
migratingClient.clusterDelSlots(SLOT),
|
||||||
|
importingClient.clusterDelSlots(SLOT),
|
||||||
|
importingClient.clusterAddSlots(SLOT)
|
||||||
|
]);
|
||||||
|
|
||||||
|
// wait for migrating node to be notified about the new topology
|
||||||
|
while ((await migratingClient.clusterInfo()).state !== 'ok') {
|
||||||
|
await promiseTimeout(50);
|
||||||
|
}
|
||||||
|
|
||||||
|
const listener = spy();
|
||||||
|
|
||||||
|
// will trigger `MOVED` error
|
||||||
|
await cluster.sSubscribe('channel', listener);
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
waitTillBeenCalled(listener),
|
||||||
|
cluster.sPublish('channel', 'message')
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert.ok(listener.calledOnceWithExactly('message', 'channel'));
|
||||||
|
}, {
|
||||||
|
serverArguments: [],
|
||||||
|
minimumDockerVersion: [7]
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -1,11 +1,13 @@
|
|||||||
import COMMANDS from './commands';
|
import COMMANDS from './commands';
|
||||||
import { RedisCommand, RedisCommandArgument, RedisCommandArguments, RedisCommandRawReply, RedisCommandReply, RedisFunctions, RedisModules, RedisExtensions, RedisScript, RedisScripts, RedisCommandSignature, RedisFunction } from '../commands';
|
import { RedisCommand, RedisCommandArgument, RedisCommandArguments, RedisCommandRawReply, RedisCommandReply, RedisFunctions, RedisModules, RedisExtensions, RedisScript, RedisScripts, RedisCommandSignature, RedisFunction } from '../commands';
|
||||||
import { ClientCommandOptions, RedisClientOptions, RedisClientType, WithFunctions, WithModules, WithScripts } from '../client';
|
import { ClientCommandOptions, RedisClientOptions, RedisClientType, WithFunctions, WithModules, WithScripts } from '../client';
|
||||||
import RedisClusterSlots, { ClusterNode, NodeAddressMap } from './cluster-slots';
|
import RedisClusterSlots, { NodeAddressMap, ShardNode } from './cluster-slots';
|
||||||
import { attachExtensions, transformCommandReply, attachCommands, transformCommandArguments } from '../commander';
|
import { attachExtensions, transformCommandReply, attachCommands, transformCommandArguments } from '../commander';
|
||||||
import { EventEmitter } from 'events';
|
import { EventEmitter } from 'events';
|
||||||
import RedisClusterMultiCommand, { InstantiableRedisClusterMultiCommandType, RedisClusterMultiCommandType } from './multi-command';
|
import RedisClusterMultiCommand, { InstantiableRedisClusterMultiCommandType, RedisClusterMultiCommandType } from './multi-command';
|
||||||
import { RedisMultiQueuedCommand } from '../multi-command';
|
import { RedisMultiQueuedCommand } from '../multi-command';
|
||||||
|
import { PubSubListener } from '../client/pub-sub';
|
||||||
|
import { ErrorReply } from '../errors';
|
||||||
|
|
||||||
export type RedisClusterClientOptions = Omit<
|
export type RedisClusterClientOptions = Omit<
|
||||||
RedisClientOptions,
|
RedisClientOptions,
|
||||||
@@ -17,10 +19,34 @@ export interface RedisClusterOptions<
|
|||||||
F extends RedisFunctions = Record<string, never>,
|
F extends RedisFunctions = Record<string, never>,
|
||||||
S extends RedisScripts = Record<string, never>
|
S extends RedisScripts = Record<string, never>
|
||||||
> extends RedisExtensions<M, F, S> {
|
> extends RedisExtensions<M, F, S> {
|
||||||
|
/**
|
||||||
|
* Should contain details for some of the cluster nodes that the client will use to discover
|
||||||
|
* the "cluster topology". We recommend including details for at least 3 nodes here.
|
||||||
|
*/
|
||||||
rootNodes: Array<RedisClusterClientOptions>;
|
rootNodes: Array<RedisClusterClientOptions>;
|
||||||
|
/**
|
||||||
|
* Default values used for every client in the cluster. Use this to specify global values,
|
||||||
|
* for example: ACL credentials, timeouts, TLS configuration etc.
|
||||||
|
*/
|
||||||
defaults?: Partial<RedisClusterClientOptions>;
|
defaults?: Partial<RedisClusterClientOptions>;
|
||||||
|
/**
|
||||||
|
* When `true`, `.connect()` will only discover the cluster topology, without actually connecting to all the nodes.
|
||||||
|
* Useful for short-term or PubSub-only connections.
|
||||||
|
*/
|
||||||
|
minimizeConnections?: boolean;
|
||||||
|
/**
|
||||||
|
* When `true`, distribute load by executing readonly commands (such as `GET`, `GEOSEARCH`, etc.) across all cluster nodes. When `false`, only use master nodes.
|
||||||
|
*/
|
||||||
useReplicas?: boolean;
|
useReplicas?: boolean;
|
||||||
|
/**
|
||||||
|
* The maximum number of times a command will be redirected due to `MOVED` or `ASK` errors.
|
||||||
|
*/
|
||||||
maxCommandRedirections?: number;
|
maxCommandRedirections?: number;
|
||||||
|
/**
|
||||||
|
* Mapping between the addresses in the cluster (see `CLUSTER SHARDS`) and the addresses the client should connect to
|
||||||
|
* Useful when the cluster is running on another network
|
||||||
|
*
|
||||||
|
*/
|
||||||
nodeAddressMap?: NodeAddressMap;
|
nodeAddressMap?: NodeAddressMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,14 +96,44 @@ export default class RedisCluster<
|
|||||||
}
|
}
|
||||||
|
|
||||||
readonly #options: RedisClusterOptions<M, F, S>;
|
readonly #options: RedisClusterOptions<M, F, S>;
|
||||||
|
|
||||||
readonly #slots: RedisClusterSlots<M, F, S>;
|
readonly #slots: RedisClusterSlots<M, F, S>;
|
||||||
|
|
||||||
|
get slots() {
|
||||||
|
return this.#slots.slots;
|
||||||
|
}
|
||||||
|
|
||||||
|
get shards() {
|
||||||
|
return this.#slots.shards;
|
||||||
|
}
|
||||||
|
|
||||||
|
get masters() {
|
||||||
|
return this.#slots.masters;
|
||||||
|
}
|
||||||
|
|
||||||
|
get replicas() {
|
||||||
|
return this.#slots.replicas;
|
||||||
|
}
|
||||||
|
|
||||||
|
get nodeByAddress() {
|
||||||
|
return this.#slots.nodeByAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
get pubSubNode() {
|
||||||
|
return this.#slots.pubSubNode;
|
||||||
|
}
|
||||||
|
|
||||||
readonly #Multi: InstantiableRedisClusterMultiCommandType<M, F, S>;
|
readonly #Multi: InstantiableRedisClusterMultiCommandType<M, F, S>;
|
||||||
|
|
||||||
|
get isOpen() {
|
||||||
|
return this.#slots.isOpen;
|
||||||
|
}
|
||||||
|
|
||||||
constructor(options: RedisClusterOptions<M, F, S>) {
|
constructor(options: RedisClusterOptions<M, F, S>) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this.#options = options;
|
this.#options = options;
|
||||||
this.#slots = new RedisClusterSlots(options, err => this.emit('error', err));
|
this.#slots = new RedisClusterSlots(options, this.emit.bind(this));
|
||||||
this.#Multi = RedisClusterMultiCommand.extend(options);
|
this.#Multi = RedisClusterMultiCommand.extend(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,7 +144,7 @@ export default class RedisCluster<
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async connect(): Promise<void> {
|
connect() {
|
||||||
return this.#slots.connect();
|
return this.#slots.connect();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,11 +152,11 @@ export default class RedisCluster<
|
|||||||
command: C,
|
command: C,
|
||||||
args: Array<unknown>
|
args: Array<unknown>
|
||||||
): Promise<RedisCommandReply<C>> {
|
): Promise<RedisCommandReply<C>> {
|
||||||
const { args: redisArgs, options } = transformCommandArguments(command, args);
|
const { jsArgs, args: redisArgs, options } = transformCommandArguments(command, args);
|
||||||
return transformCommandReply(
|
return transformCommandReply(
|
||||||
command,
|
command,
|
||||||
await this.sendCommand(
|
await this.sendCommand(
|
||||||
RedisCluster.extractFirstKey(command, args, redisArgs),
|
RedisCluster.extractFirstKey(command, jsArgs, redisArgs),
|
||||||
command.IS_READ_ONLY,
|
command.IS_READ_ONLY,
|
||||||
redisArgs,
|
redisArgs,
|
||||||
options
|
options
|
||||||
@@ -188,34 +244,33 @@ export default class RedisCluster<
|
|||||||
executor: (client: RedisClientType<M, F, S>) => Promise<Reply>
|
executor: (client: RedisClientType<M, F, S>) => Promise<Reply>
|
||||||
): Promise<Reply> {
|
): Promise<Reply> {
|
||||||
const maxCommandRedirections = this.#options.maxCommandRedirections ?? 16;
|
const maxCommandRedirections = this.#options.maxCommandRedirections ?? 16;
|
||||||
let client = this.#slots.getClient(firstKey, isReadonly);
|
let client = await this.#slots.getClient(firstKey, isReadonly);
|
||||||
for (let i = 0;; i++) {
|
for (let i = 0;; i++) {
|
||||||
try {
|
try {
|
||||||
return await executor(client);
|
return await executor(client);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (++i > maxCommandRedirections || !(err instanceof Error)) {
|
if (++i > maxCommandRedirections || !(err instanceof ErrorReply)) {
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (err.message.startsWith('ASK')) {
|
if (err.message.startsWith('ASK')) {
|
||||||
const address = err.message.substring(err.message.lastIndexOf(' ') + 1);
|
const address = err.message.substring(err.message.lastIndexOf(' ') + 1);
|
||||||
if (this.#slots.getNodeByAddress(address)?.client === client) {
|
let redirectTo = await this.#slots.getMasterByAddress(address);
|
||||||
await client.asking();
|
if (!redirectTo) {
|
||||||
continue;
|
await this.#slots.rediscover(client);
|
||||||
|
redirectTo = await this.#slots.getMasterByAddress(address);
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.#slots.rediscover(client);
|
|
||||||
const redirectTo = this.#slots.getNodeByAddress(address);
|
|
||||||
if (!redirectTo) {
|
if (!redirectTo) {
|
||||||
throw new Error(`Cannot find node ${address}`);
|
throw new Error(`Cannot find node ${address}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
await redirectTo.client.asking();
|
await redirectTo.asking();
|
||||||
client = redirectTo.client;
|
client = redirectTo;
|
||||||
continue;
|
continue;
|
||||||
} else if (err.message.startsWith('MOVED')) {
|
} else if (err.message.startsWith('MOVED')) {
|
||||||
await this.#slots.rediscover(client);
|
await this.#slots.rediscover(client);
|
||||||
client = this.#slots.getClient(firstKey, isReadonly);
|
client = await this.#slots.getClient(firstKey, isReadonly);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -239,14 +294,94 @@ export default class RedisCluster<
|
|||||||
|
|
||||||
multi = this.MULTI;
|
multi = this.MULTI;
|
||||||
|
|
||||||
getMasters(): Array<ClusterNode<M, F, S>> {
|
async SUBSCRIBE<T extends boolean = false>(
|
||||||
return this.#slots.getMasters();
|
channels: string | Array<string>,
|
||||||
|
listener: PubSubListener<T>,
|
||||||
|
bufferMode?: T
|
||||||
|
) {
|
||||||
|
return (await this.#slots.getPubSubClient())
|
||||||
|
.SUBSCRIBE(channels, listener, bufferMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
getSlotMaster(slot: number): ClusterNode<M, F, S> {
|
subscribe = this.SUBSCRIBE;
|
||||||
return this.#slots.getSlotMaster(slot);
|
|
||||||
|
async UNSUBSCRIBE<T extends boolean = false>(
|
||||||
|
channels?: string | Array<string>,
|
||||||
|
listener?: PubSubListener<boolean>,
|
||||||
|
bufferMode?: T
|
||||||
|
) {
|
||||||
|
return this.#slots.executeUnsubscribeCommand(client =>
|
||||||
|
client.UNSUBSCRIBE(channels, listener, bufferMode)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
unsubscribe = this.UNSUBSCRIBE;
|
||||||
|
|
||||||
|
async PSUBSCRIBE<T extends boolean = false>(
|
||||||
|
patterns: string | Array<string>,
|
||||||
|
listener: PubSubListener<T>,
|
||||||
|
bufferMode?: T
|
||||||
|
) {
|
||||||
|
return (await this.#slots.getPubSubClient())
|
||||||
|
.PSUBSCRIBE(patterns, listener, bufferMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
pSubscribe = this.PSUBSCRIBE;
|
||||||
|
|
||||||
|
async PUNSUBSCRIBE<T extends boolean = false>(
|
||||||
|
patterns?: string | Array<string>,
|
||||||
|
listener?: PubSubListener<T>,
|
||||||
|
bufferMode?: T
|
||||||
|
) {
|
||||||
|
return this.#slots.executeUnsubscribeCommand(client =>
|
||||||
|
client.PUNSUBSCRIBE(patterns, listener, bufferMode)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pUnsubscribe = this.PUNSUBSCRIBE;
|
||||||
|
|
||||||
|
async SSUBSCRIBE<T extends boolean = false>(
|
||||||
|
channels: string | Array<string>,
|
||||||
|
listener: PubSubListener<T>,
|
||||||
|
bufferMode?: T
|
||||||
|
) {
|
||||||
|
const maxCommandRedirections = this.#options.maxCommandRedirections ?? 16,
|
||||||
|
firstChannel = Array.isArray(channels) ? channels[0] : channels;
|
||||||
|
let client = await this.#slots.getShardedPubSubClient(firstChannel);
|
||||||
|
for (let i = 0;; i++) {
|
||||||
|
try {
|
||||||
|
return await client.SSUBSCRIBE(channels, listener, bufferMode);
|
||||||
|
} catch (err) {
|
||||||
|
if (++i > maxCommandRedirections || !(err instanceof ErrorReply)) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (err.message.startsWith('MOVED')) {
|
||||||
|
await this.#slots.rediscover(client);
|
||||||
|
client = await this.#slots.getShardedPubSubClient(firstChannel);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sSubscribe = this.SSUBSCRIBE;
|
||||||
|
|
||||||
|
SUNSUBSCRIBE<T extends boolean = false>(
|
||||||
|
channels: string | Array<string>,
|
||||||
|
listener?: PubSubListener<T>,
|
||||||
|
bufferMode?: T
|
||||||
|
) {
|
||||||
|
return this.#slots.executeShardedUnsubscribeCommand(
|
||||||
|
Array.isArray(channels) ? channels[0] : channels,
|
||||||
|
client => client.SUNSUBSCRIBE(channels, listener, bufferMode)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
sUnsubscribe = this.SUNSUBSCRIBE;
|
||||||
|
|
||||||
quit(): Promise<void> {
|
quit(): Promise<void> {
|
||||||
return this.#slots.quit();
|
return this.#slots.quit();
|
||||||
}
|
}
|
||||||
@@ -254,6 +389,32 @@ export default class RedisCluster<
|
|||||||
disconnect(): Promise<void> {
|
disconnect(): Promise<void> {
|
||||||
return this.#slots.disconnect();
|
return this.#slots.disconnect();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nodeClient(node: ShardNode<M, F, S>) {
|
||||||
|
return this.#slots.nodeClient(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
getRandomNode() {
|
||||||
|
return this.#slots.getRandomNode();
|
||||||
|
}
|
||||||
|
|
||||||
|
getSlotRandomNode(slot: number) {
|
||||||
|
return this.#slots.getSlotRandomNode(slot);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated use `.masters` instead
|
||||||
|
*/
|
||||||
|
getMasters() {
|
||||||
|
return this.masters;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated use `.slots[<SLOT>]` instead
|
||||||
|
*/
|
||||||
|
getSlotMaster(slot: number) {
|
||||||
|
return this.slots[slot].master;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
attachCommands({
|
attachCommands({
|
||||||
|
@@ -120,11 +120,8 @@ export default class RedisClusterMultiCommand {
|
|||||||
return this.execAsPipeline();
|
return this.execAsPipeline();
|
||||||
}
|
}
|
||||||
|
|
||||||
const commands = this.#multi.exec();
|
|
||||||
if (!commands) return [];
|
|
||||||
|
|
||||||
return this.#multi.handleExecReplies(
|
return this.#multi.handleExecReplies(
|
||||||
await this.#executor(commands, this.#firstKey, RedisMultiCommand.generateChainId())
|
await this.#executor(this.#multi.queue, this.#firstKey, RedisMultiCommand.generateChainId())
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -108,6 +108,7 @@ export function transformCommandArguments<T = ClientCommandOptions>(
|
|||||||
command: RedisCommand,
|
command: RedisCommand,
|
||||||
args: Array<unknown>
|
args: Array<unknown>
|
||||||
): {
|
): {
|
||||||
|
jsArgs: Array<unknown>;
|
||||||
args: RedisCommandArguments;
|
args: RedisCommandArguments;
|
||||||
options: CommandOptions<T> | undefined;
|
options: CommandOptions<T> | undefined;
|
||||||
} {
|
} {
|
||||||
@@ -118,6 +119,7 @@ export function transformCommandArguments<T = ClientCommandOptions>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
jsArgs: args,
|
||||||
args: command.transformArguments(...args),
|
args: command.transformArguments(...args),
|
||||||
options
|
options
|
||||||
};
|
};
|
||||||
|
@@ -13,32 +13,22 @@ describe('ACL GETUSER', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
testUtils.testWithClient('client.aclGetUser', async client => {
|
testUtils.testWithClient('client.aclGetUser', async client => {
|
||||||
const expectedReply: any = {
|
const reply = await client.aclGetUser('default');
|
||||||
passwords: [],
|
|
||||||
commands: '+@all',
|
assert.ok(Array.isArray(reply.passwords));
|
||||||
};
|
assert.equal(typeof reply.commands, 'string');
|
||||||
|
assert.ok(Array.isArray(reply.flags));
|
||||||
|
|
||||||
if (testUtils.isVersionGreaterThan([7])) {
|
if (testUtils.isVersionGreaterThan([7])) {
|
||||||
expectedReply.flags = ['on', 'nopass'];
|
assert.equal(typeof reply.keys, 'string');
|
||||||
expectedReply.keys = '~*';
|
assert.equal(typeof reply.channels, 'string');
|
||||||
expectedReply.channels = '&*';
|
assert.ok(Array.isArray(reply.selectors));
|
||||||
expectedReply.selectors = [];
|
|
||||||
} else {
|
} else {
|
||||||
expectedReply.keys = ['*'];
|
assert.ok(Array.isArray(reply.keys));
|
||||||
expectedReply.selectors = undefined;
|
|
||||||
|
|
||||||
if (testUtils.isVersionGreaterThan([6, 2])) {
|
if (testUtils.isVersionGreaterThan([6, 2])) {
|
||||||
expectedReply.flags = ['on', 'allkeys', 'allchannels', 'allcommands', 'nopass'];
|
assert.ok(Array.isArray(reply.channels));
|
||||||
expectedReply.channels = ['*'];
|
}
|
||||||
} else {
|
|
||||||
expectedReply.flags = ['on', 'allkeys', 'allcommands', 'nopass'];
|
|
||||||
expectedReply.channels = undefined;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
assert.deepEqual(
|
|
||||||
await client.aclGetUser('default'),
|
|
||||||
expectedReply
|
|
||||||
);
|
|
||||||
}, GLOBAL.SERVERS.OPEN);
|
}, GLOBAL.SERVERS.OPEN);
|
||||||
});
|
});
|
||||||
|
@@ -1,7 +1,10 @@
|
|||||||
import { strict as assert } from 'assert';
|
import { strict as assert } from 'assert';
|
||||||
import { transformArguments, transformReply } from './CLIENT_INFO';
|
import { transformArguments, transformReply } from './CLIENT_INFO';
|
||||||
|
import testUtils, { GLOBAL } from '../test-utils';
|
||||||
|
|
||||||
describe('CLIENT INFO', () => {
|
describe('CLIENT INFO', () => {
|
||||||
|
testUtils.isVersionGreaterThanHook([6, 2]);
|
||||||
|
|
||||||
it('transformArguments', () => {
|
it('transformArguments', () => {
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
transformArguments(),
|
transformArguments(),
|
||||||
@@ -9,34 +12,39 @@ describe('CLIENT INFO', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('transformReply', () => {
|
testUtils.testWithClient('client.clientInfo', async client => {
|
||||||
assert.deepEqual(
|
const reply = await client.clientInfo();
|
||||||
transformReply('id=526512 addr=127.0.0.1:36244 laddr=127.0.0.1:6379 fd=8 name= age=11213 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=26 qbuf-free=40928 argv-mem=10 obl=0 oll=0 omem=0 tot-mem=61466 events=r cmd=client user=default redir=-1\n'),
|
assert.equal(typeof reply.id, 'number');
|
||||||
{
|
assert.equal(typeof reply.addr, 'string');
|
||||||
id: 526512,
|
assert.equal(typeof reply.laddr, 'string');
|
||||||
addr: '127.0.0.1:36244',
|
assert.equal(typeof reply.fd, 'number');
|
||||||
laddr: '127.0.0.1:6379',
|
assert.equal(typeof reply.name, 'string');
|
||||||
fd: 8,
|
assert.equal(typeof reply.age, 'number');
|
||||||
name: '',
|
assert.equal(typeof reply.idle, 'number');
|
||||||
age: 11213,
|
assert.equal(typeof reply.flags, 'string');
|
||||||
idle: 0,
|
assert.equal(typeof reply.db, 'number');
|
||||||
flags: 'N',
|
assert.equal(typeof reply.sub, 'number');
|
||||||
db: 0,
|
assert.equal(typeof reply.psub, 'number');
|
||||||
sub: 0,
|
assert.equal(typeof reply.multi, 'number');
|
||||||
psub: 0,
|
assert.equal(typeof reply.qbuf, 'number');
|
||||||
multi: -1,
|
assert.equal(typeof reply.qbufFree, 'number');
|
||||||
qbuf: 26,
|
assert.equal(typeof reply.argvMem, 'number');
|
||||||
qbufFree: 40928,
|
assert.equal(typeof reply.obl, 'number');
|
||||||
argvMem: 10,
|
assert.equal(typeof reply.oll, 'number');
|
||||||
obl: 0,
|
assert.equal(typeof reply.omem, 'number');
|
||||||
oll: 0,
|
assert.equal(typeof reply.totMem, 'number');
|
||||||
omem: 0,
|
assert.equal(typeof reply.events, 'string');
|
||||||
totMem: 61466,
|
assert.equal(typeof reply.cmd, 'string');
|
||||||
events: 'r',
|
assert.equal(typeof reply.user, 'string');
|
||||||
cmd: 'client',
|
assert.equal(typeof reply.redir, 'number');
|
||||||
user: 'default',
|
|
||||||
redir: -1
|
if (testUtils.isVersionGreaterThan([7, 0])) {
|
||||||
}
|
assert.equal(typeof reply.multiMem, 'number');
|
||||||
);
|
assert.equal(typeof reply.resp, 'number');
|
||||||
});
|
}
|
||||||
|
|
||||||
|
if (testUtils.isVersionGreaterThan([7, 0, 3])) {
|
||||||
|
assert.equal(typeof reply.ssub, 'number');
|
||||||
|
}
|
||||||
|
}, GLOBAL.SERVERS.OPEN);
|
||||||
});
|
});
|
||||||
|
@@ -1,11 +1,13 @@
|
|||||||
|
export const IS_READ_ONLY = true;
|
||||||
|
|
||||||
export function transformArguments(): Array<string> {
|
export function transformArguments(): Array<string> {
|
||||||
return ['CLIENT', 'INFO'];
|
return ['CLIENT', 'INFO'];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ClientInfoReply {
|
export interface ClientInfoReply {
|
||||||
id: number;
|
id: number;
|
||||||
addr: string;
|
addr: string;
|
||||||
laddr: string;
|
laddr?: string; // 6.2
|
||||||
fd: number;
|
fd: number;
|
||||||
name: string;
|
name: string;
|
||||||
age: number;
|
age: number;
|
||||||
@@ -14,72 +16,79 @@ interface ClientInfoReply {
|
|||||||
db: number;
|
db: number;
|
||||||
sub: number;
|
sub: number;
|
||||||
psub: number;
|
psub: number;
|
||||||
|
ssub?: number; // 7.0.3
|
||||||
multi: number;
|
multi: number;
|
||||||
qbuf: number;
|
qbuf: number;
|
||||||
qbufFree: number;
|
qbufFree: number;
|
||||||
argvMem: number;
|
argvMem?: number; // 6.0
|
||||||
|
multiMem?: number; // 7.0
|
||||||
obl: number;
|
obl: number;
|
||||||
oll: number;
|
oll: number;
|
||||||
omem: number;
|
omem: number;
|
||||||
totMem: number;
|
totMem?: number; // 6.0
|
||||||
events: string;
|
events: string;
|
||||||
cmd: string;
|
cmd: string;
|
||||||
user: string;
|
user?: string; // 6.0
|
||||||
redir: number;
|
redir?: number; // 6.2
|
||||||
|
resp?: number; // 7.0
|
||||||
|
// 7.2
|
||||||
|
libName?: string;
|
||||||
|
libVer?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const REGEX = /=([^\s]*)/g;
|
const CLIENT_INFO_REGEX = /([^\s=]+)=([^\s]*)/g;
|
||||||
|
|
||||||
export function transformReply(reply: string): ClientInfoReply {
|
export function transformReply(rawReply: string): ClientInfoReply {
|
||||||
const [
|
const map: Record<string, string> = {};
|
||||||
[, id],
|
for (const item of rawReply.matchAll(CLIENT_INFO_REGEX)) {
|
||||||
[, addr],
|
map[item[1]] = item[2];
|
||||||
[, laddr],
|
}
|
||||||
[, fd],
|
|
||||||
[, name],
|
|
||||||
[, age],
|
|
||||||
[, idle],
|
|
||||||
[, flags],
|
|
||||||
[, db],
|
|
||||||
[, sub],
|
|
||||||
[, psub],
|
|
||||||
[, multi],
|
|
||||||
[, qbuf],
|
|
||||||
[, qbufFree],
|
|
||||||
[, argvMem],
|
|
||||||
[, obl],
|
|
||||||
[, oll],
|
|
||||||
[, omem],
|
|
||||||
[, totMem],
|
|
||||||
[, events],
|
|
||||||
[, cmd],
|
|
||||||
[, user],
|
|
||||||
[, redir]
|
|
||||||
] = [...reply.matchAll(REGEX)];
|
|
||||||
|
|
||||||
return {
|
const reply: ClientInfoReply = {
|
||||||
id: Number(id),
|
id: Number(map.id),
|
||||||
addr,
|
addr: map.addr,
|
||||||
laddr,
|
fd: Number(map.fd),
|
||||||
fd: Number(fd),
|
name: map.name,
|
||||||
name,
|
age: Number(map.age),
|
||||||
age: Number(age),
|
idle: Number(map.idle),
|
||||||
idle: Number(idle),
|
flags: map.flags,
|
||||||
flags,
|
db: Number(map.db),
|
||||||
db: Number(db),
|
sub: Number(map.sub),
|
||||||
sub: Number(sub),
|
psub: Number(map.psub),
|
||||||
psub: Number(psub),
|
multi: Number(map.multi),
|
||||||
multi: Number(multi),
|
qbuf: Number(map.qbuf),
|
||||||
qbuf: Number(qbuf),
|
qbufFree: Number(map['qbuf-free']),
|
||||||
qbufFree: Number(qbufFree),
|
argvMem: Number(map['argv-mem']),
|
||||||
argvMem: Number(argvMem),
|
obl: Number(map.obl),
|
||||||
obl: Number(obl),
|
oll: Number(map.oll),
|
||||||
oll: Number(oll),
|
omem: Number(map.omem),
|
||||||
omem: Number(omem),
|
totMem: Number(map['tot-mem']),
|
||||||
totMem: Number(totMem),
|
events: map.events,
|
||||||
events,
|
cmd: map.cmd,
|
||||||
cmd,
|
user: map.user,
|
||||||
user,
|
libName: map['lib-name'],
|
||||||
redir: Number(redir)
|
libVer: map['lib-ver'],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (map.laddr !== undefined) {
|
||||||
|
reply.laddr = map.laddr;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (map.redir !== undefined) {
|
||||||
|
reply.redir = Number(map.redir);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (map.ssub !== undefined) {
|
||||||
|
reply.ssub = Number(map.ssub);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (map['multi-mem'] !== undefined) {
|
||||||
|
reply.multiMem = Number(map['multi-mem']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (map.resp !== undefined) {
|
||||||
|
reply.resp = Number(map.resp);
|
||||||
|
}
|
||||||
|
|
||||||
|
return reply;
|
||||||
}
|
}
|
||||||
|
@@ -65,6 +65,16 @@ describe('CLIENT KILL', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('MAXAGE', () => {
|
||||||
|
assert.deepEqual(
|
||||||
|
transformArguments({
|
||||||
|
filter: ClientKillFilters.MAXAGE,
|
||||||
|
maxAge: 10
|
||||||
|
}),
|
||||||
|
['CLIENT', 'KILL', 'MAXAGE', '10']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
describe('SKIP_ME', () => {
|
describe('SKIP_ME', () => {
|
||||||
it('undefined', () => {
|
it('undefined', () => {
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
|
@@ -6,7 +6,8 @@ export enum ClientKillFilters {
|
|||||||
ID = 'ID',
|
ID = 'ID',
|
||||||
TYPE = 'TYPE',
|
TYPE = 'TYPE',
|
||||||
USER = 'USER',
|
USER = 'USER',
|
||||||
SKIP_ME = 'SKIPME'
|
SKIP_ME = 'SKIPME',
|
||||||
|
MAXAGE = 'MAXAGE'
|
||||||
}
|
}
|
||||||
|
|
||||||
interface KillFilter<T extends ClientKillFilters> {
|
interface KillFilter<T extends ClientKillFilters> {
|
||||||
@@ -37,7 +38,11 @@ type KillSkipMe = ClientKillFilters.SKIP_ME | (KillFilter<ClientKillFilters.SKIP
|
|||||||
skipMe: boolean;
|
skipMe: boolean;
|
||||||
});
|
});
|
||||||
|
|
||||||
type KillFilters = KillAddress | KillLocalAddress | KillId | KillType | KillUser | KillSkipMe;
|
interface KillMaxAge extends KillFilter<ClientKillFilters.MAXAGE> {
|
||||||
|
maxAge: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
type KillFilters = KillAddress | KillLocalAddress | KillId | KillType | KillUser | KillSkipMe | KillMaxAge;
|
||||||
|
|
||||||
export function transformArguments(filters: KillFilters | Array<KillFilters>): RedisCommandArguments {
|
export function transformArguments(filters: KillFilters | Array<KillFilters>): RedisCommandArguments {
|
||||||
const args = ['CLIENT', 'KILL'];
|
const args = ['CLIENT', 'KILL'];
|
||||||
@@ -89,6 +94,10 @@ function pushFilter(args: RedisCommandArguments, filter: KillFilters): void {
|
|||||||
case ClientKillFilters.SKIP_ME:
|
case ClientKillFilters.SKIP_ME:
|
||||||
args.push(filter.skipMe ? 'yes' : 'no');
|
args.push(filter.skipMe ? 'yes' : 'no');
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case ClientKillFilters.MAXAGE:
|
||||||
|
args.push(filter.maxAge.toString());
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
78
packages/client/lib/commands/CLIENT_LIST.spec.ts
Normal file
78
packages/client/lib/commands/CLIENT_LIST.spec.ts
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import { strict as assert } from 'assert';
|
||||||
|
import { transformArguments, transformReply } from './CLIENT_LIST';
|
||||||
|
import testUtils, { GLOBAL } from '../test-utils';
|
||||||
|
|
||||||
|
describe('CLIENT LIST', () => {
|
||||||
|
describe('transformArguments', () => {
|
||||||
|
it('simple', () => {
|
||||||
|
assert.deepEqual(
|
||||||
|
transformArguments(),
|
||||||
|
['CLIENT', 'LIST']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('with TYPE', () => {
|
||||||
|
assert.deepEqual(
|
||||||
|
transformArguments({
|
||||||
|
TYPE: 'NORMAL'
|
||||||
|
}),
|
||||||
|
['CLIENT', 'LIST', 'TYPE', 'NORMAL']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('with ID', () => {
|
||||||
|
assert.deepEqual(
|
||||||
|
transformArguments({
|
||||||
|
ID: ['1', '2']
|
||||||
|
}),
|
||||||
|
['CLIENT', 'LIST', 'ID', '1', '2']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
testUtils.testWithClient('client.clientList', async client => {
|
||||||
|
const reply = await client.clientList();
|
||||||
|
assert.ok(Array.isArray(reply));
|
||||||
|
|
||||||
|
for (const item of reply) {
|
||||||
|
assert.equal(typeof item.id, 'number');
|
||||||
|
assert.equal(typeof item.addr, 'string');
|
||||||
|
assert.equal(typeof item.fd, 'number');
|
||||||
|
assert.equal(typeof item.name, 'string');
|
||||||
|
assert.equal(typeof item.age, 'number');
|
||||||
|
assert.equal(typeof item.idle, 'number');
|
||||||
|
assert.equal(typeof item.flags, 'string');
|
||||||
|
assert.equal(typeof item.db, 'number');
|
||||||
|
assert.equal(typeof item.sub, 'number');
|
||||||
|
assert.equal(typeof item.psub, 'number');
|
||||||
|
assert.equal(typeof item.multi, 'number');
|
||||||
|
assert.equal(typeof item.qbuf, 'number');
|
||||||
|
assert.equal(typeof item.qbufFree, 'number');
|
||||||
|
assert.equal(typeof item.obl, 'number');
|
||||||
|
assert.equal(typeof item.oll, 'number');
|
||||||
|
assert.equal(typeof item.omem, 'number');
|
||||||
|
assert.equal(typeof item.events, 'string');
|
||||||
|
assert.equal(typeof item.cmd, 'string');
|
||||||
|
|
||||||
|
if (testUtils.isVersionGreaterThan([6, 0])) {
|
||||||
|
assert.equal(typeof item.argvMem, 'number');
|
||||||
|
assert.equal(typeof item.totMem, 'number');
|
||||||
|
assert.equal(typeof item.user, 'string');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (testUtils.isVersionGreaterThan([6, 2])) {
|
||||||
|
assert.equal(typeof item.redir, 'number');
|
||||||
|
assert.equal(typeof item.laddr, 'string');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (testUtils.isVersionGreaterThan([7, 0])) {
|
||||||
|
assert.equal(typeof item.multiMem, 'number');
|
||||||
|
assert.equal(typeof item.resp, 'number');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (testUtils.isVersionGreaterThan([7, 0, 3])) {
|
||||||
|
assert.equal(typeof item.ssub, 'number');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, GLOBAL.SERVERS.OPEN);
|
||||||
|
});
|
43
packages/client/lib/commands/CLIENT_LIST.ts
Normal file
43
packages/client/lib/commands/CLIENT_LIST.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { RedisCommandArguments, RedisCommandArgument } from '.';
|
||||||
|
import { pushVerdictArguments } from './generic-transformers';
|
||||||
|
import { transformReply as transformClientInfoReply, ClientInfoReply } from './CLIENT_INFO';
|
||||||
|
|
||||||
|
interface ListFilterType {
|
||||||
|
TYPE: 'NORMAL' | 'MASTER' | 'REPLICA' | 'PUBSUB';
|
||||||
|
ID?: never;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ListFilterId {
|
||||||
|
ID: Array<RedisCommandArgument>;
|
||||||
|
TYPE?: never;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ListFilter = ListFilterType | ListFilterId;
|
||||||
|
|
||||||
|
export const IS_READ_ONLY = true;
|
||||||
|
|
||||||
|
export function transformArguments(filter?: ListFilter): RedisCommandArguments {
|
||||||
|
let args: RedisCommandArguments = ['CLIENT', 'LIST'];
|
||||||
|
|
||||||
|
if (filter) {
|
||||||
|
if (filter.TYPE !== undefined) {
|
||||||
|
args.push('TYPE', filter.TYPE);
|
||||||
|
} else {
|
||||||
|
args.push('ID');
|
||||||
|
args = pushVerdictArguments(args, filter.ID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function transformReply(rawReply: string): Array<ClientInfoReply> {
|
||||||
|
const split = rawReply.split('\n'),
|
||||||
|
length = split.length - 1,
|
||||||
|
reply: Array<ClientInfoReply> = [];
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
reply.push(transformClientInfoReply(split[i]));
|
||||||
|
}
|
||||||
|
|
||||||
|
return reply;
|
||||||
|
}
|
30
packages/client/lib/commands/CLIENT_NO-TOUCH.spec.ts
Normal file
30
packages/client/lib/commands/CLIENT_NO-TOUCH.spec.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { strict as assert } from 'assert';
|
||||||
|
import testUtils, { GLOBAL } from '../test-utils';
|
||||||
|
import { transformArguments } from './CLIENT_NO-TOUCH';
|
||||||
|
|
||||||
|
describe('CLIENT NO-TOUCH', () => {
|
||||||
|
testUtils.isVersionGreaterThanHook([7, 2]);
|
||||||
|
|
||||||
|
describe('transformArguments', () => {
|
||||||
|
it('true', () => {
|
||||||
|
assert.deepEqual(
|
||||||
|
transformArguments(true),
|
||||||
|
['CLIENT', 'NO-TOUCH', 'ON']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('false', () => {
|
||||||
|
assert.deepEqual(
|
||||||
|
transformArguments(false),
|
||||||
|
['CLIENT', 'NO-TOUCH', 'OFF']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
testUtils.testWithClient('client.clientNoTouch', async client => {
|
||||||
|
assert.equal(
|
||||||
|
await client.clientNoTouch(true),
|
||||||
|
'OK'
|
||||||
|
);
|
||||||
|
}, GLOBAL.SERVERS.OPEN);
|
||||||
|
});
|
11
packages/client/lib/commands/CLIENT_NO-TOUCH.ts
Normal file
11
packages/client/lib/commands/CLIENT_NO-TOUCH.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { RedisCommandArguments } from '.';
|
||||||
|
|
||||||
|
export function transformArguments(value: boolean): RedisCommandArguments {
|
||||||
|
return [
|
||||||
|
'CLIENT',
|
||||||
|
'NO-TOUCH',
|
||||||
|
value ? 'ON' : 'OFF'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
export declare function transformReply(): 'OK' | Buffer;
|
@@ -11,8 +11,9 @@ describe('CLUSTER BUMPEPOCH', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
testUtils.testWithCluster('clusterNode.clusterBumpEpoch', async cluster => {
|
testUtils.testWithCluster('clusterNode.clusterBumpEpoch', async cluster => {
|
||||||
|
const client = await cluster.nodeClient(cluster.masters[0]);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
typeof await cluster.getSlotMaster(0).client.clusterBumpEpoch(),
|
typeof await client.clusterBumpEpoch(),
|
||||||
'string'
|
'string'
|
||||||
);
|
);
|
||||||
}, GLOBAL.SERVERS.OPEN);
|
}, GLOBAL.SERVERS.OPEN);
|
||||||
|
@@ -11,7 +11,7 @@ describe('CLUSTER COUNT-FAILURE-REPORTS', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
testUtils.testWithCluster('clusterNode.clusterCountFailureReports', async cluster => {
|
testUtils.testWithCluster('clusterNode.clusterCountFailureReports', async cluster => {
|
||||||
const { client } = cluster.getSlotMaster(0);
|
const client = await cluster.nodeClient(cluster.masters[0]);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
typeof await client.clusterCountFailureReports(
|
typeof await client.clusterCountFailureReports(
|
||||||
await client.clusterMyId()
|
await client.clusterMyId()
|
||||||
|
@@ -11,8 +11,9 @@ describe('CLUSTER COUNTKEYSINSLOT', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
testUtils.testWithCluster('clusterNode.clusterCountKeysInSlot', async cluster => {
|
testUtils.testWithCluster('clusterNode.clusterCountKeysInSlot', async cluster => {
|
||||||
|
const client = await cluster.nodeClient(cluster.masters[0]);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
typeof await cluster.getSlotMaster(0).client.clusterCountKeysInSlot(0),
|
typeof await client.clusterCountKeysInSlot(0),
|
||||||
'number'
|
'number'
|
||||||
);
|
);
|
||||||
}, GLOBAL.CLUSTERS.OPEN);
|
}, GLOBAL.CLUSTERS.OPEN);
|
||||||
|
@@ -11,7 +11,8 @@ describe('CLUSTER GETKEYSINSLOT', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
testUtils.testWithCluster('clusterNode.clusterGetKeysInSlot', async cluster => {
|
testUtils.testWithCluster('clusterNode.clusterGetKeysInSlot', async cluster => {
|
||||||
const reply = await cluster.getSlotMaster(0).client.clusterGetKeysInSlot(0, 1);
|
const client = await cluster.nodeClient(cluster.masters[0]),
|
||||||
|
reply = await client.clusterGetKeysInSlot(0, 1);
|
||||||
assert.ok(Array.isArray(reply));
|
assert.ok(Array.isArray(reply));
|
||||||
for (const item of reply) {
|
for (const item of reply) {
|
||||||
assert.equal(typeof item, 'string');
|
assert.equal(typeof item, 'string');
|
||||||
|
@@ -46,8 +46,9 @@ describe('CLUSTER INFO', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
testUtils.testWithCluster('clusterNode.clusterInfo', async cluster => {
|
testUtils.testWithCluster('clusterNode.clusterInfo', async cluster => {
|
||||||
|
const client = await cluster.nodeClient(cluster.masters[0]);
|
||||||
assert.notEqual(
|
assert.notEqual(
|
||||||
await cluster.getSlotMaster(0).client.clusterInfo(),
|
await client.clusterInfo(),
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
}, GLOBAL.CLUSTERS.OPEN);
|
}, GLOBAL.CLUSTERS.OPEN);
|
||||||
|
@@ -11,8 +11,9 @@ describe('CLUSTER KEYSLOT', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
testUtils.testWithCluster('clusterNode.clusterKeySlot', async cluster => {
|
testUtils.testWithCluster('clusterNode.clusterKeySlot', async cluster => {
|
||||||
|
const client = await cluster.nodeClient(cluster.masters[0]);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
typeof await cluster.getSlotMaster(0).client.clusterKeySlot('key'),
|
typeof await client.clusterKeySlot('key'),
|
||||||
'number'
|
'number'
|
||||||
);
|
);
|
||||||
}, GLOBAL.CLUSTERS.OPEN);
|
}, GLOBAL.CLUSTERS.OPEN);
|
||||||
|
@@ -13,7 +13,8 @@ describe('CLUSTER LINKS', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
testUtils.testWithCluster('clusterNode.clusterLinks', async cluster => {
|
testUtils.testWithCluster('clusterNode.clusterLinks', async cluster => {
|
||||||
const links = await cluster.getSlotMaster(0).client.clusterLinks();
|
const client = await cluster.nodeClient(cluster.masters[0]),
|
||||||
|
links = await client.clusterLinks();
|
||||||
assert.ok(Array.isArray(links));
|
assert.ok(Array.isArray(links));
|
||||||
for (const link of links) {
|
for (const link of links) {
|
||||||
assert.equal(typeof link.direction, 'string');
|
assert.equal(typeof link.direction, 'string');
|
||||||
|
@@ -11,9 +11,11 @@ describe('CLUSTER MYID', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
testUtils.testWithCluster('clusterNode.clusterMyId', async cluster => {
|
testUtils.testWithCluster('clusterNode.clusterMyId', async cluster => {
|
||||||
|
const [master] = cluster.masters,
|
||||||
|
client = await cluster.nodeClient(master);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
typeof await cluster.getSlotMaster(0).client.clusterMyId(),
|
await client.clusterMyId(),
|
||||||
'string'
|
master.id
|
||||||
);
|
);
|
||||||
}, GLOBAL.CLUSTERS.OPEN);
|
}, GLOBAL.CLUSTERS.OPEN);
|
||||||
});
|
});
|
||||||
|
22
packages/client/lib/commands/CLUSTER_MYSHARDID.spec.ts
Normal file
22
packages/client/lib/commands/CLUSTER_MYSHARDID.spec.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { strict as assert } from 'assert';
|
||||||
|
import testUtils, { GLOBAL } from '../test-utils';
|
||||||
|
import { transformArguments } from './CLUSTER_MYSHARDID';
|
||||||
|
|
||||||
|
describe('CLUSTER MYSHARDID', () => {
|
||||||
|
testUtils.isVersionGreaterThanHook([7, 2]);
|
||||||
|
|
||||||
|
it('transformArguments', () => {
|
||||||
|
assert.deepEqual(
|
||||||
|
transformArguments(),
|
||||||
|
['CLUSTER', 'MYSHARDID']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
testUtils.testWithCluster('clusterNode.clusterMyShardId', async cluster => {
|
||||||
|
const client = await cluster.nodeClient(cluster.masters[0]);
|
||||||
|
assert.equal(
|
||||||
|
typeof await client.clusterMyShardId(),
|
||||||
|
'string'
|
||||||
|
);
|
||||||
|
}, GLOBAL.CLUSTERS.OPEN);
|
||||||
|
});
|
7
packages/client/lib/commands/CLUSTER_MYSHARDID.ts
Normal file
7
packages/client/lib/commands/CLUSTER_MYSHARDID.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export const IS_READ_ONLY = true;
|
||||||
|
|
||||||
|
export function transformArguments() {
|
||||||
|
return ['CLUSTER', 'MYSHARDID'];
|
||||||
|
}
|
||||||
|
|
||||||
|
export declare function transformReply(): string | Buffer;
|
@@ -11,8 +11,9 @@ describe('CLUSTER SAVECONFIG', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
testUtils.testWithCluster('clusterNode.clusterSaveConfig', async cluster => {
|
testUtils.testWithCluster('clusterNode.clusterSaveConfig', async cluster => {
|
||||||
|
const client = await cluster.nodeClient(cluster.masters[0]);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
await cluster.getSlotMaster(0).client.clusterSaveConfig(),
|
await client.clusterSaveConfig(),
|
||||||
'OK'
|
'OK'
|
||||||
);
|
);
|
||||||
}, GLOBAL.CLUSTERS.OPEN);
|
}, GLOBAL.CLUSTERS.OPEN);
|
||||||
|
@@ -13,7 +13,7 @@ type ClusterSlotsRawReply = Array<[
|
|||||||
...replicas: Array<ClusterSlotsRawNode>
|
...replicas: Array<ClusterSlotsRawNode>
|
||||||
]>;
|
]>;
|
||||||
|
|
||||||
type ClusterSlotsNode = {
|
export interface ClusterSlotsNode {
|
||||||
ip: string;
|
ip: string;
|
||||||
port: number;
|
port: number;
|
||||||
id: string;
|
id: string;
|
||||||
|
40
packages/client/lib/commands/HEXPIRE.spec.ts
Normal file
40
packages/client/lib/commands/HEXPIRE.spec.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { strict as assert } from 'node:assert';
|
||||||
|
import testUtils, { GLOBAL } from '../test-utils';
|
||||||
|
import { transformArguments } from './HEXPIRE';
|
||||||
|
import { HASH_EXPIRATION_TIME } from './HEXPIRETIME';
|
||||||
|
|
||||||
|
describe('HEXPIRE', () => {
|
||||||
|
testUtils.isVersionGreaterThanHook([7, 4]);
|
||||||
|
|
||||||
|
describe('transformArguments', () => {
|
||||||
|
it('string', () => {
|
||||||
|
assert.deepEqual(
|
||||||
|
transformArguments('key', 'field', 1),
|
||||||
|
['HEXPIRE', 'key', '1', 'FIELDS', '1', 'field']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('array', () => {
|
||||||
|
assert.deepEqual(
|
||||||
|
transformArguments('key', ['field1', 'field2'], 1),
|
||||||
|
['HEXPIRE', 'key', '1', 'FIELDS', '2', 'field1', 'field2']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('with set option', () => {
|
||||||
|
assert.deepEqual(
|
||||||
|
transformArguments('key', ['field1'], 1, 'NX'),
|
||||||
|
['HEXPIRE', 'key', '1', 'NX', 'FIELDS', '1', 'field1']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
testUtils.testWithClient('hexpire', async client => {
|
||||||
|
assert.deepEqual(
|
||||||
|
await client.hExpire('key', ['field1'], 0),
|
||||||
|
[HASH_EXPIRATION_TIME.FIELD_NOT_EXISTS]
|
||||||
|
);
|
||||||
|
}, {
|
||||||
|
...GLOBAL.SERVERS.OPEN
|
||||||
|
});
|
||||||
|
});
|
44
packages/client/lib/commands/HEXPIRE.ts
Normal file
44
packages/client/lib/commands/HEXPIRE.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { RedisCommandArgument } from '.';
|
||||||
|
import { pushVerdictArgument } from './generic-transformers';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @readonly
|
||||||
|
* @enum {number}
|
||||||
|
*/
|
||||||
|
export const HASH_EXPIRATION = {
|
||||||
|
/** @property {number} */
|
||||||
|
/** The field does not exist */
|
||||||
|
FIELD_NOT_EXISTS: -2,
|
||||||
|
/** @property {number} */
|
||||||
|
/** Specified NX | XX | GT | LT condition not met */
|
||||||
|
CONDITION_NOT_MET: 0,
|
||||||
|
/** @property {number} */
|
||||||
|
/** Expiration time was set or updated */
|
||||||
|
UPDATED: 1,
|
||||||
|
/** @property {number} */
|
||||||
|
/** Field deleted because the specified expiration time is in the past */
|
||||||
|
DELETED: 2
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type HashExpiration = typeof HASH_EXPIRATION[keyof typeof HASH_EXPIRATION];
|
||||||
|
|
||||||
|
export const FIRST_KEY_INDEX = 1;
|
||||||
|
|
||||||
|
export function transformArguments(
|
||||||
|
key: RedisCommandArgument,
|
||||||
|
fields: RedisCommandArgument| Array<RedisCommandArgument>,
|
||||||
|
seconds: number,
|
||||||
|
mode?: 'NX' | 'XX' | 'GT' | 'LT',
|
||||||
|
) {
|
||||||
|
const args = ['HEXPIRE', key, seconds.toString()];
|
||||||
|
|
||||||
|
if (mode) {
|
||||||
|
args.push(mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
args.push('FIELDS');
|
||||||
|
|
||||||
|
return pushVerdictArgument(args, fields);
|
||||||
|
}
|
||||||
|
|
||||||
|
export declare function transformReply(): Array<HashExpiration>;
|
49
packages/client/lib/commands/HEXPIREAT.spec.ts
Normal file
49
packages/client/lib/commands/HEXPIREAT.spec.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import { strict as assert } from 'node:assert';
|
||||||
|
import testUtils, { GLOBAL } from '../test-utils';
|
||||||
|
import { transformArguments } from './HEXPIREAT';
|
||||||
|
import { HASH_EXPIRATION_TIME } from './HEXPIRETIME';
|
||||||
|
|
||||||
|
describe('HEXPIREAT', () => {
|
||||||
|
testUtils.isVersionGreaterThanHook([7, 4]);
|
||||||
|
|
||||||
|
describe('transformArguments', () => {
|
||||||
|
it('string + number', () => {
|
||||||
|
assert.deepEqual(
|
||||||
|
transformArguments('key', 'field', 1),
|
||||||
|
['HEXPIREAT', 'key', '1', 'FIELDS', '1', 'field']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('array + number', () => {
|
||||||
|
assert.deepEqual(
|
||||||
|
transformArguments('key', ['field1', 'field2'], 1),
|
||||||
|
['HEXPIREAT', 'key', '1', 'FIELDS', '2', 'field1', 'field2']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('date', () => {
|
||||||
|
const d = new Date();
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
transformArguments('key', ['field1'], d),
|
||||||
|
['HEXPIREAT', 'key', Math.floor(d.getTime() / 1000).toString(), 'FIELDS', '1', 'field1']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('with set option', () => {
|
||||||
|
assert.deepEqual(
|
||||||
|
transformArguments('key', 'field1', 1, 'GT'),
|
||||||
|
['HEXPIREAT', 'key', '1', 'GT', 'FIELDS', '1', 'field1']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
testUtils.testWithClient('expireAt', async client => {
|
||||||
|
assert.deepEqual(
|
||||||
|
await client.hExpireAt('key', 'field1', 1),
|
||||||
|
[HASH_EXPIRATION_TIME.FIELD_NOT_EXISTS]
|
||||||
|
);
|
||||||
|
}, {
|
||||||
|
...GLOBAL.SERVERS.OPEN,
|
||||||
|
});
|
||||||
|
});
|
28
packages/client/lib/commands/HEXPIREAT.ts
Normal file
28
packages/client/lib/commands/HEXPIREAT.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { RedisCommandArgument } from '.';
|
||||||
|
import { pushVerdictArgument, transformEXAT } from './generic-transformers';
|
||||||
|
import { HashExpiration } from './HEXPIRE';
|
||||||
|
|
||||||
|
export const FIRST_KEY_INDEX = 1;
|
||||||
|
|
||||||
|
export function transformArguments(
|
||||||
|
key: RedisCommandArgument,
|
||||||
|
fields: RedisCommandArgument | Array<RedisCommandArgument>,
|
||||||
|
timestamp: number | Date,
|
||||||
|
mode?: 'NX' | 'XX' | 'GT' | 'LT'
|
||||||
|
) {
|
||||||
|
const args = [
|
||||||
|
'HEXPIREAT',
|
||||||
|
key,
|
||||||
|
transformEXAT(timestamp)
|
||||||
|
];
|
||||||
|
|
||||||
|
if (mode) {
|
||||||
|
args.push(mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
args.push('FIELDS')
|
||||||
|
|
||||||
|
return pushVerdictArgument(args, fields);
|
||||||
|
}
|
||||||
|
|
||||||
|
export declare function transformReply(): Array<HashExpiration>;
|
32
packages/client/lib/commands/HEXPIRETIME.spec.ts
Normal file
32
packages/client/lib/commands/HEXPIRETIME.spec.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { strict as assert } from 'node:assert';
|
||||||
|
import testUtils, { GLOBAL } from '../test-utils';
|
||||||
|
import { HASH_EXPIRATION_TIME, transformArguments } from './HEXPIRETIME';
|
||||||
|
|
||||||
|
describe('HEXPIRETIME', () => {
|
||||||
|
testUtils.isVersionGreaterThanHook([7, 4]);
|
||||||
|
|
||||||
|
describe('transformArguments', () => {
|
||||||
|
it('string', () => {
|
||||||
|
assert.deepEqual(
|
||||||
|
transformArguments('key', 'field'),
|
||||||
|
['HEXPIRETIME', 'key', 'FIELDS', '1', 'field']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('array', () => {
|
||||||
|
assert.deepEqual(
|
||||||
|
transformArguments('key', ['field1', 'field2']),
|
||||||
|
['HEXPIRETIME', 'key', 'FIELDS', '2', 'field1', 'field2']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
|
testUtils.testWithClient('hExpireTime', async client => {
|
||||||
|
assert.deepEqual(
|
||||||
|
await client.hExpireTime('key', 'field1'),
|
||||||
|
[HASH_EXPIRATION_TIME.FIELD_NOT_EXISTS]
|
||||||
|
);
|
||||||
|
}, {
|
||||||
|
...GLOBAL.SERVERS.OPEN,
|
||||||
|
});
|
||||||
|
});
|
21
packages/client/lib/commands/HEXPIRETIME.ts
Normal file
21
packages/client/lib/commands/HEXPIRETIME.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { RedisCommandArgument } from '.';
|
||||||
|
import { pushVerdictArgument } from './generic-transformers';
|
||||||
|
|
||||||
|
export const HASH_EXPIRATION_TIME = {
|
||||||
|
/** @property {number} */
|
||||||
|
/** The field does not exist */
|
||||||
|
FIELD_NOT_EXISTS: -2,
|
||||||
|
/** @property {number} */
|
||||||
|
/** The field exists but has no associated expire */
|
||||||
|
NO_EXPIRATION: -1,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const FIRST_KEY_INDEX = 1
|
||||||
|
|
||||||
|
export const IS_READ_ONLY = true;
|
||||||
|
|
||||||
|
export function transformArguments(key: RedisCommandArgument, fields: RedisCommandArgument | Array<RedisCommandArgument>) {
|
||||||
|
return pushVerdictArgument(['HEXPIRETIME', key, 'FIELDS'], fields);
|
||||||
|
}
|
||||||
|
|
||||||
|
export declare function transformReply(): Array<number>;
|
33
packages/client/lib/commands/HPERSIST.spec.ts
Normal file
33
packages/client/lib/commands/HPERSIST.spec.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { strict as assert } from 'node:assert';
|
||||||
|
import testUtils, { GLOBAL } from '../test-utils';
|
||||||
|
import { transformArguments } from './HPERSIST';
|
||||||
|
import { HASH_EXPIRATION_TIME } from './HEXPIRETIME';
|
||||||
|
|
||||||
|
describe('HPERSIST', () => {
|
||||||
|
testUtils.isVersionGreaterThanHook([7, 4]);
|
||||||
|
|
||||||
|
describe('transformArguments', () => {
|
||||||
|
it('string', () => {
|
||||||
|
assert.deepEqual(
|
||||||
|
transformArguments('key', 'field'),
|
||||||
|
['HPERSIST', 'key', 'FIELDS', '1', 'field']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('array', () => {
|
||||||
|
assert.deepEqual(
|
||||||
|
transformArguments('key', ['field1', 'field2']),
|
||||||
|
['HPERSIST', 'key', 'FIELDS', '2', 'field1', 'field2']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
|
testUtils.testWithClient('hPersist', async client => {
|
||||||
|
assert.deepEqual(
|
||||||
|
await client.hPersist('key', 'field1'),
|
||||||
|
[HASH_EXPIRATION_TIME.FIELD_NOT_EXISTS]
|
||||||
|
);
|
||||||
|
}, {
|
||||||
|
...GLOBAL.SERVERS.OPEN,
|
||||||
|
});
|
||||||
|
});
|
10
packages/client/lib/commands/HPERSIST.ts
Normal file
10
packages/client/lib/commands/HPERSIST.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { RedisCommandArgument } from '.';
|
||||||
|
import { pushVerdictArgument } from './generic-transformers';
|
||||||
|
|
||||||
|
export const FIRST_KEY_INDEX = 1;
|
||||||
|
|
||||||
|
export function transformArguments(key: RedisCommandArgument, fields: RedisCommandArgument | Array<RedisCommandArgument>) {
|
||||||
|
return pushVerdictArgument(['HPERSIST', key, 'FIELDS'], fields);
|
||||||
|
}
|
||||||
|
|
||||||
|
export declare function transformReply(): Array<number> | null;
|
40
packages/client/lib/commands/HPEXPIRE.spec.ts
Normal file
40
packages/client/lib/commands/HPEXPIRE.spec.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { strict as assert } from 'node:assert';
|
||||||
|
import testUtils, { GLOBAL } from '../test-utils';
|
||||||
|
import { transformArguments } from './HPEXPIRE';
|
||||||
|
import { HASH_EXPIRATION_TIME } from './HEXPIRETIME';
|
||||||
|
|
||||||
|
describe('HEXPIRE', () => {
|
||||||
|
testUtils.isVersionGreaterThanHook([7, 4]);
|
||||||
|
|
||||||
|
describe('transformArguments', () => {
|
||||||
|
it('string', () => {
|
||||||
|
assert.deepEqual(
|
||||||
|
transformArguments('key', 'field', 1),
|
||||||
|
['HPEXPIRE', 'key', '1', 'FIELDS', '1', 'field']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('array', () => {
|
||||||
|
assert.deepEqual(
|
||||||
|
transformArguments('key', ['field1', 'field2'], 1),
|
||||||
|
['HPEXPIRE', 'key', '1', 'FIELDS', '2', 'field1', 'field2']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('with set option', () => {
|
||||||
|
assert.deepEqual(
|
||||||
|
transformArguments('key', ['field1'], 1, 'NX'),
|
||||||
|
['HPEXPIRE', 'key', '1', 'NX', 'FIELDS', '1', 'field1']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
testUtils.testWithClient('hexpire', async client => {
|
||||||
|
assert.deepEqual(
|
||||||
|
await client.hpExpire('key', ['field1'], 0),
|
||||||
|
[HASH_EXPIRATION_TIME.FIELD_NOT_EXISTS]
|
||||||
|
);
|
||||||
|
}, {
|
||||||
|
...GLOBAL.SERVERS.OPEN
|
||||||
|
});
|
||||||
|
});
|
24
packages/client/lib/commands/HPEXPIRE.ts
Normal file
24
packages/client/lib/commands/HPEXPIRE.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { RedisCommandArgument } from '.';
|
||||||
|
import { pushVerdictArgument } from './generic-transformers';
|
||||||
|
import { HashExpiration } from "./HEXPIRE";
|
||||||
|
|
||||||
|
export const FIRST_KEY_INDEX = 1;
|
||||||
|
|
||||||
|
export function transformArguments(
|
||||||
|
key: RedisCommandArgument,
|
||||||
|
fields: RedisCommandArgument | Array<RedisCommandArgument>,
|
||||||
|
ms: number,
|
||||||
|
mode?: 'NX' | 'XX' | 'GT' | 'LT',
|
||||||
|
) {
|
||||||
|
const args = ['HPEXPIRE', key, ms.toString()];
|
||||||
|
|
||||||
|
if (mode) {
|
||||||
|
args.push(mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
args.push('FIELDS')
|
||||||
|
|
||||||
|
return pushVerdictArgument(args, fields);
|
||||||
|
}
|
||||||
|
|
||||||
|
export declare function transformReply(): Array<HashExpiration> | null;
|
48
packages/client/lib/commands/HPEXPIREAT.spec.ts
Normal file
48
packages/client/lib/commands/HPEXPIREAT.spec.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { strict as assert } from 'node:assert';
|
||||||
|
import testUtils, { GLOBAL } from '../test-utils';
|
||||||
|
import { transformArguments } from './HPEXPIREAT';
|
||||||
|
import { HASH_EXPIRATION_TIME } from './HEXPIRETIME';
|
||||||
|
|
||||||
|
describe('HPEXPIREAT', () => {
|
||||||
|
testUtils.isVersionGreaterThanHook([7, 4]);
|
||||||
|
|
||||||
|
describe('transformArguments', () => {
|
||||||
|
it('string + number', () => {
|
||||||
|
assert.deepEqual(
|
||||||
|
transformArguments('key', 'field', 1),
|
||||||
|
['HPEXPIREAT', 'key', '1', 'FIELDS', '1', 'field']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('array + number', () => {
|
||||||
|
assert.deepEqual(
|
||||||
|
transformArguments('key', ['field1', 'field2'], 1),
|
||||||
|
['HPEXPIREAT', 'key', '1', 'FIELDS', '2', 'field1', 'field2']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('date', () => {
|
||||||
|
const d = new Date();
|
||||||
|
assert.deepEqual(
|
||||||
|
transformArguments('key', ['field1'], d),
|
||||||
|
['HPEXPIREAT', 'key', d.getTime().toString(), 'FIELDS', '1', 'field1']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('with set option', () => {
|
||||||
|
assert.deepEqual(
|
||||||
|
transformArguments('key', ['field1'], 1, 'XX'),
|
||||||
|
['HPEXPIREAT', 'key', '1', 'XX', 'FIELDS', '1', 'field1']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
testUtils.testWithClient('hpExpireAt', async client => {
|
||||||
|
assert.deepEqual(
|
||||||
|
await client.hpExpireAt('key', ['field1'], 1),
|
||||||
|
[HASH_EXPIRATION_TIME.FIELD_NOT_EXISTS]
|
||||||
|
);
|
||||||
|
}, {
|
||||||
|
...GLOBAL.SERVERS.OPEN,
|
||||||
|
});
|
||||||
|
});
|
25
packages/client/lib/commands/HPEXPIREAT.ts
Normal file
25
packages/client/lib/commands/HPEXPIREAT.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { RedisCommandArgument } from '.';
|
||||||
|
import { pushVerdictArgument, transformEXAT, transformPXAT } from './generic-transformers';
|
||||||
|
import { HashExpiration } from './HEXPIRE';
|
||||||
|
|
||||||
|
export const FIRST_KEY_INDEX = 1;
|
||||||
|
export const IS_READ_ONLY = true;
|
||||||
|
|
||||||
|
export function transformArguments(
|
||||||
|
key: RedisCommandArgument,
|
||||||
|
fields: RedisCommandArgument | Array<RedisCommandArgument>,
|
||||||
|
timestamp: number | Date,
|
||||||
|
mode?: 'NX' | 'XX' | 'GT' | 'LT'
|
||||||
|
) {
|
||||||
|
const args = ['HPEXPIREAT', key, transformPXAT(timestamp)];
|
||||||
|
|
||||||
|
if (mode) {
|
||||||
|
args.push(mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
args.push('FIELDS')
|
||||||
|
|
||||||
|
return pushVerdictArgument(args, fields);
|
||||||
|
}
|
||||||
|
|
||||||
|
export declare function transformReply(): Array<HashExpiration> | null;
|
33
packages/client/lib/commands/HPEXPIRETIME.spec.ts
Normal file
33
packages/client/lib/commands/HPEXPIRETIME.spec.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { strict as assert } from 'node:assert';
|
||||||
|
import testUtils, { GLOBAL } from '../test-utils';
|
||||||
|
import { transformArguments } from './HPEXPIRETIME';
|
||||||
|
import { HASH_EXPIRATION_TIME } from './HEXPIRETIME';
|
||||||
|
|
||||||
|
describe('HPEXPIRETIME', () => {
|
||||||
|
testUtils.isVersionGreaterThanHook([7, 4]);
|
||||||
|
|
||||||
|
describe('transformArguments', () => {
|
||||||
|
it('string', () => {
|
||||||
|
assert.deepEqual(
|
||||||
|
transformArguments('key', 'field'),
|
||||||
|
['HPEXPIRETIME', 'key', 'FIELDS', '1', 'field']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('array', () => {
|
||||||
|
assert.deepEqual(
|
||||||
|
transformArguments('key', ['field1', 'field2']),
|
||||||
|
['HPEXPIRETIME', 'key', 'FIELDS', '2', 'field1', 'field2']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
testUtils.testWithClient('hpExpireTime', async client => {
|
||||||
|
assert.deepEqual(
|
||||||
|
await client.hpExpireTime('key', 'field1'),
|
||||||
|
[HASH_EXPIRATION_TIME.FIELD_NOT_EXISTS]
|
||||||
|
);
|
||||||
|
}, {
|
||||||
|
...GLOBAL.SERVERS.OPEN
|
||||||
|
});
|
||||||
|
});
|
11
packages/client/lib/commands/HPEXPIRETIME.ts
Normal file
11
packages/client/lib/commands/HPEXPIRETIME.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { RedisCommandArgument } from '.';
|
||||||
|
import { pushVerdictArgument } from './generic-transformers';
|
||||||
|
|
||||||
|
export const FIRST_KEY_INDEX = 1;
|
||||||
|
export const IS_READ_ONLY = true;
|
||||||
|
|
||||||
|
export function transformArguments(key: RedisCommandArgument, fields: RedisCommandArgument | Array<RedisCommandArgument>) {
|
||||||
|
return pushVerdictArgument(['HPEXPIRETIME', key, 'FIELDS'], fields);
|
||||||
|
}
|
||||||
|
|
||||||
|
export declare function transformReply(): Array<number> | null;
|
33
packages/client/lib/commands/HPTTL.spec.ts
Normal file
33
packages/client/lib/commands/HPTTL.spec.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { strict as assert } from 'node:assert';
|
||||||
|
import testUtils, { GLOBAL } from '../test-utils';
|
||||||
|
import { transformArguments } from './HPTTL';
|
||||||
|
import { HASH_EXPIRATION_TIME } from './HEXPIRETIME';
|
||||||
|
|
||||||
|
describe('HPTTL', () => {
|
||||||
|
testUtils.isVersionGreaterThanHook([7, 4]);
|
||||||
|
|
||||||
|
describe('transformArguments', () => {
|
||||||
|
it('string', () => {
|
||||||
|
assert.deepEqual(
|
||||||
|
transformArguments('key', 'field'),
|
||||||
|
['HPTTL', 'key', 'FIELDS', '1', 'field']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('array', () => {
|
||||||
|
assert.deepEqual(
|
||||||
|
transformArguments('key', ['field1', 'field2']),
|
||||||
|
['HPTTL', 'key', 'FIELDS', '2', 'field1', 'field2']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
testUtils.testWithClient('hpTTL', async client => {
|
||||||
|
assert.deepEqual(
|
||||||
|
await client.hpTTL('key', 'field1'),
|
||||||
|
[HASH_EXPIRATION_TIME.FIELD_NOT_EXISTS]
|
||||||
|
);
|
||||||
|
}, {
|
||||||
|
...GLOBAL.SERVERS.OPEN
|
||||||
|
});
|
||||||
|
});
|
11
packages/client/lib/commands/HPTTL.ts
Normal file
11
packages/client/lib/commands/HPTTL.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { RedisCommandArgument } from '.';
|
||||||
|
import { pushVerdictArgument } from './generic-transformers';
|
||||||
|
|
||||||
|
export const FIRST_KEY_INDEX = 1;
|
||||||
|
export const IS_READ_ONLY = true;
|
||||||
|
|
||||||
|
export function transformArguments(key: RedisCommandArgument, fields: RedisCommandArgument | Array<RedisCommandArgument>) {
|
||||||
|
return pushVerdictArgument(['HPTTL', key, 'FIELDS'], fields);
|
||||||
|
}
|
||||||
|
|
||||||
|
export declare function transformReply(): Array<number> | null;
|
@@ -73,5 +73,18 @@ describe('HSCAN', () => {
|
|||||||
tuples: []
|
tuples: []
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
client.hSet('key', 'a', '1'),
|
||||||
|
client.hSet('key', 'b', '2')
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
await client.hScan('key', 0),
|
||||||
|
{
|
||||||
|
cursor: 0,
|
||||||
|
tuples: [{field: 'a', value: '1'}, {field: 'b', value: '2'}]
|
||||||
|
}
|
||||||
|
);
|
||||||
}, GLOBAL.SERVERS.OPEN);
|
}, GLOBAL.SERVERS.OPEN);
|
||||||
});
|
});
|
||||||
|
@@ -16,7 +16,7 @@ export function transformArguments(
|
|||||||
], cursor, options);
|
], cursor, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
type HScanRawReply = [RedisCommandArgument, Array<RedisCommandArgument>];
|
export type HScanRawReply = [RedisCommandArgument, Array<RedisCommandArgument>];
|
||||||
|
|
||||||
export interface HScanTuple {
|
export interface HScanTuple {
|
||||||
field: RedisCommandArgument;
|
field: RedisCommandArgument;
|
||||||
|
79
packages/client/lib/commands/HSCAN_NOVALUES.spec.ts
Normal file
79
packages/client/lib/commands/HSCAN_NOVALUES.spec.ts
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import { strict as assert } from 'assert';
|
||||||
|
import testUtils, { GLOBAL } from '../test-utils';
|
||||||
|
import { transformArguments, transformReply } from './HSCAN_NOVALUES';
|
||||||
|
|
||||||
|
describe('HSCAN_NOVALUES', () => {
|
||||||
|
testUtils.isVersionGreaterThanHook([7, 4]);
|
||||||
|
|
||||||
|
describe('transformArguments', () => {
|
||||||
|
it('cusror only', () => {
|
||||||
|
assert.deepEqual(
|
||||||
|
transformArguments('key', 0),
|
||||||
|
['HSCAN', 'key', '0', 'NOVALUES']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('with MATCH', () => {
|
||||||
|
assert.deepEqual(
|
||||||
|
transformArguments('key', 0, {
|
||||||
|
MATCH: 'pattern'
|
||||||
|
}),
|
||||||
|
['HSCAN', 'key', '0', 'MATCH', 'pattern', 'NOVALUES']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('with COUNT', () => {
|
||||||
|
assert.deepEqual(
|
||||||
|
transformArguments('key', 0, {
|
||||||
|
COUNT: 1
|
||||||
|
}),
|
||||||
|
['HSCAN', 'key', '0', 'COUNT', '1', 'NOVALUES']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('transformReply', () => {
|
||||||
|
it('without keys', () => {
|
||||||
|
assert.deepEqual(
|
||||||
|
transformReply(['0', []]),
|
||||||
|
{
|
||||||
|
cursor: 0,
|
||||||
|
keys: []
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('with keys', () => {
|
||||||
|
assert.deepEqual(
|
||||||
|
transformReply(['0', ['key1', 'key2']]),
|
||||||
|
{
|
||||||
|
cursor: 0,
|
||||||
|
keys: ['key1', 'key2']
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
testUtils.testWithClient('client.hScanNoValues', async client => {
|
||||||
|
assert.deepEqual(
|
||||||
|
await client.hScanNoValues('key', 0),
|
||||||
|
{
|
||||||
|
cursor: 0,
|
||||||
|
keys: []
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
client.hSet('key', 'a', '1'),
|
||||||
|
client.hSet('key', 'b', '2')
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
await client.hScanNoValues('key', 0),
|
||||||
|
{
|
||||||
|
cursor: 0,
|
||||||
|
keys: ['a', 'b']
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}, GLOBAL.SERVERS.OPEN);
|
||||||
|
});
|
27
packages/client/lib/commands/HSCAN_NOVALUES.ts
Normal file
27
packages/client/lib/commands/HSCAN_NOVALUES.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { RedisCommandArgument, RedisCommandArguments } from '.';
|
||||||
|
import { ScanOptions } from './generic-transformers';
|
||||||
|
import { HScanRawReply, transformArguments as transformHScanArguments } from './HSCAN';
|
||||||
|
|
||||||
|
export { FIRST_KEY_INDEX, IS_READ_ONLY } from './HSCAN';
|
||||||
|
|
||||||
|
export function transformArguments(
|
||||||
|
key: RedisCommandArgument,
|
||||||
|
cursor: number,
|
||||||
|
options?: ScanOptions
|
||||||
|
): RedisCommandArguments {
|
||||||
|
const args = transformHScanArguments(key, cursor, options);
|
||||||
|
args.push('NOVALUES');
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface HScanNoValuesReply {
|
||||||
|
cursor: number;
|
||||||
|
keys: Array<RedisCommandArgument>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function transformReply([cursor, rawData]: HScanRawReply): HScanNoValuesReply {
|
||||||
|
return {
|
||||||
|
cursor: Number(cursor),
|
||||||
|
keys: rawData
|
||||||
|
};
|
||||||
|
}
|
34
packages/client/lib/commands/HTTL.spec.ts
Normal file
34
packages/client/lib/commands/HTTL.spec.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { strict as assert } from 'node:assert';
|
||||||
|
import testUtils, { GLOBAL } from '../test-utils';
|
||||||
|
import { transformArguments } from './HTTL';
|
||||||
|
import { HASH_EXPIRATION_TIME } from './HEXPIRETIME';
|
||||||
|
|
||||||
|
describe('HTTL', () => {
|
||||||
|
testUtils.isVersionGreaterThanHook([7, 4]);
|
||||||
|
|
||||||
|
describe('transformArguments', () => {
|
||||||
|
it('string', () => {
|
||||||
|
assert.deepEqual(
|
||||||
|
transformArguments('key', 'field'),
|
||||||
|
['HTTL', 'key', 'FIELDS', '1', 'field']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('array', () => {
|
||||||
|
assert.deepEqual(
|
||||||
|
transformArguments('key', ['field1', 'field2']),
|
||||||
|
['HTTL', 'key', 'FIELDS', '2', 'field1', 'field2']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
testUtils.testWithClient('hTTL', async client => {
|
||||||
|
assert.deepEqual(
|
||||||
|
await client.hTTL('key', 'field1'),
|
||||||
|
[HASH_EXPIRATION_TIME.FIELD_NOT_EXISTS]
|
||||||
|
);
|
||||||
|
}, {
|
||||||
|
...GLOBAL.SERVERS.OPEN
|
||||||
|
});
|
||||||
|
});
|
11
packages/client/lib/commands/HTTL.ts
Normal file
11
packages/client/lib/commands/HTTL.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { RedisCommandArgument } from '.';
|
||||||
|
import { pushVerdictArgument } from './generic-transformers';
|
||||||
|
|
||||||
|
export const FIRST_KEY_INDEX = 1;
|
||||||
|
export const IS_READ_ONLY = true;
|
||||||
|
|
||||||
|
export function transformArguments(key: RedisCommandArgument, fields: RedisCommandArgument | Array<RedisCommandArgument>) {
|
||||||
|
return pushVerdictArgument(['HTTL', key, 'FIELDS'], fields);
|
||||||
|
}
|
||||||
|
|
||||||
|
export declare function transformReply(): Array<number> | null;
|
@@ -24,9 +24,5 @@ describe('LATENCY GRAPH', () => {
|
|||||||
typeof await client.latencyGraph('command'),
|
typeof await client.latencyGraph('command'),
|
||||||
'string'
|
'string'
|
||||||
);
|
);
|
||||||
}, {
|
}, GLOBAL.SERVERS.OPEN);
|
||||||
serverArguments: testUtils.isVersionGreaterThan([7]) ?
|
|
||||||
['--enable-debug-command', 'yes'] :
|
|
||||||
GLOBAL.SERVERS.OPEN.serverArguments
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
26
packages/client/lib/commands/LATENCY_HISTORY.spec.ts
Normal file
26
packages/client/lib/commands/LATENCY_HISTORY.spec.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import {strict as assert} from 'assert';
|
||||||
|
import testUtils, {GLOBAL} from '../test-utils';
|
||||||
|
import { transformArguments } from './LATENCY_HISTORY';
|
||||||
|
|
||||||
|
describe('LATENCY HISTORY', () => {
|
||||||
|
it('transformArguments', () => {
|
||||||
|
assert.deepEqual(
|
||||||
|
transformArguments('command'),
|
||||||
|
['LATENCY', 'HISTORY', 'command']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
testUtils.testWithClient('client.latencyHistory', async client => {
|
||||||
|
await Promise.all([
|
||||||
|
client.configSet('latency-monitor-threshold', '100'),
|
||||||
|
client.sendCommand(['DEBUG', 'SLEEP', '1'])
|
||||||
|
]);
|
||||||
|
|
||||||
|
const latencyHisRes = await client.latencyHistory('command');
|
||||||
|
assert.ok(Array.isArray(latencyHisRes));
|
||||||
|
for (const [timestamp, latency] of latencyHisRes) {
|
||||||
|
assert.equal(typeof timestamp, 'number');
|
||||||
|
assert.equal(typeof latency, 'number');
|
||||||
|
}
|
||||||
|
}, GLOBAL.SERVERS.OPEN);
|
||||||
|
});
|
27
packages/client/lib/commands/LATENCY_HISTORY.ts
Normal file
27
packages/client/lib/commands/LATENCY_HISTORY.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
export type EventType = (
|
||||||
|
'active-defrag-cycle' |
|
||||||
|
'aof-fsync-always' |
|
||||||
|
'aof-stat' |
|
||||||
|
'aof-rewrite-diff-write' |
|
||||||
|
'aof-rename' |
|
||||||
|
'aof-write' |
|
||||||
|
'aof-write-active-child' |
|
||||||
|
'aof-write-alone' |
|
||||||
|
'aof-write-pending-fsync' |
|
||||||
|
'command' |
|
||||||
|
'expire-cycle' |
|
||||||
|
'eviction-cycle' |
|
||||||
|
'eviction-del' |
|
||||||
|
'fast-command' |
|
||||||
|
'fork' |
|
||||||
|
'rdb-unlink-temp-file'
|
||||||
|
);
|
||||||
|
|
||||||
|
export function transformArguments(event: EventType) {
|
||||||
|
return ['LATENCY', 'HISTORY', event];
|
||||||
|
}
|
||||||
|
|
||||||
|
export declare function transformReply(): Array<[
|
||||||
|
timestamp: number,
|
||||||
|
latency: number,
|
||||||
|
]>;
|
27
packages/client/lib/commands/LATENCY_LATEST.spec.ts
Normal file
27
packages/client/lib/commands/LATENCY_LATEST.spec.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import {strict as assert} from 'assert';
|
||||||
|
import testUtils, {GLOBAL} from '../test-utils';
|
||||||
|
import { transformArguments } from './LATENCY_LATEST';
|
||||||
|
|
||||||
|
describe('LATENCY LATEST', () => {
|
||||||
|
it('transformArguments', () => {
|
||||||
|
assert.deepEqual(
|
||||||
|
transformArguments(),
|
||||||
|
['LATENCY', 'LATEST']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
testUtils.testWithClient('client.latencyLatest', async client => {
|
||||||
|
await Promise.all([
|
||||||
|
client.configSet('latency-monitor-threshold', '100'),
|
||||||
|
client.sendCommand(['DEBUG', 'SLEEP', '1'])
|
||||||
|
]);
|
||||||
|
const latency = await client.latencyLatest();
|
||||||
|
assert.ok(Array.isArray(latency));
|
||||||
|
for (const [name, timestamp, latestLatency, allTimeLatency] of latency) {
|
||||||
|
assert.equal(typeof name, 'string');
|
||||||
|
assert.equal(typeof timestamp, 'number');
|
||||||
|
assert.equal(typeof latestLatency, 'number');
|
||||||
|
assert.equal(typeof allTimeLatency, 'number');
|
||||||
|
}
|
||||||
|
}, GLOBAL.SERVERS.OPEN);
|
||||||
|
});
|
12
packages/client/lib/commands/LATENCY_LATEST.ts
Normal file
12
packages/client/lib/commands/LATENCY_LATEST.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { RedisCommandArguments } from '.';
|
||||||
|
|
||||||
|
export function transformArguments(): RedisCommandArguments {
|
||||||
|
return ['LATENCY', 'LATEST'];
|
||||||
|
}
|
||||||
|
|
||||||
|
export declare function transformReply(): Array<[
|
||||||
|
name: string,
|
||||||
|
timestamp: number,
|
||||||
|
latestLatency: number,
|
||||||
|
allTimeLatency: number
|
||||||
|
]>;
|
@@ -1,8 +1,24 @@
|
|||||||
import { strict as assert } from 'assert';
|
import { strict as assert } from 'assert';
|
||||||
import testUtils, { GLOBAL } from '../test-utils';
|
import testUtils, { GLOBAL } from '../test-utils';
|
||||||
import RedisClient from '../client';
|
import { transformArguments } from './PING';
|
||||||
|
|
||||||
describe('PING', () => {
|
describe('PING', () => {
|
||||||
|
describe('transformArguments', () => {
|
||||||
|
it('default', () => {
|
||||||
|
assert.deepEqual(
|
||||||
|
transformArguments(),
|
||||||
|
['PING']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('with message', () => {
|
||||||
|
assert.deepEqual(
|
||||||
|
transformArguments('message'),
|
||||||
|
['PING', 'message']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('client.ping', () => {
|
describe('client.ping', () => {
|
||||||
testUtils.testWithClient('string', async client => {
|
testUtils.testWithClient('string', async client => {
|
||||||
assert.equal(
|
assert.equal(
|
||||||
@@ -13,7 +29,7 @@ describe('PING', () => {
|
|||||||
|
|
||||||
testUtils.testWithClient('buffer', async client => {
|
testUtils.testWithClient('buffer', async client => {
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
await client.ping(RedisClient.commandOptions({ returnBuffers: true })),
|
await client.ping(client.commandOptions({ returnBuffers: true })),
|
||||||
Buffer.from('PONG')
|
Buffer.from('PONG')
|
||||||
);
|
);
|
||||||
}, GLOBAL.SERVERS.OPEN);
|
}, GLOBAL.SERVERS.OPEN);
|
||||||
|
@@ -1,7 +1,12 @@
|
|||||||
import { RedisCommandArgument } from '.';
|
import { RedisCommandArgument, RedisCommandArguments } from '.';
|
||||||
|
|
||||||
export function transformArguments(): Array<string> {
|
export function transformArguments(message?: RedisCommandArgument): RedisCommandArguments {
|
||||||
return ['PING'];
|
const args: RedisCommandArguments = ['PING'];
|
||||||
|
if (message) {
|
||||||
|
args.push(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return args;
|
||||||
}
|
}
|
||||||
|
|
||||||
export declare function transformReply(): RedisCommandArgument;
|
export declare function transformReply(): RedisCommandArgument;
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user