├── .github ├── FUNDING.yml └── ISSUE_TEMPLATE │ └── bug_report.md ├── .gitignore ├── .npmignore ├── .vscode └── settings.json ├── README.md ├── doc └── zh-CN.md ├── eng ├── .eslintrc.json ├── .nycrc.json ├── format.ts ├── gencmd.ts ├── husky │ └── pre-commit ├── tsconfig.dev.json └── tsconfig.prod.json ├── package-lock.json ├── package.json ├── src ├── client │ ├── baseClient.ts │ ├── client.v2.ts │ └── client.v3.ts ├── command │ ├── baseCommand.ts │ ├── command.v2.ts │ ├── command.v3.ts │ └── commands.json ├── index.ts ├── library │ ├── redisError.ts │ └── verbatimString.ts └── parser │ ├── baseParser.ts │ ├── parser.v2.ts │ └── parser.v3.ts ├── test ├── index.ts ├── parser.v2.test.ts └── parser.v3.test.ts └── tsconfig.json /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # pikadun 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: ['https://6672-free-8g398ef2c16f5d20-1255503321.tcb.qcloud.la/image/wx.png','https://6672-free-8g398ef2c16f5d20-1255503321.tcb.qcloud.la/image/zfb.jpg'] 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Environment 11 | 12 | + Node version: 13 | + Redis Server version: 14 | + Redis protocol version: 15 | + Camaro Redis version: 16 | 17 | ## Describe the bug 18 | 19 | A clear and concise description of what the bug is. 20 | 21 | ## To Reproduce 22 | 23 | Code to reproduce the behavior: 24 | 25 | ``` js 26 | 27 | ``` 28 | 29 | **Additional context** 30 | Add any other context about the problem here. 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Test result 2 | .nyc/ 3 | coverage/ 4 | 5 | # Modules 6 | node_modules/ 7 | 8 | # Compile output 9 | lib/ 10 | types/ 11 | 12 | # husky folder 13 | eng/_ 14 | eng/husky/.gitignore -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .github/ 2 | .nyc/ 3 | .vscode/ 4 | coverage/ 5 | eng/ 6 | src/ 7 | test/ 8 | 9 | .gitignore 10 | tsconfig.json -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Camaro Redis 2 | 3 | [![Azure DevOps builds](https://img.shields.io/azure-devops/build/camarojs/redis/1)](https://dev.azure.com/camarojs/redis/_build/latest?definitionId=1) 4 | [![Azure DevOps coverage](https://img.shields.io/azure-devops/coverage/camarojs/redis/1)](https://dev.azure.com/camarojs/redis/_build/latest?definitionId=1) 5 | [![npm](https://img.shields.io/npm/dm/@camaro/redis)](https://www.npmjs.com/package/@camaro/redis) 6 | [![GitHub stars](https://img.shields.io/github/stars/camarojs/redis)](https://github.com/camarojs/redis/stargazers) 7 | 8 | [中文文档说明](./doc/zh-CN.md) 9 | 10 | Redis client for node, support resp2/3, redis6, and all commands of redis. 11 | 12 | ## Features 13 | 14 | The first redis client to support [resp3](https://github.com/antirez/RESP3/blob/master/spec.md) . 15 | 16 | + All commands of redis support. 17 | + All command results return promise. 18 | + Support for ES6 types, such as Map and Set. 19 | + Support for TLS. 20 | 21 | ## Quick Start 22 | 23 | ### Install 24 | 25 | ```bash 26 | npm install @camaro/redis 27 | ``` 28 | 29 | ### Usage 30 | 31 | ```js 32 | // If you want to use resp2 ,change `ClientV3` to `ClientV2`. 33 | const { ClientV3: Client } = require('@camaro/redis') 34 | const client = new Client(); 35 | 36 | client.SET('foo', 'foo').then(() => { 37 | return client.GET('foo') 38 | }).then(reply => { 39 | console.log(reply) // 'foo' 40 | }) 41 | 42 | // Or if you want to use async/await. 43 | await client.SET('bar', 'bar') 44 | const reply = await client.GET('bar') 45 | console.log(reply) // 'bar' 46 | ``` 47 | 48 | ### Client Options 49 | 50 | | Option | Default | Description | 51 | | --- | --- | --- | 52 | | host | 127.0.0.1 | IP address of the redis server. | 53 | | port | 6379 | Port of the redis server. | 54 | | username | default | Username of redis client. | 55 | | password | undefined | Password of redis client. | 56 | | db | 0 | If set, client will run `SELECT` command on connect. | 57 | | reconnection | true | Whether to reconnect when an error occurs. | 58 | | logger | undefined | Provide a callback function to process commands log. | 59 | | tls | undefined | Object passed as options to [tls.connect()](https://nodejs.org/api/tls.html#tls_tls_connect_options_callback). | 60 | 61 | ## API Reference 62 | 63 | ### Client.Constructor 64 | 65 | The `Client` class has a constructor that can pass `Options`. 66 | 67 | ### Commands 68 | 69 | All redis command can be used like this: 70 | 71 | ```js 72 | const client = new Client(); 73 | client.SET(...args); 74 | client.GET(...args); 75 | // ... other commands 76 | ``` 77 | 78 | See the complete command list: [https://redis.io/commands](https://redis.io/commands) . 79 | 80 | ### Events 81 | 82 | + `message`: See [Pub/Sub](#Pub/Sub) 83 | + `error`: Emitted when a error occurs. 84 | + `connect`: Emitted when the client is established. Commands issued before the `connect` event are queued, then replayed just before this event is emitted. 85 | 86 | ### Pub/Sub 87 | 88 | You can receive `pub/sub` message by `message` event and handle it in the callback function. 89 | 90 | If you are using the resp2, you need to create a new client to receive messages. 91 | 92 | ```js 93 | const client = new Client(); 94 | client.SUBSCRIBE('test'); 95 | client.on('message', (data)=>{ 96 | // data: ['message','somechannel','this is the message'] 97 | console.log(data); 98 | }) 99 | ``` 100 | -------------------------------------------------------------------------------- /doc/zh-CN.md: -------------------------------------------------------------------------------- 1 | # Camaro Redis 文档 2 | 3 | ## 快速开始 4 | 5 | ### 安装 6 | 7 | ```bash 8 | npm install @camaro/redis 9 | ``` 10 | 11 | ### 使用 12 | 13 | ```js 14 | // 如果你想使用resp2,将`ClientV3`改为`ClientV2` 15 | const { ClientV3: Client } = require('@camaro/redis') 16 | const client = new Client(); 17 | 18 | client.SET('foo', 'foo').then(() => { 19 | return client.GET('foo') 20 | }).then(reply => { 21 | console.log(reply) // 'foo' 22 | }) 23 | 24 | // 或者使用 async/await 。 25 | await client.SET('bar', 'bar') 26 | const reply = await client.GET('bar') 27 | console.log(reply) // 'bar' 28 | ``` 29 | 30 | ### 客户端选项 31 | 32 | | 选项 | 默认值 | 说明 | 33 | | --- | --- | --- | 34 | | host | 127.0.0.1 | redis服务器的地址。 | 35 | | port | 6379 | redis服务器的端口。 | 36 | | username | default | 连接服务器使用的用户名。 | 37 | | password | null | 连接服务器使用的密码。 | 38 | | db | 0 | 如果设置了,会执行 `SELECT` 命令切换到对应的数据库。 | 39 | | reconnection | true | 发生错误时是否重新连接。 | 40 | | logger | undefined | 用来记录命令执行日志的函数。 | 41 | | tls | undefined | 传递给 [tls.connect()](https://nodejs.org/api/tls.html#tls_tls_connect_options_callback) 作为 `options` 的对象。 | 42 | 43 | ## API 说明 44 | 45 | ### Client 构造函数 46 | 47 | 类 `Client` 可以使用提供 `Options`参数 的构造函数。 48 | 49 | ### redis 命令 50 | 51 | 所有的 redis 命令都可以这样使用: 52 | 53 | ```js 54 | const client = new Client(); 55 | client.SET(...args); 56 | client.GET(...args); 57 | // ... other commands 58 | ``` 59 | 60 | 查看完整 redis 命令列表: [https://redis.io/commands](https://redis.io/commands)。 61 | 62 | ### Events 63 | 64 | + `message`: 查看 [Pub/Sub](#Pub/Sub)。 65 | + `error`: 发生连接错误时触发。 66 | + `connect`: 成功建立socket连接时触发。 67 | 68 | ### Pub/Sub 69 | 70 | 可以通过注册 `message` 事件,接收来自 `pub/sub` 的消息。 71 | 72 | 如果你使用 resp2,需要实例化一个新的客户端来接收消息。 73 | 74 | ```js 75 | const client = new Client(); 76 | client.SUBSCRIBE('test'); 77 | client.on('message', (data)=>{ 78 | // data: ['message','somechannel','this is the message'] 79 | console.log(data); 80 | }) 81 | ``` 82 | -------------------------------------------------------------------------------- /eng/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "plugins": [ 4 | "@typescript-eslint" 5 | ], 6 | "extends": [ 7 | "eslint:recommended", 8 | "plugin:@typescript-eslint/recommended" 9 | ], 10 | "rules": { 11 | "@typescript-eslint/quotes": [ 12 | "error", 13 | "single" 14 | ], 15 | "@typescript-eslint/semi": [ 16 | "error", 17 | "always" 18 | ], 19 | "@typescript-eslint/indent": [ 20 | "error", 21 | 4 22 | ], 23 | "prefer-const": [ 24 | "error", 25 | { 26 | "destructuring": "all" 27 | } 28 | ] 29 | } 30 | } -------------------------------------------------------------------------------- /eng/.nycrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extension": [ 3 | ".ts" 4 | ], 5 | "check-coverage": true, 6 | "include": [ 7 | "src/**" 8 | ], 9 | "reporter": [ 10 | "text", 11 | "text-summary", 12 | "cobertura" 13 | ], 14 | "statements": 70, 15 | "branches": 70, 16 | "functions": 75, 17 | "lines": 70, 18 | "temp-dir": "./.nyc" 19 | } -------------------------------------------------------------------------------- /eng/format.ts: -------------------------------------------------------------------------------- 1 | import { execSync } from 'child_process'; 2 | 3 | // 获取全部暂存区的文件,并对暂存区文件进行lint验证 4 | const stagedFiles = execSync('git diff --cached --name-only --diff-filter=ACM').toString().split('\n'); 5 | let pass = true; 6 | const result = []; 7 | for (let i = 0; i < stagedFiles.length; i++) { 8 | const file = stagedFiles[i]; 9 | if (file && file.match(/.ts$/)) { 10 | try { 11 | const res = execSync(`eslint --fix ${file}`); 12 | execSync(`git add ${file}`); 13 | if (res.length > 0) { 14 | throw Error(); 15 | } else { 16 | console.log(`Fix ${file} success.`); 17 | } 18 | } catch (e) { 19 | pass = false; 20 | result.push(`Eslint fix '${file}' failed!\n`); 21 | result.push(`Use eslint --fix '${file}' to show detail.\n`); 22 | } 23 | } 24 | } 25 | 26 | console.log('\n\x1B[31m%s\x1B[0m', result.join('')); 27 | 28 | if (!pass) { 29 | process.exit(1); 30 | } 31 | process.exit(0); 32 | -------------------------------------------------------------------------------- /eng/gencmd.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync } from 'fs'; 2 | import { writeFileSync } from 'fs'; 3 | import { resolve } from 'path'; 4 | import { ClientV3 } from '../'; 5 | 6 | const jsonPath = resolve(__dirname, '..', 'src', 'command', 'commands.json'); 7 | const dtsPath = resolve(__dirname, '..', 'src', 'command', 'baseCommand.ts'); 8 | const dtsPathV2 = resolve(__dirname, '..', 'src', 'command', 'command.v2.ts'); 9 | const dtsPathV3 = resolve(__dirname, '..', 'src', 'command', 'command.v3.ts'); 10 | 11 | const dtsBase = readFileSync(dtsPath).toString(); 12 | const dtsV2 = readFileSync(dtsPathV2).toString(); 13 | const dtsV3 = readFileSync(dtsPathV3).toString(); 14 | 15 | const redis = new ClientV3(); 16 | 17 | const notInCommands = ['zscan', 'quit']; 18 | 19 | async function dodo() { 20 | const redisCommands = await redis.COMMAND<[string][]>(); 21 | const commands = redisCommands.filter(rc => rc instanceof Array).map(rc => rc[0]); 22 | commands.push(...notInCommands); 23 | commands.sort(); 24 | writeFileSync(jsonPath, JSON.stringify(commands, undefined, '\u0020\u0020\u0020\u0020')); 25 | 26 | commands.forEach(cmdText => { 27 | checkDTS(cmdText); 28 | }); 29 | process.exit(0); 30 | } 31 | 32 | function checkDTS(cmdText: string) { 33 | const reg = new RegExp(`${cmdText.toUpperCase()}()?(.*)`); 34 | if (!reg.test(dtsBase)) { 35 | if (!reg.test(dtsV2) || !reg.test(dtsV3)) { 36 | console.log(`Command '${cmdText}' is not defined in dts.`); 37 | } 38 | } 39 | } 40 | 41 | dodo(); -------------------------------------------------------------------------------- /eng/husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npm run format 5 | npm run coverage -------------------------------------------------------------------------------- /eng/tsconfig.dev.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "inlineSourceMap": true 5 | } 6 | } -------------------------------------------------------------------------------- /eng/tsconfig.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json" 3 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@camaro/redis", 3 | "version": "2.4.1", 4 | "main": "lib/index.js", 5 | "description": "Redis client for node, support resp3 and redis6.", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/camarojs/redis" 9 | }, 10 | "license": "MIT", 11 | "scripts": { 12 | "prepare": "husky install ./eng/husky", 13 | "test": "ts-node test/index.ts", 14 | "coverage": "nyc --nycrc-path=./eng/.nycrc.json npm test", 15 | "lint": "eslint '**/*.ts'", 16 | "format": "ts-node ./eng/format.ts", 17 | "watch": "tsc --build ./eng/tsconfig.dev.json --watch", 18 | "compile": "tsc --build ./eng/tsconfig.prod.json", 19 | "gencmd": "ts-node ./eng/gencmd.ts" 20 | }, 21 | "keywords": [], 22 | "types": "./types/index.d.ts", 23 | "eslintConfig": { 24 | "extends": "./eng/.eslintrc.json" 25 | }, 26 | "devDependencies": { 27 | "@types/mocha": "^8.2.0", 28 | "@types/node": "^15.0.2", 29 | "@typescript-eslint/eslint-plugin": "^4.11.1", 30 | "@typescript-eslint/parser": "^4.11.1", 31 | "eslint": "^7.16.0", 32 | "husky": "^6.0.0", 33 | "mocha": "^8.2.1", 34 | "nyc": "^15.1.0", 35 | "ts-node": "^10.0.0", 36 | "typescript": "^4.1.3" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/client/baseClient.ts: -------------------------------------------------------------------------------- 1 | import net, { NetConnectOpts, Socket } from 'net'; 2 | import tls, { SecureContextOptions, TLSSocket } from 'tls'; 3 | import commands from '../command/commands.json'; 4 | import ParserV2 from '../parser/parser.v2'; 5 | import ParserV3 from '../parser/parser.v3'; 6 | 7 | export interface IClientOptions { 8 | host?: string; 9 | port?: number; 10 | db?: number; 11 | username?: string; 12 | password?: string; 13 | reconnection?: boolean; 14 | tls?: SecureContextOptions; 15 | logger?: (err: Error, reply: unknown, command: string, args?: string[]) => void 16 | } 17 | 18 | export type ProtoVer = 2 | 3; 19 | 20 | declare module 'net' { 21 | interface Socket { 22 | pending: boolean 23 | } 24 | } 25 | 26 | export abstract class BaseClient { 27 | private socket!: Socket | TLSSocket; 28 | private parser: ParserV2 | ParserV3; 29 | /** Store the command that executed before connect */ 30 | private queue: string[] = []; 31 | options: IClientOptions; 32 | private connectionOpts: NetConnectOpts; 33 | private attempts = 0; 34 | constructor(options: IClientOptions, protover: ProtoVer) { 35 | this.parser = protover === 3 ? new ParserV3() : new ParserV2(); 36 | commands.forEach(command => { this.addCommand(command); }); 37 | this.options = this.initOptions(options); 38 | this.connectionOpts = { 39 | port: this.options.port as number, 40 | host: this.options.host 41 | }; 42 | Object.assign(this.connectionOpts, this.options.tls); 43 | 44 | // Ignore HostName Mismatch 45 | if (this.options.host === '127.0.0.1') { 46 | Object.assign(this.connectionOpts, { rejectUnauthorized: false }); 47 | } 48 | 49 | this.connect(); 50 | } 51 | 52 | private connect() { 53 | this.socket = this.options.tls 54 | ? tls.connect(this.connectionOpts) 55 | : net.createConnection(this.connectionOpts); 56 | this.socket.setKeepAlive(true); 57 | 58 | const connectEventName = this.options.tls ? 'secureConnect' : 'connect'; 59 | this.socket.on(connectEventName, async () => { 60 | this.attempts = 0; 61 | this.queue.forEach(elem => { 62 | this.socket.write(elem); 63 | }); 64 | }); 65 | 66 | this.socket.on('data', (data) => { 67 | this.parser.decodeReply(data); 68 | }); 69 | 70 | this.socket.on('error', err => { 71 | if (this.handleError) { 72 | this.handleError(err); 73 | } else { 74 | console.error(err); 75 | } 76 | }); 77 | 78 | this.socket.on('close', (hadError) => { 79 | /** 80 | * In addition to actively disconnecting the client or server, 81 | * it will automatically reconnect 82 | */ 83 | if (hadError && this.options.reconnection) { 84 | this.reconnect(); 85 | } 86 | }); 87 | 88 | this.init().then(() => { 89 | this.handleConnect?.(); 90 | }).catch(error => { 91 | if (this.handleError) { 92 | this.handleError(error); 93 | } else { 94 | throw error; 95 | } 96 | }); 97 | } 98 | 99 | abstract init(): Promise; 100 | 101 | private reconnect(): void { 102 | this.attempts++; 103 | setTimeout(() => { 104 | this.socket.destroy(); 105 | this.socket.unref(); 106 | this.connect(); 107 | }, Math.min(this.attempts * this.attempts * 100, 15000)); 108 | } 109 | 110 | private handleConnect?: () => void; 111 | private handleError?: (err: Error) => void; 112 | 113 | private initOptions(options: IClientOptions): IClientOptions { 114 | const cloneOptions: IClientOptions = {}; 115 | cloneOptions.host = options.host || '127.0.0.1'; 116 | cloneOptions.port = options.port || 6379; 117 | cloneOptions.db = options.db || 0; 118 | cloneOptions.username = options.username || 'default'; 119 | cloneOptions.reconnection = options.reconnection !== false; 120 | cloneOptions.password = options.password; 121 | cloneOptions.logger = options.logger; 122 | if (options.tls) { 123 | cloneOptions.tls = {}; 124 | Object.assign(cloneOptions.tls, options.tls); 125 | } 126 | return cloneOptions; 127 | } 128 | 129 | private addCommand(command: string): void { 130 | const fn = async (...args: string[]) => { 131 | return this.runCommand(command, args); 132 | }; 133 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 134 | (this as any)[command] = (this as any)[command.toUpperCase()] = fn; 135 | } 136 | 137 | private runCommand(command: string, args?: string[]) { 138 | args = args?.map(arg => '' + arg); 139 | 140 | // TODO: MONITOR commands 141 | return new Promise((resolve, reject) => { 142 | this.parser.callbacks.push((err, reply) => { 143 | this.options.logger?.(err as Error, reply, command, args); 144 | err ? reject(err) : resolve(reply); 145 | }); 146 | const buffer = this.parser.encodeCommand(command, args); 147 | if (this.socket.pending) { 148 | this.queue.push(buffer); 149 | } else { 150 | this.socket.write(buffer); 151 | } 152 | }); 153 | } 154 | 155 | public on(event: 'message' | 'error' | 'connect', listener: (data?: unknown) => void): void { 156 | switch (event) { 157 | case 'message': 158 | this.parser.on(event, listener); 159 | break; 160 | case 'error': 161 | this.handleError = listener; 162 | break; 163 | case 'connect': 164 | this.handleConnect = listener; 165 | } 166 | } 167 | } -------------------------------------------------------------------------------- /src/client/client.v2.ts: -------------------------------------------------------------------------------- 1 | import { IClientCommand } from '../command/command.v2'; 2 | import { BaseClient, IClientOptions } from './baseClient'; 3 | 4 | export interface Client extends IClientCommand { 5 | options: IClientOptions 6 | } 7 | 8 | export class Client extends BaseClient { 9 | constructor(options: IClientOptions = {}) { 10 | super(options, 2); 11 | } 12 | 13 | async init(): Promise { 14 | if (this.options.password) { 15 | await this.AUTH(this.options.password); 16 | } 17 | await this.SELECT(this.options.db as number); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/client/client.v3.ts: -------------------------------------------------------------------------------- 1 | import { IClientCommand } from '../command/command.v2'; 2 | import { BaseClient, IClientOptions } from './baseClient'; 3 | 4 | export interface Client extends IClientCommand { 5 | options: IClientOptions 6 | } 7 | 8 | export class Client extends BaseClient { 9 | constructor(options: IClientOptions = {}) { 10 | super(options, 3); 11 | } 12 | 13 | async init(): Promise { 14 | if (this.options.password) { 15 | await this.HELLO(3, 'auth', this.options.username as string, this.options.password); 16 | } else { 17 | await this.HELLO(3); 18 | } 19 | await this.SELECT(this.options.db as number); 20 | } 21 | } -------------------------------------------------------------------------------- /src/command/baseCommand.ts: -------------------------------------------------------------------------------- 1 | export interface IBaseCommand { 2 | ACL(subcommand: 'load'): Promise; 3 | ACL(subcommand: 'save'): Promise; 4 | ACL(subcommand: 'list'): Promise; 5 | ACL(subcommand: 'users'): Promise; 6 | ACL(subcommand: 'getuser', username: string): Promise; 7 | ACL(subcommand: 'setuser', username: string, ...rule: string[]): Promise; 8 | ACL(subcommand: 'deluser', ...username: string[]): Promise; 9 | ACL(subcommand: 'cat', categoryname?: string): Promise; 10 | ACL(subcommand: 'genpass', bits?: number): Promise; 11 | ACL(subcommand: 'whoami'): Promise; 12 | ACL(subcommand: 'log', count: number): Promise; 13 | ACL(subcommand: 'log', reset: 'reset'): Promise; 14 | ACL(subcommand: 'help'): Promise; 15 | 16 | APPEND(key: string, value: string): Promise; 17 | 18 | AUTH(password: string): Promise; 19 | AUTH(username: string, password: string): Promise; 20 | 21 | BGREWRITEAOF(): Promise; 22 | 23 | BGSAVE(schedule?: unknown): Promise; 24 | 25 | BITCOUNT(key: string, start?: number, end?: number): Promise; 26 | 27 | BITFIELD(key: string, subcommand: 'get', type: string, offset: number): Promise; 28 | BITFIELD(key: string, subcommand: 'set', type: string, offset: number, value: string): Promise; 29 | BITFIELD(key: string, subcommand: 'incrby', type: string, offset: number, increment: string): Promise; 30 | BITFIELD(key: string, subcommand: 'overflow', behavior: 'wrap' | 'sat' | 'fail'): Promise; 31 | BITFIELD_RO(...arg: string[]): Promise; 32 | 33 | BITOP(operation: 'and' | 'or' | 'xor' | 'not', destkey: 'destkey', ...key: string[]): Promise; 34 | 35 | BITPOS(key: string, bit: number, start?: number, end?: number): Promise; 36 | 37 | BLPOP(...arg: [...T, number]): Promise; 38 | 39 | BRPOP(...arg: [...T, number]): Promise; 40 | 41 | BRPOPLPUSH(source: string, destination: string, timeout: number): Promise; 42 | 43 | BLMOVE(source: string, destination: string, whereto: 'left' | 'right', wherefrom: 'left' | 'right', timeout: number): Promise 44 | 45 | BZPOPMIN(...arg: [...T, number]): Promise; 46 | 47 | BZPOPMAX(...arg: [...T, number]): Promise; 48 | 49 | CLIENT(subcommand: 'caching', mode: 'yes' | 'no'): Promise; 50 | CLIENT(subcommand: 'id'): Promise; 51 | CLIENT(subcommand: 'info'): Promise; 52 | CLIENT(subcommand: 'kill', ip: string, port: number): Promise; 53 | CLIENT(subcommand: 'kill', way: 'id', clientId: number): Promise; 54 | CLIENT(subcommand: 'kill', way: 'type', type: 'normal' | 'master' | 'slave' | 'pubsub'): Promise; 55 | CLIENT(subcommand: 'kill', way: 'user', username: string): Promise; 56 | CLIENT(subcommand: 'kill', way: 'addr', ipport: string): Promise; 57 | CLIENT(subcommand: 'kill', way: 'skipme', mode: 'yes' | 'no'): Promise; 58 | CLIENT(subcommand: 'list', way: 'type', type: 'normal' | 'master' | 'slave' | 'pubsub'): Promise; 59 | CLIENT(subcommand: 'list', way: 'id', clientId: number[]): Promise; 60 | CLIENT(subcommand: 'getname'): Promise; 61 | CLIENT(subcommand: 'getredir'): Promise; 62 | CLIENT(subcommand: 'unpause'): Promise; 63 | CLIENT(subcommand: 'pause', timeout: number, mode: 'write' | 'all'): Promise; 64 | CLIENT(subcommand: 'reply', mode: 'on' | 'off' | 'skip'): Promise; 65 | CLIENT(subcommand: 'setname', name: string): Promise; 66 | CLIENT(subcommand: 'tracking', mode: 'on' | 'off', ...arg: string[]): Promise; 67 | CLIENT(subcommand: 'trackinginfo'): Promise; 68 | CLIENT(subcommand: 'unblock', clientId: number, behavior: 'timeout' | 'error'): Promise; 69 | 70 | CLUSTER(subcommand: 'addslots', ...slot: string[]): Promise; 71 | CLUSTER(subcommand: 'bumpepoch'): Promise; 72 | CLUSTER(subcommand: 'count-failure-reports', nodeId: string): Promise; 73 | CLUSTER(subcommand: 'countkeysinslot'): Promise; 74 | CLUSTER(subcommand: 'delslots', ...slot: string[]): Promise; 75 | CLUSTER(subcommand: 'failover', option: 'force' | 'takeover'): Promise; 76 | CLUSTER(subcommand: 'flushslots'): Promise; 77 | CLUSTER(subcommand: 'forget', nodeId: string): Promise; 78 | CLUSTER(subcommand: 'getkeysinslot', slot: string, count: number): Promise; 79 | CLUSTER(subcommand: 'info'): Promise; 80 | CLUSTER(subcommand: 'keyslot', key: string): Promise; 81 | CLUSTER(subcommand: 'meet', ip: string, port: number): Promise; 82 | CLUSTER(subcommand: 'myid'): Promise; 83 | CLUSTER(subcommand: 'nodes'): Promise; 84 | CLUSTER(subcommand: 'replicate', nodeId: string): Promise; 85 | CLUSTER(subcommand: 'reset', type: 'hard' | 'soft'): Promise; 86 | CLUSTER(subcommand: 'saveconfig'): Promise; 87 | CLUSTER(subcommand: 'set-config-epoch'): Promise; 88 | CLUSTER(subcommand1: 'setslot', slot: string, subcommand2: 'importing' | 'migrating' | 'stable' | 'node', nodeId?: string): Promise; 89 | CLUSTER(subcommand: 'slaves', nodeId: string): Promise; 90 | CLUSTER(subcommand: 'replicas', nodeId: string): Promise; 91 | CLUSTER(subcommand: 'slots'): Promise; 92 | 93 | COMMAND(): Promise; 94 | COMMAND(subcommand: 'count'): Promise; 95 | COMMAND(subcommand: 'getkeys'): Promise; 96 | COMMAND(subcommand: 'info', ...name: string[]): Promise; 97 | 98 | CONFIG(subcommand: 'get', parameter: string): Promise; 99 | CONFIG(subcommand: 'rewrite'): Promise; 100 | CONFIG(subcommand: 'set', parameter: string, value: string): Promise; 101 | CONFIG(subcommand: 'resetstat'): Promise; 102 | 103 | COPY(source: string, destination: string, ...arg: string[]): Promise; 104 | 105 | DBSIZE(): Promise; 106 | 107 | DEBUG(subcommand: 'object', key: string): Promise; 108 | DEBUG(subcommand: 'segfault'): Promise; 109 | 110 | DECR(key: string): Promise; 111 | 112 | DECRBY(key: string, decrement: number): Promise; 113 | 114 | DEL(...key: string[]): Promise; 115 | 116 | DISCARD(): Promise; 117 | 118 | DUMP(key: string): Promise; 119 | 120 | ECHO(message: string): Promise; 121 | 122 | EVAL(script: string, numkeys: number, ...arg: string[]): Promise; 123 | 124 | EVALSHA(sha1: string, numkeys: number, ...arg: string[]): Promise; 125 | 126 | EXEC(): Promise; 127 | 128 | EXISTS(...key: string[]): Promise; 129 | 130 | EXPIRE(key: string, seconds: number): Promise; 131 | 132 | EXPIREAT(key: string, timestamp: number): Promise; 133 | 134 | FAILOVER(subcommand: 'to', host: string, port: number, force?: boolean): Promise; 135 | FAILOVER(subcommand: 'abort'): Promise; 136 | FAILOVER(subcommand: 'timeout', milliseconds: number): Promise; 137 | 138 | FLUSHALL(mode?: 'async' | 'sync'): Promise; 139 | 140 | FLUSHDB(mode?: 'async' | 'sync'): Promise; 141 | 142 | GEOADD(key: string, ...arg: string[]): Promise; 143 | 144 | GEOHASH(key: string, ...member: string[]): Promise; 145 | 146 | GEOPOS(key: string, ...member: string[]): Promise; 147 | 148 | GEODIST(key: string, member1: string, member2: string, unit: 'm' | 'km' | 'mi' | 'ft'): Promise; 149 | 150 | GEORADIUS(...arg: string[]): Promise; 151 | GEORADIUS_RO(...arg: string[]): Promise; 152 | 153 | GEORADIUSBYMEMBER(...arg: string[]): Promise; 154 | GEORADIUSBYMEMBER_RO(...arg: string[]): Promise; 155 | 156 | GEOSEARCH(...arg: string[]): Promise; 157 | 158 | GEOSEARCHSTORE(...arg: string[]): Promise; 159 | 160 | GET(key: string): Promise; 161 | 162 | GETBIT(key: string, offset: number): Promise; 163 | 164 | GETDEL(key: string): Promise; 165 | 166 | GETEX(key: string, option: 'ex', seconds: number): Promise; 167 | GETEX(key: string, option: 'px', milliseconds: number): Promise; 168 | GETEX(key: string, option: 'exat', timestamp: number): Promise; 169 | GETEX(key: string, option: 'pxat', millisecondsTimestamp: number): Promise; 170 | GETEX(key: string, option: 'persist'): Promise; 171 | 172 | GETRANGE(key: string, start: number, end: number): Promise; 173 | 174 | GETSET(key: string, value: string): Promise; 175 | 176 | HDEL(key: string, ...field: string[]): Promise; 177 | 178 | HELLO(protover: number): Promise; 179 | HELLO(protover: number, option: 'auth', password: string): Promise; 180 | HELLO(protover: number, option: 'auth', username: string, password: string): Promise; 181 | HELLO(protover: number, option: 'setname', clientname: string): Promise; 182 | 183 | HEXISTS(key: string, field: string): Promise; 184 | 185 | HGET(key: string, field: string): Promise; 186 | 187 | HINCRBY(key: string, field: string, increment: number): Promise; 188 | 189 | HINCRBYFLOAT(key: string, field: string, increment: number): Promise; 190 | 191 | HKEYS(key: string): Promise; 192 | 193 | HLEN(key: string): Promise; 194 | 195 | HMGET(key: string, ...field: string[]): Promise; 196 | 197 | HMSET(key: string, ...fieldValue: string[]): Promise; 198 | 199 | HSET(key: string, ...fieldValue: string[]): Promise; 200 | 201 | HSETNX(key: string, field: string, value: string): Promise; 202 | 203 | HRANDFIELD(key: string): Promise; 204 | HRANDFIELD(key: string, count: number, withvalues?: 'withvalues'): Promise; 205 | 206 | HSCAN(key: string, cursor: number): Promise<[string, string[]]>; 207 | HSCAN(key: string, cursor: number, option1: 'match', pattern: string): Promise<[string, string[]]>; 208 | HSCAN(key: string, cursor: number, option1: 'count', count: number): Promise<[string, string[]]>; 209 | HSCAN(key: string, cursor: number, option1: 'match', pattern: string, option2: 'count', count: number): Promise<[string, string[]]>; 210 | 211 | HSTRLEN(key: string, field: string): Promise; 212 | 213 | HVALS(key: string, field: string): Promise; 214 | 215 | INCR(key: string, field: string): Promise; 216 | 217 | INCRBY(key: string, field: string, increment: string): Promise; 218 | 219 | INCRBYFLOAT(key: string, field: string, increment: string): Promise; 220 | 221 | INFO( 222 | section?: 'all' | 'default' | 'everything' | 'server' | 'clients' | 'memory' 223 | | 'persistence' | 'stats' | 'replication' | 'cpu' | 'commandstats' | 'cluster' 224 | | 'modules' | 'keyspace' | 'errorstats' 225 | ): Promise; 226 | 227 | KEYS(pattern: string): Promise; 228 | 229 | LATENCY(subcommand: 'doctor'): Promise; 230 | LATENCY(subcommand: 'graph', event: string): Promise; 231 | LATENCY(subcommand: 'history', event: string): Promise<[number, number][]>; 232 | LATENCY(subcommand: 'latest', event: string): Promise; 233 | LATENCY(subcommand: 'reset', ...event: string[]): Promise; 234 | LATENCY(subcommand: 'help'): Promise; 235 | 236 | LOLWUT(): Promise; 237 | LOLWUT(subcommand: 'version', version: number): Promise; 238 | 239 | LASTSAVE(): Promise; 240 | 241 | LINDEX(key: string, index: number): Promise; 242 | 243 | LINSERT(key: string, pos: 'before' | 'after', pivot: string, element: string): Promise; 244 | 245 | LLEN(key: string): Promise; 246 | 247 | LMOVE(source: string, destination: string, whereto: 'left' | 'right', wherefrom: 'left' | 'right'): Promise 248 | 249 | LPOP(key: string): Promise; 250 | 251 | LPOS(key: string, element: string, ...arg: string[]): Promise; 252 | 253 | LPUSH(key: string, ...element: string[]): Promise; 254 | 255 | LPUSHX(key: string, ...element: string[]): Promise; 256 | 257 | LRANGE(key: string, start: number, end: number): Promise; 258 | 259 | LREM(key: string, count: number, element: string): Promise; 260 | 261 | LSET(key: string, index: number, element: string): Promise; 262 | 263 | LTRIM(key: string, start: number, stop: number): Promise; 264 | 265 | MEMORY(subcommand: 'doctor'): Promise; 266 | MEMORY(subcommand: 'help'): Promise; 267 | MEMORY(subcommand: 'malloc-stats'): Promise; 268 | MEMORY(subcommand: 'purge'): Promise; 269 | MEMORY(subcommand: 'stats'): Promise; 270 | MEMORY(subcommand: 'usage', key: string): Promise; 271 | MEMORY(subcommand: 'usage', key: string, option: 'samples', count: number): Promise; 272 | 273 | MGET(...key: string[]): Promise; 274 | 275 | MIGRATE(...arg: string[]): Promise; 276 | 277 | MODULE(subcommand: 'list'): Promise<{ name: string, ver: string }[]>; 278 | MODULE(subcommand: 'load', path: string, ...arg: string[]): Promise; 279 | MODULE(subcommand: 'unload', name: string): Promise; 280 | 281 | MONITOR(): Promise; 282 | 283 | MOVE(key: string, db: number): Promise; 284 | 285 | MSET(key: string, value: string, ...arg: string[]): Promise; 286 | 287 | MSETNX(key: string, value: string, ...arg: string[]): Promise; 288 | 289 | MULTI(): Promise; 290 | 291 | OBJECT(subcommand: 'refcount', key: string): Promise; 292 | OBJECT(subcommand: 'encoding', key: string): Promise; 293 | OBJECT(subcommand: 'idletime', key: string): Promise; 294 | OBJECT(subcommand: 'freq', key: string): Promise; 295 | OBJECT(subcommand: 'help'): Promise; 296 | 297 | PERSIST(key: string): Promise; 298 | 299 | PEXPIRE(key: string, milliseconds: number): Promise; 300 | 301 | PEXPIREAT(key: string, millisecondsTimestamp: number): Promise; 302 | 303 | PFADD(key: string, ...element: string[]): Promise; 304 | 305 | PFCOUNT(...key: string[]): Promise; 306 | 307 | PFMERGE(destkey: string, ...sourcekey: string[]): Promise; 308 | 309 | PING(message?: string): Promise; 310 | 311 | PSETEX(key: string, milliseconds: number, value: string): Promise; 312 | 313 | PSUBSCRIBE(...pattern: string[]): Promise; 314 | 315 | PUBSUB(subcommand: 'channels', pattern?: string): Promise; 316 | PUBSUB(subcommand: 'numsub', ...channel: string[]): Promise; 317 | PUBSUB(subcommand: 'numpat'): Promise; 318 | 319 | PTTL(key: string): Promise; 320 | 321 | PUBLISH(channel: string, message: string): Promise; 322 | 323 | PUNSUBSCRIBE(...pattern: string[]): Promise; 324 | 325 | QUIT(): Promise; 326 | 327 | RANDOMKEY(): Promise; 328 | 329 | READONLY(): Promise; 330 | 331 | READWRITE(): Promise; 332 | 333 | RENAME(key: string, newkey: string): Promise; 334 | 335 | RENAMENX(key: string, newkey: string): Promise; 336 | 337 | REPLICAOF(host: string, port: number): Promise; 338 | 339 | RESET(): Promise; 340 | 341 | RESTORE(key: string, ttl: number, serializedValue: string, ...arg: string[]): Promise; 342 | 343 | ROLE(): Promise; 344 | 345 | RPOP(key: string): Promise; 346 | RPOP(key: string, count: number): Promise; 347 | 348 | RPOPLPUSH(source: string, destination: string): Promise; 349 | 350 | RPUSH(key: string, ...element: string[]): Promise; 351 | 352 | RPUSHX(key: string, ...element: string[]): Promise; 353 | 354 | SADD(key: string, ...member: string[]): Promise; 355 | 356 | SAVE(): Promise; 357 | 358 | SCAN(cursor: number): Promise<[string, string[]]>; 359 | SCAN(cursor: number, option1: 'match', pattern: string): Promise<[string, string[]]>; 360 | SCAN(cursor: number, option1: 'count', count: number): Promise<[string, string[]]>; 361 | SCAN(cursor: number, option1: 'type', type: string): Promise<[string, string[]]>; 362 | SCAN(cursor: number, option1: 'match', pattern: string, option2: 'count', count: number): Promise<[string, string[]]>; 363 | SCAN(cursor: number, option1: 'match', pattern: string, option2: 'type', type: string): Promise<[string, string[]]>; 364 | SCAN(cursor: number, option1: 'count', count: number, option2: 'type', type: string): Promise<[string, string[]]>; 365 | SCAN(cursor: number, option1: 'match', pattern: string, option2: 'count', count: number, option3: 'type', type: string): Promise<[string, string[]]>; 366 | 367 | SCARD(key: string): Promise; 368 | 369 | SCRIPT(subcommand: 'debug', mode: 'yes' | 'sync' | 'no'): Promise; 370 | SCRIPT(subcommand: 'exists', ...sha1: string[]): Promise; 371 | SCRIPT(subcommand: 'flush', modifier?: 'async' | 'sync'): Promise; 372 | SCRIPT(subcommand: 'kill'): Promise; 373 | SCRIPT(subcommand: 'load', script: string): Promise; 374 | 375 | SDIFF(...key: string[]): Promise; 376 | 377 | SDIFFSTORE(destination: string, ...key: string[]): Promise; 378 | 379 | SELECT(index: number): Promise; 380 | 381 | SET(key: string, value: string, ...arg: string[]): Promise; 382 | 383 | SETBIT(key: string, offset: number, value: string): Promise; 384 | 385 | SETEX(key: string, seconds: number, value: string): Promise; 386 | 387 | SETRANGE(key: string, offset: number, value: string): Promise; 388 | 389 | SHUTDOWN(modifier?: 'save' | 'nosave'): Promise; 390 | 391 | SINTER(...key: string[]): Promise; 392 | 393 | SINTERSTORE(destination: string, ...key: string[]): Promise; 394 | 395 | SISMEMBER(key: string, member: string): Promise; 396 | 397 | SMISMEMBER(key: string, ...member: string[]): Promise; 398 | 399 | SLAVEOF(host: string, port: number): Promise; 400 | 401 | SLOWLOG(...arg: string[]): Promise; 402 | 403 | SMEMBERS(key: string): Promise; 404 | 405 | SMOVE(key: string, source: string, destination: string, member: string): Promise; 406 | 407 | SORT(key: string, ...arg: string[]): Promise; 408 | 409 | SPOP(key: string): Promise; 410 | SPOP(key: string, count: number): Promise; 411 | 412 | SRANDMEMBER(key: string): Promise; 413 | SRANDMEMBER(key: string, count: number): Promise; 414 | 415 | SREM(key: string, ...member: string[]): Promise; 416 | 417 | SSCAN(key: string, cursor: number): Promise<[string, string[]]>; 418 | SSCAN(key: string, cursor: number, option1: 'match', pattern: string): Promise<[string, string[]]>; 419 | SSCAN(key: string, cursor: number, option1: 'count', count: number): Promise<[string, string[]]>; 420 | SSCAN(key: string, cursor: number, option1: 'match', pattern: string, option2: 'count', count: number): Promise<[string, string[]]>; 421 | 422 | STRALGO(subcommand: 'lcs', ...arg: string[]): Promise; 423 | 424 | STRLEN(key: string): Promise; 425 | 426 | SUBSCRIBE(...channel: string[]): Promise; 427 | 428 | SUNION(...key: string[]): Promise; 429 | 430 | SUNIONSTORE(destination: string, ...key: string[]): Promise; 431 | 432 | SWAPDB(index1: number, index2: number,): Promise; 433 | 434 | SYNC(): Promise; 435 | 436 | PSYNC(replicationid: string, offset: string): Promise; 437 | 438 | TIME(): Promise; 439 | 440 | TOUCH(...key: string[]): Promise; 441 | 442 | TTL(key: string): Promise; 443 | 444 | TYPE(key: string): Promise; 445 | 446 | UNSUBSCRIBE(...channel: string[]): Promise; 447 | 448 | UNLINK(...key: string[]): Promise; 449 | 450 | UNWATCH(): Promise; 451 | 452 | WAIT(numreplicas: string, timeout: number): Promise; 453 | 454 | WATCH(...key: string[]): Promise; 455 | 456 | XADD(key: string, ...arg: string[]): Promise; 457 | 458 | XACK(key: string, group: string, ...id: string[]): Promise; 459 | 460 | XAUTOCLAIM(key: string, group: string, consumer: string, minIdleIime: number, start: string, subcommand2?: 'JUSTID'): Promise; 461 | XAUTOCLAIM(key: string, group: string, consumer: string, minIdleIime: number, start: string, subcommand1: 'count', count: number, subcommand2?: 'JUSTID'): Promise; 462 | 463 | XCLAIM(key: string, group: string, consumer: string, ...arg: string[]): Promise; 464 | 465 | XDEL(key: string, ...id: string[]): Promise; 466 | 467 | XGROUP(...arg: string[]): Promise; 468 | 469 | XINFO(...arg: string[]): Promise; 470 | 471 | XLEN(key: string): Promise; 472 | 473 | XPENDING(key: string, group: string): Promise; 474 | 475 | XRANGE(key: string, start: number, end: number): Promise; 476 | XRANGE(key: string, start: number, end: number, option: 'count', count: number): Promise; 477 | 478 | XREVRANGE(key: string, end: number, start: number): Promise; 479 | XREVRANGE(key: string, end: number, start: number, option: 'count', count: number): Promise; 480 | 481 | XREAD(...arg: string[]): Promise; 482 | 483 | XREADGROUP(...arg: string[]): Promise; 484 | 485 | XTRIM(key: string, subcommand: 'maxlen' | 'minid', ...arg: string[]): Promise; 486 | 487 | ZADD(key: string, ...arg: string[]): Promise; 488 | 489 | ZCARD(key: string): Promise; 490 | 491 | ZCOUNT(key: string, min: number, max: number): Promise; 492 | 493 | ZDIFF(numkeys: number, ...arg: string[]): Promise; 494 | 495 | ZDIFFSTORE(destination: string, numkeys: number, ...key: string[]): Promise; 496 | 497 | ZINCRBY(key: string, increment: number, member: string): Promise; 498 | 499 | ZINTER(numkeys: number, ...arg: string[]): Promise; 500 | 501 | ZINTERSTORE(destination: string, numkeys: number, ...key: string[]): Promise; 502 | 503 | ZLEXCOUNT(key: string, min: number, max: number): Promise; 504 | 505 | ZMSCORE(key: string, member: string): Promise; 506 | 507 | ZPOPMAX(key: string, count?: number): Promise; 508 | 509 | ZPOPMIN(key: string, count?: number): Promise; 510 | 511 | ZRANDMEMBER(key: string): Promise; 512 | ZRANDMEMBER(key: string, count: number, option?: 'withscores'): Promise; 513 | 514 | ZRANGESTORE(dst: string, src: string, min: number, max: number, ...arg: string[]): Promise; 515 | 516 | ZRANGE(key: string, min: number, max: number, ...arg: string[]): Promise; 517 | 518 | ZRANGEBYLEX(key: string, min: number, max: number): Promise; 519 | ZRANGEBYLEX(key: string, min: number, max: number, option: 'limit', offset: number, count: number): Promise; 520 | 521 | ZREVRANGEBYLEX(key: string, min: number, max: number): Promise; 522 | ZREVRANGEBYLEX(key: string, min: number, max: number, option: 'limit', offset: number, count: number): Promise; 523 | 524 | ZRANGEBYSCORE(key: string, min: number, max: number): Promise; 525 | ZRANGEBYSCORE(key: string, min: number, max: number, option: 'limit', offset: number, count: number): Promise; 526 | ZRANGEBYSCORE(key: string, min: number, max: number, option: 'withscores'): Promise; 527 | ZRANGEBYSCORE(key: string, min: number, max: number, option1: 'withscores', option2: 'limit', offset: number, count: number): Promise; 528 | 529 | ZRANK(key: string, member: string): Promise; 530 | 531 | ZREM(key: string, ...member: string[]): Promise; 532 | 533 | ZREMRANGEBYLEX(key: string, min: number, max: number): Promise; 534 | 535 | ZREMRANGEBYRANK(key: string, start: number, stop: number): Promise; 536 | 537 | ZREMRANGEBYSCORE(key: string, min: number, max: number): Promise; 538 | 539 | ZREVRANGE(key: string, start: number, stop: number, option: 'withscores'): Promise; 540 | 541 | ZREVRANGEBYSCORE(key: string, min: number, max: number): Promise; 542 | ZREVRANGEBYSCORE(key: string, min: number, max: number, option: 'limit', offset: number, count: number): Promise; 543 | ZREVRANGEBYSCORE(key: string, min: number, max: number, option: 'withscores'): Promise; 544 | ZREVRANGEBYSCORE(key: string, min: number, max: number, option1: 'withscores', option2: 'limit', offset: number, count: number): Promise; 545 | 546 | ZREVRANK(key: string, member: string): Promise; 547 | 548 | ZSCAN(key: string, cursor: number): Promise<[string, string[]]>; 549 | ZSCAN(key: string, cursor: number, option1: 'match', pattern: string): Promise<[string, string[]]>; 550 | ZSCAN(key: string, cursor: number, option1: 'count', count: number): Promise<[string, string[]]>; 551 | ZSCAN(key: string, cursor: number, option1: 'match', pattern: string, option2: 'count', count: number): Promise<[string, string[]]>; 552 | 553 | ZSCORE(key: string, member: string): Promise; 554 | 555 | ZUNION(numkeys: number, ...args: string[]): Promise; 556 | 557 | ZUNIONSTORE(destination: string, numkeys: number, ...arg: string[]): Promise; 558 | 559 | //#region No documentation 560 | ASKING(...arg: string[]): Promise; 561 | 'HOST:'(...arg: string[]): Promise; 562 | PFDEBUG(...arg: string[]): Promise; 563 | PFSELFTEST(...arg: string[]): Promise; 564 | POST(...arg: string[]): Promise; 565 | 'RESTORE-ASKING'(...arg: string[]): Promise; 566 | REPLCONF(...arg: string[]): Promise; 567 | SUBSTR(...arg: string[]): Promise; 568 | XSETID(...arg: string[]): Promise; 569 | //#endregion 570 | } -------------------------------------------------------------------------------- /src/command/command.v2.ts: -------------------------------------------------------------------------------- 1 | import { IBaseCommand } from './baseCommand'; 2 | 3 | export interface IClientCommand extends IBaseCommand { 4 | HGETALL(key: string): Promise; 5 | } -------------------------------------------------------------------------------- /src/command/command.v3.ts: -------------------------------------------------------------------------------- 1 | import { IBaseCommand } from './baseCommand'; 2 | 3 | export interface IClientCommand extends IBaseCommand { 4 | HGETALL(key: string): Promise>; 5 | } -------------------------------------------------------------------------------- /src/command/commands.json: -------------------------------------------------------------------------------- 1 | [ 2 | "acl", 3 | "append", 4 | "asking", 5 | "auth", 6 | "bgrewriteaof", 7 | "bgsave", 8 | "bitcount", 9 | "bitfield", 10 | "bitfield_ro", 11 | "bitop", 12 | "bitpos", 13 | "blmove", 14 | "blpop", 15 | "brpop", 16 | "brpoplpush", 17 | "bzpopmax", 18 | "bzpopmin", 19 | "client", 20 | "cluster", 21 | "command", 22 | "config", 23 | "copy", 24 | "dbsize", 25 | "debug", 26 | "decr", 27 | "decrby", 28 | "del", 29 | "discard", 30 | "dump", 31 | "echo", 32 | "eval", 33 | "evalsha", 34 | "exec", 35 | "exists", 36 | "expire", 37 | "expireat", 38 | "failover", 39 | "flushall", 40 | "flushdb", 41 | "geoadd", 42 | "geodist", 43 | "geohash", 44 | "geopos", 45 | "georadius", 46 | "georadius_ro", 47 | "georadiusbymember", 48 | "georadiusbymember_ro", 49 | "geosearch", 50 | "geosearchstore", 51 | "get", 52 | "getbit", 53 | "getdel", 54 | "getex", 55 | "getrange", 56 | "getset", 57 | "hdel", 58 | "hello", 59 | "hexists", 60 | "hget", 61 | "hgetall", 62 | "hincrby", 63 | "hincrbyfloat", 64 | "hkeys", 65 | "hlen", 66 | "hmget", 67 | "hmset", 68 | "host:", 69 | "hrandfield", 70 | "hscan", 71 | "hset", 72 | "hsetnx", 73 | "hstrlen", 74 | "hvals", 75 | "incr", 76 | "incrby", 77 | "incrbyfloat", 78 | "info", 79 | "keys", 80 | "lastsave", 81 | "latency", 82 | "lindex", 83 | "linsert", 84 | "llen", 85 | "lmove", 86 | "lolwut", 87 | "lpop", 88 | "lpos", 89 | "lpush", 90 | "lpushx", 91 | "lrange", 92 | "lrem", 93 | "lset", 94 | "ltrim", 95 | "memory", 96 | "mget", 97 | "migrate", 98 | "module", 99 | "monitor", 100 | "move", 101 | "mset", 102 | "msetnx", 103 | "multi", 104 | "object", 105 | "persist", 106 | "pexpire", 107 | "pexpireat", 108 | "pfadd", 109 | "pfcount", 110 | "pfdebug", 111 | "pfmerge", 112 | "pfselftest", 113 | "ping", 114 | "post", 115 | "psetex", 116 | "psubscribe", 117 | "psync", 118 | "pttl", 119 | "publish", 120 | "pubsub", 121 | "punsubscribe", 122 | "quit", 123 | "randomkey", 124 | "readonly", 125 | "readwrite", 126 | "rename", 127 | "renamenx", 128 | "replconf", 129 | "replicaof", 130 | "reset", 131 | "restore", 132 | "restore-asking", 133 | "role", 134 | "rpop", 135 | "rpoplpush", 136 | "rpush", 137 | "rpushx", 138 | "sadd", 139 | "save", 140 | "scan", 141 | "scard", 142 | "script", 143 | "sdiff", 144 | "sdiffstore", 145 | "select", 146 | "set", 147 | "setbit", 148 | "setex", 149 | "setnx", 150 | "setrange", 151 | "shutdown", 152 | "sinter", 153 | "sinterstore", 154 | "sismember", 155 | "slaveof", 156 | "slowlog", 157 | "smembers", 158 | "smismember", 159 | "smove", 160 | "sort", 161 | "spop", 162 | "srandmember", 163 | "srem", 164 | "sscan", 165 | "stralgo", 166 | "strlen", 167 | "subscribe", 168 | "substr", 169 | "sunion", 170 | "sunionstore", 171 | "swapdb", 172 | "sync", 173 | "time", 174 | "touch", 175 | "ttl", 176 | "type", 177 | "unlink", 178 | "unsubscribe", 179 | "unwatch", 180 | "wait", 181 | "watch", 182 | "xack", 183 | "xadd", 184 | "xautoclaim", 185 | "xclaim", 186 | "xdel", 187 | "xgroup", 188 | "xinfo", 189 | "xlen", 190 | "xpending", 191 | "xrange", 192 | "xread", 193 | "xreadgroup", 194 | "xrevrange", 195 | "xsetid", 196 | "xtrim", 197 | "zadd", 198 | "zcard", 199 | "zcount", 200 | "zdiff", 201 | "zdiffstore", 202 | "zincrby", 203 | "zinter", 204 | "zinterstore", 205 | "zlexcount", 206 | "zmscore", 207 | "zpopmax", 208 | "zpopmin", 209 | "zrandmember", 210 | "zrange", 211 | "zrangebylex", 212 | "zrangebyscore", 213 | "zrangestore", 214 | "zrank", 215 | "zrem", 216 | "zremrangebylex", 217 | "zremrangebyrank", 218 | "zremrangebyscore", 219 | "zrevrange", 220 | "zrevrangebylex", 221 | "zrevrangebyscore", 222 | "zrevrank", 223 | "zscan", 224 | "zscore", 225 | "zunion", 226 | "zunionstore" 227 | ] -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { IClientOptions } from './client/baseClient'; 2 | 3 | export { Client as ClientV2 } from './client/client.v2'; 4 | export { Client as ClientV3 } from './client/client.v3'; 5 | -------------------------------------------------------------------------------- /src/library/redisError.ts: -------------------------------------------------------------------------------- 1 | export class RedisError extends Error { 2 | constructor( 3 | public message: string, 4 | public code?: number 5 | ) { 6 | super(); 7 | this.name = 'Redis Reply Error'; 8 | } 9 | } -------------------------------------------------------------------------------- /src/library/verbatimString.ts: -------------------------------------------------------------------------------- 1 | export class VerbatimString extends String { 2 | constructor( 3 | public format: string, 4 | value: string 5 | ) { 6 | super(value); 7 | } 8 | } -------------------------------------------------------------------------------- /src/parser/baseParser.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'events'; 2 | import { ProtoVer } from '../client/baseClient'; 3 | import { RedisError } from '../library/redisError'; 4 | 5 | export default abstract class BaseParser extends EventEmitter { 6 | constructor( 7 | private readonly protover: ProtoVer 8 | ) { 9 | super(); 10 | } 11 | 12 | callbacks = new Array<(error: RedisError | undefined, reply: unknown | undefined) => void>(); 13 | buffer: number[] = []; 14 | offset = 0; 15 | parsing = false; 16 | 17 | get inBounds(): boolean { 18 | return this.offset < this.buffer.length; 19 | } 20 | 21 | /** 22 | * Convert command to resp string. 23 | * @param command Redis command. 24 | * @param args Redis command args. 25 | */ 26 | encodeCommand(command: string, args?: string[]): string { 27 | const result = []; 28 | result.push('*', args ? args.length + 1 : 1, '\r\n'); 29 | // If the command contains only letters, 30 | // the length of the command string is the same as the length of the Buffer 31 | result.push('$', command.length, '\r\n'); 32 | result.push(command, '\r\n'); 33 | 34 | args?.forEach(arg => { 35 | // The length of the buffer 36 | result.push('$', Buffer.from(arg).length, '\r\n'); 37 | result.push(arg, '\r\n'); 38 | }); 39 | 40 | return result.join(''); 41 | } 42 | 43 | async decodeReply(data: Buffer): Promise { 44 | this.buffer.push(...data); 45 | this.emit('newData'); 46 | if (this.parsing) { 47 | return; 48 | } 49 | 50 | let reply = await this.parseReply(); 51 | while (reply !== undefined) { 52 | const cb = this.callbacks.shift(); 53 | 54 | if (cb) { 55 | if (reply instanceof RedisError) { 56 | cb(reply, undefined); 57 | } else { 58 | cb(undefined, reply); 59 | } 60 | } 61 | 62 | /** 63 | * When the array is emptied but there is unprocessed data in the buffer, 64 | * there may be pubsub data. 65 | * And if the protover is 2, we should return pubsub data. 66 | */ 67 | if (this.callbacks.length === 0 && this.protover === 2) { 68 | this.emit('message', reply); 69 | } 70 | 71 | if (this.callbacks.length === 0 && (!this.inBounds)) { 72 | break; 73 | } 74 | 75 | reply = await this.parseReply(); 76 | } 77 | 78 | this.reset(); 79 | } 80 | 81 | /** 82 | * Reset parser status. 83 | */ 84 | private reset() { 85 | this.buffer = []; 86 | this.offset = 0; 87 | this.parsing = false; 88 | } 89 | 90 | /** 91 | * Get the next character synchronously and move offset. 92 | */ 93 | nextChar(): number { 94 | return this.buffer[this.offset++] as number; 95 | } 96 | 97 | /** 98 | * Get the next character asynchronously and move offset. 99 | */ 100 | nextCharAsync(): Promise { 101 | this.buffer = []; 102 | this.offset = 0; 103 | return new Promise((resolve) => { 104 | this.once('newData', () => { 105 | resolve(this.buffer[this.offset++] as number); 106 | }); 107 | }); 108 | } 109 | 110 | /** 111 | * Get the next character synchronously. 112 | */ 113 | peekChar(): number { 114 | return this.buffer[this.offset] as number; 115 | } 116 | 117 | /** 118 | * Get the next character asynchronously. 119 | */ 120 | peekCharAsync(): Promise { 121 | this.buffer = []; 122 | this.offset = 0; 123 | return new Promise((resolve) => { 124 | this.once('newData', () => { 125 | resolve(this.buffer[this.offset] as number); 126 | }); 127 | }); 128 | } 129 | 130 | abstract parseReply(): Promise; 131 | 132 | async parseSimpleString(): Promise { 133 | const result: number[] = []; 134 | let char = this.inBounds ? this.nextChar() : await this.nextCharAsync(); 135 | 136 | while (char !== 13) { // '\r' 137 | result.push(char); 138 | char = this.inBounds ? this.nextChar() : await this.nextCharAsync(); 139 | } 140 | // skip '\r\n' 141 | this.offset++; 142 | return Buffer.from(result).toString(); 143 | } 144 | 145 | async parseSimpleError(): Promise { 146 | const msg = await this.parseSimpleString(); 147 | return new RedisError(msg); 148 | } 149 | 150 | async parseNumber(): Promise { 151 | let result = 0; 152 | let char = this.inBounds ? this.nextChar() : await this.nextCharAsync(); 153 | let sign = false; 154 | if (char === 45) { // '-' 155 | sign = true; 156 | char = this.inBounds ? this.nextChar() : await this.nextCharAsync(); 157 | } 158 | 159 | while (char !== 13) { // '\r' 160 | result = result * 10 + (char - 48); // '0' 161 | char = this.inBounds ? this.nextChar() : await this.nextCharAsync(); 162 | } 163 | // skip '\r\n' 164 | this.offset++; 165 | return sign ? -result : result; 166 | } 167 | } -------------------------------------------------------------------------------- /src/parser/parser.v2.ts: -------------------------------------------------------------------------------- 1 | import BaseParser from './baseParser'; 2 | 3 | class Parser extends BaseParser { 4 | constructor() { 5 | super(2); 6 | } 7 | 8 | async parseReply(): Promise { 9 | this.parsing = true; 10 | 11 | const char = this.inBounds ? this.nextChar() : await this.nextCharAsync(); 12 | switch (char) { 13 | case 43: // '+' 14 | return this.parseSimpleString(); 15 | case 45: // '-' 16 | return this.parseSimpleError(); 17 | case 58: // ':' 18 | return this.parseNumber(); 19 | case 36: // '$' 20 | return this.parseBulkString(); 21 | case 42: // '*' 22 | return this.parseArray(); 23 | default: 24 | return undefined; 25 | } 26 | } 27 | 28 | private async parseBulkString() { 29 | const result: number[] = []; 30 | let char = this.inBounds ? this.peekChar() : await this.peekCharAsync(); 31 | const length = await this.parseNumber(); 32 | 33 | if (length === -1) { 34 | return null; 35 | } 36 | 37 | for (let i = 0; i < length; i++) { 38 | char = this.inBounds ? this.nextChar() : await this.nextCharAsync(); 39 | result.push(char); 40 | } 41 | 42 | this.offset += 2; 43 | return Buffer.from(result).toString(); 44 | } 45 | 46 | private async parseArray() { 47 | const length = await this.parseNumber(); 48 | if (length === -1) { 49 | return null; 50 | } 51 | 52 | const array = []; 53 | 54 | for (let i = 0; i < length; i++) { 55 | array.push(await this.parseReply()); 56 | } 57 | 58 | return array; 59 | } 60 | } 61 | 62 | export default Parser; -------------------------------------------------------------------------------- /src/parser/parser.v3.ts: -------------------------------------------------------------------------------- 1 | import BaseParser from './baseParser'; 2 | import { RedisError } from '../library/redisError'; 3 | import { VerbatimString } from '../library/verbatimString'; 4 | 5 | class Parser extends BaseParser { 6 | constructor() { 7 | super(3); 8 | } 9 | 10 | async parseReply(): Promise { 11 | this.parsing = true; 12 | 13 | const char = this.inBounds ? this.nextChar() : await this.nextCharAsync(); 14 | switch (char) { 15 | case 36: // '$' 16 | return this.parseBlobString(); 17 | case 37: // '%' 18 | return this.parseMap(); 19 | case 58: // ':' 20 | return this.parseNumber(); 21 | case 42: // '*' 22 | return this.parseArray(); 23 | case 43: // '+' 24 | return this.parseSimpleString(); 25 | case 45: // '-' 26 | return this.parseSimpleError(); 27 | case 44: // ',' 28 | return this.parseDouble(); 29 | case 35: // '#' 30 | return this.parseBoolean(); 31 | case 33: // '!' 32 | return this.parseBlobError(); 33 | case 61: // '=' 34 | return this.parseVerbatimString(); 35 | case 40: // '(' 36 | return this.parseBigNumber(); 37 | case 126: // '~' 38 | return this.parseSet(); 39 | case 124: // '|' 40 | return this.parseAttribute(); 41 | case 62: // '>' 42 | return this.parsePubSub(); 43 | case 95: // '_' 44 | return this.parseNull(); 45 | default: 46 | return undefined; 47 | } 48 | } 49 | 50 | private async parseMap() { 51 | let char = this.inBounds ? this.peekChar() : await this.peekCharAsync(); 52 | const map = new Map(); 53 | if (char === 63) { // '?' 54 | this.offset += 3; // skip '?\r\n' 55 | char = this.inBounds ? this.peekChar() : await this.peekCharAsync(); 56 | while (char !== 46) { // '.' 57 | const key = await this.parseReply(); 58 | const value = await this.parseReply(); 59 | map.set(key, value); 60 | char = this.inBounds ? this.peekChar() : await this.peekCharAsync(); 61 | } 62 | // skip the ending '.\r\n' 63 | this.offset += 3; 64 | } else { 65 | const length = await this.parseNumber(); 66 | for (let i = 0; i < length; i++) { 67 | const key = await this.parseReply(); 68 | const value = await this.parseReply(); 69 | map.set(key, value); 70 | } 71 | } 72 | return map; 73 | } 74 | 75 | private async parseBlobString() { 76 | const result: number[] = []; 77 | let char = this.inBounds ? this.peekChar() : await this.peekCharAsync(); 78 | 79 | let length: number; 80 | if (char === 63) { // '?' stream string 81 | this.offset += 4; // skip '?\r\n;' 82 | length = await this.parseNumber(); 83 | while (length !== 0) { 84 | for (let i = 0; i < length; i++) { 85 | char = this.inBounds ? this.nextChar() : await this.nextCharAsync(); 86 | result.push(char); 87 | } 88 | this.offset += 3; 89 | length = await this.parseNumber(); 90 | } 91 | } else { // common string 92 | length = await this.parseNumber(); 93 | for (let i = 0; i < length; i++) { 94 | char = this.inBounds ? this.nextChar() : await this.nextCharAsync(); 95 | result.push(char); 96 | } 97 | } 98 | // skip the ending '\r\n' 99 | this.offset += 2; 100 | return Buffer.from(result).toString(); 101 | } 102 | 103 | private async parseArray() { 104 | let char = this.inBounds ? this.peekChar() : await this.peekCharAsync(); 105 | const array = []; 106 | if (char === 63) { // '?' 107 | this.offset += 3; // skip '?\r\n' 108 | char = this.inBounds ? this.peekChar() : await this.peekCharAsync(); 109 | while (char !== 46) { // '.' 110 | array.push(await this.parseReply()); 111 | char = this.inBounds ? this.peekChar() : await this.peekCharAsync(); 112 | } 113 | this.offset += 3; // skip '.\r\n' 114 | } else { 115 | const length = await this.parseNumber(); 116 | for (let i = 0; i < length; i++) { 117 | array.push(await this.parseReply()); 118 | } 119 | } 120 | return array; 121 | } 122 | 123 | private async parseDouble() { 124 | const result: number[] = []; 125 | let char = this.inBounds ? this.nextChar() : await this.nextCharAsync(); 126 | if (char === 45) { // '-' 127 | result.unshift(45); 128 | char = this.inBounds ? this.nextChar() : await this.nextCharAsync(); 129 | } 130 | 131 | while (char !== 13) { // '\r' 132 | result.push(char); 133 | char = this.inBounds ? this.nextChar() : await this.nextCharAsync(); 134 | } 135 | 136 | this.offset++; 137 | return parseFloat(Buffer.from(result).toString()); 138 | } 139 | 140 | private async parseBoolean() { 141 | const char = this.inBounds ? this.nextChar() : await this.nextCharAsync(); 142 | this.offset += 2; 143 | return char === 116; // 't' 144 | } 145 | 146 | private async parseBlobError() { 147 | const code = await this.parseNumber(); 148 | const msg: number[] = []; 149 | let char = this.inBounds ? this.nextChar() : await this.nextCharAsync(); 150 | while (char !== 13) { // '\r' 151 | msg.push(char); 152 | char = this.inBounds ? this.nextChar() : await this.nextCharAsync(); 153 | } 154 | 155 | return new RedisError(Buffer.from(msg).toString(), code); 156 | } 157 | 158 | private async parseVerbatimString() { 159 | const length = await this.parseNumber(); 160 | const result: number[] = []; 161 | const format: number[] = []; 162 | for (let i = 0; i < length; i++) { 163 | const char = this.inBounds ? this.nextChar() : await this.nextCharAsync(); 164 | // the fourth byte is always ':' 165 | if (i < 3) { 166 | format.push(char); 167 | } else if (i > 3) { 168 | result.push(char); 169 | } 170 | } 171 | return new VerbatimString(Buffer.from(format).toString(), Buffer.from(result).toString()); 172 | } 173 | 174 | private async parseBigNumber() { 175 | const result: number[] = []; 176 | let char = this.inBounds ? this.nextChar() : await this.nextCharAsync(); 177 | if (char === 45) { // '-' 178 | result.unshift(45); 179 | char = this.inBounds ? this.nextChar() : await this.nextCharAsync(); 180 | } 181 | while (char !== 13) { // '\r' 182 | result.push(char); 183 | char = this.inBounds ? this.nextChar() : await this.nextCharAsync(); 184 | } 185 | this.offset++; 186 | return BigInt(Buffer.from(result).toString()); 187 | } 188 | 189 | private async parseSet() { 190 | let char = this.inBounds ? this.peekChar() : await this.peekCharAsync(); 191 | const set = new Set(); 192 | if (char === 63) { // '?' 193 | this.offset += 3; // skip '?\r\n' 194 | char = this.inBounds ? this.peekChar() : await this.peekCharAsync(); 195 | while (char !== 46) { // '.' 196 | set.add(await this.parseReply()); 197 | char = this.inBounds ? this.peekChar() : await this.peekCharAsync(); 198 | } 199 | this.offset += 3; // skip '.\r\n' 200 | } else { 201 | const length = await this.parseNumber(); 202 | for (let i = 0; i < length; i++) { 203 | set.add(await this.parseReply()); 204 | } 205 | } 206 | return set; 207 | } 208 | 209 | private async parseAttribute() { 210 | /** 211 | * Attribute type, just discard it. 212 | * 213 | * Is there a real need for the attribute type? 214 | * https://github.com/antirez/RESP3/issues/20#issuecomment-583073317 215 | */ 216 | await this.parseMap(); 217 | return this.parseReply(); 218 | } 219 | 220 | private async parsePubSub() { 221 | const length = await this.parseNumber(); 222 | const message = []; 223 | for (let i = 0; i < length; i++) { 224 | const tmp = await this.parseReply(); 225 | message.push(tmp); 226 | } 227 | this.emit('message', message); 228 | } 229 | 230 | private parseNull() { 231 | this.offset += 2; 232 | return null; 233 | } 234 | } 235 | 236 | export default Parser; -------------------------------------------------------------------------------- /test/index.ts: -------------------------------------------------------------------------------- 1 | import { spawn } from 'child_process'; 2 | 3 | const MOCHA_PATH = require.resolve('mocha/bin/mocha'); 4 | const TS_NODE_PATH = require.resolve('ts-node/register'); 5 | 6 | const mocha = spawn(process.execPath, [ 7 | MOCHA_PATH, 8 | '-r', 9 | TS_NODE_PATH, 10 | './**/*.test.ts' 11 | ], { 12 | stdio: 'inherit', 13 | }); 14 | 15 | mocha.on('exit', (code, signal) => { 16 | process.on('exit', () => { 17 | if (signal) { 18 | process.kill(process.pid, signal); 19 | } else { 20 | process.exit(code as number); 21 | } 22 | }); 23 | }); 24 | 25 | process.on('SIGINT', function () { 26 | mocha.kill('SIGINT'); 27 | mocha.kill('SIGTERM'); 28 | }); -------------------------------------------------------------------------------- /test/parser.v2.test.ts: -------------------------------------------------------------------------------- 1 | import { strictEqual } from 'assert'; 2 | import { describe, it } from 'mocha'; 3 | import Parser from '../src/parser/parser.v2'; 4 | 5 | const parser = new Parser(); 6 | 7 | describe('Parser.parseBulkString', () => { 8 | it('should return a string.', async () => { 9 | const buffer = Buffer.from('$6\r\nfoobar\r\n'); 10 | const p = new Promise((resolve) => { 11 | parser.callbacks.push((_err, reply) => { 12 | resolve(reply); 13 | }); 14 | }); 15 | parser.decodeReply(buffer); 16 | const result = await p; 17 | strictEqual(result, 'foobar'); 18 | }); 19 | 20 | it('should return an empty string.', async () => { 21 | const buffer = Buffer.from('$0\r\n\r\n'); 22 | const p = new Promise((resolve) => { 23 | parser.callbacks.push((_err, reply) => { 24 | resolve(reply); 25 | }); 26 | }); 27 | parser.decodeReply(buffer); 28 | const result = await p; 29 | strictEqual(result, ''); 30 | }); 31 | 32 | it('should return null.', async () => { 33 | const buffer = Buffer.from('$-1\r\n'); 34 | const p = new Promise((resolve) => { 35 | parser.callbacks.push((_err, reply) => { 36 | resolve(reply); 37 | }); 38 | }); 39 | parser.decodeReply(buffer); 40 | const result = await p; 41 | strictEqual(result, null); 42 | }); 43 | }); 44 | 45 | describe('Parser.parseArray', () => { 46 | it('should return an array.', async () => { 47 | const buffer = Buffer.from('*2\r\n$3\r\nfoo\r\n$3\r\nbar\r\n'); 48 | const p = new Promise((resolve) => { 49 | parser.callbacks.push((_err, reply) => { 50 | resolve(reply); 51 | }); 52 | }); 53 | parser.decodeReply(buffer); 54 | const result = (await p) as string[]; 55 | strictEqual(result[0], 'foo'); 56 | }); 57 | 58 | it('should return null.', async () => { 59 | const buffer = Buffer.from('*-1\r\n'); 60 | const p = new Promise((resolve) => { 61 | parser.callbacks.push((_err, reply) => { 62 | resolve(reply); 63 | }); 64 | }); 65 | parser.decodeReply(buffer); 66 | const result = await p; 67 | strictEqual(result, null); 68 | }); 69 | }); -------------------------------------------------------------------------------- /test/parser.v3.test.ts: -------------------------------------------------------------------------------- 1 | import { strictEqual } from 'assert'; 2 | import { describe, it } from 'mocha'; 3 | import { RedisError } from '../src/library/redisError'; 4 | import { VerbatimString } from '../src/library/verbatimString'; 5 | import Parser from '../src/parser/parser.v3'; 6 | 7 | const parser = new Parser(); 8 | 9 | describe('Parser.encodeCommand', () => { 10 | it('should be encoded correctly.', () => { 11 | strictEqual( 12 | parser.encodeCommand('SET', ['mykey', 'myvalue']), 13 | '*3\r\n$3\r\nSET\r\n$5\r\nmykey\r\n$7\r\nmyvalue\r\n' 14 | ); 15 | }); 16 | }); 17 | 18 | // sticky packet 19 | describe('Parser.decodeReply', () => { 20 | it('should be able to handle sticky packets correctly.', async () => { 21 | const p1 = new Promise((resolve) => { 22 | parser.callbacks.push((_err, reply) => { 23 | resolve(reply); 24 | }); 25 | }); 26 | const p2 = new Promise((resolve) => { 27 | parser.callbacks.push((_err, reply) => { 28 | resolve(reply); 29 | }); 30 | }); 31 | 32 | const buffer1 = Buffer.from('+hello'); 33 | const buffer2 = Buffer.from(' world\r\n'); 34 | 35 | setTimeout(() => { 36 | parser.decodeReply(buffer1); 37 | }, 0); 38 | setTimeout(() => { 39 | parser.decodeReply(buffer2); 40 | }, 10); 41 | setTimeout(() => { 42 | parser.decodeReply(buffer1); 43 | }, 20); 44 | setTimeout(() => { 45 | parser.decodeReply(buffer2); 46 | }, 30); 47 | 48 | const result1 = await p1; 49 | const result2 = await p2; 50 | strictEqual(result1, 'hello world'); 51 | strictEqual(result2, 'hello world'); 52 | }); 53 | }); 54 | 55 | describe('Parser.parseSimpleError', () => { 56 | it('should return an RedisError without code.', async () => { 57 | const buffer = Buffer.from('-ERR this is the error description\r\n'); 58 | const p = new Promise((resolve) => { 59 | parser.callbacks.push((_err) => { 60 | resolve(_err); 61 | }); 62 | }); 63 | parser.decodeReply(buffer); 64 | const result = (await p) as RedisError; 65 | strictEqual(result.message, 'ERR this is the error description'); 66 | strictEqual(result.code, undefined); 67 | }); 68 | }); 69 | 70 | describe('Parser.parseBlobString', () => { 71 | // common string 72 | it('should return a string.', async () => { 73 | const buffer = Buffer.from('$11\r\nhello world\r\n'); 74 | const p = new Promise((resolve) => { 75 | parser.callbacks.push((_err, reply) => { 76 | resolve(reply); 77 | }); 78 | }); 79 | parser.decodeReply(buffer); 80 | const result = await p; 81 | strictEqual(result, 'hello world'); 82 | }); 83 | // empty string 84 | it('should return an empty string.', async () => { 85 | const buffer = Buffer.from('$0\r\n\r\n'); 86 | const p = new Promise((resolve) => { 87 | parser.callbacks.push((_err, reply) => { 88 | resolve(reply); 89 | }); 90 | }); 91 | parser.decodeReply(buffer); 92 | const result = await p; 93 | strictEqual(result, ''); 94 | }); 95 | // streamed string 96 | it('should decode streamed string correctly.', async () => { 97 | const buffer = Buffer.from('$?\r\n;4\r\nHell\r\n;5\r\no wor\r\n;2\r\nld\r\n;0\r\n'); 98 | const p = new Promise((resolve) => { 99 | parser.callbacks.push((_err, reply) => { 100 | resolve(reply); 101 | }); 102 | }); 103 | parser.decodeReply(buffer); 104 | const result = (await p) as string; 105 | strictEqual(result, 'Hello world'); 106 | }); 107 | }); 108 | 109 | describe('Parser.parseMap', () => { 110 | it('should return a map', async () => { 111 | const buffer = Buffer.from('%2\r\n+first\r\n:1\r\n+second\r\n:2\r\n'); 112 | const p = new Promise((resolve) => { 113 | parser.callbacks.push((_err, reply) => { 114 | resolve(reply); 115 | }); 116 | }); 117 | parser.decodeReply(buffer); 118 | const result = (await p) as Map; 119 | strictEqual(result.get('first'), 1); 120 | strictEqual(result.get('second'), 2); 121 | }); 122 | 123 | it('should decode streamed map correctly.', async () => { 124 | const buffer = Buffer.from('%?\r\n+a\r\n:1\r\n+b\r\n:2\r\n.\r\n'); 125 | const p = new Promise((resolve) => { 126 | parser.callbacks.push((_err, reply) => { 127 | resolve(reply); 128 | }); 129 | }); 130 | parser.decodeReply(buffer); 131 | const result = (await p) as Map; 132 | strictEqual(result.get('a'), 1); 133 | strictEqual(result.get('b'), 2); 134 | }); 135 | }); 136 | 137 | describe('Parser.parseArray', () => { 138 | it('should return an array', async () => { 139 | const buffer = Buffer.from('*3\r\n:1\r\n:2\r\n:-3\r\n'); 140 | const p = new Promise((resolve) => { 141 | parser.callbacks.push((_err, reply) => { 142 | resolve(reply); 143 | }); 144 | }); 145 | parser.decodeReply(buffer); 146 | const result = (await p) as Array; 147 | strictEqual(result[0], 1); 148 | strictEqual(result[2], -3); 149 | }); 150 | 151 | it('should decode streamed array correctly.', async () => { 152 | const buffer = Buffer.from('*?\r\n:1\r\n:2\r\n:3\r\n.\r\n'); 153 | const p = new Promise((resolve) => { 154 | parser.callbacks.push((_err, reply) => { 155 | resolve(reply); 156 | }); 157 | }); 158 | parser.decodeReply(buffer); 159 | const result = (await p) as Array; 160 | strictEqual(result[0], 1); 161 | strictEqual(result[2], 3); 162 | }); 163 | }); 164 | 165 | describe('Parser.parseDouble', () => { 166 | it('should return a floating point number.', async () => { 167 | const buffer = Buffer.from(',-1.23\r\n'); 168 | const p = new Promise((resolve) => { 169 | parser.callbacks.push((_err, reply) => { 170 | resolve(reply); 171 | }); 172 | }); 173 | parser.decodeReply(buffer); 174 | const result = await p; 175 | strictEqual(result, -1.23); 176 | }); 177 | }); 178 | 179 | describe('Parser.parseBoolean', () => { 180 | it('should return a boolean.', async () => { 181 | const buffer = Buffer.from('#t\r\n'); 182 | const p = new Promise((resolve) => { 183 | parser.callbacks.push((_err, reply) => { 184 | resolve(reply); 185 | }); 186 | }); 187 | parser.decodeReply(buffer); 188 | const result = await p; 189 | strictEqual(result, true); 190 | }); 191 | }); 192 | 193 | describe('Parser.parseBlobError', () => { 194 | it('should return a RedisError with code', async () => { 195 | const buffer = Buffer.from('!21\r\nSYNTAX invalid syntax\r\n'); 196 | const p = new Promise((resolve) => { 197 | parser.callbacks.push((_err) => { 198 | resolve(_err); 199 | }); 200 | }); 201 | parser.decodeReply(buffer); 202 | const result = (await p) as RedisError; 203 | strictEqual(result.message, 'SYNTAX invalid syntax'); 204 | strictEqual(result.code, 21); 205 | }); 206 | }); 207 | 208 | describe('Parser.parseVerbatimString', () => { 209 | it('should return a VerbatimString', async () => { 210 | const buffer = Buffer.from('=15\r\ntxt:Some string\r\n'); 211 | const p = new Promise((resolve) => { 212 | parser.callbacks.push((_err, reply) => { 213 | resolve(reply); 214 | }); 215 | }); 216 | parser.decodeReply(buffer); 217 | const result = (await p) as VerbatimString; 218 | strictEqual(result.format, 'txt'); 219 | strictEqual(result.toString(), 'Some string'); 220 | }); 221 | }); 222 | 223 | describe('Parser.parseBigNumber', () => { 224 | it('should return a BigInt.', async () => { 225 | const buffer = Buffer.from('(-3492890328409238509324850943850943825024385\r\n'); 226 | const p = new Promise((resolve) => { 227 | parser.callbacks.push((_err, reply) => { 228 | resolve(reply); 229 | }); 230 | }); 231 | parser.decodeReply(buffer); 232 | const result = (await p) as BigInt; 233 | strictEqual(result, BigInt('-3492890328409238509324850943850943825024385')); 234 | }); 235 | }); 236 | 237 | describe('Parser.parseSet', () => { 238 | it('should return a set.', async () => { 239 | const buffer = Buffer.from('~5\r\n+orange\r\n+apple\r\n#t\r\n:100\r\n:999\r\n'); 240 | const p = new Promise((resolve) => { 241 | parser.callbacks.push((_err, reply) => { 242 | resolve(reply); 243 | }); 244 | }); 245 | parser.decodeReply(buffer); 246 | const result = (await p) as Set; 247 | strictEqual(result.has('apple'), true); 248 | strictEqual(result.has(100), true); 249 | }); 250 | it('should decode streamed set correctly.', async () => { 251 | const buffer = Buffer.from('~?\r\n:1\r\n:2\r\n:3\r\n.\r\n'); 252 | const p = new Promise((resolve) => { 253 | parser.callbacks.push((_err, reply) => { 254 | resolve(reply); 255 | }); 256 | }); 257 | parser.decodeReply(buffer); 258 | const result = (await p) as Set; 259 | strictEqual(result.has(1), true); 260 | strictEqual(result.has(3), true); 261 | }); 262 | }); 263 | 264 | describe('Parser.parseAttribute', () => { 265 | it('should discard attribute type.', async () => { 266 | const buffer = Buffer.from('|1\r\n+key-popularity\r\n%2\r\n$1\r\na\r\n,0.1923\r\n$1\r\nb\r\n,0.0012\r\n*2\r\n:2039123\r\n:9543892\r\n'); 267 | const p = new Promise((resolve) => { 268 | parser.callbacks.push((_err, reply) => { 269 | resolve(reply); 270 | }); 271 | }); 272 | parser.decodeReply(buffer); 273 | const result = (await p) as Array; 274 | strictEqual(result[0], 2039123); 275 | strictEqual(result[1], 9543892); 276 | }); 277 | }); 278 | 279 | describe('Parser.parsePubSub', () => { 280 | it('should receive pubsub message.', async () => { 281 | const buffer = Buffer.from('$9\r\nGet-Reply\r\n>4\r\n+pubsub\r\n+message\r\n+somechannel\r\n+this is the message\r\n'); 282 | const p = new Promise((resolve) => { 283 | parser.on('message', (data) => { 284 | resolve(data); 285 | }); 286 | }); 287 | parser.decodeReply(buffer); 288 | const result = (await p) as Array; 289 | strictEqual(result[0], 'pubsub'); 290 | }); 291 | }); 292 | 293 | describe('Parser.parseNull', () => { 294 | it('should return null.', async () => { 295 | const buffer = Buffer.from('_\r\n'); 296 | const p = new Promise((resolve) => { 297 | parser.callbacks.push((_err, reply) => { 298 | resolve(reply); 299 | }); 300 | }); 301 | parser.decodeReply(buffer); 302 | const result = await p; 303 | strictEqual(result, null); 304 | }); 305 | }); 306 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | "target": "es2017", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ 5 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ 6 | "outDir": "./lib", /* Redirect output structure to the directory. */ 7 | "rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 8 | "strict": true, /* Enable all strict type-checking options. */ 9 | "noUnusedLocals": true, /* Report errors on unused locals. */ 10 | "noUnusedParameters": true, /* Report errors on unused parameters. */ 11 | "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 12 | "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 13 | "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 14 | "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 15 | "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 16 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 17 | "skipLibCheck": true, /* Skip type checking of declaration files. */ 18 | "forceConsistentCasingInFileNames": true, /* Disallow inconsistently-cased references to the same file. */ 19 | "declaration": true, /* Generate .d.ts files for every TypeScript or JavaScript file inside your project. */ 20 | "declarationDir": "./types", /* Offers a way to configure the root directory for where declaration files are emitted. */ 21 | "resolveJsonModule": true /* Allows importing modules with a ‘.json’ extension, which is a common practice in node projects. */ 22 | }, 23 | "include": [ 24 | "src/**/*" 25 | ] 26 | } --------------------------------------------------------------------------------