diff --git a/.circleci/config.yml b/.circleci/config.yml index 909d095be9c..ed7d7212934 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -169,9 +169,6 @@ workflows: - test_node_12: requires: - build - - test_node_10: - requires: - - build - lint: requires: - build diff --git a/integration/microservices/e2e/disconnected-client.spec.ts b/integration/microservices/e2e/disconnected-client.spec.ts index 39feaeb3452..35b65dfb6b3 100644 --- a/integration/microservices/e2e/disconnected-client.spec.ts +++ b/integration/microservices/e2e/disconnected-client.spec.ts @@ -34,7 +34,7 @@ describe('Disconnected client', () => { .send({ transport: Transport.REDIS, options: { - url: 'redis://localhost:3333', + port: '3333', }, }) .expect(408); diff --git a/integration/microservices/src/disconnected.controller.ts b/integration/microservices/src/disconnected.controller.ts index 4239fbeb65e..0212ab1ce45 100644 --- a/integration/microservices/src/disconnected.controller.ts +++ b/integration/microservices/src/disconnected.controller.ts @@ -3,7 +3,7 @@ import { Controller, InternalServerErrorException, Post, - RequestTimeoutException, + RequestTimeoutException } from '@nestjs/common'; import { ClientProxyFactory } from '@nestjs/microservices'; import { Observable, throwError } from 'rxjs'; @@ -24,7 +24,8 @@ export class DisconnectedClientController { return throwError(() => code === 'ECONNREFUSED' || code === 'CONN_ERR' || - code === 'CONNECTION_REFUSED' + code === 'CONNECTION_REFUSED' || + error.message === 'Connection is closed.' ? new RequestTimeoutException('ECONNREFUSED') : new InternalServerErrorException(), ); diff --git a/package-lock.json b/package-lock.json index 4a5daddd738..10a79b73e0a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1707,6 +1707,12 @@ "integrity": "sha512-H9XAx3hc0BQHY6l+IFSWHDySypcXsvsuLhgYLUGywmJ5pswRVQJUHpOsobnLYp2ZUaUlKiKDrgWWhosOwAEM8Q==", "dev": true }, + "@ioredis/commands": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.1.1.tgz", + "integrity": "sha512-fsR4P/ROllzf/7lXYyElUJCheWdTJVJvOTps8v9IWKFATxR61ANOlnoPqhH099xYLrJGpc2ZQ28B3rMeUt5VQg==", + "dev": true + }, "@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -2993,15 +2999,6 @@ "integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==", "dev": true }, - "@types/redis": { - "version": "4.0.11", - "resolved": "https://registry.npmjs.org/@types/redis/-/redis-4.0.11.tgz", - "integrity": "sha512-bI+gth8La8Wg/QCR1+V1fhrL9+LZUSWfcqpOj2Kc80ZQ4ffbdL173vQd5wovmoV9i071FU9oP2g6etLuEwb6Rg==", - "dev": true, - "requires": { - "redis": "*" - } - }, "@types/reflect-metadata": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/@types/reflect-metadata/-/reflect-metadata-0.1.0.tgz", @@ -6062,6 +6059,12 @@ } } }, + "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==", + "dev": true + }, "code-block-writer": { "version": "11.0.0", "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-11.0.0.tgz", @@ -13150,6 +13153,46 @@ "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=", "dev": true }, + "ioredis": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.0.4.tgz", + "integrity": "sha512-qFJw3MnPNsJF1lcIOP3vztbsasOXK3nDdNAgjQj7t7/Bn/w10PGchTOpqylQNxjzPbLoYDu34LjeJtSWiKBntQ==", + "dev": true, + "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" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "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==", + "dev": true + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, "ip": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", @@ -15121,6 +15164,12 @@ "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=", "dev": true }, + "lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw=", + "dev": true + }, "lodash.escape": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/lodash.escape/-/lodash.escape-3.2.0.tgz", @@ -20699,6 +20748,12 @@ "integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=", "dev": true }, + "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==", + "dev": true + }, "static-eval": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/static-eval/-/static-eval-2.0.2.tgz", diff --git a/package.json b/package.json index 4364e602550..c2651e38701 100644 --- a/package.json +++ b/package.json @@ -72,6 +72,10 @@ "@codechecks/client": "0.1.12", "@commitlint/cli": "17.0.0", "@commitlint/config-angular": "17.0.0", + "@fastify/cors": "7.0.0", + "@fastify/formbody": "6.0.0", + "@fastify/multipart": "6.0.0", + "@fastify/static": "5.0.0", "@grpc/grpc-js": "1.6.7", "@grpc/proto-loader": "0.6.12", "@nestjs/apollo": "10.0.11", @@ -89,7 +93,6 @@ "@types/http-errors": "1.8.2", "@types/mocha": "9.1.1", "@types/node": "17.0.34", - "@types/redis": "4.0.11", "@types/reflect-metadata": "0.1.0", "@types/sinon": "10.0.11", "@types/socket.io": "3.0.2", @@ -122,10 +125,6 @@ "eventsource": "2.0.2", "fancy-log": "2.0.0", "fastify": "3.29.0", - "@fastify/cors": "7.0.0", - "@fastify/formbody": "6.0.0", - "@fastify/multipart": "6.0.0", - "@fastify/static": "5.0.0", "graphql": "15.8.0", "graphql-tools": "8.2.9", "gulp": "4.0.2", @@ -137,6 +136,7 @@ "http-errors": "2.0.0", "husky": "8.0.1", "imports-loader": "3.1.1", + "ioredis": "5.0.4", "json-loader": "0.5.7", "kafkajs": "2.0.0", "lerna": "3.0.0", @@ -156,7 +156,6 @@ "nyc": "15.1.0", "point-of-view": "5.3.0", "prettier": "2.6.2", - "redis": "3.1.2", "rxjs-compat": "6.6.7", "sinon": "14.0.0", "sinon-chai": "3.7.0", diff --git a/packages/microservices/client/client-redis.ts b/packages/microservices/client/client-redis.ts index 8b42ce6a0ec..517ea271cee 100644 --- a/packages/microservices/client/client-redis.ts +++ b/packages/microservices/client/client-redis.ts @@ -1,48 +1,31 @@ import { Logger } from '@nestjs/common/services/logger.service'; import { loadPackage } from '@nestjs/common/utils/load-package.util'; import { - EmptyError, - fromEvent, - lastValueFrom, - merge, - Subject, - zip, -} from 'rxjs'; -import { share, take, tap } from 'rxjs/operators'; -import { - CONNECT_EVENT, - ECONNREFUSED, ERROR_EVENT, MESSAGE_EVENT, - REDIS_DEFAULT_URL, + REDIS_DEFAULT_HOST, + REDIS_DEFAULT_PORT, } from '../constants'; -import { - ClientOpts, - RedisClient, - RetryStrategyOptions, -} from '../external/redis.interface'; import { ReadPacket, RedisOptions, WritePacket } from '../interfaces'; import { ClientProxy } from './client-proxy'; -let redisPackage: any = {}; +type Redis = any; + +let redisPackage = {} as any; export class ClientRedis extends ClientProxy { protected readonly logger = new Logger(ClientProxy.name); protected readonly subscriptionsCount = new Map(); - protected readonly url: string; - protected pubClient: RedisClient; - protected subClient: RedisClient; + protected pubClient: Redis; + protected subClient: Redis; protected connection: Promise; protected isExplicitlyTerminated = false; constructor(protected readonly options: RedisOptions['options']) { super(); - this.url = - this.getOptionsProp(options, 'url') || - (!this.getOptionsProp(options, 'host') && REDIS_DEFAULT_URL); - redisPackage = loadPackage('redis', ClientRedis.name, () => - require('redis'), + redisPackage = loadPackage('ioredis', ClientRedis.name, () => + require('ioredis'), ); this.initializeSerializer(options); @@ -64,73 +47,57 @@ export class ClientRedis extends ClientProxy { this.isExplicitlyTerminated = true; } - public connect(): Promise { + public async connect(): Promise { if (this.pubClient && this.subClient) { return this.connection; } - const error$ = new Subject(); - - this.pubClient = this.createClient(error$); - this.subClient = this.createClient(error$); + this.pubClient = this.createClient(); + this.subClient = this.createClient(); this.handleError(this.pubClient); this.handleError(this.subClient); - const pubConnect$ = fromEvent(this.pubClient, CONNECT_EVENT); - const subClient$ = fromEvent(this.subClient, CONNECT_EVENT); + this.connection = Promise.all([ + this.subClient.connect(), + this.pubClient.connect(), + ]); + await this.connection; - this.connection = lastValueFrom( - merge(error$, zip(pubConnect$, subClient$)).pipe( - take(1), - tap(() => - this.subClient.on(MESSAGE_EVENT, this.createResponseCallback()), - ), - share(), - ), - ).catch(err => { - if (err instanceof EmptyError) { - return; - } - throw err; - }); + this.subClient.on(MESSAGE_EVENT, this.createResponseCallback()); return this.connection; } - public createClient(error$: Subject): RedisClient { - return redisPackage.createClient({ - ...this.getClientOptions(error$), - url: this.url, + public createClient(): Redis { + return new redisPackage({ + host: REDIS_DEFAULT_HOST, + port: REDIS_DEFAULT_PORT, + ...this.getClientOptions(), + lazyConnect: true, }); } - public handleError(client: RedisClient) { + public handleError(client: Redis) { client.addListener(ERROR_EVENT, (err: any) => this.logger.error(err)); } - public getClientOptions(error$: Subject): Partial { - const retry_strategy = (options: RetryStrategyOptions) => - this.createRetryStrategy(options, error$); + public getClientOptions(): Partial { + const retryStrategy = (times: number) => this.createRetryStrategy(times); return { ...(this.options || {}), - retry_strategy, + retryStrategy, }; } - public createRetryStrategy( - options: RetryStrategyOptions, - error$: Subject, - ): undefined | number | Error { - if (options.error && (options.error as any).code === ECONNREFUSED) { - error$.error(options.error); - } + public createRetryStrategy(times: number): undefined | number { if (this.isExplicitlyTerminated) { return undefined; } if ( !this.getOptionsProp(this.options, 'retryAttempts') || - options.attempt > this.getOptionsProp(this.options, 'retryAttempts') + times > this.getOptionsProp(this.options, 'retryAttempts') ) { - return new Error('Retry time exhausted'); + this.logger.error('Retry time exhausted'); + return; } return this.getOptionsProp(this.options, 'retryDelay') || 0; } diff --git a/packages/microservices/constants.ts b/packages/microservices/constants.ts index a47c40762e6..8d423667aec 100644 --- a/packages/microservices/constants.ts +++ b/packages/microservices/constants.ts @@ -2,7 +2,10 @@ import { ROUTE_ARGS_METADATA } from '@nestjs/common/constants'; export const TCP_DEFAULT_PORT = 3000; export const TCP_DEFAULT_HOST = 'localhost'; -export const REDIS_DEFAULT_URL = 'redis://localhost:6379'; + +export const REDIS_DEFAULT_PORT = 6379; +export const REDIS_DEFAULT_HOST = 'localhost'; + export const NATS_DEFAULT_URL = 'nats://localhost:4222'; export const MQTT_DEFAULT_URL = 'mqtt://localhost:1883'; export const GRPC_DEFAULT_URL = 'localhost:5000'; diff --git a/packages/microservices/external/redis.interface.ts b/packages/microservices/external/redis.interface.ts index 00ae1bba7b3..6de36c43919 100644 --- a/packages/microservices/external/redis.interface.ts +++ b/packages/microservices/external/redis.interface.ts @@ -1,383 +1,213 @@ -/* eslint-disable @typescript-eslint/adjacent-overload-signatures */ +import { ConnectionOptions } from 'tls'; -export interface RetryStrategyOptions { - error: Error; - total_retry_time: number; - times_connected: number; - attempt: number; -} +/** + * @see https://github.dev/luin/ioredis/blob/df04dd8d87a44d3b64b385c86581915248554508/lib/redis/RedisOptions.ts#L184 + */ +export interface IORedisOptions { + Connector?: any; + retryStrategy?: (times: number) => number | void | null; + + /** + * If a command does not return a reply within a set number of milliseconds, + * a "Command timed out" error will be thrown. + */ + commandTimeout?: number; + /** + * Enable/disable keep-alive functionality. + * @link https://nodejs.org/api/net.html#socketsetkeepaliveenable-initialdelay + * @default 0 + */ + keepAlive?: number; + + /** + * Enable/disable the use of Nagle's algorithm. + * @link https://nodejs.org/api/net.html#socketsetnodelaynodelay + * @default true + */ + noDelay?: boolean; + + /** + * Set the name of the connection to make it easier to identity the connection + * in client list. + * @link https://redis.io/commands/client-setname + */ + connectionName?: string; + + /** + * If set, client will send AUTH command with the value of this option as the first argument when connected. + * This is supported since Redis 6. + */ + username?: string; -export interface ClientOpts { - auth_pass?: string; - command_queue_high_water?: number; - command_queue_low_water?: number; - connect_timeout?: number; - db?: string; - detect_buffers?: boolean; - disable_resubscribing?: boolean; - enable_offline_queue?: boolean; - family?: string; - host?: string; - max_attempts?: number; - no_ready_check?: boolean; - parser?: string; + /** + * If set, client will send AUTH command with the value of this option when connected. + */ password?: string; - path?: string; - port?: number; - prefix?: string; - rename_commands?: any; - retry_max_delay?: number; - retry_strategy?: any; - retry_unfulfilled_commands?: boolean; - return_buffers?: boolean; - socket_keepalive?: boolean; - socket_nodelay?: boolean; - string_numbers?: boolean; - tls?: any; - url?: string; -} -export interface RedisClient { - // event: connect - // event: error - // event: message - // event: pmessage - // event: subscribe - // event: psubscribe - // event: unsubscribe - // event: punsubscribe + /** + * Database index to use. + * + * @default 0 + */ + db?: number; - connected: boolean; - retry_delay: number; - retry_backoff: number; - command_queue: any[]; - offline_queue: any[]; - server_info: any; + /** + * When the client reconnects, channels subscribed in the previous connection will be + * resubscribed automatically if `autoResubscribe` is `true`. + * @default true + */ + autoResubscribe?: boolean; /** - * Forcibly close the connection to the Redis server. Note that this does not wait until all replies have been parsed. If you want to exit cleanly, call client.quit() + * Whether or not to resend unfulfilled commands on reconnect. + * Unfulfilled commands are most likely to be blocking commands such as `brpop` or `blpop`. + * @default true + */ + autoResendUnfulfilledCommands?: boolean; + /** + * Whether or not to reconnect on certain Redis errors. + * This options by default is `null`, which means it should never reconnect on Redis errors. + * You can pass a function that accepts an Redis error, and returns: + * - `true` or `1` to trigger a reconnection. + * - `false` or `0` to not reconnect. + * - `2` to reconnect and resend the failed command (who triggered the error) after reconnection. + * @example + * ```js + * const redis = new Redis({ + * reconnectOnError(err) { + * const targetError = "READONLY"; + * if (err.message.includes(targetError)) { + * // Only reconnect when the error contains "READONLY" + * return true; // or `return 1;` + * } + * }, + * }); + * ``` + * @default null + */ + reconnectOnError?: ((err: Error) => boolean | 1 | 2) | null; + + /** + * @default false + */ + readOnly?: boolean; + /** + * When enabled, numbers returned by Redis will be converted to JavaScript strings instead of numbers. + * This is necessary if you want to handle big numbers (above `Number.MAX_SAFE_INTEGER` === 2^53). + * @default false + */ + stringNumbers?: boolean; + + /** + * How long the client will wait before killing a socket due to inactivity during initial connection. + * @default 10000 + */ + connectTimeout?: number; + + /** + * This option is used internally when you call `redis.monitor()` to tell Redis + * to enter the monitor mode when the connection is established. * - * @param {boolean} flush You should set flush to true, if you are not absolutely sure you do not care about any other commands. If you set flush to false all still running commands will silently fail. + * @default false */ - end(flush: boolean): void; - unref(): void; + monitor?: boolean; /** - * Stop sending commands and queue the commands. + * The commands that don't get a reply due to the connection to the server is lost are + * put into a queue and will be resent on reconnect (if allowed by the `retryStrategy` option). + * This option is used to configure how many reconnection attempts should be allowed before + * the queue is flushed with a `MaxRetriesPerRequestError` error. + * Set this options to `null` instead of a number to let commands wait forever + * until the connection is alive again. + * + * @default 20 */ - cork(): void; + maxRetriesPerRequest?: number | null; /** - * Resume and send the queued commands at once. + * @default 10000 */ - uncork(): void; + maxLoadingRetryTime?: number; + /** + * @default false + */ + enableAutoPipelining?: boolean; + /** + * @default [] + */ + autoPipeliningIgnoredCommands?: string[]; + offlineQueue?: boolean; + commandQueue?: boolean; - // Low level command execution - send_command(command: string, ...args: any[]): boolean; + /** + * + * By default, if the connection to Redis server has not been established, commands are added to a queue + * and are executed once the connection is "ready" (when `enableReadyCheck` is true, "ready" means + * the Redis server has loaded the database from disk, otherwise means the connection to the Redis + * server has been established). If this option is false, when execute the command when the connection + * isn't ready, an error will be returned. + * + * @default true + */ + enableOfflineQueue?: boolean; - // Connection (http://redis.io/commands#connection) - auth(password: string, callback?: any): boolean; - ping(callback?: any): boolean; + /** + * The client will sent an INFO command to check whether the server is still loading data from the disk ( + * which happens when the server is just launched) when the connection is established, and only wait until + * the loading process is finished before emitting the `ready` event. + * + * @default true + */ + enableReadyCheck?: boolean; - // Strings (http://redis.io/commands#strings) - append(key: string, value: string, callback?: any): boolean; - bitcount(key: string, callback?: any): boolean; - bitcount(key: string, start: number, end: number, callback?: any): boolean; - set(key: string, value: string, callback?: any): boolean; - get(key: string, callback?: any): boolean; - exists(key: string, value: string, callback?: any): boolean; + /** + * When a Redis instance is initialized, a connection to the server is immediately established. Set this to + * true will delay the connection to the server until the first command is sent or `redis.connect()` is called + * explicitly. + * + * @default false + */ - publish(channel: string, value: any): boolean; - subscribe(channel: string): boolean; - on(event: string, callback: Function): any; - off(event: string, callback: Function): any; - addListener(event: string, callback: Function): any; + lazyConnect?: boolean; - /* - commands = set_union([ - "get", "set", "setnx", "setex", "append", "strlen", "del", "exists", "setbit", "getbit", "setrange", "getrange", "substr", - "incr", "decr", "mget", "rpush", "lpush", "rpushx", "lpushx", "linsert", "rpop", "lpop", "brpop", "brpoplpush", "blpop", "llen", "lindex", - "lset", "lrange", "ltrim", "lrem", "rpoplpush", "sadd", "srem", "smove", "sismember", "scard", "spop", "srandmember", "sinter", "sinterstore", - "sunion", "sunionstore", "sdiff", "sdiffstore", "smembers", "zadd", "zincrby", "zrem", "zremrangebyscore", "zremrangebyrank", "zunionstore", - "zinterstore", "zrange", "zrangebyscore", "zrevrangebyscore", "zcount", "zrevrange", "zcard", "zscore", "zrank", "zrevrank", "hset", "hsetnx", - "hget", "hmset", "hmget", "hincrby", "hincrbyfloat", "hdel", "hlen", "hkeys", "hvals", "hgetall", "hexists", "incrby", "decrby", "getset", "mset", "msetnx", - "randomkey", "select", "move", "rename", "renamenx", "expire", "expireat", "keys", "dbsize", "auth", "ping", "echo", "save", "bgsave", - "bgrewriteaof", "shutdown", "lastsave", "type", "any", "exec", "discard", "sync", "flushdb", "flushall", "sort", "info", "monitor", "ttl", - "persist", "slaveof", "debug", "config", "subscribe", "unsubscribe", "psubscribe", "punsubscribe", "publish", "watch", "unwatch", "cluster", - "restore", "migrate", "dump", "object", "client", "eval", "evalsha"], require("./lib/commands")); + /** + * @default undefined */ + scripts?: Record< + string, + { lua: string; numberOfKeys?: number; readOnly?: boolean } + >; - get(args: any[], callback?: any): boolean; - get(...args: any[]): boolean; - set(args: any[], callback?: any): boolean; - set(...args: any[]): boolean; - setnx(args: any[], callback?: any): boolean; - setnx(...args: any[]): boolean; - setex(args: any[], callback?: any): boolean; - setex(...args: any[]): boolean; - append(args: any[], callback?: any): boolean; - append(...args: any[]): boolean; - strlen(args: any[], callback?: any): boolean; - strlen(...args: any[]): boolean; - del(args: any[], callback?: any): boolean; - del(...args: any[]): boolean; - exists(args: any[], callback?: any): boolean; - exists(...args: any[]): boolean; - setbit(args: any[], callback?: any): boolean; - setbit(...args: any[]): boolean; - getbit(args: any[], callback?: any): boolean; - getbit(...args: any[]): boolean; - setrange(args: any[], callback?: any): boolean; - setrange(...args: any[]): boolean; - getrange(args: any[], callback?: any): boolean; - getrange(...args: any[]): boolean; - substr(args: any[], callback?: any): boolean; - substr(...args: any[]): boolean; - incr(args: any[], callback?: any): boolean; - incr(...args: any[]): boolean; - decr(args: any[], callback?: any): boolean; - decr(...args: any[]): boolean; - mget(args: any[], callback?: any): boolean; - mget(...args: any[]): boolean; - rpush(...args: any[]): boolean; - lpush(args: any[], callback?: any): boolean; - lpush(...args: any[]): boolean; - rpushx(args: any[], callback?: any): boolean; - rpushx(...args: any[]): boolean; - lpushx(args: any[], callback?: any): boolean; - lpushx(...args: any[]): boolean; - linsert(args: any[], callback?: any): boolean; - linsert(...args: any[]): boolean; - rpop(args: any[], callback?: any): boolean; - rpop(...args: any[]): boolean; - lpop(args: any[], callback?: any): boolean; - lpop(...args: any[]): boolean; - brpop(args: any[], callback?: any): boolean; - brpop(...args: any[]): boolean; - brpoplpush(args: any[], callback?: any): boolean; - brpoplpush(...args: any[]): boolean; - blpop(args: any[], callback?: any): boolean; - blpop(...args: any[]): boolean; - llen(args: any[], callback?: any): boolean; - llen(...args: any[]): boolean; - lindex(args: any[], callback?: any): boolean; - lindex(...args: any[]): boolean; - lset(args: any[], callback?: any): boolean; - lset(...args: any[]): boolean; - lrange(args: any[], callback?: any): boolean; - lrange(...args: any[]): boolean; - ltrim(args: any[], callback?: any): boolean; - ltrim(...args: any[]): boolean; - lrem(args: any[], callback?: any): boolean; - lrem(...args: any[]): boolean; - rpoplpush(args: any[], callback?: any): boolean; - rpoplpush(...args: any[]): boolean; - sadd(args: any[], callback?: any): boolean; - sadd(...args: any[]): boolean; - srem(args: any[], callback?: any): boolean; - srem(...args: any[]): boolean; - smove(args: any[], callback?: any): boolean; - smove(...args: any[]): boolean; - sismember(args: any[], callback?: any): boolean; - sismember(...args: any[]): boolean; - scard(args: any[], callback?: any): boolean; - scard(...args: any[]): boolean; - spop(args: any[], callback?: any): boolean; - spop(...args: any[]): boolean; - srandmember(args: any[], callback?: any): boolean; - srandmember(...args: any[]): boolean; - sinter(args: any[], callback?: any): boolean; - sinter(...args: any[]): boolean; - sinterstore(args: any[], callback?: any): boolean; - sinterstore(...args: any[]): boolean; - sunion(args: any[], callback?: any): boolean; - sunion(...args: any[]): boolean; - sunionstore(args: any[], callback?: any): boolean; - sunionstore(...args: any[]): boolean; - sdiff(args: any[], callback?: any): boolean; - sdiff(...args: any[]): boolean; - sdiffstore(args: any[], callback?: any): boolean; - sdiffstore(...args: any[]): boolean; - smembers(args: any[], callback?: any): boolean; - smembers(...args: any[]): boolean; - zadd(args: any[], callback?: any): boolean; - zadd(...args: any[]): boolean; - zincrby(args: any[], callback?: any): boolean; - zincrby(...args: any[]): boolean; - zrem(args: any[], callback?: any): boolean; - zrem(...args: any[]): boolean; - zremrangebyscore(args: any[], callback?: any): boolean; - zremrangebyscore(...args: any[]): boolean; - zremrangebyrank(args: any[], callback?: any): boolean; - zremrangebyrank(...args: any[]): boolean; - zunionstore(args: any[], callback?: any): boolean; - zunionstore(...args: any[]): boolean; - zinterstore(args: any[], callback?: any): boolean; - zinterstore(...args: any[]): boolean; - zrange(args: any[], callback?: any): boolean; - zrange(...args: any[]): boolean; - zrangebyscore(args: any[], callback?: any): boolean; - zrangebyscore(...args: any[]): boolean; - zrevrangebyscore(args: any[], callback?: any): boolean; - zrevrangebyscore(...args: any[]): boolean; - zcount(args: any[], callback?: any): boolean; - zcount(...args: any[]): boolean; - zrevrange(args: any[], callback?: any): boolean; - zrevrange(...args: any[]): boolean; - zcard(args: any[], callback?: any): boolean; - zcard(...args: any[]): boolean; - zscore(args: any[], callback?: any): boolean; - zscore(...args: any[]): boolean; - zrank(args: any[], callback?: any): boolean; - zrank(...args: any[]): boolean; - zrevrank(args: any[], callback?: any): boolean; - zrevrank(...args: any[]): boolean; - hset(args: any[], callback?: any): boolean; - hset(...args: any[]): boolean; - hsetnx(args: any[], callback?: any): boolean; - hsetnx(...args: any[]): boolean; - hget(args: any[], callback?: any): boolean; - hget(...args: any[]): boolean; - hmset(args: any[], callback?: any): boolean; - hmset(key: string, hash: any, callback?: any): boolean; - hmset(...args: any[]): boolean; - hmget(args: any[], callback?: any): boolean; - hmget(...args: any[]): boolean; - hincrby(args: any[], callback?: any): boolean; - hincrby(...args: any[]): boolean; - hincrbyfloat(args: any[], callback?: any): boolean; - hincrbyfloat(...args: any[]): boolean; - hdel(args: any[], callback?: any): boolean; - hdel(...args: any[]): boolean; - hlen(args: any[], callback?: any): boolean; - hlen(...args: any[]): boolean; - hkeys(args: any[], callback?: any): boolean; - hkeys(...args: any[]): boolean; - hvals(args: any[], callback?: any): boolean; - hvals(...args: any[]): boolean; - hgetall(args: any[], callback?: any): boolean; - hgetall(...args: any[]): boolean; - hgetall(key: string, callback?: any): boolean; - hexists(args: any[], callback?: any): boolean; - hexists(...args: any[]): boolean; - incrby(args: any[], callback?: any): boolean; - incrby(...args: any[]): boolean; - decrby(args: any[], callback?: any): boolean; - decrby(...args: any[]): boolean; - getset(args: any[], callback?: any): boolean; - getset(...args: any[]): boolean; - mset(args: any[], callback?: any): boolean; - mset(...args: any[]): boolean; - msetnx(args: any[], callback?: any): boolean; - msetnx(...args: any[]): boolean; - randomkey(args: any[], callback?: any): boolean; - randomkey(...args: any[]): boolean; - select(args: any[], callback?: any): void; - select(...args: any[]): void; - move(args: any[], callback?: any): boolean; - move(...args: any[]): boolean; - rename(args: any[], callback?: any): boolean; - rename(...args: any[]): boolean; - renamenx(args: any[], callback?: any): boolean; - renamenx(...args: any[]): boolean; - expire(args: any[], callback?: any): boolean; - expire(...args: any[]): boolean; - expireat(args: any[], callback?: any): boolean; - expireat(...args: any[]): boolean; - keys(args: any[], callback?: any): boolean; - keys(...args: any[]): boolean; - dbsize(args: any[], callback?: any): boolean; - dbsize(...args: any[]): boolean; - auth(args: any[], callback?: any): void; - auth(...args: any[]): void; - ping(args: any[], callback?: any): boolean; - ping(...args: any[]): boolean; - echo(args: any[], callback?: any): boolean; - echo(...args: any[]): boolean; - save(args: any[], callback?: any): boolean; - save(...args: any[]): boolean; - bgsave(args: any[], callback?: any): boolean; - bgsave(...args: any[]): boolean; - bgrewriteaof(args: any[], callback?: any): boolean; - bgrewriteaof(...args: any[]): boolean; - shutdown(args: any[], callback?: any): boolean; - shutdown(...args: any[]): boolean; - lastsave(args: any[], callback?: any): boolean; - lastsave(...args: any[]): boolean; - type(args: any[], callback?: any): boolean; - type(...args: any[]): boolean; - any(args: any[], callback?: any): any; - any(...args: any[]): any; - exec(args: any[], callback?: any): boolean; - exec(...args: any[]): boolean; - discard(args: any[], callback?: any): boolean; - discard(...args: any[]): boolean; - sync(args: any[], callback?: any): boolean; - sync(...args: any[]): boolean; - flushdb(args: any[], callback?: any): boolean; - flushdb(...args: any[]): boolean; - flushall(args: any[], callback?: any): boolean; - flushall(...args: any[]): boolean; - sort(args: any[], callback?: any): boolean; - sort(...args: any[]): boolean; - info(args: any[], callback?: any): boolean; - info(...args: any[]): boolean; - monitor(args: any[], callback?: any): boolean; - monitor(...args: any[]): boolean; - ttl(args: any[], callback?: any): boolean; - ttl(...args: any[]): boolean; - persist(args: any[], callback?: any): boolean; - persist(...args: any[]): boolean; - slaveof(args: any[], callback?: any): boolean; - slaveof(...args: any[]): boolean; - debug(args: any[], callback?: any): boolean; - debug(...args: any[]): boolean; - config(args: any[], callback?: any): boolean; - config(...args: any[]): boolean; - subscribe(args: any[], callback?: any): boolean; - subscribe(...args: any[]): boolean; - unsubscribe(args: any[], callback?: any): boolean; - unsubscribe(...args: any[]): boolean; - psubscribe(args: any[], callback?: any): boolean; - psubscribe(...args: any[]): boolean; - punsubscribe(args: any[], callback?: any): boolean; - punsubscribe(...args: any[]): boolean; - publish(args: any[], callback?: any): boolean; - publish(...args: any[]): boolean; - watch(args: any[], callback?: any): boolean; - watch(...args: any[]): boolean; - unwatch(args: any[], callback?: any): boolean; - unwatch(...args: any[]): boolean; - cluster(args: any[], callback?: any): boolean; - cluster(...args: any[]): boolean; - restore(args: any[], callback?: any): boolean; - restore(...args: any[]): boolean; - migrate(args: any[], callback?: any): boolean; - migrate(...args: any[]): boolean; - dump(args: any[], callback?: any): boolean; - dump(...args: any[]): boolean; - object(args: any[], callback?: any): boolean; - object(...args: any[]): boolean; - client(args: any[], callback?: any): boolean; - client(...args: any[]): boolean; - eval(args: any[], callback?: any): boolean; - eval(...args: any[]): boolean; - evalsha(args: any[], callback?: any): boolean; - evalsha(...args: any[]): boolean; - script(args: any[], callback?: any): boolean; - script(...args: any[]): boolean; - script(key: string, callback?: any): boolean; - quit(args: any[], callback?: any): boolean; - quit(...args: any[]): boolean; - sscan(...args: any[]): boolean; - sscan(args: any[], callback?: any): boolean; - scan(...args: any[]): boolean; - scan(args: any[], callback?: any): boolean; - hscan(...args: any[]): boolean; - hscan(args: any[], callback?: any): boolean; - zscan(...args: any[]): boolean; - zscan(args: any[], callback?: any): boolean; + keyPrefix?: string; + showFriendlyErrorStack?: boolean; - // Extras - duplicate(options?: any[], callback?: any): RedisClient; + // StandaloneConnectionOptions + disconnectTimeout?: number; + tls?: ConnectionOptions; + + // SentinelConnectionOptions + /** + * Master group name of the Sentinel + */ + name?: string; + /** + * @default "master" + */ + role?: 'master' | 'slave'; + sentinelUsername?: string; + sentinelPassword?: string; + sentinels?: Array>; + sentinelRetryStrategy?: (retryAttempts: number) => number | void | null; + sentinelReconnectStrategy?: (retryAttempts: number) => number | void | null; + preferredSlaves?: any; + sentinelCommandTimeout?: number; + enableTLSForSentinelMode?: boolean; + sentinelTLS?: ConnectionOptions; + natMap?: any; + updateSentinels?: boolean; + /** + * @default 10 + */ + sentinelMaxConnections?: number; + failoverDetector?: boolean; } diff --git a/packages/microservices/interfaces/microservice-configuration.interface.ts b/packages/microservices/interfaces/microservice-configuration.interface.ts index 2647e409b91..8126b16fe59 100644 --- a/packages/microservices/interfaces/microservice-configuration.interface.ts +++ b/packages/microservices/interfaces/microservice-configuration.interface.ts @@ -10,7 +10,7 @@ import { ProducerRecord, } from '../external/kafka.interface'; import { MqttClientOptions, QoS } from '../external/mqtt-options.interface'; -import { ClientOpts } from '../external/redis.interface'; +import { IORedisOptions } from '../external/redis.interface'; import { RmqUrl } from '../external/rmq-url.interface'; import { TcpSocket } from '../helpers'; import { CustomTransportStrategy } from './custom-transport-strategy.interface'; @@ -91,7 +91,7 @@ export interface RedisOptions { retryDelay?: number; serializer?: Serializer; deserializer?: Deserializer; - } & ClientOpts; + } & IORedisOptions; } export interface MqttOptions { diff --git a/packages/microservices/listeners-controller.ts b/packages/microservices/listeners-controller.ts index abc77d59795..df275cb112c 100644 --- a/packages/microservices/listeners-controller.ts +++ b/packages/microservices/listeners-controller.ts @@ -72,7 +72,8 @@ export class ListenersController { transport === server.transportId, ) .reduce((acc, handler) => { - handler.patterns.forEach(pattern => + // Optional chaining for backward-compatibility + handler.patterns?.forEach(pattern => acc.push({ ...handler, patterns: [pattern] }), ); return acc; diff --git a/packages/microservices/server/server-redis.ts b/packages/microservices/server/server-redis.ts index beaf02f8536..6e5530165aa 100644 --- a/packages/microservices/server/server-redis.ts +++ b/packages/microservices/server/server-redis.ts @@ -1,41 +1,37 @@ import { isUndefined } from '@nestjs/common/utils/shared.utils'; import { Observable } from 'rxjs'; import { - CONNECT_EVENT, ERROR_EVENT, MESSAGE_EVENT, NO_MESSAGE_HANDLER, - REDIS_DEFAULT_URL, + REDIS_DEFAULT_HOST, + REDIS_DEFAULT_PORT, } from '../constants'; import { RedisContext } from '../ctx-host'; import { Transport } from '../enums'; import { - ClientOpts, - RedisClient, - RetryStrategyOptions, -} from '../external/redis.interface'; -import { CustomTransportStrategy, IncomingRequest } from '../interfaces'; -import { RedisOptions } from '../interfaces/microservice-configuration.interface'; + CustomTransportStrategy, + IncomingRequest, + RedisOptions, +} from '../interfaces'; import { Server } from './server'; -let redisPackage: any = {}; +type Redis = any; + +let redisPackage = {} as any; export class ServerRedis extends Server implements CustomTransportStrategy { public readonly transportId = Transport.REDIS; - private readonly url: string; - private subClient: RedisClient; - private pubClient: RedisClient; + private subClient: Redis; + private pubClient: Redis; private isExplicitlyTerminated = false; constructor(private readonly options: RedisOptions['options']) { super(); - this.url = - this.getOptionsProp(options, 'url') || - (!this.getOptionsProp(options, 'host') && REDIS_DEFAULT_URL); - redisPackage = this.loadPackage('redis', ServerRedis.name, () => - require('redis'), + redisPackage = this.loadPackage('ioredis', ServerRedis.name, () => + require('ioredis'), ); this.initializeSerializer(options); @@ -51,6 +47,7 @@ export class ServerRedis extends Server implements CustomTransportStrategy { this.handleError(this.pubClient); this.handleError(this.subClient); + this.start(callback); } catch (err) { callback(err); @@ -58,11 +55,15 @@ export class ServerRedis extends Server implements CustomTransportStrategy { } public start(callback?: () => void) { - this.bindEvents(this.subClient, this.pubClient); - this.subClient.on(CONNECT_EVENT, callback); + Promise.all([this.subClient.connect(), this.pubClient.connect()]) + .then(() => { + this.bindEvents(this.subClient, this.pubClient); + callback(); + }) + .catch(callback); } - public bindEvents(subClient: RedisClient, pubClient: RedisClient) { + public bindEvents(subClient: Redis, pubClient: Redis) { subClient.on(MESSAGE_EVENT, this.getMessageHandler(pubClient).bind(this)); const subscribePatterns = [...this.messageHandlers.keys()]; subscribePatterns.forEach(pattern => { @@ -79,14 +80,16 @@ export class ServerRedis extends Server implements CustomTransportStrategy { this.subClient && this.subClient.quit(); } - public createRedisClient(): RedisClient { - return redisPackage.createClient({ + public createRedisClient(): Redis { + return new redisPackage({ + port: REDIS_DEFAULT_PORT, + host: REDIS_DEFAULT_HOST, ...this.getClientOptions(), - url: this.url, + lazyConnect: true, }); } - public getMessageHandler(pub: RedisClient) { + public getMessageHandler(pub: Redis) { return async (channel: string, buffer: string | any) => this.handleMessage(channel, buffer, pub); } @@ -94,7 +97,7 @@ export class ServerRedis extends Server implements CustomTransportStrategy { public async handleMessage( channel: string, buffer: string | any, - pub: RedisClient, + pub: Redis, ) { const rawMessage = this.parseMessage(buffer); const packet = await this.deserializer.deserialize(rawMessage, { channel }); @@ -125,7 +128,7 @@ export class ServerRedis extends Server implements CustomTransportStrategy { response$ && this.send(response$, publish); } - public getPublisher(pub: RedisClient, pattern: any, id: string) { + public getPublisher(pub: Redis, pattern: any, id: string) { return (response: any) => { Object.assign(response, { id }); const outgoingResponse = this.serializer.serialize(response); @@ -157,31 +160,25 @@ export class ServerRedis extends Server implements CustomTransportStrategy { stream.on(ERROR_EVENT, (err: any) => this.logger.error(err)); } - public getClientOptions(): Partial { - const retry_strategy = (options: RetryStrategyOptions) => - this.createRetryStrategy(options); + public getClientOptions(): Partial { + const retryStrategy = (times: number) => this.createRetryStrategy(times); return { ...(this.options || {}), - retry_strategy, + retryStrategy, }; } - public createRetryStrategy( - options: RetryStrategyOptions, - ): undefined | number | void { - if (options.error && (options.error as any).code === 'ECONNREFUSED') { - this.logger.error(`Error ECONNREFUSED: ${this.url}`); - } + public createRetryStrategy(times: number): undefined | number | void { if (this.isExplicitlyTerminated) { return undefined; } if ( !this.getOptionsProp(this.options, 'retryAttempts') || - options.attempt > this.getOptionsProp(this.options, 'retryAttempts') + times > this.getOptionsProp(this.options, 'retryAttempts') ) { - this.logger.error(`Retry time exhausted: ${this.url}`); - throw new Error('Retry time exhausted'); + this.logger.error(`Retry time exhausted`); + return; } return this.getOptionsProp(this.options, 'retryDelay') || 0; } diff --git a/packages/microservices/test/client/client-redis.spec.ts b/packages/microservices/test/client/client-redis.spec.ts index 8045f90a509..b7fb3ec5e1a 100644 --- a/packages/microservices/test/client/client-redis.spec.ts +++ b/packages/microservices/test/client/client-redis.spec.ts @@ -1,9 +1,7 @@ import { expect } from 'chai'; -import { Subject } from 'rxjs'; import * as sinon from 'sinon'; import { ClientRedis } from '../../client/client-redis'; import { ERROR_EVENT } from '../../constants'; -import { Client } from '../../external/nats-client.interface'; describe('ClientRedis', () => { const test = 'test'; @@ -252,53 +250,39 @@ describe('ClientRedis', () => { }); }); describe('getClientOptions', () => { - it('should return options object with "retry_strategy" and call "createRetryStrategy"', () => { + it('should return options object with "retryStrategy" and call "createRetryStrategy"', () => { const createSpy = sinon.spy(client, 'createRetryStrategy'); - const { retry_strategy } = client.getClientOptions(new Subject()); + const { retryStrategy } = client.getClientOptions(); try { - retry_strategy({} as any); + retryStrategy({} as any); } catch {} expect(createSpy.called).to.be.true; }); }); describe('createRetryStrategy', () => { - const subject = new Subject(); describe('when is terminated', () => { it('should return undefined', () => { (client as any).isExplicitlyTerminated = true; - const result = client.createRetryStrategy({} as any, subject); + const result = client.createRetryStrategy(0); expect(result).to.be.undefined; }); }); describe('when "retryAttempts" does not exist', () => { - it('should return an error', () => { + it('should return undefined', () => { (client as any).isExplicitlyTerminated = false; (client as any).options.options = {}; (client as any).options.options.retryAttempts = undefined; - const result = client.createRetryStrategy({} as any, subject); - expect(result).to.be.instanceOf(Error); + const result = client.createRetryStrategy(1); + expect(result).to.be.undefined; }); }); describe('when "attempts" count is max', () => { - it('should return an error', () => { + it('should return undefined', () => { (client as any).isExplicitlyTerminated = false; (client as any).options.options = {}; (client as any).options.options.retryAttempts = 3; - const result = client.createRetryStrategy( - { attempt: 4 } as any, - subject, - ); - expect(result).to.be.instanceOf(Error); - }); - }); - describe('when ECONNREFUSED', () => { - it('should return error', () => { - (client as any).options.options = {}; - (client as any).options.options.retryAttempts = 10; - - const error = { code: 'ECONNREFUSED' }; - const result = client.createRetryStrategy({ error } as any, subject); - expect(result).to.be.instanceOf(Error); + const result = client.createRetryStrategy(4); + expect(result).to.be.undefined; }); }); describe('otherwise', () => { @@ -307,10 +291,7 @@ describe('ClientRedis', () => { (client as any).isExplicitlyTerminated = false; (client as any).options.retryAttempts = 3; (client as any).options.retryDelay = 3; - const result = client.createRetryStrategy( - { attempt: 2 } as any, - subject, - ); + const result = client.createRetryStrategy(2); expect(result).to.be.eql((client as any).options.retryDelay); }); }); diff --git a/packages/microservices/test/listeners-controller.spec.ts b/packages/microservices/test/listeners-controller.spec.ts index af02d816171..8ad10437a89 100644 --- a/packages/microservices/test/listeners-controller.spec.ts +++ b/packages/microservices/test/listeners-controller.spec.ts @@ -138,11 +138,15 @@ describe('ListenersController', () => { it(`should call "addHandler" method of server with custom transportID for pattern handler with the same custom token`, () => { const serverHandlers = [ { - pattern: { cmd: 'test' }, + patterns: [{ cmd: 'test' }], targetCallback: 'tt', transport: customTransport, }, - { pattern: 'test2', targetCallback: '2', transport: Transport.KAFKA }, + { + patterns: ['test2'], + targetCallback: '2', + transport: Transport.KAFKA, + }, ]; explorer.expects('explore').returns(serverHandlers); diff --git a/packages/microservices/test/server/server-redis.spec.ts b/packages/microservices/test/server/server-redis.spec.ts index fc9650c8764..2f0602f909a 100644 --- a/packages/microservices/test/server/server-redis.spec.ts +++ b/packages/microservices/test/server/server-redis.spec.ts @@ -15,13 +15,17 @@ describe('ServerRedis', () => { }); describe('listen', () => { let onSpy: sinon.SinonSpy; + let connectSpy: sinon.SinonSpy; let client: any; let callbackSpy: sinon.SinonSpy; beforeEach(() => { onSpy = sinon.spy(); + connectSpy = sinon.spy(); + client = { on: onSpy, + connect: connectSpy, }; sinon.stub(server, 'createRedisClient').callsFake(() => client); @@ -31,13 +35,9 @@ describe('ServerRedis', () => { server.listen(callbackSpy); expect(onSpy.getCall(0).args[0]).to.be.equal('error'); }); - it('should bind "connect" event to handler', () => { - server.listen(callbackSpy); - expect(onSpy.getCall(3).args[0]).to.be.equal('connect'); - }); - it('should bind "message" event to handler', () => { + it('should call "RedisClient#connect()"', () => { server.listen(callbackSpy); - expect(onSpy.getCall(2).args[0]).to.be.equal('message'); + expect(connectSpy.called).to.be.true; }); describe('when "start" throws an exception', () => { it('should call callback with a thrown error as an argument', () => { @@ -191,11 +191,11 @@ describe('ServerRedis', () => { }); }); describe('getClientOptions', () => { - it('should return options object with "retry_strategy" and call "createRetryStrategy"', () => { + it('should return options object with "retryStrategy" and call "createRetryStrategy"', () => { const createSpy = sinon.spy(server, 'createRetryStrategy'); - const { retry_strategy } = server.getClientOptions(); + const { retryStrategy } = server.getClientOptions(); try { - retry_strategy({} as any); + retryStrategy(0); } catch {} expect(createSpy.called).to.be.true; }); @@ -204,37 +204,24 @@ describe('ServerRedis', () => { describe('when is terminated', () => { it('should return undefined', () => { (server as any).isExplicitlyTerminated = true; - const result = server.createRetryStrategy({} as any); + const result = server.createRetryStrategy(0); expect(result).to.be.undefined; }); }); describe('when "retryAttempts" does not exist', () => { - it('should throw an exception', () => { + it('should return undefined', () => { (server as any).options.options = {}; (server as any).options.options.retryAttempts = undefined; - expect(() => server.createRetryStrategy({} as any)).to.throw(Error); + expect(server.createRetryStrategy(4)).to.be.undefined; }); }); describe('when "attempts" count is max', () => { - it('should throw an exception', () => { + it('should return undefined', () => { (server as any).options.options = {}; (server as any).options.options.retryAttempts = 3; - expect(() => - server.createRetryStrategy({ attempt: 4 } as any), - ).to.throw(Error); - }); - }); - describe('when ECONNREFUSED', () => { - it('should call logger', () => { - const loggerErrorSpy = sinon.spy((server as any).logger, 'error'); - try { - server.createRetryStrategy({ - error: { code: 'ECONNREFUSED' }, - } as any); - } catch {} - expect(loggerErrorSpy.called).to.be.true; + expect(server.createRetryStrategy(4)).to.be.undefined; }); }); describe('otherwise', () => { @@ -243,7 +230,7 @@ describe('ServerRedis', () => { (server as any).isExplicitlyTerminated = false; (server as any).options.retryAttempts = 3; (server as any).options.retryDelay = 3; - const result = server.createRetryStrategy({ attempt: 2 } as any); + const result = server.createRetryStrategy(2); expect(result).to.be.eql((server as any).options.retryDelay); }); });