diff --git a/pkg/commands/bitfield.test.ts b/pkg/commands/bitfield.test.ts new file mode 100644 index 00000000..7ea916ab --- /dev/null +++ b/pkg/commands/bitfield.test.ts @@ -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]); + }); +}); diff --git a/pkg/commands/bitfield.ts b/pkg/commands/bitfield.ts new file mode 100644 index 00000000..3b5da3e7 --- /dev/null +++ b/pkg/commands/bitfield.ts @@ -0,0 +1,51 @@ +import { type Requester } from "../http"; +import { Command, type CommandOptions } from "./command"; + +type SubCommandArgs = [ + encoding: string, // u1 - u63 | i1 - i64 + offset: number | string, // | # + ...TRest, +]; + +/** + * @see https://redis.io/commands/bitfield + */ +export class BitFieldCommand> { + private command: (string | number)[]; + + constructor( + args: [key: string], + private client: Requester, + private opts?: CommandOptions, + private execOperation = (command: Command) => + 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); + } +} diff --git a/pkg/commands/mod.ts b/pkg/commands/mod.ts index 79abd9e2..0ca3cd7f 100644 --- a/pkg/commands/mod.ts +++ b/pkg/commands/mod.ts @@ -1,5 +1,6 @@ export * from "./append"; export * from "./bitcount"; +export * from "./bitfield"; export * from "./bitop"; export * from "./bitpos"; export * from "./command"; @@ -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"; diff --git a/pkg/error.ts b/pkg/error.ts index f57702f1..a766e302 100644 --- a/pkg/error.ts +++ b/pkg/error.ts @@ -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"; } } diff --git a/pkg/pipeline.test.ts b/pkg/pipeline.test.ts index 2c571a7c..c3cee85a 100644 --- a/pkg/pipeline.test.ts +++ b/pkg/pipeline.test.ts @@ -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() @@ -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); }); }); diff --git a/pkg/pipeline.ts b/pkg/pipeline.ts index 18194eea..003bd61d 100644 --- a/pkg/pipeline.ts +++ b/pkg/pipeline.ts @@ -3,6 +3,7 @@ import { HRandFieldCommand } from "./commands/hrandfield"; import { AppendCommand, BitCountCommand, + BitFieldCommand, BitOpCommand, BitPosCommand, CopyCommand, @@ -322,6 +323,25 @@ export class Pipeline[] = []> { bitcount = (...args: CommandArgs) => 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) => + new BitFieldCommand(args, this.client, this.commandOptions, this.chain.bind(this)); + /** * @see https://redis.io/commands/bitop */ diff --git a/pkg/redis.ts b/pkg/redis.ts index dbf3e1e6..a22abec3 100644 --- a/pkg/redis.ts +++ b/pkg/redis.ts @@ -2,6 +2,7 @@ import { createAutoPipelineProxy } from "../pkg/auto-pipeline"; import { AppendCommand, BitCountCommand, + BitFieldCommand, BitOpCommand, BitPosCommand, CommandOptions, @@ -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) => + new BitFieldCommand(args, this.client, this.opts); + /** * @see https://redis.io/commands/append */