You've already forked node-redis
mirror of
https://github.com/redis/node-redis.git
synced 2025-08-06 02:15:48 +03:00
* fix #1846 - handle arguments that are not buffers or strings * use toString() instead of throw TypeError * remove .only and uncomment tests
This commit is contained in:
@@ -26,7 +26,7 @@ interface CommandWaitingToBeSent extends CommandWaitingForReply {
|
|||||||
|
|
||||||
interface CommandWaitingForReply {
|
interface CommandWaitingForReply {
|
||||||
resolve(reply?: unknown): void;
|
resolve(reply?: unknown): void;
|
||||||
reject(err: Error): void;
|
reject(err: unknown): void;
|
||||||
channelsCounter?: number;
|
channelsCounter?: number;
|
||||||
returnBuffers?: boolean;
|
returnBuffers?: boolean;
|
||||||
}
|
}
|
||||||
@@ -142,7 +142,9 @@ export default class RedisCommandsQueue {
|
|||||||
},
|
},
|
||||||
returnError: (err: Error) => this.#shiftWaitingForReply().reject(err)
|
returnError: (err: Error) => this.#shiftWaitingForReply().reject(err)
|
||||||
});
|
});
|
||||||
|
|
||||||
#chainInExecution: symbol | undefined;
|
#chainInExecution: symbol | undefined;
|
||||||
|
|
||||||
constructor(maxLength: number | null | undefined) {
|
constructor(maxLength: number | null | undefined) {
|
||||||
this.#maxLength = maxLength;
|
this.#maxLength = maxLength;
|
||||||
}
|
}
|
||||||
@@ -155,6 +157,7 @@ export default class RedisCommandsQueue {
|
|||||||
} else if (options?.signal?.aborted) {
|
} else if (options?.signal?.aborted) {
|
||||||
return Promise.reject(new AbortError());
|
return Promise.reject(new AbortError());
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const node = new LinkedList.Node<CommandWaitingToBeSent>({
|
const node = new LinkedList.Node<CommandWaitingToBeSent>({
|
||||||
args,
|
args,
|
||||||
@@ -178,6 +181,7 @@ export default class RedisCommandsQueue {
|
|||||||
once: true
|
once: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options?.asap) {
|
if (options?.asap) {
|
||||||
this.#waitingToBeSent.unshiftNode(node);
|
this.#waitingToBeSent.unshiftNode(node);
|
||||||
} else {
|
} else {
|
||||||
@@ -386,12 +390,13 @@ export default class RedisCommandsQueue {
|
|||||||
|
|
||||||
flushWaitingForReply(err: Error): void {
|
flushWaitingForReply(err: Error): void {
|
||||||
RedisCommandsQueue.#flushQueue(this.#waitingForReply, err);
|
RedisCommandsQueue.#flushQueue(this.#waitingForReply, err);
|
||||||
if (!this.#chainInExecution) {
|
|
||||||
return;
|
if (!this.#chainInExecution) return;
|
||||||
}
|
|
||||||
while (this.#waitingToBeSent.head?.value.chainId === this.#chainInExecution) {
|
while (this.#waitingToBeSent.head?.value.chainId === this.#chainInExecution) {
|
||||||
this.#waitingToBeSent.shift();
|
this.#waitingToBeSent.shift();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.#chainInExecution = undefined;
|
this.#chainInExecution = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import { strict as assert } from 'assert';
|
import { strict as assert } from 'assert';
|
||||||
import testUtils, { GLOBAL, waitTillBeenCalled } from '../test-utils';
|
import testUtils, { GLOBAL, waitTillBeenCalled } from '../test-utils';
|
||||||
import RedisClient, { ClientLegacyCommandArguments, RedisClientType } from '.';
|
import RedisClient, { RedisClientType } from '.';
|
||||||
import { RedisClientMultiCommandType } from './multi-command';
|
import { RedisClientMultiCommandType } from './multi-command';
|
||||||
import { RedisCommandArguments, RedisCommandRawReply, RedisModules, RedisScripts } from '../commands';
|
import { RedisCommandArguments, RedisCommandRawReply, RedisModules, RedisScripts } from '../commands';
|
||||||
import { AbortError, AuthError, ClientClosedError, ConnectionTimeoutError, DisconnectsClientError, SocketClosedUnexpectedlyError, WatchError } from '../errors';
|
import { AbortError, AuthError, ClientClosedError, ConnectionTimeoutError, DisconnectsClientError, SocketClosedUnexpectedlyError, WatchError } from '../errors';
|
||||||
@@ -183,7 +183,7 @@ describe('Client', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function setAsync<M extends RedisModules, S extends RedisScripts>(client: RedisClientType<M, S>, ...args: ClientLegacyCommandArguments): Promise<RedisCommandRawReply> {
|
function setAsync<M extends RedisModules, S extends RedisScripts>(client: RedisClientType<M, S>, ...args: Array<any>): Promise<RedisCommandRawReply> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
(client as any).set(...args, (err: Error | undefined, reply: RedisCommandRawReply) => {
|
(client as any).set(...args, (err: Error | undefined, reply: RedisCommandRawReply) => {
|
||||||
if (err) return reject(err);
|
if (err) return reject(err);
|
||||||
@@ -353,6 +353,18 @@ describe('Client', () => {
|
|||||||
);
|
);
|
||||||
}, GLOBAL.SERVERS.OPEN);
|
}, GLOBAL.SERVERS.OPEN);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testUtils.testWithClient('undefined and null should not break the client', async client => {
|
||||||
|
await assert.rejects(
|
||||||
|
client.sendCommand([null as any, undefined as any]),
|
||||||
|
'ERR unknown command ``, with args beginning with: ``'
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
await client.ping(),
|
||||||
|
'PONG'
|
||||||
|
);
|
||||||
|
}, GLOBAL.SERVERS.OPEN);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('multi', () => {
|
describe('multi', () => {
|
||||||
|
@@ -9,7 +9,7 @@ import { CommandOptions, commandOptions, isCommandOptions } from '../command-opt
|
|||||||
import { ScanOptions, ZMember } from '../commands/generic-transformers';
|
import { ScanOptions, ZMember } from '../commands/generic-transformers';
|
||||||
import { ScanCommandOptions } from '../commands/SCAN';
|
import { ScanCommandOptions } from '../commands/SCAN';
|
||||||
import { HScanTuple } from '../commands/HSCAN';
|
import { HScanTuple } from '../commands/HSCAN';
|
||||||
import { extendWithCommands, extendWithModulesAndScripts, LegacyCommandArguments, transformCommandArguments, transformCommandReply, transformLegacyCommandArguments } from '../commander';
|
import { extendWithCommands, extendWithModulesAndScripts, transformCommandArguments, transformCommandReply } from '../commander';
|
||||||
import { Pool, Options as PoolOptions, createPool } from 'generic-pool';
|
import { Pool, Options as PoolOptions, createPool } from 'generic-pool';
|
||||||
import { ClientClosedError, DisconnectsClientError, AuthError } from '../errors';
|
import { ClientClosedError, DisconnectsClientError, AuthError } from '../errors';
|
||||||
import { URL } from 'url';
|
import { URL } from 'url';
|
||||||
@@ -83,7 +83,6 @@ export interface ClientCommandOptions extends QueueCommandOptions {
|
|||||||
|
|
||||||
type ClientLegacyCallback = (err: Error | null, reply?: RedisCommandRawReply) => void;
|
type ClientLegacyCallback = (err: Error | null, reply?: RedisCommandRawReply) => void;
|
||||||
|
|
||||||
export type ClientLegacyCommandArguments = LegacyCommandArguments | [...LegacyCommandArguments, ClientLegacyCallback];
|
|
||||||
export default class RedisClient<M extends RedisModules, S extends RedisScripts> extends EventEmitter {
|
export default class RedisClient<M extends RedisModules, S extends RedisScripts> extends EventEmitter {
|
||||||
static commandOptions<T extends ClientCommandOptions>(options: T): CommandOptions<T> {
|
static commandOptions<T extends ClientCommandOptions>(options: T): CommandOptions<T> {
|
||||||
return commandOptions(options);
|
return commandOptions(options);
|
||||||
@@ -292,13 +291,13 @@ export default class RedisClient<M extends RedisModules, S extends RedisScripts>
|
|||||||
if (!this.#options?.legacyMode) return;
|
if (!this.#options?.legacyMode) return;
|
||||||
|
|
||||||
(this as any).#v4.sendCommand = this.#sendCommand.bind(this);
|
(this as any).#v4.sendCommand = this.#sendCommand.bind(this);
|
||||||
(this as any).sendCommand = (...args: ClientLegacyCommandArguments): void => {
|
(this as any).sendCommand = (...args: Array<any>): void => {
|
||||||
let callback: ClientLegacyCallback;
|
let callback: ClientLegacyCallback;
|
||||||
if (typeof args[args.length - 1] === 'function') {
|
if (typeof args[args.length - 1] === 'function') {
|
||||||
callback = args.pop() as ClientLegacyCallback;
|
callback = args.pop() as ClientLegacyCallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.#sendCommand(transformLegacyCommandArguments(args as LegacyCommandArguments))
|
this.#sendCommand(args.flat())
|
||||||
.then((reply: RedisCommandRawReply) => {
|
.then((reply: RedisCommandRawReply) => {
|
||||||
if (!callback) return;
|
if (!callback) return;
|
||||||
|
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import COMMANDS from './commands';
|
import COMMANDS from './commands';
|
||||||
import { RedisCommand, RedisCommandArguments, RedisCommandRawReply, RedisModules, RedisPlugins, RedisScript, RedisScripts } from '../commands';
|
import { RedisCommand, RedisCommandArguments, RedisCommandRawReply, RedisModules, RedisPlugins, RedisScript, RedisScripts } from '../commands';
|
||||||
import RedisMultiCommand, { RedisMultiQueuedCommand } from '../multi-command';
|
import RedisMultiCommand, { RedisMultiQueuedCommand } from '../multi-command';
|
||||||
import { extendWithCommands, extendWithModulesAndScripts, LegacyCommandArguments, transformLegacyCommandArguments } from '../commander';
|
import { extendWithCommands, extendWithModulesAndScripts } from '../commander';
|
||||||
import { ExcludeMappedString } from '.';
|
import { ExcludeMappedString } from '.';
|
||||||
|
|
||||||
type RedisClientMultiCommandSignature<C extends RedisCommand, M extends RedisModules, S extends RedisScripts> =
|
type RedisClientMultiCommandSignature<C extends RedisCommand, M extends RedisModules, S extends RedisScripts> =
|
||||||
@@ -53,8 +53,8 @@ export default class RedisClientMultiCommand {
|
|||||||
|
|
||||||
#legacyMode(): void {
|
#legacyMode(): void {
|
||||||
this.v4.addCommand = this.addCommand.bind(this);
|
this.v4.addCommand = this.addCommand.bind(this);
|
||||||
(this as any).addCommand = (...args: LegacyCommandArguments): this => {
|
(this as any).addCommand = (...args: Array<any>): this => {
|
||||||
this.#multi.addCommand(transformLegacyCommandArguments(args));
|
this.#multi.addCommand(args.flat());
|
||||||
return this;
|
return this;
|
||||||
};
|
};
|
||||||
this.v4.exec = this.exec.bind(this);
|
this.v4.exec = this.exec.bind(this);
|
||||||
|
@@ -249,18 +249,16 @@ export default class RedisSocket extends EventEmitter {
|
|||||||
#isCorked = false;
|
#isCorked = false;
|
||||||
|
|
||||||
cork(): void {
|
cork(): void {
|
||||||
if (!this.#socket) {
|
if (!this.#socket || this.#isCorked) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.#isCorked) {
|
this.#socket.cork();
|
||||||
this.#socket.cork();
|
this.#isCorked = true;
|
||||||
this.#isCorked = true;
|
|
||||||
|
|
||||||
queueMicrotask(() => {
|
queueMicrotask(() => {
|
||||||
this.#socket?.uncork();
|
this.#socket?.uncork();
|
||||||
this.#isCorked = false;
|
this.#isCorked = false;
|
||||||
});
|
});
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -2,42 +2,34 @@ import { strict as assert } from 'assert';
|
|||||||
import { describe } from 'mocha';
|
import { describe } from 'mocha';
|
||||||
import { encodeCommand } from './commander';
|
import { encodeCommand } from './commander';
|
||||||
|
|
||||||
function encodeCommandToString(...args: Parameters<typeof encodeCommand>): string {
|
|
||||||
const arr = [];
|
|
||||||
for (const item of encodeCommand(...args)) {
|
|
||||||
arr.push(item.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
return arr.join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('Commander', () => {
|
describe('Commander', () => {
|
||||||
describe('encodeCommand (see #1628)', () => {
|
describe('encodeCommand (see #1628)', () => {
|
||||||
it('1 byte', () => {
|
it('1 byte', () => {
|
||||||
assert.equal(
|
assert.deepEqual(
|
||||||
encodeCommandToString(['a', 'z']),
|
[...encodeCommand(['a', 'z'])],
|
||||||
'*2\r\n$1\r\na\r\n$1\r\nz\r\n'
|
['*2\r\n$1\r\na\r\n$1\r\nz\r\n']
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('2 bytes', () => {
|
it('2 bytes', () => {
|
||||||
assert.equal(
|
assert.deepEqual(
|
||||||
encodeCommandToString(['א', 'ת']),
|
[...encodeCommand(['א', 'ת'])],
|
||||||
'*2\r\n$2\r\nא\r\n$2\r\nת\r\n'
|
['*2\r\n$2\r\nא\r\n$2\r\nת\r\n']
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('4 bytes', () => {
|
it('4 bytes', () => {
|
||||||
assert.equal(
|
assert.deepEqual(
|
||||||
encodeCommandToString(['🐣', '🐤']),
|
[...encodeCommand(['🐣', '🐤'])],
|
||||||
'*2\r\n$4\r\n🐣\r\n$4\r\n🐤\r\n'
|
['*2\r\n$4\r\n🐣\r\n$4\r\n🐤\r\n']
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('with a buffer', () => {
|
it('with a buffer', () => {
|
||||||
assert.equal(
|
assert.deepEqual(
|
||||||
encodeCommandToString([Buffer.from('string')]),
|
[...encodeCommand([Buffer.from('string')])],
|
||||||
'*1\r\n$6\r\nstring\r\n'
|
['*1\r\n$6\r\n', Buffer.from('string'), '\r\n']
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -95,25 +95,25 @@ export function* encodeCommand(args: RedisCommandArguments): IterableIterator<Re
|
|||||||
let strings = `*${args.length}${DELIMITER}`,
|
let strings = `*${args.length}${DELIMITER}`,
|
||||||
stringsLength = 0;
|
stringsLength = 0;
|
||||||
for (const arg of args) {
|
for (const arg of args) {
|
||||||
const isString = typeof arg === 'string',
|
if (Buffer.isBuffer(arg)) {
|
||||||
byteLength = isString ? Buffer.byteLength(arg): arg.length;
|
yield `${strings}$${arg.length}${DELIMITER}`;
|
||||||
strings += `$${byteLength}${DELIMITER}`;
|
|
||||||
|
|
||||||
if (isString) {
|
|
||||||
const totalLength = stringsLength + byteLength;
|
|
||||||
if (totalLength > 1024) {
|
|
||||||
yield strings;
|
|
||||||
strings = arg;
|
|
||||||
stringsLength = byteLength;
|
|
||||||
} else {
|
|
||||||
strings += arg;
|
|
||||||
stringsLength = totalLength;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
yield strings;
|
|
||||||
strings = '';
|
strings = '';
|
||||||
stringsLength = 0;
|
stringsLength = 0;
|
||||||
yield arg;
|
yield arg;
|
||||||
|
} else {
|
||||||
|
const string = arg?.toString?.() ?? '',
|
||||||
|
byteLength = Buffer.byteLength(string);
|
||||||
|
strings += `$${byteLength}${DELIMITER}`;
|
||||||
|
|
||||||
|
const totalLength = stringsLength + byteLength;
|
||||||
|
if (totalLength > 1024) {
|
||||||
|
yield strings;
|
||||||
|
strings = string;
|
||||||
|
stringsLength = byteLength;
|
||||||
|
} else {
|
||||||
|
strings += string;
|
||||||
|
stringsLength = totalLength;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
strings += DELIMITER;
|
strings += DELIMITER;
|
||||||
@@ -133,18 +133,3 @@ export function transformCommandReply(
|
|||||||
|
|
||||||
return command.transformReply(rawReply, preserved);
|
return command.transformReply(rawReply, preserved);
|
||||||
}
|
}
|
||||||
|
|
||||||
export type LegacyCommandArguments = Array<string | number | Buffer | LegacyCommandArguments>;
|
|
||||||
|
|
||||||
export function transformLegacyCommandArguments(args: LegacyCommandArguments, flat: RedisCommandArguments = []): RedisCommandArguments {
|
|
||||||
for (const arg of args) {
|
|
||||||
if (Array.isArray(arg)) {
|
|
||||||
transformLegacyCommandArguments(arg, flat);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
flat.push(typeof arg === 'number' ? arg.toString() : arg);
|
|
||||||
}
|
|
||||||
|
|
||||||
return flat;
|
|
||||||
}
|
|
||||||
|
Reference in New Issue
Block a user