Skip to content

Commit cac6492

Browse files
authored
DX-997: Make cursor field in scan commands string (#1115)
* fix: make return type of cursor commands string scan commands can return a '0' or a long number as a string. If we deserialize, we get 0 from '0' and a truncated number from the long number string. Second one is non-ideal. In this case, we want to return the cursor as a string. This means excluding the cursor from the deserialization, hence the new deserializeScanResponse method. * fix: reorder deserialize and commandOptions this way, overwriting deserialize will be possible.
1 parent 98652ea commit cac6492

File tree

10 files changed

+86
-50
lines changed

10 files changed

+86
-50
lines changed

pkg/commands/geo_search.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,8 +130,8 @@ export class GeoSearchCommand<
130130
...(opts?.withHash ? ["WITHHASH"] : []),
131131
],
132132
{
133-
...commandOptions,
134133
deserialize: transform,
134+
...commandOptions,
135135
},
136136
);
137137
}

pkg/commands/hscan.test.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ describe("without options", () => {
1414
const res = await new HScanCommand([key, 0]).exec(client);
1515

1616
expect(res.length).toBe(2);
17-
expect(typeof res[0]).toBe("number");
17+
expect(typeof res[0]).toBe("string");
1818
expect(res![1].length > 0).toBe(true);
1919
});
2020
});
@@ -23,10 +23,10 @@ describe("with match", () => {
2323
test("returns cursor and members", async () => {
2424
const key = newKey();
2525
await new HSetCommand([key, { field: "value" }]).exec(client);
26-
const res = await new HScanCommand([key, 0, { match: "field" }]).exec(client);
26+
const res = await new HScanCommand([key, "0", { match: "field" }]).exec(client);
2727

2828
expect(res.length).toBe(2);
29-
expect(typeof res[0]).toBe("number");
29+
expect(typeof res[0]).toBe("string");
3030
expect(res![1].length > 0).toBe(true);
3131
});
3232
});
@@ -35,10 +35,10 @@ describe("with count", () => {
3535
test("returns cursor and members", async () => {
3636
const key = newKey();
3737
await new HSetCommand([key, { field: "value" }]).exec(client);
38-
const res = await new HScanCommand([key, 0, { count: 1 }]).exec(client);
38+
const res = await new HScanCommand([key, "0", { count: 1 }]).exec(client);
3939

4040
expect(res.length).toBe(2);
41-
expect(typeof res[0]).toBe("number");
41+
expect(typeof res[0]).toBe("string");
4242
expect(res![1].length > 0).toBe(true);
4343
});
4444
});

pkg/commands/hscan.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,29 @@
1+
import { deserializeScanResponse } from "../util";
12
import { Command, CommandOptions } from "./command";
23
import { ScanCommandOptions } from "./scan";
34

45
/**
56
* @see https://redis.io/commands/hscan
67
*/
78
export class HScanCommand extends Command<
8-
[number, (string | number)[]],
9-
[number, (string | number)[]]
9+
[string, (string | number)[]],
10+
[string, (string | number)[]]
1011
> {
1112
constructor(
12-
[key, cursor, cmdOpts]: [key: string, cursor: number, cmdOpts?: ScanCommandOptions],
13-
opts?: CommandOptions<[number, (string | number)[]], [number, (string | number)[]]>,
13+
[key, cursor, cmdOpts]: [key: string, cursor: string | number, cmdOpts?: ScanCommandOptions],
14+
opts?: CommandOptions<[string, (string | number)[]], [string, (string | number)[]]>,
1415
) {
15-
const command = ["hscan", key, cursor];
16+
const command: (number | string)[] = ["hscan", key, cursor];
1617
if (cmdOpts?.match) {
1718
command.push("match", cmdOpts.match);
1819
}
1920
if (typeof cmdOpts?.count === "number") {
2021
command.push("count", cmdOpts.count);
2122
}
2223

23-
super(command, opts);
24+
super(command, {
25+
deserialize: deserializeScanResponse,
26+
...opts,
27+
});
2428
}
2529
}

