1
0
mirror of https://github.com/redis/node-redis.git synced 2025-12-11 09:22:35 +03:00

feat(client): add msetex command and tests for it (#3116)

This commit is contained in:
Pavel Pashov
2025-11-03 13:53:18 +02:00
committed by GitHub
parent 2fdb6def45
commit 38bfaa7c90
3 changed files with 539 additions and 0 deletions

View File

@@ -0,0 +1,393 @@
import { strict as assert } from "node:assert";
import testUtils, { GLOBAL } from "../test-utils";
import MSETEX, { ExpirationMode, SetMode } from "./MSETEX";
import { parseArgs } from "./generic-transformers";
describe("MSETEX", () => {
describe("transformArguments", () => {
it("single key-value pair as array", () => {
assert.deepEqual(parseArgs(MSETEX, ["key1", "value1"]), [
"MSETEX",
"1",
"key1",
"value1",
]);
});
it("array of key value pairs", () => {
assert.deepEqual(
parseArgs(MSETEX, [
"key1",
"value1",
"key2",
"value2",
"key3",
"value3",
]),
["MSETEX", "3", "key1", "value1", "key2", "value2", "key3", "value3"]
);
});
it("array of tuples", () => {
assert.deepEqual(
parseArgs(MSETEX, [
["key1", "value1"],
["key2", "value2"],
]),
["MSETEX", "2", "key1", "value1", "key2", "value2"]
);
});
it("object of key value pairs", () => {
assert.deepEqual(
parseArgs(MSETEX, {
key1: "value1",
key2: "value2",
}),
["MSETEX", "2", "key1", "value1", "key2", "value2"]
);
});
it("with EX expiration", () => {
assert.deepEqual(
parseArgs(
MSETEX,
{
key1: "value1",
key2: "value2",
},
{
expiration: {
type: ExpirationMode.EX,
value: 1,
},
}
),
["MSETEX", "2", "key1", "value1", "key2", "value2", "EX", "1"]
);
});
it("with NX set mode", () => {
assert.deepEqual(
parseArgs(
MSETEX,
[
["key1", "value1"],
["key2", "value2"],
],
{
mode: SetMode.NX,
}
),
["MSETEX", "2", "key1", "value1", "key2", "value2", "NX"]
);
});
it("with XX set mode and PX expiration", () => {
assert.deepEqual(
parseArgs(MSETEX, ["key1", "value1", "key2", "value2"], {
mode: SetMode.XX,
expiration: {
type: ExpirationMode.PX,
value: 1,
},
}),
["MSETEX", "2", "key1", "value1", "key2", "value2", "XX", "PX", "1"]
);
});
it("with EXAT Date expiration", () => {
assert.deepEqual(
parseArgs(
MSETEX,
{
key1: "value1",
key2: "value2",
},
{
expiration: {
type: ExpirationMode.EXAT,
value: new Date("2025-10-28T11:23:36.203Z"),
},
}
),
[
"MSETEX",
"2",
"key1",
"value1",
"key2",
"value2",
"EXAT",
"1761650616",
]
);
});
it("with EXAT numeric expiration", () => {
assert.deepEqual(
parseArgs(
MSETEX,
[
["key1", "value1"],
["key2", "value2"],
],
{
expiration: {
type: ExpirationMode.EXAT,
value: 1761650616,
},
}
),
[
"MSETEX",
"2",
"key1",
"value1",
"key2",
"value2",
"EXAT",
"1761650616",
]
);
});
it("with PXAT Date expiration", () => {
assert.deepEqual(
parseArgs(MSETEX, ["key1", "value1", "key2", "value2"], {
expiration: {
type: ExpirationMode.PXAT,
value: new Date("2025-10-28T11:23:36.203Z"),
},
}),
[
"MSETEX",
"2",
"key1",
"value1",
"key2",
"value2",
"PXAT",
"1761650616203",
]
);
});
it("with PXAT numeric expiration", () => {
assert.deepEqual(
parseArgs(
MSETEX,
{
key1: "value1",
key2: "value2",
},
{
expiration: {
type: ExpirationMode.PXAT,
value: 1761650616203,
},
}
),
[
"MSETEX",
"2",
"key1",
"value1",
"key2",
"value2",
"PXAT",
"1761650616203",
]
);
});
it("with KEEPTTL expiration", () => {
assert.deepEqual(
parseArgs(MSETEX, ["key1", "value1", "key2", "value2"], {
expiration: {
type: ExpirationMode.KEEPTTL,
},
}),
["MSETEX", "2", "key1", "value1", "key2", "value2", "KEEPTTL"]
);
});
it("with empty expiration object", () => {
assert.deepEqual(
parseArgs(
MSETEX,
[
["key1", "value1"],
["key2", "value2"],
],
{
expiration: {},
}
),
["MSETEX", "2", "key1", "value1", "key2", "value2"]
);
});
});
testUtils.testAll(
"basic mSetEx",
async (client) => {
assert.equal(
await client.mSetEx(["{key}1", "value1", "{key}2", "value2"]),
1
);
},
{
client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 4] },
cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 4] },
}
);
testUtils.testAll(
"mSetEx with XX",
async (client) => {
const keyValuePairs = {
"{key}1": "value1",
"{key}2": "value2",
};
const keysDoNotExist = await client.mSetEx(keyValuePairs, {
mode: SetMode.XX,
});
assert.equal(keysDoNotExist, 0);
await client.mSet(keyValuePairs);
const keysExist = await client.mSetEx(keyValuePairs, {
mode: SetMode.XX,
});
assert.equal(keysExist, 1);
},
{
client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 4] },
cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 4] },
}
);
testUtils.testAll(
"mSetEx with NX",
async (client) => {
const keyValuePairs = [
["{key}1", "value1"],
["{key}2", "value2"],
] as Array<[string, string]>;
const firstAttempt = await client.mSetEx(keyValuePairs, {
mode: SetMode.NX,
});
assert.equal(firstAttempt, 1);
const secondAttempt = await client.mSetEx(keyValuePairs, {
mode: SetMode.NX,
});
assert.equal(secondAttempt, 0);
},
{
client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 4] },
cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 4] },
}
);
testUtils.testAll(
"mSetEx with PX expiration",
async (client) => {
assert.equal(
await client.mSetEx(
[
["{key}1", "value1"],
["{key}2", "value2"],
],
{
expiration: {
type: ExpirationMode.PX,
value: 500,
},
}
),
1
);
},
{
client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 4] },
cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 4] },
}
);
testUtils.testAll(
"mSetEx with EXAT expiration",
async (client) => {
assert.equal(
await client.mSetEx(
[
["{key}1", "value1"],
["{key}2", "value2"],
],
{
expiration: {
type: ExpirationMode.EXAT,
value: new Date(Date.now() + 10000),
},
}
),
1
);
},
{
client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 4] },
cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 4] },
}
);
testUtils.testAll(
"mSetEx with KEEPTTL expiration",
async (client) => {
assert.equal(
await client.mSetEx(["{key}1", "value1", "{key}2", "value2"], {
expiration: {
type: ExpirationMode.KEEPTTL,
},
}),
1
);
},
{
client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 4] },
cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 4] },
}
);
testUtils.testAll(
"mSetEx with all options",
async (client) => {
assert.equal(
await client.mSetEx(
{
"{key}1": "value1",
"{key}2": "value2",
},
{
expiration: {
type: ExpirationMode.PXAT,
value: Date.now() + 10000,
},
mode: SetMode.NX,
}
),
1
);
},
{
client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 4] },
cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 4] },
}
);
});

