Skip to content

Commit c00b02d

Browse files
authored
feat: add bitfield command (#1159)
* feat: add bitfield resolves #1150 * fix: bitfield tests not running * fix: type constraints and protected props * fix: type constraints * feat: add bitfield to pipeline (also addresses pr comments) * fix: code complexity opt to pass private properties / methods from pipeline directly into the constructor for the bitfield command. no need to create two separate classes for command and pipeline
1 parent 70bc178 commit c00b02d

File tree

7 files changed

+153
-3
lines changed

7 files changed

+153
-3
lines changed

pkg/commands/bitfield.test.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { afterAll, describe, expect, test } from "bun:test";
2+
import { keygen, newHttpClient } from "../test-utils";
3+
import { BitFieldCommand } from "./bitfield";
4+
5+
const client = newHttpClient();
6+
7+
const { newKey, cleanup } = keygen();
8+
afterAll(cleanup);
9+
10+
describe("when key is not set", () => {
11+
test("returns 0", async () => {
12+
const key = newKey();
13+
const res = await new BitFieldCommand([key], client).get("u4", "#0").exec();
14+
expect(res).toEqual([0]);
15+
});
16+
});
17+
18+
describe("when key is set", () => {
19+
test("sets / gets value", async () => {
20+
const key = newKey();
21+
const value = 42;
22+
const res = await new BitFieldCommand([key], client)
23+
.set("u8", "#0", value)
24+
.get("u8", "#0")
25+
.exec();
26+
expect(res).toEqual([0, value]);
27+
});
28+
29+
test("increments value", async () => {
30+
const key = newKey();
31+
const value = 42;
32+
const increment = 10;
33+
const res = await new BitFieldCommand([key], client)
34+
.set("u8", "#0", value)
35+
.incrby("u8", "#0", increment)
36+
.exec();
37+
expect(res).toEqual([0, value + increment]);
38+
});
39+
40+
test("overflows", async () => {
41+
const key = newKey();
42+
const value = 255;
43+
const bitWidth = 8;
44+
const res = await new BitFieldCommand([key], client)
45+
.set(`u${bitWidth}`, "#0", value)
46+
.incrby(`u${bitWidth}`, "#0", 10)
47+
.overflow("WRAP")
48+
.exec();
49+
expect(res).toEqual([0, (value + 10) % 2 ** bitWidth]);
50+
});
51+
});

pkg/commands/bitfield.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { type Requester } from "../http";
2+
import { Command, type CommandOptions } from "./command";
3+
4+
type SubCommandArgs<TRest extends unknown[] = []> = [
5+
encoding: string, // u1 - u63 | i1 - i64
6+
offset: number | string, // <int> | #<int>
7+
...TRest,
8+
];
9+
10+
/**
11+
* @see https://redis.io/commands/bitfield
12+
*/
13+
export class BitFieldCommand<T = Promise<number[]>> {
14+
private command: (string | number)[];
15+
16+
constructor(
17+
args: [key: string],
18+
private client: Requester,
19+
private opts?: CommandOptions<number[], number[]>,
20+
private execOperation = (command: Command<number[], number[]>) =>
21+
command.exec(this.client) as T,
22+
) {
23+
this.command = ["bitfield", ...args];
24+
}
25+
26+
private chain(...args: typeof this.command) {
27+
this.command.push(...args);
28+
return this;
29+
}
30+
31+
get(...args: SubCommandArgs) {
32+
return this.chain("get", ...args);
33+
}
34+
35+
set(...args: SubCommandArgs<[value: number]>) {
36+
return this.chain("set", ...args);
37+
}
38+
39+
incrby(...args: SubCommandArgs<[increment: number]>) {
40+
return this.chain("incrby", ...args);
41+
}
42+
43+
overflow(overflow: "WRAP" | "SAT" | "FAIL") {
44+
return this.chain("overflow", overflow);
45+
}
46+
47+
exec() {
48+
const command = new Command(this.command, this.opts);
49+
return this.execOperation(command);
50+
}
51+
}

pkg/commands/mod.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export * from "./append";
22
export * from "./bitcount";
3+
export * from "./bitfield";
34
export * from "./bitop";
45
export * from "./bitpos";
56
export * from "./command";
@@ -72,8 +73,8 @@ export * from "./lindex";
7273
export * from "./linsert";
7374
export * from "./llen";
7475
export * from "./lmove";
75-
export * from "./lpop";
7676
export * from "./lmpop";
77+
export * from "./lpop";
7778
export * from "./lpos";
7879
export * from "./lpush";
7980
export * from "./lpushx";

pkg/error.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ export class UpstashError extends Error {
1010

1111
export class UrlError extends Error {
1212
constructor(url: string) {
13-
super(`Upstash Redis client was passed an invalid URL. You should pass the URL together with https. Received: "${url}". `);
13+
super(
14+
`Upstash Redis client was passed an invalid URL. You should pass the URL together with https. Received: "${url}". `,
15+
);
1416
this.name = "UrlError";
1517
}
1618
}

pkg/pipeline.test.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,12 @@ describe("use all the things", () => {
123123

124124
p.append(newKey(), "hello")
125125
.bitcount(newKey(), 0, 1)
126+
.bitfield(newKey())
127+
.set("u4", "#0", 15)
128+
.get("u4", "#0")
129+
.overflow("WRAP")
130+
.incrby("u4", "#0", 10)
131+
.exec()
126132
.bitop("and", newKey(), newKey())
127133
.bitpos(newKey(), 1, 0)
128134
.dbsize()
@@ -243,6 +249,6 @@ describe("use all the things", () => {
243249
.json.set(newKey(), "$", { hello: "world" });
244250

245251
const res = await p.exec();
246-
expect(res.length).toEqual(120);
252+
expect(res.length).toEqual(121);
247253
});
248254
});

pkg/pipeline.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { HRandFieldCommand } from "./commands/hrandfield";
33
import {
44
AppendCommand,
55
BitCountCommand,
6+
BitFieldCommand,
67
BitOpCommand,
78
BitPosCommand,
89
CopyCommand,
@@ -322,6 +323,25 @@ export class Pipeline<TCommands extends Command<any, any>[] = []> {
322323
bitcount = (...args: CommandArgs<typeof BitCountCommand>) =>
323324
this.chain(new BitCountCommand(args, this.commandOptions));
324325

326+
/**
327+
* Returns an instance that can be used to execute `BITFIELD` commands on one key.
328+
*
329+
* @example
330+
* ```typescript
331+
* redis.set("mykey", 0);
332+
* const result = await redis.pipeline()
333+
* .bitfield("mykey")
334+
* .set("u4", 0, 16)
335+
* .incr("u4", "#1", 1)
336+
* .exec();
337+
* console.log(result); // [[0, 1]]
338+
* ```
339+
*
340+
* @see https://redis.io/commands/bitfield
341+
*/
342+
bitfield = (...args: CommandArgs<typeof BitFieldCommand>) =>
343+
new BitFieldCommand(args, this.client, this.commandOptions, this.chain.bind(this));
344+
325345
/**
326346
* @see https://redis.io/commands/bitop
327347
*/

pkg/redis.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { createAutoPipelineProxy } from "../pkg/auto-pipeline";
22
import {
33
AppendCommand,
44
BitCountCommand,
5+
BitFieldCommand,
56
BitOpCommand,
67
BitPosCommand,
78
CommandOptions,
@@ -402,6 +403,24 @@ export class Redis {
402403
multiExec: true,
403404
});
404405

406+
/**
407+
* Returns an instance that can be used to execute `BITFIELD` commands on one key.
408+
*
409+
* @example
410+
* ```typescript
411+
* redis.set("mykey", 0);
412+
* const result = await redis.bitfield("mykey")
413+
* .set("u4", 0, 16)
414+
* .incr("u4", "#1", 1)
415+
* .exec();
416+
* console.log(result); // [0, 1]
417+
* ```
418+
*
419+
* @see https://redis.io/commands/bitfield
420+
*/
421+
bitfield = (...args: CommandArgs<typeof BitFieldCommand>) =>
422+
new BitFieldCommand(args, this.client, this.opts);
423+
405424
/**
406425
* @see https://redis.io/commands/append
407426
*/

0 commit comments

Comments
 (0)