pkg/commands/scan.test.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,13 @@ test("without options", () => {
1515
const key = newKey();
1616
const value = randomID();
1717
await new SetCommand([key, value]).exec(client);
18-
let cursor = 0;
18+
let cursor = "0";
1919
const found: string[] = [];
2020
do {
2121
const res = await new ScanCommand([cursor]).exec(client);
2222
cursor = res[0];
2323
found.push(...res[1]);
24-
} while (cursor !== 0);
24+
} while (cursor !== "0");
2525
expect(found.includes(key)).toBeTrue();
2626
});
2727
});
@@ -32,14 +32,14 @@ test("with match", () => {
3232
const value = randomID();
3333
await new SetCommand([key, value]).exec(client);
3434

35-
let cursor = 0;
35+
let cursor = "0";
3636
const found: string[] = [];
3737
do {
3838
const res = await new ScanCommand([cursor, { match: key }]).exec(client);
3939
expect(typeof res[0]).toEqual("number");
4040
cursor = res[0];
4141
found.push(...res[1]);
42-
} while (cursor !== 0);
42+
} while (cursor !== "0");
4343

4444
expect(found).toEqual([key]);
4545
});
@@ -51,13 +51,13 @@ test("with count", () => {
5151
const value = randomID();
5252
await new SetCommand([key, value]).exec(client);
5353

54-
let cursor = 0;
54+
let cursor = "0";
5555
const found: string[] = [];
5656
do {
5757
const res = await new ScanCommand([cursor, { count: 1 }]).exec(client);
5858
cursor = res[0];
5959
found.push(...res[1]);
60-
} while (cursor !== 0);
60+
} while (cursor !== "0");
6161

6262
expect(found.includes(key)).toEqual(true);
6363
});
@@ -74,13 +74,13 @@ test("with type", () => {
7474
// Add a non-string type
7575
await new ZAddCommand([key2, { score: 1, member: "abc" }]).exec(client);
7676

77-
let cursor = 0;
77+
let cursor = "0";
7878
const found: string[] = [];
7979
do {
8080
const res = await new ScanCommand([cursor, { type: "string" }]).exec(client);
8181
cursor = res[0];
8282
found.push(...res[1]);
83-
} while (cursor !== 0);
83+
} while (cursor !== "0");
8484

8585
expect(found.length).toEqual(1);
8686
for (const key of found) {

pkg/commands/scan.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { deserializeScanResponse } from "../util";
12
import { Command, CommandOptions } from "./command";
23

34
export type ScanCommandOptions = {
@@ -8,12 +9,12 @@ export type ScanCommandOptions = {
89
/**
910
* @see https://redis.io/commands/scan
1011
*/
11-
export class ScanCommand extends Command<[number, string[]], [number, string[]]> {
12+
export class ScanCommand extends Command<[string, string[]], [string, string[]]> {
1213
constructor(
13-
[cursor, opts]: [cursor: number, opts?: ScanCommandOptions],
14-
cmdOpts?: CommandOptions<[number, string[]], [number, string[]]>,
14+
[cursor, opts]: [cursor: string | number, opts?: ScanCommandOptions],
15+
cmdOpts?: CommandOptions<[string, string[]], [string, string[]]>,
1516
) {
16-
const command = ["scan", cursor];
17+
const command: (number | string)[] = ["scan", cursor];
1718
if (opts?.match) {
1819
command.push("match", opts.match);
1920
}
@@ -23,6 +24,12 @@ export class ScanCommand extends Command<[number, string[]], [number, string[]]>
2324
if (opts?.type && opts.type.length > 0) {
2425
command.push("type", opts.type);
2526
}
26-
super(command, cmdOpts);
27+
super(
28+
command,
29+
{
30+
deserialize: deserializeScanResponse,
31+
...cmdOpts,
32+
}
33+
);
2734
}
2835
}

pkg/commands/sscan.test.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,48 @@
11
import { keygen, newHttpClient, randomID } from "../test-utils";
22

3-
import { afterAll, expect, test } from "bun:test";
3+
import { afterAll, describe, expect, test } from "bun:test";
44
import { SAddCommand } from "./sadd";
55
import { SScanCommand } from "./sscan";
66
const client = newHttpClient();
77

88
const { newKey, cleanup } = keygen();
99

1010
afterAll(cleanup);
11-
test("without options", () => {
11+
describe("without options", () => {
1212
test("returns cursor and members", async () => {
1313
const key = newKey();
1414
const member = randomID();
1515
await new SAddCommand([key, member]).exec(client);
1616
const res = await new SScanCommand([key, 0]).exec(client);
1717

1818
expect(res.length).toBe(2);
19-
expect(typeof res[0]).toBe("number");
19+
expect(typeof res[0]).toBe("string");
2020
expect(res![1].length > 0).toBe(true);
2121
});
2222
});
2323

24-
test("with match", () => {
24+
describe("with match", () => {
2525
test("returns cursor and members", async () => {
2626
const key = newKey();
2727
const member = randomID();
2828
await new SAddCommand([key, member]).exec(client);
29-
const res = await new SScanCommand([key, 0, { match: member }]).exec(client);
29+
const res = await new SScanCommand([key, "0", { match: member }]).exec(client);
3030

3131
expect(res.length).toBe(2);
32-
expect(typeof res[0]).toBe("number");
32+
expect(typeof res[0]).toBe("string");
3333
expect(res![1].length > 0).toBe(true);
3434
});
3535
});
3636

37-
test("with count", () => {
37+
describe("with count", () => {
3838
test("returns cursor and members", async () => {
3939
const key = newKey();
4040
const member = randomID();
4141
await new SAddCommand([key, member]).exec(client);
42-
const res = await new SScanCommand([key, 0, { count: 1 }]).exec(client);
42+
const res = await new SScanCommand([key, "0", { count: 1 }]).exec(client);
4343

4444
expect(res.length).toBe(2);
45-
expect(typeof res[0]).toBe("number");
45+
expect(typeof res[0]).toBe("string");
4646
expect(res![1].length > 0).toBe(true);
4747
});
4848
});

pkg/commands/sscan.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,32 @@
1+
import { deserializeScanResponse } from "../util";
12
import { Command, CommandOptions } from "./command";
23
import { ScanCommandOptions } from "./scan";
34

45
/**
56
* @see https://redis.io/commands/sscan
67
*/
78
export class SScanCommand extends Command<
8-
[number, (string | number)[]],
9-
[number, (string | number)[]]
9+
[string, (string | number)[]],
10+
[string, (string | number)[]]
1011
> {
1112
constructor(
12-
[key, cursor, opts]: [key: string, cursor: number, opts?: ScanCommandOptions],
13-
cmdOpts?: CommandOptions<[number, (string | number)[]], [number, (string | number)[]]>,
13+
[key, cursor, opts]: [key: string, cursor: string | number, opts?: ScanCommandOptions],
14+
cmdOpts?: CommandOptions<[string, (string | number)[]], [string, (string | number)[]]>,
1415
) {
15-
const command = ["sscan", key, cursor];
16+
const command: (number | string)[] = ["sscan", key, cursor];
1617
if (opts?.match) {
1718
command.push("match", opts.match);
1819
}
1920
if (typeof opts?.count === "number") {
2021
command.push("count", opts.count);
2122
}
2223

23-
super(command, cmdOpts);
24+
super(
25+
command,
26+
{
27+
deserialize: deserializeScanResponse,
28+
...cmdOpts,
29+
}
30+
);
2431
}
2532
}

pkg/commands/zscan.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ describe("without options", () => {
1616
const res = await new ZScanCommand([key, 0]).exec(client);
1717

1818
expect(res.length).toBe(2);
19-
expect(typeof res[0]).toBe("number");
19+
expect(typeof res[0]).toBe("string");
2020
expect(res![1].length > 0).toBe(true);
2121
});
2222
});
@@ -26,10 +26,10 @@ describe("with match", () => {
2626
const key = newKey();
2727
const value = randomID();
2828
await new ZAddCommand([key, { score: 0, member: value }]).exec(client);
29-
const res = await new ZScanCommand([key, 0, { match: value }]).exec(client);
29+
const res = await new ZScanCommand([key, "0", { match: value }]).exec(client);
3030

3131
expect(res.length).toBe(2);
32-
expect(typeof res[0]).toBe("number");
32+
expect(typeof res[0]).toBe("string");
3333
expect(res![1].length > 0).toBe(true);
3434
});
3535
});
@@ -39,7 +39,7 @@ test("with count", () => {
3939
const key = newKey();
4040
const value = randomID();
4141
await new ZAddCommand([key, { score: 0, member: value }]).exec(client);
42-
const res = await new ZScanCommand([key, 0, { count: 1 }]).exec(client);
42+
const res = await new ZScanCommand([key, "0", { count: 1 }]).exec(client);
4343

4444
expect(res.length).toBe(2);
4545
expect(typeof res[0]).toBe("number");

pkg/commands/zscan.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,32 @@
1+
import { deserializeScanResponse } from "../util";
12
import { Command, CommandOptions } from "./command";
23
import { ScanCommandOptions } from "./scan";
34

45
/**
56
* @see https://redis.io/commands/zscan
67
*/
78
export class ZScanCommand extends Command<
8-
[number, (string | number)[]],
9-
[number, (string | number)[]]
9+
[string, (string | number)[]],
10+
[string, (string | number)[]]
1011
> {
1112
constructor(
12-
[key, cursor, opts]: [key: string, cursor: number, opts?: ScanCommandOptions],
13-
cmdOpts?: CommandOptions<[number, (string | number)[]], [number, (string | number)[]]>,
13+
[key, cursor, opts]: [key: string, cursor: string | number, opts?: ScanCommandOptions],
14+
cmdOpts?: CommandOptions<[string, (string | number)[]], [string, (string | number)[]]>,
1415
) {
15-
const command = ["zscan", key, cursor];
16+
const command: (number | string)[] = ["zscan", key, cursor];
1617
if (opts?.match) {
1718
command.push("match", opts.match);
1819
}
1920
if (typeof opts?.count === "number") {
2021
command.push("count", opts.count);
2122
}
2223

23-
super(command, cmdOpts);
24+
super(
25+
command,
26+
{
27+
deserialize: deserializeScanResponse,
28+
...cmdOpts,
29+
}
30+
);
2431
}
2532
}

pkg/util.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,14 @@ export function parseResponse<TResult>(result: unknown): TResult {
2929
return result as TResult;
3030
}
3131
}
32+
33+
/**
34+
* Deserializes a scan result, excluding the cursor
35+
* which can be string "0" or a big number string.
36+
* Either way, we want it to stay as a string.
37+
*
38+
* @param result
39+
*/
40+
export function deserializeScanResponse<TResult>(result: [string, ...any]): TResult {
41+
return [result[0], ...parseResponse<any[]>(result.slice(1))] as TResult;
42+
}

0 commit comments

Comments
 (0)