View File

@@ -0,0 +1,143 @@
import { CommandParser } from "../client/parser";
import { NumberReply, Command, RedisArgument } from "../RESP/types";
import { transformEXAT, transformPXAT } from "./generic-transformers";
import { MSetArguments } from "./MSET";
export const SetMode = {
/**
* Only set if all keys exist
*/
XX: "XX",
/**
* Only set if none of the keys exist
*/
NX: "NX",
} as const;
export type SetMode = (typeof SetMode)[keyof typeof SetMode];
export const ExpirationMode = {
/**
* Relative expiration (seconds)
*/
EX: "EX",
/**
* Relative expiration (milliseconds)
*/
PX: "PX",
/**
* Absolute expiration (Unix timestamp in seconds)
*/
EXAT: "EXAT",
/**
* Absolute expiration (Unix timestamp in milliseconds)
*/
PXAT: "PXAT",
/**
* Keep existing TTL
*/
KEEPTTL: "KEEPTTL",
} as const;
export type ExpirationMode =
(typeof ExpirationMode)[keyof typeof ExpirationMode];
type SetConditionOption = typeof SetMode.XX | typeof SetMode.NX;
type ExpirationOption =
| { type: typeof ExpirationMode.EX; value: number }
| { type: typeof ExpirationMode.PX; value: number }
| { type: typeof ExpirationMode.EXAT; value: number | Date }
| { type: typeof ExpirationMode.PXAT; value: number | Date }
| { type: typeof ExpirationMode.KEEPTTL };
export function parseMSetExArguments(
parser: CommandParser,
keyValuePairs: MSetArguments
) {
let tuples: Array<[RedisArgument, RedisArgument]> = [];
if (Array.isArray(keyValuePairs)) {
if (keyValuePairs.length == 0) {
throw new Error("empty keyValuePairs Argument");
}
if (Array.isArray(keyValuePairs[0])) {
tuples = keyValuePairs as Array<[RedisArgument, RedisArgument]>;
} else {
const arr = keyValuePairs as Array<RedisArgument>;
for (let i = 0; i < arr.length; i += 2) {
tuples.push([arr[i], arr[i + 1]]);
}
}
} else {
for (const tuple of Object.entries(keyValuePairs)) {
tuples.push([tuple[0], tuple[1]]);
}
}
// Push the number of keys
parser.push(tuples.length.toString());
for (const tuple of tuples) {
parser.pushKey(tuple[0]);
parser.push(tuple[1]);
}
}
export default {
/**
* Constructs the MSETEX command.
*
* Atomically sets multiple string keys with a shared expiration in a single operation.
*
* @param parser - The command parser
* @param keyValuePairs - Key-value pairs to set (array of tuples, flat array, or object)
* @param options - Configuration for expiration and set modes
* @see https://redis.io/commands/msetex/
*/
parseCommand(
parser: CommandParser,
keyValuePairs: MSetArguments,
options?: {
expiration?: ExpirationOption;
mode?: SetConditionOption;
}
) {
parser.push("MSETEX");
// Push number of keys and key-value pairs before the options
parseMSetExArguments(parser, keyValuePairs);
if (options?.mode) {
parser.push(options.mode);
}
if (options?.expiration) {
switch (options.expiration.type) {
case ExpirationMode.EXAT:
parser.push(
ExpirationMode.EXAT,
transformEXAT(options.expiration.value)
);
break;
case ExpirationMode.PXAT:
parser.push(
ExpirationMode.PXAT,
transformPXAT(options.expiration.value)
);
break;
case ExpirationMode.KEEPTTL:
parser.push(ExpirationMode.KEEPTTL);
break;
case ExpirationMode.EX:
case ExpirationMode.PX:
parser.push(
options.expiration.type,
options.expiration.value?.toString()
);
break;
}
}
},
transformReply: undefined as unknown as () => NumberReply<0 | 1>,
} as const satisfies Command;

View File

@@ -206,6 +206,7 @@ import MODULE_LOAD from './MODULE_LOAD';
import MODULE_UNLOAD from './MODULE_UNLOAD';
import MOVE from './MOVE';
import MSET from './MSET';
import MSETEX from './MSETEX';
import MSETNX from './MSETNX';
import OBJECT_ENCODING from './OBJECT_ENCODING';
import OBJECT_FREQ from './OBJECT_FREQ';
@@ -788,6 +789,8 @@ export default {
move: MOVE,
MSET,
mSet: MSET,
MSETEX,
mSetEx: MSETEX,
MSETNX,
mSetNX: MSETNX,
OBJECT_ENCODING,