Skip to content

Commit e138cbd

Browse files
authored
fix(init): option parsing (#3086)
* refactor(maint): rename options * fix(init): option parsing * refactor(client): #options cannot be undefined
1 parent ed7b905 commit e138cbd

File tree

4 files changed

+108
-54
lines changed

4 files changed

+108
-54
lines changed
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import assert from "node:assert";
2+
import { createClient } from "../../";
3+
4+
describe("EnterpriseMaintenanceManager does not prevent proper options parsing", () => {
5+
it("should not throw when initializing without options", async () => {
6+
const client = createClient();
7+
assert.doesNotThrow(async () => {
8+
//Expected to reject because there is no url or socket provided and there is no running server on localhost
9+
await assert.rejects(client.connect);
10+
});
11+
});
12+
13+
it("should not throw when initializing without url/socket and with maint", async () => {
14+
const client = createClient({
15+
maintNotifications: "enabled",
16+
RESP: 3,
17+
});
18+
assert.doesNotThrow(async () => {
19+
//Expected to reject because there is no url or socket provided and there is no running server on localhost
20+
await assert.rejects(client.connect);
21+
});
22+
});
23+
it("should not throw when initializing with url and with maint", async () => {
24+
const client = createClient({
25+
maintNotifications: "enabled",
26+
RESP: 3,
27+
url: "redis://localhost:6379",
28+
});
29+
assert.doesNotThrow(async () => {
30+
//Expected to reject because there is no url or socket provided and there is no running server on localhost
31+
await assert.rejects(client.connect);
32+
});
33+
});
34+
35+
it("should not throw when initializing with socket and with maint", async () => {
36+
const client = createClient({
37+
maintNotifications: "enabled",
38+
RESP: 3,
39+
socket: {
40+
host: "localhost",
41+
port: 6379,
42+
},
43+
});
44+
assert.doesNotThrow(async () => {
45+
//Expected to reject because there is no url or socket provided and there is no running server on localhost
46+
await assert.rejects(client.connect);
47+
});
48+
});
49+
});

packages/client/lib/client/enterprise-maintenance-manager.ts

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { isIP } from "net";
55
import { lookup } from "dns/promises";
66
import assert from "node:assert";
77
import { setTimeout } from "node:timers/promises";
8-
import RedisSocket from "./socket";
8+
import RedisSocket, { RedisTcpSocketOptions } from "./socket";
99
import diagnostics_channel from "node:diagnostics_channel";
1010

1111
export const MAINTENANCE_EVENTS = {
@@ -64,12 +64,12 @@ export default class EnterpriseMaintenanceManager {
6464
#client: Client;
6565

6666
static setupDefaultMaintOptions(options: RedisClientOptions) {
67-
if (options.maintPushNotifications === undefined) {
68-
options.maintPushNotifications =
67+
if (options.maintNotifications === undefined) {
68+
options.maintNotifications =
6969
options?.RESP === 3 ? "auto" : "disabled";
7070
}
71-
if (options.maintMovingEndpointType === undefined) {
72-
options.maintMovingEndpointType = "auto";
71+
if (options.maintEndpointType === undefined) {
72+
options.maintEndpointType = "auto";
7373
}
7474
if (options.maintRelaxedSocketTimeout === undefined) {
7575
options.maintRelaxedSocketTimeout = 10000;
@@ -80,14 +80,20 @@ export default class EnterpriseMaintenanceManager {
8080
}
8181

8282
static async getHandshakeCommand(
83-
tls: boolean,
84-
host: string,
8583
options: RedisClientOptions,
8684
): Promise<
8785
| { cmd: Array<RedisArgument>; errorHandler: (error: Error) => void }
8886
| undefined
8987
> {
90-
if (options.maintPushNotifications === "disabled") return;
88+
if (options.maintNotifications === "disabled") return;
89+
90+
const host = options.url
91+
? new URL(options.url).hostname
92+
: (options.socket as RedisTcpSocketOptions | undefined)?.host;
93+
94+
if (!host) return;
95+
96+
const tls = options.socket?.tls ?? false
9197

9298
const movingEndpointType = await determineEndpoint(tls, host, options);
9399
return {
@@ -100,7 +106,7 @@ export default class EnterpriseMaintenanceManager {
100106
],
101107
errorHandler: (error: Error) => {
102108
dbgMaintenance("handshake failed:", error);
103-
if (options.maintPushNotifications === "enabled") {
109+
if (options.maintNotifications === "enabled") {
104110
throw error;
105111
}
106112
},
@@ -189,7 +195,7 @@ export default class EnterpriseMaintenanceManager {
189195
// reconnect to its currently configured endpoint after half of the grace
190196
// period that was communicated by the server is over.
191197
if (url === null) {
192-
assert(this.#options.maintMovingEndpointType === "none");
198+
assert(this.#options.maintEndpointType === "none");
193199
assert(this.#options.socket !== undefined);
194200
assert("host" in this.#options.socket);
195201
assert(typeof this.#options.socket.host === "string");
@@ -329,12 +335,12 @@ async function determineEndpoint(
329335
host: string,
330336
options: RedisClientOptions,
331337
): Promise<MovingEndpointType> {
332-
assert(options.maintMovingEndpointType !== undefined);
333-
if (options.maintMovingEndpointType !== "auto") {
338+
assert(options.maintEndpointType !== undefined);
339+
if (options.maintEndpointType !== "auto") {
334340
dbgMaintenance(
335-
`Determine endpoint type: ${options.maintMovingEndpointType}`,
341+
`Determine endpoint type: ${options.maintEndpointType}`,
336342
);
337-
return options.maintMovingEndpointType;
343+
return options.maintEndpointType;
338344
}
339345

340346
const ip = isIP(host) ? host : (await lookup(host, { family: 0 })).address;

packages/client/lib/client/index.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { strict as assert } from 'node:assert';
22
import testUtils, { GLOBAL, waitTillBeenCalled } from '../test-utils';
33
import RedisClient, { RedisClientOptions, RedisClientType } from '.';
4-
import { AbortError, ClientClosedError, ClientOfflineError, ConnectionTimeoutError, DisconnectsClientError, ErrorReply, MultiErrorReply, SocketClosedUnexpectedlyError, TimeoutError, WatchError } from '../errors';
4+
import { AbortError, ClientClosedError, ClientOfflineError, ConnectionTimeoutError, DisconnectsClientError, ErrorReply, MultiErrorReply, TimeoutError, WatchError } from '../errors';
55
import { defineScript } from '../lua-script';
66
import { spy, stub } from 'sinon';
77
import { once } from 'node:events';

packages/client/lib/client/index.ts

Lines changed: 38 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import COMMANDS from '../commands';
2-
import RedisSocket, { RedisSocketOptions, RedisTcpSocketOptions } from './socket';
2+
import RedisSocket, { RedisSocketOptions } from './socket';
33
import { BasicAuth, CredentialsError, CredentialsProvider, StreamingCredentialsProvider, UnableToObtainNewCredentialsError, Disposable } from '../authx';
44
import RedisCommandsQueue, { CommandOptions } from './commands-queue';
55
import { EventEmitter } from 'node:events';
@@ -154,7 +154,7 @@ export interface RedisClientOptions<
154154
*
155155
* The default is `auto`.
156156
*/
157-
maintPushNotifications?: 'disabled' | 'enabled' | 'auto';
157+
maintNotifications?: 'disabled' | 'enabled' | 'auto';
158158
/**
159159
* Controls how the client requests the endpoint to reconnect to during a MOVING notification in Redis Enterprise maintenance.
160160
*
@@ -167,19 +167,19 @@ export interface RedisClientOptions<
167167
168168
* The default is `auto`.
169169
*/
170-
maintMovingEndpointType?: MovingEndpointType;
170+
maintEndpointType?: MovingEndpointType;
171171
/**
172172
* Specifies a more relaxed timeout (in milliseconds) for commands during a maintenance window.
173-
* This helps minimize command timeouts during maintenance. If not provided, the `commandOptions.timeout`
174-
* will be used instead. Timeouts during maintenance period result in a `CommandTimeoutDuringMaintenance` error.
173+
* This helps minimize command timeouts during maintenance. Timeouts during maintenance period result
174+
* in a `CommandTimeoutDuringMaintenance` error.
175175
*
176176
* The default is 10000
177177
*/
178178
maintRelaxedCommandTimeout?: number;
179179
/**
180180
* Specifies a more relaxed timeout (in milliseconds) for the socket during a maintenance window.
181-
* This helps minimize socket timeouts during maintenance. If not provided, the `socket.timeout`
182-
* will be used instead. Timeouts during maintenance period result in a `SocketTimeoutDuringMaintenance` error.
181+
* This helps minimize socket timeouts during maintenance. Timeouts during maintenance period result
182+
* in a `SocketTimeoutDuringMaintenance` error.
183183
*
184184
* The default is 10000
185185
*/
@@ -429,7 +429,7 @@ export default class RedisClient<
429429
return parsed;
430430
}
431431

432-
readonly #options?: RedisClientOptions<M, F, S, RESP, TYPE_MAPPING>;
432+
readonly #options: RedisClientOptions<M, F, S, RESP, TYPE_MAPPING>;
433433
#socket: RedisSocket;
434434
readonly #queue: RedisCommandsQueue;
435435
#selectedDB = 0;
@@ -453,7 +453,7 @@ export default class RedisClient<
453453
return this._self.#clientSideCache;
454454
}
455455

456-
get options(): RedisClientOptions<M, F, S, RESP> | undefined {
456+
get options(): RedisClientOptions<M, F, S, RESP> {
457457
return this._self.#options;
458458
}
459459

@@ -503,15 +503,15 @@ export default class RedisClient<
503503
this.#socket = this.#initiateSocket();
504504

505505

506-
if(options?.maintPushNotifications !== 'disabled') {
507-
new EnterpriseMaintenanceManager(this.#queue, this, this.#options!);
506+
if(this.#options.maintNotifications !== 'disabled') {
507+
new EnterpriseMaintenanceManager(this.#queue, this, this.#options);
508508
};
509509

510-
if (options?.clientSideCache) {
511-
if (options.clientSideCache instanceof ClientSideCacheProvider) {
512-
this.#clientSideCache = options.clientSideCache;
510+
if (this.#options.clientSideCache) {
511+
if (this.#options.clientSideCache instanceof ClientSideCacheProvider) {
512+
this.#clientSideCache = this.#options.clientSideCache;
513513
} else {
514-
const cscConfig = options.clientSideCache;
514+
const cscConfig = this.#options.clientSideCache;
515515
this.#clientSideCache = new BasicClientSideCache(cscConfig);
516516
}
517517
this.#queue.addPushHandler((push: Array<any>): boolean => {
@@ -535,16 +535,16 @@ export default class RedisClient<
535535
throw new Error('Client Side Caching is only supported with RESP3');
536536
}
537537

538-
if (options?.maintPushNotifications && options?.maintPushNotifications !== 'disabled' && options?.RESP !== 3) {
538+
if (options?.maintNotifications && options?.maintNotifications !== 'disabled' && options?.RESP !== 3) {
539539
throw new Error('Graceful Maintenance is only supported with RESP3');
540540
}
541541

542542
}
543543

544-
#initiateOptions(options?: RedisClientOptions<M, F, S, RESP, TYPE_MAPPING>): RedisClientOptions<M, F, S, RESP, TYPE_MAPPING> | undefined {
544+
#initiateOptions(options: RedisClientOptions<M, F, S, RESP, TYPE_MAPPING> = {}): RedisClientOptions<M, F, S, RESP, TYPE_MAPPING> {
545545

546546
// Convert username/password to credentialsProvider if no credentialsProvider is already in place
547-
if (!options?.credentialsProvider && (options?.username || options?.password)) {
547+
if (!options.credentialsProvider && (options.username || options.password)) {
548548

549549
options.credentialsProvider = {
550550
type: 'async-credentials-provider',
@@ -555,19 +555,19 @@ export default class RedisClient<
555555
};
556556
}
557557

558-
if (options?.database) {
558+
if (options.database) {
559559
this._self.#selectedDB = options.database;
560560
}
561561

562-
if (options?.commandOptions) {
562+
if (options.commandOptions) {
563563
this._commandOptions = options.commandOptions;
564564
}
565565

566-
if(options?.maintPushNotifications !== 'disabled') {
567-
EnterpriseMaintenanceManager.setupDefaultMaintOptions(options!);
566+
if(options.maintNotifications !== 'disabled') {
567+
EnterpriseMaintenanceManager.setupDefaultMaintOptions(options);
568568
}
569569

570-
if (options?.url) {
570+
if (options.url) {
571571
const parsedOptions = RedisClient.parseOptions(options);
572572
if (parsedOptions?.database) {
573573
this._self.#selectedDB = parsedOptions.database;
@@ -580,8 +580,8 @@ export default class RedisClient<
580580

581581
#initiateQueue(): RedisCommandsQueue {
582582
return new RedisCommandsQueue(
583-
this.#options?.RESP ?? 2,
584-
this.#options?.commandsQueueMaxLength,
583+
this.#options.RESP ?? 2,
584+
this.#options.commandsQueueMaxLength,
585585
(channel, listeners) => this.emit('sharded-channel-moved', channel, listeners)
586586
);
587587
}
@@ -591,7 +591,7 @@ export default class RedisClient<
591591
*/
592592
private reAuthenticate = async (credentials: BasicAuth) => {
593593
// Re-authentication is not supported on RESP2 with PubSub active
594-
if (!(this.isPubSubActive && !this.#options?.RESP)) {
594+
if (!(this.isPubSubActive && !this.#options.RESP)) {
595595
await this.sendCommand(
596596
parseArgs(COMMANDS.AUTH, {
597597
username: credentials.username,
@@ -640,9 +640,9 @@ export default class RedisClient<
640640
Array<{ cmd: CommandArguments } & { errorHandler?: (err: Error) => void }>
641641
> {
642642
const commands = [];
643-
const cp = this.#options?.credentialsProvider;
643+
const cp = this.#options.credentialsProvider;
644644

645-
if (this.#options?.RESP) {
645+
if (this.#options.RESP) {
646646
const hello: HelloOptions = {};
647647

648648
if (cp && cp.type === 'async-credentials-provider') {
@@ -702,7 +702,7 @@ export default class RedisClient<
702702
}
703703
}
704704

705-
if (this.#options?.name) {
705+
if (this.#options.name) {
706706
commands.push({
707707
cmd: parseArgs(COMMANDS.CLIENT_SETNAME, this.#options.name)
708708
});
@@ -713,11 +713,11 @@ export default class RedisClient<
713713
commands.push({ cmd: ['SELECT', this.#selectedDB.toString()] });
714714
}
715715

716-
if (this.#options?.readonly) {
716+
if (this.#options.readonly) {
717717
commands.push({ cmd: parseArgs(COMMANDS.READONLY) });
718718
}
719719

720-
if (!this.#options?.disableClientInfo) {
720+
if (!this.#options.disableClientInfo) {
721721
commands.push({
722722
cmd: ['CLIENT', 'SETINFO', 'LIB-VER', version],
723723
errorHandler: () => {
@@ -732,7 +732,7 @@ export default class RedisClient<
732732
'CLIENT',
733733
'SETINFO',
734734
'LIB-NAME',
735-
this.#options?.clientInfoTag
735+
this.#options.clientInfoTag
736736
? `node-redis(${this.#options.clientInfoTag})`
737737
: 'node-redis'
738738
],
@@ -748,8 +748,7 @@ export default class RedisClient<
748748
commands.push({cmd: this.#clientSideCache.trackingOn()});
749749
}
750750

751-
const { tls, host } = this.#options!.socket as RedisTcpSocketOptions;
752-
const maintenanceHandshakeCmd = await EnterpriseMaintenanceManager.getHandshakeCommand(!!tls, host!, this.#options!);
751+
const maintenanceHandshakeCmd = await EnterpriseMaintenanceManager.getHandshakeCommand(this.#options);
753752
if(maintenanceHandshakeCmd) {
754753
commands.push(maintenanceHandshakeCmd);
755754
};
@@ -769,7 +768,7 @@ export default class RedisClient<
769768
.on('error', err => {
770769
this.emit('error', err);
771770
this.#clientSideCache?.onError();
772-
if (this.#socket.isOpen && !this.#options?.disableOfflineQueue) {
771+
if (this.#socket.isOpen && !this.#options.disableOfflineQueue) {
773772
this.#queue.flushWaitingForReply(err);
774773
} else {
775774
this.#queue.flushAll(err);
@@ -817,15 +816,15 @@ export default class RedisClient<
817816
}
818817
};
819818

820-
const socket = new RedisSocket(socketInitiator, this.#options?.socket);
819+
const socket = new RedisSocket(socketInitiator, this.#options.socket);
821820
this.#attachListeners(socket);
822821
return socket;
823822
}
824823

825824
#pingTimer?: NodeJS.Timeout;
826825

827826
#setPingTimer(): void {
828-
if (!this.#options?.pingInterval || !this.#socket.isReady) return;
827+
if (!this.#options.pingInterval || !this.#socket.isReady) return;
829828
clearTimeout(this.#pingTimer);
830829

831830
this.#pingTimer = setTimeout(() => {
@@ -986,7 +985,7 @@ export default class RedisClient<
986985
transformReply: TransformReply | undefined,
987986
) {
988987
const csc = this._self.#clientSideCache;
989-
const defaultTypeMapping = this._self.#options?.commandOptions === commandOptions;
988+
const defaultTypeMapping = this._self.#options.commandOptions === commandOptions;
990989

991990
const fn = () => { return this.sendCommand(parser.redisArgs, commandOptions) };
992991

@@ -1035,7 +1034,7 @@ export default class RedisClient<
10351034
): Promise<T> {
10361035
if (!this._self.#socket.isOpen) {
10371036
return Promise.reject(new ClientClosedError());
1038-
} else if (!this._self.#socket.isReady && this._self.#options?.disableOfflineQueue) {
1037+
} else if (!this._self.#socket.isReady && this._self.#options.disableOfflineQueue) {
10391038
return Promise.reject(new ClientOfflineError());
10401039
}
10411040

0 commit comments

Comments
 (0)