Skip to content

Commit 3bbe51b

Browse files
authored
Node: added ZMPOP command (#1994)
* Add zmpop command Signed-off-by: Guian Gumpac <guian.gumpac@improving.com>
1 parent db92a93 commit 3bbe51b

File tree

8 files changed

+167
-1
lines changed

8 files changed

+167
-1
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
* Node: Added FUNCTION LOAD command ([#1969](https://github.com/valkey-io/valkey-glide/pull/1969))
2121
* Node: Added FUNCTION DELETE command ([#1990](https://github.com/valkey-io/valkey-glide/pull/1990))
2222
* Node: Added FUNCTION FLUSH command ([#1984](https://github.com/valkey-io/valkey-glide/pull/1984))
23+
* Node: Added ZMPOP command ([#1994](https://github.com/valkey-io/valkey-glide/pull/1994))
2324

2425
## 1.0.0 (2024-07-09)
2526

node/npm/glide/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ function initialize() {
116116
ClusterTransaction,
117117
Transaction,
118118
PubSubMsg,
119+
ScoreFilter,
119120
createLeakedArray,
120121
createLeakedAttribute,
121122
createLeakedBigint,
@@ -168,6 +169,7 @@ function initialize() {
168169
ClusterTransaction,
169170
Transaction,
170171
PubSubMsg,
172+
ScoreFilter,
171173
createLeakedArray,
172174
createLeakedAttribute,
173175
createLeakedBigint,

node/src/BaseClient.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
RangeByLex,
2020
RangeByScore,
2121
ScoreBoundary,
22+
ScoreFilter,
2223
SetOptions,
2324
StreamAddOptions,
2425
StreamReadOptions,
@@ -112,6 +113,7 @@ import {
112113
createZDiffWithScores,
113114
createZInterCard,
114115
createZInterstore,
116+
createZMPop,
115117
createZMScore,
116118
createZPopMax,
117119
createZPopMin,
@@ -3416,6 +3418,39 @@ export class BaseClient {
34163418
);
34173419
}
34183420

3421+
/**
3422+
* Pops a member-score pair from the first non-empty sorted set, with the given `keys`
3423+
* being checked in the order they are provided.
3424+
*
3425+
* See https://valkey.io/commands/zmpop/ for more details.
3426+
*
3427+
* @remarks When in cluster mode, all `keys` must map to the same hash slot.
3428+
* @param keys - The keys of the sorted sets.
3429+
* @param modifier - The element pop criteria - either {@link ScoreFilter.MIN} or
3430+
* {@link ScoreFilter.MAX} to pop the member with the lowest/highest score accordingly.
3431+
* @param count - The number of elements to pop.
3432+
* @returns A two-element `array` containing the key name of the set from which the element
3433+
* was popped, and a member-score `Record` of the popped element.
3434+
* If no member could be popped, returns `null`.
3435+
*
3436+
* since Valkey version 7.0.0.
3437+
*
3438+
* @example
3439+
* ```typescript
3440+
* await client.zadd("zSet1", { one: 1.0, two: 2.0, three: 3.0 });
3441+
* await client.zadd("zSet2", { four: 4.0 });
3442+
* console.log(await client.zmpop(["zSet1", "zSet2"], ScoreFilter.MAX, 2));
3443+
* // Output: [ "zSet1", { three: 3, two: 2 } ] - "three" with score 3 and "two" with score 2 were popped from "zSet1".
3444+
* ```
3445+
*/
3446+
public zmpop(
3447+
key: string[],
3448+
modifier: ScoreFilter,
3449+
count?: number,
3450+
): Promise<[string, [Record<string, number>]] | null> {
3451+
return this.createWritePromise(createZMPop(key, modifier, count));
3452+
}
3453+
34193454
/**
34203455
* @internal
34213456
*/

node/src/Commands.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1869,3 +1869,33 @@ export function createZRevRankWithScore(
18691869
): command_request.Command {
18701870
return createCommand(RequestType.ZRevRank, [key, member, "WITHSCORE"]);
18711871
}
1872+
1873+
/**
1874+
* Mandatory option for zmpop.
1875+
* Defines which elements to pop from the sorted set.
1876+
*/
1877+
export enum ScoreFilter {
1878+
/** Pop elements with the highest scores. */
1879+
MAX = "MAX",
1880+
/** Pop elements with the lowest scores. */
1881+
MIN = "MIN",
1882+
}
1883+
1884+
/**
1885+
* @internal
1886+
*/
1887+
export function createZMPop(
1888+
keys: string[],
1889+
modifier: ScoreFilter,
1890+
count?: number,
1891+
): command_request.Command {
1892+
const args: string[] = [keys.length.toString()].concat(keys);
1893+
args.push(modifier);
1894+
1895+
if (count !== undefined) {
1896+
args.push("COUNT");
1897+
args.push(count.toString());
1898+
}
1899+
1900+
return createCommand(RequestType.ZMPop, args);
1901+
}

node/src/Transaction.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
RangeByLex,
1414
RangeByScore,
1515
ScoreBoundary,
16+
ScoreFilter,
1617
SetOptions,
1718
StreamAddOptions,
1819
StreamReadOptions,
@@ -136,6 +137,7 @@ import {
136137
createZRemRangeByScore,
137138
createZRevRank,
138139
createZRevRankWithScore,
140+
createZMPop,
139141
createZScore,
140142
} from "./Commands";
141143
import { command_request } from "./ProtobufMessage";
@@ -1940,7 +1942,7 @@ export class BaseTransaction<T extends BaseTransaction<T>> {
19401942
* @param element - The value to search for within the list.
19411943
* @param options - The LPOS options.
19421944
*
1943-
* Command Response - The index of `element`, or `null` if `element` is not in the list. If the `count`
1945+
* Command Response - The index of `element`, or `null` if `element` is not in the list. If the `count`
19441946
* option is specified, then the function returns an `array` of indices of matching elements within the list.
19451947
*
19461948
* since - Valkey version 6.0.6.
@@ -2001,6 +2003,27 @@ export class BaseTransaction<T extends BaseTransaction<T>> {
20012003
createGeoAdd(key, membersToGeospatialData, options),
20022004
);
20032005
}
2006+
2007+
/**
2008+
* Pops a member-score pair from the first non-empty sorted set, with the given `keys`
2009+
* being checked in the order they are provided.
2010+
*
2011+
* See https://valkey.io/commands/zmpop/ for more details.
2012+
*
2013+
* @param keys - The keys of the sorted sets.
2014+
* @param modifier - The element pop criteria - either {@link ScoreFilter.MIN} or
2015+
* {@link ScoreFilter.MAX} to pop the member with the lowest/highest score accordingly.
2016+
* @param count - The number of elements to pop.
2017+
*
2018+
* Command Response - A two-element `array` containing the key name of the set from which the
2019+
* element was popped, and a member-score `Record` of the popped element.
2020+
* If no member could be popped, returns `null`.
2021+
*
2022+
* since Valkey version 7.0.0.
2023+
*/
2024+
public zmpop(keys: string[], modifier: ScoreFilter, count?: number): T {
2025+
return this.addAndReturn(createZMPop(keys, modifier, count));
2026+
}
20042027
}
20052028

20062029
/**

node/tests/RedisClusterClient.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
InfoOptions,
2020
ProtocolVersion,
2121
Routes,
22+
ScoreFilter,
2223
} from "..";
2324
import { RedisCluster } from "../../utils/TestUtils.js";
2425
import { FlushMode } from "../build-ts/src/commands/FlushMode";
@@ -323,6 +324,9 @@ describe("GlideClusterClient", () => {
323324

324325
if (!versionLessThan7) {
325326
promises.push(client.zintercard(["abc", "zxy", "lkn"]));
327+
promises.push(
328+
client.zmpop(["abc", "zxy", "lkn"], ScoreFilter.MAX),
329+
);
326330
}
327331

328332
for (const promise of promises) {

node/tests/SharedTests.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
InsertPosition,
1515
ProtocolVersion,
1616
RequestError,
17+
ScoreFilter,
1718
Script,
1819
parseInfoResponse,
1920
} from "../";
@@ -4545,6 +4546,71 @@ export function runBaseTests<Context>(config: {
45454546
},
45464547
config.timeout,
45474548
);
4549+
4550+
it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])(
4551+
`zmpop test_%p`,
4552+
async (protocol) => {
4553+
await runTest(async (client: BaseClient) => {
4554+
if (await checkIfServerVersionLessThan("7.0.0")) return;
4555+
const key1 = "{key}-1" + uuidv4();
4556+
const key2 = "{key}-2" + uuidv4();
4557+
const nonExistingKey = "{key}-0" + uuidv4();
4558+
const stringKey = "{key}-string" + uuidv4();
4559+
4560+
expect(await client.zadd(key1, { a1: 1, b1: 2 })).toEqual(2);
4561+
expect(await client.zadd(key2, { a2: 0.1, b2: 0.2 })).toEqual(
4562+
2,
4563+
);
4564+
4565+
checkSimple(
4566+
await client.zmpop([key1, key2], ScoreFilter.MAX),
4567+
).toEqual([key1, { b1: 2 }]);
4568+
checkSimple(
4569+
await client.zmpop([key2, key1], ScoreFilter.MAX, 10),
4570+
).toEqual([key2, { a2: 0.1, b2: 0.2 }]);
4571+
4572+
expect(await client.zmpop([nonExistingKey], ScoreFilter.MIN))
4573+
.toBeNull;
4574+
expect(await client.zmpop([nonExistingKey], ScoreFilter.MIN, 1))
4575+
.toBeNull;
4576+
4577+
// key exists, but it is not a sorted set
4578+
expect(await client.set(stringKey, "value")).toEqual("OK");
4579+
await expect(
4580+
client.zmpop([stringKey], ScoreFilter.MAX),
4581+
).rejects.toThrow(RequestError);
4582+
await expect(
4583+
client.zmpop([stringKey], ScoreFilter.MAX, 1),
4584+
).rejects.toThrow(RequestError);
4585+
4586+
// incorrect argument: key list should not be empty
4587+
await expect(
4588+
client.zmpop([], ScoreFilter.MAX, 1),
4589+
).rejects.toThrow(RequestError);
4590+
4591+
// incorrect argument: count should be greater than 0
4592+
await expect(
4593+
client.zmpop([key1], ScoreFilter.MAX, 0),
4594+
).rejects.toThrow(RequestError);
4595+
4596+
// check that order of entries in the response is preserved
4597+
const entries: Record<string, number> = {};
4598+
4599+
for (let i = 0; i < 10; i++) {
4600+
// a0 => 0, a1 => 1 etc
4601+
entries["a" + i] = i;
4602+
}
4603+
4604+
expect(await client.zadd(key2, entries)).toEqual(10);
4605+
const result = await client.zmpop([key2], ScoreFilter.MIN, 10);
4606+
4607+
if (result) {
4608+
expect(result[1]).toEqual(entries);
4609+
}
4610+
}, protocol);
4611+
},
4612+
config.timeout,
4613+
);
45484614
}
45494615

45504616
export function runCommonTests<Context>(config: {

node/tests/TestUtilities.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
Logger,
1717
ProtocolVersion,
1818
ReturnType,
19+
ScoreFilter,
1920
Transaction,
2021
} from "..";
2122
import {
@@ -593,6 +594,10 @@ export async function transactionTest(
593594
args.push(0);
594595
baseTransaction.zintercard([key8, key14], 1);
595596
args.push(0);
597+
baseTransaction.zmpop([key14], ScoreFilter.MAX);
598+
args.push([key14, { two: 2.0 }]);
599+
baseTransaction.zmpop([key14], ScoreFilter.MAX, 1);
600+
args.push([key14, { one: 1.0 }]);
596601
}
597602

598603
baseTransaction.xadd(key9, [["field", "value1"]], { id: "0-1" });

0 commit comments

Comments
 (0)