diff --git a/packages/bun-types/redis.d.ts b/packages/bun-types/redis.d.ts index 414b01288b..3011e66c1c 100644 --- a/packages/bun-types/redis.d.ts +++ b/packages/bun-types/redis.d.ts @@ -34,14 +34,7 @@ declare module "bun" { * TLS options * Can be a boolean or an object with TLS options */ - tls?: - | boolean - | { - key?: string | Buffer; - cert?: string | Buffer; - ca?: string | Buffer | Array; - rejectUnauthorized?: boolean; - }; + tls?: boolean | Bun.TLSOptions; /** * Whether to enable auto-pipelining @@ -245,6 +238,22 @@ declare module "bun" { */ incr(key: RedisClient.KeyLike): Promise; + /** + * Increment the integer value of a key by the given amount + * @param key The key to increment + * @param increment The amount to increment by + * @returns Promise that resolves with the new value after incrementing + */ + incrby(key: RedisClient.KeyLike, increment: number): Promise; + + /** + * Increment the float value of a key by the given amount + * @param key The key to increment + * @param increment The amount to increment by (can be a float) + * @returns Promise that resolves with the new value as a string after incrementing + */ + incrbyfloat(key: RedisClient.KeyLike, increment: number | string): Promise; + /** * Decrement the integer value of a key by one * @param key The key to decrement @@ -252,6 +261,14 @@ declare module "bun" { */ decr(key: RedisClient.KeyLike): Promise; + /** + * Decrement the integer value of a key by the given amount + * @param key The key to decrement + * @param decrement The amount to decrement by + * @returns Promise that resolves with the new value after decrementing + */ + decrby(key: RedisClient.KeyLike, decrement: number): Promise; + /** * Determine if a key exists * @param key The key to check @@ -268,6 +285,22 @@ declare module "bun" { */ expire(key: RedisClient.KeyLike, seconds: number): Promise; + /** + * Set the expiration for a key as a Unix timestamp (in seconds) + * @param key The key to set expiration on + * @param timestamp Unix timestamp in seconds when the key should expire + * @returns Promise that resolves with 1 if timeout was set, 0 if key does not exist + */ + expireat(key: RedisClient.KeyLike, timestamp: number): Promise; + + /** + * Set a key's time to live in milliseconds + * @param key The key to set the expiration for + * @param milliseconds The number of milliseconds until expiration + * @returns Promise that resolves with 1 if the timeout was set, 0 if the key does not exist + */ + pexpire(key: RedisClient.KeyLike, milliseconds: number): Promise; + /** * Get the time to live for a key in seconds * @param key The key to get the TTL for @@ -276,13 +309,313 @@ declare module "bun" { */ ttl(key: RedisClient.KeyLike): Promise; + /** + * Set the value of a hash field or multiple fields + * @param key The hash key + * @param fields Object/Record with field-value pairs + * @returns Promise that resolves with the number of fields that were added + */ + hset(key: RedisClient.KeyLike, fields: Record): Promise; + + /** + * Set the value of a hash field or multiple fields (variadic) + * @param key The hash key + * @param field The field name + * @param value The value to set + * @param rest Additional field-value pairs + * @returns Promise that resolves with the number of fields that were added + */ + hset( + key: RedisClient.KeyLike, + field: RedisClient.KeyLike, + value: RedisClient.KeyLike, + ...rest: RedisClient.KeyLike[] + ): Promise; + + /** + * Set the value of a hash field, only if the field does not exist + * @param key The hash key + * @param field The field to set + * @param value The value to set + * @returns Promise that resolves with true if field was set, false if field already exists + */ + hsetnx(key: RedisClient.KeyLike, field: RedisClient.KeyLike, value: RedisClient.KeyLike): Promise; + + /** + * Get and delete one or more hash fields (Redis 8.0.0+) + * Syntax: HGETDEL key FIELDS numfields field [field ...] + * @param key The hash key + * @param fieldsKeyword Must be the literal string "FIELDS" + * @param numfields Number of fields to follow + * @param fields The field names to get and delete + * @returns Promise that resolves with array of field values (null for non-existent fields) + * @example redis.hgetdel("mykey", "FIELDS", 2, "field1", "field2") + */ + hgetdel( + key: RedisClient.KeyLike, + fieldsKeyword: "FIELDS", + numfields: number, + ...fields: RedisClient.KeyLike[] + ): Promise>; + + /** + * Get hash field values with expiration options (Redis 8.0.0+) + * Syntax: HGETEX key [EX seconds | PX milliseconds | EXAT unix-time-seconds | PXAT unix-time-milliseconds | PERSIST] FIELDS numfields field [field ...] + * @example redis.hgetex("mykey", "FIELDS", 1, "field1") + * @example redis.hgetex("mykey", "EX", 10, "FIELDS", 1, "field1") + * @example redis.hgetex("mykey", "PX", 5000, "FIELDS", 2, "field1", "field2") + * @example redis.hgetex("mykey", "PERSIST", "FIELDS", 1, "field1") + */ + //prettier-ignore + hgetex(key: RedisClient.KeyLike, fieldsKeyword: "FIELDS", numfields: number, ...fields: RedisClient.KeyLike[]): Promise>; + //prettier-ignore + hgetex(key: RedisClient.KeyLike, ex: "EX", seconds: number, fieldsKeyword: "FIELDS", numfields: number, ...fields: RedisClient.KeyLike[]): Promise>; + //prettier-ignore + hgetex(key: RedisClient.KeyLike, px: "PX", milliseconds: number, fieldsKeyword: "FIELDS", numfields: number, ...fields: RedisClient.KeyLike[]): Promise>; + //prettier-ignore + hgetex(key: RedisClient.KeyLike, exat: "EXAT", unixTimeSeconds: number, fieldsKeyword: "FIELDS", numfields: number, ...fields: RedisClient.KeyLike[]): Promise>; + //prettier-ignore + hgetex(key: RedisClient.KeyLike, pxat: "PXAT", unixTimeMilliseconds: number, fieldsKeyword: "FIELDS", numfields: number, ...fields: RedisClient.KeyLike[]): Promise>; + //prettier-ignore + hgetex(key: RedisClient.KeyLike, persist: "PERSIST", fieldsKeyword: "FIELDS", numfields: number, ...fields: RedisClient.KeyLike[]): Promise>; + + /** + * Set hash fields with expiration options (Redis 8.0.0+) + * Syntax: HSETEX key [FNX | FXX] [EX seconds | PX milliseconds | EXAT unix-time-seconds | PXAT unix-time-milliseconds | KEEPTTL] FIELDS numfields field value [field value ...] + * @example redis.hsetex("mykey", "FIELDS", 1, "field1", "value1") + * @example redis.hsetex("mykey", "EX", 10, "FIELDS", 1, "field1", "value1") + * @example redis.hsetex("mykey", "FNX", "EX", 10, "FIELDS", 1, "field1", "value1") + */ + //prettier-ignore + hsetex(key: RedisClient.KeyLike, fieldsKeyword: "FIELDS", numfields: number, ...fieldValues: RedisClient.KeyLike[]): Promise; + //prettier-ignore + hsetex(key: RedisClient.KeyLike, fnx: "FNX", fieldsKeyword: "FIELDS", numfields: number, ...fieldValues: RedisClient.KeyLike[]): Promise; + //prettier-ignore + hsetex(key: RedisClient.KeyLike, fxx: "FXX", fieldsKeyword: "FIELDS", numfields: number, ...fieldValues: RedisClient.KeyLike[]): Promise; + //prettier-ignore + hsetex(key: RedisClient.KeyLike, ex: "EX", seconds: number, fieldsKeyword: "FIELDS", numfields: number, ...fieldValues: RedisClient.KeyLike[]): Promise; + //prettier-ignore + hsetex(key: RedisClient.KeyLike, px: "PX", milliseconds: number, fieldsKeyword: "FIELDS", numfields: number, ...fieldValues: RedisClient.KeyLike[]): Promise; + //prettier-ignore + hsetex(key: RedisClient.KeyLike, exat: "EXAT", unixTimeSeconds: number, fieldsKeyword: "FIELDS", numfields: number, ...fieldValues: RedisClient.KeyLike[]): Promise; + //prettier-ignore + hsetex(key: RedisClient.KeyLike, pxat: "PXAT", unixTimeMilliseconds: number, fieldsKeyword: "FIELDS", numfields: number, ...fieldValues: RedisClient.KeyLike[]): Promise; + //prettier-ignore + hsetex(key: RedisClient.KeyLike, keepttl: "KEEPTTL", fieldsKeyword: "FIELDS", numfields: number, ...fieldValues: RedisClient.KeyLike[]): Promise; + //prettier-ignore + hsetex(key: RedisClient.KeyLike, fnx: "FNX", ex: "EX", seconds: number, fieldsKeyword: "FIELDS", numfields: number, ...fieldValues: RedisClient.KeyLike[]): Promise; + //prettier-ignore + hsetex(key: RedisClient.KeyLike, fnx: "FNX", px: "PX", milliseconds: number, fieldsKeyword: "FIELDS", numfields: number, ...fieldValues: RedisClient.KeyLike[]): Promise; + //prettier-ignore + hsetex(key: RedisClient.KeyLike, fnx: "FNX", exat: "EXAT", unixTimeSeconds: number, fieldsKeyword: "FIELDS", numfields: number, ...fieldValues: RedisClient.KeyLike[]): Promise; + //prettier-ignore + hsetex(key: RedisClient.KeyLike, fnx: "FNX", pxat: "PXAT", unixTimeMilliseconds: number, fieldsKeyword: "FIELDS", numfields: number, ...fieldValues: RedisClient.KeyLike[]): Promise; + //prettier-ignore + hsetex(key: RedisClient.KeyLike, fnx: "FNX", keepttl: "KEEPTTL", fieldsKeyword: "FIELDS", numfields: number, ...fieldValues: RedisClient.KeyLike[]): Promise; + //prettier-ignore + hsetex(key: RedisClient.KeyLike, fxx: "FXX", ex: "EX", seconds: number, fieldsKeyword: "FIELDS", numfields: number, ...fieldValues: RedisClient.KeyLike[]): Promise; + //prettier-ignore + hsetex(key: RedisClient.KeyLike, fxx: "FXX", px: "PX", milliseconds: number, fieldsKeyword: "FIELDS", numfields: number, ...fieldValues: RedisClient.KeyLike[]): Promise; + //prettier-ignore + hsetex(key: RedisClient.KeyLike, fxx: "FXX", exat: "EXAT", unixTimeSeconds: number, fieldsKeyword: "FIELDS", numfields: number, ...fieldValues: RedisClient.KeyLike[]): Promise; + //prettier-ignore + hsetex(key: RedisClient.KeyLike, fxx: "FXX", pxat: "PXAT", unixTimeMilliseconds: number, fieldsKeyword: "FIELDS", numfields: number, ...fieldValues: RedisClient.KeyLike[]): Promise; + //prettier-ignore + hsetex(key: RedisClient.KeyLike, fxx: "FXX", keepttl: "KEEPTTL", fieldsKeyword: "FIELDS", numfields: number, ...fieldValues: RedisClient.KeyLike[]): Promise; + + /** + * Set expiration for hash fields (Redis 7.4+) + * Syntax: HEXPIRE key seconds [NX | XX | GT | LT] FIELDS numfields field [field ...] + * @returns Array where each element is: -2 (field doesn't exist), 0 (condition not met), 1 (expiration set), 2 (field deleted) + * @example redis.hexpire("mykey", 10, "FIELDS", 1, "field1") + * @example redis.hexpire("mykey", 10, "NX", "FIELDS", 2, "field1", "field2") + */ + hexpire( + key: RedisClient.KeyLike, + seconds: number, + fieldsKeyword: "FIELDS", + numfields: number, + ...fields: RedisClient.KeyLike[] + ): Promise; + hexpire( + key: RedisClient.KeyLike, + seconds: number, + condition: "NX" | "XX" | "GT" | "LT", + fieldsKeyword: "FIELDS", + numfields: number, + ...fields: RedisClient.KeyLike[] + ): Promise; + + /** + * Set expiration for hash fields using Unix timestamp in seconds (Redis 7.4+) + * Syntax: HEXPIREAT key unix-time-seconds [NX | XX | GT | LT] FIELDS numfields field [field ...] + * @returns Array where each element is: -2 (field doesn't exist), 0 (condition not met), 1 (expiration set), 2 (field deleted) + * @example redis.hexpireat("mykey", 1735689600, "FIELDS", 1, "field1") + */ + hexpireat( + key: RedisClient.KeyLike, + unixTimeSeconds: number, + fieldsKeyword: "FIELDS", + numfields: number, + ...fields: RedisClient.KeyLike[] + ): Promise; + hexpireat( + key: RedisClient.KeyLike, + unixTimeSeconds: number, + condition: "NX" | "XX" | "GT" | "LT", + fieldsKeyword: "FIELDS", + numfields: number, + ...fields: RedisClient.KeyLike[] + ): Promise; + + /** + * Get expiration time of hash fields as Unix timestamp in seconds (Redis 7.4+) + * Syntax: HEXPIRETIME key FIELDS numfields field [field ...] + * @returns Array where each element is: -2 (field doesn't exist), -1 (no expiration), Unix timestamp in seconds + * @example redis.hexpiretime("mykey", "FIELDS", 2, "field1", "field2") + */ + hexpiretime( + key: RedisClient.KeyLike, + fieldsKeyword: "FIELDS", + numfields: number, + ...fields: RedisClient.KeyLike[] + ): Promise; + + /** + * Remove expiration from hash fields (Redis 7.4+) + * Syntax: HPERSIST key FIELDS numfields field [field ...] + * @returns Array where each element is: -2 (field doesn't exist), -1 (no expiration), 1 (expiration removed) + * @example redis.hpersist("mykey", "FIELDS", 1, "field1") + */ + hpersist( + key: RedisClient.KeyLike, + fieldsKeyword: "FIELDS", + numfields: number, + ...fields: RedisClient.KeyLike[] + ): Promise; + + /** + * Set expiration for hash fields in milliseconds (Redis 7.4+) + * Syntax: HPEXPIRE key milliseconds [NX | XX | GT | LT] FIELDS numfields field [field ...] + * @returns Array where each element is: -2 (field doesn't exist), 0 (condition not met), 1 (expiration set), 2 (field deleted) + * @example redis.hpexpire("mykey", 10000, "FIELDS", 1, "field1") + */ + hpexpire( + key: RedisClient.KeyLike, + milliseconds: number, + fieldsKeyword: "FIELDS", + numfields: number, + ...fields: RedisClient.KeyLike[] + ): Promise; + hpexpire( + key: RedisClient.KeyLike, + milliseconds: number, + condition: "NX" | "XX" | "GT" | "LT", + fieldsKeyword: "FIELDS", + numfields: number, + ...fields: RedisClient.KeyLike[] + ): Promise; + + /** + * Set expiration for hash fields using Unix timestamp in milliseconds (Redis 7.4+) + * Syntax: HPEXPIREAT key unix-time-milliseconds [NX | XX | GT | LT] FIELDS numfields field [field ...] + * @returns Array where each element is: -2 (field doesn't exist), 0 (condition not met), 1 (expiration set), 2 (field deleted) + * @example redis.hpexpireat("mykey", 1735689600000, "FIELDS", 1, "field1") + */ + hpexpireat( + key: RedisClient.KeyLike, + unixTimeMilliseconds: number, + fieldsKeyword: "FIELDS", + numfields: number, + ...fields: RedisClient.KeyLike[] + ): Promise; + hpexpireat( + key: RedisClient.KeyLike, + unixTimeMilliseconds: number, + condition: "NX" | "XX" | "GT" | "LT", + fieldsKeyword: "FIELDS", + numfields: number, + ...fields: RedisClient.KeyLike[] + ): Promise; + + /** + * Get expiration time of hash fields as Unix timestamp in milliseconds (Redis 7.4+) + * Syntax: HPEXPIRETIME key FIELDS numfields field [field ...] + * @returns Array where each element is: -2 (field doesn't exist), -1 (no expiration), Unix timestamp in milliseconds + * @example redis.hpexpiretime("mykey", "FIELDS", 2, "field1", "field2") + */ + hpexpiretime( + key: RedisClient.KeyLike, + fieldsKeyword: "FIELDS", + numfields: number, + ...fields: RedisClient.KeyLike[] + ): Promise; + + /** + * Get TTL of hash fields in milliseconds (Redis 7.4+) + * Syntax: HPTTL key FIELDS numfields field [field ...] + * @returns Array where each element is: -2 (field doesn't exist), -1 (no expiration), TTL in milliseconds + * @example redis.hpttl("mykey", "FIELDS", 2, "field1", "field2") + */ + hpttl( + key: RedisClient.KeyLike, + fieldsKeyword: "FIELDS", + numfields: number, + ...fields: RedisClient.KeyLike[] + ): Promise; + + /** + * Get TTL of hash fields in seconds (Redis 7.4+) + * Syntax: HTTL key FIELDS numfields field [field ...] + * @returns Array where each element is: -2 (field doesn't exist), -1 (no expiration), TTL in seconds + * @example redis.httl("mykey", "FIELDS", 2, "field1", "field2") + */ + httl( + key: RedisClient.KeyLike, + fieldsKeyword: "FIELDS", + numfields: number, + ...fields: RedisClient.KeyLike[] + ): Promise; + /** * Set multiple hash fields to multiple values + * + * @deprecated Use {@link hset} instead. Since Redis 4.0.0, `HSET` supports multiple field-value pairs. + * + * @param key The hash key + * @param fields Object/Record with field-value pairs + * @returns Promise that resolves with "OK" + */ + hmset(key: RedisClient.KeyLike, fields: Record): Promise<"OK">; + + /** + * Set multiple hash fields to multiple values (variadic) + * + * @deprecated Use {@link hset} instead. Since Redis 4.0.0, `HSET` supports multiple field-value pairs. + * + * @param key The hash key + * @param field The field name + * @param value The value to set + * @param rest Additional field-value pairs + * @returns Promise that resolves with "OK" + */ + hmset( + key: RedisClient.KeyLike, + field: RedisClient.KeyLike, + value: RedisClient.KeyLike, + ...rest: RedisClient.KeyLike[] + ): Promise<"OK">; + + /** + * Set multiple hash fields to multiple values (array syntax, backward compat) + * + * @deprecated Use {@link hset} instead. Since Redis 4.0.0, `HSET` supports multiple field-value pairs. + * * @param key The hash key * @param fieldValues An array of alternating field names and values - * @returns Promise that resolves with "OK" on success + * @returns Promise that resolves with "OK" */ - hmset(key: RedisClient.KeyLike, fieldValues: string[]): Promise; + hmset(key: RedisClient.KeyLike, fieldValues: RedisClient.KeyLike[]): Promise<"OK">; /** * Get the value of a hash field @@ -292,6 +625,14 @@ declare module "bun" { */ hget(key: RedisClient.KeyLike, field: RedisClient.KeyLike): Promise; + /** + * Get the values of all the given hash fields + * @param key The hash key + * @param fields The fields to get + * @returns Promise that resolves with an array of values + */ + hmget(key: RedisClient.KeyLike, ...fields: string[]): Promise>; + /** * Get the values of all the given hash fields * @param key The hash key @@ -300,6 +641,104 @@ declare module "bun" { */ hmget(key: RedisClient.KeyLike, fields: string[]): Promise>; + /** + * Delete one or more hash fields + * @param key The hash key + * @param field The field to delete + * @param rest Additional fields to delete + * @returns Promise that resolves with the number of fields that were removed + */ + hdel(key: RedisClient.KeyLike, field: RedisClient.KeyLike, ...rest: RedisClient.KeyLike[]): Promise; + + /** + * Determine if a hash field exists + * @param key The hash key + * @param field The field to check + * @returns Promise that resolves with true if the field exists, false otherwise + */ + hexists(key: RedisClient.KeyLike, field: RedisClient.KeyLike): Promise; + + /** + * Get one or multiple random fields from a hash + * @param key The hash key + * @returns Promise that resolves with a random field name, or null if the hash doesn't exist + */ + hrandfield(key: RedisClient.KeyLike): Promise; + + /** + * Get one or multiple random fields from a hash + * @param key The hash key + * @param count The number of fields to return (positive for unique fields, negative for potentially duplicate fields) + * @returns Promise that resolves with an array of random field names + */ + hrandfield(key: RedisClient.KeyLike, count: number): Promise; + + /** + * Get one or multiple random fields with values from a hash + * @param key The hash key + * @param count The number of fields to return + * @param withValues Literal "WITHVALUES" to include values + * @returns Promise that resolves with an array of alternating field names and values + */ + hrandfield(key: RedisClient.KeyLike, count: number, withValues: "WITHVALUES"): Promise<[string, string][]>; + + /** + * Incrementally iterate hash fields and values + * @param key The hash key + * @param cursor The cursor value (0 to start iteration) + * @returns Promise that resolves with [next_cursor, [field1, value1, field2, value2, ...]] + */ + hscan(key: RedisClient.KeyLike, cursor: number | string): Promise<[string, string[]]>; + + /** + * Incrementally iterate hash fields and values with pattern matching + * @param key The hash key + * @param cursor The cursor value (0 to start iteration) + * @param match Literal "MATCH" + * @param pattern Pattern to match field names against + * @returns Promise that resolves with [next_cursor, [field1, value1, field2, value2, ...]] + */ + hscan( + key: RedisClient.KeyLike, + cursor: number | string, + match: "MATCH", + pattern: string, + ): Promise<[string, string[]]>; + + /** + * Incrementally iterate hash fields and values with count limit + * @param key The hash key + * @param cursor The cursor value (0 to start iteration) + * @param count Literal "COUNT" + * @param limit Maximum number of fields to return per call + * @returns Promise that resolves with [next_cursor, [field1, value1, field2, value2, ...]] + */ + hscan( + key: RedisClient.KeyLike, + cursor: number | string, + count: "COUNT", + limit: number, + ): Promise<[string, string[]]>; + + /** + * Incrementally iterate hash fields and values with pattern and count + * @param key The hash key + * @param cursor The cursor value (0 to start iteration) + * @param match Literal "MATCH" + * @param pattern Pattern to match field names against + * @param count Literal "COUNT" + * @param limit Maximum number of fields to return per call + * @returns Promise that resolves with [next_cursor, [field1, value1, field2, value2, ...]] + */ + hscan( + key: RedisClient.KeyLike, + cursor: number | string, + match: "MATCH", + pattern: string, + count: "COUNT", + limit: number, + ): Promise<[string, string[]]>; + /** * Check if a value is a member of a set * @param key The set key @@ -310,22 +749,29 @@ declare module "bun" { sismember(key: RedisClient.KeyLike, member: string): Promise; /** - * Add a member to a set + * Add one or more members to a set * @param key The set key - * @param member The member to add - * @returns Promise that resolves with 1 if the member was added, 0 if it - * already existed + * @param members The members to add + * @returns Promise that resolves with the number of members added */ - sadd(key: RedisClient.KeyLike, member: string): Promise; + sadd(key: RedisClient.KeyLike, ...members: string[]): Promise; /** - * Remove a member from a set + * Remove one or more members from a set * @param key The set key - * @param member The member to remove - * @returns Promise that resolves with 1 if the member was removed, 0 if it - * didn't exist + * @param members The members to remove + * @returns Promise that resolves with the number of members removed */ - srem(key: RedisClient.KeyLike, member: string): Promise; + srem(key: RedisClient.KeyLike, ...members: string[]): Promise; + + /** + * Move a member from one set to another + * @param source The source set key + * @param destination The destination set key + * @param member The member to move + * @returns Promise that resolves with true if the element was moved, false if it wasn't a member of source + */ + smove(source: RedisClient.KeyLike, destination: RedisClient.KeyLike, member: string): Promise; /** * Get all the members in a set @@ -342,6 +788,14 @@ declare module "bun" { */ srandmember(key: RedisClient.KeyLike): Promise; + /** + * Get count random members from a set + * @param key The set key + * @returns Promise that resolves with an array of up to count random members, or null if the set + * doesn't exist + */ + srandmember(key: RedisClient.KeyLike, count: number): Promise; + /** * Remove and return a random member from a set * @param key The set key @@ -350,6 +804,57 @@ declare module "bun" { */ spop(key: RedisClient.KeyLike): Promise; + /** + * Remove and return count members from the set + * @param key The set key + * @returns Promise that resolves with the removed members, or null if the + * set is empty + */ + spop(key: RedisClient.KeyLike, count: number): Promise; + + /** + * Post a message to a shard channel + * @param channel The shard channel name + * @param message The message to publish + * @returns Promise that resolves with the number of clients that received the message + */ + spublish(channel: RedisClient.KeyLike, message: string): Promise; + + /** + * Store the difference of multiple sets in a key + * @param destination The destination key to store the result + * @param key The first set key + * @param keys Additional set keys to subtract from the first set + * @returns Promise that resolves with the number of elements in the resulting set + */ + sdiffstore( + destination: RedisClient.KeyLike, + key: RedisClient.KeyLike, + ...keys: RedisClient.KeyLike[] + ): Promise; + + /** + * Check if multiple members are members of a set + * @param key The set key + * @param member The first member to check + * @param members Additional members to check + * @returns Promise that resolves with an array of 1s and 0s indicating membership + */ + smismember( + key: RedisClient.KeyLike, + member: RedisClient.KeyLike, + ...members: RedisClient.KeyLike[] + ): Promise; + + /** + * Incrementally iterate over a set + * @param key The set key + * @param cursor The cursor value + * @param args Additional SSCAN options (MATCH pattern, COUNT hint) + * @returns Promise that resolves with a tuple [cursor, members[]] + */ + sscan(key: RedisClient.KeyLike, cursor: number | string, ...args: (string | number)[]): Promise<[string, string[]]>; + /** * Increment the integer value of a hash field by the given number * @param key The hash key @@ -371,9 +876,9 @@ declare module "bun" { /** * Get all the fields and values in a hash * @param key The hash key - * @returns Promise that resolves with an object containing all fields and values + * @returns Promise that resolves with an object containing all fields and values, or empty object if key does not exist */ - hgetall(key: RedisClient.KeyLike): Promise | null>; + hgetall(key: RedisClient.KeyLike): Promise>; /** * Get all field names in a hash @@ -389,6 +894,14 @@ declare module "bun" { */ hlen(key: RedisClient.KeyLike): Promise; + /** + * Get the string length of the value stored in a hash field + * @param key The hash key + * @param field The field name + * @returns Promise that resolves with the length of the string value, or 0 if the field doesn't exist + */ + hstrlen(key: RedisClient.KeyLike, field: string): Promise; + /** * Get all values in a hash * @param key The hash key @@ -403,6 +916,157 @@ declare module "bun" { */ keys(pattern: string): Promise; + /** + * Blocking pop from head of one or more lists + * + * Blocks until an element is available in one of the lists or the timeout expires. + * Checks keys in order and pops from the first non-empty list. + * + * @param args Keys followed by timeout in seconds (can be fractional, 0 = block indefinitely) + * @returns Promise that resolves with [key, element] or null on timeout + * + * @example + * ```ts + * // Block for up to 1 second + * const result = await redis.blpop("mylist", 1.0); + * if (result) { + * const [key, element] = result; + * console.log(`Popped ${element} from ${key}`); + * } + * + * // Block indefinitely (timeout = 0) + * const result2 = await redis.blpop("list1", "list2", 0); + * ``` + */ + blpop(...args: (RedisClient.KeyLike | number)[]): Promise<[string, string] | null>; + + /** + * Blocking pop from tail of one or more lists + * + * Blocks until an element is available in one of the lists or the timeout expires. + * Checks keys in order and pops from the first non-empty list. + * + * @param args Keys followed by timeout in seconds (can be fractional, 0 = block indefinitely) + * @returns Promise that resolves with [key, element] or null on timeout + * + * @example + * ```ts + * // Block for up to 1 second + * const result = await redis.brpop("mylist", 1.0); + * if (result) { + * const [key, element] = result; + * console.log(`Popped ${element} from ${key}`); + * } + * + * // Block indefinitely (timeout = 0) + * const result2 = await redis.brpop("list1", "list2", 0); + * ``` + */ + brpop(...args: (RedisClient.KeyLike | number)[]): Promise<[string, string] | null>; + + /** + * Blocking move from one list to another + * + * Atomically moves an element from source to destination list, blocking until an element is available + * or the timeout expires. Allows specifying which end to pop from (LEFT/RIGHT) and which end to push to (LEFT/RIGHT). + * + * @param source Source list key + * @param destination Destination list key + * @param from Direction to pop from source: "LEFT" or "RIGHT" + * @param to Direction to push to destination: "LEFT" or "RIGHT" + * @param timeout Timeout in seconds (can be fractional, 0 = block indefinitely) + * @returns Promise that resolves with the moved element or null on timeout + * + * @example + * ```ts + * // Move from right of source to left of destination (like BRPOPLPUSH) + * const element = await redis.blmove("mylist", "otherlist", "RIGHT", "LEFT", 1.0); + * if (element) { + * console.log(`Moved element: ${element}`); + * } + * + * // Move from left to left + * await redis.blmove("list1", "list2", "LEFT", "LEFT", 0.5); + * ``` + */ + blmove( + source: RedisClient.KeyLike, + destination: RedisClient.KeyLike, + from: "LEFT" | "RIGHT", + to: "LEFT" | "RIGHT", + timeout: number, + ): Promise; + + /** + * Blocking pop multiple elements from lists + * + * Blocks until an element is available from one of the specified lists or the timeout expires. + * Can pop from the LEFT or RIGHT end and optionally pop multiple elements at once using COUNT. + * + * @param timeout Timeout in seconds (can be fractional, 0 = block indefinitely) + * @param numkeys Number of keys that follow + * @param args Keys, direction ("LEFT" or "RIGHT"), and optional COUNT modifier + * @returns Promise that resolves with [key, [elements]] or null on timeout + * + * @example + * ```ts + * // Pop from left end of first available list, wait 1 second + * const result = await redis.blmpop(1.0, 2, "list1", "list2", "LEFT"); + * if (result) { + * const [key, elements] = result; + * console.log(`Popped from ${key}: ${elements.join(", ")}`); + * } + * + * // Pop 3 elements from right end + * const result2 = await redis.blmpop(0.5, 1, "mylist", "RIGHT", "COUNT", 3); + * // Returns: ["mylist", ["elem1", "elem2", "elem3"]] or null if timeout + * ``` + */ + blmpop(timeout: number, numkeys: number, ...args: (string | number)[]): Promise<[string, string[]] | null>; + + /** + * Blocking right pop from source and left push to destination + * + * Atomically pops an element from the tail of source list and pushes it to the head of destination list, + * blocking until an element is available or the timeout expires. This is the blocking version of RPOPLPUSH. + * + * @param source Source list key + * @param destination Destination list key + * @param timeout Timeout in seconds (can be fractional, 0 = block indefinitely) + * @returns Promise that resolves with the moved element or null on timeout + * + * @example + * ```ts + * // Block for up to 1 second + * const element = await redis.brpoplpush("tasks", "processing", 1.0); + * if (element) { + * console.log(`Processing task: ${element}`); + * } else { + * console.log("No tasks available"); + * } + * + * // Block indefinitely (timeout = 0) + * const task = await redis.brpoplpush("queue", "active", 0); + * ``` + */ + brpoplpush(source: RedisClient.KeyLike, destination: RedisClient.KeyLike, timeout: number): Promise; + + /** + * Get element at index from a list + * @param key The list key + * @param index Zero-based index (negative indexes count from the end, -1 is last element) + * @returns Promise that resolves with the element at index, or null if index is out of range + * + * @example + * ```ts + * await redis.lpush("mylist", "three", "two", "one"); + * console.log(await redis.lindex("mylist", 0)); // "one" + * console.log(await redis.lindex("mylist", -1)); // "three" + * console.log(await redis.lindex("mylist", 5)); // null + * ``` + */ + lindex(key: RedisClient.KeyLike, index: number): Promise; + /** * Get the length of a list * @param key The list key @@ -410,14 +1074,144 @@ declare module "bun" { */ llen(key: RedisClient.KeyLike): Promise; + /** + * Atomically pop an element from a source list and push it to a destination list + * + * Pops an element from the source list (from LEFT or RIGHT) and pushes it + * to the destination list (to LEFT or RIGHT). + * + * @param source The source list key + * @param destination The destination list key + * @param from Direction to pop from source: "LEFT" (head) or "RIGHT" (tail) + * @param to Direction to push to destination: "LEFT" (head) or "RIGHT" (tail) + * @returns Promise that resolves with the element moved, or null if the source list is empty + * + * @example + * ```ts + * await redis.lpush("source", "a", "b", "c"); + * const result1 = await redis.lmove("source", "dest", "LEFT", "RIGHT"); + * // result1: "c" (popped from head of source, pushed to tail of dest) + * + * const result2 = await redis.lmove("source", "dest", "RIGHT", "LEFT"); + * // result2: "a" (popped from tail of source, pushed to head of dest) + * ``` + */ + lmove( + source: RedisClient.KeyLike, + destination: RedisClient.KeyLike, + from: "LEFT" | "RIGHT", + to: "LEFT" | "RIGHT", + ): Promise; + /** * Remove and get the first element in a list * @param key The list key - * @returns Promise that resolves with the first element, or null if the - * list is empty + * @returns Promise that resolves with the first element, or null if the list is empty */ lpop(key: RedisClient.KeyLike): Promise; + /** + * Remove and get the first count elements in a list + * @param key The list key + * @returns Promise that resolves with a list of elements, or null if the list doesn't exist + */ + lpop(key: RedisClient.KeyLike, count: number): Promise; + + /** + * Find the position(s) of an element in a list + * + * Returns the index of matching elements inside a Redis list. + * By default, returns the index of the first match. Use RANK to find the nth occurrence, + * COUNT to get multiple positions, and MAXLEN to limit the search. + * + * @param key The list key + * @param element The element to search for + * @param options Optional arguments: "RANK", rank, "COUNT", num, "MAXLEN", len + * @returns Promise that resolves with the index (number), an array of indices (number[]), + * or null if element is not found. Returns array when COUNT option is used. + * + * @example + * ```ts + * await redis.lpush("mylist", "a", "b", "c", "b", "d"); + * const pos1 = await redis.lpos("mylist", "b"); + * // pos1: 1 (first occurrence of "b") + * + * const pos2 = await redis.lpos("mylist", "b", "RANK", 2); + * // pos2: 3 (second occurrence of "b") + * + * const positions = await redis.lpos("mylist", "b", "COUNT", 0); + * // positions: [1, 3] (all occurrences of "b") + * + * const pos3 = await redis.lpos("mylist", "x"); + * // pos3: null (element not found) + * ``` + */ + lpos( + key: RedisClient.KeyLike, + element: RedisClient.KeyLike, + ...options: (string | number)[] + ): Promise; + + /** + * Pop one or more elements from one or more lists + * + * Pops elements from the first non-empty list in the specified order (LEFT = from head, RIGHT = from tail). + * Optionally specify COUNT to pop multiple elements at once. + * + * @param numkeys The number of keys that follow + * @param args Keys followed by LEFT or RIGHT, optionally followed by "COUNT" and count value + * @returns Promise that resolves with [key, [elements]] or null if all lists are empty + * + * @example + * ```ts + * await redis.lpush("list1", "a", "b", "c"); + * const result1 = await redis.lmpop(1, "list1", "LEFT"); + * // result1: ["list1", ["c"]] + * + * const result2 = await redis.lmpop(1, "list1", "RIGHT", "COUNT", 2); + * // result2: ["list1", ["a", "b"]] + * + * const result3 = await redis.lmpop(2, "emptylist", "list1", "LEFT"); + * // result3: null (if both lists are empty) + * ``` + */ + lmpop(numkeys: number, ...args: (string | number)[]): Promise<[string, string[]] | null>; + + /** + * Get a range of elements from a list + * @param key The list key + * @param start Zero-based start index (negative indexes count from the end) + * @param stop Zero-based stop index (negative indexes count from the end) + * @returns Promise that resolves with array of elements in the specified range + * + * @example + * ```ts + * await redis.lpush("mylist", "three", "two", "one"); + * console.log(await redis.lrange("mylist", 0, -1)); // ["one", "two", "three"] + * console.log(await redis.lrange("mylist", 0, 1)); // ["one", "two"] + * console.log(await redis.lrange("mylist", -2, -1)); // ["two", "three"] + * ``` + */ + lrange(key: RedisClient.KeyLike, start: number, stop: number): Promise; + + /** + * Set element at index in a list + * @param key The list key + * @param index Zero-based index (negative indexes count from the end) + * @param element The value to set + * @returns Promise that resolves with "OK" on success + * + * @example + * ```ts + * await redis.lpush("mylist", "three", "two", "one"); + * await redis.lset("mylist", 0, "zero"); + * console.log(await redis.lrange("mylist", 0, -1)); // ["zero", "two", "three"] + * await redis.lset("mylist", -1, "last"); + * console.log(await redis.lrange("mylist", 0, -1)); // ["zero", "two", "last"] + * ``` + */ + lset(key: RedisClient.KeyLike, index: number, element: RedisClient.KeyLike): Promise; + /** * Remove the expiration from a key * @param key The key to persist @@ -426,6 +1220,14 @@ declare module "bun" { */ persist(key: RedisClient.KeyLike): Promise; + /** + * Set the expiration for a key as a Unix timestamp in milliseconds + * @param key The key to set expiration on + * @param millisecondsTimestamp Unix timestamp in milliseconds when the key should expire + * @returns Promise that resolves with 1 if timeout was set, 0 if key does not exist + */ + pexpireat(key: RedisClient.KeyLike, millisecondsTimestamp: number): Promise; + /** * Get the expiration time of a key as a UNIX timestamp in milliseconds * @param key The key to check @@ -442,6 +1244,25 @@ declare module "bun" { */ pttl(key: RedisClient.KeyLike): Promise; + /** + * Return a random key from the keyspace + * + * Returns a random key from the currently selected database. + * + * @returns Promise that resolves with a random key name, or null if the + * database is empty + * + * @example + * ```ts + * await redis.set("key1", "value1"); + * await redis.set("key2", "value2"); + * await redis.set("key3", "value3"); + * const randomKey = await redis.randomkey(); + * console.log(randomKey); // One of: "key1", "key2", or "key3" + * ``` + */ + randomkey(): Promise; + /** * Remove and get the last element in a list * @param key The list key @@ -449,6 +1270,128 @@ declare module "bun" { */ rpop(key: RedisClient.KeyLike): Promise; + /** + * Remove and get the last element in a list + * @param key The list key + * @returns Promise that resolves with the last element, or null if the list is empty + */ + rpop(key: RedisClient.KeyLike, count: number): Promise; + + /** + * Atomically pop the last element from a source list and push it to the head of a destination list + * + * This is equivalent to LMOVE with "RIGHT" "LEFT". It's an atomic operation that removes + * the last element (tail) from the source list and pushes it to the head of the destination list. + * + * @param source The source list key + * @param destination The destination list key + * @returns Promise that resolves with the element moved, or null if the source list is empty + * + * @example + * ```ts + * await redis.lpush("source", "a", "b", "c"); + * // source: ["c", "b", "a"] + * + * const result = await redis.rpoplpush("source", "dest"); + * // result: "a" (removed from tail of source, added to head of dest) + * // source: ["c", "b"] + * // dest: ["a"] + * ``` + */ + rpoplpush(source: RedisClient.KeyLike, destination: RedisClient.KeyLike): Promise; + + /** + * Incrementally iterate the keyspace + * + * The SCAN command is used to incrementally iterate over a collection of + * elements. SCAN iterates the set of keys in the currently selected Redis + * database. + * + * SCAN is a cursor based iterator. This means that at every call of the + * command, the server returns an updated cursor that the user needs to use + * as the cursor argument in the next call. + * + * An iteration starts when the cursor is set to "0", and terminates when + * the cursor returned by the server is "0". + * + * @param cursor The cursor value (use "0" to start a new iteration) + * @returns Promise that resolves with a tuple [cursor, keys[]] where cursor + * is the next cursor to use (or "0" if iteration is complete) and keys is + * an array of matching keys + * + * @example + * ```ts + * // Basic scan - iterate all keys + * let cursor = "0"; + * const allKeys: string[] = []; + * do { + * const [nextCursor, keys] = await redis.scan(cursor); + * allKeys.push(...keys); + * cursor = nextCursor; + * } while (cursor !== "0"); + * ``` + * + * @example + * ```ts + * // Scan with MATCH pattern + * const [cursor, keys] = await redis.scan("0", "MATCH", "user:*"); + * ``` + * + * @example + * ```ts + * // Scan with COUNT hint + * const [cursor, keys] = await redis.scan("0", "COUNT", "100"); + * ``` + */ + scan(cursor: string | number): Promise<[string, string[]]>; + + /** + * Incrementally iterate the keyspace with a pattern match + * + * @param cursor The cursor value (use "0" to start a new iteration) + * @param match The "MATCH" keyword + * @param pattern The pattern to match (supports glob-style patterns like "user:*") + * @returns Promise that resolves with a tuple [cursor, keys[]] + */ + scan(cursor: string | number, match: "MATCH", pattern: string): Promise<[string, string[]]>; + + /** + * Incrementally iterate the keyspace with a count hint + * + * @param cursor The cursor value (use "0" to start a new iteration) + * @param count The "COUNT" keyword + * @param hint The number of elements to return per call (hint only, not exact) + * @returns Promise that resolves with a tuple [cursor, keys[]] + */ + scan(cursor: string | number, count: "COUNT", hint: number): Promise<[string, string[]]>; + + /** + * Incrementally iterate the keyspace with pattern match and count hint + * + * @param cursor The cursor value (use "0" to start a new iteration) + * @param match The "MATCH" keyword + * @param pattern The pattern to match + * @param count The "COUNT" keyword + * @param hint The number of elements to return per call + * @returns Promise that resolves with a tuple [cursor, keys[]] + */ + scan( + cursor: string | number, + match: "MATCH", + pattern: string, + count: "COUNT", + hint: number, + ): Promise<[string, string[]]>; + + /** + * Incrementally iterate the keyspace with options + * + * @param cursor The cursor value + * @param options Additional SCAN options (MATCH pattern, COUNT hint, etc.) + * @returns Promise that resolves with a tuple [cursor, keys[]] + */ + scan(cursor: string | number, ...options: (string | number)[]): Promise<[string, string[]]>; + /** * Get the number of members in a set * @param key The set key @@ -457,6 +1400,48 @@ declare module "bun" { */ scard(key: RedisClient.KeyLike): Promise; + /** + * Get the difference of multiple sets + * @param key The first set key + * @param keys Additional set keys to subtract from the first set + * @returns Promise that resolves with an array of members in the difference + */ + sdiff(key: RedisClient.KeyLike, ...keys: RedisClient.KeyLike[]): Promise; + + /** + * Get the intersection of multiple sets + * @param key The first set key + * @param keys Additional set keys to intersect + * @returns Promise that resolves with an array of members in the intersection + */ + sinter(key: RedisClient.KeyLike, ...keys: RedisClient.KeyLike[]): Promise; + + /** + * Store the intersection of multiple sets in a key + * @param destination The destination key to store the result + * @param key The first set key + * @param keys Additional set keys to intersect + * @returns Promise that resolves with the number of elements in the resulting set + */ + sinterstore( + destination: RedisClient.KeyLike, + key: RedisClient.KeyLike, + ...keys: RedisClient.KeyLike[] + ): Promise; + + /** + * Get the cardinality of the intersection of multiple sets + * @param numkeys The number of keys to intersect + * @param key The first set key + * @param args Additional set keys and optional LIMIT argument + * @returns Promise that resolves with the number of elements in the intersection + */ + sintercard( + numkeys: number, + key: RedisClient.KeyLike, + ...args: (RedisClient.KeyLike | "LIMIT" | number)[] + ): Promise; + /** * Get the length of the value stored in a key * @param key The key to check @@ -465,6 +1450,57 @@ declare module "bun" { */ strlen(key: RedisClient.KeyLike): Promise; + /** + * Get the union of multiple sets + * @param key The first set key + * @param keys Additional set keys to union + * @returns Promise that resolves with an array of members in the union + */ + sunion(key: RedisClient.KeyLike, ...keys: RedisClient.KeyLike[]): Promise; + + /** + * Store the union of multiple sets in a key + * @param destination The destination key to store the result + * @param key The first set key + * @param keys Additional set keys to union + * @returns Promise that resolves with the number of elements in the resulting set + */ + sunionstore( + destination: RedisClient.KeyLike, + key: RedisClient.KeyLike, + ...keys: RedisClient.KeyLike[] + ): Promise; + + /** + * Determine the type of value stored at key + * + * The TYPE command returns the string representation of the type of the + * value stored at key. The different types that can be returned are: + * string, list, set, zset, hash and stream. + * + * @param key The key to check + * @returns Promise that resolves with the type of value stored at key, or + * "none" if the key doesn't exist + * + * @example + * ```ts + * await redis.set("mykey", "Hello"); + * console.log(await redis.type("mykey")); // "string" + * + * await redis.lpush("mylist", "value"); + * console.log(await redis.type("mylist")); // "list" + * + * await redis.sadd("myset", "value"); + * console.log(await redis.type("myset")); // "set" + * + * await redis.hset("myhash", "field", "value"); + * console.log(await redis.type("myhash")); // "hash" + * + * console.log(await redis.type("nonexistent")); // "none" + * ``` + */ + type(key: RedisClient.KeyLike): Promise<"none" | "string" | "list" | "set" | "zset" | "hash" | "stream">; + /** * Get the number of members in a sorted set * @param key The sorted set key @@ -473,21 +1509,87 @@ declare module "bun" { */ zcard(key: RedisClient.KeyLike): Promise; + /** + * Count the members in a sorted set with scores within the given range + * @param key The sorted set key + * @param min Minimum score (inclusive, use "-inf" for negative infinity) + * @param max Maximum score (inclusive, use "+inf" for positive infinity) + * @returns Promise that resolves with the count of elements in the specified score range + */ + zcount(key: RedisClient.KeyLike, min: string | number, max: string | number): Promise; + + /** + * Count the members in a sorted set within a lexicographical range + * @param key The sorted set key + * @param min Minimum value (use "[" for inclusive, "(" for exclusive, e.g., "[aaa") + * @param max Maximum value (use "[" for inclusive, "(" for exclusive, e.g., "[zzz") + * @returns Promise that resolves with the count of elements in the specified range + */ + zlexcount(key: RedisClient.KeyLike, min: string, max: string): Promise; + /** * Remove and return members with the highest scores in a sorted set * @param key The sorted set key - * @returns Promise that resolves with the removed member and its score, or - * null if the set is empty + * @returns Promise that resolves with either [member, score] or empty + * array if the set is empty */ - zpopmax(key: RedisClient.KeyLike): Promise; + zpopmax(key: RedisClient.KeyLike): Promise<[string, number] | []>; + + /** + * Remove and return members with the highest scores in a sorted set + * @param key The sorted set key + * @param count Optional number of members to pop (default: 1) + * @returns Promise that resolves with an array of [member, score] tuples + */ + zpopmax(key: RedisClient.KeyLike, count: number): Promise>; /** * Remove and return members with the lowest scores in a sorted set * @param key The sorted set key - * @returns Promise that resolves with the removed member and its score, or - * null if the set is empty + * @returns Promise that resolves with array of [member, score] tuples, or + * empty array if the set is empty */ - zpopmin(key: RedisClient.KeyLike): Promise; + zpopmin(key: RedisClient.KeyLike): Promise<[string, number] | []>; + + /** + * Remove and return members with the lowest scores in a sorted set + * @param key The sorted set key + * @param count Optional number of members to pop (default: 1) + * @returns Promise that resolves with an array of [member, score] tuples + */ + zpopmin(key: RedisClient.KeyLike, count: number): Promise<[string, number][]>; + + /** + * Remove and return the member with the lowest score from one or more sorted sets, or block until one is available + * @param args Keys followed by timeout in seconds (e.g., "key1", "key2", 1.0) + * @returns Promise that resolves with [key, member, score] or null if timeout + * @example + * ```ts + * // Block for up to 1 second waiting for an element + * const result = await redis.bzpopmin("myzset", 1.0); + * if (result) { + * const [key, member, score] = result; + * console.log(`Popped ${member} with score ${score} from ${key}`); + * } + * ``` + */ + bzpopmin(...args: (RedisClient.KeyLike | number)[]): Promise<[string, string, number] | null>; + + /** + * Remove and return the member with the highest score from one or more sorted sets, or block until one is available + * @param args Keys followed by timeout in seconds (e.g., "key1", "key2", 1.0) + * @returns Promise that resolves with [key, member, score] or null if timeout + * @example + * ```ts + * // Block for up to 1 second waiting for an element + * const result = await redis.bzpopmax("myzset", 1.0); + * if (result) { + * const [key, member, score] = result; + * console.log(`Popped ${member} with score ${score} from ${key}`); + * } + * ``` + */ + bzpopmax(...args: (RedisClient.KeyLike | number)[]): Promise<[string, string, number] | null>; /** * Get one or multiple random members from a sorted set @@ -497,6 +1599,187 @@ declare module "bun" { */ zrandmember(key: RedisClient.KeyLike): Promise; + /** + * Get one or multiple random members from a sorted set + * @param key The sorted set key + * @returns Promise that resolves with a random member, or null if the set + * is empty + */ + zrandmember(key: RedisClient.KeyLike, count: number): Promise; + + /** + * Get one or multiple random members from a sorted set, with scores + * @param key The sorted set key + * @returns Promise that resolves with a random member, or null if the set + * is empty + */ + zrandmember(key: RedisClient.KeyLike, count: number, withscores: "WITHSCORES"): Promise<[string, number][] | null>; + + /** + * Return a range of members in a sorted set with their scores + * + * @param key The sorted set key + * @param start The starting index + * @param stop The stopping index + * @param withscores Return members with their scores + * @returns Promise that resolves with an array of [member, score, member, score, ...] + * + * @example + * ```ts + * const results = await redis.zrange("myzset", 0, -1, "WITHSCORES"); + * // Returns ["member1", "1.5", "member2", "2.5", ...] + * ``` + */ + zrange( + key: RedisClient.KeyLike, + start: string | number, + stop: string | number, + withscores: "WITHSCORES", + ): Promise<[string, number][]>; + + /** + * Return a range of members in a sorted set by score + * + * @param key The sorted set key + * @param start The minimum score (use "-inf" for negative infinity, "(" prefix for exclusive) + * @param stop The maximum score (use "+inf" for positive infinity, "(" prefix for exclusive) + * @param byscore Indicates score-based range + * @returns Promise that resolves with an array of members with scores in the range + * + * @example + * ```ts + * // Get members with score between 1 and 3 + * const members = await redis.zrange("myzset", "1", "3", "BYSCORE"); + * + * // Get members with score > 1 and <= 3 (exclusive start) + * const members2 = await redis.zrange("myzset", "(1", "3", "BYSCORE"); + * ``` + */ + zrange( + key: RedisClient.KeyLike, + start: string | number, + stop: string | number, + byscore: "BYSCORE", + ): Promise; + + /** + * Return a range of members in a sorted set lexicographically + * + * @param key The sorted set key + * @param start The minimum lexicographical value (use "-" for start, "[" for inclusive, "(" for exclusive) + * @param stop The maximum lexicographical value (use "+" for end, "[" for inclusive, "(" for exclusive) + * @param bylex Indicates lexicographical range + * @returns Promise that resolves with an array of members in the lexicographical range + * + * @example + * ```ts + * // Get members lexicographically from "a" to "c" (inclusive) + * const members = await redis.zrange("myzset", "[a", "[c", "BYLEX"); + * ``` + */ + zrange(key: RedisClient.KeyLike, start: string, stop: string, bylex: "BYLEX"): Promise; + + /** + * Return a range of members in a sorted set with various options + * + * @param key The sorted set key + * @param start The starting value (index, score, or lex depending on options) + * @param stop The stopping value + * @param options Additional options (BYSCORE, BYLEX, REV, LIMIT offset count, WITHSCORES) + * @returns Promise that resolves with an array of members (or with scores if WITHSCORES) + * + * @example + * ```ts + * // Get members by score with limit + * const members = await redis.zrange("myzset", "1", "10", "BYSCORE", "LIMIT", "0", "5"); + * + * // Get members in reverse order with scores + * const reversed = await redis.zrange("myzset", "0", "-1", "REV", "WITHSCORES"); + * ``` + */ + zrange( + key: RedisClient.KeyLike, + start: string | number, + stop: string | number, + ...options: string[] + ): Promise; + + /** + * Return a range of members in a sorted set + * + * Returns the specified range of elements in the sorted set stored at key. + * The elements are considered to be ordered from the lowest to the highest score by default. + * + * @param key The sorted set key + * @param start The starting index (0-based, can be negative to count from end) + * @param stop The stopping index (0-based, can be negative to count from end) + * @returns Promise that resolves with an array of members in the specified range + * + * @example + * ```ts + * // Get all members + * const members = await redis.zrange("myzset", 0, -1); + * + * // Get first 3 members + * const top3 = await redis.zrange("myzset", 0, 2); + * ``` + */ + zrange(key: RedisClient.KeyLike, start: string | number, stop: string | number): Promise; + + /** + * Return a range of members in a sorted set, by index, with scores ordered from high to low + * + * This is equivalent to ZRANGE with the REV option. Returns members in reverse order. + * + * @param key The sorted set key + * @param start The starting index (0-based, can be negative to count from end) + * @param stop The stopping index (0-based, can be negative to count from end) + * @returns Promise that resolves with an array of members in reverse order + * + * @example + * ```ts + * // Get all members in reverse order (highest to lowest score) + * const members = await redis.zrevrange("myzset", 0, -1); + * + * // Get top 3 members with highest scores + * const top3 = await redis.zrevrange("myzset", 0, 2); + * ``` + */ + zrevrange(key: RedisClient.KeyLike, start: number, stop: number): Promise; + + /** + * Return a range of members in a sorted set with their scores, ordered from high to low + * + * @param key The sorted set key + * @param start The starting index + * @param stop The stopping index + * @param withscores Return members with their scores + * @returns Promise that resolves with an array of [member, score, member, score, ...] in reverse order + * + * @example + * ```ts + * const results = await redis.zrevrange("myzset", 0, -1, "WITHSCORES"); + * // Returns ["member3", "3.5", "member2", "2.5", "member1", "1.5", ...] + * ``` + */ + zrevrange( + key: RedisClient.KeyLike, + start: number, + stop: number, + withscores: "WITHSCORES", + ): Promise<[string, number][]>; + + /** + * Return a range of members in a sorted set with options, ordered from high to low + * + * @param key The sorted set key + * @param start The starting index + * @param stop The stopping index + * @param options Additional options (WITHSCORES) + * @returns Promise that resolves with an array of members (or with scores if WITHSCORES) + */ + zrevrange(key: RedisClient.KeyLike, start: number, stop: number, ...options: string[]): Promise; + /** * Append a value to a key * @param key The key to append to @@ -515,6 +1798,29 @@ declare module "bun" { */ getset(key: RedisClient.KeyLike, value: RedisClient.KeyLike): Promise; + /** + * Insert an element before or after another element in a list + * @param key The list key + * @param position "BEFORE" or "AFTER" to specify where to insert + * @param pivot The pivot element to insert before or after + * @param element The element to insert + * @returns Promise that resolves with the length of the list after insert, -1 if pivot not found, or 0 if key doesn't exist + * + * @example + * ```ts + * await redis.lpush("mylist", "World"); + * await redis.lpush("mylist", "Hello"); + * await redis.linsert("mylist", "BEFORE", "World", "There"); + * // List is now: ["Hello", "There", "World"] + * ``` + */ + linsert( + key: RedisClient.KeyLike, + position: "BEFORE" | "AFTER", + pivot: RedisClient.KeyLike, + element: RedisClient.KeyLike, + ): Promise; + /** * Prepend one or multiple values to a list * @param key The list key @@ -522,7 +1828,7 @@ declare module "bun" { * @returns Promise that resolves with the length of the list after the push * operation */ - lpush(key: RedisClient.KeyLike, value: RedisClient.KeyLike): Promise; + lpush(key: RedisClient.KeyLike, value: RedisClient.KeyLike, ...rest: RedisClient.KeyLike[]): Promise; /** * Prepend a value to a list, only if the list exists @@ -533,6 +1839,41 @@ declare module "bun" { */ lpushx(key: RedisClient.KeyLike, value: RedisClient.KeyLike): Promise; + /** + * Remove elements from a list + * @param key The list key + * @param count Number of elements to remove + * - count > 0: Remove count occurrences from head to tail + * - count < 0: Remove count occurrences from tail to head + * - count = 0: Remove all occurrences + * @param element The element to remove + * @returns Promise that resolves with the number of elements removed + * + * @example + * ```ts + * await redis.rpush("mylist", "hello", "hello", "world", "hello"); + * await redis.lrem("mylist", 2, "hello"); // Removes first 2 "hello" + * // List is now: ["world", "hello"] + * ``` + */ + lrem(key: RedisClient.KeyLike, count: number, element: RedisClient.KeyLike): Promise; + + /** + * Trim a list to the specified range + * @param key The list key + * @param start The start index (0-based, can be negative) + * @param stop The stop index (0-based, can be negative) + * @returns Promise that resolves with "OK" + * + * @example + * ```ts + * await redis.rpush("mylist", "one", "two", "three", "four"); + * await redis.ltrim("mylist", 1, 2); + * // List is now: ["two", "three"] + * ``` + */ + ltrim(key: RedisClient.KeyLike, start: number, stop: number): Promise; + /** * Add one or more members to a HyperLogLog * @param key The HyperLogLog key @@ -549,7 +1890,7 @@ declare module "bun" { * @returns Promise that resolves with the length of the list after the push * operation */ - rpush(key: RedisClient.KeyLike, value: RedisClient.KeyLike): Promise; + rpush(key: RedisClient.KeyLike, value: RedisClient.KeyLike, ...rest: RedisClient.KeyLike[]): Promise; /** * Append a value to a list, only if the list exists @@ -569,14 +1910,583 @@ declare module "bun" { */ setnx(key: RedisClient.KeyLike, value: RedisClient.KeyLike): Promise; + /** + * Set key to hold the string value with expiration time in seconds + * @param key The key to set + * @param seconds The expiration time in seconds + * @param value The value to set + * @returns Promise that resolves with "OK" on success + * + * @example + * ```ts + * await redis.setex("mykey", 10, "Hello"); + * // Key will expire after 10 seconds + * ``` + */ + setex(key: RedisClient.KeyLike, seconds: number, value: RedisClient.KeyLike): Promise<"OK">; + + /** + * Set key to hold the string value with expiration time in milliseconds + * @param key The key to set + * @param milliseconds The expiration time in milliseconds + * @param value The value to set + * @returns Promise that resolves with "OK" on success + * + * @example + * ```ts + * await redis.psetex("mykey", 10000, "Hello"); + * // Key will expire after 10000 milliseconds (10 seconds) + * ``` + */ + psetex(key: RedisClient.KeyLike, milliseconds: number, value: RedisClient.KeyLike): Promise<"OK">; + /** * Get the score associated with the given member in a sorted set * @param key The sorted set key * @param member The member to get the score for - * @returns Promise that resolves with the score of the member as a string, + * @returns Promise that resolves with the score of the member as a number, * or null if the member or key doesn't exist */ - zscore(key: RedisClient.KeyLike, member: string): Promise; + zscore(key: RedisClient.KeyLike, member: string): Promise; + + /** + * Increment the score of a member in a sorted set + * @param key The sorted set key + * @param increment The increment value + * @param member The member to increment + * @returns Promise that resolves with the new score + */ + zincrby(key: RedisClient.KeyLike, increment: number, member: RedisClient.KeyLike): Promise; + + /** + * Returns the scores associated with the specified members in the sorted set + * @param key The sorted set key + * @param member The first member to get the score for + * @param members Additional members to get scores for + * @returns Promise that resolves with an array of scores (number for each score, or null if member doesn't exist) + */ + zmscore( + key: RedisClient.KeyLike, + member: RedisClient.KeyLike, + ...members: RedisClient.KeyLike[] + ): Promise<(number | null)[]>; + + /** + * Add one or more members to a sorted set, or update scores if they already exist + * + * ZADD adds all the specified members with the specified scores to the sorted set stored at key. + * It is possible to specify multiple score / member pairs. If a specified member is already a + * member of the sorted set, the score is updated and the element reinserted at the right position + * to ensure the correct ordering. + * + * If key does not exist, a new sorted set with the specified members as sole members is created. + * If the key exists but does not hold a sorted set, an error is returned. + * + * The score values should be the string representation of a double precision floating point number. + * +inf and -inf values are valid values as well. + * + * Options: + * - NX: Only add new elements. Don't update already existing elements. + * - XX: Only update elements that already exist. Never add elements. + * - GT: Only update existing elements if the new score is greater than the current score. This flag doesn't prevent adding new elements. + * - LT: Only update existing elements if the new score is less than the current score. This flag doesn't prevent adding new elements. + * - CH: Modify the return value from the number of new elements added, to the total number of elements changed (CH is an abbreviation of changed). + * - INCR: When this option is specified ZADD acts like ZINCRBY. Only one score-member pair can be specified in this mode. + * + * Note: The GT, LT and NX options are mutually exclusive. + * + * @param key The sorted set key + * @param args Score-member pairs and optional flags (NX, XX, GT, LT, CH, INCR) + * @returns Promise that resolves with the number of elements added (or changed if CH is used, or new score if INCR is used) + * + * @example + * ```ts + * // Add members with scores + * await redis.zadd("myzset", "1", "one", "2", "two", "3", "three"); + * + * // Add with NX option (only if member doesn't exist) + * await redis.zadd("myzset", "NX", "4", "four"); + * + * // Add with XX option (only if member exists) + * await redis.zadd("myzset", "XX", "2.5", "two"); + * + * // Add with CH option (return count of changed elements) + * await redis.zadd("myzset", "CH", "5", "five", "2.1", "two"); + * + * // Use INCR option (increment score) + * await redis.zadd("myzset", "INCR", "1.5", "one"); + * ``` + */ + zadd(key: RedisClient.KeyLike, ...args: (string | number)[]): Promise; + + /** + * Incrementally iterate sorted set elements and their scores + * + * The ZSCAN command is used in order to incrementally iterate over sorted set elements and their scores. + * ZSCAN is a cursor based iterator. This means that at every call of the command, the server returns an + * updated cursor that the user needs to use as the cursor argument in the next call. + * + * An iteration starts when the cursor is set to 0, and terminates when the cursor returned by the server is 0. + * + * ZSCAN and the other SCAN family commands are able to provide to the user a set of guarantees associated + * to full iterations: + * - A full iteration always retrieves all the elements that were present in the collection from the start + * to the end of a full iteration. This means that if a given element is inside the collection when an + * iteration is started, and is still there when an iteration terminates, then at some point ZSCAN returned it. + * - A full iteration never returns any element that was NOT present in the collection from the start to the + * end of a full iteration. So if an element was removed before the start of an iteration, and is never + * added back to the collection for all the time an iteration lasts, ZSCAN ensures that this element will + * never be returned. + * + * Options: + * - MATCH pattern: Only return elements matching the pattern (glob-style) + * - COUNT count: Amount of work done at every call (hint, not exact) + * + * @param key The sorted set key + * @param cursor The cursor value (use 0 to start a new iteration) + * @param options Additional ZSCAN options (MATCH pattern, COUNT hint, etc.) + * @returns Promise that resolves with a tuple [cursor, [member1, score1, member2, score2, ...]] + * + * @example + * ```ts + * // Basic scan - iterate all elements + * let cursor = "0"; + * const allElements: string[] = []; + * do { + * const [nextCursor, elements] = await redis.zscan("myzset", cursor); + * allElements.push(...elements); + * cursor = nextCursor; + * } while (cursor !== "0"); + * ``` + * + * @example + * ```ts + * // Scan with MATCH pattern + * const [cursor, elements] = await redis.zscan("myzset", "0", "MATCH", "user:*"); + * ``` + * + * @example + * ```ts + * // Scan with COUNT hint + * const [cursor, elements] = await redis.zscan("myzset", "0", "COUNT", "100"); + * ``` + */ + zscan(key: RedisClient.KeyLike, cursor: string | number, ...options: string[]): Promise<[string, string[]]>; + + /** + * Remove one or more members from a sorted set + * @param key The sorted set key + * @param member The first member to remove + * @param members Additional members to remove + * @returns Promise that resolves with the number of members removed (not including non-existing members) + */ + zrem(key: RedisClient.KeyLike, member: RedisClient.KeyLike, ...members: RedisClient.KeyLike[]): Promise; + + /** + * Remove all members in a sorted set within the given lexicographical range + * @param key The sorted set key + * @param min Minimum value (use "[" for inclusive, "(" for exclusive, e.g., "[aaa") + * @param max Maximum value (use "[" for inclusive, "(" for exclusive, e.g., "[zzz") + * @returns Promise that resolves with the number of elements removed + */ + zremrangebylex(key: RedisClient.KeyLike, min: string, max: string): Promise; + + /** + * Remove all members in a sorted set within the given rank range + * @param key The sorted set key + * @param start Start rank (0-based, can be negative to indicate offset from end) + * @param stop Stop rank (0-based, can be negative to indicate offset from end) + * @returns Promise that resolves with the number of elements removed + */ + zremrangebyrank(key: RedisClient.KeyLike, start: number, stop: number): Promise; + + /** + * Remove all members in a sorted set within the given score range + * @param key The sorted set key + * @param min Minimum score (inclusive, use "-inf" for negative infinity, "(" prefix for exclusive) + * @param max Maximum score (inclusive, use "+inf" for positive infinity, "(" prefix for exclusive) + * @returns Promise that resolves with the number of elements removed + */ + zremrangebyscore(key: RedisClient.KeyLike, min: string | number, max: string | number): Promise; + + /** + * Return members in a sorted set within a lexicographical range + * + * When all the elements in a sorted set have the same score, this command + * returns the elements between min and max in lexicographical order. + * + * Lex ranges: + * - `[member` for inclusive lower bound + * - `(member` for exclusive lower bound + * - `-` for negative infinity + * - `+` for positive infinity + * + * @param key The sorted set key (all members must have the same score) + * @param min Minimum lexicographical value (use "-" for negative infinity, "[" or "(" for inclusive/exclusive) + * @param max Maximum lexicographical value (use "+" for positive infinity, "[" or "(" for inclusive/exclusive) + * @returns Promise that resolves with array of members + * + * @example + * ```ts + * await redis.send("ZADD", ["myzset", "0", "apple", "0", "banana", "0", "cherry"]); + * const members = await redis.zrangebylex("myzset", "[banana", "[cherry"); + * // Returns: ["banana", "cherry"] + * ``` + */ + zrangebylex(key: RedisClient.KeyLike, min: string, max: string): Promise; + + /** + * Return members in a sorted set within a lexicographical range, with pagination + * + * @param key The sorted set key + * @param min Minimum lexicographical value + * @param max Maximum lexicographical value + * @param limit The "LIMIT" keyword + * @param offset The number of elements to skip + * @param count The maximum number of elements to return + * @returns Promise that resolves with array of members + * + * @example + * ```ts + * await redis.send("ZADD", ["myzset", "0", "a", "0", "b", "0", "c", "0", "d"]); + * const result = await redis.zrangebylex("myzset", "-", "+", "LIMIT", 1, 2); + * // Returns: ["b", "c"] + * ``` + */ + zrangebylex( + key: RedisClient.KeyLike, + min: string, + max: string, + limit: "LIMIT", + offset: number, + count: number, + ): Promise; + + /** + * Return members in a sorted set within a lexicographical range, with options + * + * @param key The sorted set key + * @param min Minimum lexicographical value + * @param max Maximum lexicographical value + * @param options Additional options (LIMIT offset count) + * @returns Promise that resolves with array of members + */ + zrangebylex(key: RedisClient.KeyLike, min: string, max: string, ...options: (string | number)[]): Promise; + + /** + * Return members in a sorted set with scores within a given range + * + * Returns all the elements in the sorted set at key with a score between min + * and max (inclusive by default). The elements are considered to be ordered + * from low to high scores. + * + * Score ranges support: + * - `-inf` and `+inf` for negative and positive infinity + * - `(` prefix for exclusive bounds (e.g., `(5` means greater than 5, not including 5) + * + * @param key The sorted set key + * @param min Minimum score (can be "-inf", a number, or prefixed with "(" for exclusive) + * @param max Maximum score (can be "+inf", a number, or prefixed with "(" for exclusive) + * @returns Promise that resolves with array of members + * + * @example + * ```ts + * await redis.send("ZADD", ["myzset", "1", "one", "2", "two", "3", "three"]); + * const members = await redis.zrangebyscore("myzset", 1, 2); + * // Returns: ["one", "two"] + * ``` + */ + zrangebyscore(key: RedisClient.KeyLike, min: string | number, max: string | number): Promise; + + /** + * Return members in a sorted set with scores within a given range, with scores + * + * @param key The sorted set key + * @param min Minimum score + * @param max Maximum score + * @param withscores The "WITHSCORES" keyword to return scores along with members + * @returns Promise that resolves with array of [member, score, member, score, ...] + * + * @example + * ```ts + * await redis.send("ZADD", ["myzset", "1", "one", "2", "two", "3", "three"]); + * const result = await redis.zrangebyscore("myzset", 1, 2, "WITHSCORES"); + * // Returns: ["one", "1", "two", "2"] + * ``` + */ + zrangebyscore( + key: RedisClient.KeyLike, + min: string | number, + max: string | number, + withscores: "WITHSCORES", + ): Promise<[string, number][]>; + + /** + * Return members in a sorted set with scores within a given range, with pagination + * + * @param key The sorted set key + * @param min Minimum score + * @param max Maximum score + * @param limit The "LIMIT" keyword + * @param offset The number of elements to skip + * @param count The maximum number of elements to return + * @returns Promise that resolves with array of members + * + * @example + * ```ts + * await redis.send("ZADD", ["myzset", "1", "one", "2", "two", "3", "three", "4", "four"]); + * const result = await redis.zrangebyscore("myzset", "-inf", "+inf", "LIMIT", 1, 2); + * // Returns: ["two", "three"] + * ``` + */ + zrangebyscore( + key: RedisClient.KeyLike, + min: string | number, + max: string | number, + limit: "LIMIT", + offset: number, + count: number, + ): Promise; + + /** + * Return members in a sorted set with scores within a given range, with the score values + * + * @param key The sorted set key + * @param min Minimum score + * @param max Maximum score + * @param options Additional options (WITHSCORES, LIMIT offset count) + * @returns Promise that resolves with array of members (and scores if WITHSCORES is used) + */ + zrangebyscore( + key: RedisClient.KeyLike, + min: string | number, + max: string | number, + withscores: "WITHSCORES", + ...options: (string | number)[] + ): Promise<[string, number][]>; + + /** + * Return members in a sorted set with scores within a given range, with the score values + * + * @param key The sorted set key + * @param min Minimum score + * @param max Maximum score + * @param options Additional options (WITHSCORES, LIMIT offset count) + * @returns Promise that resolves with array of members (and scores if WITHSCORES is used) + */ + zrangebyscore( + key: RedisClient.KeyLike, + min: string | number, + max: string | number, + withscores: "WITHSCORES", + limit: "LIMIT", + offset: number, + count: number, + ...options: (string | number)[] + ): Promise<[string, number][]>; + + /** + * Return members in a sorted set with scores within a given range, with various options + * + * @param key The sorted set key + * @param min Minimum score + * @param max Maximum score + * @param options Additional options (WITHSCORES, LIMIT offset count) + * @returns Promise that resolves with array of members (and scores if WITHSCORES is used) + */ + zrangebyscore( + key: RedisClient.KeyLike, + min: string | number, + max: string | number, + ...options: (string | number)[] + ): Promise; + + /** + * Return members in a sorted set with scores within a given range, ordered from high to low + * + * Returns all the elements in the sorted set at key with a score between max + * and min (note: max comes before min). The elements are considered to be + * ordered from high to low scores. + * + * Score ranges support: + * - `-inf` and `+inf` for negative and positive infinity + * - `(` prefix for exclusive bounds (e.g., `(5` means less than 5, not including 5) + * + * @param key The sorted set key + * @param max Maximum score (can be "+inf", a number, or prefixed with "(" for exclusive) + * @param min Minimum score (can be "-inf", a number, or prefixed with "(" for exclusive) + * @returns Promise that resolves with array of members + * + * @example + * ```ts + * await redis.send("ZADD", ["myzset", "1", "one", "2", "two", "3", "three"]); + * const members = await redis.zrevrangebyscore("myzset", 2, 1); + * // Returns: ["two", "one"] + * ``` + */ + zrevrangebyscore(key: RedisClient.KeyLike, max: string | number, min: string | number): Promise; + + /** + * Return members in a sorted set with scores within a given range, ordered from high to low, with scores + * + * @param key The sorted set key + * @param max Maximum score + * @param min Minimum score + * @param withscores The "WITHSCORES" keyword to return scores along with members + * @returns Promise that resolves with array of [member, score, member, score, ...] + * + * @example + * ```ts + * await redis.send("ZADD", ["myzset", "1", "one", "2", "two", "3", "three"]); + * const result = await redis.zrevrangebyscore("myzset", 2, 1, "WITHSCORES"); + * // Returns: ["two", "2", "one", "1"] + * ``` + */ + zrevrangebyscore( + key: RedisClient.KeyLike, + max: string | number, + min: string | number, + withscores: "WITHSCORES", + ): Promise<[string, number][]>; + + /** + * Return members in a sorted set with scores within a given range, ordered from high to low, with pagination + * + * @param key The sorted set key + * @param max Maximum score + * @param min Minimum score + * @param limit The "LIMIT" keyword + * @param offset The number of elements to skip + * @param count The maximum number of elements to return + * @returns Promise that resolves with array of members + */ + zrevrangebyscore( + key: RedisClient.KeyLike, + max: string | number, + min: string | number, + limit: "LIMIT", + offset: number, + count: number, + ): Promise; + + /** + * Return members in a sorted set with scores within a given range, ordered from high to low, with options + * + * @param key The sorted set key + * @param max Maximum score + * @param min Minimum score + * @param options Additional options (WITHSCORES, LIMIT offset count) + * @returns Promise that resolves with array of members (and scores if WITHSCORES is used) + */ + zrevrangebyscore( + key: RedisClient.KeyLike, + max: string | number, + min: string | number, + ...options: (string | number)[] + ): Promise; + + /** + * Return members in a sorted set within a lexicographical range, ordered from high to low + * + * All members in a sorted set must have the same score for this command to work correctly. + * The max and min arguments have the same meaning as in ZRANGEBYLEX, but in reverse order. + * + * Use "[" for inclusive bounds and "(" for exclusive bounds. Use "-" for negative infinity and "+" for positive infinity. + * + * @param key The sorted set key + * @param max The maximum lexicographical value (inclusive with "[", exclusive with "(") + * @param min The minimum lexicographical value (inclusive with "[", exclusive with "(") + * @param options Optional LIMIT clause: ["LIMIT", offset, count] + * @returns Promise that resolves with an array of members in reverse lexicographical order + * + * @example + * ```ts + * // Add members with same score + * await redis.send("ZADD", ["myzset", "0", "a", "0", "b", "0", "c", "0", "d"]); + * + * // Get range from highest to lowest + * const members = await redis.zrevrangebylex("myzset", "[d", "[b"); + * console.log(members); // ["d", "c", "b"] + * + * // With LIMIT + * const limited = await redis.zrevrangebylex("myzset", "+", "-", "LIMIT", "0", "2"); + * console.log(limited); // ["d", "c"] (first 2 members) + * ``` + */ + zrevrangebylex(key: RedisClient.KeyLike, max: string, min: string, ...options: string[]): Promise; + + /** + * Store a range of members from a sorted set into a destination key + * + * This command is like ZRANGE but stores the result in a destination key instead of returning it. + * Supports all the same options as ZRANGE including BYSCORE, BYLEX, REV, and LIMIT. + * + * @param destination The destination key to store results + * @param source The source sorted set key + * @param start The starting index or score + * @param stop The ending index or score + * @param options Optional flags: ["BYSCORE"], ["BYLEX"], ["REV"], ["LIMIT", offset, count] + * @returns Promise that resolves with the number of elements in the resulting sorted set + * + * @example + * ```ts + * // Add members to source set + * await redis.send("ZADD", ["source", "1", "one", "2", "two", "3", "three"]); + * + * // Store range by rank + * const count1 = await redis.zrangestore("dest1", "source", 0, 1); + * console.log(count1); // 2 + * + * // Store range by score + * const count2 = await redis.zrangestore("dest2", "source", "1", "2", "BYSCORE"); + * console.log(count2); // 2 + * + * // Store in reverse order with limit + * const count3 = await redis.zrangestore("dest3", "source", "0", "-1", "REV", "LIMIT", "0", "2"); + * console.log(count3); // 2 + * ``` + */ + zrangestore( + destination: RedisClient.KeyLike, + source: RedisClient.KeyLike, + start: string | number, + stop: string | number, + ...options: string[] + ): Promise; + + /** + * Determine the index of a member in a sorted set + * @param key The sorted set key + * @param member The member to find + * @returns Promise that resolves with the rank (index) of the member, or null if the member doesn't exist + */ + zrank(key: RedisClient.KeyLike, member: string): Promise; + + /** + * Determine the index of a member in a sorted set with score + * @param key The sorted set key + * @param member The member to find + * @param withscore "WITHSCORE" to include the score + * @returns Promise that resolves with [rank, score] or null if the member doesn't exist + */ + zrank(key: RedisClient.KeyLike, member: string, withscore: "WITHSCORE"): Promise<[number, number] | null>; + + /** + * Determine the index of a member in a sorted set, with scores ordered from high to low + * @param key The sorted set key + * @param member The member to find + * @returns Promise that resolves with the rank (index) of the member, or null if the member doesn't exist + */ + zrevrank(key: RedisClient.KeyLike, member: string): Promise; + + /** + * Determine the index of a member in a sorted set with score, with scores ordered from high to low + * @param key The sorted set key + * @param member The member to find + * @param withscore "WITHSCORE" to include the score + * @returns Promise that resolves with [rank, score] or null if the member doesn't exist + */ + zrevrank(key: RedisClient.KeyLike, member: string, withscore: "WITHSCORE"): Promise<[number, number] | null>; /** * Get the values of all specified keys @@ -586,6 +2496,55 @@ declare module "bun" { */ mget(...keys: RedisClient.KeyLike[]): Promise<(string | null)[]>; + /** + * Set multiple keys to multiple values atomically + * + * Sets the given keys to their respective values. MSET replaces existing + * values with new values, just as regular SET. Use MSETNX if you don't want + * to overwrite existing values. + * + * MSET is atomic, so all given keys are set at once. It is not possible for + * clients to see that some of the keys were updated while others are + * unchanged. + * + * @param keyValuePairs Alternating keys and values (key1, value1, key2, value2, ...) + * @returns Promise that resolves with "OK" on success + * + * @example + * ```ts + * await redis.mset("key1", "value1", "key2", "value2"); + * ``` + */ + mset(...keyValuePairs: RedisClient.KeyLike[]): Promise<"OK">; + + /** + * Set multiple keys to multiple values, only if none of the keys exist + * + * Sets the given keys to their respective values. MSETNX will not perform + * any operation at all even if just a single key already exists. + * + * Because of this semantic, MSETNX can be used in order to set different + * keys representing different fields of a unique logic object in a way that + * ensures that either all the fields or none at all are set. + * + * MSETNX is atomic, so all given keys are set at once. It is not possible + * for clients to see that some of the keys were updated while others are + * unchanged. + * + * @param keyValuePairs Alternating keys and values (key1, value1, key2, value2, ...) + * @returns Promise that resolves with 1 if all keys were set, 0 if no key was set + * + * @example + * ```ts + * // Returns 1 if keys don't exist + * await redis.msetnx("key1", "value1", "key2", "value2"); + * + * // Returns 0 if any key already exists + * await redis.msetnx("key1", "newvalue", "key3", "value3"); + * ``` + */ + msetnx(...keyValuePairs: RedisClient.KeyLike[]): Promise; + /** * Count the number of set bits (population counting) in a string * @param key The key to count bits in @@ -593,6 +2552,52 @@ declare module "bun" { */ bitcount(key: RedisClient.KeyLike): Promise; + /** + * Returns the bit value at offset in the string value stored at key + * @param key The key containing the string value + * @param offset The bit offset (zero-based) + * @returns Promise that resolves with the bit value (0 or 1) at the specified offset + */ + getbit(key: RedisClient.KeyLike, offset: number): Promise; + + /** + * Sets or clears the bit at offset in the string value stored at key + * @param key The key to modify + * @param offset The bit offset (zero-based) + * @param value The bit value to set (0 or 1) + * @returns Promise that resolves with the original bit value stored at offset + */ + setbit(key: RedisClient.KeyLike, offset: number, value: 0 | 1): Promise; + + /** + * Get a substring of the string stored at a key + * @param key The key to get the substring from + * @param start The starting offset (can be negative to count from the end) + * @param end The ending offset (can be negative to count from the end) + * @returns Promise that resolves with the substring, or an empty string if the key doesn't exist + */ + getrange(key: RedisClient.KeyLike, start: number, end: number): Promise; + + /** + * Get a substring of the string stored at a key + * @param key The key to retrieve from + * @param start The starting offset + * @param end The ending offset + * @returns Promise that resolves with the substring value + * + * @deprecated Use {@link getrange} instead. SUBSTR is a deprecated Redis command. + */ + substr(key: RedisClient.KeyLike, start: number, end: number): Promise; + + /** + * Overwrite part of a string at key starting at the specified offset + * @param key The key to modify + * @param offset The offset at which to start overwriting (zero-based) + * @param value The string value to write at the offset + * @returns Promise that resolves with the length of the string after modification + */ + setrange(key: RedisClient.KeyLike, offset: number, value: RedisClient.KeyLike): Promise; + /** * Return a serialized version of the value stored at the specified key * @param key The key to dump @@ -812,6 +2817,526 @@ declare module "bun" { * This will open up a new connection to the Redis server. */ duplicate(): Promise; + + /** + * Copy the value stored at the source key to the destination key + * + * By default, the destination key is created in the logical database used + * by the connection. The REPLACE option removes the destination key before + * copying the value to it. + * + * @param source The source key to copy from + * @param destination The destination key to copy to + * @returns Promise that resolves with 1 if the key was copied, 0 if not + * + * @example + * ```ts + * await redis.set("mykey", "Hello"); + * await redis.copy("mykey", "myotherkey"); + * console.log(await redis.get("myotherkey")); // "Hello" + * ``` + */ + copy(source: RedisClient.KeyLike, destination: RedisClient.KeyLike): Promise; + + /** + * Copy the value stored at the source key to the destination key, optionally replacing it + * + * The REPLACE option removes the destination key before copying the value to it. + * + * @param source The source key to copy from + * @param destination The destination key to copy to + * @param replace "REPLACE" - Remove the destination key before copying + * @returns Promise that resolves with 1 if the key was copied, 0 if not + * + * @example + * ```ts + * await redis.set("mykey", "Hello"); + * await redis.set("myotherkey", "World"); + * await redis.copy("mykey", "myotherkey", "REPLACE"); + * console.log(await redis.get("myotherkey")); // "Hello" + * ``` + */ + copy(source: RedisClient.KeyLike, destination: RedisClient.KeyLike, replace: "REPLACE"): Promise; + + /** + * Asynchronously delete one or more keys + * + * This command is very similar to DEL: it removes the specified keys. + * Just like DEL a key is ignored if it does not exist. However, the + * command performs the actual memory reclaiming in a different thread, so + * it is not blocking, while DEL is. This is particularly useful when + * deleting large values or large numbers of keys. + * + * @param keys The keys to delete + * @returns Promise that resolves with the number of keys that were unlinked + * + * @example + * ```ts + * await redis.set("key1", "Hello"); + * await redis.set("key2", "World"); + * const count = await redis.unlink("key1", "key2", "key3"); + * console.log(count); // 2 + * ``` + */ + unlink(...keys: RedisClient.KeyLike[]): Promise; + + /** + * Alters the last access time of one or more keys + * + * A key is ignored if it does not exist. The command returns the number + * of keys that were touched. + * + * This command is useful in conjunction with maxmemory-policy + * allkeys-lru / volatile-lru to change the last access time of keys for + * eviction purposes. + * + * @param keys One or more keys to touch + * @returns Promise that resolves with the number of keys that were touched + * + * @example + * ```ts + * await redis.set("key1", "Hello"); + * await redis.set("key2", "World"); + * const touched = await redis.touch("key1", "key2", "key3"); + * console.log(touched); // 2 (key3 doesn't exist) + * ``` + */ + touch(...keys: RedisClient.KeyLike[]): Promise; + + /** + * Rename a key to a new key + * + * Renames key to newkey. If newkey already exists, it is overwritten. If + * key does not exist, an error is returned. + * + * @param key The key to rename + * @param newkey The new key name + * @returns Promise that resolves with "OK" on success + * + * @example + * ```ts + * await redis.set("mykey", "Hello"); + * await redis.rename("mykey", "myotherkey"); + * const value = await redis.get("myotherkey"); // "Hello" + * const oldValue = await redis.get("mykey"); // null + * ``` + */ + rename(key: RedisClient.KeyLike, newkey: RedisClient.KeyLike): Promise<"OK">; + + /** + * Rename a key to a new key only if the new key does not exist + * + * Renames key to newkey only if newkey does not yet exist. If key does not + * exist, an error is returned. + * + * @param key The key to rename + * @param newkey The new key name + * @returns Promise that resolves with 1 if the key was renamed, 0 if newkey already exists + * + * @example + * ```ts + * await redis.set("mykey", "Hello"); + * await redis.renamenx("mykey", "myotherkey"); // Returns 1 + * await redis.set("mykey2", "World"); + * await redis.renamenx("mykey2", "myotherkey"); // Returns 0 (myotherkey exists) + * ``` + */ + renamenx(key: RedisClient.KeyLike, newkey: RedisClient.KeyLike): Promise; + + /** + * Compute the difference between sorted sets with scores + * + * @param numkeys The number of sorted set keys + * @param keys The sorted set keys followed by "WITHSCORES" + * @returns Promise that resolves with an array of [member, score] pairs + * + * @example + * ```ts + * await redis.send("ZADD", ["zset1", "1", "one", "2", "two", "3", "three"]); + * await redis.send("ZADD", ["zset2", "1", "one", "2", "two"]); + * const diff = await redis.zdiff(2, "zset1", "zset2", "WITHSCORES"); + * console.log(diff); // ["three", "3"] + * ``` + */ + zdiff( + numkeys: number, + ...args: [...keys: RedisClient.KeyLike[], withscores: "WITHSCORES"] + ): Promise<[string, number][]>; + + /** + * Compute the difference between the first sorted set and all successive sorted sets + * + * Returns the members of the sorted set resulting from the difference between the first + * sorted set and all the successive sorted sets. The first key is the only one used to + * compute the members of the difference. + * + * @param numkeys The number of sorted set keys + * @param keys The sorted set keys to compare + * @returns Promise that resolves with an array of members + * + * @example + * ```ts + * await redis.send("ZADD", ["zset1", "1", "one", "2", "two", "3", "three"]); + * await redis.send("ZADD", ["zset2", "1", "one", "2", "two"]); + * const diff = await redis.zdiff(2, "zset1", "zset2"); + * console.log(diff); // ["three"] + * ``` + */ + zdiff(numkeys: number, ...keys: RedisClient.KeyLike[]): Promise; + + /** + * Compute the difference between sorted sets and store the result + * + * Computes the difference between the first and all successive sorted sets given by the + * specified keys and stores the result in destination. Keys that do not exist are + * considered to be empty sets. + * + * @param destination The destination key to store the result + * @param numkeys The number of input sorted set keys + * @param keys The sorted set keys to compare + * @returns Promise that resolves with the number of elements in the resulting sorted set + * + * @example + * ```ts + * await redis.send("ZADD", ["zset1", "1", "one", "2", "two", "3", "three"]); + * await redis.send("ZADD", ["zset2", "1", "one"]); + * const count = await redis.zdiffstore("out", 2, "zset1", "zset2"); + * console.log(count); // 2 (two, three) + * ``` + */ + zdiffstore(destination: RedisClient.KeyLike, numkeys: number, ...keys: RedisClient.KeyLike[]): Promise; + + /** + * Compute the intersection of multiple sorted sets + * + * Returns the members of the set resulting from the intersection of all the given sorted sets. + * Keys that do not exist are considered to be empty sets. + * + * By default, the resulting score of each member is the sum of its scores in the sorted sets where it exists. + * + * Options: + * - WEIGHTS: Multiply the score of each member in the corresponding sorted set by the given weight before aggregation + * - AGGREGATE SUM|MIN|MAX: Specify how the scores are aggregated (default: SUM) + * - WITHSCORES: Return the scores along with the members + * + * @param numkeys The number of input keys (sorted sets) + * @param keys The sorted set keys to intersect + * @returns Promise that resolves with an array of members (or [member, score] pairs if WITHSCORES) + * + * @example + * ```ts + * // Set up sorted sets + * await redis.zadd("zset1", "1", "a", "2", "b", "3", "c"); + * await redis.zadd("zset2", "1", "b", "2", "c", "3", "d"); + * + * // Basic intersection - returns members that exist in all sets + * const result1 = await redis.zinter(2, "zset1", "zset2"); + * // Returns: ["b", "c"] + * + * // With scores (sum by default) + * const result2 = await redis.zinter(2, "zset1", "zset2", "WITHSCORES"); + * // Returns: ["b", "3", "c", "5"] (b: 2+1=3, c: 3+2=5) + * + * // With weights + * const result3 = await redis.zinter(2, "zset1", "zset2", "WEIGHTS", "2", "3", "WITHSCORES"); + * // Returns: ["b", "7", "c", "12"] (b: 2*2+1*3=7, c: 3*2+2*3=12) + * + * // With MIN aggregation + * const result4 = await redis.zinter(2, "zset1", "zset2", "AGGREGATE", "MIN", "WITHSCORES"); + * // Returns: ["b", "1", "c", "2"] (minimum scores) + * ``` + */ + zinter( + numkeys: number, + ...args: [...args: (string | number)[], withscores: "WITHSCORES"] + ): Promise<[string, number][]>; + + /** + * Compute the intersection of multiple sorted sets + * + * Returns the members of the set resulting from the intersection of all the given sorted sets. + * Keys that do not exist are considered to be empty sets. + * + * By default, the resulting score of each member is the sum of its scores in the sorted sets where it exists. + * + * Options: + * - WEIGHTS: Multiply the score of each member in the corresponding sorted set by the given weight before aggregation + * - AGGREGATE SUM|MIN|MAX: Specify how the scores are aggregated (default: SUM) + * - WITHSCORES: Return the scores along with the members + * + * @param numkeys The number of input keys (sorted sets) + * @param keys The sorted set keys to intersect + * @returns Promise that resolves with an array of members (or [member, score] pairs if WITHSCORES) + * + * @example + * ```ts + * // Set up sorted sets + * await redis.zadd("zset1", "1", "a", "2", "b", "3", "c"); + * await redis.zadd("zset2", "1", "b", "2", "c", "3", "d"); + * + * // Basic intersection - returns members that exist in all sets + * const result1 = await redis.zinter(2, "zset1", "zset2"); + * // Returns: ["b", "c"] + * + * // With scores (sum by default) + * const result2 = await redis.zinter(2, "zset1", "zset2", "WITHSCORES"); + * // Returns: ["b", "3", "c", "5"] (b: 2+1=3, c: 3+2=5) + * + * // With weights + * const result3 = await redis.zinter(2, "zset1", "zset2", "WEIGHTS", "2", "3", "WITHSCORES"); + * // Returns: ["b", "7", "c", "12"] (b: 2*2+1*3=7, c: 3*2+2*3=12) + * + * // With MIN aggregation + * const result4 = await redis.zinter(2, "zset1", "zset2", "AGGREGATE", "MIN", "WITHSCORES"); + * // Returns: ["b", "1", "c", "2"] (minimum scores) + * ``` + */ + zinter(numkeys: number, ...args: (string | number)[]): Promise; + + /** + * Count the number of members in the intersection of multiple sorted sets + * + * Computes the cardinality of the intersection of the sorted sets at the specified keys. + * The intersection includes only elements that exist in all of the given sorted sets. + * + * When a LIMIT is provided, the command stops counting once the limit is reached, which + * is useful for performance when you only need to know if the cardinality exceeds a + * certain threshold. + * + * @param numkeys The number of sorted set keys + * @param keys The sorted set keys to intersect + * @returns Promise that resolves with the number of elements in the intersection + * + * @example + * ```ts + * await redis.send("ZADD", ["zset1", "1", "one", "2", "two", "3", "three"]); + * await redis.send("ZADD", ["zset2", "1", "one", "2", "two", "4", "four"]); + * const count = await redis.zintercard(2, "zset1", "zset2"); + * console.log(count); // 2 (one, two) + * ``` + */ + zintercard(numkeys: number, ...keys: RedisClient.KeyLike[]): Promise; + + /** + * Count the number of members in the intersection with a limit + * + * @param numkeys The number of sorted set keys + * @param keys The sorted set keys followed by "LIMIT" and limit value + * @returns Promise that resolves with the number of elements (up to limit) + * + * @example + * ```ts + * await redis.send("ZADD", ["zset1", "1", "a", "2", "b", "3", "c"]); + * await redis.send("ZADD", ["zset2", "1", "a", "2", "b", "3", "c"]); + * const count = await redis.zintercard(2, "zset1", "zset2", "LIMIT", 2); + * console.log(count); // 2 (stopped at limit) + * ``` + */ + zintercard(numkeys: number, ...args: (RedisClient.KeyLike | "LIMIT" | number)[]): Promise; + + /** + * Compute the intersection of multiple sorted sets and store in destination + * + * This command is similar to ZINTER, but instead of returning the result, it stores it in the destination key. + * If the destination key already exists, it is overwritten. + * + * Options: + * - WEIGHTS: Multiply the score of each member in the corresponding sorted set by the given weight before aggregation + * - AGGREGATE SUM|MIN|MAX: Specify how the scores are aggregated (default: SUM) + * + * @param destination The destination key to store the result + * @param numkeys The number of input keys (sorted sets) + * @param keys The sorted set keys to intersect and optional WEIGHTS/AGGREGATE options + * @returns Promise that resolves with the number of elements in the resulting sorted set + * + * @example + * ```ts + * // Set up sorted sets + * await redis.zadd("zset1", "1", "a", "2", "b", "3", "c"); + * await redis.zadd("zset2", "1", "b", "2", "c", "3", "d"); + * + * // Basic intersection store + * const count1 = await redis.zinterstore("out", 2, "zset1", "zset2"); + * // Returns: 2 (stored "b" and "c" in "out") + * + * // With weights + * const count2 = await redis.zinterstore("out2", 2, "zset1", "zset2", "WEIGHTS", "2", "3"); + * // Returns: 2 + * + * // With MAX aggregation + * const count3 = await redis.zinterstore("out3", 2, "zset1", "zset2", "AGGREGATE", "MAX"); + * // Returns: 2 (stores maximum scores) + * ``` + */ + zinterstore(destination: RedisClient.KeyLike, numkeys: number, ...args: (string | number)[]): Promise; + + /** + * Compute the union of multiple sorted sets + * + * Returns the union of the sorted sets given by the specified keys. + * For every element that appears in at least one of the input sorted sets, the output will contain that element. + * + * Options: + * - WEIGHTS: Multiply the score of each member in the corresponding sorted set by the given weight before aggregation + * - AGGREGATE SUM|MIN|MAX: Specify how the scores are aggregated (default: SUM) + * - WITHSCORES: Include scores in the result + * + * @param numkeys The number of input keys (sorted sets) + * @param keys The sorted set keys to union and optional WEIGHTS/AGGREGATE/WITHSCORES options + * @returns Promise that resolves with an array of members (or members with scores if WITHSCORES is used) + * + * @example + * ```ts + * // Set up sorted sets + * await redis.zadd("zset1", "1", "a", "2", "b", "3", "c"); + * await redis.zadd("zset2", "4", "b", "5", "c", "6", "d"); + * + * // Basic union + * const members1 = await redis.zunion(2, "zset1", "zset2"); + * // Returns: ["a", "b", "c", "d"] + * + * // With weights + * const members2 = await redis.zunion(2, "zset1", "zset2", "WEIGHTS", "2", "3"); + * // Returns: ["a", "b", "c", "d"] with calculated scores + * + * // With MIN aggregation + * const members3 = await redis.zunion(2, "zset1", "zset2", "AGGREGATE", "MIN"); + * // Returns: ["a", "b", "c", "d"] with minimum scores + * + * // With scores + * const withScores = await redis.zunion(2, "zset1", "zset2", "WITHSCORES"); + * // Returns: ["a", "1", "b", "2", "c", "3", "d", "6"] (alternating member and score) + * ``` + */ + zunion( + numkeys: number, + ...args: [...args: (string | number)[], withscores: "WITHSCORES"] + ): Promise<[string, number][]>; + + /** + * Compute the union of multiple sorted sets + * + * Returns the union of the sorted sets given by the specified keys. + * For every element that appears in at least one of the input sorted sets, the output will contain that element. + * + * Options: + * - WEIGHTS: Multiply the score of each member in the corresponding sorted set by the given weight before aggregation + * - AGGREGATE SUM|MIN|MAX: Specify how the scores are aggregated (default: SUM) + * - WITHSCORES: Include scores in the result + * + * @param numkeys The number of input keys (sorted sets) + * @param keys The sorted set keys to union and optional WEIGHTS/AGGREGATE/WITHSCORES options + * @returns Promise that resolves with an array of members (or members with scores if WITHSCORES is used) + * + * @example + * ```ts + * // Set up sorted sets + * await redis.zadd("zset1", "1", "a", "2", "b", "3", "c"); + * await redis.zadd("zset2", "4", "b", "5", "c", "6", "d"); + * + * // Basic union + * const members1 = await redis.zunion(2, "zset1", "zset2"); + * // Returns: ["a", "b", "c", "d"] + * + * // With weights + * const members2 = await redis.zunion(2, "zset1", "zset2", "WEIGHTS", "2", "3"); + * // Returns: ["a", "b", "c", "d"] with calculated scores + * + * // With MIN aggregation + * const members3 = await redis.zunion(2, "zset1", "zset2", "AGGREGATE", "MIN"); + * // Returns: ["a", "b", "c", "d"] with minimum scores + * + * // With scores + * const withScores = await redis.zunion(2, "zset1", "zset2", "WITHSCORES"); + * // Returns: ["a", "1", "b", "2", "c", "3", "d", "6"] (alternating member and score) + * ``` + */ + zunion(numkeys: number, ...args: (string | number)[]): Promise; + + /** + * Compute the union of multiple sorted sets and store in destination + * + * This command is similar to ZUNION, but instead of returning the result, it stores it in the destination key. + * If the destination key already exists, it is overwritten. + * + * Options: + * - WEIGHTS: Multiply the score of each member in the corresponding sorted set by the given weight before aggregation + * - AGGREGATE SUM|MIN|MAX: Specify how the scores are aggregated (default: SUM) + * + * @param destination The destination key to store the result + * @param numkeys The number of input keys (sorted sets) + * @param keys The sorted set keys to union and optional WEIGHTS/AGGREGATE options + * @returns Promise that resolves with the number of elements in the resulting sorted set + * + * @example + * ```ts + * // Set up sorted sets + * await redis.zadd("zset1", "1", "a", "2", "b", "3", "c"); + * await redis.zadd("zset2", "4", "b", "5", "c", "6", "d"); + * + * // Basic union store + * const count1 = await redis.zunionstore("out", 2, "zset1", "zset2"); + * // Returns: 4 (stored "a", "b", "c", "d" in "out") + * + * // With weights + * const count2 = await redis.zunionstore("out2", 2, "zset1", "zset2", "WEIGHTS", "2", "3"); + * // Returns: 4 + * + * // With MAX aggregation + * const count3 = await redis.zunionstore("out3", 2, "zset1", "zset2", "AGGREGATE", "MAX"); + * // Returns: 4 (stores maximum scores) + * ``` + */ + zunionstore(destination: RedisClient.KeyLike, numkeys: number, ...args: (string | number)[]): Promise; + + /** + * Remove and return members with scores from one or more sorted sets. + * Pops from the first non-empty sorted set. + * + * @example + * ```ts + * // Pop lowest score from one set + * const result1 = await redis.zmpop(1, "myzset", "MIN"); + * // Returns: ["myzset", [["member1", 1]]] + * + * // Pop highest score from multiple sets + * const result2 = await redis.zmpop(2, "zset1", "zset2", "MAX"); + * // Returns: ["zset1", [["member5", 5]]] (pops from first non-empty) + * + * // Pop multiple members + * const result3 = await redis.zmpop(1, "myzset", "MIN", "COUNT", 3); + * // Returns: ["myzset", [["member1", 1], ["member2", 2], ["member3", 3]]] + * + * // Empty set returns null + * const result4 = await redis.zmpop(1, "emptyset", "MIN"); + * // Returns: null + * ``` + */ + zmpop(numkeys: number, ...args: (string | number)[]): Promise<[string, [string, number][]] | null>; + + /** + * Blocking version of ZMPOP. Blocks until a member is available or timeout expires. + * + * @example + * ```ts + * // Block for 5 seconds waiting for a member + * const result1 = await redis.bzmpop(5, 1, "myzset", "MIN"); + * // Returns: ["myzset", [["member1", 1]]] or null if timeout + * + * // Block indefinitely (timeout 0) + * const result2 = await redis.bzmpop(0, 2, "zset1", "zset2", "MAX"); + * // Returns: ["zset1", [["member5", 5]]] + * + * // Block with COUNT option + * const result3 = await redis.bzmpop(1, 1, "myzset", "MIN", "COUNT", 2); + * // Returns: ["myzset", [["member1", 1], ["member2", 2]]] or null if timeout + * ``` + */ + bzmpop( + timeout: number, + numkeys: number, + ...args: (string | number)[] + ): Promise<[string, [string, number][]] | null>; } /** diff --git a/src/bun.js/api/valkey.classes.ts b/src/bun.js/api/valkey.classes.ts index 3828db06d1..07d8e16a43 100644 --- a/src/bun.js/api/valkey.classes.ts +++ b/src/bun.js/api/valkey.classes.ts @@ -48,10 +48,22 @@ export default [ fn: "incr", length: 1, }, + incrby: { + fn: "incrby", + length: 2, + }, + incrbyfloat: { + fn: "incrbyfloat", + length: 2, + }, decr: { fn: "decr", length: 1, }, + decrby: { + fn: "decrby", + length: 2, + }, exists: { fn: "exists", length: 1, @@ -60,6 +72,14 @@ export default [ fn: "expire", length: 2, }, + expireat: { + fn: "expireat", + length: 2, + }, + pexpire: { + fn: "pexpire", + length: 2, + }, connect: { fn: "jsConnect", length: 0, @@ -78,6 +98,14 @@ export default [ }, hmset: { fn: "hmset", + length: 2, + }, + hset: { + fn: "hset", + length: 2, + }, + hsetnx: { + fn: "hsetnx", length: 3, }, hget: { @@ -88,6 +116,70 @@ export default [ fn: "hmget", length: 2, }, + hdel: { + fn: "hdel", + length: 2, + }, + hexists: { + fn: "hexists", + length: 2, + }, + hrandfield: { + fn: "hrandfield", + length: 1, + }, + hscan: { + fn: "hscan", + length: 2, + }, + hgetdel: { + fn: "hgetdel", + length: 2, + }, + hgetex: { + fn: "hgetex", + length: 2, + }, + hsetex: { + fn: "hsetex", + length: 3, + }, + hexpire: { + fn: "hexpire", + length: 3, + }, + hexpireat: { + fn: "hexpireat", + length: 3, + }, + hexpiretime: { + fn: "hexpiretime", + length: 2, + }, + hpersist: { + fn: "hpersist", + length: 2, + }, + hpexpire: { + fn: "hpexpire", + length: 3, + }, + hpexpireat: { + fn: "hpexpireat", + length: 3, + }, + hpexpiretime: { + fn: "hpexpiretime", + length: 2, + }, + hpttl: { + fn: "hpttl", + length: 2, + }, + httl: { + fn: "httl", + length: 2, + }, sismember: { fn: "sismember", length: 2, @@ -123,6 +215,42 @@ export default [ bitcount: { fn: "bitcount", }, + blmove: { + fn: "blmove", + length: 5, + }, + blmpop: { + fn: "blmpop", + length: 3, + }, + blpop: { + fn: "blpop", + length: 2, + }, + brpop: { + fn: "brpop", + length: 2, + }, + brpoplpush: { + fn: "brpoplpush", + length: 3, + }, + getbit: { + fn: "getbit", + length: 2, + }, + setbit: { + fn: "setbit", + length: 3, + }, + getrange: { + fn: "getrange", + length: 3, + }, + setrange: { + fn: "setrange", + length: 3, + }, dump: { fn: "dump", }, @@ -150,33 +278,132 @@ export default [ keys: { fn: "keys", }, + lindex: { + fn: "lindex", + length: 2, + }, + linsert: { + fn: "linsert", + length: 4, + }, llen: { fn: "llen", }, + lmove: { + fn: "lmove", + length: 4, + }, + lmpop: { + fn: "lmpop", + length: 2, + }, lpop: { fn: "lpop", }, + lpos: { + fn: "lpos", + length: 2, + }, + lrange: { + fn: "lrange", + length: 3, + }, + lrem: { + fn: "lrem", + length: 3, + }, + lset: { + fn: "lset", + length: 3, + }, + ltrim: { + fn: "ltrim", + length: 3, + }, persist: { fn: "persist", }, + pexpireat: { + fn: "pexpireat", + length: 2, + }, pexpiretime: { fn: "pexpiretime", }, pttl: { fn: "pttl", }, + randomkey: { + fn: "randomkey", + length: 0, + }, rpop: { fn: "rpop", }, + rpoplpush: { + fn: "rpoplpush", + length: 2, + }, + scan: { + fn: "scan", + }, scard: { fn: "scard", }, + sdiff: { + fn: "sdiff", + length: 1, + }, + sdiffstore: { + fn: "sdiffstore", + length: 2, + }, + sinter: { + fn: "sinter", + length: 1, + }, + sintercard: { + fn: "sintercard", + length: 1, + }, + sinterstore: { + fn: "sinterstore", + length: 2, + }, + smismember: { + fn: "smismember", + length: 2, + }, + sscan: { + fn: "sscan", + length: 2, + }, strlen: { fn: "strlen", }, + sunion: { + fn: "sunion", + length: 1, + }, + sunionstore: { + fn: "sunionstore", + length: 2, + }, + type: { + fn: "type", + length: 1, + }, zcard: { fn: "zcard", }, + zcount: { + fn: "zcount", + length: 3, + }, + zlexcount: { + fn: "zlexcount", + length: 3, + }, zpopmax: { fn: "zpopmax", }, @@ -186,6 +413,50 @@ export default [ zrandmember: { fn: "zrandmember", }, + zrange: { + fn: "zrange", + length: 3, + }, + zrangebylex: { + fn: "zrangebylex", + length: 3, + }, + zrangebyscore: { + fn: "zrangebyscore", + length: 3, + }, + zrangestore: { + fn: "zrangestore", + length: 4, + }, + zrem: { + fn: "zrem", + length: 2, + }, + zremrangebylex: { + fn: "zremrangebylex", + length: 3, + }, + zremrangebyrank: { + fn: "zremrangebyrank", + length: 3, + }, + zremrangebyscore: { + fn: "zremrangebyscore", + length: 3, + }, + zrevrange: { + fn: "zrevrange", + length: 3, + }, + zrevrangebylex: { + fn: "zrevrangebylex", + length: 3, + }, + zrevrangebyscore: { + fn: "zrevrangebyscore", + length: 3, + }, append: { fn: "append", }, @@ -210,12 +481,85 @@ export default [ setnx: { fn: "setnx", }, + setex: { + fn: "setex", + length: 3, + }, + psetex: { + fn: "psetex", + length: 3, + }, zscore: { fn: "zscore", }, + zincrby: { + fn: "zincrby", + length: 3, + }, + zmscore: { + fn: "zmscore", + }, + zadd: { + fn: "zadd", + length: 3, + }, + zscan: { + fn: "zscan", + length: 2, + }, + zdiff: { + fn: "zdiff", + length: 1, + }, + zdiffstore: { + fn: "zdiffstore", + length: 2, + }, + zinter: { + fn: "zinter", + length: 2, + }, + zintercard: { + fn: "zintercard", + length: 1, + }, + zinterstore: { + fn: "zinterstore", + length: 3, + }, + zunion: { + fn: "zunion", + length: 2, + }, + zunionstore: { + fn: "zunionstore", + length: 3, + }, + zmpop: { + fn: "zmpop", + length: 2, + }, + bzmpop: { + fn: "bzmpop", + length: 3, + }, + bzpopmin: { + fn: "bzpopmin", + length: 2, + }, + bzpopmax: { + fn: "bzpopmax", + length: 2, + }, mget: { fn: "mget", }, + mset: { + fn: "mset", + }, + msetnx: { + fn: "msetnx", + }, ping: { fn: "ping" }, publish: { fn: "publish" }, script: { fn: "script" }, @@ -232,6 +576,11 @@ export default [ unsubscribe: { fn: "unsubscribe" }, punsubscribe: { fn: "punsubscribe" }, pubsub: { fn: "pubsub" }, + copy: { fn: "copy" }, + unlink: { fn: "unlink" }, + touch: { fn: "touch" }, + rename: { fn: "rename", length: 2 }, + renamenx: { fn: "renamenx", length: 2 }, }, values: ["onconnect", "onclose", "connectionPromise", "hello", "subscriptionCallbackMap"], }), diff --git a/src/valkey/js_valkey.zig b/src/valkey/js_valkey.zig index 31342eaa1b..3dd9d3b92a 100644 --- a/src/valkey/js_valkey.zig +++ b/src/valkey/js_valkey.zig @@ -1243,72 +1243,163 @@ pub const JSValkeyClient = struct { pub const @"type" = fns.type; pub const append = fns.append; pub const bitcount = fns.bitcount; + pub const blpop = fns.blpop; + pub const brpop = fns.brpop; + pub const copy = fns.copy; pub const decr = fns.decr; + pub const decrby = fns.decrby; pub const del = fns.del; pub const dump = fns.dump; pub const duplicate = fns.duplicate; pub const exists = fns.exists; pub const expire = fns.expire; + pub const expireat = fns.expireat; pub const expiretime = fns.expiretime; pub const get = fns.get; pub const getBuffer = fns.getBuffer; + pub const getbit = fns.getbit; pub const getdel = fns.getdel; pub const getex = fns.getex; + pub const getrange = fns.getrange; pub const getset = fns.getset; pub const hgetall = fns.hgetall; pub const hget = fns.hget; pub const hincrby = fns.hincrby; pub const hincrbyfloat = fns.hincrbyfloat; pub const hkeys = fns.hkeys; + pub const hdel = fns.hdel; + pub const hexists = fns.hexists; + pub const hgetdel = fns.hgetdel; + pub const hgetex = fns.hgetex; pub const hlen = fns.hlen; pub const hmget = fns.hmget; pub const hmset = fns.hmset; + pub const hrandfield = fns.hrandfield; + pub const hscan = fns.hscan; + pub const hset = fns.hset; + pub const hsetex = fns.hsetex; + pub const hsetnx = fns.hsetnx; pub const hstrlen = fns.hstrlen; pub const hvals = fns.hvals; + pub const hexpire = fns.hexpire; + pub const hexpireat = fns.hexpireat; + pub const hexpiretime = fns.hexpiretime; + pub const hpersist = fns.hpersist; + pub const hpexpire = fns.hpexpire; + pub const hpexpireat = fns.hpexpireat; + pub const hpexpiretime = fns.hpexpiretime; + pub const hpttl = fns.hpttl; + pub const httl = fns.httl; pub const incr = fns.incr; + pub const incrby = fns.incrby; + pub const incrbyfloat = fns.incrbyfloat; pub const keys = fns.keys; + pub const lindex = fns.lindex; + pub const linsert = fns.linsert; pub const llen = fns.llen; + pub const lmove = fns.lmove; + pub const lmpop = fns.lmpop; pub const lpop = fns.lpop; + pub const lpos = fns.lpos; pub const lpush = fns.lpush; pub const lpushx = fns.lpushx; + pub const lrange = fns.lrange; + pub const lrem = fns.lrem; + pub const lset = fns.lset; + pub const ltrim = fns.ltrim; pub const mget = fns.mget; + pub const mset = fns.mset; + pub const msetnx = fns.msetnx; pub const persist = fns.persist; + pub const pexpire = fns.pexpire; + pub const pexpireat = fns.pexpireat; pub const pexpiretime = fns.pexpiretime; pub const pfadd = fns.pfadd; pub const ping = fns.ping; + pub const psetex = fns.psetex; pub const psubscribe = fns.psubscribe; pub const pttl = fns.pttl; pub const publish = fns.publish; pub const pubsub = fns.pubsub; pub const punsubscribe = fns.punsubscribe; + pub const randomkey = fns.randomkey; + pub const rename = fns.rename; + pub const renamenx = fns.renamenx; pub const rpop = fns.rpop; + pub const rpoplpush = fns.rpoplpush; pub const rpush = fns.rpush; pub const rpushx = fns.rpushx; pub const sadd = fns.sadd; + pub const scan = fns.scan; pub const scard = fns.scard; pub const script = fns.script; + pub const sdiff = fns.sdiff; + pub const sdiffstore = fns.sdiffstore; + pub const sinter = fns.sinter; + pub const sintercard = fns.sintercard; + pub const sinterstore = fns.sinterstore; pub const select = fns.select; pub const set = fns.set; + pub const setbit = fns.setbit; + pub const setex = fns.setex; pub const setnx = fns.setnx; + pub const setrange = fns.setrange; pub const sismember = fns.sismember; pub const smembers = fns.smembers; + pub const smismember = fns.smismember; pub const smove = fns.smove; pub const spop = fns.spop; pub const spublish = fns.spublish; pub const srandmember = fns.srandmember; pub const srem = fns.srem; + pub const sscan = fns.sscan; pub const strlen = fns.strlen; pub const subscribe = fns.subscribe; pub const substr = fns.substr; + pub const sunion = fns.sunion; + pub const sunionstore = fns.sunionstore; + pub const touch = fns.touch; pub const ttl = fns.ttl; + pub const unlink = fns.unlink; pub const unsubscribe = fns.unsubscribe; pub const zcard = fns.zcard; + pub const zcount = fns.zcount; + pub const zlexcount = fns.zlexcount; pub const zpopmax = fns.zpopmax; pub const zpopmin = fns.zpopmin; pub const zrandmember = fns.zrandmember; + pub const zrange = fns.zrange; + pub const zrangebylex = fns.zrangebylex; + pub const zrangebyscore = fns.zrangebyscore; + pub const zrangestore = fns.zrangestore; pub const zrank = fns.zrank; + pub const zrem = fns.zrem; + pub const zremrangebylex = fns.zremrangebylex; + pub const zremrangebyrank = fns.zremrangebyrank; + pub const zremrangebyscore = fns.zremrangebyscore; + pub const zrevrange = fns.zrevrange; + pub const zrevrangebylex = fns.zrevrangebylex; + pub const zrevrangebyscore = fns.zrevrangebyscore; pub const zrevrank = fns.zrevrank; pub const zscore = fns.zscore; + pub const zincrby = fns.zincrby; + pub const zmscore = fns.zmscore; + pub const zadd = fns.zadd; + pub const zscan = fns.zscan; + pub const zdiff = fns.zdiff; + pub const zdiffstore = fns.zdiffstore; + pub const zinter = fns.zinter; + pub const zintercard = fns.zintercard; + pub const zinterstore = fns.zinterstore; + pub const zunion = fns.zunion; + pub const zunionstore = fns.zunionstore; + pub const zmpop = fns.zmpop; + pub const bzmpop = fns.bzmpop; + pub const bzpopmin = fns.bzpopmin; + pub const bzpopmax = fns.bzpopmax; + pub const blmove = fns.blmove; + pub const blmpop = fns.blmpop; + pub const brpoplpush = fns.brpoplpush; const fns = @import("./js_valkey_functions.zig"); }; diff --git a/src/valkey/js_valkey_functions.zig b/src/valkey/js_valkey_functions.zig index 06f6befd51..c3f4f9e1d5 100644 --- a/src/valkey/js_valkey_functions.zig +++ b/src/valkey/js_valkey_functions.zig @@ -274,14 +274,34 @@ pub fn ttl(this: *JSValkeyClient, globalObject: *jsc.JSGlobalObject, callframe: pub fn srem(this: *JSValkeyClient, globalObject: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!JSValue { try requireNotSubscriber(this, @src().fn_name); + const args_view = callframe.arguments(); + if (args_view.len < 2) { + return globalObject.throw("SREM requires at least a key and one member", .{}); + } + + var stack_fallback = std.heap.stackFallback(512, bun.default_allocator); + var args = try std.ArrayList(JSArgument).initCapacity(stack_fallback.get(), args_view.len); + defer { + for (args.items) |*item| { + item.deinit(); + } + args.deinit(); + } + const key = (try fromJS(globalObject, callframe.argument(0))) orelse { return globalObject.throwInvalidArgumentType("srem", "key", "string or buffer"); }; - defer key.deinit(); - const value = (try fromJS(globalObject, callframe.argument(1))) orelse { - return globalObject.throwInvalidArgumentType("srem", "value", "string or buffer"); - }; - defer value.deinit(); + args.appendAssumeCapacity(key); + + for (args_view[1..]) |arg| { + if (arg.isUndefinedOrNull()) { + break; + } + const value = (try fromJS(globalObject, arg)) orelse { + return globalObject.throwInvalidArgumentType("srem", "member", "string or buffer"); + }; + args.appendAssumeCapacity(value); + } // Send SREM command const promise = this.send( @@ -289,7 +309,7 @@ pub fn srem(this: *JSValkeyClient, globalObject: *jsc.JSGlobalObject, callframe: callframe.this(), &.{ .command = "SREM", - .args = .{ .args = &.{ key, value } }, + .args = .{ .args = args.items }, }, ) catch |err| { return protocol.valkeyErrorToJS(globalObject, "Failed to send SREM command", err); @@ -301,10 +321,28 @@ pub fn srem(this: *JSValkeyClient, globalObject: *jsc.JSGlobalObject, callframe: pub fn srandmember(this: *JSValkeyClient, globalObject: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!JSValue { try requireNotSubscriber(this, @src().fn_name); + const args_view = callframe.arguments(); + var stack_fallback = std.heap.stackFallback(512, bun.default_allocator); + var args = try std.ArrayList(JSArgument).initCapacity(stack_fallback.get(), args_view.len); + defer { + for (args.items) |*item| { + item.deinit(); + } + args.deinit(); + } + const key = (try fromJS(globalObject, callframe.argument(0))) orelse { return globalObject.throwInvalidArgumentType("srandmember", "key", "string or buffer"); }; - defer key.deinit(); + args.appendAssumeCapacity(key); + + // Optional count argument + if (args_view.len > 1 and !callframe.argument(1).isUndefinedOrNull()) { + const count_arg = try fromJS(globalObject, callframe.argument(1)) orelse { + return globalObject.throwInvalidArgumentType("srandmember", "count", "number or string"); + }; + args.appendAssumeCapacity(count_arg); + } // Send SRANDMEMBER command const promise = this.send( @@ -312,7 +350,7 @@ pub fn srandmember(this: *JSValkeyClient, globalObject: *jsc.JSGlobalObject, cal callframe.this(), &.{ .command = "SRANDMEMBER", - .args = .{ .args = &.{key} }, + .args = .{ .args = args.items }, }, ) catch |err| { return protocol.valkeyErrorToJS(globalObject, "Failed to send SRANDMEMBER command", err); @@ -347,10 +385,28 @@ pub fn smembers(this: *JSValkeyClient, globalObject: *jsc.JSGlobalObject, callfr pub fn spop(this: *JSValkeyClient, globalObject: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!JSValue { try requireNotSubscriber(this, @src().fn_name); + const args_view = callframe.arguments(); + var stack_fallback = std.heap.stackFallback(512, bun.default_allocator); + var args = try std.ArrayList(JSArgument).initCapacity(stack_fallback.get(), args_view.len); + defer { + for (args.items) |*item| { + item.deinit(); + } + args.deinit(); + } + const key = (try fromJS(globalObject, callframe.argument(0))) orelse { return globalObject.throwInvalidArgumentType("spop", "key", "string or buffer"); }; - defer key.deinit(); + args.appendAssumeCapacity(key); + + // Optional count argument + if (args_view.len > 1 and !callframe.argument(1).isUndefinedOrNull()) { + const count_arg = try fromJS(globalObject, callframe.argument(1)) orelse { + return globalObject.throwInvalidArgumentType("spop", "count", "number or string"); + }; + args.appendAssumeCapacity(count_arg); + } // Send SPOP command const promise = this.send( @@ -358,7 +414,7 @@ pub fn spop(this: *JSValkeyClient, globalObject: *jsc.JSGlobalObject, callframe: callframe.this(), &.{ .command = "SPOP", - .args = .{ .args = &.{key} }, + .args = .{ .args = args.items }, }, ) catch |err| { return protocol.valkeyErrorToJS(globalObject, "Failed to send SPOP command", err); @@ -370,14 +426,34 @@ pub fn spop(this: *JSValkeyClient, globalObject: *jsc.JSGlobalObject, callframe: pub fn sadd(this: *JSValkeyClient, globalObject: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!JSValue { try requireNotSubscriber(this, @src().fn_name); + const args_view = callframe.arguments(); + if (args_view.len < 2) { + return globalObject.throw("SADD requires at least a key and one member", .{}); + } + + var stack_fallback = std.heap.stackFallback(512, bun.default_allocator); + var args = try std.ArrayList(JSArgument).initCapacity(stack_fallback.get(), args_view.len); + defer { + for (args.items) |*item| { + item.deinit(); + } + args.deinit(); + } + const key = (try fromJS(globalObject, callframe.argument(0))) orelse { return globalObject.throwInvalidArgumentType("sadd", "key", "string or buffer"); }; - defer key.deinit(); - const value = (try fromJS(globalObject, callframe.argument(1))) orelse { - return globalObject.throwInvalidArgumentType("sadd", "value", "string or buffer"); - }; - defer value.deinit(); + args.appendAssumeCapacity(key); + + for (args_view[1..]) |arg| { + if (arg.isUndefinedOrNull()) { + break; + } + const value = (try fromJS(globalObject, arg)) orelse { + return globalObject.throwInvalidArgumentType("sadd", "member", "string or buffer"); + }; + args.appendAssumeCapacity(value); + } // Send SADD command const promise = this.send( @@ -385,7 +461,7 @@ pub fn sadd(this: *JSValkeyClient, globalObject: *jsc.JSGlobalObject, callframe: callframe.this(), &.{ .command = "SADD", - .args = .{ .args = &.{ key, value } }, + .args = .{ .args = args.items }, }, ) catch |err| { return protocol.valkeyErrorToJS(globalObject, "Failed to send SADD command", err); @@ -425,35 +501,49 @@ pub fn sismember(this: *JSValkeyClient, globalObject: *jsc.JSGlobalObject, callf pub fn hmget(this: *JSValkeyClient, globalObject: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!JSValue { try requireNotSubscriber(this, @src().fn_name); - const key = (try fromJS(globalObject, callframe.argument(0))) orelse { - return globalObject.throwInvalidArgumentType("hmget", "key", "string or buffer"); - }; - defer key.deinit(); - - // Get field array argument - const fields_array = callframe.argument(1); - if (!fields_array.isObject() or !fields_array.isArray()) { - return globalObject.throw("Fields must be an array", .{}); + const args_view = callframe.arguments(); + if (args_view.len < 2) { + return globalObject.throw("HMGET requires at least a key and one field", .{}); } - var iter = try fields_array.arrayIterator(globalObject); - var args = try std.ArrayList(jsc.ZigString.Slice).initCapacity(bun.default_allocator, iter.len + 1); + var stack_fallback = std.heap.stackFallback(512, bun.default_allocator); + var args = try std.ArrayList(JSArgument).initCapacity(stack_fallback.get(), args_view.len); defer { - for (args.items) |item| { + for (args.items) |*item| { item.deinit(); } args.deinit(); } - args.appendAssumeCapacity(jsc.ZigString.Slice.fromUTF8NeverFree(key.slice())); + const key = (try fromJS(globalObject, callframe.argument(0))) orelse { + return globalObject.throwInvalidArgumentType("hmget", "key", "string or buffer"); + }; + args.appendAssumeCapacity(key); - // Add field names as arguments - while (try iter.next()) |field_js| { - const field_str = try field_js.toBunString(globalObject); - defer field_str.deref(); + const second_arg = callframe.argument(1); + if (second_arg.isArray()) { + const array_len = try second_arg.getLength(globalObject); + if (array_len == 0) { + return globalObject.throw("HMGET requires at least one field", .{}); + } - const field_slice = field_str.toUTF8WithoutRef(bun.default_allocator); - args.appendAssumeCapacity(field_slice); + var array_iter = try second_arg.arrayIterator(globalObject); + while (try array_iter.next()) |element| { + const field = (try fromJS(globalObject, element)) orelse { + return globalObject.throwInvalidArgumentType("hmget", "field", "string or buffer"); + }; + try args.append(field); + } + } else { + for (args_view[1..]) |arg| { + if (arg.isUndefinedOrNull()) { + break; + } + const field = (try fromJS(globalObject, arg)) orelse { + return globalObject.throwInvalidArgumentType("hmget", "field", "string or buffer"); + }; + try args.append(field); + } } // Send HMGET command @@ -462,7 +552,7 @@ pub fn hmget(this: *JSValkeyClient, globalObject: *jsc.JSGlobalObject, callframe callframe.this(), &.{ .command = "HMGET", - .args = .{ .slices = args.items }, + .args = .{ .args = args.items }, }, ) catch |err| { return protocol.valkeyErrorToJS(globalObject, "Failed to send HMGET command", err); @@ -534,66 +624,183 @@ pub fn hincrbyfloat(this: *JSValkeyClient, globalObject: *jsc.JSGlobalObject, ca return promise.toJS(); } -// Implement hmset (set multiple values in hash) -pub fn hmset(this: *JSValkeyClient, globalObject: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!JSValue { - try requireNotSubscriber(this, @src().fn_name); +fn hsetImpl(this: *JSValkeyClient, globalObject: *jsc.JSGlobalObject, callframe: *jsc.CallFrame, comptime command: []const u8) bun.JSError!JSValue { + try requireNotSubscriber(this, command); const key = try callframe.argument(0).toBunString(globalObject); defer key.deref(); - // For simplicity, let's accept a list of alternating keys and values - const array_arg = callframe.argument(1); - if (!array_arg.isObject() or !array_arg.isArray()) { - return globalObject.throw("Arguments must be an array of alternating field names and values", .{}); - } + const second_arg = callframe.argument(1); - var iter = try array_arg.arrayIterator(globalObject); - if (iter.len % 2 != 0) { - return globalObject.throw("Arguments must be an array of alternating field names and values", .{}); - } - - var args = try std.ArrayList(jsc.ZigString.Slice).initCapacity(bun.default_allocator, iter.len + 1); + var args = std.ArrayList(jsc.ZigString.Slice).init(bun.default_allocator); defer { - for (args.items) |item| { - item.deinit(); - } + for (args.items) |item| item.deinit(); args.deinit(); } - // Add key as first argument - const key_slice = key.toUTF8WithoutRef(bun.default_allocator); - defer key_slice.deinit(); - args.appendAssumeCapacity(key_slice); + try args.append(key.toUTF8(bun.default_allocator)); - // Add field-value pairs - while (try iter.next()) |field_js| { - // Add field name - const field_str = try field_js.toBunString(globalObject); - defer field_str.deref(); - const field_slice = field_str.toUTF8WithoutRef(bun.default_allocator); - args.appendAssumeCapacity(field_slice); + if (second_arg.isObject() and !second_arg.isArray()) { + // Pattern 1: Object/Record - hset(key, {field: value, ...}) + const obj = second_arg.getObject() orelse { + return globalObject.throwInvalidArgumentType(command, "fields", "object"); + }; - // Add value - if (try iter.next()) |value_js| { - const value_str = try value_js.toBunString(globalObject); + var object_iter = try jsc.JSPropertyIterator(.{ + .skip_empty_name = false, + .include_value = true, + }).init(globalObject, obj); + defer object_iter.deinit(); + + try args.ensureTotalCapacity(1 + object_iter.len * 2); + + while (try object_iter.next()) |field_name| { + const field_slice = field_name.toUTF8(bun.default_allocator); + args.appendAssumeCapacity(field_slice); + + const value_str = try object_iter.value.toBunString(globalObject); defer value_str.deref(); - const value_slice = value_str.toUTF8WithoutRef(bun.default_allocator); + + const value_slice = value_str.toUTF8(bun.default_allocator); args.appendAssumeCapacity(value_slice); - } else { - return globalObject.throw("Arguments must be an array of alternating field names and values", .{}); + } + } else if (second_arg.isArray()) { + // Pattern 3: Array - hmset(key, [field, value, ...]) + var iter = try second_arg.arrayIterator(globalObject); + if (iter.len % 2 != 0) { + return globalObject.throw("Array must have an even number of elements (field-value pairs)", .{}); + } + + try args.ensureTotalCapacity(1 + iter.len); + + while (try iter.next()) |field_js| { + const field_str = try field_js.toBunString(globalObject); + args.appendAssumeCapacity(field_str.toUTF8(bun.default_allocator)); + field_str.deref(); + + const value_js = try iter.next() orelse { + return globalObject.throw("Array must have an even number of elements (field-value pairs)", .{}); + }; + const value_str = try value_js.toBunString(globalObject); + args.appendAssumeCapacity(value_str.toUTF8(bun.default_allocator)); + value_str.deref(); + } + } else { + // Pattern 2: Variadic - hset(key, field, value, ...) + const args_count = callframe.argumentsCount(); + if (args_count < 3) { + return globalObject.throw("HSET requires at least key, field, and value arguments", .{}); + } + + const field_value_count = args_count - 1; // Exclude key + if (field_value_count % 2 != 0) { + return globalObject.throw("HSET requires field-value pairs (even number of arguments after key)", .{}); + } + + try args.ensureTotalCapacity(args_count); + + var i: u32 = 1; + while (i < args_count) : (i += 1) { + const arg_str = try callframe.argument(i).toBunString(globalObject); + args.appendAssumeCapacity(arg_str.toUTF8(bun.default_allocator)); + arg_str.deref(); } } - // Send HMSET command + if (args.items.len == 1) { + return globalObject.throw("HSET requires at least one field-value pair", .{}); + } + const promise = this.send( globalObject, callframe.this(), &.{ - .command = "HMSET", + .command = command, .args = .{ .slices = args.items }, }, ) catch |err| { - return protocol.valkeyErrorToJS(globalObject, "Failed to send HMSET command", err); + const msg = if (bun.strings.eqlComptime(command, "HSET")) "Failed to send HSET command" else "Failed to send HMSET command"; + return protocol.valkeyErrorToJS(globalObject, msg, err); + }; + + return promise.toJS(); +} + +pub fn hset(this: *JSValkeyClient, globalObject: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!JSValue { + return hsetImpl(this, globalObject, callframe, "HSET"); +} + +pub fn hmset(this: *JSValkeyClient, globalObject: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!JSValue { + return hsetImpl(this, globalObject, callframe, "HMSET"); +} + +pub const hdel = compile.@"(key: RedisKey, ...args: RedisKey[])"("hdel", "HDEL", "key", .not_subscriber).call; +pub const hrandfield = compile.@"(key: RedisKey, ...args: RedisKey[])"("hrandfield", "HRANDFIELD", "key", .not_subscriber).call; +pub const hscan = compile.@"(key: RedisKey, ...args: RedisKey[])"("hscan", "HSCAN", "key", .not_subscriber).call; +pub const hgetdel = compile.@"(...strings: string[])"("hgetdel", "HGETDEL", .not_subscriber).call; +pub const hgetex = compile.@"(...strings: string[])"("hgetex", "HGETEX", .not_subscriber).call; +pub const hsetex = compile.@"(...strings: string[])"("hsetex", "HSETEX", .not_subscriber).call; +pub const hexpire = compile.@"(...strings: string[])"("hexpire", "HEXPIRE", .not_subscriber).call; +pub const hexpireat = compile.@"(...strings: string[])"("hexpireat", "HEXPIREAT", .not_subscriber).call; +pub const hexpiretime = compile.@"(...strings: string[])"("hexpiretime", "HEXPIRETIME", .not_subscriber).call; +pub const hpersist = compile.@"(...strings: string[])"("hpersist", "HPERSIST", .not_subscriber).call; +pub const hpexpire = compile.@"(...strings: string[])"("hpexpire", "HPEXPIRE", .not_subscriber).call; +pub const hpexpireat = compile.@"(...strings: string[])"("hpexpireat", "HPEXPIREAT", .not_subscriber).call; +pub const hpexpiretime = compile.@"(...strings: string[])"("hpexpiretime", "HPEXPIRETIME", .not_subscriber).call; +pub const hpttl = compile.@"(...strings: string[])"("hpttl", "HPTTL", .not_subscriber).call; +pub const httl = compile.@"(...strings: string[])"("httl", "HTTL", .not_subscriber).call; + +pub fn hsetnx(this: *JSValkeyClient, globalObject: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!JSValue { + try requireNotSubscriber(this, "hsetnx"); + + const key = (try fromJS(globalObject, callframe.argument(0))) orelse { + return globalObject.throwInvalidArgumentType("hsetnx", "key", "string or buffer"); + }; + defer key.deinit(); + const field = (try fromJS(globalObject, callframe.argument(1))) orelse { + return globalObject.throwInvalidArgumentType("hsetnx", "field", "string or buffer"); + }; + defer field.deinit(); + const value = (try fromJS(globalObject, callframe.argument(2))) orelse { + return globalObject.throwInvalidArgumentType("hsetnx", "value", "string or buffer"); + }; + defer value.deinit(); + + const promise = this.send( + globalObject, + callframe.this(), + &.{ + .command = "HSETNX", + .args = .{ .args = &.{ key, field, value } }, + .meta = .{ .return_as_bool = true }, + }, + ) catch |err| { + return protocol.valkeyErrorToJS(globalObject, "Failed to send HSETNX command", err); + }; + return promise.toJS(); +} + +pub fn hexists(this: *JSValkeyClient, globalObject: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!JSValue { + try requireNotSubscriber(this, "hexists"); + + const key = (try fromJS(globalObject, callframe.argument(0))) orelse + return globalObject.throwInvalidArgumentType("hexists", "key", "string or buffer"); + defer key.deinit(); + + const field = (try fromJS(globalObject, callframe.argument(1))) orelse + return globalObject.throwInvalidArgumentType("hexists", "field", "string or buffer"); + defer field.deinit(); + + const promise = this.send( + globalObject, + callframe.this(), + &.{ + .command = "HEXISTS", + .args = .{ .args = &.{ key, field } }, + .meta = .{ .return_as_bool = true }, + }, + ) catch |err| { + return protocol.valkeyErrorToJS(globalObject, "Failed to send HEXISTS command", err); }; return promise.toJS(); } @@ -631,7 +838,17 @@ pub fn ping(this: *JSValkeyClient, globalObject: *jsc.JSGlobalObject, callframe: } pub const bitcount = compile.@"(key: RedisKey)"("bitcount", "BITCOUNT", "key", .not_subscriber).call; +pub const blmove = compile.@"(...strings: string[])"("blmove", "BLMOVE", .not_subscriber).call; +pub const blmpop = compile.@"(...strings: string[])"("blmpop", "BLMPOP", .not_subscriber).call; +pub const blpop = compile.@"(...strings: string[])"("blpop", "BLPOP", .not_subscriber).call; +pub const brpop = compile.@"(...strings: string[])"("brpop", "BRPOP", .not_subscriber).call; +pub const brpoplpush = compile.@"(key: RedisKey, value: RedisValue, value2: RedisValue)"("brpoplpush", "BRPOPLPUSH", "source", "destination", "timeout", .not_subscriber).call; +pub const getbit = compile.@"(key: RedisKey, value: RedisValue)"("getbit", "GETBIT", "key", "offset", .not_subscriber).call; +pub const setbit = compile.@"(key: RedisKey, value: RedisValue, value2: RedisValue)"("setbit", "SETBIT", "key", "offset", "value", .not_subscriber).call; +pub const getrange = compile.@"(key: RedisKey, value: RedisValue, value2: RedisValue)"("getrange", "GETRANGE", "key", "start", "end", .not_subscriber).call; +pub const setrange = compile.@"(key: RedisKey, value: RedisValue, value2: RedisValue)"("setrange", "SETRANGE", "key", "offset", "value", .not_subscriber).call; pub const dump = compile.@"(key: RedisKey)"("dump", "DUMP", "key", .not_subscriber).call; +pub const expireat = compile.@"(key: RedisKey, value: RedisValue)"("expireat", "EXPIREAT", "key", "timestamp", .not_subscriber).call; pub const expiretime = compile.@"(key: RedisKey)"("expiretime", "EXPIRETIME", "key", .not_subscriber).call; pub const getdel = compile.@"(key: RedisKey)"("getdel", "GETDEL", "key", .not_subscriber).call; pub const getex = compile.@"(...strings: string[])"("getex", "GETEX", .not_subscriber).call; @@ -640,45 +857,133 @@ pub const hkeys = compile.@"(key: RedisKey)"("hkeys", "HKEYS", "key", .not_subsc pub const hlen = compile.@"(key: RedisKey)"("hlen", "HLEN", "key", .not_subscriber).call; pub const hvals = compile.@"(key: RedisKey)"("hvals", "HVALS", "key", .not_subscriber).call; pub const keys = compile.@"(key: RedisKey)"("keys", "KEYS", "key", .not_subscriber).call; +pub const lindex = compile.@"(key: RedisKey, value: RedisValue)"("lindex", "LINDEX", "key", "index", .not_subscriber).call; +pub const linsert = compile.@"(...strings: string[])"("linsert", "LINSERT", .not_subscriber).call; pub const llen = compile.@"(key: RedisKey)"("llen", "LLEN", "key", .not_subscriber).call; -pub const lpop = compile.@"(key: RedisKey)"("lpop", "LPOP", "key", .not_subscriber).call; +pub const lmove = compile.@"(...strings: string[])"("lmove", "LMOVE", .not_subscriber).call; +pub const lmpop = compile.@"(...strings: string[])"("lmpop", "LMPOP", .not_subscriber).call; +pub const lpop = compile.@"(key: RedisKey, ...args: RedisKey[])"("lpop", "LPOP", "key", .not_subscriber).call; +pub const lpos = compile.@"(...strings: string[])"("lpos", "LPOS", .not_subscriber).call; +pub const lrange = compile.@"(key: RedisKey, value: RedisValue, value2: RedisValue)"("lrange", "LRANGE", "key", "start", "stop", .not_subscriber).call; +pub const lrem = compile.@"(key: RedisKey, value: RedisValue, value2: RedisValue)"("lrem", "LREM", "key", "count", "element", .not_subscriber).call; +pub const lset = compile.@"(key: RedisKey, value: RedisValue, value2: RedisValue)"("lset", "LSET", "key", "index", "element", .not_subscriber).call; +pub const ltrim = compile.@"(key: RedisKey, value: RedisValue, value2: RedisValue)"("ltrim", "LTRIM", "key", "start", "stop", .not_subscriber).call; pub const persist = compile.@"(key: RedisKey)"("persist", "PERSIST", "key", .not_subscriber).call; +pub const pexpire = compile.@"(key: RedisKey, value: RedisValue)"("pexpire", "PEXPIRE", "key", "milliseconds", .not_subscriber).call; +pub const pexpireat = compile.@"(key: RedisKey, value: RedisValue)"("pexpireat", "PEXPIREAT", "key", "milliseconds-timestamp", .not_subscriber).call; pub const pexpiretime = compile.@"(key: RedisKey)"("pexpiretime", "PEXPIRETIME", "key", .not_subscriber).call; pub const pttl = compile.@"(key: RedisKey)"("pttl", "PTTL", "key", .not_subscriber).call; -pub const rpop = compile.@"(key: RedisKey)"("rpop", "RPOP", "key", .not_subscriber).call; +pub const randomkey = compile.@"()"("randomkey", "RANDOMKEY", .not_subscriber).call; +pub const rpop = compile.@"(key: RedisKey, ...args: RedisKey[])"("rpop", "RPOP", "key", .not_subscriber).call; +pub const rpoplpush = compile.@"(key: RedisKey, value: RedisValue)"("rpoplpush", "RPOPLPUSH", "source", "destination", .not_subscriber).call; +pub const scan = compile.@"(...strings: string[])"("scan", "SCAN", .not_subscriber).call; pub const scard = compile.@"(key: RedisKey)"("scard", "SCARD", "key", .not_subscriber).call; +pub const sdiff = compile.@"(...strings: string[])"("sdiff", "SDIFF", .not_subscriber).call; +pub const sdiffstore = compile.@"(...strings: string[])"("sdiffstore", "SDIFFSTORE", .not_subscriber).call; +pub const sinter = compile.@"(...strings: string[])"("sinter", "SINTER", .not_subscriber).call; +pub const sintercard = compile.@"(...strings: string[])"("sintercard", "SINTERCARD", .not_subscriber).call; +pub const sinterstore = compile.@"(...strings: string[])"("sinterstore", "SINTERSTORE", .not_subscriber).call; +pub const smismember = compile.@"(...strings: string[])"("smismember", "SMISMEMBER", .not_subscriber).call; +pub const sscan = compile.@"(...strings: string[])"("sscan", "SSCAN", .not_subscriber).call; pub const strlen = compile.@"(key: RedisKey)"("strlen", "STRLEN", "key", .not_subscriber).call; +pub const sunion = compile.@"(...strings: string[])"("sunion", "SUNION", .not_subscriber).call; +pub const sunionstore = compile.@"(...strings: string[])"("sunionstore", "SUNIONSTORE", .not_subscriber).call; pub const @"type" = compile.@"(key: RedisKey)"("type", "TYPE", "key", .not_subscriber).call; pub const zcard = compile.@"(key: RedisKey)"("zcard", "ZCARD", "key", .not_subscriber).call; -pub const zpopmax = compile.@"(key: RedisKey)"("zpopmax", "ZPOPMAX", "key", .not_subscriber).call; -pub const zpopmin = compile.@"(key: RedisKey)"("zpopmin", "ZPOPMIN", "key", .not_subscriber).call; -pub const zrandmember = compile.@"(key: RedisKey)"("zrandmember", "ZRANDMEMBER", "key", .not_subscriber).call; - +pub const zcount = compile.@"(key: RedisKey, value: RedisValue, value2: RedisValue)"("zcount", "ZCOUNT", "key", "min", "max", .not_subscriber).call; +pub const zlexcount = compile.@"(key: RedisKey, value: RedisValue, value2: RedisValue)"("zlexcount", "ZLEXCOUNT", "key", "min", "max", .not_subscriber).call; +pub const zpopmax = compile.@"(key: RedisKey, ...args: RedisKey[])"("zpopmax", "ZPOPMAX", "key", .not_subscriber).call; +pub const zpopmin = compile.@"(key: RedisKey, ...args: RedisKey[])"("zpopmin", "ZPOPMIN", "key", .not_subscriber).call; +pub const zrandmember = compile.@"(key: RedisKey, ...args: RedisKey[])"("zrandmember", "ZRANDMEMBER", "key", .not_subscriber).call; +pub const zrange = compile.@"(...strings: string[])"("zrange", "ZRANGE", .not_subscriber).call; +pub const zrevrange = compile.@"(...strings: string[])"("zrevrange", "ZREVRANGE", .not_subscriber).call; +pub const zrangebyscore = compile.@"(...strings: string[])"("zrangebyscore", "ZRANGEBYSCORE", .not_subscriber).call; +pub const zrevrangebyscore = compile.@"(...strings: string[])"("zrevrangebyscore", "ZREVRANGEBYSCORE", .not_subscriber).call; +pub const zrangebylex = compile.@"(key: RedisKey, ...args: RedisKey[])"("zrangebylex", "ZRANGEBYLEX", "key", .not_subscriber).call; +pub const zrevrangebylex = compile.@"(key: RedisKey, ...args: RedisKey[])"("zrevrangebylex", "ZREVRANGEBYLEX", "key", .not_subscriber).call; pub const append = compile.@"(key: RedisKey, value: RedisValue)"("append", "APPEND", "key", "value", .not_subscriber).call; pub const getset = compile.@"(key: RedisKey, value: RedisValue)"("getset", "GETSET", "key", "value", .not_subscriber).call; pub const hget = compile.@"(key: RedisKey, value: RedisValue)"("hget", "HGET", "key", "field", .not_subscriber).call; +pub const incrby = compile.@"(key: RedisKey, value: RedisValue)"("incrby", "INCRBY", "key", "increment", .not_subscriber).call; +pub const incrbyfloat = compile.@"(key: RedisKey, value: RedisValue)"("incrbyfloat", "INCRBYFLOAT", "key", "increment", .not_subscriber).call; +pub const decrby = compile.@"(key: RedisKey, value: RedisValue)"("decrby", "DECRBY", "key", "decrement", .not_subscriber).call; pub const lpush = compile.@"(key: RedisKey, value: RedisValue, ...args: RedisValue)"("lpush", "LPUSH", .not_subscriber).call; pub const lpushx = compile.@"(key: RedisKey, value: RedisValue, ...args: RedisValue)"("lpushx", "LPUSHX", .not_subscriber).call; pub const pfadd = compile.@"(key: RedisKey, value: RedisValue)"("pfadd", "PFADD", "key", "value", .not_subscriber).call; pub const rpush = compile.@"(key: RedisKey, value: RedisValue, ...args: RedisValue)"("rpush", "RPUSH", .not_subscriber).call; pub const rpushx = compile.@"(key: RedisKey, value: RedisValue, ...args: RedisValue)"("rpushx", "RPUSHX", .not_subscriber).call; pub const setnx = compile.@"(key: RedisKey, value: RedisValue)"("setnx", "SETNX", "key", "value", .not_subscriber).call; +pub const setex = compile.@"(key: RedisKey, value: RedisValue, value2: RedisValue)"("setex", "SETEX", "key", "seconds", "value", .not_subscriber).call; +pub const psetex = compile.@"(key: RedisKey, value: RedisValue, value2: RedisValue)"("psetex", "PSETEX", "key", "milliseconds", "value", .not_subscriber).call; pub const zscore = compile.@"(key: RedisKey, value: RedisValue)"("zscore", "ZSCORE", "key", "value", .not_subscriber).call; - +pub const zincrby = compile.@"(key: RedisKey, value: RedisValue, value2: RedisValue)"("zincrby", "ZINCRBY", "key", "increment", "member", .not_subscriber).call; +pub const zmscore = compile.@"(key: RedisKey, value: RedisValue, ...args: RedisValue)"("zmscore", "ZMSCORE", .not_subscriber).call; +pub const zadd = compile.@"(...strings: string[])"("zadd", "ZADD", .not_subscriber).call; +pub const zscan = compile.@"(...strings: string[])"("zscan", "ZSCAN", .not_subscriber).call; +pub const zdiff = compile.@"(...strings: string[])"("zdiff", "ZDIFF", .not_subscriber).call; +pub const zdiffstore = compile.@"(...strings: string[])"("zdiffstore", "ZDIFFSTORE", .not_subscriber).call; +pub const zinter = compile.@"(...strings: string[])"("zinter", "ZINTER", .not_subscriber).call; +pub const zintercard = compile.@"(...strings: string[])"("zintercard", "ZINTERCARD", .not_subscriber).call; +pub const zinterstore = compile.@"(...strings: string[])"("zinterstore", "ZINTERSTORE", .not_subscriber).call; +pub const zunion = compile.@"(...strings: string[])"("zunion", "ZUNION", .not_subscriber).call; +pub const zunionstore = compile.@"(...strings: string[])"("zunionstore", "ZUNIONSTORE", .not_subscriber).call; +pub const zmpop = compile.@"(...strings: string[])"("zmpop", "ZMPOP", .not_subscriber).call; +pub const bzmpop = compile.@"(...strings: string[])"("bzmpop", "BZMPOP", .not_subscriber).call; +pub const bzpopmin = compile.@"(...strings: string[])"("bzpopmin", "BZPOPMIN", .not_subscriber).call; +pub const bzpopmax = compile.@"(...strings: string[])"("bzpopmax", "BZPOPMAX", .not_subscriber).call; pub const del = compile.@"(key: RedisKey, ...args: RedisKey[])"("del", "DEL", "key", .not_subscriber).call; pub const mget = compile.@"(key: RedisKey, ...args: RedisKey[])"("mget", "MGET", "key", .not_subscriber).call; - +pub const mset = compile.@"(...strings: string[])"("mset", "MSET", .not_subscriber).call; +pub const msetnx = compile.@"(...strings: string[])"("msetnx", "MSETNX", .not_subscriber).call; pub const script = compile.@"(...strings: string[])"("script", "SCRIPT", .not_subscriber).call; pub const select = compile.@"(...strings: string[])"("select", "SELECT", .not_subscriber).call; -pub const spublish = compile.@"(...strings: string[])"("spublish", "SPUBLISH", .not_subscriber).call; -pub const smove = compile.@"(...strings: string[])"("smove", "SMOVE", .not_subscriber).call; -pub const substr = compile.@"(...strings: string[])"("substr", "SUBSTR", .not_subscriber).call; -pub const hstrlen = compile.@"(...strings: string[])"("hstrlen", "HSTRLEN", .not_subscriber).call; -pub const zrank = compile.@"(...strings: string[])"("zrank", "ZRANK", .not_subscriber).call; -pub const zrevrank = compile.@"(...strings: string[])"("zrevrank", "ZREVRANK", .not_subscriber).call; +pub const spublish = compile.@"(key: RedisKey, value: RedisValue)"("spublish", "SPUBLISH", "channel", "message", .not_subscriber).call; +pub fn smove(this: *JSValkeyClient, globalObject: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!JSValue { + try requireNotSubscriber(this, @src().fn_name); + + const source = (try fromJS(globalObject, callframe.argument(0))) orelse { + return globalObject.throwInvalidArgumentType("smove", "source", "string or buffer"); + }; + defer source.deinit(); + const destination = (try fromJS(globalObject, callframe.argument(1))) orelse { + return globalObject.throwInvalidArgumentType("smove", "destination", "string or buffer"); + }; + defer destination.deinit(); + const member = (try fromJS(globalObject, callframe.argument(2))) orelse { + return globalObject.throwInvalidArgumentType("smove", "member", "string or buffer"); + }; + defer member.deinit(); + + const promise = this.send( + globalObject, + callframe.this(), + &.{ + .command = "SMOVE", + .args = .{ .args = &.{ source, destination, member } }, + .meta = .{ .return_as_bool = true }, + }, + ) catch |err| { + return protocol.valkeyErrorToJS(globalObject, "Failed to send SMOVE command", err); + }; + return promise.toJS(); +} +pub const substr = compile.@"(key: RedisKey, value: RedisValue, value2: RedisValue)"("substr", "SUBSTR", "key", "start", "end", .not_subscriber).call; +pub const hstrlen = compile.@"(key: RedisKey, value: RedisValue)"("hstrlen", "HSTRLEN", "key", "field", .not_subscriber).call; +pub const zrank = compile.@"(key: RedisKey, ...args: RedisKey[])"("zrank", "ZRANK", "key", .not_subscriber).call; +pub const zrangestore = compile.@"(...strings: string[])"("zrangestore", "ZRANGESTORE", .not_subscriber).call; +pub const zrem = compile.@"(key: RedisKey, ...args: RedisKey[])"("zrem", "ZREM", "key", .not_subscriber).call; +pub const zremrangebylex = compile.@"(key: RedisKey, value: RedisValue, value2: RedisValue)"("zremrangebylex", "ZREMRANGEBYLEX", "key", "min", "max", .not_subscriber).call; +pub const zremrangebyrank = compile.@"(key: RedisKey, value: RedisValue, value2: RedisValue)"("zremrangebyrank", "ZREMRANGEBYRANK", "key", "start", "stop", .not_subscriber).call; +pub const zremrangebyscore = compile.@"(key: RedisKey, value: RedisValue, value2: RedisValue)"("zremrangebyscore", "ZREMRANGEBYSCORE", "key", "min", "max", .not_subscriber).call; +pub const zrevrank = compile.@"(key: RedisKey, ...args: RedisKey[])"("zrevrank", "ZREVRANK", "key", .not_subscriber).call; pub const psubscribe = compile.@"(...strings: string[])"("psubscribe", "PSUBSCRIBE", .dont_care).call; pub const punsubscribe = compile.@"(...strings: string[])"("punsubscribe", "PUNSUBSCRIBE", .dont_care).call; pub const pubsub = compile.@"(...strings: string[])"("pubsub", "PUBSUB", .dont_care).call; +pub const copy = compile.@"(...strings: string[])"("copy", "COPY", .not_subscriber).call; +pub const unlink = compile.@"(key: RedisKey, ...args: RedisKey[])"("unlink", "UNLINK", "key", .not_subscriber).call; +pub const touch = compile.@"(key: RedisKey, ...args: RedisKey[])"("touch", "TOUCH", "key", .not_subscriber).call; +pub const rename = compile.@"(key: RedisKey, value: RedisValue)"("rename", "RENAME", "key", "newkey", .not_subscriber).call; +pub const renamenx = compile.@"(key: RedisKey, value: RedisValue)"("renamenx", "RENAMENX", "key", "newkey", .not_subscriber).call; pub fn publish( this: *JSValkeyClient, @@ -1004,6 +1309,30 @@ const compile = struct { }; } + pub fn @"()"( + comptime name: []const u8, + comptime command: []const u8, + comptime client_state_requirement: ClientStateRequirement, + ) type { + return struct { + pub fn call(this: *JSValkeyClient, globalObject: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!JSValue { + try testCorrectState(this, name, client_state_requirement); + + const promise = this.send( + globalObject, + callframe.this(), + &.{ + .command = command, + .args = .{ .args = &.{} }, + }, + ) catch |err| { + return protocol.valkeyErrorToJS(globalObject, "Failed to send " ++ command, err); + }; + return promise.toJS(); + } + }; + } + pub fn @"(key: RedisKey)"( comptime name: []const u8, comptime command: []const u8, @@ -1117,6 +1446,46 @@ const compile = struct { }; } + pub fn @"(key: RedisKey, value: RedisValue, value2: RedisValue)"( + comptime name: []const u8, + comptime command: []const u8, + comptime arg0_name: []const u8, + comptime arg1_name: []const u8, + comptime arg2_name: []const u8, + comptime client_state_requirement: ClientStateRequirement, + ) type { + return struct { + pub fn call(this: *JSValkeyClient, globalObject: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JSError!JSValue { + try testCorrectState(this, name, client_state_requirement); + + const key = (try fromJS(globalObject, callframe.argument(0))) orelse { + return globalObject.throwInvalidArgumentType(name, arg0_name, "string or buffer"); + }; + defer key.deinit(); + const value = (try fromJS(globalObject, callframe.argument(1))) orelse { + return globalObject.throwInvalidArgumentType(name, arg1_name, "string or buffer"); + }; + defer value.deinit(); + const value2 = (try fromJS(globalObject, callframe.argument(2))) orelse { + return globalObject.throwInvalidArgumentType(name, arg2_name, "string or buffer"); + }; + defer value2.deinit(); + + const promise = this.send( + globalObject, + callframe.this(), + &.{ + .command = command, + .args = .{ .args = &.{ key, value, value2 } }, + }, + ) catch |err| { + return protocol.valkeyErrorToJS(globalObject, "Failed to send " ++ command, err); + }; + return promise.toJS(); + } + }; + } + pub fn @"(...strings: string[])"( comptime name: []const u8, comptime command: []const u8, diff --git a/test/js/valkey/docker-unified/Dockerfile b/test/js/valkey/docker-unified/Dockerfile index 7b03eba6fa..8518dc934d 100644 --- a/test/js/valkey/docker-unified/Dockerfile +++ b/test/js/valkey/docker-unified/Dockerfile @@ -1,5 +1,5 @@ # Unified Dockerfile for Valkey/Redis with TCP, TLS, Unix socket, and authentication support -FROM redis:7-alpine +FROM redis:8-alpine # Set user to root USER root @@ -10,6 +10,7 @@ RUN apk add --no-cache bash # Create directories RUN mkdir -p /etc/redis/certs RUN mkdir -p /docker-entrypoint-initdb.d +RUN mkdir -p /data/appendonlydir && chown -R redis:redis /data # Copy certificates COPY server.key /etc/redis/certs/ @@ -25,7 +26,9 @@ RUN chmod +x /docker-entrypoint-initdb.d/init-redis.sh # Expose ports EXPOSE 6379 6380 -WORKDIR /docker-entrypoint-initdb.d +WORKDIR /data + +USER redis # Use custom entrypoint to run initialization script CMD ["redis-server", "/etc/redis/redis.conf"] \ No newline at end of file diff --git a/test/js/valkey/unit/set-operations.test.ts b/test/js/valkey/unit/set-operations.test.ts deleted file mode 100644 index 6fabab94fc..0000000000 --- a/test/js/valkey/unit/set-operations.test.ts +++ /dev/null @@ -1,349 +0,0 @@ -import { beforeEach, describe, expect, test } from "bun:test"; -import { ConnectionType, createClient, ctx, expectType, isEnabled } from "../test-utils"; - -/** - * Test suite covering Redis set operations - * - Basic operations (SADD, SREM, SISMEMBER) - * - Set retrieval (SMEMBERS, SCARD) - * - Set manipulation (SPOP, SRANDMEMBER) - * - Set operations (SUNION, SINTER, SDIFF) - */ -describe.skipIf(!isEnabled)("Valkey: Set Data Type Operations", () => { - beforeEach(() => { - if (ctx.redis?.connected) { - ctx.redis.close?.(); - } - ctx.redis = createClient(ConnectionType.TCP); - }); - - describe("Basic Set Operations", () => { - test("SADD and SISMEMBER commands", async () => { - const key = ctx.generateKey("set-test"); - - // Add a single member - const singleAddResult = await ctx.redis.sadd(key, "member1"); - console.log("singleAddResult", singleAddResult); - expectType(singleAddResult, "number"); - expect(singleAddResult).toBe(1); // 1 new member added - - // Add multiple members using sendCommand - const multiAddResult = await ctx.redis.send("SADD", [key, "member2", "member3", "member1"]); - expectType(multiAddResult, "number"); - expect(multiAddResult).toBe(2); // 2 new members added, 1 duplicate ignored - - // Check if member exists - const isFirstMember = await ctx.redis.sismember(key, "member1"); - expect(isFirstMember).toBe(true); - - // Check if non-existent member exists - const isNonMember = await ctx.redis.sismember(key, "nonexistent"); - expect(isNonMember).toBe(false); - }); - - test("SREM command", async () => { - const key = ctx.generateKey("srem-test"); - - // Add multiple members - await ctx.redis.send("SADD", [key, "member1", "member2", "member3", "member4"]); - - // Remove a single member - const singleRemResult = await ctx.redis.srem(key, "member1"); - expectType(singleRemResult, "number"); - expect(singleRemResult).toBe(1); // 1 member removed - - // Remove multiple members using sendCommand - const multiRemResult = await ctx.redis.send("SREM", [key, "member2", "member3", "nonexistent"]); - expectType(multiRemResult, "number"); - expect(multiRemResult).toBe(2); // 2 members removed, non-existent member ignored - - // Verify only member4 remains - const members = await ctx.redis.smembers(key); - expect(Array.isArray(members)).toBe(true); - expect(members.length).toBe(1); - expect(members[0]).toBe("member4"); - }); - - test("SMEMBERS command", async () => { - const key = ctx.generateKey("smembers-test"); - - // Add members one at a time using direct sadd method - await ctx.redis.sadd(key, "apple"); - await ctx.redis.sadd(key, "banana"); - await ctx.redis.sadd(key, "cherry"); - - // Get all members using direct smembers method - const members = await ctx.redis.smembers(key); - expect(Array.isArray(members)).toBe(true); - - // Sort for consistent snapshot since set members can come in any order - const sortedMembers = [...members].sort(); - expect(sortedMembers).toMatchInlineSnapshot(` - [ - "apple", - "banana", - "cherry", - ] - `); - }); - - test("SCARD command", async () => { - const key = ctx.generateKey("scard-test"); - - // Add members - using direct sadd method for first item, then send for multiple - await ctx.redis.sadd(key, "item1"); - await ctx.redis.send("SADD", [key, "item2", "item3", "item4"]); - - // Get set cardinality (size) - // TODO: When a direct scard method is implemented, use that instead - const size = await ctx.redis.send("SCARD", [key]); - expectType(size, "number"); - expect(size).toMatchInlineSnapshot(`4`); - - // Remove some members - using direct srem method for first item, then send for second - await ctx.redis.srem(key, "item1"); - await ctx.redis.send("SREM", [key, "item2"]); - - // Check size again - const updatedSize = await ctx.redis.send("SCARD", [key]); - expectType(updatedSize, "number"); - expect(updatedSize).toMatchInlineSnapshot(`2`); - }); - }); - - describe("Set Manipulation", () => { - test("SPOP command", async () => { - const key = ctx.generateKey("spop-test"); - - // Add members - using send for multiple values - // TODO: When a SADD method that supports multiple values is added, use that instead - await ctx.redis.send("SADD", [key, "red", "green", "blue", "yellow", "purple"]); - - // Pop a single member - using direct spop method - const popResult = await ctx.redis.spop(key); - expect(popResult).toBeDefined(); - expect(typeof popResult).toBe("string"); - - // Pop multiple members - // TODO: When SPOP method that supports count parameter is added, use that instead - const multiPopResult = await ctx.redis.send("SPOP", [key, "2"]); - expect(Array.isArray(multiPopResult)).toBe(true); - expect(multiPopResult.length).toMatchInlineSnapshot(`2`); - - // Verify remaining members - // TODO: When a direct scard method is added, use that instead - const remainingCount = await ctx.redis.send("SCARD", [key]); - expectType(remainingCount, "number"); - expect(remainingCount).toMatchInlineSnapshot(`2`); // 5 original - 1 - 2 = 2 remaining - }); - - test("SRANDMEMBER command", async () => { - const key = ctx.generateKey("srandmember-test"); - - // Add members - first with direct sadd, then with send for remaining - await ctx.redis.sadd(key, "one"); - await ctx.redis.send("SADD", [key, "two", "three", "four", "five"]); - - // Get a random member - using direct srandmember method - const randResult = await ctx.redis.srandmember(key); - expect(randResult).toBeDefined(); - expect(typeof randResult).toBe("string"); - - // Get multiple random members - // TODO: When srandmember method with count parameter is added, use that instead - const multiRandResult = await ctx.redis.send("SRANDMEMBER", [key, "3"]); - expect(Array.isArray(multiRandResult)).toBe(true); - expect(multiRandResult.length).toMatchInlineSnapshot(`3`); - - // Verify set is unchanged - const count = await ctx.redis.send("SCARD", [key]); - expectType(count, "number"); - expect(count).toMatchInlineSnapshot(`5`); // All members still present unlike SPOP - }); - - test("SMOVE command", async () => { - const sourceKey = ctx.generateKey("smove-source"); - const destinationKey = ctx.generateKey("smove-dest"); - - // Set up source and destination sets - await ctx.redis.send("SADD", [sourceKey, "a", "b", "c"]); - await ctx.redis.send("SADD", [destinationKey, "c", "d", "e"]); - - // Move a member from source to destination - const moveResult = await ctx.redis.send("SMOVE", [sourceKey, destinationKey, "b"]); - expectType(moveResult, "number"); - expect(moveResult).toBe(1); // 1 indicates success - - // Try to move a non-existent member - const failedMoveResult = await ctx.redis.send("SMOVE", [sourceKey, destinationKey, "z"]); - expectType(failedMoveResult, "number"); - expect(failedMoveResult).toBe(0); // 0 indicates failure - - // Verify source set (should have "a" and "c" left) - const sourceMembers = await ctx.redis.smembers(sourceKey); - expect(Array.isArray(sourceMembers)).toBe(true); - expect(sourceMembers.length).toBe(2); - expect(sourceMembers).toContain("a"); - expect(sourceMembers).toContain("c"); - expect(sourceMembers).not.toContain("b"); - - // Verify destination set (should have "b", "c", "d", "e") - const destMembers = await ctx.redis.smembers(destinationKey); - expect(Array.isArray(destMembers)).toBe(true); - expect(destMembers.length).toBe(4); - expect(destMembers).toContain("b"); - expect(destMembers).toContain("c"); - expect(destMembers).toContain("d"); - expect(destMembers).toContain("e"); - }); - }); - - describe("Set Operations", () => { - test("SUNION and SUNIONSTORE commands", async () => { - const set1 = ctx.generateKey("sunion-1"); - const set2 = ctx.generateKey("sunion-2"); - const set3 = ctx.generateKey("sunion-3"); - const destSet = ctx.generateKey("sunion-dest"); - - // Set up test sets - await ctx.redis.send("SADD", [set1, "a", "b", "c"]); - await ctx.redis.send("SADD", [set2, "c", "d", "e"]); - await ctx.redis.send("SADD", [set3, "e", "f", "g"]); - - // Get union of two sets - const unionResult = await ctx.redis.send("SUNION", [set1, set2]); - expect(Array.isArray(unionResult)).toBe(true); - expect(unionResult.length).toBe(5); - expect(unionResult).toContain("a"); - expect(unionResult).toContain("b"); - expect(unionResult).toContain("c"); - expect(unionResult).toContain("d"); - expect(unionResult).toContain("e"); - - // Store union of three sets - const storeResult = await ctx.redis.send("SUNIONSTORE", [destSet, set1, set2, set3]); - expectType(storeResult, "number"); - expect(storeResult).toBe(7); // 7 unique members across all sets - - // Verify destination set - const destMembers = await ctx.redis.smembers(destSet); - expect(Array.isArray(destMembers)).toBe(true); - expect(destMembers.length).toBe(7); - expect(destMembers).toContain("a"); - expect(destMembers).toContain("b"); - expect(destMembers).toContain("c"); - expect(destMembers).toContain("d"); - expect(destMembers).toContain("e"); - expect(destMembers).toContain("f"); - expect(destMembers).toContain("g"); - }); - - test("SINTER and SINTERSTORE commands", async () => { - const set1 = ctx.generateKey("sinter-1"); - const set2 = ctx.generateKey("sinter-2"); - const set3 = ctx.generateKey("sinter-3"); - const destSet = ctx.generateKey("sinter-dest"); - - // Set up test sets - await ctx.redis.send("SADD", [set1, "a", "b", "c", "d"]); - await ctx.redis.send("SADD", [set2, "c", "d", "e"]); - await ctx.redis.send("SADD", [set3, "a", "c", "e"]); - - // Get intersection of two sets - const interResult = await ctx.redis.send("SINTER", [set1, set2]); - expect(Array.isArray(interResult)).toBe(true); - expect(interResult.length).toBe(2); - expect(interResult).toContain("c"); - expect(interResult).toContain("d"); - - // Store intersection of three sets - const storeResult = await ctx.redis.send("SINTERSTORE", [destSet, set1, set2, set3]); - expectType(storeResult, "number"); - expect(storeResult).toBe(1); // Only "c" is in all three sets - - // Verify destination set - const destMembers = await ctx.redis.smembers(destSet); - expect(Array.isArray(destMembers)).toBe(true); - expect(destMembers.length).toBe(1); - expect(destMembers[0]).toBe("c"); - }); - - test("SDIFF and SDIFFSTORE commands", async () => { - const set1 = ctx.generateKey("sdiff-1"); - const set2 = ctx.generateKey("sdiff-2"); - const destSet = ctx.generateKey("sdiff-dest"); - - // Set up test sets - await ctx.redis.send("SADD", [set1, "a", "b", "c", "d"]); - await ctx.redis.send("SADD", [set2, "c", "d", "e"]); - - // Get difference (elements in set1 that aren't in set2) - const diffResult = await ctx.redis.send("SDIFF", [set1, set2]); - expect(Array.isArray(diffResult)).toBe(true); - expect(diffResult.length).toBe(2); - expect(diffResult).toContain("a"); - expect(diffResult).toContain("b"); - - // Store difference - const storeResult = await ctx.redis.send("SDIFFSTORE", [destSet, set1, set2]); - expectType(storeResult, "number"); - expect(storeResult).toBe(2); // "a" and "b" are only in set1 - - // Verify destination set - const destMembers = await ctx.redis.smembers(destSet); - expect(Array.isArray(destMembers)).toBe(true); - expect(destMembers.length).toBe(2); - expect(destMembers).toContain("a"); - expect(destMembers).toContain("b"); - }); - }); - - describe("Scanning Operations", () => { - test("SSCAN command", async () => { - const key = ctx.generateKey("sscan-test"); - - // Create a set with many members - const memberCount = 100; - const members = []; - for (let i = 0; i < memberCount; i++) { - members.push(`member:${i}`); - } - - await ctx.redis.send("SADD", [key, ...members]); - - // Use SSCAN to iterate through members - const scanResult = await ctx.redis.send("SSCAN", [key, "0", "COUNT", "20"]); - expect(Array.isArray(scanResult)).toBe(true); - expect(scanResult.length).toBe(2); - - const cursor = scanResult[0]; - const items = scanResult[1]; - - // Cursor should be either "0" (done) or a string number - expect(typeof cursor).toBe("string"); - - // Items should be an array of members - expect(Array.isArray(items)).toBe(true); - - // All results should match our expected pattern - for (const item of items) { - expect(item.startsWith("member:")).toBe(true); - } - - // Verify MATCH pattern works - const patternResult = await ctx.redis.send("SSCAN", [key, "0", "MATCH", "member:1*", "COUNT", "1000"]); - expect(Array.isArray(patternResult)).toBe(true); - expect(patternResult.length).toBe(2); - - const patternItems = patternResult[1]; - expect(Array.isArray(patternItems)).toBe(true); - - // Should return only members that match the pattern (member:1, member:10-19, etc) - // There should be at least "member:1" and "member:10" through "member:19" - expect(patternItems.length).toBeGreaterThan(0); - - for (const item of patternItems) { - expect(item.startsWith("member:1")).toBe(true); - } - }); - }); -}); diff --git a/test/js/valkey/valkey.test.ts b/test/js/valkey/valkey.test.ts index 1cd2de2fd0..d69ac7d6d6 100644 --- a/test/js/valkey/valkey.test.ts +++ b/test/js/valkey/valkey.test.ts @@ -1,5 +1,6 @@ import { randomUUIDv7, RedisClient, spawn } from "bun"; import { beforeAll, beforeEach, describe, expect, test } from "bun:test"; +import { bunExe } from "harness"; import { ctx as _ctx, awaitableCounter, @@ -16,10 +17,9 @@ import { import type { RedisTestStartMessage } from "./valkey.failing-subscriber"; for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { - const ctx = { ..._ctx, redis: connectionType ? _ctx.redis : _ctx.redisTLS }; + const ctx = { ..._ctx, redis: connectionType ? _ctx.redis : (_ctx.redisTLS as RedisClient) }; describe.skipIf(!isEnabled)(`Valkey Redis Client (${connectionType})`, () => { beforeAll(async () => { - // Ensure container is ready before tests run await setupDockerContainer(); if (!ctx.redis) { ctx.redis = createClient(connectionType); @@ -27,12 +27,10 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { }); beforeEach(async () => { - // Don't create a new client, just ensure we have one if (!ctx.redis) { ctx.redis = createClient(connectionType); } - // Flush all data for clean test state await ctx.redis.connect(); await ctx.redis.send("FLUSHALL", ["SYNC"]); }); @@ -43,105 +41,6123 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const testKey = "greeting"; const testValue = "Hello from Bun Redis!"; - // Using direct set and get methods const setResult = await redis.set(testKey, testValue); expect(setResult).toMatchInlineSnapshot(`"OK"`); const setResult2 = await redis.set(testKey, testValue, "GET"); expect(setResult2).toMatchInlineSnapshot(`"${testValue}"`); - // GET should return the value we set const getValue = await redis.get(testKey); expect(getValue).toMatchInlineSnapshot(`"${testValue}"`); }); test("should test key existence", async () => { const redis = ctx.redis; - // Let's set a key first + await redis.set("greeting", "test existence"); - // EXISTS in Redis normally returns integer 1 if key exists, 0 if not - // The current implementation doesn't transform exists correctly yet const exists = await redis.exists("greeting"); expect(exists).toBeDefined(); - // Should be true for existing keys (fixed in special handling for EXISTS) + expect(exists).toBe(true); - // For non-existent keys const randomKey = "nonexistent-key-" + randomUUIDv7(); const notExists = await redis.exists(randomKey); expect(notExists).toBeDefined(); - // Should be false for non-existing keys + expect(notExists).toBe(false); }); test("should increment and decrement counters", async () => { const redis = ctx.redis; const counterKey = "counter"; - // First set a counter value + await redis.set(counterKey, "10"); - // INCR should increment and return the new value const incrementedValue = await redis.incr(counterKey); expect(incrementedValue).toBeDefined(); expect(typeof incrementedValue).toBe("number"); expect(incrementedValue).toBe(11); - // DECR should decrement and return the new value const decrementedValue = await redis.decr(counterKey); expect(decrementedValue).toBeDefined(); expect(typeof decrementedValue).toBe("number"); expect(decrementedValue).toBe(10); }); + test("should increment by specified amount with INCRBY", async () => { + const redis = ctx.redis; + const counterKey = "incrby-counter"; + await redis.set(counterKey, "5"); + + const result1 = await redis.incrby(counterKey, 10); + expect(result1).toBe(15); + + const result2 = await redis.incrby(counterKey, -3); + expect(result2).toBe(12); + + const result3 = await redis.incrby("new-incrby-key", 5); + expect(result3).toBe(5); + }); + + test("should increment by float amount with INCRBYFLOAT", async () => { + const redis = ctx.redis; + const floatKey = "float-counter"; + await redis.set(floatKey, "10.5"); + + const result1 = await redis.incrbyfloat(floatKey, 2.3); + expect(result1).toBe("12.8"); + + const result2 = await redis.incrbyfloat(floatKey, -0.8); + expect(result2).toBe("12"); + + const result3 = await redis.incrbyfloat("new-float-key", 3.14); + expect(result3).toBe("3.14"); + }); + + test("should decrement by specified amount with DECRBY", async () => { + const redis = ctx.redis; + const counterKey = "decrby-counter"; + await redis.set(counterKey, "20"); + + const result1 = await redis.decrby(counterKey, 5); + expect(result1).toBe(15); + + const result2 = await redis.decrby(counterKey, 10); + expect(result2).toBe(5); + + const result3 = await redis.decrby("new-decrby-key", 3); + expect(result3).toBe(-3); + }); + + test("should rename a key with RENAME", async () => { + const redis = ctx.redis; + const oldKey = "old-key"; + const newKey = "new-key"; + const value = "test-value"; + + await redis.set(oldKey, value); + + const result = await redis.rename(oldKey, newKey); + expect(result).toBe("OK"); + + const newValue = await redis.get(newKey); + expect(newValue).toBe(value); + + const oldValue = await redis.get(oldKey); + expect(oldValue).toBeNull(); + }); + + test("should rename a key with RENAME overwriting existing key", async () => { + const redis = ctx.redis; + const oldKey = "old-key-overwrite"; + const newKey = "new-key-overwrite"; + + await redis.set(oldKey, "old-value"); + await redis.set(newKey, "existing-value"); + + const result = await redis.rename(oldKey, newKey); + expect(result).toBe("OK"); + + const newValue = await redis.get(newKey); + expect(newValue).toBe("old-value"); + + const oldValue = await redis.get(oldKey); + expect(oldValue).toBeNull(); + }); + + test("should rename a key only if new key does not exist with RENAMENX", async () => { + const redis = ctx.redis; + const oldKey = "old-key-nx"; + const newKey = "new-key-nx"; + const value = "test-value"; + + await redis.set(oldKey, value); + + const result1 = await redis.renamenx(oldKey, newKey); + expect(result1).toBe(1); + + const newValue = await redis.get(newKey); + expect(newValue).toBe(value); + + const oldValue = await redis.get(oldKey); + expect(oldValue).toBeNull(); + }); + + test("should not rename if new key exists with RENAMENX", async () => { + const redis = ctx.redis; + const oldKey = "old-key-nx-fail"; + const newKey = "new-key-nx-fail"; + + await redis.set(oldKey, "old-value"); + await redis.set(newKey, "existing-value"); + + const result = await redis.renamenx(oldKey, newKey); + expect(result).toBe(0); + + const oldValue = await redis.get(oldKey); + expect(oldValue).toBe("old-value"); + + const newValue = await redis.get(newKey); + expect(newValue).toBe("existing-value"); + }); + + test("should set multiple keys with MSET", async () => { + const redis = ctx.redis; + + const result = await redis.mset("mset-key1", "value1", "mset-key2", "value2", "mset-key3", "value3"); + expect(result).toBe("OK"); + + const value1 = await redis.get("mset-key1"); + expect(value1).toBe("value1"); + const value2 = await redis.get("mset-key2"); + expect(value2).toBe("value2"); + const value3 = await redis.get("mset-key3"); + expect(value3).toBe("value3"); + }); + + test("should set multiple keys only if none exist with MSETNX", async () => { + const redis = ctx.redis; + + const result1 = await redis.msetnx("msetnx-key1", "value1", "msetnx-key2", "value2"); + expect(result1).toBe(1); + + const value1 = await redis.get("msetnx-key1"); + expect(value1).toBe("value1"); + const value2 = await redis.get("msetnx-key2"); + expect(value2).toBe("value2"); + + const result2 = await redis.msetnx("msetnx-key1", "newvalue", "msetnx-key3", "value3"); + expect(result2).toBe(0); + + const unchangedValue = await redis.get("msetnx-key1"); + expect(unchangedValue).toBe("value1"); + + const nonExistentKey = await redis.get("msetnx-key3"); + expect(nonExistentKey).toBeNull(); + }); + test("should manage key expiration", async () => { const redis = ctx.redis; - // Set a key first + const tempKey = "temporary"; await redis.set(tempKey, "will expire"); - // EXPIRE should return 1 if the timeout was set, 0 otherwise const result = await redis.expire(tempKey, 60); - // Using native expire command instead of send() + expect(result).toMatchInlineSnapshot(`1`); - // Use the TTL command directly const ttl = await redis.ttl(tempKey); expectType(ttl, "number"); expect(ttl).toBeGreaterThan(0); - expect(ttl).toBeLessThanOrEqual(60); // Should be positive and not exceed our set time + expect(ttl).toBeLessThanOrEqual(60); + }); + + test("should set key with expiration using SETEX", async () => { + const redis = ctx.redis; + const key = "setex-test-key"; + const value = "test-value"; + + const result = await redis.setex(key, 10, value); + expect(result).toBe("OK"); + + const getValue = await redis.get(key); + expect(getValue).toBe(value); + + const ttl = await redis.ttl(key); + expect(ttl).toBeGreaterThan(0); + expect(ttl).toBeLessThanOrEqual(10); + }); + + test("should set key with expiration using PSETEX", async () => { + const redis = ctx.redis; + const key = "psetex-test-key"; + const value = "test-value"; + + const result = await redis.psetex(key, 5000, value); + expect(result).toBe("OK"); + + const getValue = await redis.get(key); + expect(getValue).toBe(value); + + const pttl = await redis.pttl(key); + expect(pttl).toBeGreaterThan(0); + expect(pttl).toBeLessThanOrEqual(5000); + }); + + test("should set expiration with EXPIREAT using Unix timestamp", async () => { + const redis = ctx.redis; + const key = "expireat-test-key"; + await redis.set(key, "test-value"); + + const futureTimestamp = Math.floor(Date.now() / 1000) + 60; + const result = await redis.expireat(key, futureTimestamp); + expect(result).toBe(1); + + const ttl = await redis.ttl(key); + expect(ttl).toBeGreaterThan(0); + expect(ttl).toBeLessThanOrEqual(60); + }); + + test("should return 0 for EXPIREAT on non-existent key", async () => { + const redis = ctx.redis; + const futureTimestamp = Math.floor(Date.now() / 1000) + 60; + const result = await redis.expireat("nonexistent-expireat-key", futureTimestamp); + expect(result).toBe(0); + }); + + test("should set expiration with PEXPIRE in milliseconds", async () => { + const redis = ctx.redis; + const key = "pexpire-test-key"; + await redis.set(key, "test-value"); + + const result = await redis.pexpire(key, 5000); + expect(result).toBe(1); + + const pttl = await redis.pttl(key); + expect(pttl).toBeGreaterThan(0); + expect(pttl).toBeLessThanOrEqual(5050); + }); + + test("should return 0 for PEXPIRE on non-existent key", async () => { + const redis = ctx.redis; + const result = await redis.pexpire("nonexistent-pexpire-key", 5000); + expect(result).toBe(0); + }); + + test("should set expiration with PEXPIREAT using Unix timestamp in milliseconds", async () => { + const redis = ctx.redis; + const key = "pexpireat-test-key"; + await redis.set(key, "test-value"); + + const futureTimestampMs = Date.now() + 5000; + const result = await redis.pexpireat(key, futureTimestampMs); + expect(result).toBe(1); + + const pttl = await redis.pttl(key); + expect(pttl).toBeGreaterThan(0); + expect(pttl).toBeLessThanOrEqual(5050); + }); + + test("should return 0 for PEXPIREAT on non-existent key", async () => { + const redis = ctx.redis; + const futureTimestampMs = Date.now() + 5000; + const result = await redis.pexpireat("nonexistent-pexpireat-key", futureTimestampMs); + expect(result).toBe(0); + }); + + test("should determine the type of a key with TYPE", async () => { + const redis = ctx.redis; + + await redis.set("string-key", "value"); + const stringType = await redis.type("string-key"); + expect(stringType).toBe("string"); + + await redis.lpush("list-key", "value"); + const listType = await redis.type("list-key"); + expect(listType).toBe("list"); + + await redis.sadd("set-key", "value"); + const setType = await redis.type("set-key"); + expect(setType).toBe("set"); + + await redis.send("HSET", ["hash-key", "field", "value"]); + const hashType = await redis.type("hash-key"); + expect(hashType).toBe("hash"); + + const noneType = await redis.type("nonexistent-key"); + expect(noneType).toBe("none"); + }); + + test("should update last access time with TOUCH", async () => { + const redis = ctx.redis; + + await redis.set("touch-key1", "value1"); + await redis.set("touch-key2", "value2"); + + const touchedCount = await redis.touch("touch-key1", "touch-key2"); + expect(touchedCount).toBe(2); + + const mixedCount = await redis.touch("touch-key1", "nonexistent-key"); + expect(mixedCount).toBe(1); + + const noneCount = await redis.touch("nonexistent-key1", "nonexistent-key2"); + expect(noneCount).toBe(0); + }); + + test("should get and set bits", async () => { + const redis = ctx.redis; + const bitKey = "mybitkey"; + + const oldValue = await redis.setbit(bitKey, 7, 1); + expect(oldValue).toBe(0); + + const bitValue = await redis.getbit(bitKey, 7); + expect(bitValue).toBe(1); + + const unsetBit = await redis.getbit(bitKey, 100); + expect(unsetBit).toBe(0); + + const oldValue2 = await redis.setbit(bitKey, 7, 0); + expect(oldValue2).toBe(1); + + const bitValue2 = await redis.getbit(bitKey, 7); + expect(bitValue2).toBe(0); + }); + + test("should handle multiple bit operations", async () => { + const redis = ctx.redis; + const bitKey = "multibit"; + + await redis.setbit(bitKey, 0, 1); + await redis.setbit(bitKey, 3, 1); + await redis.setbit(bitKey, 7, 1); + + expect(await redis.getbit(bitKey, 0)).toBe(1); + expect(await redis.getbit(bitKey, 1)).toBe(0); + expect(await redis.getbit(bitKey, 2)).toBe(0); + expect(await redis.getbit(bitKey, 3)).toBe(1); + expect(await redis.getbit(bitKey, 4)).toBe(0); + expect(await redis.getbit(bitKey, 5)).toBe(0); + expect(await redis.getbit(bitKey, 6)).toBe(0); + expect(await redis.getbit(bitKey, 7)).toBe(1); + + const count = await redis.bitcount(bitKey); + expect(count).toBe(3); + }); + + test("should get range of string", async () => { + const redis = ctx.redis; + const key = "rangetest"; + await redis.set(key, "Hello World"); + + const result1 = await redis.getrange(key, 0, 4); + expect(result1).toBe("Hello"); + + const result2 = await redis.getrange(key, 6, 10); + expect(result2).toBe("World"); + + const result3 = await redis.getrange(key, -5, -1); + expect(result3).toBe("World"); + + const result4 = await redis.getrange(key, 0, -1); + expect(result4).toBe("Hello World"); + }); + + test("should set range of string", async () => { + const redis = ctx.redis; + const key = "setrangetest"; + await redis.set(key, "Hello World"); + + const newLength = await redis.setrange(key, 6, "Redis"); + expect(newLength).toBe(11); + + const result = await redis.get(key); + expect(result).toBe("Hello Redis"); + + const key2 = "newkey"; + const newLength2 = await redis.setrange(key2, 5, "Redis"); + expect(newLength2).toBeGreaterThanOrEqual(10); + }); + + test("should append to string with APPEND", async () => { + const redis = ctx.redis; + const key = "append-test"; + + const len1 = await redis.append(key, "Hello"); + expect(len1).toBe(5); + + const len2 = await redis.append(key, " World"); + expect(len2).toBe(11); + + const value = await redis.get(key); + expect(value).toBe("Hello World"); + }); + + test("should delete keys with DEL", async () => { + const redis = ctx.redis; + + await redis.set("del-key1", "value1"); + await redis.set("del-key2", "value2"); + await redis.set("del-key3", "value3"); + + const count1 = await redis.del("del-key1"); + expect(count1).toBe(1); + + const value1 = await redis.get("del-key1"); + expect(value1).toBeNull(); + + const count2 = await redis.del("del-key2", "del-key3"); + expect(count2).toBe(2); + + const count3 = await redis.del("nonexistent"); + expect(count3).toBe(0); + }); + + test("should serialize key with DUMP", async () => { + const redis = ctx.redis; + const key = "dump-test"; + + await redis.set(key, "test-value"); + + const serialized = await redis.dump(key); + expect(serialized).toBeDefined(); + expect(serialized).not.toBeNull(); + + const empty = await redis.dump("nonexistent"); + expect(empty).toBeNull(); + }); + + test("should get value as Buffer with getBuffer", async () => { + const redis = ctx.redis; + const key = "getbuffer-test"; + + await redis.set(key, "test-value"); + + const buffer = await redis.getBuffer(key); + expect(buffer).toBeInstanceOf(Buffer); + expect(buffer?.toString()).toBe("test-value"); + + const empty = await redis.getBuffer("nonexistent"); + expect(empty).toBeNull(); + }); + + test("should get and delete with GETDEL", async () => { + const redis = ctx.redis; + const key = "getdel-test"; + + await redis.set(key, "test-value"); + + const value = await redis.getdel(key); + expect(value).toBe("test-value"); + + const deleted = await redis.get(key); + expect(deleted).toBeNull(); + + const empty = await redis.getdel("nonexistent"); + expect(empty).toBeNull(); + }); + + test("should get and set expiration with GETEX", async () => { + const redis = ctx.redis; + const key = "getex-test"; + + await redis.set(key, "test-value"); + + const value1 = await redis.getex(key, "EX", 60); + expect(value1).toBe("test-value"); + const ttl1 = await redis.ttl(key); + expect(ttl1).toBeGreaterThan(0); + expect(ttl1).toBeLessThanOrEqual(60); + + const value2 = await redis.getex(key, "PX", 5000); + expect(value2).toBe("test-value"); + const pttl = await redis.pttl(key); + expect(pttl).toBeGreaterThan(0); + expect(pttl).toBeLessThanOrEqual(5000); + + const empty = await redis.getex("nonexistent", "EX", 60); + expect(empty).toBeNull(); + }); + + test("should get old value and set new with GETSET", async () => { + const redis = ctx.redis; + const key = "getset-test"; + + const old1 = await redis.getset(key, "value1"); + expect(old1).toBeNull(); + + const old2 = await redis.getset(key, "value2"); + expect(old2).toBe("value1"); + + const current = await redis.get(key); + expect(current).toBe("value2"); + }); + + test("should get string length with STRLEN", async () => { + const redis = ctx.redis; + const key = "strlen-test"; + + const len1 = await redis.strlen(key); + expect(len1).toBe(0); + + await redis.set(key, "Hello"); + const len2 = await redis.strlen(key); + expect(len2).toBe(5); + + await redis.set(key, "Hello World"); + const len3 = await redis.strlen(key); + expect(len3).toBe(11); + }); + + test("should get substring with SUBSTR", async () => { + const redis = ctx.redis; + const key = "substr-test"; + + await redis.set(key, "Hello World"); + + const result = await redis.substr(key, 0, 4); + expect(result).toBe("Hello"); + }); + + test("should get expiration time with EXPIRETIME", async () => { + const redis = ctx.redis; + const key = "expiretime-test"; + + await redis.set(key, "value"); + + const futureTs = Math.floor(Date.now() / 1000) + 60; + await redis.expireat(key, futureTs); + + const expireTime = await redis.expiretime(key); + expect(expireTime).toBeGreaterThan(0); + expect(expireTime).toBeLessThanOrEqual(futureTs); + + const key2 = "no-expire"; + await redis.set(key2, "value"); + const noExpire = await redis.expiretime(key2); + expect(noExpire).toBe(-1); + + const nonExist = await redis.expiretime("nonexistent"); + expect(nonExist).toBe(-2); + }); + + test("should get expiration time in ms with PEXPIRETIME", async () => { + const redis = ctx.redis; + const key = "pexpiretime-test"; + + await redis.set(key, "value"); + + const futureTs = Date.now() + 5000; + await redis.pexpireat(key, futureTs); + + const pexpireTime = await redis.pexpiretime(key); + expect(pexpireTime).toBeGreaterThan(0); + expect(pexpireTime).toBeLessThanOrEqual(futureTs); + + const key2 = "no-expire"; + await redis.set(key2, "value"); + const noExpire = await redis.pexpiretime(key2); + expect(noExpire).toBe(-1); + + const nonExist = await redis.pexpiretime("nonexistent"); + expect(nonExist).toBe(-2); + }); + + test("should remove expiration with PERSIST", async () => { + const redis = ctx.redis; + const key = "persist-test"; + + await redis.set(key, "value"); + await redis.expire(key, 60); + + const ttlBefore = await redis.ttl(key); + expect(ttlBefore).toBeGreaterThan(0); + + const result = await redis.persist(key); + expect(result).toBe(1); + + const ttlAfter = await redis.ttl(key); + expect(ttlAfter).toBe(-1); + + const result2 = await redis.persist(key); + expect(result2).toBe(0); + + const result3 = await redis.persist("nonexistent"); + expect(result3).toBe(0); + }); + + test("should get multiple values with MGET", async () => { + const redis = ctx.redis; + + await redis.set("mget-key1", "value1"); + await redis.set("mget-key2", "value2"); + await redis.set("mget-key3", "value3"); + + const values = await redis.mget("mget-key1", "mget-key2", "mget-key3"); + expect(values).toEqual(["value1", "value2", "value3"]); + + const mixed = await redis.mget("mget-key1", "nonexistent", "mget-key2"); + expect(mixed).toEqual(["value1", null, "value2"]); + + const allNull = await redis.mget("none1", "none2", "none3"); + expect(allNull).toEqual([null, null, null]); + }); + + test("should set only if not exists with SETNX", async () => { + const redis = ctx.redis; + const key = "setnx-test"; + + const result1 = await redis.setnx(key, "value1"); + expect(result1).toBe(1); + + const value1 = await redis.get(key); + expect(value1).toBe("value1"); + + const result2 = await redis.setnx(key, "value2"); + expect(result2).toBe(0); + + const value2 = await redis.get(key); + expect(value2).toBe("value1"); + }); + + test("should add to HyperLogLog with PFADD", async () => { + const redis = ctx.redis; + const key = "pfadd-test"; + + const result1 = await redis.pfadd(key, "element1"); + expect(result1).toBe(1); + + const result2 = await redis.pfadd(key, "element2"); + expect(result2).toBe(1); + + const result3 = await redis.pfadd(key, "element1"); + expect(result3).toBe(0); }); test("should implement TTL command correctly for different cases", async () => { const redis = ctx.redis; - // 1. Key with expiration + const tempKey = "ttl-test-key"; await redis.set(tempKey, "ttl test value"); await redis.expire(tempKey, 60); - // Use native ttl command const ttl = await redis.ttl(tempKey); expectType(ttl, "number"); expect(ttl).toBeGreaterThan(0); expect(ttl).toBeLessThanOrEqual(60); - // 2. Key with no expiration const permanentKey = "permanent-key"; await redis.set(permanentKey, "no expiry"); const noExpiry = await redis.ttl(permanentKey); - expect(noExpiry).toMatchInlineSnapshot(`-1`); // -1 indicates no expiration + expect(noExpiry).toMatchInlineSnapshot(`-1`); - // 3. Non-existent key const nonExistentKey = "non-existent-" + randomUUIDv7(); const noKey = await redis.ttl(nonExistentKey); - expect(noKey).toMatchInlineSnapshot(`-2`); // -2 indicates key doesn't exist + expect(noKey).toMatchInlineSnapshot(`-2`); + }); + + test("should copy a key to a new key with COPY", async () => { + const redis = ctx.redis; + const sourceKey = "copy-source"; + const destKey = "copy-dest"; + + await redis.set(sourceKey, "Hello World"); + + const result = await redis.copy(sourceKey, destKey); + expect(result).toBe(1); + + const sourceValue = await redis.get(sourceKey); + const destValue = await redis.get(destKey); + expect(sourceValue).toBe("Hello World"); + expect(destValue).toBe("Hello World"); + + const result2 = await redis.copy(sourceKey, destKey); + expect(result2).toBe(0); + }); + + test("should copy a key with REPLACE option", async () => { + const redis = ctx.redis; + const sourceKey = "copy-replace-source"; + const destKey = "copy-replace-dest"; + + await redis.set(sourceKey, "New Value"); + await redis.set(destKey, "Old Value"); + + const result = await redis.copy(sourceKey, destKey, "REPLACE"); + expect(result).toBe(1); + + const destValue = await redis.get(destKey); + expect(destValue).toBe("New Value"); + }); + + test("should unlink one or more keys asynchronously with UNLINK", async () => { + const redis = ctx.redis; + + await redis.set("unlink-key1", "value1"); + await redis.set("unlink-key2", "value2"); + await redis.set("unlink-key3", "value3"); + + const result = await redis.unlink("unlink-key1", "unlink-key2", "unlink-key3"); + expect(result).toBe(3); + + expect(await redis.get("unlink-key1")).toBeNull(); + expect(await redis.get("unlink-key2")).toBeNull(); + expect(await redis.get("unlink-key3")).toBeNull(); + }); + + test("should unlink with non-existent keys", async () => { + const redis = ctx.redis; + + await redis.set("unlink-exists", "value"); + + const result = await redis.unlink("unlink-exists", "unlink-nonexist1", "unlink-nonexist2"); + expect(result).toBe(1); + + expect(await redis.get("unlink-exists")).toBeNull(); + }); + + test("should return a random key with RANDOMKEY", async () => { + const redis = ctx.redis; + + const emptyResult = await redis.randomkey(); + expect(emptyResult).toBeNull(); + + await redis.set("random-key1", "value1"); + await redis.set("random-key2", "value2"); + await redis.set("random-key3", "value3"); + + const randomKey = await redis.randomkey(); + expect(randomKey).toBeDefined(); + expect(randomKey).not.toBeNull(); + expect(["random-key1", "random-key2", "random-key3"]).toContain(randomKey); + + const value = await redis.get(randomKey!); + expect(value).toBeDefined(); + }); + + test("should iterate keys with SCAN", async () => { + const redis = ctx.redis; + + const testKeys = ["scan-test:1", "scan-test:2", "scan-test:3", "scan-test:4", "scan-test:5"]; + for (const key of testKeys) { + await redis.set(key, "value"); + } + + let cursor = "0"; + const foundKeys: string[] = []; + do { + const [nextCursor, keys] = await redis.scan(cursor); + foundKeys.push(...keys); + cursor = nextCursor; + } while (cursor !== "0"); + + for (const testKey of testKeys) { + expect(foundKeys).toContain(testKey); + } + }); + + test("should iterate keys with SCAN and MATCH pattern", async () => { + const redis = ctx.redis; + + await redis.set("user:1", "alice"); + await redis.set("user:2", "bob"); + await redis.set("post:1", "hello"); + await redis.set("post:2", "world"); + + let cursor = "0"; + const userKeys: string[] = []; + do { + const [nextCursor, keys] = await redis.scan(cursor, "MATCH", "user:*"); + userKeys.push(...keys); + cursor = nextCursor; + } while (cursor !== "0"); + + expect(userKeys).toContain("user:1"); + expect(userKeys).toContain("user:2"); + expect(userKeys).not.toContain("post:1"); + expect(userKeys).not.toContain("post:2"); + }); + + test("should reject invalid object argument in SCAN", async () => { + const redis = ctx.redis; + expect(async () => { + await redis.scan({} as any); + }).toThrowErrorMatchingInlineSnapshot(`"Expected additional arguments to be a string or buffer for 'scan'."`); + }); + + test("should reject invalid array argument in SCAN", async () => { + const redis = ctx.redis; + expect(async () => { + await redis.scan([] as any); + }).toThrowErrorMatchingInlineSnapshot(`"Expected additional arguments to be a string or buffer for 'scan'."`); + }); + + test("should reject invalid null argument in SCAN", async () => { + const redis = ctx.redis; + expect(async () => { + await redis.scan(null as any); + }).toThrowErrorMatchingInlineSnapshot(`"Expected additional arguments to be a string or buffer for 'scan'."`); + }); + + test("should reject invalid source key in COPY", async () => { + const redis = ctx.redis; + expect(async () => { + await redis.copy({} as any, "dest"); + }).toThrowErrorMatchingInlineSnapshot(`"Expected additional arguments to be a string or buffer for 'copy'."`); + }); + + test("should reject invalid destination key in COPY", async () => { + const redis = ctx.redis; + expect(async () => { + await redis.copy("source", [] as any); + }).toThrowErrorMatchingInlineSnapshot(`"Expected additional arguments to be a string or buffer for 'copy'."`); + }); + + test("should reject invalid option in COPY", async () => { + const redis = ctx.redis; + await redis.set("copy-invalid-opt-source", "value"); + expect(async () => { + await redis.copy("copy-invalid-opt-source", "copy-invalid-opt-dest", "NOTVALID" as any); + }).toThrowErrorMatchingInlineSnapshot(`"ERR syntax error"`); + }); + + test("should reject invalid old key in RENAME", async () => { + const redis = ctx.redis; + expect(async () => { + await redis.rename({} as any, "newkey"); + }).toThrowErrorMatchingInlineSnapshot(`"Expected key to be a string or buffer for 'rename'."`); + }); + + test("should reject invalid new key in RENAME", async () => { + const redis = ctx.redis; + expect(async () => { + await redis.rename("oldkey", null as any); + }).toThrowErrorMatchingInlineSnapshot(`"Expected newkey to be a string or buffer for 'rename'."`); + }); + + test("should reject invalid key in GETRANGE", async () => { + const redis = ctx.redis; + expect(async () => { + await redis.getrange({} as any, 0, 5); + }).toThrowErrorMatchingInlineSnapshot(`"Expected key to be a string or buffer for 'getrange'."`); + }); + + test("should reject invalid key in SETRANGE", async () => { + const redis = ctx.redis; + expect(async () => { + await redis.setrange(undefined as any, 0, "value"); + }).toThrowErrorMatchingInlineSnapshot(`"Expected key to be a string or buffer for 'setrange'."`); + }); + + test("should reject invalid key in INCRBY", async () => { + const redis = ctx.redis; + expect(async () => { + await redis.incrby([] as any, 10); + }).toThrowErrorMatchingInlineSnapshot(`"Expected key to be a string or buffer for 'incrby'."`); + }); + + test("should reject invalid value in MSET", async () => { + const redis = ctx.redis; + expect(async () => { + await redis.mset("key", {} as any); + }).toThrowErrorMatchingInlineSnapshot(`"Expected additional arguments to be a string or buffer for 'mset'."`); + }); + + test("should reject invalid value in MSETNX", async () => { + const redis = ctx.redis; + expect(async () => { + await redis.msetnx("key1", "value1", "key2", [] as any); + }).toThrowErrorMatchingInlineSnapshot(`"Expected additional arguments to be a string or buffer for 'msetnx'."`); + }); + + test("should reject invalid key in SETBIT", async () => { + const redis = ctx.redis; + expect(async () => { + await redis.setbit({} as any, 0, 1); + }).toThrowErrorMatchingInlineSnapshot(`"Expected key to be a string or buffer for 'setbit'."`); + }); + + test("should reject invalid key in SETEX", async () => { + const redis = ctx.redis; + expect(async () => { + await redis.setex(null as any, 10, "value"); + }).toThrowErrorMatchingInlineSnapshot(`"Expected key to be a string or buffer for 'setex'."`); + }); + + test("should reject invalid key in PSETEX", async () => { + const redis = ctx.redis; + expect(async () => { + await redis.psetex([] as any, 1000, "value"); + }).toThrowErrorMatchingInlineSnapshot(`"Expected key to be a string or buffer for 'psetex'."`); + }); + + test("should reject invalid key in UNLINK", async () => { + const redis = ctx.redis; + expect(async () => { + await redis.unlink({} as any); + }).toThrowErrorMatchingInlineSnapshot(`"Expected additional arguments to be a string or buffer for 'unlink'."`); + }); + + test("should reject invalid additional key in UNLINK", async () => { + const redis = ctx.redis; + expect(async () => { + await redis.unlink("valid-key", [] as any); + }).toThrowErrorMatchingInlineSnapshot(`"Expected additional arguments to be a string or buffer for 'unlink'."`); + }); + + test("should reject invalid key in TOUCH", async () => { + const redis = ctx.redis; + expect(async () => { + await redis.touch(null as any); + }).toThrowErrorMatchingInlineSnapshot(`"The "key" argument must be specified"`); + }); + + test("should reject invalid additional key in TOUCH", async () => { + const redis = ctx.redis; + expect(async () => { + await redis.touch("valid-key", {} as any); + }).toThrowErrorMatchingInlineSnapshot(`"Expected additional arguments to be a string or buffer for 'touch'."`); + }); + + test("should reject invalid key in EXPIREAT", async () => { + const redis = ctx.redis; + expect(async () => { + await redis.expireat({} as any, 1234567890); + }).toThrowErrorMatchingInlineSnapshot(`"Expected key to be a string or buffer for 'expireat'."`); + }); + + test("should reject invalid key in PEXPIRE", async () => { + const redis = ctx.redis; + expect(async () => { + await redis.pexpire([] as any, 5000); + }).toThrowErrorMatchingInlineSnapshot(`"Expected key to be a string or buffer for 'pexpire'."`); + }); + + test("should reject invalid key in PEXPIREAT", async () => { + const redis = ctx.redis; + expect(async () => { + await redis.pexpireat(null as any, 1234567890000); + }).toThrowErrorMatchingInlineSnapshot(`"Expected key to be a string or buffer for 'pexpireat'."`); + }); + }); + + describe("String commands", () => { + test("should append value to key with APPEND", async () => { + const redis = ctx.redis; + const key = "append-test"; + + const length1 = await redis.append(key, "Hello"); + expect(length1).toBe(5); + + const value1 = await redis.get(key); + expect(value1).toBe("Hello"); + + const length2 = await redis.append(key, " World"); + expect(length2).toBe(11); + + const value2 = await redis.get(key); + expect(value2).toBe("Hello World"); + }); + + test("should delete one or more keys with DEL", async () => { + const redis = ctx.redis; + + await redis.set("del-test-1", "value1"); + await redis.set("del-test-2", "value2"); + await redis.set("del-test-3", "value3"); + + const result1 = await redis.del("del-test-1"); + expect(result1).toBe(1); + + expect(await redis.get("del-test-1")).toBeNull(); + + const result2 = await redis.del("del-test-2", "del-test-3"); + expect(result2).toBe(2); + + expect(await redis.get("del-test-2")).toBeNull(); + expect(await redis.get("del-test-3")).toBeNull(); + + const result3 = await redis.del("del-test-nonexistent"); + expect(result3).toBe(0); + }); + + test("should serialize key with DUMP", async () => { + const redis = ctx.redis; + const key = "dump-test"; + + await redis.set(key, "Hello World"); + + const serialized = await redis.dump(key); + expect(serialized).toBeDefined(); + expect(serialized).not.toBeNull(); + + expect(typeof serialized === "string" || Buffer.isBuffer(serialized)).toBe(true); + + const nonExistent = await redis.dump("dump-test-nonexistent"); + expect(nonExistent).toBeNull(); + }); + + test("should get value as Buffer with getBuffer", async () => { + const redis = ctx.redis; + const key = "getbuffer-test"; + + await redis.set(key, "Hello Buffer"); + + const buffer = await redis.getBuffer(key); + expect(buffer).toBeDefined(); + expect(buffer).not.toBeNull(); + expect(Buffer.isBuffer(buffer)).toBe(true); + expect(buffer!.toString()).toBe("Hello Buffer"); + + const nonExistent = await redis.getBuffer("getbuffer-nonexistent"); + expect(nonExistent).toBeNull(); + }); + + test("should get and delete key with GETDEL", async () => { + const redis = ctx.redis; + const key = "getdel-test"; + + await redis.set(key, "Delete me"); + + const value = await redis.getdel(key); + expect(value).toBe("Delete me"); + + const check = await redis.get(key); + expect(check).toBeNull(); + + const nonExistent = await redis.getdel("getdel-nonexistent"); + expect(nonExistent).toBeNull(); + }); + + test("should get key with expiration using GETEX with EX", async () => { + const redis = ctx.redis; + const key = "getex-ex-test"; + + await redis.set(key, "Expire me"); + + const value = await redis.getex(key, "EX", 10); + expect(value).toBe("Expire me"); + + const check = await redis.get(key); + expect(check).toBe("Expire me"); + + const ttl = await redis.ttl(key); + expect(ttl).toBeGreaterThan(0); + expect(ttl).toBeLessThanOrEqual(10); + }); + + test("should get key with expiration using GETEX with PX", async () => { + const redis = ctx.redis; + const key = "getex-px-test"; + + await redis.set(key, "Expire me"); + + const value = await redis.getex(key, "PX", 5000); + expect(value).toBe("Expire me"); + + const ttl = await redis.ttl(key); + expect(ttl).toBeGreaterThan(0); + expect(ttl).toBeLessThanOrEqual(5); + }); + + test("should get key with expiration using GETEX with EXAT", async () => { + const redis = ctx.redis; + const key = "getex-exat-test"; + + await redis.set(key, "Expire at timestamp"); + + const futureTimestamp = Math.floor(Date.now() / 1000) + 60; + const value = await redis.getex(key, "EXAT", futureTimestamp); + expect(value).toBe("Expire at timestamp"); + + const ttl = await redis.ttl(key); + expect(ttl).toBeGreaterThan(0); + expect(ttl).toBeLessThanOrEqual(60); + }); + + test("should get key with expiration using GETEX with PXAT", async () => { + const redis = ctx.redis; + const key = "getex-pxat-test"; + + await redis.set(key, "Expire at timestamp"); + + const futureTimestamp = Date.now() + 60000; + const value = await redis.getex(key, "PXAT", futureTimestamp); + expect(value).toBe("Expire at timestamp"); + + const ttl = await redis.ttl(key); + expect(ttl).toBeGreaterThan(0); + expect(ttl).toBeLessThanOrEqual(60); + }); + + test("should persist key expiration using GETEX with PERSIST", async () => { + const redis = ctx.redis; + const key = "getex-persist-test"; + + await redis.set(key, "Remove expiration", "EX", 100); + + const ttlBefore = await redis.ttl(key); + expect(ttlBefore).toBeGreaterThan(0); + + const value = await redis.getex(key, "PERSIST"); + expect(value).toBe("Remove expiration"); + + const ttlAfter = await redis.ttl(key); + expect(ttlAfter).toBe(-1); + }); + + test("should get non-existent key with GETEX", async () => { + const redis = ctx.redis; + + const value = await redis.getex("getex-nonexistent", "EX", 10); + expect(value).toBeNull(); + }); + + test("should get and set in one operation with GETSET", async () => { + const redis = ctx.redis; + const key = "getset-test"; + + const oldValue1 = await redis.getset(key, "new value"); + expect(oldValue1).toBeNull(); + + const check1 = await redis.get(key); + expect(check1).toBe("new value"); + + const oldValue2 = await redis.getset(key, "newer value"); + expect(oldValue2).toBe("new value"); + + const check2 = await redis.get(key); + expect(check2).toBe("newer value"); + }); + + test("should get string length with STRLEN", async () => { + const redis = ctx.redis; + const key = "strlen-test"; + + const length1 = await redis.strlen("strlen-nonexistent"); + expect(length1).toBe(0); + + await redis.set(key, "Hello World"); + const length2 = await redis.strlen(key); + expect(length2).toBe(11); + + await redis.set(key, "Hi"); + const length3 = await redis.strlen(key); + expect(length3).toBe(2); + }); + }); + + describe("List Operations", () => { + test("should get list length with LLEN", async () => { + const redis = ctx.redis; + const key = "llen-test"; + + const len1 = await redis.llen(key); + expect(len1).toBe(0); + + await redis.lpush(key, "one", "two", "three"); + + const len2 = await redis.llen(key); + expect(len2).toBe(3); + + await redis.lpop(key); + const len3 = await redis.llen(key); + expect(len3).toBe(2); + }); + + test("should pop left with LPOP", async () => { + const redis = ctx.redis; + const key = "lpop-test"; + + await redis.lpush(key, "three", "two", "one"); + + const elem1 = await redis.lpop(key); + expect(elem1).toBe("one"); + + const elem2 = await redis.lpop(key, 2); + expect(elem2).toEqual(["two", "three"]); + + const empty = await redis.lpop(key); + expect(empty).toBeNull(); + }); + + test("should push to existing list with LPUSHX", async () => { + const redis = ctx.redis; + const key = "lpushx-test"; + + const len1 = await redis.lpushx(key, "value"); + expect(len1).toBe(0); + + await redis.lpush(key, "one"); + + const len2 = await redis.lpushx(key, "two"); + expect(len2).toBe(2); + + const list = await redis.lrange(key, 0, -1); + expect(list).toEqual(["two", "one"]); + }); + + test("should pop right with RPOP", async () => { + const redis = ctx.redis; + const key = "rpop-test"; + + await redis.rpush(key, "one", "two", "three"); + + const elem1 = await redis.rpop(key); + expect(elem1).toBe("three"); + + const elem2 = await redis.rpop(key, 2); + expect(elem2).toEqual(["two", "one"]); + + const empty = await redis.rpop(key); + expect(empty).toBeNull(); + }); + + test("should push to existing list with RPUSHX", async () => { + const redis = ctx.redis; + const key = "rpushx-test"; + + const len1 = await redis.rpushx(key, "value"); + expect(len1).toBe(0); + + await redis.rpush(key, "one"); + + const len2 = await redis.rpushx(key, "two"); + expect(len2).toBe(2); + + const list = await redis.lrange(key, 0, -1); + expect(list).toEqual(["one", "two"]); + }); + + test("should get range of elements with LRANGE", async () => { + const redis = ctx.redis; + const key = "lrange-test"; + + await redis.lpush(key, "three"); + await redis.lpush(key, "two"); + await redis.lpush(key, "one"); + + const fullList = await redis.lrange(key, 0, -1); + expect(fullList).toEqual(["one", "two", "three"]); + + const firstTwo = await redis.lrange(key, 0, 1); + expect(firstTwo).toEqual(["one", "two"]); + + const lastTwo = await redis.lrange(key, -2, -1); + expect(lastTwo).toEqual(["two", "three"]); + + const middle = await redis.lrange(key, 1, 1); + expect(middle).toEqual(["two"]); + + const outOfRange = await redis.lrange(key, 10, 20); + expect(outOfRange).toEqual([]); + + const nonExistent = await redis.lrange("nonexistent-list", 0, -1); + expect(nonExistent).toEqual([]); + }); + + test("should get element at index with LINDEX", async () => { + const redis = ctx.redis; + const key = "lindex-test"; + + await redis.lpush(key, "three"); + await redis.lpush(key, "two"); + await redis.lpush(key, "one"); + + const first = await redis.lindex(key, 0); + expect(first).toBe("one"); + + const second = await redis.lindex(key, 1); + expect(second).toBe("two"); + + const third = await redis.lindex(key, 2); + expect(third).toBe("three"); + + const last = await redis.lindex(key, -1); + expect(last).toBe("three"); + + const secondLast = await redis.lindex(key, -2); + expect(secondLast).toBe("two"); + + const outOfRange = await redis.lindex(key, 10); + expect(outOfRange).toBeNull(); + + const outOfRangeNeg = await redis.lindex(key, -10); + expect(outOfRangeNeg).toBeNull(); + + const nonExistent = await redis.lindex("nonexistent-list", 0); + expect(nonExistent).toBeNull(); + }); + + test("should set element at index with LSET", async () => { + const redis = ctx.redis; + const key = "lset-test"; + + await redis.lpush(key, "three"); + await redis.lpush(key, "two"); + await redis.lpush(key, "one"); + + const result1 = await redis.lset(key, 0, "zero"); + expect(result1).toBe("OK"); + + const first = await redis.lindex(key, 0); + expect(first).toBe("zero"); + + const result2 = await redis.lset(key, -1, "last"); + expect(result2).toBe("OK"); + + const last = await redis.lindex(key, -1); + expect(last).toBe("last"); + + const fullList = await redis.lrange(key, 0, -1); + expect(fullList).toEqual(["zero", "two", "last"]); + }); + + test("should handle LSET errors", async () => { + const redis = ctx.redis; + + await redis.lpush("lset-error-test", "value"); + + expect(async () => { + await redis.lset("lset-error-test", 10, "newvalue"); + }).toThrow(/index out of range/i); + + expect(async () => { + await redis.lset("nonexistent-list", 0, "value"); + }).toThrow(/no such key/i); + + await redis.set("string-key", "value"); + expect(async () => { + await redis.lset("string-key", 0, "value"); + }).toThrow(/wrong.*type|WRONGTYPE/i); + }); + + test("should handle LRANGE with various ranges", async () => { + const redis = ctx.redis; + const key = "lrange-advanced"; + + for (let i = 5; i >= 1; i--) { + await redis.lpush(key, String(i)); + } + + const fullList = await redis.lrange(key, 0, -1); + expect(fullList).toEqual(["1", "2", "3", "4", "5"]); + + const invalid = await redis.lrange(key, 3, 1); + expect(invalid).toEqual([]); + + const mixed = await redis.lrange(key, -3, 4); + expect(mixed).toEqual(["3", "4", "5"]); + + const bothNeg = await redis.lrange(key, -4, -2); + expect(bothNeg).toEqual(["2", "3", "4"]); + }); + + test("should handle LINDEX and LSET with numbers", async () => { + const redis = ctx.redis; + const key = "list-numbers"; + + await redis.lpush(key, "100"); + await redis.lpush(key, "200"); + await redis.lpush(key, "300"); + + const elem = await redis.lindex(key, 1); + expect(elem).toBe("200"); + + await redis.lset(key, 1, "250"); + const updated = await redis.lindex(key, 1); + expect(updated).toBe("250"); + }); + + test("should insert element before pivot with LINSERT", async () => { + const redis = ctx.redis; + const key = "linsert-before-test"; + + await redis.lpush(key, "World"); + await redis.lpush(key, "Hello"); + + const result = await redis.linsert(key, "BEFORE", "World", "There"); + expect(result).toBe(3); + + const list = await redis.lrange(key, 0, -1); + expect(list).toEqual(["Hello", "There", "World"]); + }); + + test("should insert element after pivot with LINSERT", async () => { + const redis = ctx.redis; + const key = "linsert-after-test"; + + await redis.lpush(key, "World"); + await redis.lpush(key, "Hello"); + + const result = await redis.linsert(key, "AFTER", "Hello", "Beautiful"); + expect(result).toBe(3); + + const list = await redis.lrange(key, 0, -1); + expect(list).toEqual(["Hello", "Beautiful", "World"]); + }); + + test("should handle LINSERT when pivot not found", async () => { + const redis = ctx.redis; + const key = "linsert-notfound-test"; + + await redis.lpush(key, "value1"); + await redis.lpush(key, "value2"); + + const result = await redis.linsert(key, "BEFORE", "nonexistent", "newvalue"); + expect(result).toBe(-1); + + const list = await redis.lrange(key, 0, -1); + expect(list).toEqual(["value2", "value1"]); + }); + + test("should handle LINSERT on non-existent key", async () => { + const redis = ctx.redis; + + const result = await redis.linsert("nonexistent-list", "BEFORE", "pivot", "element"); + expect(result).toBe(0); + }); + + test("should remove elements from head with LREM", async () => { + const redis = ctx.redis; + const key = "lrem-positive-test"; + + await redis.rpush(key, "hello"); + await redis.rpush(key, "hello"); + await redis.rpush(key, "world"); + await redis.rpush(key, "hello"); + + const result = await redis.lrem(key, 2, "hello"); + expect(result).toBe(2); + + const list = await redis.lrange(key, 0, -1); + expect(list).toEqual(["world", "hello"]); + }); + + test("should remove elements from tail with LREM", async () => { + const redis = ctx.redis; + const key = "lrem-negative-test"; + + await redis.rpush(key, "hello"); + await redis.rpush(key, "world"); + await redis.rpush(key, "hello"); + await redis.rpush(key, "hello"); + + const result = await redis.lrem(key, -2, "hello"); + expect(result).toBe(2); + + const list = await redis.lrange(key, 0, -1); + expect(list).toEqual(["hello", "world"]); + }); + + test("should remove all occurrences with LREM count=0", async () => { + const redis = ctx.redis; + const key = "lrem-all-test"; + + await redis.rpush(key, "hello"); + await redis.rpush(key, "world"); + await redis.rpush(key, "hello"); + await redis.rpush(key, "foo"); + await redis.rpush(key, "hello"); + + const result = await redis.lrem(key, 0, "hello"); + expect(result).toBe(3); + + const list = await redis.lrange(key, 0, -1); + expect(list).toEqual(["world", "foo"]); + }); + + test("should handle LREM when element not found", async () => { + const redis = ctx.redis; + const key = "lrem-notfound-test"; + + await redis.rpush(key, "value1"); + await redis.rpush(key, "value2"); + + const result = await redis.lrem(key, 1, "nonexistent"); + expect(result).toBe(0); + + const list = await redis.lrange(key, 0, -1); + expect(list).toEqual(["value1", "value2"]); + }); + + test("should trim list to range with LTRIM", async () => { + const redis = ctx.redis; + const key = "ltrim-test"; + + await redis.rpush(key, "one"); + await redis.rpush(key, "two"); + await redis.rpush(key, "three"); + await redis.rpush(key, "four"); + + const result = await redis.ltrim(key, 1, 2); + expect(result).toBe("OK"); + + const list = await redis.lrange(key, 0, -1); + expect(list).toEqual(["two", "three"]); + }); + + test("should handle LTRIM with negative indexes", async () => { + const redis = ctx.redis; + const key = "ltrim-negative-test"; + + await redis.rpush(key, "one"); + await redis.rpush(key, "two"); + await redis.rpush(key, "three"); + await redis.rpush(key, "four"); + await redis.rpush(key, "five"); + + const result = await redis.ltrim(key, -3, -1); + expect(result).toBe("OK"); + + const list = await redis.lrange(key, 0, -1); + expect(list).toEqual(["three", "four", "five"]); + }); + + test("should handle LTRIM with out of range indexes", async () => { + const redis = ctx.redis; + const key = "ltrim-outofrange-test"; + + await redis.rpush(key, "one"); + await redis.rpush(key, "two"); + await redis.rpush(key, "three"); + + const result = await redis.ltrim(key, 0, 100); + expect(result).toBe("OK"); + + const list = await redis.lrange(key, 0, -1); + expect(list).toEqual(["one", "two", "three"]); + }); + + test("should empty list with LTRIM when stop < start", async () => { + const redis = ctx.redis; + const key = "ltrim-empty-test"; + + await redis.rpush(key, "one"); + await redis.rpush(key, "two"); + await redis.rpush(key, "three"); + + const result = await redis.ltrim(key, 2, 0); + expect(result).toBe("OK"); + + const list = await redis.lrange(key, 0, -1); + expect(list).toEqual([]); + }); + + test("should block and pop element with BLPOP", async () => { + const redis = ctx.redis; + const key = "blpop-test"; + + await redis.lpush(key, "value1"); + + const result = await redis.blpop(key, 0.1); + expect(result).toEqual([key, "value1"]); + + const timeout = await redis.blpop(key, 0.1); + expect(timeout).toBeNull(); + }); + + test("should block and pop element with BRPOP", async () => { + const redis = ctx.redis; + const key = "brpop-test"; + + await redis.lpush(key, "value2"); + await redis.lpush(key, "value1"); + + const result = await redis.brpop(key, 0.1); + expect(result).toEqual([key, "value2"]); + + await redis.brpop(key, 0.1); + const timeout = await redis.brpop(key, 0.1); + expect(timeout).toBeNull(); + }); + + test("should pop from first non-empty list with BLPOP", async () => { + const redis = ctx.redis; + const key1 = "blpop-list1"; + const key2 = "blpop-list2"; + + await redis.lpush(key2, "value2"); + + const result = await redis.blpop(key1, key2, 0.1); + expect(result).toEqual([key2, "value2"]); + }); + + test("should pop elements with LMPOP LEFT", async () => { + const redis = ctx.redis; + const key = "lmpop-left-test"; + + await redis.lpush(key, "three"); + await redis.lpush(key, "two"); + await redis.lpush(key, "one"); + + const result = await redis.lmpop(1, key, "LEFT"); + expect(result).toEqual([key, ["one"]]); + + const remaining = await redis.lrange(key, 0, -1); + expect(remaining).toEqual(["two", "three"]); + }); + + test("should pop elements with LMPOP RIGHT", async () => { + const redis = ctx.redis; + const key = "lmpop-right-test"; + + await redis.lpush(key, "three"); + await redis.lpush(key, "two"); + await redis.lpush(key, "one"); + + const result = await redis.lmpop(1, key, "RIGHT"); + expect(result).toEqual([key, ["three"]]); + + const remaining = await redis.lrange(key, 0, -1); + expect(remaining).toEqual(["one", "two"]); + }); + + test("should pop multiple elements with LMPOP COUNT", async () => { + const redis = ctx.redis; + const key = "lmpop-count-test"; + + await redis.lpush(key, "three"); + await redis.lpush(key, "two"); + await redis.lpush(key, "one"); + + const result = await redis.lmpop(1, key, "LEFT", "COUNT", 2); + expect(result).toEqual([key, ["one", "two"]]); + + const remaining = await redis.lrange(key, 0, -1); + expect(remaining).toEqual(["three"]); + }); + + test("should return null for LMPOP on empty list", async () => { + const redis = ctx.redis; + + const result = await redis.lmpop(1, "nonexistent-list", "LEFT"); + expect(result).toBeNull(); + }); + + test("should pop from first non-empty list with LMPOP", async () => { + const redis = ctx.redis; + const key1 = "lmpop-empty"; + const key2 = "lmpop-full"; + + await redis.lpush(key2, "value"); + + const result = await redis.lmpop(2, key1, key2, "LEFT"); + expect(result).toEqual([key2, ["value"]]); + }); + + test("should find position of element with LPOS", async () => { + const redis = ctx.redis; + const key = "lpos-test"; + + await redis.lpush(key, "d"); + await redis.lpush(key, "b"); + await redis.lpush(key, "c"); + await redis.lpush(key, "b"); + await redis.lpush(key, "a"); + + const pos1 = await redis.lpos(key, "b"); + expect(pos1).toBe(1); + + const pos2 = await redis.lpos(key, "a"); + expect(pos2).toBe(0); + + const pos3 = await redis.lpos(key, "d"); + expect(pos3).toBe(4); + + const pos4 = await redis.lpos(key, "x"); + expect(pos4).toBeNull(); + }); + + test("should find position with RANK option in LPOS", async () => { + const redis = ctx.redis; + const key = "lpos-rank-test"; + + await redis.lpush(key, "b"); + await redis.lpush(key, "a"); + await redis.lpush(key, "b"); + await redis.lpush(key, "a"); + await redis.lpush(key, "b"); + + const first = await redis.lpos(key, "b"); + expect(first).toBe(0); + + const second = await redis.lpos(key, "b", "RANK", 2); + expect(second).toBe(2); + + const third = await redis.lpos(key, "b", "RANK", 3); + expect(third).toBe(4); + + const fourth = await redis.lpos(key, "b", "RANK", 4); + expect(fourth).toBeNull(); + + const fromEnd = await redis.lpos(key, "b", "RANK", -1); + expect(fromEnd).toBe(4); + + const fromEnd2 = await redis.lpos(key, "b", "RANK", -2); + expect(fromEnd2).toBe(2); + }); + + test("should find multiple positions with COUNT option in LPOS", async () => { + const redis = ctx.redis; + const key = "lpos-count-test"; + + await redis.lpush(key, "c"); + await redis.lpush(key, "b"); + await redis.lpush(key, "b"); + await redis.lpush(key, "a"); + await redis.lpush(key, "b"); + + const all = await redis.lpos(key, "b", "COUNT", 0); + expect(all).toEqual([0, 2, 3]); + + const first2 = await redis.lpos(key, "b", "COUNT", 2); + expect(first2).toEqual([0, 2]); + + const more = await redis.lpos(key, "b", "COUNT", 10); + expect(more).toEqual([0, 2, 3]); + + const none = await redis.lpos(key, "x", "COUNT", 5); + expect(none).toEqual([]); + }); + + test("should find position with MAXLEN option in LPOS", async () => { + const redis = ctx.redis; + const key = "lpos-maxlen-test"; + + for (let i = 5; i >= 1; i--) { + await redis.lpush(key, String(i)); + } + await redis.lpush(key, "target"); + + const found = await redis.lpos(key, "target", "MAXLEN", 6); + expect(found).toBe(0); + + const notFound = await redis.lpos(key, "5", "MAXLEN", 3); + expect(notFound).toBeNull(); + + const found3 = await redis.lpos(key, "3", "MAXLEN", 10); + expect(found3).toBe(3); + }); + + test("should move element from source to destination with LMOVE", async () => { + const redis = ctx.redis; + const source = "lmove-source"; + const dest = "lmove-dest"; + + await redis.lpush(source, "three"); + await redis.lpush(source, "two"); + await redis.lpush(source, "one"); + + const result1 = await redis.lmove(source, dest, "LEFT", "RIGHT"); + expect(result1).toBe("one"); + + const sourceList1 = await redis.lrange(source, 0, -1); + expect(sourceList1).toEqual(["two", "three"]); + + const destList1 = await redis.lrange(dest, 0, -1); + expect(destList1).toEqual(["one"]); + + const result2 = await redis.lmove(source, dest, "RIGHT", "LEFT"); + expect(result2).toBe("three"); + + const sourceList2 = await redis.lrange(source, 0, -1); + expect(sourceList2).toEqual(["two"]); + + const destList2 = await redis.lrange(dest, 0, -1); + expect(destList2).toEqual(["three", "one"]); + }); + + test("should handle all LMOVE direction combinations", async () => { + const redis = ctx.redis; + + await redis.lpush("src1", "b", "a"); + const res1 = await redis.lmove("src1", "dst1", "LEFT", "LEFT"); + expect(res1).toBe("a"); + expect(await redis.lrange("dst1", 0, -1)).toEqual(["a"]); + + await redis.lpush("src2", "b", "a"); + const res2 = await redis.lmove("src2", "dst2", "LEFT", "RIGHT"); + expect(res2).toBe("a"); + expect(await redis.lrange("dst2", 0, -1)).toEqual(["a"]); + + await redis.lpush("src3", "b", "a"); + const res3 = await redis.lmove("src3", "dst3", "RIGHT", "LEFT"); + expect(res3).toBe("b"); + expect(await redis.lrange("dst3", 0, -1)).toEqual(["b"]); + + await redis.lpush("src4", "b", "a"); + const res4 = await redis.lmove("src4", "dst4", "RIGHT", "RIGHT"); + expect(res4).toBe("b"); + expect(await redis.lrange("dst4", 0, -1)).toEqual(["b"]); + }); + + test("should return null for LMOVE on empty source", async () => { + const redis = ctx.redis; + + const result = await redis.lmove("empty-source", "some-dest", "LEFT", "RIGHT"); + expect(result).toBeNull(); + + const destList = await redis.lrange("some-dest", 0, -1); + expect(destList).toEqual([]); + }); + + test("should handle LMOVE to same list", async () => { + const redis = ctx.redis; + const key = "circular-list"; + + await redis.lpush(key, "c", "b", "a"); + + const result = await redis.lmove(key, key, "LEFT", "RIGHT"); + expect(result).toBe("a"); + expect(await redis.lrange(key, 0, -1)).toEqual(["b", "c", "a"]); + }); + + test("should pop from source and push to dest with RPOPLPUSH", async () => { + const redis = ctx.redis; + const source = "rpoplpush-source"; + const dest = "rpoplpush-dest"; + + await redis.lpush(source, "three"); + await redis.lpush(source, "two"); + await redis.lpush(source, "one"); + + const result = await redis.rpoplpush(source, dest); + expect(result).toBe("three"); + + const sourceList = await redis.lrange(source, 0, -1); + expect(sourceList).toEqual(["one", "two"]); + + const destList = await redis.lrange(dest, 0, -1); + expect(destList).toEqual(["three"]); + + const result2 = await redis.rpoplpush(source, dest); + expect(result2).toBe("two"); + + const sourceList2 = await redis.lrange(source, 0, -1); + expect(sourceList2).toEqual(["one"]); + + const destList2 = await redis.lrange(dest, 0, -1); + expect(destList2).toEqual(["two", "three"]); + }); + + test("should return null for RPOPLPUSH on empty source", async () => { + const redis = ctx.redis; + + const result = await redis.rpoplpush("empty-source", "some-dest"); + expect(result).toBeNull(); + }); + + test("should handle RPOPLPUSH to same list (circular)", async () => { + const redis = ctx.redis; + const key = "circular-rpoplpush"; + + await redis.lpush(key, "c", "b", "a"); + + const result = await redis.rpoplpush(key, key); + expect(result).toBe("c"); + expect(await redis.lrange(key, 0, -1)).toEqual(["c", "a", "b"]); + }); + + test("should block and move element with BLMOVE", async () => { + const redis = ctx.redis; + const source = "blmove-source"; + const dest = "blmove-dest"; + + await redis.lpush(source, "three"); + await redis.lpush(source, "two"); + await redis.lpush(source, "one"); + + const result = await redis.blmove(source, dest, "RIGHT", "LEFT", 0.1); + expect(result).toBe("three"); + + const sourceRemaining = await redis.lrange(source, 0, -1); + expect(sourceRemaining).toEqual(["one", "two"]); + + const destElements = await redis.lrange(dest, 0, -1); + expect(destElements).toEqual(["three"]); + + const result2 = await redis.blmove(source, dest, "LEFT", "RIGHT", 0.1); + expect(result2).toBe("one"); + + const finalSource = await redis.lrange(source, 0, -1); + expect(finalSource).toEqual(["two"]); + + const finalDest = await redis.lrange(dest, 0, -1); + expect(finalDest).toEqual(["three", "one"]); + }); + + test("should timeout and return null with BLMOVE on empty list", async () => { + const redis = ctx.redis; + + const result = await redis.blmove("empty-source", "dest", "LEFT", "RIGHT", 0.1); + expect(result).toBeNull(); + }); + + test("should block and pop multiple elements with BLMPOP", async () => { + const redis = ctx.redis; + const key = "blmpop-test"; + + await redis.lpush(key, "three"); + await redis.lpush(key, "two"); + await redis.lpush(key, "one"); + + const result = await redis.blmpop(0.1, 1, key, "LEFT"); + expect(result).toEqual([key, ["one"]]); + + const result2 = await redis.blmpop(0.1, 1, key, "RIGHT", "COUNT", 2); + expect(result2).toEqual([key, ["three", "two"]]); + + const remaining = await redis.lrange(key, 0, -1); + expect(remaining).toEqual([]); + }); + + test("should pop from first non-empty list with BLMPOP", async () => { + const redis = ctx.redis; + const key1 = "blmpop-empty"; + const key2 = "blmpop-full"; + + await redis.lpush(key2, "value"); + + const result = await redis.blmpop(0.1, 2, key1, key2, "LEFT"); + expect(result).toEqual([key2, ["value"]]); + }); + + test("should timeout and return null with BLMPOP on empty lists", async () => { + const redis = ctx.redis; + + const result = await redis.blmpop(0.1, 2, "empty-list1", "empty-list2", "LEFT"); + expect(result).toBeNull(); + }); + + test("should block and move element with BRPOPLPUSH", async () => { + const redis = ctx.redis; + const source = "brpoplpush-source"; + const dest = "brpoplpush-dest"; + + await redis.lpush(source, "value2"); + await redis.lpush(source, "value1"); + + const result = await redis.brpoplpush(source, dest, 0.1); + expect(result).toBe("value2"); + + const sourceRemaining = await redis.lrange(source, 0, -1); + expect(sourceRemaining).toEqual(["value1"]); + + const destElements = await redis.lrange(dest, 0, -1); + expect(destElements).toEqual(["value2"]); + + const result2 = await redis.brpoplpush(source, dest, 0.1); + expect(result2).toBe("value1"); + + const finalSource = await redis.lrange(source, 0, -1); + expect(finalSource).toEqual([]); + + const finalDest = await redis.lrange(dest, 0, -1); + expect(finalDest).toEqual(["value1", "value2"]); + }); + + test("should timeout and return null with BRPOPLPUSH on empty list", async () => { + const redis = ctx.redis; + + const result = await redis.brpoplpush("empty-source", "dest", 0.1); + expect(result).toBeNull(); + }); + + test("should get list length with LLEN", async () => { + const redis = ctx.redis; + const key = "llen-test"; + + const empty = await redis.llen(key); + expect(empty).toBe(0); + + await redis.lpush(key, "three"); + await redis.lpush(key, "two"); + await redis.lpush(key, "one"); + + const length = await redis.llen(key); + expect(length).toBe(3); + + const nonExistent = await redis.llen("nonexistent-list"); + expect(nonExistent).toBe(0); + }); + + test("should pop element from head with LPOP", async () => { + const redis = ctx.redis; + const key = "lpop-test"; + + await redis.lpush(key, "three"); + await redis.lpush(key, "two"); + await redis.lpush(key, "one"); + + const first = await redis.lpop(key); + expect(first).toBe("one"); + + const second = await redis.lpop(key); + expect(second).toBe("two"); + + const remaining = await redis.lrange(key, 0, -1); + expect(remaining).toEqual(["three"]); + + const last = await redis.lpop(key); + expect(last).toBe("three"); + + const empty = await redis.lpop(key); + expect(empty).toBeNull(); + + const nonExistent = await redis.lpop("nonexistent-list"); + expect(nonExistent).toBeNull(); + }); + + test("should pop element from tail with RPOP", async () => { + const redis = ctx.redis; + const key = "rpop-test"; + + await redis.lpush(key, "three"); + await redis.lpush(key, "two"); + await redis.lpush(key, "one"); + + const first = await redis.rpop(key); + expect(first).toBe("three"); + + const second = await redis.rpop(key); + expect(second).toBe("two"); + + const remaining = await redis.lrange(key, 0, -1); + expect(remaining).toEqual(["one"]); + + const last = await redis.rpop(key); + expect(last).toBe("one"); + + const empty = await redis.rpop(key); + expect(empty).toBeNull(); + + const nonExistent = await redis.rpop("nonexistent-list"); + expect(nonExistent).toBeNull(); + }); + + test("should push element to head only if key exists with LPUSHX", async () => { + const redis = ctx.redis; + const key = "lpushx-test"; + + const nonExistent = await redis.lpushx(key, "value"); + expect(nonExistent).toBe(0); + + const exists = await redis.exists(key); + expect(exists).toBe(false); + + await redis.lpush(key, "initial"); + + const result = await redis.lpushx(key, "new"); + expect(result).toBe(2); + + const list = await redis.lrange(key, 0, -1); + expect(list).toEqual(["new", "initial"]); + + const result2 = await redis.lpushx(key, "newer"); + expect(result2).toBe(3); + + const finalList = await redis.lrange(key, 0, -1); + expect(finalList).toEqual(["newer", "new", "initial"]); + }); + + test("should push element to tail only if key exists with RPUSHX", async () => { + const redis = ctx.redis; + const key = "rpushx-test"; + + const nonExistent = await redis.rpushx(key, "value"); + expect(nonExistent).toBe(0); + + const exists = await redis.exists(key); + expect(exists).toBe(false); + + await redis.lpush(key, "initial"); + + const result = await redis.rpushx(key, "new"); + expect(result).toBe(2); + + const list = await redis.lrange(key, 0, -1); + expect(list).toEqual(["initial", "new"]); + + const result2 = await redis.rpushx(key, "newer"); + expect(result2).toBe(3); + + const finalList = await redis.lrange(key, 0, -1); + expect(finalList).toEqual(["initial", "new", "newer"]); + }); + }); + + describe("Set Operations", () => { + test("should get set cardinality with SCARD", async () => { + const redis = ctx.redis; + const key = "scard-test"; + + const count1 = await redis.scard(key); + expect(count1).toBe(0); + + await redis.sadd(key, "one", "two", "three"); + const count2 = await redis.scard(key); + expect(count2).toBe(3); + + await redis.sadd(key, "two"); + const count3 = await redis.scard(key); + expect(count3).toBe(3); + }); + + test("should get set difference with SDIFF", async () => { + const redis = ctx.redis; + const key1 = "sdiff-test1"; + const key2 = "sdiff-test2"; + + await redis.sadd(key1, "a", "b", "c", "d"); + await redis.sadd(key2, "c", "d", "e"); + + const diff = await redis.sdiff(key1, key2); + expect(diff.sort()).toEqual(["a", "b"]); + + const diff2 = await redis.sdiff(key1, "nonexistent"); + expect(diff2.sort()).toEqual(["a", "b", "c", "d"]); + }); + + test("should check set membership with SISMEMBER", async () => { + const redis = ctx.redis; + const key = "sismember-test"; + + await redis.sadd(key, "one", "two", "three"); + + const result1 = await redis.sismember(key, "one"); + expect(result1).toBe(true); + + const result2 = await redis.sismember(key, "nonexistent"); + expect(result2).toBe(false); + + const result3 = await redis.sismember("nonexistent", "member"); + expect(result3).toBe(false); + }); + + test("should move member between sets with SMOVE", async () => { + const redis = ctx.redis; + const source = "smove-source"; + const dest = "smove-dest"; + + await redis.sadd(source, "one", "two", "three"); + await redis.sadd(dest, "four"); + + const result1 = await redis.smove(source, dest, "one"); + expect(result1).toBe(true); + + const sourceMembers = await redis.smembers(source); + expect(sourceMembers.sort()).toEqual(["three", "two"]); + + const destMembers = await redis.smembers(dest); + expect(destMembers.sort()).toEqual(["four", "one"]); + + const result2 = await redis.smove(source, dest, "nonexistent"); + expect(result2).toBe(false); + }); + + test("should pop random member with SPOP", async () => { + const redis = ctx.redis; + const key = "spop-test"; + + await redis.sadd(key, "one", "two", "three", "four"); + + const popped1 = await redis.spop(key); + expect(["one", "two", "three", "four"]).toContain(popped1); + + const remaining1 = await redis.scard(key); + expect(remaining1).toBe(3); + + const popped2 = await redis.spop(key, 2); + expect(Array.isArray(popped2)).toBe(true); + expect(popped2).toBeDefined(); + expect(popped2!.length).toBe(2); + + const remaining2 = await redis.scard(key); + expect(remaining2).toBe(1); + + await redis.spop(key); + const empty = await redis.spop(key); + expect(empty).toBeNull(); + }); + + test("should publish to sharded channel with SPUBLISH", async () => { + const redis = ctx.redis; + + const result = await redis.spublish("test-channel", "test-message"); + expect(typeof result).toBe("number"); + expect(result).toBeGreaterThanOrEqual(0); + }); + + test("should get random member with SRANDMEMBER", async () => { + const redis = ctx.redis; + const key = "srandmember-test"; + + await redis.sadd(key, "one", "two", "three"); + + const member1 = await redis.srandmember(key); + expect(["one", "two", "three"]).toContain(member1); + + const members = await redis.srandmember(key, 2); + expect(Array.isArray(members)).toBe(true); + expect(members!.length).toBeLessThanOrEqual(2); + + const count = await redis.scard(key); + expect(count).toBe(3); + + const empty = await redis.srandmember("nonexistent"); + expect(empty).toBeNull(); + }); + + test("should remove members with SREM", async () => { + const redis = ctx.redis; + const key = "srem-test"; + + await redis.sadd(key, "one", "two", "three", "four"); + + const count1 = await redis.srem(key, "one"); + expect(count1).toBe(1); + + const count2 = await redis.srem(key, "two", "three"); + expect(count2).toBe(2); + + const remaining = await redis.smembers(key); + expect(remaining).toEqual(["four"]); + + const count3 = await redis.srem(key, "nonexistent"); + expect(count3).toBe(0); + }); + + test("should get set union with SUNION", async () => { + const redis = ctx.redis; + const key1 = "sunion-test1"; + const key2 = "sunion-test2"; + + await redis.sadd(key1, "a", "b", "c"); + await redis.sadd(key2, "c", "d", "e"); + + const union = await redis.sunion(key1, key2); + expect(union.sort()).toEqual(["a", "b", "c", "d", "e"]); + + const union2 = await redis.sunion(key1, "nonexistent"); + expect(union2.sort()).toEqual(["a", "b", "c"]); + }); + + test("should store set union with SUNIONSTORE", async () => { + const redis = ctx.redis; + const key1 = "sunionstore-test1"; + const key2 = "sunionstore-test2"; + const dest = "sunionstore-dest"; + + await redis.sadd(key1, "a", "b", "c"); + await redis.sadd(key2, "c", "d", "e"); + + const count = await redis.sunionstore(dest, key1, key2); + expect(count).toBe(5); + + const stored = await redis.smembers(dest); + expect(stored.sort()).toEqual(["a", "b", "c", "d", "e"]); + + await redis.sadd(dest, "z"); + const count2 = await redis.sunionstore(dest, key1, key2); + expect(count2).toBe(5); + + const stored2 = await redis.smembers(dest); + expect(stored2.sort()).toEqual(["a", "b", "c", "d", "e"]); + expect(stored2).not.toContain("z"); + }); + + test("should return intersection of two sets with SINTER", async () => { + const redis = ctx.redis; + const key1 = "set1"; + const key2 = "set2"; + + await redis.sadd(key1, "a"); + await redis.sadd(key1, "b"); + await redis.sadd(key1, "c"); + await redis.sadd(key2, "b"); + await redis.sadd(key2, "c"); + await redis.sadd(key2, "d"); + + const result = await redis.sinter(key1, key2); + expect(result.sort()).toEqual(["b", "c"]); + }); + + test("should return intersection of multiple sets with SINTER", async () => { + const redis = ctx.redis; + const key1 = "set1"; + const key2 = "set2"; + const key3 = "set3"; + + await redis.sadd(key1, "a"); + await redis.sadd(key1, "b"); + await redis.sadd(key1, "c"); + await redis.sadd(key2, "b"); + await redis.sadd(key2, "c"); + await redis.sadd(key2, "d"); + await redis.sadd(key3, "c"); + await redis.sadd(key3, "d"); + await redis.sadd(key3, "e"); + + const result = await redis.sinter(key1, key2, key3); + expect(result).toEqual(["c"]); + }); + + test("should return empty array when sets have no intersection with SINTER", async () => { + const redis = ctx.redis; + const key1 = "set1"; + const key2 = "set2"; + + await redis.sadd(key1, "a"); + await redis.sadd(key1, "b"); + await redis.sadd(key2, "c"); + await redis.sadd(key2, "d"); + + const result = await redis.sinter(key1, key2); + expect(result).toEqual([]); + }); + + test("should return empty array when one set does not exist with SINTER", async () => { + const redis = ctx.redis; + const key1 = "set1"; + const key2 = "nonexistent"; + + await redis.sadd(key1, "a"); + await redis.sadd(key1, "b"); + + const result = await redis.sinter(key1, key2); + expect(result).toEqual([]); + }); + + test("should store intersection in destination with SINTERSTORE", async () => { + const redis = ctx.redis; + const key1 = "set1"; + const key2 = "set2"; + const dest = "dest-set"; + + await redis.sadd(key1, "a"); + await redis.sadd(key1, "b"); + await redis.sadd(key1, "c"); + await redis.sadd(key2, "b"); + await redis.sadd(key2, "c"); + await redis.sadd(key2, "d"); + + const count = await redis.sinterstore(dest, key1, key2); + expect(count).toBe(2); + + const members = await redis.smembers(dest); + expect(members.sort()).toEqual(["b", "c"]); + }); + + test("should overwrite existing destination with SINTERSTORE", async () => { + const redis = ctx.redis; + const key1 = "set1"; + const key2 = "set2"; + const dest = "dest-set"; + + await redis.sadd(dest, "old"); + await redis.sadd(dest, "data"); + + await redis.sadd(key1, "a"); + await redis.sadd(key1, "b"); + await redis.sadd(key2, "b"); + await redis.sadd(key2, "c"); + + const count = await redis.sinterstore(dest, key1, key2); + expect(count).toBe(1); + + const members = await redis.smembers(dest); + expect(members).toEqual(["b"]); + }); + + test("should return 0 when storing empty intersection with SINTERSTORE", async () => { + const redis = ctx.redis; + const key1 = "set1"; + const key2 = "set2"; + const dest = "dest-set"; + + await redis.sadd(key1, "a"); + await redis.sadd(key2, "b"); + + const count = await redis.sinterstore(dest, key1, key2); + expect(count).toBe(0); + + const members = await redis.smembers(dest); + expect(members).toEqual([]); + }); + + test("should return cardinality of intersection with SINTERCARD", async () => { + const redis = ctx.redis; + const key1 = "set1"; + const key2 = "set2"; + + await redis.sadd(key1, "a"); + await redis.sadd(key1, "b"); + await redis.sadd(key1, "c"); + await redis.sadd(key2, "b"); + await redis.sadd(key2, "c"); + await redis.sadd(key2, "d"); + + const count = await redis.sintercard(2, key1, key2); + expect(count).toBe(2); + }); + + test("should return 0 for empty intersection with SINTERCARD", async () => { + const redis = ctx.redis; + const key1 = "set1"; + const key2 = "set2"; + + await redis.sadd(key1, "a"); + await redis.sadd(key2, "b"); + + const count = await redis.sintercard(2, key1, key2); + expect(count).toBe(0); + }); + + test("should support LIMIT option with SINTERCARD", async () => { + const redis = ctx.redis; + const key1 = "set1"; + const key2 = "set2"; + + await redis.sadd(key1, "a"); + await redis.sadd(key1, "b"); + await redis.sadd(key1, "c"); + await redis.sadd(key1, "d"); + await redis.sadd(key2, "a"); + await redis.sadd(key2, "b"); + await redis.sadd(key2, "c"); + await redis.sadd(key2, "d"); + + const count = await redis.sintercard(2, key1, key2, "LIMIT", 2); + expect(count).toBe(2); + }); + + test("should throw error when SINTER receives no keys", async () => { + const redis = ctx.redis; + + expect(async () => { + // @ts-expect-error + await redis.sinter(); + }).toThrowErrorMatchingInlineSnapshot(`"ERR wrong number of arguments for 'sinter' command"`); + }); + + test("should throw error when SINTERSTORE receives no keys", async () => { + const redis = ctx.redis; + + expect(async () => { + // @ts-expect-error + await redis.sinterstore(); + }).toThrowErrorMatchingInlineSnapshot(`"ERR wrong number of arguments for 'sinterstore' command"`); + }); + + test("should throw error when SINTERCARD receives no keys", async () => { + const redis = ctx.redis; + + expect(async () => { + // @ts-expect-error + await redis.sintercard(); + }).toThrowErrorMatchingInlineSnapshot(`"ERR wrong number of arguments for 'sintercard' command"`); + }); + + test("should store set difference with SDIFFSTORE", async () => { + const redis = ctx.redis; + const key1 = "set1"; + const key2 = "set2"; + const key3 = "set3"; + const dest = "diff-result"; + + await redis.sadd(key1, "a"); + await redis.sadd(key1, "b"); + await redis.sadd(key1, "c"); + await redis.sadd(key1, "d"); + + await redis.sadd(key2, "b"); + await redis.sadd(key2, "c"); + + await redis.sadd(key3, "d"); + + const count1 = await redis.sdiffstore(dest, key1, key2); + expect(count1).toBe(2); + + const members1 = await redis.smembers(dest); + expect(members1.sort()).toEqual(["a", "d"]); + + const count2 = await redis.sdiffstore(dest, key1, key2, key3); + expect(count2).toBe(1); + + const members2 = await redis.smembers(dest); + expect(members2).toEqual(["a"]); + }); + + test("should throw error with SDIFFSTORE on invalid arguments", async () => { + const redis = ctx.redis; + + expect(async () => { + await (redis as any).sdiffstore(); + }).toThrowErrorMatchingInlineSnapshot(`"ERR wrong number of arguments for 'sdiffstore' command"`); + }); + + test("should check multiple members with SMISMEMBER", async () => { + const redis = ctx.redis; + const key = "test-set"; + + await redis.sadd(key, "a"); + await redis.sadd(key, "b"); + await redis.sadd(key, "c"); + + const result = await redis.smismember(key, "a", "b", "d", "e"); + expect(result).toEqual([1, 1, 0, 0]); + + const result2 = await redis.smismember(key, "c"); + expect(result2).toEqual([1]); + + const result3 = await redis.smismember("nonexistent", "a", "b"); + expect(result3).toEqual([0, 0]); + }); + + test("should throw error with SMISMEMBER on invalid arguments", async () => { + const redis = ctx.redis; + + expect(async () => { + await (redis as any).smismember(); + }).toThrowErrorMatchingInlineSnapshot(`"ERR wrong number of arguments for 'smismember' command"`); + }); + + test("should scan set members with SSCAN", async () => { + const redis = ctx.redis; + const key = "scan-set"; + + for (let i = 0; i < 20; i++) { + await redis.sadd(key, `member${i}`); + } + + let cursor = "0"; + const allMembers: string[] = []; + + do { + const [nextCursor, members] = await redis.sscan(key, cursor); + allMembers.push(...members); + cursor = nextCursor; + } while (cursor !== "0"); + + expect(allMembers.length).toBe(20); + expect(new Set(allMembers).size).toBe(20); + + for (let i = 0; i < 20; i++) { + expect(allMembers).toContain(`member${i}`); + } + }); + + test("should scan set with MATCH pattern using SSCAN", async () => { + const redis = ctx.redis; + const key = "scan-pattern-set"; + + await redis.sadd(key, "user:1"); + await redis.sadd(key, "user:2"); + await redis.sadd(key, "user:3"); + await redis.sadd(key, "admin:1"); + await redis.sadd(key, "admin:2"); + + const [cursor, members] = await redis.sscan(key, "0", "MATCH", "user:*"); + + let allUserMembers: string[] = [...members]; + let scanCursor = cursor; + + while (scanCursor !== "0") { + const [nextCursor, nextMembers] = await redis.sscan(key, scanCursor, "MATCH", "user:*"); + allUserMembers.push(...nextMembers); + scanCursor = nextCursor; + } + + const userMembers = allUserMembers.filter(m => m.startsWith("user:")); + expect(userMembers.length).toBeGreaterThanOrEqual(0); + }); + + test("should scan empty set with SSCAN", async () => { + const redis = ctx.redis; + const key = "empty-scan-set"; + + const [cursor, members] = await redis.sscan(key, "0"); + expect(cursor).toBe("0"); + expect(members).toEqual([]); + }); + + test("should throw error with SSCAN on invalid arguments", async () => { + const redis = ctx.redis; + + expect(async () => { + await (redis as any).sscan(); + }).toThrowErrorMatchingInlineSnapshot(`"ERR wrong number of arguments for 'sscan' command"`); + }); + + test("should get cardinality of set with SCARD", async () => { + const redis = ctx.redis; + const key = "scard-test"; + + const emptyCount = await redis.scard(key); + expect(emptyCount).toBe(0); + + await redis.sadd(key, "a"); + await redis.sadd(key, "b"); + await redis.sadd(key, "c"); + + const count = await redis.scard(key); + expect(count).toBe(3); + + await redis.sadd(key, "a"); + const sameCount = await redis.scard(key); + expect(sameCount).toBe(3); + }); + + test("should get difference of sets with SDIFF", async () => { + const redis = ctx.redis; + const key1 = "sdiff-test1"; + const key2 = "sdiff-test2"; + const key3 = "sdiff-test3"; + + await redis.sadd(key1, "a"); + await redis.sadd(key1, "b"); + await redis.sadd(key1, "c"); + await redis.sadd(key1, "d"); + + await redis.sadd(key2, "b"); + await redis.sadd(key2, "c"); + + await redis.sadd(key3, "d"); + + const diff1 = await redis.sdiff(key1, key2); + expect(diff1.sort()).toEqual(["a", "d"]); + + const diff2 = await redis.sdiff(key1, key2, key3); + expect(diff2.sort()).toEqual(["a"]); + + const diff3 = await redis.sdiff(key1, "nonexistent"); + expect(diff3.sort()).toEqual(["a", "b", "c", "d"]); + + const diff4 = await redis.sdiff(key2, key1); + expect(diff4).toEqual([]); + }); + + test("should check if member exists in set with SISMEMBER", async () => { + const redis = ctx.redis; + const key = "sismember-test"; + + await redis.sadd(key, "a"); + await redis.sadd(key, "b"); + await redis.sadd(key, "c"); + + const exists1 = await redis.sismember(key, "a"); + expect(exists1).toBe(true); + + const exists2 = await redis.sismember(key, "b"); + expect(exists2).toBe(true); + + const notExists = await redis.sismember(key, "z"); + expect(notExists).toBe(false); + + const notExistsSet = await redis.sismember("nonexistent", "a"); + expect(notExistsSet).toBe(false); + }); + + test("should move member between sets with SMOVE", async () => { + const redis = ctx.redis; + const source = "smove-source"; + const dest = "smove-dest"; + + await redis.sadd(source, "a"); + await redis.sadd(source, "b"); + await redis.sadd(source, "c"); + + await redis.sadd(dest, "x"); + await redis.sadd(dest, "y"); + + const moved = await redis.smove(source, dest, "b"); + expect(moved).toBe(true); + + const sourceMembers = await redis.smembers(source); + expect(sourceMembers.sort()).toEqual(["a", "c"]); + + const destMembers = await redis.smembers(dest); + expect(destMembers.sort()).toEqual(["b", "x", "y"]); + + const notMoved = await redis.smove(source, dest, "z"); + expect(notMoved).toBe(false); + + const notMoved2 = await redis.smove("nonexistent", dest, "a"); + expect(notMoved2).toBe(false); + }); + + test("should remove and return random member with SPOP", async () => { + const redis = ctx.redis; + const key = "spop-test"; + + await redis.sadd(key, "a"); + await redis.sadd(key, "b"); + await redis.sadd(key, "c"); + await redis.sadd(key, "d"); + await redis.sadd(key, "e"); + + const popped = await redis.spop(key); + expect(popped).toBeDefined(); + expect(["a", "b", "c", "d", "e"]).toContain(popped); + + const remaining = await redis.scard(key); + expect(remaining).toBe(4); + + const poppedMultiple = await redis.spop(key, 2); + expect(Array.isArray(poppedMultiple)).toBe(true); + expect(poppedMultiple!.length).toBe(2); + poppedMultiple!.forEach(member => { + expect(["a", "b", "c", "d", "e"]).toContain(member); + }); + + const remainingAfter = await redis.scard(key); + expect(remainingAfter).toBe(2); + + await redis.spop(key, 10); + const emptyPop = await redis.spop(key); + expect(emptyPop).toBeNull(); + }); + + test("should publish to sharded channel with SPUBLISH", async () => { + const redis = ctx.redis; + const channel = "spublish-channel"; + + const count = await redis.spublish(channel, "test message"); + expect(typeof count).toBe("number"); + expect(count).toBe(0); + }); + + test("should get random member without removing with SRANDMEMBER", async () => { + const redis = ctx.redis; + const key = "srandmember-test"; + + await redis.sadd(key, "a"); + await redis.sadd(key, "b"); + await redis.sadd(key, "c"); + await redis.sadd(key, "d"); + await redis.sadd(key, "e"); + + const random = await redis.srandmember(key); + expect(random).toBeDefined(); + expect(["a", "b", "c", "d", "e"]).toContain(random); + + const count = await redis.scard(key); + expect(count).toBe(5); + + const randomMultiple = await redis.srandmember(key, 3); + expect(Array.isArray(randomMultiple)).toBe(true); + expect(randomMultiple!.length).toBe(3); + randomMultiple!.forEach(member => { + expect(["a", "b", "c", "d", "e"]).toContain(member); + }); + + const countAfter = await redis.scard(key); + expect(countAfter).toBe(5); + + const tooMany = await redis.srandmember(key, 10); + expect(Array.isArray(tooMany)).toBe(true); + expect(tooMany!.length).toBe(5); + + const withDuplicates = await redis.srandmember(key, -10); + expect(withDuplicates!.length).toBe(10); + withDuplicates!.forEach(member => { + expect(["a", "b", "c", "d", "e"]).toContain(member); + }); + + const emptyRandom = await redis.srandmember("nonexistent"); + expect(emptyRandom).toBeNull(); + }); + + test("should remove members from set with SREM", async () => { + const redis = ctx.redis; + const key = "srem-test"; + + await redis.sadd(key, "a"); + await redis.sadd(key, "b"); + await redis.sadd(key, "c"); + await redis.sadd(key, "d"); + await redis.sadd(key, "e"); + + const removed1 = await redis.srem(key, "a"); + expect(removed1).toBe(1); + + const members1 = await redis.smembers(key); + expect(members1.sort()).toEqual(["b", "c", "d", "e"]); + + const removed2 = await redis.srem(key, "b", "c", "z"); + expect(removed2).toBe(2); + + const members2 = await redis.smembers(key); + expect(members2.sort()).toEqual(["d", "e"]); + + const removed3 = await redis.srem(key, "nonexistent"); + expect(removed3).toBe(0); + + const removed4 = await redis.srem("nonexistent", "a"); + expect(removed4).toBe(0); + }); + + test("should get union of sets with SUNION", async () => { + const redis = ctx.redis; + const key1 = "sunion-test1"; + const key2 = "sunion-test2"; + const key3 = "sunion-test3"; + + await redis.sadd(key1, "a"); + await redis.sadd(key1, "b"); + await redis.sadd(key1, "c"); + + await redis.sadd(key2, "c"); + await redis.sadd(key2, "d"); + await redis.sadd(key2, "e"); + + await redis.sadd(key3, "e"); + await redis.sadd(key3, "f"); + await redis.sadd(key3, "g"); + + const union1 = await redis.sunion(key1, key2); + expect(union1.sort()).toEqual(["a", "b", "c", "d", "e"]); + + const union2 = await redis.sunion(key1, key2, key3); + expect(union2.sort()).toEqual(["a", "b", "c", "d", "e", "f", "g"]); + + const union3 = await redis.sunion(key1, "nonexistent"); + expect(union3.sort()).toEqual(["a", "b", "c"]); + + const union4 = await redis.sunion("nonexistent1", "nonexistent2"); + expect(union4).toEqual([]); + }); + + test("should store union of sets with SUNIONSTORE", async () => { + const redis = ctx.redis; + const key1 = "sunionstore-test1"; + const key2 = "sunionstore-test2"; + const key3 = "sunionstore-test3"; + const dest = "sunionstore-dest"; + + await redis.sadd(key1, "a"); + await redis.sadd(key1, "b"); + await redis.sadd(key1, "c"); + + await redis.sadd(key2, "c"); + await redis.sadd(key2, "d"); + await redis.sadd(key2, "e"); + + await redis.sadd(key3, "e"); + await redis.sadd(key3, "f"); + + const count1 = await redis.sunionstore(dest, key1, key2); + expect(count1).toBe(5); + + const members1 = await redis.smembers(dest); + expect(members1.sort()).toEqual(["a", "b", "c", "d", "e"]); + + const count2 = await redis.sunionstore(dest, key1, key2, key3); + expect(count2).toBe(6); + + const members2 = await redis.smembers(dest); + expect(members2.sort()).toEqual(["a", "b", "c", "d", "e", "f"]); + + const count3 = await redis.sunionstore(dest, key1, "nonexistent"); + expect(count3).toBe(3); + + const members3 = await redis.smembers(dest); + expect(members3.sort()).toEqual(["a", "b", "c"]); + + const count4 = await redis.sunionstore(dest, "nonexistent1", "nonexistent2"); + expect(count4).toBe(0); + + const members4 = await redis.smembers(dest); + expect(members4).toEqual([]); + }); + }); + + describe("Sorted Set Operations", () => { + test("should get cardinality with ZCARD", async () => { + const redis = ctx.redis; + const key = "zcard-test"; + + const count1 = await redis.zcard(key); + expect(count1).toBe(0); + + await redis.zadd(key, 1, "one", 2, "two", 3, "three"); + + const count2 = await redis.zcard(key); + expect(count2).toBe(3); + + await redis.zadd(key, 4, "one"); + const count3 = await redis.zcard(key); + expect(count3).toBe(3); + }); + + test("should pop max with ZPOPMAX", async () => { + const redis = ctx.redis; + const key = "zpopmax-test"; + + await redis.zadd(key, 1, "one", 2, "two", 3, "three", 4, "four"); + + const result1 = await redis.zpopmax(key); + expect(result1).toEqual(["four", 4]); + + const result2 = await redis.zpopmax(key, 2); + expect(result2).toEqual([ + ["three", 3], + ["two", 2], + ]); + + const remaining = await redis.zcard(key); + expect(remaining).toBe(1); + + await redis.zpopmax(key); + const empty = await redis.zpopmax(key); + expect(empty).toEqual([]); + }); + + test("should pop min with ZPOPMIN", async () => { + const redis = ctx.redis; + const key = "zpopmin-test"; + + await redis.zadd(key, 1, "one", 2, "two", 3, "three", 4, "four"); + + const result1 = await redis.zpopmin(key); + expect(result1).toEqual(["one", 1]); + + const result2 = await redis.zpopmin(key, 2); + expect(result2).toEqual([ + ["two", 2], + ["three", 3], + ]); + + const remaining = await redis.zcard(key); + expect(remaining).toBe(1); + + await redis.zpopmin(key); + const empty = await redis.zpopmin(key); + expect(empty).toEqual([]); + }); + + test("should get random member with ZRANDMEMBER", async () => { + const redis = ctx.redis; + const key = "zrandmember-test"; + + await redis.zadd(key, 1, "one", 2, "two", 3, "three"); + + const result1 = await redis.zrandmember(key); + expect(result1).toBeDefined(); + expect(["one", "two", "three"]).toContain(result1); + + const result2 = await redis.zrandmember(key, 2); + expect(Array.isArray(result2)).toBe(true); + expect(result2!.length).toBeLessThanOrEqual(2); + + result2!.forEach((member: string) => { + expect(["one", "two", "three"]).toContain(member); + }); + + const result3 = await redis.zrandmember(key, 1, "WITHSCORES"); + + expect<([string, number][] | null)[]>([[["one", 1]], [["two", 2]], [["three", 3]]]).toContainEqual(result3); + + const emptyKey = "zrandmember-empty-" + randomUUIDv7(); + const empty = await redis.zrandmember(emptyKey); + expect(empty).toBeNull(); + }); + + test("should get rank with ZRANK", async () => { + const redis = ctx.redis; + const key = "zrank-test"; + + await redis.zadd(key, 1, "one", 2, "two", 3, "three"); + + const rank1 = await redis.zrank(key, "one"); + expect(rank1).toBe(0); + + const rank2 = await redis.zrank(key, "two"); + expect(rank2).toBe(1); + + const rank3 = await redis.zrank(key, "three"); + expect(rank3).toBe(2); + + const rank4 = await redis.zrank(key, "nonexistent"); + expect(rank4).toBeNull(); + }); + + test("should get reverse rank with ZREVRANK", async () => { + const redis = ctx.redis; + const key = "zrevrank-test"; + + await redis.zadd(key, 1, "one", 2, "two", 3, "three"); + + const rank1 = await redis.zrevrank(key, "three"); + expect(rank1).toBe(0); + + const rank2 = await redis.zrevrank(key, "two"); + expect(rank2).toBe(1); + + const rank3 = await redis.zrevrank(key, "one"); + expect(rank3).toBe(2); + + const rank4 = await redis.zrevrank(key, "nonexistent"); + expect(rank4).toBeNull(); + }); + + test("should increment score with ZINCRBY", async () => { + const redis = ctx.redis; + const key = "zincrby-test"; + + await redis.send("ZADD", [key, "1.0", "member1", "2.0", "member2"]); + + const newScore1 = await redis.zincrby(key, 2.5, "member1"); + expect(newScore1).toBe(3.5); + + const newScore2 = await redis.zincrby(key, -1.5, "member2"); + expect(newScore2).toBe(0.5); + + const newScore3 = await redis.zincrby(key, 5, "member3"); + expect(newScore3).toBe(5); + }); + + test("should count members in score range with ZCOUNT", async () => { + const redis = ctx.redis; + const key = "zcount-test"; + + await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three", "4", "four", "5", "five"]); + + const count1 = await redis.zcount(key, "-inf", "+inf"); + expect(count1).toBe(5); + + const count2 = await redis.zcount(key, 2, 4); + expect(count2).toBe(3); + + const count3 = await redis.zcount(key, 1, 3); + expect(count3).toBe(3); + + const count4 = await redis.zcount(key, 10, 20); + expect(count4).toBe(0); + }); + + test("should count members in lexicographical range with ZLEXCOUNT", async () => { + const redis = ctx.redis; + const key = "zlexcount-test"; + + await redis.send("ZADD", [key, "0", "apple", "0", "banana", "0", "cherry", "0", "date", "0", "elderberry"]); + + const count1 = await redis.zlexcount(key, "-", "+"); + expect(count1).toBe(5); + + const count2 = await redis.zlexcount(key, "[banana", "[date"); + expect(count2).toBe(3); + + const count3 = await redis.zlexcount(key, "(banana", "(date"); + expect(count3).toBe(1); + + const count4 = await redis.zlexcount(key, "[zebra", "[zoo"); + expect(count4).toBe(0); + }); + + test("should compute difference between sorted sets with ZDIFF", async () => { + const redis = ctx.redis; + const key1 = "zdiff-test1"; + const key2 = "zdiff-test2"; + const key3 = "zdiff-test3"; + + await redis.send("ZADD", [key1, "1", "one", "2", "two", "3", "three", "4", "four"]); + await redis.send("ZADD", [key2, "1", "one", "2", "two"]); + await redis.send("ZADD", [key3, "3", "three"]); + + const diff1 = await redis.zdiff(2, key1, key2); + expect(diff1).toEqual(["three", "four"]); + + const diff2 = await redis.zdiff(3, key1, key2, key3); + expect(diff2).toEqual(["four"]); + + const diff3 = await redis.zdiff(2, key1, key2, "WITHSCORES"); + expect(diff3).toEqual([ + ["three", 3], + ["four", 4], + ]); + + const diff4 = await redis.zdiff(2, key1, "nonexistent"); + expect(diff4.length).toBe(4); + expect(diff4).toEqual(["one", "two", "three", "four"]); + + const diff5 = await redis.zdiff(2, key2, key1); + expect(diff5).toEqual([]); + }); + + test("should store difference between sorted sets with ZDIFFSTORE", async () => { + const redis = ctx.redis; + const key1 = "zdiffstore-test1"; + const key2 = "zdiffstore-test2"; + const dest = "zdiffstore-dest"; + + await redis.send("ZADD", [key1, "1", "one", "2", "two", "3", "three"]); + await redis.send("ZADD", [key2, "1", "one"]); + + const count = await redis.zdiffstore(dest, 2, key1, key2); + expect(count).toBe(2); + + const members = await redis.send("ZRANGE", [dest, "0", "-1"]); + expect(members).toEqual(["two", "three"]); + + const membersWithScores = await redis.send("ZRANGE", [dest, "0", "-1", "WITHSCORES"]); + expect(membersWithScores).toEqual([ + ["two", 2], + ["three", 3], + ]); + + const count2 = await redis.zdiffstore(dest, 2, key2, key1); + expect(count2).toBe(0); + + const finalCount = await redis.send("ZCARD", [dest]); + expect(finalCount).toBe(0); + }); + + test("should count intersection with ZINTERCARD", async () => { + const redis = ctx.redis; + const key1 = "zintercard-test1"; + const key2 = "zintercard-test2"; + const key3 = "zintercard-test3"; + + await redis.send("ZADD", [key1, "1", "one", "2", "two", "3", "three"]); + await redis.send("ZADD", [key2, "1", "one", "2", "two", "4", "four"]); + await redis.send("ZADD", [key3, "1", "one", "5", "five"]); + + const count1 = await redis.zintercard(2, key1, key2); + expect(count1).toBe(2); + + const count2 = await redis.zintercard(3, key1, key2, key3); + expect(count2).toBe(1); + + const count3 = await redis.zintercard(2, key1, key2, "LIMIT", 1); + expect(count3).toBe(1); + + const count4 = await redis.zintercard(2, key1, key3); + expect(count4).toBe(1); + + const count5 = await redis.zintercard(2, key1, "nonexistent"); + expect(count5).toBe(0); + }); + + test("should reject invalid arguments in ZDIFF", async () => { + const redis = ctx.redis; + expect(async () => { + await redis.zdiff({} as any, "key1"); + }).toThrowErrorMatchingInlineSnapshot(`"Expected additional arguments to be a string or buffer for 'zdiff'."`); + }); + + test("should reject invalid arguments in ZDIFFSTORE", async () => { + const redis = ctx.redis; + expect(async () => { + await redis.zdiffstore("dest", {} as any, "key1"); + }).toThrowErrorMatchingInlineSnapshot( + `"Expected additional arguments to be a string or buffer for 'zdiffstore'."`, + ); + }); + + test("should reject invalid arguments in ZINTERCARD", async () => { + const redis = ctx.redis; + expect(async () => { + await redis.zintercard({} as any, "key1"); + }).toThrowErrorMatchingInlineSnapshot( + `"Expected additional arguments to be a string or buffer for 'zintercard'."`, + ); + }); + + test("should remove members by rank with ZREMRANGEBYRANK", async () => { + const redis = ctx.redis; + const key = "zremrangebyrank-test"; + + await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three", "4", "four", "5", "five"]); + + const removed1 = await redis.zremrangebyrank(key, 0, 1); + expect(removed1).toBe(2); + + const remaining = await redis.send("ZCARD", [key]); + expect(remaining).toBe(3); + + const removed2 = await redis.zremrangebyrank(key, -1, -1); + expect(removed2).toBe(1); + + const final = await redis.send("ZCARD", [key]); + expect(final).toBe(2); + }); + + test("should remove members by score range with ZREMRANGEBYSCORE", async () => { + const redis = ctx.redis; + const key = "zremrangebyscore-test"; + + await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three", "4", "four", "5", "five"]); + + const removed1 = await redis.zremrangebyscore(key, 2, 4); + expect(removed1).toBe(3); + + const remaining = await redis.send("ZCARD", [key]); + expect(remaining).toBe(2); + + const removed2 = await redis.zremrangebyscore(key, "-inf", "+inf"); + expect(removed2).toBe(2); + }); + + test("should remove members by lexicographical range with ZREMRANGEBYLEX", async () => { + const redis = ctx.redis; + const key = "zremrangebylex-test"; + + await redis.send("ZADD", [key, "0", "apple", "0", "banana", "0", "cherry", "0", "date", "0", "elderberry"]); + + const removed1 = await redis.zremrangebylex(key, "[banana", "[date"); + expect(removed1).toBe(3); + + const remaining = await redis.send("ZCARD", [key]); + expect(remaining).toBe(2); + + const removed2 = await redis.zremrangebylex(key, "-", "+"); + expect(removed2).toBe(2); + }); + + test("should reject invalid key in ZINCRBY", async () => { + const redis = ctx.redis; + expect(async () => { + await redis.zincrby({} as any, 1, "member"); + }).toThrowErrorMatchingInlineSnapshot(`"Expected key to be a string or buffer for 'zincrby'."`); + }); + + test("should reject invalid key in ZCOUNT", async () => { + const redis = ctx.redis; + expect(async () => { + await redis.zcount([] as any, 0, 10); + }).toThrowErrorMatchingInlineSnapshot(`"Expected key to be a string or buffer for 'zcount'."`); + }); + + test("should reject invalid key in ZLEXCOUNT", async () => { + const redis = ctx.redis; + expect(async () => { + await redis.zlexcount(null as any, "[a", "[z"); + }).toThrowErrorMatchingInlineSnapshot(`"Expected key to be a string or buffer for 'zlexcount'."`); + }); + + test("should reject invalid key in ZREMRANGEBYRANK", async () => { + const redis = ctx.redis; + expect(async () => { + await redis.zremrangebyrank({} as any, 0, 10); + }).toThrowErrorMatchingInlineSnapshot(`"Expected key to be a string or buffer for 'zremrangebyrank'."`); + }); + + test("should reject invalid key in ZREMRANGEBYSCORE", async () => { + const redis = ctx.redis; + expect(async () => { + await redis.zremrangebyscore([] as any, 0, 10); + }).toThrowErrorMatchingInlineSnapshot(`"Expected key to be a string or buffer for 'zremrangebyscore'."`); + }); + + test("should reject invalid key in ZREMRANGEBYLEX", async () => { + const redis = ctx.redis; + expect(async () => { + await redis.zremrangebylex(null as any, "[a", "[z"); + }).toThrowErrorMatchingInlineSnapshot(`"Expected key to be a string or buffer for 'zremrangebylex'."`); + }); + + test("should remove one or more members with ZREM", async () => { + const redis = ctx.redis; + const key = "zrem-test"; + + await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three", "4", "four"]); + + const removed1 = await redis.zrem(key, "two"); + expect(removed1).toBe(1); + + const removed2 = await redis.zrem(key, "one", "three"); + expect(removed2).toBe(2); + + const removed3 = await redis.zrem(key, "nonexistent"); + expect(removed3).toBe(0); + + const removed4 = await redis.zrem(key, "four", "nothere"); + expect(removed4).toBe(1); + }); + + test("should get scores with ZMSCORE", async () => { + const redis = ctx.redis; + const key = "zmscore-test"; + + await redis.send("ZADD", [key, "1.5", "one", "2.7", "two", "3.9", "three"]); + + const scores1 = await redis.zmscore(key, "two"); + expect(scores1).toEqual([2.7]); + + const scores2 = await redis.zmscore(key, "one", "three"); + expect(scores2).toEqual([1.5, 3.9]); + + const scores3 = await redis.zmscore(key, "one", "nonexistent", "three"); + expect(scores3).toEqual([1.5, null, 3.9]); + + const scores4 = await redis.zmscore(key, "nothere", "alsonothere"); + expect(scores4).toEqual([null, null]); + }); + + test("should reject invalid key in ZREM", async () => { + const redis = ctx.redis; + expect(async () => { + await redis.zrem({} as any, "member"); + }).toThrowErrorMatchingInlineSnapshot(`"Expected additional arguments to be a string or buffer for 'zrem'."`); + }); + + test("should reject invalid key in ZMSCORE", async () => { + const redis = ctx.redis; + expect(async () => { + await redis.zmscore([] as any, "member"); + }).toThrowErrorMatchingInlineSnapshot( + `"Expected additional arguments to be a string or buffer for 'zmscore'."`, + ); + }); + + test("should add members to sorted set with ZADD", async () => { + const redis = ctx.redis; + const key = "zadd-basic-test"; + + const added1 = await redis.zadd(key, "1", "one"); + expect(added1).toBe(1); + + const added2 = await redis.zadd(key, "2", "two", "3", "three"); + expect(added2).toBe(2); + + const added3 = await redis.zadd(key, "1.5", "one"); + expect(added3).toBe(0); + + const score = await redis.zscore(key, "one"); + expect(score).toBe(1.5); + }); + + test("should add members with NX option in ZADD", async () => { + const redis = ctx.redis; + const key = "zadd-nx-test"; + + await redis.zadd(key, "1", "one"); + + const added1 = await redis.zadd(key, "NX", "2", "one"); + expect(added1).toBe(0); + + const score1 = await redis.zscore(key, "one"); + expect(score1).toBe(1); + + const added2 = await redis.zadd(key, "NX", "2", "two"); + expect(added2).toBe(1); + + const score2 = await redis.zscore(key, "two"); + expect(score2).toBe(2); + }); + + test("should update members with XX option in ZADD", async () => { + const redis = ctx.redis; + const key = "zadd-xx-test"; + + await redis.zadd(key, "1", "one"); + + const updated1 = await redis.zadd(key, "XX", "2", "one"); + expect(updated1).toBe(0); + + const score1 = await redis.zscore(key, "one"); + expect(score1).toBe(2); + + const added = await redis.zadd(key, "XX", "3", "three"); + expect(added).toBe(0); + + const score2 = await redis.zscore(key, "three"); + expect(score2).toBeNull(); + }); + + test("should return changed count with CH option in ZADD", async () => { + const redis = ctx.redis; + const key = "zadd-ch-test"; + + await redis.zadd(key, "1", "one", "2", "two"); + + const changed = await redis.zadd(key, "CH", "1.5", "one", "3", "three"); + expect(changed).toBe(2); + }); + + test("should increment score with INCR option in ZADD", async () => { + const redis = ctx.redis; + const key = "zadd-incr-test"; + + await redis.zadd(key, "1", "one"); + + const newScore = await redis.zadd(key, "INCR", "2.5", "one"); + expect(newScore).toBe(3.5); + + const score = await redis.zscore(key, "one"); + expect(score).toBe(3.5); + }); + + test("should handle GT option in ZADD", async () => { + const redis = ctx.redis; + const key = "zadd-gt-test"; + + await redis.zadd(key, "5", "one"); + + const updated1 = await redis.zadd(key, "GT", "3", "one"); + expect(updated1).toBe(0); + + const score1 = await redis.zscore(key, "one"); + expect(score1).toBe(5); + + const updated2 = await redis.zadd(key, "GT", "7", "one"); + expect(updated2).toBe(0); + + const score2 = await redis.zscore(key, "one"); + expect(score2).toBe(7); + }); + + test("should handle LT option in ZADD", async () => { + const redis = ctx.redis; + const key = "zadd-lt-test"; + + await redis.zadd(key, "5", "one"); + + const updated1 = await redis.zadd(key, "LT", "7", "one"); + expect(updated1).toBe(0); + + const score1 = await redis.zscore(key, "one"); + expect(score1).toBe(5); + + const updated2 = await redis.zadd(key, "LT", "3", "one"); + expect(updated2).toBe(0); + + const score2 = await redis.zscore(key, "one"); + expect(score2).toBe(3); + }); + + test("should iterate sorted set with ZSCAN", async () => { + const redis = ctx.redis; + const key = "zscan-test"; + + await redis.zadd(key, "1", "one", "2", "two", "3", "three", "4", "four", "5", "five"); + + let cursor = "0"; + const allElements: string[] = []; + do { + const [nextCursor, elements] = await redis.zscan(key, cursor); + allElements.push(...elements); + cursor = nextCursor; + } while (cursor !== "0"); + + expect(allElements.length).toBe(10); + + const members = allElements.filter((_, index) => index % 2 === 0); + expect(members).toContain("one"); + expect(members).toContain("two"); + expect(members).toContain("three"); + expect(members).toContain("four"); + expect(members).toContain("five"); + }); + + test("should iterate sorted set with ZSCAN and MATCH", async () => { + const redis = ctx.redis; + const key = "zscan-match-test"; + + await redis.zadd(key, "1", "user:1", "2", "user:2", "3", "post:1", "4", "post:2"); + + let cursor = "0"; + const userElements: string[] = []; + do { + const [nextCursor, elements] = await redis.zscan(key, cursor, "MATCH", "user:*"); + userElements.push(...elements); + cursor = nextCursor; + } while (cursor !== "0"); + + const members = userElements.filter((_, index) => index % 2 === 0); + + expect(members).toContain("user:1"); + expect(members).toContain("user:2"); + expect(members).not.toContain("post:1"); + expect(members).not.toContain("post:2"); + }); + + test("should iterate sorted set with ZSCAN and COUNT", async () => { + const redis = ctx.redis; + const key = "zscan-count-test"; + + const promises: Promise[] = []; + for (let i = 0; i < 100; i++) { + promises.push(redis.zadd(key, String(i), `member:${i}`)); + } + await Promise.all(promises); + + let cursor = "0"; + const allElements: string[] = []; + do { + const [nextCursor, elements] = await redis.zscan(key, cursor, "COUNT", "10"); + allElements.push(...elements); + cursor = nextCursor; + } while (cursor !== "0"); + + expect(allElements.length).toBe(200); + + const members = allElements.filter((_, index) => index % 2 === 0); + expect(members.length).toBe(100); + for (let i = 0; i < 100; i++) { + expect(members).toContain(`member:${i}`); + } + + cursor = 0 as any; + const allElements2: string[] = []; + do { + const [nextCursor, elements] = await redis.zscan(key, cursor, "COUNT", "10"); + allElements2.push(...elements); + cursor = nextCursor; + } while (cursor !== "0"); + + expect(allElements2.length).toBe(200); + }); + + test("should reject invalid key in ZADD", async () => { + const redis = ctx.redis; + expect(async () => { + await redis.zadd({} as any, "1", "member"); + }).toThrowErrorMatchingInlineSnapshot(`"Expected additional arguments to be a string or buffer for 'zadd'."`); + }); + + test("should reject invalid key in ZSCAN", async () => { + const redis = ctx.redis; + expect(async () => { + await redis.zscan([] as any, 0); + }).toThrowErrorMatchingInlineSnapshot(`"Expected additional arguments to be a string or buffer for 'zscan'."`); + }); + + test("should return range of members with ZRANGE", async () => { + const redis = ctx.redis; + const key = "zrange-basic-test"; + + await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three", "4", "four", "5", "five"]); + + const all = await redis.zrange(key, 0, -1); + expect(all).toEqual(["one", "two", "three", "four", "five"]); + + const first3 = await redis.zrange(key, 0, 2); + expect(first3).toEqual(["one", "two", "three"]); + + const last2 = await redis.zrange(key, -2, -1); + expect(last2).toEqual(["four", "five"]); + }); + + test("should return members with scores using WITHSCORES option in ZRANGE", async () => { + const redis = ctx.redis; + const key = "zrange-withscores-test"; + + await redis.send("ZADD", [key, "1", "one", "2.5", "two", "3", "three"]); + + const result = await redis.zrange(key, 0, -1, "WITHSCORES"); + expect(result).toEqual([ + ["one", 1], + ["two", 2.5], + ["three", 3], + ]); + }); + + test("should return members by score range with BYSCORE option in ZRANGE", async () => { + const redis = ctx.redis; + const key = "zrange-byscore-test"; + + await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three", "4", "four", "5", "five"]); + + const range1 = await redis.zrange(key, "2", "4", "BYSCORE"); + expect(range1).toEqual(["two", "three", "four"]); + + const range2 = await redis.zrange(key, "(2", "4", "BYSCORE"); + expect(range2).toEqual(["three", "four"]); + + const all = await redis.zrange(key, "-inf", "+inf", "BYSCORE"); + expect(all).toEqual(["one", "two", "three", "four", "five"]); + }); + + test("should return members in reverse order with REV option in ZRANGE", async () => { + const redis = ctx.redis; + const key = "zrange-rev-test"; + + await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three"]); + + const reversed = await redis.zrange(key, 0, -1, "REV"); + expect(reversed).toEqual(["three", "two", "one"]); + + const top2 = await redis.zrange(key, 0, 1, "REV"); + expect(top2).toEqual(["three", "two"]); + }); + + test("should support LIMIT option with BYSCORE in ZRANGE", async () => { + const redis = ctx.redis; + const key = "zrange-limit-test"; + + await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three", "4", "four", "5", "five"]); + + const result = await redis.zrange(key, "1", "5", "BYSCORE", "LIMIT", "1", "2"); + expect(result).toEqual(["two", "three"]); + }); + + test("should return members by lexicographical range with BYLEX option in ZRANGE", async () => { + const redis = ctx.redis; + const key = "zrange-bylex-test"; + + await redis.send("ZADD", [key, "0", "apple", "0", "banana", "0", "cherry", "0", "date"]); + + const range1 = await redis.zrange(key, "[banana", "[cherry", "BYLEX"); + expect(range1).toEqual(["banana", "cherry"]); + + const range2 = await redis.zrange(key, "(banana", "(date", "BYLEX"); + expect(range2).toEqual(["cherry"]); + }); + + test("should return members in reverse order with ZREVRANGE", async () => { + const redis = ctx.redis; + const key = "zrevrange-test"; + + await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three", "4", "four", "5", "five"]); + + const all = await redis.zrevrange(key, 0, -1); + expect(all).toEqual(["five", "four", "three", "two", "one"]); + + const top3 = await redis.zrevrange(key, 0, 2); + expect(top3).toEqual(["five", "four", "three"]); + + const last2 = await redis.zrevrange(key, -2, -1); + expect(last2).toEqual(["two", "one"]); + }); + + test("should return members with scores using WITHSCORES option in ZREVRANGE", async () => { + const redis = ctx.redis; + const key = "zrevrange-withscores-test"; + + await redis.send("ZADD", [key, "1.5", "one", "2", "two", "3.7", "three"]); + + const result = await redis.zrevrange(key, 0, -1, "WITHSCORES"); + expect(result).toEqual([ + ["three", 3.7], + ["two", 2], + ["one", 1.5], + ]); + }); + + test("should handle empty sorted set with ZRANGE", async () => { + const redis = ctx.redis; + const key = "zrange-empty-test"; + + const result = await redis.zrange(key, 0, -1); + expect(result).toEqual([]); + }); + + test("should handle empty sorted set with ZREVRANGE", async () => { + const redis = ctx.redis; + const key = "zrevrange-empty-test"; + + const result = await redis.zrevrange(key, 0, -1); + expect(result).toEqual([]); + }); + + test("should reject invalid key in ZRANGE", async () => { + const redis = ctx.redis; + expect(async () => { + await redis.zrange({} as any, 0, 10); + }).toThrowErrorMatchingInlineSnapshot(`"Expected additional arguments to be a string or buffer for 'zrange'."`); + }); + + test("should reject invalid key in ZREVRANGE", async () => { + const redis = ctx.redis; + expect(async () => { + await redis.zrevrange([] as any, 0, 10); + }).toThrowErrorMatchingInlineSnapshot( + `"Expected additional arguments to be a string or buffer for 'zrevrange'."`, + ); + }); + test("should return members by score range with ZRANGEBYSCORE", async () => { + const redis = ctx.redis; + const key = "zrangebyscore-test"; + + await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three", "4", "four", "5", "five"]); + + const all = await redis.zrangebyscore(key, "-inf", "+inf"); + expect(all).toEqual(["one", "two", "three", "four", "five"]); + + const range1 = await redis.zrangebyscore(key, 2, 4); + expect(range1).toEqual(["two", "three", "four"]); + + const range2 = await redis.zrangebyscore(key, "(2", 4); + expect(range2).toEqual(["three", "four"]); + + const range3 = await redis.zrangebyscore(key, 2, "(4"); + expect(range3).toEqual(["two", "three"]); + + const range4 = await redis.zrangebyscore(key, "(2", "(4"); + expect(range4).toEqual(["three"]); + }); + + test("should support WITHSCORES option with ZRANGEBYSCORE", async () => { + const redis = ctx.redis; + const key = "zrangebyscore-withscores-test"; + + await redis.send("ZADD", [key, "1.5", "one", "2.7", "two", "3.9", "three"]); + + const result = await redis.zrangebyscore(key, 1, 3, "WITHSCORES"); + expect(result).toEqual([ + ["one", 1.5], + ["two", 2.7], + ]); + }); + + test("should support LIMIT option with ZRANGEBYSCORE", async () => { + const redis = ctx.redis; + const key = "zrangebyscore-limit-test"; + + await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three", "4", "four", "5", "five"]); + + const limited1 = await redis.zrangebyscore(key, "-inf", "+inf", "LIMIT", 0, 2); + expect(limited1).toEqual(["one", "two"]); + + const limited2 = await redis.zrangebyscore(key, "-inf", "+inf", "LIMIT", 1, 2); + expect(limited2).toEqual(["two", "three"]); + + const limited3 = await redis.zrangebyscore(key, 2, 5, "LIMIT", 1, 2); + expect(limited3).toEqual(["three", "four"]); + }); + + test("should support WITHSCORES with ZRANGEBYSCORE", async () => { + const redis = ctx.redis; + const key = "zrangebyscore-withscores-only-test"; + + await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three", "4", "four"]); + + const result = await redis.zrangebyscore(key, "-inf", "+inf", "WITHSCORES"); + expect(result).toEqual([ + ["one", 1], + ["two", 2], + ["three", 3], + ["four", 4], + ]); + }); + + test("should support LIMIT and WITHSCORES together with ZRANGEBYSCORE", async () => { + const redis = ctx.redis; + const key = "zrangebyscore-combined-test"; + + await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three", "4", "four"]); + + const result = await redis.zrangebyscore(key, "-inf", "+inf", "WITHSCORES", "LIMIT", 1, 2); + expect(result).toEqual([ + ["two", 2], + ["three", 3], + ]); + }); + + test("should return members by score range in reverse with ZREVRANGEBYSCORE", async () => { + const redis = ctx.redis; + const key = "zrevrangebyscore-test"; + + await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three", "4", "four", "5", "five"]); + + const all = await redis.zrevrangebyscore(key, "+inf", "-inf"); + expect(all).toEqual(["five", "four", "three", "two", "one"]); + + const range1 = await redis.zrevrangebyscore(key, 4, 2); + expect(range1).toEqual(["four", "three", "two"]); + + const range2 = await redis.zrevrangebyscore(key, "(4", "(2"); + expect(range2).toEqual(["three"]); + + const range3 = await redis.zrevrangebyscore(key, 4, "(2"); + expect(range3).toEqual(["four", "three"]); + }); + + test("should support WITHSCORES option with ZREVRANGEBYSCORE", async () => { + const redis = ctx.redis; + const key = "zrevrangebyscore-withscores-test"; + + await redis.send("ZADD", [key, "1.5", "one", "2.7", "two", "3.9", "three"]); + + const result = await redis.zrevrangebyscore(key, 3, 1, "WITHSCORES"); + expect(result).toEqual([ + ["two", 2.7], + ["one", 1.5], + ]); + }); + + test("should support LIMIT option with ZREVRANGEBYSCORE", async () => { + const redis = ctx.redis; + const key = "zrevrangebyscore-limit-test"; + + await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three", "4", "four", "5", "five"]); + + const limited1 = await redis.zrevrangebyscore(key, "+inf", "-inf", "LIMIT", 0, 2); + expect(limited1).toEqual(["five", "four"]); + + const limited2 = await redis.zrevrangebyscore(key, "+inf", "-inf", "LIMIT", 1, 2); + expect(limited2).toEqual(["four", "three"]); + }); + + test("should return members by lexicographical range with ZRANGEBYLEX", async () => { + const redis = ctx.redis; + const key = "zrangebylex-test"; + + await redis.send("ZADD", [key, "0", "apple", "0", "banana", "0", "cherry", "0", "date", "0", "elderberry"]); + + const all = await redis.zrangebylex(key, "-", "+"); + expect(all).toEqual(["apple", "banana", "cherry", "date", "elderberry"]); + + const range1 = await redis.zrangebylex(key, "[banana", "[date"); + expect(range1).toEqual(["banana", "cherry", "date"]); + + const range2 = await redis.zrangebylex(key, "(banana", "(date"); + expect(range2).toEqual(["cherry"]); + + const range3 = await redis.zrangebylex(key, "[banana", "(date"); + expect(range3).toEqual(["banana", "cherry"]); + + const range4 = await redis.zrangebylex(key, "-", "[cherry"); + expect(range4).toEqual(["apple", "banana", "cherry"]); + + const range5 = await redis.zrangebylex(key, "[cherry", "+"); + expect(range5).toEqual(["cherry", "date", "elderberry"]); + }); + + test("should support LIMIT option with ZRANGEBYLEX", async () => { + const redis = ctx.redis; + const key = "zrangebylex-limit-test"; + + await redis.send("ZADD", [key, "0", "a", "0", "b", "0", "c", "0", "d", "0", "e", "0", "f", "0", "g"]); + + const limited1 = await redis.zrangebylex(key, "-", "+", "LIMIT", 0, 3); + expect(limited1).toEqual(["a", "b", "c"]); + + const limited2 = await redis.zrangebylex(key, "-", "+", "LIMIT", 2, 3); + expect(limited2).toEqual(["c", "d", "e"]); + + const limited3 = await redis.zrangebylex(key, "-", "+", "LIMIT", 5, 10); + expect(limited3).toEqual(["f", "g"]); + }); + + test("should get cardinality of sorted set with ZCARD", async () => { + const redis = ctx.redis; + const key = "zcard-test"; + + const count0 = await redis.zcard(key); + expect(count0).toBe(0); + + await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three"]); + + const count1 = await redis.zcard(key); + expect(count1).toBe(3); + + await redis.send("ZADD", [key, "4", "four", "5", "five"]); + + const count2 = await redis.zcard(key); + expect(count2).toBe(5); + }); + + test("should pop member with lowest score using ZPOPMIN", async () => { + const redis = ctx.redis; + const key = "zpopmin-test"; + + await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three", "4", "four", "5", "five"]); + + const result1 = await redis.zpopmin(key); + expect(result1).toEqual(["one", 1]); + + const result2 = await redis.zpopmin(key); + expect(result2).toEqual(["two", 2]); + + const count = await redis.send("ZCARD", [key]); + expect(count).toBe(3); + }); + + test("should pop multiple members with ZPOPMIN using COUNT", async () => { + const redis = ctx.redis; + const key = "zpopmin-count-test"; + + await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three", "4", "four", "5", "five"]); + + const result = await redis.zpopmin(key, 3); + expect(result).toEqual([ + ["one", 1], + ["two", 2], + ["three", 3], + ]); + + const count = await redis.send("ZCARD", [key]); + expect(count).toBe(2); + }); + + test("should return empty array when ZPOPMIN on empty set", async () => { + const redis = ctx.redis; + const emptyKey = "zpopmin-empty-test"; + + const result = await redis.zpopmin(emptyKey); + expect(result).toEqual([]); + }); + + test("should return empty array when ZPOPMIN on non-existent key", async () => { + const redis = ctx.redis; + const nonExistentKey = "zpopmin-nonexistent-" + randomUUIDv7(); + + const result = await redis.zpopmin(nonExistentKey); + expect(result).toEqual([]); + }); + + test("should handle ties in score with ZPOPMIN", async () => { + const redis = ctx.redis; + const key = "zpopmin-tie-test"; + + await redis.send("ZADD", [key, "1", "a", "1", "b", "1", "c", "2", "d"]); + + const result = await redis.zpopmin(key, 2); + expect(result).toEqual([ + ["a", 1], + ["b", 1], + ]); + + const count = await redis.send("ZCARD", [key]); + expect(count).toBe(2); + }); + + test("should pop member with highest score using ZPOPMAX", async () => { + const redis = ctx.redis; + const key = "zpopmax-test"; + + await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three", "4", "four", "5", "five"]); + + const result1 = await redis.zpopmax(key); + expect(result1).toEqual(["five", 5]); + + const result2 = await redis.zpopmax(key); + expect(result2).toEqual(["four", 4]); + + const count = await redis.send("ZCARD", [key]); + expect(count).toBe(3); + }); + + test("should pop multiple members with ZPOPMAX using COUNT", async () => { + const redis = ctx.redis; + const key = "zpopmax-count-test"; + + await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three", "4", "four", "5", "five"]); + + const result = await redis.zpopmax(key, 3); + expect(result).toEqual([ + ["five", 5], + ["four", 4], + ["three", 3], + ]); + + const count = await redis.send("ZCARD", [key]); + expect(count).toBe(2); + }); + + test("should return empty array when ZPOPMAX on empty set", async () => { + const redis = ctx.redis; + const emptyKey = "zpopmax-empty-test"; + + const result = await redis.zpopmax(emptyKey); + expect(result).toEqual([]); + }); + + test("should return empty array when ZPOPMAX on non-existent key", async () => { + const redis = ctx.redis; + const nonExistentKey = "zpopmax-nonexistent-" + randomUUIDv7(); + const result = await redis.zpopmax(nonExistentKey); + expect(result).toEqual([]); + }); + + test("should handle ties in score with ZPOPMAX", async () => { + const redis = ctx.redis; + const key = "zpopmax-tie-test"; + + await redis.send("ZADD", [key, "1", "a", "2", "b", "2", "c", "2", "d"]); + + const result = await redis.zpopmax(key, 2); + expect(result).toEqual([ + ["d", 2], + ["c", 2], + ]); + + const count = await redis.send("ZCARD", [key]); + expect(count).toBe(2); + }); + + test("should get random member with ZRANDMEMBER", async () => { + const redis = ctx.redis; + const key = "zrandmember-test"; + + await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three", "4", "four", "5", "five"]); + + const result = await redis.zrandmember(key); + expect(result).toBeDefined(); + expect(typeof result).toBe("string"); + expect(["one", "two", "three", "four", "five"]).toContain(result); + }); + + test("should return null when ZRANDMEMBER on empty set", async () => { + const redis = ctx.redis; + const emptyKey = "zrandmember-empty-test"; + + const result = await redis.zrandmember(emptyKey); + expect(result).toBeNull(); + }); + + test("should get multiple random members with ZRANDMEMBER", async () => { + const redis = ctx.redis; + const key = "zrandmember-count-test"; + + await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three"]); + + const result = await redis.zrandmember(key, 2); + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + expect(result!.length).toBe(2); + + for (const member of result!) { + expect(["one", "two", "three"]).toContain(member); + } + + const count = await redis.send("ZCARD", [key]); + expect(count).toBe(3); + }); + + test("should get random members with scores using WITHSCORES in ZRANDMEMBER", async () => { + const redis = ctx.redis; + const key = "zrandmember-withscores-test"; + + await redis.send("ZADD", [key, "1.5", "one", "2.7", "two", "3.9", "three"]); + + const result = await redis.zrandmember(key, 2, "WITHSCORES"); + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + expect(result!.length).toBe(2); + + for (const item of result!) { + expect(Array.isArray(item)).toBe(true); + expect(item.length).toBe(2); + expect(typeof item[0]).toBe("string"); + expect(typeof item[1]).toBe("number"); + expect(["one", "two", "three"]).toContain(item[0]); + } + }); + + test("should allow negative count for ZRANDMEMBER to allow duplicates", async () => { + const redis = ctx.redis; + const key = "zrandmember-negative-test"; + + await redis.send("ZADD", [key, "1", "one", "2", "two"]); + + const result = await redis.zrandmember(key, -5); + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + expect(result!.length).toBe(5); + + for (const member of result!) { + expect(["one", "two"]).toContain(member); + } + }); + + test("should get rank of member with ZRANK", async () => { + const redis = ctx.redis; + const key = "zrank-test"; + + await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three", "4", "four", "5", "five"]); + + const rank1 = await redis.zrank(key, "one"); + expect(rank1).toBe(0); + + const rank2 = await redis.zrank(key, "three"); + expect(rank2).toBe(2); + + const rank3 = await redis.zrank(key, "five"); + expect(rank3).toBe(4); + + const rank4 = await redis.zrank(key, "nonexistent"); + expect(rank4).toBeNull(); + }); + + test("should get rank with score using WITHSCORE in ZRANK", async () => { + const redis = ctx.redis; + const key = "zrank-withscore-test"; + + await redis.send("ZADD", [key, "1.5", "one", "2.7", "two", "3.9", "three"]); + + const result = await redis.zrank(key, "two", "WITHSCORE"); + expect(result).toEqual([1, 2.7]); + + const result2 = await redis.zrank(key, "one", "WITHSCORE"); + expect(result2).toEqual([0, 1.5]); + + const result3 = await redis.zrank(key, "nonexistent", "WITHSCORE"); + expect(result3).toBeNull(); + }); + + test("should handle ties in score with ZRANK", async () => { + const redis = ctx.redis; + const key = "zrank-tie-test"; + + await redis.send("ZADD", [key, "1", "a", "1", "b", "1", "c", "2", "d"]); + + const rankA = await redis.zrank(key, "a"); + expect(rankA).toBe(0); + + const rankB = await redis.zrank(key, "b"); + expect(rankB).toBe(1); + + const rankC = await redis.zrank(key, "c"); + expect(rankC).toBe(2); + + const rankD = await redis.zrank(key, "d"); + expect(rankD).toBe(3); + }); + + test("should get reverse rank of member with ZREVRANK", async () => { + const redis = ctx.redis; + const key = "zrevrank-test"; + + await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three", "4", "four", "5", "five"]); + + const rank1 = await redis.zrevrank(key, "five"); + expect(rank1).toBe(0); + + const rank2 = await redis.zrevrank(key, "three"); + expect(rank2).toBe(2); + + const rank3 = await redis.zrevrank(key, "one"); + expect(rank3).toBe(4); + + const rank4 = await redis.zrevrank(key, "nonexistent"); + expect(rank4).toBeNull(); + }); + + test("should get reverse rank with score using WITHSCORE in ZREVRANK", async () => { + const redis = ctx.redis; + const key = "zrevrank-withscore-test"; + + await redis.send("ZADD", [key, "1.5", "one", "2.7", "two", "3.9", "three"]); + + const result = await redis.zrevrank(key, "two", "WITHSCORE"); + expect(result).toEqual([1, 2.7]); + + const result2 = await redis.zrevrank(key, "three", "WITHSCORE"); + expect(result2).toEqual([0, 3.9]); + + const result3 = await redis.zrevrank(key, "nonexistent", "WITHSCORE"); + expect(result3).toBeNull(); + }); + + test("should handle ties in score with ZREVRANK", async () => { + const redis = ctx.redis; + const key = "zrevrank-tie-test"; + + await redis.send("ZADD", [key, "1", "a", "2", "b", "2", "c", "2", "d"]); + + const rankD = await redis.zrevrank(key, "d"); + expect(rankD).toBe(0); + + const rankC = await redis.zrevrank(key, "c"); + expect(rankC).toBe(1); + + const rankB = await redis.zrevrank(key, "b"); + expect(rankB).toBe(2); + + const rankA = await redis.zrevrank(key, "a"); + expect(rankA).toBe(3); + }); + test("should reject invalid key in ZRANGEBYSCORE", async () => { + const redis = ctx.redis; + expect(async () => { + await redis.zrangebyscore({} as any, 0, 10); + }).toThrowErrorMatchingInlineSnapshot( + `"Expected additional arguments to be a string or buffer for 'zrangebyscore'."`, + ); + }); + + test("should reject invalid key in ZREVRANGEBYSCORE", async () => { + const redis = ctx.redis; + expect(async () => { + await redis.zrevrangebyscore([] as any, 10, 0); + }).toThrowErrorMatchingInlineSnapshot( + `"Expected additional arguments to be a string or buffer for 'zrevrangebyscore'."`, + ); + }); + + test("should reject invalid key in ZRANGEBYLEX", async () => { + const redis = ctx.redis; + expect(async () => { + await redis.zrangebylex(null as any, "-", "+"); + }).toThrowErrorMatchingInlineSnapshot(`"The "key" argument must be specified"`); + }); + + test("should return members in reverse lexicographical order with ZREVRANGEBYLEX", async () => { + const redis = ctx.redis; + const key = "zrevrangebylex-test"; + + await redis.send("ZADD", [key, "0", "apple", "0", "banana", "0", "cherry", "0", "date", "0", "elderberry"]); + + const all = await redis.zrevrangebylex(key, "+", "-"); + expect(all).toEqual(["elderberry", "date", "cherry", "banana", "apple"]); + + const range1 = await redis.zrevrangebylex(key, "[date", "[banana"); + expect(range1).toEqual(["date", "cherry", "banana"]); + + const range2 = await redis.zrevrangebylex(key, "(date", "(banana"); + expect(range2).toEqual(["cherry"]); + + const range3 = await redis.zrevrangebylex(key, "[elderberry", "(cherry"); + expect(range3).toEqual(["elderberry", "date"]); + }); + + test("should support LIMIT option with ZREVRANGEBYLEX", async () => { + const redis = ctx.redis; + const key = "zrevrangebylex-limit-test"; + + await redis.send("ZADD", [key, "0", "a", "0", "b", "0", "c", "0", "d", "0", "e", "0", "f", "0", "g"]); + + const limited1 = await redis.zrevrangebylex(key, "+", "-", "LIMIT", "0", "3"); + expect(limited1).toEqual(["g", "f", "e"]); + + const limited2 = await redis.zrevrangebylex(key, "+", "-", "LIMIT", "2", "3"); + expect(limited2).toEqual(["e", "d", "c"]); + + const limited3 = await redis.zrevrangebylex(key, "+", "-", "LIMIT", "5", "10"); + expect(limited3).toEqual(["b", "a"]); + }); + + test("should store range of members with ZRANGESTORE", async () => { + const redis = ctx.redis; + const source = "zrangestore-source"; + const dest = "zrangestore-dest"; + + await redis.send("ZADD", [source, "1", "one", "2", "two", "3", "three", "4", "four", "5", "five"]); + + const count1 = await redis.zrangestore(dest, source, 1, 3); + expect(count1).toBe(3); + + const stored = await redis.send("ZRANGE", [dest, "0", "-1"]); + expect(stored).toEqual(["two", "three", "four"]); + }); + + test("should store range with BYSCORE option in ZRANGESTORE", async () => { + const redis = ctx.redis; + const source = "zrangestore-byscore-source"; + const dest = "zrangestore-byscore-dest"; + + await redis.send("ZADD", [source, "1", "one", "2", "two", "3", "three", "4", "four", "5", "five"]); + + const count = await redis.zrangestore(dest, source, "2", "4", "BYSCORE"); + expect(count).toBe(3); + + const stored = await redis.send("ZRANGE", [dest, "0", "-1", "WITHSCORES"]); + expect(stored).toEqual([ + ["two", 2], + ["three", 3], + ["four", 4], + ]); + }); + + test("should store range in reverse order with REV option in ZRANGESTORE", async () => { + const redis = ctx.redis; + const source = "zrangestore-rev-source"; + const dest = "zrangestore-rev-dest"; + + await redis.send("ZADD", [source, "1", "one", "2", "two", "3", "three"]); + + const count = await redis.zrangestore(dest, source, "0", "-1", "REV"); + expect(count).toBe(3); + + const stored = await redis.send("ZRANGE", [dest, "0", "-1"]); + expect(stored).toEqual(["one", "two", "three"]); + }); + + test("should support LIMIT option with ZRANGESTORE", async () => { + const redis = ctx.redis; + const source = "zrangestore-limit-source"; + const dest = "zrangestore-limit-dest"; + + await redis.send("ZADD", [source, "1", "one", "2", "two", "3", "three", "4", "four", "5", "five"]); + + const count = await redis.zrangestore(dest, source, "-inf", "+inf", "BYSCORE", "LIMIT", "1", "2"); + expect(count).toBe(2); + + const stored = await redis.send("ZRANGE", [dest, "0", "-1"]); + expect(stored).toEqual(["two", "three"]); + }); + + test("should reject invalid key in ZREVRANGEBYLEX", async () => { + const redis = ctx.redis; + expect(async () => { + await redis.zrevrangebylex({} as any, "+", "-"); + }).toThrowErrorMatchingInlineSnapshot( + `"Expected additional arguments to be a string or buffer for 'zrevrangebylex'."`, + ); + }); + + test("should reject invalid destination in ZRANGESTORE", async () => { + const redis = ctx.redis; + expect(async () => { + await redis.zrangestore([] as any, "source", 0, 10); + }).toThrowErrorMatchingInlineSnapshot( + `"Expected additional arguments to be a string or buffer for 'zrangestore'."`, + ); + }); + + test("should reject invalid source in ZRANGESTORE", async () => { + const redis = ctx.redis; + expect(async () => { + await redis.zrangestore("dest", null as any, 0, 10); + }).toThrowErrorMatchingInlineSnapshot( + `"Expected additional arguments to be a string or buffer for 'zrangestore'."`, + ); + }); + + test("should compute intersection with ZINTER", async () => { + const redis = ctx.redis; + const key1 = "zinter-test-1"; + const key2 = "zinter-test-2"; + + await redis.zadd(key1, "1", "a", "2", "b", "3", "c"); + await redis.zadd(key2, "1", "b", "2", "c", "3", "d"); + + const result1 = await redis.zinter(2, key1, key2); + expect(result1).toEqual(["b", "c"]); + + const result2 = await redis.zinter(2, key1, key2, "WITHSCORES"); + expect(result2).toEqual([ + ["b", 3], + ["c", 5], + ]); + }); + + test("should compute intersection with WEIGHTS in ZINTER", async () => { + const redis = ctx.redis; + const key1 = "zinter-weights-1"; + const key2 = "zinter-weights-2"; + + await redis.zadd(key1, "1", "a", "2", "b", "3", "c"); + await redis.zadd(key2, "1", "b", "2", "c", "3", "d"); + + const result = await redis.zinter(2, key1, key2, "WEIGHTS", "2", "3", "WITHSCORES"); + expect(result).toEqual([ + ["b", 7], + ["c", 12], + ]); + }); + + test("should compute intersection with AGGREGATE in ZINTER", async () => { + const redis = ctx.redis; + const key1 = "zinter-agg-1"; + const key2 = "zinter-agg-2"; + + await redis.zadd(key1, "1", "a", "2", "b", "3", "c"); + await redis.zadd(key2, "1", "b", "2", "c", "3", "d"); + + const result1 = await redis.zinter(2, key1, key2, "AGGREGATE", "MIN", "WITHSCORES"); + expect(result1).toEqual([ + ["b", 1], + ["c", 2], + ]); + + const result2 = await redis.zinter(2, key1, key2, "AGGREGATE", "MAX", "WITHSCORES"); + expect(result2).toEqual([ + ["b", 2], + ["c", 3], + ]); + }); + + test("should handle empty intersection with ZINTER", async () => { + const redis = ctx.redis; + const key1 = "zinter-empty-1"; + const key2 = "zinter-empty-2"; + + await redis.zadd(key1, "1", "a", "2", "b"); + await redis.zadd(key2, "1", "c", "2", "d"); + + const result = await redis.zinter(2, key1, key2); + expect(result).toEqual([]); + }); + + test("should store intersection with ZINTERSTORE", async () => { + const redis = ctx.redis; + const key1 = "zinterstore-test-1"; + const key2 = "zinterstore-test-2"; + const dest = "zinterstore-dest"; + + await redis.zadd(key1, "1", "a", "2", "b", "3", "c"); + await redis.zadd(key2, "1", "b", "2", "c", "3", "d"); + + const count = await redis.zinterstore(dest, 2, key1, key2); + expect(count).toBe(2); + + const stored = await redis.send("ZRANGE", [dest, "0", "-1", "WITHSCORES"]); + expect(stored).toEqual([ + ["b", 3], + ["c", 5], + ]); + }); + + test("should store intersection with WEIGHTS in ZINTERSTORE", async () => { + const redis = ctx.redis; + const key1 = "zinterstore-weights-1"; + const key2 = "zinterstore-weights-2"; + const dest = "zinterstore-weights-dest"; + + await redis.zadd(key1, "1", "x", "2", "y"); + await redis.zadd(key2, "2", "x", "3", "y"); + + const count = await redis.zinterstore(dest, 2, key1, key2, "WEIGHTS", "2", "3"); + expect(count).toBe(2); + + const stored = await redis.send("ZRANGE", [dest, "0", "-1", "WITHSCORES"]); + expect(stored).toEqual([ + ["x", 8], + ["y", 13], + ]); + }); + + test("should store intersection with AGGREGATE in ZINTERSTORE", async () => { + const redis = ctx.redis; + const key1 = "zinterstore-agg-1"; + const key2 = "zinterstore-agg-2"; + const destMin = "zinterstore-agg-min"; + const destMax = "zinterstore-agg-max"; + + await redis.zadd(key1, "1", "m", "3", "n"); + await redis.zadd(key2, "2", "m", "1", "n"); + + const count1 = await redis.zinterstore(destMin, 2, key1, key2, "AGGREGATE", "MIN"); + expect(count1).toBe(2); + const storedMin = await redis.send("ZRANGE", [destMin, "0", "-1", "WITHSCORES"]); + expect(storedMin).toEqual([ + ["m", 1], + ["n", 1], + ]); + + const count2 = await redis.zinterstore(destMax, 2, key1, key2, "AGGREGATE", "MAX"); + expect(count2).toBe(2); + const storedMax = await redis.send("ZRANGE", [destMax, "0", "-1", "WITHSCORES"]); + expect(storedMax).toEqual([ + ["m", 2], + ["n", 3], + ]); + }); + + test("should handle empty result with ZINTERSTORE", async () => { + const redis = ctx.redis; + const key1 = "zinterstore-empty-1"; + const key2 = "zinterstore-empty-2"; + const dest = "zinterstore-empty-dest"; + + await redis.zadd(key1, "1", "a", "2", "b"); + await redis.zadd(key2, "1", "c", "2", "d"); + + const count = await redis.zinterstore(dest, 2, key1, key2); + expect(count).toBe(0); + + const exists = await redis.exists(dest); + expect(exists).toBe(false); + }); + + test("should compute union with ZUNION", async () => { + const redis = ctx.redis; + const key1 = "zunion-test-1"; + const key2 = "zunion-test-2"; + + await redis.zadd(key1, "1", "a", "2", "b", "3", "c"); + await redis.zadd(key2, "4", "b", "5", "c", "6", "d"); + + const result1 = await redis.zunion(2, key1, key2); + expect(result1).toEqual(["a", "b", "d", "c"]); + + const result2 = await redis.zunion(2, key1, key2, "WITHSCORES"); + expect(result2).toEqual([ + ["a", 1], + ["b", 6], + ["d", 6], + ["c", 8], + ]); + }); + + test("should compute union with WEIGHTS in ZUNION", async () => { + const redis = ctx.redis; + const key1 = "zunion-weights-1"; + const key2 = "zunion-weights-2"; + + await redis.zadd(key1, "1", "x", "2", "y", "3", "z"); + await redis.zadd(key2, "2", "y", "3", "z", "4", "w"); + + const result = await redis.zunion(2, key1, key2, "WEIGHTS", "2", "3", "WITHSCORES"); + expect(result).toEqual([ + ["x", 2], + ["y", 10], + ["w", 12], + ["z", 15], + ]); + }); + + test("should compute union with AGGREGATE MIN in ZUNION", async () => { + const redis = ctx.redis; + const key1 = "zunion-min-1"; + const key2 = "zunion-min-2"; + + await redis.zadd(key1, "1", "p", "3", "q"); + await redis.zadd(key2, "2", "p", "1", "q"); + + const result = await redis.zunion(2, key1, key2, "AGGREGATE", "MIN", "WITHSCORES"); + expect(result).toEqual([ + ["p", 1], + ["q", 1], + ]); + }); + + test("should compute union with AGGREGATE MAX in ZUNION", async () => { + const redis = ctx.redis; + const key1 = "zunion-max-1"; + const key2 = "zunion-max-2"; + + await redis.zadd(key1, "1", "r", "3", "s"); + await redis.zadd(key2, "2", "r", "1", "s"); + + const result = await redis.zunion(2, key1, key2, "AGGREGATE", "MAX", "WITHSCORES"); + expect(result).toEqual([ + ["r", 2], + ["s", 3], + ]); + }); + + test("should compute union with single set in ZUNION", async () => { + const redis = ctx.redis; + const key = "zunion-single"; + + await redis.zadd(key, "1", "one", "2", "two", "3", "three"); + + const result = await redis.zunion(1, key); + expect(result).toEqual(["one", "two", "three"]); + }); + + test("should compute union with three sets in ZUNION", async () => { + const redis = ctx.redis; + const key1 = "zunion-three-1"; + const key2 = "zunion-three-2"; + const key3 = "zunion-three-3"; + + await redis.zadd(key1, "1", "a", "2", "b"); + await redis.zadd(key2, "2", "b", "3", "c"); + await redis.zadd(key3, "3", "c", "4", "d"); + + const result = await redis.zunion(3, key1, key2, key3, "WITHSCORES"); + expect(result).toEqual([ + ["a", 1], + ["b", 4], + ["d", 4], + ["c", 6], + ]); + }); + + test("should handle empty set in ZUNION", async () => { + const redis = ctx.redis; + const key1 = "zunion-empty-1"; + const key2 = "zunion-empty-2"; + + await redis.zadd(key1, "1", "a", "2", "b"); + + const result = await redis.zunion(2, key1, key2); + expect(result).toEqual(["a", "b"]); + }); + + test("should store union with ZUNIONSTORE", async () => { + const redis = ctx.redis; + const key1 = "zunionstore-test-1"; + const key2 = "zunionstore-test-2"; + const dest = "zunionstore-dest"; + + await redis.zadd(key1, "1", "a", "2", "b", "3", "c"); + await redis.zadd(key2, "4", "b", "5", "c", "6", "d"); + + const count = await redis.zunionstore(dest, 2, key1, key2); + expect(count).toBe(4); + + const stored = await redis.send("ZRANGE", [dest, "0", "-1", "WITHSCORES"]); + expect(stored).toEqual([ + ["a", 1], + ["b", 6], + ["d", 6], + ["c", 8], + ]); + }); + + test("should store union with WEIGHTS in ZUNIONSTORE", async () => { + const redis = ctx.redis; + const key1 = "zunionstore-weights-1"; + const key2 = "zunionstore-weights-2"; + const dest = "zunionstore-weights-dest"; + + await redis.zadd(key1, "1", "x", "2", "y"); + await redis.zadd(key2, "2", "x", "3", "y"); + + const count = await redis.zunionstore(dest, 2, key1, key2, "WEIGHTS", "2", "3"); + expect(count).toBe(2); + + const stored = await redis.send("ZRANGE", [dest, "0", "-1", "WITHSCORES"]); + expect(stored).toEqual([ + ["x", 8], + ["y", 13], + ]); + }); + + test("should store union with AGGREGATE MIN in ZUNIONSTORE", async () => { + const redis = ctx.redis; + const key1 = "zunionstore-agg-min-1"; + const key2 = "zunionstore-agg-min-2"; + const dest = "zunionstore-agg-min-dest"; + + await redis.zadd(key1, "1", "m", "3", "n"); + await redis.zadd(key2, "2", "m", "1", "n"); + + const count = await redis.zunionstore(dest, 2, key1, key2, "AGGREGATE", "MIN"); + expect(count).toBe(2); + const stored = await redis.send("ZRANGE", [dest, "0", "-1", "WITHSCORES"]); + expect(stored).toEqual([ + ["m", 1], + ["n", 1], + ]); + }); + + test("should store union with AGGREGATE MAX in ZUNIONSTORE", async () => { + const redis = ctx.redis; + const key1 = "zunionstore-agg-max-1"; + const key2 = "zunionstore-agg-max-2"; + const dest = "zunionstore-agg-max-dest"; + + await redis.zadd(key1, "1", "m", "3", "n"); + await redis.zadd(key2, "2", "m", "1", "n"); + + const count = await redis.zunionstore(dest, 2, key1, key2, "AGGREGATE", "MAX"); + expect(count).toBe(2); + const stored = await redis.send("ZRANGE", [dest, "0", "-1", "WITHSCORES"]); + expect(stored).toEqual([ + ["m", 2], + ["n", 3], + ]); + }); + + test("should overwrite existing destination with ZUNIONSTORE", async () => { + const redis = ctx.redis; + const key1 = "zunionstore-overwrite-1"; + const key2 = "zunionstore-overwrite-2"; + const dest = "zunionstore-overwrite-dest"; + + await redis.zadd(dest, "100", "old"); + + await redis.zadd(key1, "1", "a", "2", "b"); + await redis.zadd(key2, "3", "c"); + + const count = await redis.zunionstore(dest, 2, key1, key2); + expect(count).toBe(3); + + const stored = await redis.send("ZRANGE", [dest, "0", "-1"]); + expect(stored).toEqual(["a", "b", "c"]); + expect(stored).not.toContain("old"); + }); + + test("should handle empty sets with ZUNIONSTORE", async () => { + const redis = ctx.redis; + const key1 = "zunionstore-empty-1"; + const key2 = "zunionstore-empty-2"; + const dest = "zunionstore-empty-dest"; + + const count = await redis.zunionstore(dest, 2, key1, key2); + expect(count).toBe(0); + + const exists = await redis.exists(dest); + expect(exists).toBe(false); + }); + + test("should reject invalid numkeys in ZUNION", async () => { + const redis = ctx.redis; + expect(async () => { + await redis.zunion(-1, "key1"); + }).toThrowErrorMatchingInlineSnapshot(`"ERR at least 1 input key is needed for 'zunion' command"`); + }); + + test("should reject invalid key in ZUNION", async () => { + const redis = ctx.redis; + expect(async () => { + await redis.zunion(1, {} as any); + }).toThrowErrorMatchingInlineSnapshot(`"Expected additional arguments to be a string or buffer for 'zunion'."`); + }); + + test("should reject invalid destination in ZUNIONSTORE", async () => { + const redis = ctx.redis; + expect(async () => { + await redis.zunionstore([] as any, 2, "key1", "key2"); + }).toThrowErrorMatchingInlineSnapshot( + `"Expected additional arguments to be a string or buffer for 'zunionstore'."`, + ); + }); + + test("should reject invalid source key in ZUNIONSTORE", async () => { + const redis = ctx.redis; + expect(async () => { + await redis.zunionstore("dest", 2, "key1", null as any); + }).toThrowErrorMatchingInlineSnapshot( + `"Expected additional arguments to be a string or buffer for 'zunionstore'."`, + ); + }); + + test("should pop members with MIN option using ZMPOP", async () => { + const redis = ctx.redis; + const key = "zmpop-min-test"; + + await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three", "4", "four", "5", "five"]); + + const result1 = await redis.zmpop(1, key, "MIN"); + expect(result1).toEqual([key, [["one", 1]]]); + + const count = await redis.send("ZCARD", [key]); + expect(count).toBe(4); + }); + + test("should pop members with MAX option using ZMPOP", async () => { + const redis = ctx.redis; + const key = "zmpop-max-test"; + + await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three", "4", "four", "5", "five"]); + + const result1 = await redis.zmpop(1, key, "MAX"); + expect(result1).toEqual([key, [["five", 5]]]); + + const count = await redis.send("ZCARD", [key]); + expect(count).toBe(4); + }); + + test("should pop multiple members with COUNT option using ZMPOP", async () => { + const redis = ctx.redis; + const key = "zmpop-count-test"; + + await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three", "4", "four", "5", "five"]); + + const result = await redis.zmpop(1, key, "MIN", "COUNT", 3); + expect(result).toBeDefined(); + expect(result).not.toBeNull(); + expect(result![0]).toBe(key); + expect(result![1]).toEqual([ + ["one", 1], + ["two", 2], + ["three", 3], + ]); + + const count = await redis.send("ZCARD", [key]); + expect(count).toBe(2); + }); + + test("should return null when ZMPOP on empty set", async () => { + const redis = ctx.redis; + const emptyKey = "zmpop-empty-test"; + + const result = await redis.zmpop(1, emptyKey, "MIN"); + expect(result).toBeNull(); + }); + + test("should pop from first non-empty set with ZMPOP", async () => { + const redis = ctx.redis; + const key1 = "zmpop-multi-test1"; + const key2 = "zmpop-multi-test2"; + const key3 = "zmpop-multi-test3"; + + await redis.send("ZADD", [key2, "1", "one", "2", "two"]); + + const result = await redis.zmpop(3, key1, key2, key3, "MIN"); + expect(result).toEqual([key2, [["one", 1]]]); + }); + + test("should block and pop with BZMPOP", async () => { + const redis = ctx.redis; + const key = "bzmpop-test"; + + await redis.send("ZADD", [key, "1", "one", "2", "two"]); + + const result = await redis.bzmpop(0.1, 1, key, "MIN"); + expect(result).toEqual([key, [["one", 1]]]); + + const count = await redis.send("ZCARD", [key]); + expect(count).toBe(1); + }); + + test("should timeout with BZMPOP on empty set", async () => { + const redis = ctx.redis; + const emptyKey = "bzmpop-timeout-test"; + + const result = await redis.bzmpop(0.1, 1, emptyKey, "MIN"); + expect(result).toBeNull(); + }); + + test("should block and pop multiple members with BZMPOP COUNT", async () => { + const redis = ctx.redis; + const key = "bzmpop-count-test"; + + await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three"]); + + const result = await redis.bzmpop(0.5, 1, key, "MAX", "COUNT", 2); + expect(result).toBeDefined(); + expect(result).not.toBeNull(); + expect(result![0]).toBe(key); + expect(result![1]).toEqual([ + ["three", 3], + ["two", 2], + ]); + + const count = await redis.send("ZCARD", [key]); + expect(count).toBe(1); + }); + + test("should reject invalid arguments in ZMPOP", async () => { + const redis = ctx.redis; + expect(async () => { + await redis.zmpop({} as any, "key1", "MIN"); + }).toThrowErrorMatchingInlineSnapshot(`"Expected additional arguments to be a string or buffer for 'zmpop'."`); + }); + + test("should reject invalid arguments in BZMPOP", async () => { + const redis = ctx.redis; + expect(async () => { + await redis.bzmpop(1, {} as any, "key1", "MIN"); + }).toThrowErrorMatchingInlineSnapshot(`"Expected additional arguments to be a string or buffer for 'bzmpop'."`); + }); + + test("should pop member with highest score using ZPOPMAX", async () => { + const redis = ctx.redis; + const key = "zpopmax-test"; + + await redis.zadd(key, 1.0, "one", 2.0, "two", 3.0, "three"); + + const result = await redis.zpopmax(key); + expect(result).toBeDefined(); + expect(result).not.toBeNull(); + expect(Array.isArray(result)).toBe(true); + expect(result).toHaveLength(2); + expect(result![0]).toBe("three"); + expect(result![1]).toBe(3); + + const count = await redis.zcard(key); + expect(count).toBe(2); + }); + + test("should pop member with lowest score using ZPOPMIN", async () => { + const redis = ctx.redis; + const key = "zpopmin-test"; + + await redis.zadd(key, 1.0, "one", 2.0, "two", 3.0, "three"); + + const result = await redis.zpopmin(key); + expect(result).toBeDefined(); + expect(result).not.toBeNull(); + expect(Array.isArray(result)).toBe(true); + expect(result).toHaveLength(2); + expect(result![0]).toBe("one"); + expect(result![1]).toBe(1); + + const count = await redis.zcard(key); + expect(count).toBe(2); + }); + + test("should return empty array for ZPOPMAX on empty set", async () => { + const redis = ctx.redis; + const key = "zpopmax-empty-test"; + + const result = await redis.zpopmax(key); + expect(result).toEqual([]); + }); + + test("should return empty array for ZPOPMIN on empty set", async () => { + const redis = ctx.redis; + const key = "zpopmin-empty-test"; + + const result = await redis.zpopmin(key); + expect(result).toEqual([]); + }); + + test("should block and pop lowest score with BZPOPMIN", async () => { + const redis = ctx.redis; + const key = "bzpopmin-test"; + + await redis.send("ZADD", [key, "1.0", "one", "2.0", "two", "3.0", "three"]); + + const result = await redis.bzpopmin(key, 0.1); + expect(result).toBeDefined(); + expect(result).toHaveLength(3); + expect(result![0]).toBe(key); + expect(result![1]).toBe("one"); + expect(result![2]).toBe(1); + + const count = await redis.send("ZCARD", [key]); + expect(count).toBe(2); + }); + + test("should timeout with BZPOPMIN when no elements available", async () => { + const redis = ctx.redis; + const key = "bzpopmin-empty-test"; + + const result = await redis.bzpopmin(key, 0.1); + expect(result).toBeNull(); + }); + + test("should block and pop highest score with BZPOPMAX", async () => { + const redis = ctx.redis; + const key = "bzpopmax-test"; + + await redis.send("ZADD", [key, "1.0", "one", "2.0", "two", "3.0", "three"]); + + const result = await redis.bzpopmax(key, 0.1); + expect(result).toBeDefined(); + expect(result).toHaveLength(3); + expect(result![0]).toBe(key); + expect(result![1]).toBe("three"); + expect(result![2]).toBe(3); + + const count = await redis.send("ZCARD", [key]); + expect(count).toBe(2); + }); + + test("should timeout with BZPOPMAX when no elements available", async () => { + const redis = ctx.redis; + const key = "bzpopmax-empty-test"; + + const result = await redis.bzpopmax(key, 0.1); + expect(result).toBeNull(); + }); + + test("should work with multiple keys in BZPOPMIN", async () => { + const redis = ctx.redis; + const key1 = "bzpopmin-multi-1"; + const key2 = "bzpopmin-multi-2"; + + await redis.send("ZADD", [key2, "5.0", "five", "6.0", "six"]); + + const result = await redis.bzpopmin(key1, key2, 0.1); + expect(result).toBeDefined(); + expect(result![0]).toBe(key2); + expect(result![1]).toBe("five"); + expect(result![2]).toBe(5); + }); + + test("should work with multiple keys in BZPOPMAX", async () => { + const redis = ctx.redis; + const key1 = "bzpopmax-multi-1"; + const key2 = "bzpopmax-multi-2"; + + await redis.send("ZADD", [key2, "5.0", "five", "6.0", "six"]); + + const result = await redis.bzpopmax(key1, key2, 0.5); + expect(result).toBeDefined(); + expect(result![0]).toBe(key2); + expect(result![1]).toBe("six"); + expect(result![2]).toBe(6); + }); + + test("should reject invalid arguments in BZPOPMIN", async () => { + const redis = ctx.redis; + expect(async () => { + await redis.bzpopmin({} as any, 1); + }).toThrowErrorMatchingInlineSnapshot( + `"Expected additional arguments to be a string or buffer for 'bzpopmin'."`, + ); + }); + + test("should reject invalid arguments in BZPOPMAX", async () => { + const redis = ctx.redis; + expect(async () => { + await redis.bzpopmax([] as any, 1); + }).toThrowErrorMatchingInlineSnapshot( + `"Expected additional arguments to be a string or buffer for 'bzpopmax'."`, + ); + }); + }); + + describe("Hash Operations", () => { + test("should increment hash field by integer with HINCRBY", async () => { + const redis = ctx.redis; + const key = "hincrby-test"; + + const val1 = await redis.hincrby(key, "field1", 5); + expect(val1).toBe(5); + + const val2 = await redis.hincrby(key, "field1", 3); + expect(val2).toBe(8); + + const val3 = await redis.hincrby(key, "field1", -2); + expect(val3).toBe(6); + }); + + test("should increment hash field by float with HINCRBYFLOAT", async () => { + const redis = ctx.redis; + const key = "hincrbyfloat-test"; + + const val1 = await redis.hincrbyfloat(key, "field1", 2.5); + expect(val1).toBe("2.5"); + + const val2 = await redis.hincrbyfloat(key, "field1", 1.3); + expect(Number.parseFloat(val2)).toBeCloseTo(3.8); + + const val3 = await redis.hincrbyfloat(key, "field1", -0.8); + expect(Number.parseFloat(val3)).toBeCloseTo(3.0); + }); + + test("should get all hash keys with HKEYS", async () => { + const redis = ctx.redis; + const key = "hkeys-test"; + + const keys1 = await redis.hkeys(key); + expect(keys1).toEqual([]); + + await redis.hset(key, "field1", "value1", "field2", "value2", "field3", "value3"); + + const keys2 = await redis.hkeys(key); + expect(keys2.sort()).toEqual(["field1", "field2", "field3"]); + }); + + test("should get hash length with HLEN", async () => { + const redis = ctx.redis; + const key = "hlen-test"; + + const len1 = await redis.hlen(key); + expect(len1).toBe(0); + + await redis.hset(key, "field1", "value1", "field2", "value2"); + const len2 = await redis.hlen(key); + expect(len2).toBe(2); + + await redis.hset(key, "field3", "value3"); + const len3 = await redis.hlen(key); + expect(len3).toBe(3); + }); + + test("should get multiple hash values with HMGET where only two arguments are passed because the second is an array", async () => { + const redis = ctx.redis; + const key = "hmget-test"; + + await redis.hset(key, "field1", "value1", "field2", "value2", "field3", "value3"); + + const values = await redis.hmget(key, ["field1", "field2", "field3"]); + expect(values).toEqual(["value1", "value2", "value3"]); + + const mixed = await redis.hmget(key, ["field1", "nonexistent", "field2"]); + expect(mixed).toEqual(["value1", null, "value2"]); + }); + + test("should get multiple hash values with HMGET", async () => { + const redis = ctx.redis; + const key = "hmget-test"; + + await redis.hset(key, "field1", "value1", "field2", "value2", "field3", "value3"); + + const values = await redis.hmget(key, "field1", "field2", "field3"); + expect(values).toEqual(["value1", "value2", "value3"]); + + const mixed = await redis.hmget(key, "field1", "nonexistent", "field2"); + expect(mixed).toEqual(["value1", null, "value2"]); + }); + + test("should get all hash values with HVALS", async () => { + const redis = ctx.redis; + const key = "hvals-test"; + + const vals1 = await redis.hvals(key); + expect(vals1).toEqual([]); + + await redis.hset(key, "field1", "value1", "field2", "value2", "field3", "value3"); + + const vals2 = await redis.hvals(key); + expect(vals2.sort()).toEqual(["value1", "value2", "value3"]); + }); + + test("should get hash field string length with HSTRLEN", async () => { + const redis = ctx.redis; + const key = "hstrlen-test"; + + await redis.hset(key, "field1", "Hello", "field2", "World!"); + + const len1 = await redis.hstrlen(key, "field1"); + expect(len1).toBe(5); + + const len2 = await redis.hstrlen(key, "field2"); + expect(len2).toBe(6); + + const len3 = await redis.hstrlen(key, "nonexistent"); + expect(len3).toBe(0); + }); + + test("should set hash field expiration with HEXPIRE", async () => { + const redis = ctx.redis; + const key = "hexpire-test"; + + await redis.hset(key, "field1", "value1", "field2", "value2"); + + const result = await redis.hexpire(key, 60, "FIELDS", 1, "field1"); + expect(result).toEqual([1]); + + const ttl = await redis.httl(key, "FIELDS", 1, "field1"); + expect(ttl[0]).toBeGreaterThan(0); + expect(ttl[0]).toBeLessThanOrEqual(60); + }); + + test("should set hash field expiration at timestamp with HEXPIREAT", async () => { + const redis = ctx.redis; + const key = "hexpireat-test"; + + await redis.hset(key, "field1", "value1"); + + const futureTs = Math.floor(Date.now() / 1000) + 60; + const result = await redis.hexpireat(key, futureTs, "FIELDS", 1, "field1"); + expect(result).toEqual([1]); + + const ttl = await redis.httl(key, "FIELDS", 1, "field1"); + expect(ttl[0]).toBeGreaterThan(0); + }); + + test("should get hash field expiration time with HEXPIRETIME", async () => { + const redis = ctx.redis; + const key = "hexpiretime-test"; + + await redis.hset(key, "field1", "value1"); + + const futureTs = Math.floor(Date.now() / 1000) + 60; + await redis.hexpireat(key, futureTs, "FIELDS", 1, "field1"); + + const expireTime = await redis.hexpiretime(key, "FIELDS", 1, "field1"); + expect(expireTime[0]).toBeGreaterThan(0); + expect(expireTime[0]).toBeLessThanOrEqual(futureTs); + }); + + test("should remove hash field expiration with HPERSIST", async () => { + const redis = ctx.redis; + const key = "hpersist-test"; + + await redis.hset(key, "field1", "value1"); + await redis.hexpire(key, 60, "FIELDS", 1, "field1"); + + const ttlBefore = await redis.httl(key, "FIELDS", 1, "field1"); + expect(ttlBefore[0]).toBeGreaterThan(0); + + const result = await redis.hpersist(key, "FIELDS", 1, "field1"); + expect(result).toEqual([1]); + + const ttlAfter = await redis.httl(key, "FIELDS", 1, "field1"); + expect(ttlAfter[0]).toBe(-1); + }); + + test("should set hash field expiration at timestamp in ms with HPEXPIREAT", async () => { + const redis = ctx.redis; + const key = "hpexpireat-test"; + + await redis.hset(key, "field1", "value1"); + + const futureTs = Date.now() + 5000; + const result = await redis.hpexpireat(key, futureTs, "FIELDS", 1, "field1"); + expect(result).toEqual([1]); + + const pttl = await redis.hpttl(key, "FIELDS", 1, "field1"); + expect(pttl[0]).toBeGreaterThan(0); + }); + + test("should get hash field expiration time in ms with HPEXPIRETIME", async () => { + const redis = ctx.redis; + const key = "hpexpiretime-test"; + + await redis.hset(key, "field1", "value1"); + + const futureTs = Date.now() + 5000; + await redis.hpexpireat(key, futureTs, "FIELDS", 1, "field1"); + + const pexpireTime = await redis.hpexpiretime(key, "FIELDS", 1, "field1"); + expect(pexpireTime[0]).toBeGreaterThan(0); + expect(pexpireTime[0]).toBeLessThanOrEqual(futureTs); + }); + + test("should set hash fields using object syntax", async () => { + const redis = ctx.redis; + const key = "hash-object-test"; + + const result = await redis.hset(key, { field1: "value1", field2: "value2", field3: "value3" }); + expect(result).toBe(3); + + const value1 = await redis.hget(key, "field1"); + expect(value1).toBe("value1"); + const value2 = await redis.hget(key, "field2"); + expect(value2).toBe("value2"); + const value3 = await redis.hget(key, "field3"); + expect(value3).toBe("value3"); + }); + + test("should set hash fields using variadic syntax", async () => { + const redis = ctx.redis; + const key = "hash-variadic-test"; + + const result = await redis.hset(key, "field1", "value1", "field2", "value2"); + expect(result).toBe(2); + + const value1 = await redis.hget(key, "field1"); + expect(value1).toBe("value1"); + const value2 = await redis.hget(key, "field2"); + expect(value2).toBe("value2"); + }); + + test("should set single hash field", async () => { + const redis = ctx.redis; + const key = "hash-single-test"; + + const result = await redis.hset(key, "field1", "value1"); + expect(result).toBe(1); + + const value = await redis.hget(key, "field1"); + expect(value).toBe("value1"); + }); + + test("should update existing hash fields", async () => { + const redis = ctx.redis; + const key = "hash-update-test"; + + const result1 = await redis.hset(key, { field1: "value1", field2: "value2" }); + expect(result1).toBe(2); + + const result2 = await redis.hset(key, { field1: "new-value1", field3: "value3" }); + expect(result2).toBe(1); + + const value1 = await redis.hget(key, "field1"); + expect(value1).toBe("new-value1"); + const value3 = await redis.hget(key, "field3"); + expect(value3).toBe("value3"); + }); + + test("should work with HMSET using object syntax", async () => { + const redis = ctx.redis; + const key = "hmset-object-test"; + + const result = await redis.hmset(key, { field1: "value1", field2: "value2" }); + expect(result).toBe("OK"); + + const value1 = await redis.hget(key, "field1"); + expect(value1).toBe("value1"); + const value2 = await redis.hget(key, "field2"); + expect(value2).toBe("value2"); + }); + + test("should work with HMSET using variadic syntax", async () => { + const redis = ctx.redis; + const key = "hmset-variadic-test"; + + const result = await redis.hmset(key, "field1", "value1", "field2", "value2"); + expect(result).toBe("OK"); + + const value1 = await redis.hget(key, "field1"); + expect(value1).toBe("value1"); + }); + + test("should work with HMSET using array syntax", async () => { + const redis = ctx.redis; + const key = "hmset-array-test"; + + const result = await redis.hmset(key, ["field1", "value1", "field2", "value2"]); + expect(result).toBe("OK"); + + const value1 = await redis.hget(key, "field1"); + expect(value1).toBe("value1"); + }); + + test("should handle numeric field names and values", async () => { + const redis = ctx.redis; + const key = "hash-numeric-test"; + + const result = await redis.hset(key, { 123: "value1", field2: 456 }); + expect(result).toBe(2); + + const value1 = await redis.hget(key, "123"); + expect(value1).toBe("value1"); + const value2 = await redis.hget(key, "field2"); + expect(value2).toBe("456"); + }); + + test("should throw error for odd number of variadic arguments", async () => { + const redis = ctx.redis; + const key = "hash-error-test"; + + expect(async () => { + await redis.hset(key, "field1", "value1", "field2"); + }).toThrow("HSET requires field-value pairs (even number of arguments after key)"); + }); + + test("should throw error for empty object", async () => { + const redis = ctx.redis; + const key = "hash-empty-test"; + + expect(async () => { + await redis.hset(key, {}); + }).toThrow("HSET requires at least one field-value pair"); + }); + + test("should throw error for array with odd number of elements", async () => { + const redis = ctx.redis; + const key = "hmset-error-test"; + + expect(async () => { + await redis.hmset(key, ["field1", "value1", "field2"]); + }).toThrow("Array must have an even number of elements (field-value pairs)"); + }); + + test("should handle large number of fields", async () => { + const redis = ctx.redis; + const key = "hash-large-test"; + + const fields: Record = {}; + for (let i = 0; i < 100; i++) { + fields[`field${i}`] = `value${i}`; + } + + const result = await redis.hset(key, fields); + expect(result).toBe(100); + + const value0 = await redis.hget(key, "field0"); + expect(value0).toBe("value0"); + const value99 = await redis.hget(key, "field99"); + expect(value99).toBe("value99"); + }); + + test("should set hash field only if it doesn't exist using hsetnx", async () => { + const redis = ctx.redis; + const key = "user:" + randomUUIDv7().substring(0, 8); + + const result1 = await redis.hsetnx(key, "name", "John"); + expect(result1).toBe(true); + + const result2 = await redis.hsetnx(key, "name", "Jane"); + expect(result2).toBe(false); + + const value = await redis.hget(key, "name"); + expect(value).toBe("John"); + + const result3 = await redis.hsetnx(key, "age", "30"); + expect(result3).toBe(true); + }); + + test("should get and delete hash field using hgetdel", async () => { + const redis = ctx.redis; + const key = "user:" + randomUUIDv7().substring(0, 8); + + await redis.hset(key, { name: "John", age: "30" }); + + const values = await redis.hgetdel(key, "FIELDS", 1, "name"); + expect(values).toEqual(["John"]); + + const check = await redis.hget(key, "name"); + expect(check).toBeNull(); + + const age = await redis.hget(key, "age"); + expect(age).toBe("30"); + }); + + test("should get and delete multiple hash fields using hgetdel", async () => { + const redis = ctx.redis; + const key = "user:" + randomUUIDv7().substring(0, 8); + + await redis.hset(key, { name: "John", age: "30", city: "NYC" }); + + const values = await redis.hgetdel(key, "FIELDS", 2, "name", "city"); + expect(values).toEqual(["John", "NYC"]); + + expect(await redis.hget(key, "name")).toBeNull(); + expect(await redis.hget(key, "city")).toBeNull(); + expect(await redis.hget(key, "age")).toBe("30"); + }); + + test("should get hash field with expiration using hgetex", async () => { + const redis = ctx.redis; + const key = "user:" + randomUUIDv7().substring(0, 8); + + await redis.hset(key, { name: "John" }); + + const values = await redis.hgetex(key, "EX", 10, "FIELDS", 1, "name"); + expect(values).toEqual(["John"]); + + const check = await redis.hget(key, "name"); + expect(check).toBe("John"); + + const ttls = await redis.httl(key, "FIELDS", 1, "name"); + expect(ttls).toHaveLength(1); + expect(ttls[0]).toBeGreaterThan(0); + expect(ttls[0]).toBeLessThanOrEqual(10); + }); + + test("should get hash fields without expiration using hgetex", async () => { + const redis = ctx.redis; + const key = "user:" + randomUUIDv7().substring(0, 8); + + await redis.hset(key, { name: "John", age: "30" }); + + const values = await redis.hgetex(key, "FIELDS", 2, "name", "age"); + expect(values).toEqual(["John", "30"]); + }); + + test("should get hash fields with PX expiration using hgetex", async () => { + const redis = ctx.redis; + const key = "user:" + randomUUIDv7().substring(0, 8); + + await redis.hset(key, { name: "John" }); + + const values = await redis.hgetex(key, "PX", 5000, "FIELDS", 1, "name"); + expect(values).toEqual(["John"]); + + const ttls = await redis.hpttl(key, "FIELDS", 1, "name"); + expect(ttls).toHaveLength(1); + expect(ttls[0]).toBeGreaterThan(0); + expect(ttls[0]).toBeLessThanOrEqual(5000); + }); + + test("should get hash fields with EXAT using hgetex", async () => { + const redis = ctx.redis; + const key = "user:" + randomUUIDv7().substring(0, 8); + + await redis.hset(key, { name: "John" }); + + const futureTimestamp = Math.floor(Date.now() / 1000) + 60; + const values = await redis.hgetex(key, "EXAT", futureTimestamp, "FIELDS", 1, "name"); + expect(values).toEqual(["John"]); + }); + + test("should get hash fields with PXAT using hgetex", async () => { + const redis = ctx.redis; + const key = "user:" + randomUUIDv7().substring(0, 8); + + await redis.hset(key, { name: "John" }); + + const futureTimestamp = Date.now() + 60000; + const values = await redis.hgetex(key, "PXAT", futureTimestamp, "FIELDS", 1, "name"); + expect(values).toEqual(["John"]); + }); + + test("should get hash fields with PERSIST using hgetex", async () => { + const redis = ctx.redis; + const key = "user:" + randomUUIDv7().substring(0, 8); + + await redis.hsetex(key, "EX", 100, "FIELDS", 1, "name", "John"); + + const values = await redis.hgetex(key, "PERSIST", "FIELDS", 1, "name"); + expect(values).toEqual(["John"]); + }); + + test("should get multiple hash fields and return null for missing fields using hgetex", async () => { + const redis = ctx.redis; + const key = "user:" + randomUUIDv7().substring(0, 8); + + await redis.hset(key, { name: "John" }); + + const values = await redis.hgetex(key, "FIELDS", 3, "name", "age", "city"); + expect(values).toEqual(["John", null, null]); + }); + + test("should set hash field with expiration using hsetex", async () => { + const redis = ctx.redis; + const key = "user:" + randomUUIDv7().substring(0, 8); + + const result = await redis.hsetex(key, "EX", 10, "FIELDS", 1, "name", "John"); + expect(result).toBe(1); + + const value = await redis.hget(key, "name"); + expect(value).toBe("John"); + + const ttls = await redis.httl(key, "FIELDS", 1, "name"); + expect(ttls).toHaveLength(1); + expect(ttls[0]).toBeGreaterThan(0); + expect(ttls[0]).toBeLessThanOrEqual(10); + }); + + test("should set multiple hash fields with expiration using hsetex", async () => { + const redis = ctx.redis; + const key = "user:" + randomUUIDv7().substring(0, 8); + + const result = await redis.hsetex(key, "EX", 10, "FIELDS", 2, "name", "John", "age", "30"); + expect(result).toBe(1); + + expect(await redis.hget(key, "name")).toBe("John"); + expect(await redis.hget(key, "age")).toBe("30"); + + const ttls = await redis.httl(key, "FIELDS", 2, "name", "age"); + expect(ttls).toHaveLength(2); + expect(ttls[0]).toBeGreaterThan(0); + expect(ttls[0]).toBeLessThanOrEqual(10); + expect(ttls[1]).toBeGreaterThan(0); + expect(ttls[1]).toBeLessThanOrEqual(10); + }); + + test("should set hash fields without expiration using hsetex", async () => { + const redis = ctx.redis; + const key = "user:" + randomUUIDv7().substring(0, 8); + + const result = await redis.hsetex(key, "FIELDS", 2, "name", "John", "age", "30"); + expect(result).toBe(1); + + expect(await redis.hget(key, "name")).toBe("John"); + expect(await redis.hget(key, "age")).toBe("30"); + + const ttls = await redis.httl(key, "FIELDS", 2, "name", "age"); + expect(ttls).toEqual([-1, -1]); + }); + + test("should set hash fields with PX (milliseconds) using hsetex", async () => { + const redis = ctx.redis; + const key = "user:" + randomUUIDv7().substring(0, 8); + + const result = await redis.hsetex(key, "PX", 5000, "FIELDS", 1, "name", "John"); + expect(result).toBe(1); + + expect(await redis.hget(key, "name")).toBe("John"); + }); + + test("should set hash fields with EXAT (unix timestamp seconds) using hsetex", async () => { + const redis = ctx.redis; + const key = "user:" + randomUUIDv7().substring(0, 8); + + const futureTimestamp = Math.floor(Date.now() / 1000) + 60; + const result = await redis.hsetex(key, "EXAT", futureTimestamp, "FIELDS", 1, "name", "John"); + expect(result).toBe(1); + + expect(await redis.hget(key, "name")).toBe("John"); + }); + + test("should set hash fields with PXAT (unix timestamp milliseconds) using hsetex", async () => { + const redis = ctx.redis; + const key = "user:" + randomUUIDv7().substring(0, 8); + + const futureTimestamp = Date.now() + 60000; + const result = await redis.hsetex(key, "PXAT", futureTimestamp, "FIELDS", 1, "name", "John"); + expect(result).toBe(1); + + expect(await redis.hget(key, "name")).toBe("John"); + }); + + test("should set hash fields with KEEPTTL using hsetex", async () => { + const redis = ctx.redis; + const key = "user:" + randomUUIDv7().substring(0, 8); + + await redis.hsetex(key, "EX", 100, "FIELDS", 1, "name", "John"); + + const result = await redis.hsetex(key, "KEEPTTL", "FIELDS", 1, "name", "Jane"); + expect(result).toBe(1); + + expect(await redis.hget(key, "name")).toBe("Jane"); + }); + + test("should set hash fields with FNX flag using hsetex", async () => { + const redis = ctx.redis; + const key = "user:" + randomUUIDv7().substring(0, 8); + + await redis.hset(key, { name: "John" }); + + const result1 = await redis.hsetex(key, "FNX", "FIELDS", 2, "name", "Jane", "age", "30"); + expect(result1).toBe(0); + + expect(await redis.hget(key, "name")).toBe("John"); + expect(await redis.hget(key, "age")).toBeNull(); + + const result2 = await redis.hsetex(key, "FNX", "FIELDS", 2, "city", "NYC", "country", "USA"); + expect(result2).toBe(1); + + expect(await redis.hget(key, "city")).toBe("NYC"); + expect(await redis.hget(key, "country")).toBe("USA"); + }); + + test("should set hash fields with FXX flag using hsetex", async () => { + const redis = ctx.redis; + const key = "user:" + randomUUIDv7().substring(0, 8); + + await redis.hset(key, { name: "John" }); + + const result1 = await redis.hsetex(key, "FXX", "FIELDS", 2, "name", "Jane", "age", "30"); + expect(result1).toBe(0); + + expect(await redis.hget(key, "name")).toBe("John"); + expect(await redis.hget(key, "age")).toBeNull(); + + await redis.hset(key, { age: "25" }); + const result2 = await redis.hsetex(key, "FXX", "FIELDS", 2, "name", "Jane", "age", "30"); + expect(result2).toBe(1); + + expect(await redis.hget(key, "name")).toBe("Jane"); + expect(await redis.hget(key, "age")).toBe("30"); + }); + + test("should set hash fields with FNX and EX combined using hsetex", async () => { + const redis = ctx.redis; + const key = "user:" + randomUUIDv7().substring(0, 8); + + const result1 = await redis.hsetex(key, "FNX", "EX", 10, "FIELDS", 2, "name", "John", "age", "30"); + expect(result1).toBe(1); + + expect(await redis.hget(key, "name")).toBe("John"); + expect(await redis.hget(key, "age")).toBe("30"); + + const result2 = await redis.hsetex(key, "FNX", "EX", 10, "FIELDS", 2, "name", "Jane", "age", "35"); + expect(result2).toBe(0); + + expect(await redis.hget(key, "name")).toBe("John"); + expect(await redis.hget(key, "age")).toBe("30"); + }); + + test("should set hash fields with FXX and PX combined using hsetex", async () => { + const redis = ctx.redis; + const key = "user:" + randomUUIDv7().substring(0, 8); + + await redis.hset(key, { name: "John", age: "30" }); + + const result = await redis.hsetex(key, "FXX", "PX", 5000, "FIELDS", 2, "name", "Jane", "age", "35"); + expect(result).toBe(1); + + expect(await redis.hget(key, "name")).toBe("Jane"); + expect(await redis.hget(key, "age")).toBe("35"); + }); + + test("should check TTL of hash fields using httl", async () => { + const redis = ctx.redis; + const key = "user:" + randomUUIDv7().substring(0, 8); + + await redis.hsetex(key, "EX", 100, "FIELDS", 1, "name", "John"); + await redis.hset(key, { age: "30" }); + + const ttls = await redis.httl(key, "FIELDS", 3, "name", "age", "nonexistent"); + expect(ttls).toHaveLength(3); + expect(ttls[0]).toBeGreaterThan(0); + expect(ttls[0]).toBeLessThanOrEqual(100); + expect(ttls[1]).toBe(-1); + expect(ttls[2]).toBe(-2); + }); + + test("should check TTL of hash fields using hpttl in milliseconds", async () => { + const redis = ctx.redis; + const key = "user:" + randomUUIDv7().substring(0, 8); + + await redis.hset(key, { name: "John", age: "30" }); + + const expireResult = await redis.hpexpire(key, 5000, "FIELDS", 1, "name"); + expect(expireResult).toEqual([1]); + + const ttls = await redis.hpttl(key, "FIELDS", 2, "name", "age"); + expect(ttls).toHaveLength(2); + expect(ttls[0]).toBeGreaterThan(0); + expect(ttls[0]).toBeLessThanOrEqual(5000); + expect(ttls[1]).toBe(-1); + }); + + test("should delete hash fields using hdel", async () => { + const redis = ctx.redis; + const key = "user:" + randomUUIDv7().substring(0, 8); + + await redis.hset(key, { name: "John", age: "30", city: "NYC" }); + + const deleted = await redis.hdel(key, "age"); + expect(deleted).toBe(1); + + const age = await redis.hget(key, "age"); + expect(age).toBeNull(); + + const name = await redis.hget(key, "name"); + expect(name).toBe("John"); + }); + + test("should delete multiple hash fields using hdel", async () => { + const redis = ctx.redis; + const key = "user:" + randomUUIDv7().substring(0, 8); + + await redis.hset(key, { name: "John", age: "30", city: "NYC", country: "USA" }); + + const deleted = await redis.hdel(key, "age", "city"); + expect(deleted).toBe(2); + + const remaining = await redis.hgetall(key); + expect(remaining).toEqual({ name: "John", country: "USA" }); + }); + + test("should return empty object for hgetall on non-existent key", async () => { + const redis = ctx.redis; + const key = "nonexistent-hgetall-" + randomUUIDv7(); + const result = await redis.hgetall(key); + expect(result).toEqual({}); + }); + + test("should check if hash field exists using hexists", async () => { + const redis = ctx.redis; + const key = "user:" + randomUUIDv7().substring(0, 8); + + await redis.hset(key, { name: "John", age: "30" }); + + const nameExists = await redis.hexists(key, "name"); + expect(nameExists).toBe(true); + + const emailExists = await redis.hexists(key, "email"); + expect(emailExists).toBe(false); + }); + + test("should get random field using hrandfield", async () => { + const redis = ctx.redis; + const key = "user:" + randomUUIDv7().substring(0, 8); + + await redis.hset(key, { name: "John", age: "30", city: "NYC" }); + + const field = await redis.hrandfield(key); + expect(["name", "age", "city"]).toContain(field); + }); + + test("should get multiple random fields using hrandfield with count", async () => { + const redis = ctx.redis; + const key = "user:" + randomUUIDv7().substring(0, 8); + + await redis.hset(key, { name: "John", age: "30", city: "NYC" }); + + const fields = await redis.hrandfield(key, 2); + expect(fields).toBeInstanceOf(Array); + expect(fields.length).toBe(2); + fields.forEach(field => { + expect(["name", "age", "city"]).toContain(field); + }); + }); + + test("should get random fields with values using hrandfield WITHVALUES", async () => { + const redis = ctx.redis; + const key = "user:" + randomUUIDv7().substring(0, 8); + + const fullData = { name: "Andy", age: "30", city: "Cupertino" }; + await redis.hset(key, fullData); + + const result = await redis.hrandfield(key, 2, "WITHVALUES"); + expect(result).toBeInstanceOf(Array); + expect(result.length).toBe(2); + + const obj = Object.fromEntries(result); + + expect(Object.keys(obj).length).toBe(2); + + for (const [field, value] of Object.entries(obj)) { + expect(fullData).toHaveProperty(field, value); + } + }); + + test("should scan hash using hscan", async () => { + const redis = ctx.redis; + const key = "user:" + randomUUIDv7().substring(0, 8); + + await redis.hset(key, { name: "John", age: "30", city: "NYC" }); + + const [cursor, fields] = await redis.hscan(key, 0); + expect(typeof cursor).toBe("string"); + expect(fields).toBeInstanceOf(Array); + expect(fields.length).toBe(6); + + const obj: Record = {}; + for (let i = 0; i < fields.length; i += 2) { + obj[fields[i]] = fields[i + 1]; + } + expect(obj).toEqual({ name: "John", age: "30", city: "NYC" }); + }); + + test("should scan hash with pattern using hscan MATCH", async () => { + const redis = ctx.redis; + const key = "user:" + randomUUIDv7().substring(0, 8); + + await redis.hset(key, { field1: "val1", field2: "val2", other: "val3" }); + + const [cursor, fields] = await redis.hscan(key, 0, "MATCH", "field*"); + expect(typeof cursor).toBe("string"); + expect(fields).toBeInstanceOf(Array); + + const obj: Record = {}; + for (let i = 0; i < fields.length; i += 2) { + obj[fields[i]] = fields[i + 1]; + } + + expect(obj.field1).toBe("val1"); + expect(obj.field2).toBe("val2"); + expect(obj.other).toBeUndefined(); + }); + + test("should scan hash with count using hscan COUNT", async () => { + const redis = ctx.redis; + const key = "user:" + randomUUIDv7().substring(0, 8); + + const fields: Record = {}; + for (let i = 0; i < 20; i++) { + fields[`field${i}`] = `value${i}`; + } + await redis.hset(key, fields); + + const [cursor, result] = await redis.hscan(key, 0, "COUNT", 5); + expect(typeof cursor).toBe("string"); + expect(result).toBeInstanceOf(Array); + + expect(result.length).toBeGreaterThan(0); + }); + + test("should increment hash field by integer using hincrby", async () => { + const redis = ctx.redis; + const key = "hincrby-test:" + randomUUIDv7().substring(0, 8); + + await redis.hset(key, { counter: "10" }); + + const result1 = await redis.hincrby(key, "counter", 5); + expect(result1).toBe(15); + + const result2 = await redis.hincrby(key, "counter", -3); + expect(result2).toBe(12); + + const value = await redis.hget(key, "counter"); + expect(value).toBe("12"); + }); + + test("should increment hash field from zero using hincrby", async () => { + const redis = ctx.redis; + const key = "hincrby-zero-test:" + randomUUIDv7().substring(0, 8); + + const result = await redis.hincrby(key, "newfield", 42); + expect(result).toBe(42); + + const value = await redis.hget(key, "newfield"); + expect(value).toBe("42"); + }); + + test("should increment hash field by float using hincrbyfloat", async () => { + const redis = ctx.redis; + const key = "hincrbyfloat-test:" + randomUUIDv7().substring(0, 8); + + await redis.hset(key, { price: "10.5" }); + + const result1 = await redis.hincrbyfloat(key, "price", 2.3); + expect(result1).toBe("12.8"); + + const result2 = await redis.hincrbyfloat(key, "price", -0.8); + expect(result2).toBe("12"); + + const value = await redis.hget(key, "price"); + expect(value).toBe("12"); + }); + + test("should increment hash field from zero using hincrbyfloat", async () => { + const redis = ctx.redis; + const key = "hincrbyfloat-zero-test:" + randomUUIDv7().substring(0, 8); + + const result = await redis.hincrbyfloat(key, "newfield", 3.14); + expect(result).toBe("3.14"); + + const value = await redis.hget(key, "newfield"); + expect(value).toBe("3.14"); + }); + + test("should get all hash keys using hkeys", async () => { + const redis = ctx.redis; + const key = "hkeys-test:" + randomUUIDv7().substring(0, 8); + + await redis.hset(key, { name: "John", age: "30", city: "NYC" }); + + const keys = await redis.hkeys(key); + expect(keys).toBeInstanceOf(Array); + expect(keys.length).toBe(3); + expect(keys).toContain("name"); + expect(keys).toContain("age"); + expect(keys).toContain("city"); + }); + + test("should return empty array for non-existent key using hkeys", async () => { + const redis = ctx.redis; + const key = "hkeys-nonexistent:" + randomUUIDv7().substring(0, 8); + + const keys = await redis.hkeys(key); + expect(keys).toEqual([]); + }); + + test("should get hash length using hlen", async () => { + const redis = ctx.redis; + const key = "hlen-test:" + randomUUIDv7().substring(0, 8); + + await redis.hset(key, { name: "John", age: "30", city: "NYC" }); + + const length = await redis.hlen(key); + expect(length).toBe(3); + + await redis.hset(key, { country: "USA" }); + const newLength = await redis.hlen(key); + expect(newLength).toBe(4); + }); + + test("should return 0 for non-existent key using hlen", async () => { + const redis = ctx.redis; + const key = "hlen-nonexistent:" + randomUUIDv7().substring(0, 8); + + const length = await redis.hlen(key); + expect(length).toBe(0); + }); + + test("should get multiple hash values using hmget", async () => { + const redis = ctx.redis; + const key = "hmget-test:" + randomUUIDv7().substring(0, 8); + + await redis.hset(key, { name: "John", age: "30", city: "NYC" }); + + const values = await redis.hmget(key, "name", "age", "city"); + expect(values).toEqual(["John", "30", "NYC"]); + }); + + test("should return null for missing fields using hmget", async () => { + const redis = ctx.redis; + const key = "hmget-missing-test:" + randomUUIDv7().substring(0, 8); + + await redis.hset(key, { name: "John" }); + + const values = await redis.hmget(key, "name", "age", "city"); + expect(values).toEqual(["John", null, null]); + }); + + test("should get all hash values using hvals", async () => { + const redis = ctx.redis; + const key = "hvals-test:" + randomUUIDv7().substring(0, 8); + + await redis.hset(key, { name: "John", age: "30", city: "NYC" }); + + const values = await redis.hvals(key); + expect(values).toBeInstanceOf(Array); + expect(values.length).toBe(3); + expect(values).toContain("John"); + expect(values).toContain("30"); + expect(values).toContain("NYC"); + }); + + test("should return empty array for non-existent key using hvals", async () => { + const redis = ctx.redis; + const key = "hvals-nonexistent:" + randomUUIDv7().substring(0, 8); + + const values = await redis.hvals(key); + expect(values).toEqual([]); + }); + + test("should get hash field string length using hstrlen", async () => { + const redis = ctx.redis; + const key = "hstrlen-test:" + randomUUIDv7().substring(0, 8); + + await redis.hset(key, { name: "John", description: "Software Engineer" }); + + const nameLen = await redis.hstrlen(key, "name"); + expect(nameLen).toBe(4); + + const descLen = await redis.hstrlen(key, "description"); + expect(descLen).toBe(17); + }); + + test("should return 0 for non-existent field using hstrlen", async () => { + const redis = ctx.redis; + const key = "hstrlen-nonexistent:" + randomUUIDv7().substring(0, 8); + + await redis.hset(key, { name: "John" }); + + const length = await redis.hstrlen(key, "age"); + expect(length).toBe(0); + }); + + test("should expire hash fields using hexpire", async () => { + const redis = ctx.redis; + const key = "hexpire-test:" + randomUUIDv7().substring(0, 8); + + await redis.hset(key, { name: "John", age: "30", city: "NYC" }); + + const result = await redis.hexpire(key, 10, "FIELDS", 2, "name", "age"); + expect(result).toEqual([1, 1]); + + const ttls = await redis.httl(key, "FIELDS", 3, "name", "age", "city"); + expect(ttls[0]).toBeGreaterThan(0); + expect(ttls[0]).toBeLessThanOrEqual(10); + expect(ttls[1]).toBeGreaterThan(0); + expect(ttls[1]).toBeLessThanOrEqual(10); + expect(ttls[2]).toBe(-1); + }); + + test("should expire hash fields with NX flag using hexpire", async () => { + const redis = ctx.redis; + const key = "hexpire-nx-test:" + randomUUIDv7().substring(0, 8); + + await redis.hsetex(key, "EX", 100, "FIELDS", 1, "name", "John"); + await redis.hset(key, { age: "30" }); + + const result = await redis.hexpire(key, 10, "NX", "FIELDS", 2, "name", "age"); + expect(result).toEqual([0, 1]); + }); + + test("should expire hash fields at specific time using hexpireat", async () => { + const redis = ctx.redis; + const key = "hexpireat-test:" + randomUUIDv7().substring(0, 8); + + await redis.hset(key, { name: "John", age: "30" }); + + const futureTimestamp = Math.floor(Date.now() / 1000) + 60; + const result = await redis.hexpireat(key, futureTimestamp, "FIELDS", 2, "name", "age"); + expect(result).toEqual([1, 1]); + + const ttls = await redis.httl(key, "FIELDS", 2, "name", "age"); + expect(ttls[0]).toBeGreaterThan(0); + expect(ttls[0]).toBeLessThanOrEqual(60); + expect(ttls[1]).toBeGreaterThan(0); + expect(ttls[1]).toBeLessThanOrEqual(60); + }); + + test("should get hash field expiration time using hexpiretime", async () => { + const redis = ctx.redis; + const key = "hexpiretime-test:" + randomUUIDv7().substring(0, 8); + + await redis.hset(key, { name: "John", age: "30" }); + + const futureTimestamp = Math.floor(Date.now() / 1000) + 100; + await redis.hexpireat(key, futureTimestamp, "FIELDS", 1, "name"); + + const expiretimes = await redis.hexpiretime(key, "FIELDS", 2, "name", "age"); + expect(expiretimes).toHaveLength(2); + expect(expiretimes[0]).toBeGreaterThan(0); + expect(expiretimes[0]).toBeLessThanOrEqual(futureTimestamp); + expect(expiretimes[1]).toBe(-1); + }); + + test("should expire hash fields at specific time in milliseconds using hpexpireat", async () => { + const redis = ctx.redis; + const key = "hpexpireat-test:" + randomUUIDv7().substring(0, 8); + + await redis.hset(key, { name: "John", age: "30" }); + + const futureTimestamp = Date.now() + 60000; + const result = await redis.hpexpireat(key, futureTimestamp, "FIELDS", 2, "name", "age"); + expect(result).toEqual([1, 1]); + + const ttls = await redis.hpttl(key, "FIELDS", 2, "name", "age"); + expect(ttls[0]).toBeGreaterThan(0); + expect(ttls[0]).toBeLessThanOrEqual(60100); + expect(ttls[1]).toBeGreaterThan(0); + expect(ttls[1]).toBeLessThanOrEqual(60100); + }); + + test("should get hash field expiration time in milliseconds using hpexpiretime", async () => { + const redis = ctx.redis; + const key = "hpexpiretime-test:" + randomUUIDv7().substring(0, 8); + + await redis.hset(key, { name: "John", age: "30" }); + + const futureTimestamp = Date.now() + 100000; + await redis.hpexpireat(key, futureTimestamp, "FIELDS", 1, "name"); + + const expiretimes = await redis.hpexpiretime(key, "FIELDS", 2, "name", "age"); + expect(expiretimes).toHaveLength(2); + expect(expiretimes[0]).toBeGreaterThan(0); + expect(expiretimes[0]).toBeLessThanOrEqual(futureTimestamp); + expect(expiretimes[1]).toBe(-1); + }); + + test("should persist hash fields using hpersist", async () => { + const redis = ctx.redis; + const key = "hpersist-test:" + randomUUIDv7().substring(0, 8); + + await redis.hsetex(key, "EX", 100, "FIELDS", 2, "name", "John", "age", "30"); + + const result = await redis.hpersist(key, "FIELDS", 2, "name", "age"); + expect(result).toEqual([1, 1]); + + const ttls = await redis.httl(key, "FIELDS", 2, "name", "age"); + expect(ttls).toEqual([-1, -1]); + }); + + test("should return 0 for fields without expiration using hpersist", async () => { + const redis = ctx.redis; + const key = "hpersist-noexpire-test:" + randomUUIDv7().substring(0, 8); + + await redis.hset(key, { name: "John", age: "30" }); + + const result = await redis.hpersist(key, "FIELDS", 2, "name", "age"); + expect(result).toEqual([-1, -1]); }); }); describe("Connection State", () => { test("should have a connected property", () => { const redis = ctx.redis; - // The client should expose a connected property + expect(typeof redis.connected).toBe("boolean"); }); }); @@ -149,16 +6165,14 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { describe("RESP3 Data Types", () => { test("should handle hash maps (dictionaries) as command responses", async () => { const redis = ctx.redis; - // HSET multiple fields + const userId = "user:" + randomUUIDv7().substring(0, 8); const setResult = await redis.send("HSET", [userId, "name", "John", "age", "30", "active", "true"]); expect(setResult).toBeDefined(); - // HGETALL returns object with key-value pairs const hash = await redis.send("HGETALL", [userId]); expect(hash).toBeDefined(); - // Proper structure checking when RESP3 maps are fixed if (typeof hash === "object" && hash !== null) { expect(hash).toHaveProperty("name"); expect(hash).toHaveProperty("age"); @@ -172,19 +6186,16 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { test("should handle sets as command responses", async () => { const redis = ctx.redis; - // Add items to a set + const setKey = "colors:" + randomUUIDv7().substring(0, 8); const addResult = await redis.send("SADD", [setKey, "red", "blue", "green"]); expect(addResult).toBeDefined(); - // Get set members const setMembers = await redis.send("SMEMBERS", [setKey]); expect(setMembers).toBeDefined(); - // Check if the response is an array expect(Array.isArray(setMembers)).toBe(true); - // Should contain our colors expect(setMembers).toContain("red"); expect(setMembers).toContain("blue"); expect(setMembers).toContain("green"); @@ -209,7 +6220,6 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { test.each([...Array(16).keys()])("Connecting to database with url $url succeeds", async (dbId: number) => { const redis = createClient(connectionType, {}, dbId); - // Ensure the value is not in the database. const testValue = await redis.get(testKeyUniquePerDb); expect(testValue).toBeNull(); @@ -219,14 +6229,9 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { describe("Reconnections", () => { test.skip("should automatically reconnect after connection drop", async () => { - // NOTE: This test was already broken before the Docker Compose migration. - // It times out after 31 seconds with "Max reconnection attempts reached" - // This appears to be an issue with the Redis client's automatic reconnection - // behavior, not related to the Docker infrastructure changes. const TEST_KEY = "test-key"; const TEST_VALUE = "test-value"; - // Ensure we have a working client to start if (!ctx.redis || !ctx.redis.connected) { ctx.redis = createClient(connectionType); } @@ -234,7 +6239,6 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const valueBeforeStart = await ctx.redis.get(TEST_KEY); expect(valueBeforeStart).toBeNull(); - // Set some value await ctx.redis.set(TEST_KEY, TEST_VALUE); const valueAfterSet = await ctx.redis.get(TEST_KEY); expect(valueAfterSet).toBe(TEST_VALUE); @@ -262,7 +6266,6 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { }; beforeEach(async () => { - // The PUB/SUB tests expect that ctx.redis is connected but not in subscriber mode. await ctx.cleanupSubscribers(); }); @@ -370,10 +6373,8 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { await counter.untilValue(TEST_MESSAGE_COUNT); - // Check that we received messages on both channels expect(Object.keys(receivedMessages).sort()).toEqual(Object.keys(sentMessages).sort()); - // Check messages match for each channel for (const channel of channels) { if (sentMessages[channel]) { expect(receivedMessages[channel]).toEqual(sentMessages[channel]); @@ -393,35 +6394,29 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { let receivedMessages: { [channel: string]: string[] } = {}; - // Total counter for all messages we expect to receive: 3 initial + 2 after unsubscribe = 5 total const counter = awaitableCounter(); - // Subscribe to three channels await subscriber.subscribe([channel1, channel2, channel3], (message, channel) => { receivedMessages[channel] = receivedMessages[channel] || []; receivedMessages[channel].push(message); counter.increment(); }); - // Send initial messages to all channels expect(await ctx.redis.publish(channel1, "msg1-before")).toBe(1); expect(await ctx.redis.publish(channel2, "msg2-before")).toBe(1); expect(await ctx.redis.publish(channel3, "msg3-before")).toBe(1); - // Wait for initial messages, then unsubscribe from channel2 await counter.untilValue(3); await subscriber.unsubscribe(channel2); - // Send messages after unsubscribing from channel2 expect(await ctx.redis.publish(channel1, "msg1-after")).toBe(1); expect(await ctx.redis.publish(channel2, "msg2-after")).toBe(0); expect(await ctx.redis.publish(channel3, "msg3-after")).toBe(1); await counter.untilValue(5); - // Check we received messages only on subscribed channels expect(receivedMessages[channel1]).toEqual(["msg1-before", "msg1-after"]); - expect(receivedMessages[channel2]).toEqual(["msg2-before"]); // No "msg2-after" + expect(receivedMessages[channel2]).toEqual(["msg2-before"]); expect(receivedMessages[channel3]).toEqual(["msg3-before", "msg3-after"]); await subscriber.unsubscribe([channel1, channel3]); @@ -446,16 +6441,13 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { counter.increment(); }; - // Subscribe to the same channel twice await subscriber.subscribe(channel, listener); await subscriber.subscribe(channel, listener2); - // Publish a single message expect(await ctx.redis.publish(channel, "test-message")).toBe(1); await counter.untilValue(2); - // Both listeners should have been called once. expect(callCount).toBe(1); expect(callCount2).toBe(1); @@ -517,7 +6509,6 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const subscriber = await ctx.newSubscriberClient(connectionType); await subscriber.subscribe(channel, () => {}); - // Ping should work in subscription mode const pong = await subscriber.ping(); expect(pong).toBe("PONG"); @@ -531,8 +6522,9 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const subscriber = await ctx.newSubscriberClient(connectionType); await subscriber.subscribe(channel, () => {}); - // Publishing from the same client should work - expect(async () => subscriber.publish(channel, "self-published")).toThrow(); + expect(async () => subscriber.publish(channel, "self-published")).toThrow( + "RedisClient.prototype.publish cannot be called while in subscriber mode.", + ); }); test("complete unsubscribe restores normal command mode", async () => { @@ -542,15 +6534,12 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const subscriber = await ctx.newSubscriberClient(connectionType); await subscriber.subscribe(channel, () => {}); - // Should fail in subscription mode expect(() => subscriber.set(testKey, testValue())).toThrow( "RedisClient.prototype.set cannot be called while in subscriber mode.", ); - // Unsubscribe from all channels await subscriber.unsubscribe(); - // Should work after unsubscribing const result = await ctx.redis.set(testKey, "value"); expect(result).toBe("OK"); @@ -561,7 +6550,6 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { test("publishing without subscribers succeeds", async () => { const channel = "no-subscribers-channel"; - // Publishing without subscribers should not throw expect(await ctx.redis.publish(channel, "message")).toBe(0); }); @@ -581,12 +6569,11 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const STEP_SECOND_MESSAGE = 3; const STEP_THIRD_MESSAGE = 4; - // stepCounter is a slight hack to track the progress of the subprocess. const stepCounter = awaitableCounter(); let currentMessage: any = {}; const subscriberProc = spawn({ - cmd: [self.process.execPath, "run", `${__dirname}/valkey.failing-subscriber.ts`], + cmd: [bunExe(), `${__dirname}/valkey.failing-subscriber.ts`], stdout: "inherit", stderr: "inherit", ipc: msg => { @@ -609,19 +6596,16 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { await stepCounter.untilValue(STEP_SUBSCRIBED); expect(currentMessage.event).toBe("ready"); - // Send multiple messages expect(await ctx.redis.publish(channel, "message1")).toBeGreaterThanOrEqual(1); await stepCounter.untilValue(STEP_FIRST_MESSAGE); expect(currentMessage.event).toBe("message"); expect(currentMessage.index).toBe(1); - // Now, the subscriber process will crash expect(await ctx.redis.publish(channel, "message2")).toBeGreaterThanOrEqual(1); await stepCounter.untilValue(STEP_SECOND_MESSAGE); expect(currentMessage.event).toBe("exception"); //expect(currentMessage.index).toBe(2); - // But it should recover and continue receiving messages expect(await ctx.redis.publish(channel, "message3")).toBeGreaterThanOrEqual(1); await stepCounter.untilValue(STEP_THIRD_MESSAGE); expect(currentMessage.event).toBe("message"); @@ -646,7 +6630,6 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const subscriber = createClient(connectionType); await subscriber.connect(); - // First phase: both listeners should receive 1 message each (2 total) const counter = awaitableCounter(); let messageCount1 = 0; const listener1 = () => { @@ -686,7 +6669,6 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { expect(duplicate.connected).toBe(true); expect(duplicate).not.toBe(ctx.redis); - // Both should work independently await ctx.redis.set("test-original", "original-value"); await duplicate.set("test-duplicate", "duplicate-value"); @@ -701,7 +6683,6 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const duplicate = await ctx.redis.duplicate(); - // Both clients should be able to perform the same operations const testKey = `duplicate-config-test-${randomUUIDv7().substring(0, 8)}`; const testValue = "test-value"; @@ -716,7 +6697,6 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { test("should allow duplicate to work independently from original", async () => { const duplicate = await ctx.redis.duplicate(); - // Close original, duplicate should still work duplicate.close(); const testKey = `independent-test-${randomUUIDv7().substring(0, 8)}`; @@ -733,12 +6713,10 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const testChannel = "test-subscriber-duplicate"; - // Put original client in subscriber mode await subscriber.subscribe(testChannel, () => {}); const duplicate = await subscriber.duplicate(); - // Duplicate should not be in subscriber mode expect(() => duplicate.set("test-key", "test-value")).not.toThrow(); await subscriber.unsubscribe(testChannel); @@ -751,12 +6729,10 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const duplicate2 = await ctx.redis.duplicate(); const duplicate3 = await ctx.redis.duplicate(); - // All should be connected expect(duplicate1.connected).toBe(true); expect(duplicate2.connected).toBe(true); expect(duplicate3.connected).toBe(true); - // All should work independently const testKey = `multi-duplicate-test-${randomUUIDv7().substring(0, 8)}`; await duplicate1.set(`${testKey}-1`, "value-1"); await duplicate2.set(`${testKey}-2`, "value-2"); @@ -766,7 +6742,6 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { expect(await duplicate2.get(`${testKey}-2`)).toBe("value-2"); expect(await duplicate3.get(`${testKey}-3`)).toBe("value-3"); - // Cross-check: each duplicate can read what others wrote expect(await duplicate1.get(`${testKey}-2`)).toBe("value-2"); expect(await duplicate2.get(`${testKey}-3`)).toBe("value-3"); expect(await duplicate3.get(`${testKey}-1`)).toBe("value-1"); @@ -777,7 +6752,6 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { }); test("should duplicate client that failed to connect", async () => { - // Create client with invalid credentials to force connection failure const url = new URL(connectionType === ConnectionType.TLS ? TLS_REDIS_URL : DEFAULT_REDIS_URL); url.username = "invaliduser"; url.password = "invalidpassword"; @@ -785,7 +6759,6 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { tls: connectionType === ConnectionType.TLS ? TLS_REDIS_OPTIONS.tls : false, }); - // Try to connect and expect it to fail let connectionFailed = false; try { await failedRedis.connect(); @@ -796,7 +6769,6 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { expect(connectionFailed).toBe(true); expect(failedRedis.connected).toBe(false); - // Duplicate should also remain unconnected const duplicate = await failedRedis.duplicate(); expect(duplicate.connected).toBe(false); }); @@ -804,17 +6776,13 @@ for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { test("should handle duplicate timing with concurrent operations", async () => { await ctx.redis.connect(); - // Start some operations on the original client const testKey = `concurrent-test-${randomUUIDv7().substring(0, 8)}`; const originalOperation = ctx.redis.set(testKey, "original-value"); - // Create duplicate while operation is in flight const duplicate = await ctx.redis.duplicate(); - // Wait for original operation to complete await originalOperation; - // Duplicate should be able to read the value expect(await duplicate.get(testKey)).toBe("original-value"); duplicate.close();