Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Node: Add XRANGE command #2069

Merged
merged 21 commits into from
Aug 14, 2024
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
* Node: Added ZMPOP command ([#1994](https://github.com/valkey-io/valkey-glide/pull/1994))
* Node: Added ZINCRBY command ([#2009](https://github.com/valkey-io/valkey-glide/pull/2009))
* Node: Added BZMPOP command ([#2018](https://github.com/valkey-io/valkey-glide/pull/2018))
* Node: Added XRANGE command ([#2069](https://github.com/valkey-io/valkey-glide/pull/2069))
* Node: Added PFMERGE command ([#2053](https://github.com/valkey-io/valkey-glide/pull/2053))
* Node: Added ZLEXCOUNT command ([#2022](https://github.com/valkey-io/valkey-glide/pull/2022))
* Node: Added ZREMRANGEBYLEX command ([#2025]((https://github.com/valkey-io/valkey-glide/pull/2025))
Expand Down
40 changes: 40 additions & 0 deletions node/src/BaseClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import {
SearchOrigin,
SetOptions,
StreamAddOptions,
StreamRangeBound,
StreamReadOptions,
StreamTrimOptions,
ZAddOptions,
Expand Down Expand Up @@ -141,6 +142,7 @@ import {
createUnlink,
createXAdd,
createXLen,
createXRange,
createXRead,
createXTrim,
createZAdd,
Expand Down Expand Up @@ -2560,6 +2562,44 @@ export class BaseClient {
return this.createWritePromise(scriptInvocation);
}

/**
* Returns stream entries matching a given range of IDs.
*
* See https://valkey.io/commands/xrange for more details.
*
* @param key - The key of the stream.
* @param start - The starting stream ID bound for the range.
Yury-Fridlyand marked this conversation as resolved.
Show resolved Hide resolved
* - Use `exclusive: "("` to specify an exclusive bounded stream ID.
* - Use `-` to start with the minimum available ID.
Yury-Fridlyand marked this conversation as resolved.
Show resolved Hide resolved
* @param end - The ending stream ID bound for the range.
* - Use `exclusive: "("` to specify an exclusive bounded stream ID.
* - Use `+` to end with the maximum available ID.
Yury-Fridlyand marked this conversation as resolved.
Show resolved Hide resolved
Yury-Fridlyand marked this conversation as resolved.
Show resolved Hide resolved
* @param count - An optional argument specifying the maximum count of stream entries to return.
* If `count` is not provided, all stream entries in the range will be returned.
* @returns A mapping of stream IDs to stream entry data, where entry data is a
Yury-Fridlyand marked this conversation as resolved.
Show resolved Hide resolved
* list of pairings with format `[[field, entry], [field, entry], ...]`.
*
* @example
* ```typescript
* await client.xadd("mystream", [["field1", "value1"]], {id: "0-1"});
* await client.xadd("mystream", [["field2", "value2"], ["field2", "value3"]], {id: "0-2"});
* const result = await client.xrange("mystream", "-", "+");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* const result = await client.xrange("mystream", "-", "+");
* console.log(await client.xrange("mystream", "-", "+"));

* // Output:
* // {
* // "0-1": [["field1", "value1"]],
* // "0-2": [["field2", "value2"], ["field2", "value3"]],
* // } // Indicates the stream IDs and their associated field-value pairs for all stream entries in "mystream".
* ```
*/
public async xrange(
key: string,
start: StreamRangeBound,
end: StreamRangeBound,
count?: number,
): Promise<Record<string, string[][]> | null> {
Yury-Fridlyand marked this conversation as resolved.
Show resolved Hide resolved
return this.createWritePromise(createXRange(key, start, end, count));
}

/** Adds members with their scores to the sorted set stored at `key`.
* If a member is already a part of the sorted set, its score is updated.
* See https://valkey.io/commands/zadd/ for more details.
Expand Down
55 changes: 55 additions & 0 deletions node/src/Commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1974,6 +1974,61 @@ export function createXTrim(
return createCommand(RequestType.XTrim, args);
}

export type StreamRangeBound =
Yury-Fridlyand marked this conversation as resolved.
Show resolved Hide resolved
/**
* Stream ID boundary used to specify the minimum stream entry ID. Can be used in the `XRANGE` or `XREVRANGE` commands
* to get the first stream ID.
*/
| "-"
/**
* Stream ID boundary used to specify the maximum stream entry ID. Can be used in the `XRANGE` or `XREVRANGE` commands
* to get the last stream ID.
*/
| "+"
/**
* Stream ID boundary used to specify a range of IDs to search. Stream ID bounds can be complete with
* a timestamp and sequence number separated by a dash ("-"), for example "1526985054069-0". Stream ID bounds can also
* be incomplete, with just a timestamp. Can be specified as inclusive or exclusive, where inclusive is the default.
*/
| {
exclusive?: "(";
Yury-Fridlyand marked this conversation as resolved.
Show resolved Hide resolved
id: string | number;
};

function addRangeBound(rangeBound: StreamRangeBound, args: string[]) {
Yury-Fridlyand marked this conversation as resolved.
Show resolved Hide resolved
if (rangeBound === "-" || rangeBound === "+") {
args.push(rangeBound);
return;
}

if (rangeBound.exclusive) {
args.push(rangeBound.exclusive + rangeBound.id.toString());
} else {
args.push(rangeBound.id.toString());
}
}
Yury-Fridlyand marked this conversation as resolved.
Show resolved Hide resolved

/**
* @internal
*/
export function createXRange(
key: string,
start: StreamRangeBound,
end: StreamRangeBound,
Yury-Fridlyand marked this conversation as resolved.
Show resolved Hide resolved
count?: number,
): command_request.Command {
const args = [key];
addRangeBound(start, args);
addRangeBound(end, args);

if (count !== undefined) {
args.push("COUNT");
args.push(count.toString());
}

return createCommand(RequestType.XRange, args);
}

/**
* @internal
*/
Expand Down
29 changes: 29 additions & 0 deletions node/src/Transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import {
SearchOrigin,
SetOptions,
StreamAddOptions,
StreamRangeBound,
StreamReadOptions,
StreamTrimOptions,
ZAddOptions,
Expand Down Expand Up @@ -165,6 +166,7 @@ import {
createUnlink,
createXAdd,
createXLen,
createXRange,
createXRead,
createXTrim,
createZAdd,
Expand Down Expand Up @@ -2013,6 +2015,33 @@ export class BaseTransaction<T extends BaseTransaction<T>> {
return this.addAndReturn(createTime());
}

/**
* Returns stream entries matching a given range of IDs.
Yury-Fridlyand marked this conversation as resolved.
Show resolved Hide resolved
*
* See https://valkey.io/commands/xrange for more details.
*
* @param key - The key of the stream.
* @param start - The starting stream ID bound for the range.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mention what id is for the range bound?

* - Use `exclusive: "("` to specify an exclusive bounded stream ID.
* - Use `-` to start with the minimum available ID.
* @param end - The ending stream ID bound for the range.
* - Use `exclusive: "("` to specify an exclusive bounded stream ID.
* - Use `+` to end with the maximum available ID.
* @param count - An optional argument specifying the maximum count of stream entries to return.
* If `count` is not provided, all stream entries in the range will be returned.
*
* Command Response - A mapping of stream IDs to stream entry data, where entry data is a
* list of pairings with format `[[field, entry], [field, entry], ...]`.
*/
public xrange(
key: string,
start: StreamRangeBound,
end: StreamRangeBound,
count?: number,
): T {
return this.addAndReturn(createXRange(key, start, end, count));
}

/**
* Reads entries from the given streams.
* See https://valkey.io/commands/xread/ for more details.
Expand Down
82 changes: 82 additions & 0 deletions node/tests/SharedTests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4263,6 +4263,88 @@ export function runBaseTests<Context>(config: {
config.timeout,
);

it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])(
`xrange test_%p`,
async (protocol) => {
await runTest(async (client: BaseClient) => {
const key = uuidv4();
const nonExistingKey = uuidv4();
const stringKey = uuidv4();
const streamId1 = "0-1";
const streamId2 = "0-2";
const streamId3 = "0-3";

expect(
await client.xadd(key, [["f1", "v1"]], { id: streamId1 }),
).toEqual(streamId1);
expect(
await client.xadd(key, [["f2", "v2"]], { id: streamId2 }),
).toEqual(streamId2);
expect(await client.xlen(key)).toEqual(2);

// get everything from the stream
expect(await client.xrange(key, "-", "+")).toEqual({
[streamId1]: [["f1", "v1"]],
[streamId2]: [["f2", "v2"]],
});

// returns empty mapping if + before -
expect(await client.xrange(key, "+", "-")).toEqual({});

expect(
await client.xadd(key, [["f3", "v3"]], { id: streamId3 }),
).toEqual(streamId3);

// get the newest entry
expect(
await client.xrange(
key,
{ exclusive: "(", id: streamId2 },
{ id: 5 },
1,
),
).toEqual({ [streamId3]: [["f3", "v3"]] });

// xrange against an emptied stream
expect(
await client.customCommand([
"XDEL",
key,
streamId1,
streamId2,
streamId3,
]),
).toEqual(3);
expect(await client.xrange(key, "-", "+", 10)).toEqual({});

expect(await client.xrange(nonExistingKey, "-", "+")).toEqual(
{},
);

// count value < 1 returns null
expect(await client.xrange(key, "-", "+", 0)).toEqual(null);
expect(await client.xrange(key, "-", "+", -1)).toEqual(null);

// key exists, but it is not a stream
expect(await client.set(stringKey, "foo"));
await expect(
client.xrange(stringKey, "-", "+"),
).rejects.toThrow(RequestError);

// invalid start bound
await expect(
client.xrange(key, { id: "not_a_stream_id" }, "+"),
).rejects.toThrow(RequestError);

// invalid end bound
await expect(
client.xrange(key, "-", { id: "not_a_stream_id" }),
).rejects.toThrow(RequestError);
}, protocol);
},
config.timeout,
);

it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])(
`zremRangeByLex test_%p`,
async (protocol) => {
Expand Down
2 changes: 2 additions & 0 deletions node/tests/TestUtilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -809,6 +809,8 @@ export async function transactionTest(
]);
baseTransaction.xlen(key9);
responseData.push(["xlen(key9)", 3]);
baseTransaction.xrange(key9, { id: "0-1" }, { id: "0-1" });
responseData.push(["xrange(key9)", { "0-1": [["field", "value1"]] }]);
baseTransaction.xread({ [key9]: "0-1" });
responseData.push([
'xread({ [key9]: "0-1" })',
Expand Down
Loading