From f16d97aec4a917de33ce302832c6a9c86873042e Mon Sep 17 00:00:00 2001 From: RiskyMH Date: Thu, 22 May 2025 18:35:41 +1000 Subject: [PATCH] `Bun.redis` - implement `incrby`, `decrby`, and `incrbyfloat` --- packages/bun-types/redis.d.ts | 24 ++++++++++ src/bun.js/api/valkey.classes.ts | 12 +++++ src/valkey/js_valkey.zig | 3 ++ src/valkey/js_valkey_functions.zig | 75 ++++++++++++++++++++++++++++++ test/js/valkey/valkey.test.ts | 48 +++++++++++++++++++ 5 files changed, 162 insertions(+) diff --git a/packages/bun-types/redis.d.ts b/packages/bun-types/redis.d.ts index dccc6f585247..16cb7fb734b5 100644 --- a/packages/bun-types/redis.d.ts +++ b/packages/bun-types/redis.d.ts @@ -596,6 +596,30 @@ declare module "bun" { * @returns Promise that resolves with the value of the key, or null if the key doesn't exist */ getex(key: string | ArrayBufferView | Blob): Promise; + + /** + * Increment the integer value of a key by the given number + * @param key The key to increment + * @param count The amount to increment by + * @returns Promise that resolves with the new value + */ + incrby(key: string | ArrayBufferView | Blob, count: string | number): Promise; + + /** + * Decrement the integer value of a key by the given number + * @param key The key to decrement + * @param count The amount to decrement by + * @returns Promise that resolves with the new value + */ + decrby(key: string | ArrayBufferView | Blob, count: string | number): Promise; + + /** + * Increment the float value of a key by the given amount + * @param key The key to increment + * @param count The amount to increment by (float or integer) + * @returns Promise that resolves with the new value as a string + */ + incrbyfloat(key: string | ArrayBufferView | Blob, count: string | number): Promise; } /** diff --git a/src/bun.js/api/valkey.classes.ts b/src/bun.js/api/valkey.classes.ts index 9a7af095ff67..db7026b521e6 100644 --- a/src/bun.js/api/valkey.classes.ts +++ b/src/bun.js/api/valkey.classes.ts @@ -226,6 +226,18 @@ export default [ unsubscribe: { fn: "unsubscribe" }, punsubscribe: { fn: "punsubscribe" }, pubsub: { fn: "pubsub" }, + incrby: { + fn: "incrby", + length: 2, + }, + decrby: { + fn: "decrby", + length: 2, + }, + incrbyfloat: { + fn: "incrbyfloat", + length: 2, + }, }, values: ["onconnect", "onclose", "connectionPromise", "hello"], }), diff --git a/src/valkey/js_valkey.zig b/src/valkey/js_valkey.zig index bbfa611895e9..f8dafbde0ecc 100644 --- a/src/valkey/js_valkey.zig +++ b/src/valkey/js_valkey.zig @@ -642,6 +642,7 @@ pub const JSValkeyClient = struct { pub const append = fns.append; pub const bitcount = fns.bitcount; pub const decr = fns.decr; + pub const decrby = fns.decrby; pub const del = fns.del; pub const dump = fns.dump; pub const exists = fns.exists; @@ -662,6 +663,8 @@ pub const JSValkeyClient = struct { pub const hstrlen = fns.hstrlen; pub const hvals = fns.hvals; pub const incr = fns.incr; + pub const incrby = fns.incrby; + pub const incrbyfloat = fns.incrbyfloat; pub const keys = fns.keys; pub const llen = fns.llen; pub const lpop = fns.lpop; diff --git a/src/valkey/js_valkey_functions.zig b/src/valkey/js_valkey_functions.zig index 46e23df32f90..f671224630dd 100644 --- a/src/valkey/js_valkey_functions.zig +++ b/src/valkey/js_valkey_functions.zig @@ -546,6 +546,81 @@ pub fn hmset(this: *JSValkeyClient, globalObject: *JSC.JSGlobalObject, callframe return promise.toJS(); } +pub fn incrby(this: *JSValkeyClient, globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue { + const key = try callframe.argument(0).toBunString(globalObject); + defer key.deref(); + const value = try callframe.argument(1).toBunString(globalObject); + defer value.deref(); + + const key_slice = key.toUTF8WithoutRef(bun.default_allocator); + defer key_slice.deinit(); + const value_slice = value.toUTF8WithoutRef(bun.default_allocator); + defer value_slice.deinit(); + + // Send INCRBY command + const promise = this.send( + globalObject, + callframe.this(), + &.{ + .command = "INCRBY", + .args = .{ .slices = &.{ key_slice, value_slice } }, + }, + ) catch |err| { + return protocol.valkeyErrorToJS(globalObject, "Failed to send INCRBY command", err); + }; + return promise.toJS(); +} + +pub fn decrby(this: *JSValkeyClient, globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue { + const key = try callframe.argument(0).toBunString(globalObject); + defer key.deref(); + const value = try callframe.argument(1).toBunString(globalObject); + defer value.deref(); + + const key_slice = key.toUTF8WithoutRef(bun.default_allocator); + defer key_slice.deinit(); + const value_slice = value.toUTF8WithoutRef(bun.default_allocator); + defer value_slice.deinit(); + + // Send DECRBY command + const promise = this.send( + globalObject, + callframe.this(), + &.{ + .command = "DECRBY", + .args = .{ .slices = &.{ key_slice, value_slice } }, + }, + ) catch |err| { + return protocol.valkeyErrorToJS(globalObject, "Failed to send DECRBY command", err); + }; + return promise.toJS(); +} + +pub fn incrbyfloat(this: *JSValkeyClient, globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue { + const key = try callframe.argument(0).toBunString(globalObject); + defer key.deref(); + const value = try callframe.argument(1).toBunString(globalObject); + defer value.deref(); + + const key_slice = key.toUTF8WithoutRef(bun.default_allocator); + defer key_slice.deinit(); + const value_slice = value.toUTF8WithoutRef(bun.default_allocator); + defer value_slice.deinit(); + + // Send INCRBYFLOAT command + const promise = this.send( + globalObject, + callframe.this(), + &.{ + .command = "INCRBYFLOAT", + .args = .{ .slices = &.{ key_slice, value_slice } }, + }, + ) catch |err| { + return protocol.valkeyErrorToJS(globalObject, "Failed to send INCRBYFLOAT command", err); + }; + return promise.toJS(); +} + pub const bitcount = compile.@"(key: RedisKey)"("bitcount", "BITCOUNT", "key").call; pub const dump = compile.@"(key: RedisKey)"("dump", "DUMP", "key").call; pub const expiretime = compile.@"(key: RedisKey)"("expiretime", "EXPIRETIME", "key").call; diff --git a/test/js/valkey/valkey.test.ts b/test/js/valkey/valkey.test.ts index 097ebc865718..12bfc754b8d5 100644 --- a/test/js/valkey/valkey.test.ts +++ b/test/js/valkey/valkey.test.ts @@ -67,6 +67,54 @@ describe.skipIf(!isEnabled)("Valkey Redis Client", () => { expect(decrementedValue).toBe(10); }); + test("should increment and decrement counters by a specific amount", async () => { + const redis = ctx.redis; + const counterKey = "counter"; + await redis.set(counterKey, "10"); + + // INCRBY should increment by a specific amount + const incrementedValue = await redis.incrby(counterKey, 5); + expect(incrementedValue).toBeDefined(); + expect(typeof incrementedValue).toBe("number"); + expect(incrementedValue).toBe(15); + + // INCRBY (with negative amount) should decrement by a specific amount + const decrementedValue = await redis.incrby(counterKey, -3); + expect(decrementedValue).toBeDefined(); + expect(typeof decrementedValue).toBe("number"); + expect(decrementedValue).toBe(12); + + // DECRBY should decrement by a specific amount + const decrementedValue2 = await redis.decrby(counterKey, 3); + expect(decrementedValue2).toBeDefined(); + expect(typeof decrementedValue2).toBe("number"); + expect(decrementedValue2).toBe(9); + + // DECRBY (with negative amount) should increment by a specific amount + const incrementedValue2 = await redis.decrby(counterKey, -3); + expect(incrementedValue2).toBeDefined(); + expect(typeof incrementedValue2).toBe("number"); + expect(incrementedValue2).toBe(12); + }); + + test("should increment and decrement counters by a specific amount with floats", async () => { + const redis = ctx.redis; + const counterKey = "counter"; + await redis.set(counterKey, "10"); + + // INCRBYFLOAT should increment by a specific amount + const incrementedValue = await redis.incrbyfloat(counterKey, 2.5); + expect(incrementedValue).toBeDefined(); + expect(typeof incrementedValue).toBe("string"); + expect(incrementedValue).toBe("12.5"); + + // INCRBYFLOAT (with negative amount) should decrement by a specific amount + const decrementedValue = await redis.incrbyfloat(counterKey, -1.5); + expect(decrementedValue).toBeDefined(); + expect(typeof decrementedValue).toBe("string"); + expect(decrementedValue).toBe("11"); + }); + test("should manage key expiration", async () => { const redis = ctx.redis; // Set a key first