Skip to content

Commit

Permalink
feat: add bitfield command (#1159)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
lewxdev authored Jul 3, 2024
1 parent 70bc178 commit c00b02d
Show file tree
Hide file tree
Showing 7 changed files with 153 additions and 3 deletions.
51 changes: 51 additions & 0 deletions pkg/commands/bitfield.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { afterAll, describe, expect, test } from "bun:test";
import { keygen, newHttpClient } from "../test-utils";
import { BitFieldCommand } from "./bitfield";

const client = newHttpClient();

const { newKey, cleanup } = keygen();
afterAll(cleanup);

describe("when key is not set", () => {
test("returns 0", async () => {
const key = newKey();
const res = await new BitFieldCommand([key], client).get("u4", "#0").exec();
expect(res).toEqual([0]);
});
});

describe("when key is set", () => {
test("sets / gets value", async () => {
const key = newKey();
const value = 42;
const res = await new BitFieldCommand([key], client)
.set("u8", "#0", value)
.get("u8", "#0")
.exec();
expect(res).toEqual([0, value]);
});

test("increments value", async () => {
const key = newKey();
const value = 42;
const increment = 10;
const res = await new BitFieldCommand([key], client)
.set("u8", "#0", value)
.incrby("u8", "#0", increment)
.exec();
expect(res).toEqual([0, value + increment]);
});

test("overflows", async () => {
const key = newKey();
const value = 255;
const bitWidth = 8;
const res = await new BitFieldCommand([key], client)
.set(`u${bitWidth}`, "#0", value)
.incrby(`u${bitWidth}`, "#0", 10)
.overflow("WRAP")
.exec();
expect(res).toEqual([0, (value + 10) % 2 ** bitWidth]);
});
});
51 changes: 51 additions & 0 deletions pkg/commands/bitfield.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { type Requester } from "../http";
import { Command, type CommandOptions } from "./command";

type SubCommandArgs<TRest extends unknown[] = []> = [
encoding: string, // u1 - u63 | i1 - i64
offset: number | string, // <int> | #<int>
...TRest,
];

/**
* @see https://redis.io/commands/bitfield
*/
export class BitFieldCommand<T = Promise<number[]>> {
private command: (string | number)[];

constructor(
args: [key: string],
private client: Requester,
private opts?: CommandOptions<number[], number[]>,
private execOperation = (command: Command<number[], number[]>) =>
command.exec(this.client) as T,
) {
this.command = ["bitfield", ...args];
}

private chain(...args: typeof this.command) {
this.command.push(...args);
return this;
}

get(...args: SubCommandArgs) {
return this.chain("get", ...args);
}

set(...args: SubCommandArgs<[value: number]>) {
return this.chain("set", ...args);
}

incrby(...args: SubCommandArgs<[increment: number]>) {
return this.chain("incrby", ...args);
}

overflow(overflow: "WRAP" | "SAT" | "FAIL") {
return this.chain("overflow", overflow);
}

exec() {
const command = new Command(this.command, this.opts);
return this.execOperation(command);
}
}
3 changes: 2 additions & 1 deletion pkg/commands/mod.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export * from "./append";
export * from "./bitcount";
export * from "./bitfield";
export * from "./bitop";
export * from "./bitpos";
export * from "./command";
Expand Down Expand Up @@ -72,8 +73,8 @@ export * from "./lindex";
export * from "./linsert";
export * from "./llen";
export * from "./lmove";
export * from "./lpop";
export * from "./lmpop";
export * from "./lpop";
export * from "./lpos";
export * from "./lpush";
export * from "./lpushx";
Expand Down
4 changes: 3 additions & 1 deletion pkg/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ export class UpstashError extends Error {

export class UrlError extends Error {
constructor(url: string) {
super(`Upstash Redis client was passed an invalid URL. You should pass the URL together with https. Received: "${url}". `);
super(
`Upstash Redis client was passed an invalid URL. You should pass the URL together with https. Received: "${url}". `,
);
this.name = "UrlError";
}
}
8 changes: 7 additions & 1 deletion pkg/pipeline.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,12 @@ describe("use all the things", () => {

p.append(newKey(), "hello")
.bitcount(newKey(), 0, 1)
.bitfield(newKey())
.set("u4", "#0", 15)
.get("u4", "#0")
.overflow("WRAP")
.incrby("u4", "#0", 10)
.exec()
.bitop("and", newKey(), newKey())
.bitpos(newKey(), 1, 0)
.dbsize()
Expand Down Expand Up @@ -243,6 +249,6 @@ describe("use all the things", () => {
.json.set(newKey(), "$", { hello: "world" });

const res = await p.exec();
expect(res.length).toEqual(120);
expect(res.length).toEqual(121);
});
});
20 changes: 20 additions & 0 deletions pkg/pipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { HRandFieldCommand } from "./commands/hrandfield";
import {
AppendCommand,
BitCountCommand,
BitFieldCommand,
BitOpCommand,
BitPosCommand,
CopyCommand,
Expand Down Expand Up @@ -322,6 +323,25 @@ export class Pipeline<TCommands extends Command<any, any>[] = []> {
bitcount = (...args: CommandArgs<typeof BitCountCommand>) =>
this.chain(new BitCountCommand(args, this.commandOptions));

/**
* Returns an instance that can be used to execute `BITFIELD` commands on one key.
*
* @example
* ```typescript
* redis.set("mykey", 0);
* const result = await redis.pipeline()
* .bitfield("mykey")
* .set("u4", 0, 16)
* .incr("u4", "#1", 1)
* .exec();
* console.log(result); // [[0, 1]]
* ```
*
* @see https://redis.io/commands/bitfield
*/
bitfield = (...args: CommandArgs<typeof BitFieldCommand>) =>
new BitFieldCommand(args, this.client, this.commandOptions, this.chain.bind(this));

/**
* @see https://redis.io/commands/bitop
*/
Expand Down
19 changes: 19 additions & 0 deletions pkg/redis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { createAutoPipelineProxy } from "../pkg/auto-pipeline";
import {
AppendCommand,
BitCountCommand,
BitFieldCommand,
BitOpCommand,
BitPosCommand,
CommandOptions,
Expand Down Expand Up @@ -402,6 +403,24 @@ export class Redis {
multiExec: true,
});

/**
* Returns an instance that can be used to execute `BITFIELD` commands on one key.
*
* @example
* ```typescript
* redis.set("mykey", 0);
* const result = await redis.bitfield("mykey")
* .set("u4", 0, 16)
* .incr("u4", "#1", 1)
* .exec();
* console.log(result); // [0, 1]
* ```
*
* @see https://redis.io/commands/bitfield
*/
bitfield = (...args: CommandArgs<typeof BitFieldCommand>) =>
new BitFieldCommand(args, this.client, this.opts);

/**
* @see https://redis.io/commands/append
*/
Expand Down

0 comments on commit c00b02d

Please sign in to comment.