diff --git a/CHANGELOG.md b/CHANGELOG.md index 288fad8259..e04e86a7df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ #### Changes +* Node: Added binary variant for commands which have `Record` as input or output ([#2207](https://github.com/valkey-io/valkey-glide/pull/2207)) * Node: Renamed `ReturnType` to `GlideReturnType` ([#2241](https://github.com/valkey-io/valkey-glide/pull/2241)) * Node, Python: Rename `stop` to `end` in sorted set queries ([#2214](https://github.com/valkey-io/valkey-glide/pull/2214)) * Node: Added binary variant to sorted set commands - part 1 ([#2190](https://github.com/valkey-io/valkey-glide/pull/2190)) diff --git a/node/npm/glide/index.ts b/node/npm/glide/index.ts index 9536d7a014..ac4544852e 100644 --- a/node/npm/glide/index.ts +++ b/node/npm/glide/index.ts @@ -117,7 +117,11 @@ function initialize() { GlideClient, GlideClusterClient, GlideClientConfiguration, + GlideRecord, GlideString, + SortedSetDataType, + StreamEntryDataType, + HashDataType, FunctionListOptions, FunctionListResponse, FunctionStatsSingleResponse, @@ -213,7 +217,11 @@ function initialize() { Decoder, DecoderOption, GeoAddOptions, + GlideRecord, GlideString, + SortedSetDataType, + StreamEntryDataType, + HashDataType, CoordOrigin, MemberOrigin, SearchOrigin, diff --git a/node/rust-client/src/lib.rs b/node/rust-client/src/lib.rs index 896478f76a..4a9a89d8c8 100644 --- a/node/rust-client/src/lib.rs +++ b/node/rust-client/src/lib.rs @@ -20,7 +20,7 @@ use napi::bindgen_prelude::Uint8Array; use napi::{Env, Error, JsObject, JsUnknown, Result, Status}; use napi_derive::napi; use num_traits::sign::Signed; -use redis::{aio::MultiplexedConnection, AsyncCommands, FromRedisValue, Value}; +use redis::{aio::MultiplexedConnection, AsyncCommands, Value}; #[cfg(feature = "testing_utilities")] use std::collections::HashMap; use std::str; @@ -195,13 +195,17 @@ fn redis_value_to_js(val: Value, js_env: Env, string_decoder: bool) -> Result { - let mut obj = js_env.create_object()?; - for (key, value) in map { - let field_name = String::from_owned_redis_value(key).map_err(to_js_error)?; - let value = redis_value_to_js(value, js_env, string_decoder)?; - obj.set_named_property(&field_name, value)?; + // Convert map to array of key-value pairs instead of a `Record` (object), + // because `Record` does not support `GlideString` as a key. + // The result is in format `GlideRecord`. + let mut js_array = js_env.create_array_with_length(map.len())?; + for (idx, (key, value)) in (0_u32..).zip(map.into_iter()) { + let mut obj = js_env.create_object()?; + obj.set_named_property("key", redis_value_to_js(key, js_env, string_decoder)?)?; + obj.set_named_property("value", redis_value_to_js(value, js_env, string_decoder)?)?; + js_array.set_element(idx, obj)?; } - Ok(obj.into_unknown()) + Ok(js_array.into_unknown()) } Value::Double(float) => js_env.create_double(float).map(|val| val.into_unknown()), Value::Boolean(bool) => js_env.get_boolean(bool).map(|val| val.into_unknown()), diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 073569fe4b..b242ca3304 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -43,7 +43,6 @@ import { RangeByLex, RangeByScore, RestoreOptions, - ReturnTypeXinfoStream, ScoreFilter, SearchOrigin, SetOptions, @@ -58,6 +57,8 @@ import { ZAddOptions, ZScanOptions, convertElementsAndScores, + convertFieldsAndValuesToHashDataType, + convertKeysAndEntries, createAppend, createBLMPop, createBLMove, @@ -325,6 +326,36 @@ export type SortedSetDataType = { }[]; /** + * Data type which represents how data are returned from hashes or insterted there. + * Similar to `Record` - see {@link GlideRecord}. + */ +export type HashDataType = { + /** The hash element name. */ + field: GlideString; + /** The hash element value. */ + value: GlideString; +}[]; + +/** + * Data type which reflects now stream entries are returned. + * The keys of the record are stream entry IDs, which are mapped to key-value pairs of the data. + */ +export type StreamEntryDataType = Record; + +/** + * @internal + * Convert `GlideRecord` recevied after resolving the value pointer into `SortedSetDataType`. + */ +function convertGlideRecordForSortedSet( + res: GlideRecord, +): SortedSetDataType { + return res.map((e) => { + return { element: e.key, score: e.value }; + }); +} + +/** + * @internal * This function converts an input from GlideRecord or Record types to GlideRecord. * * @param keysAndValues - key names and their values. @@ -334,8 +365,8 @@ export function convertGlideRecord( keysAndValues: GlideRecord | Record, ): GlideRecord { if (!Array.isArray(keysAndValues)) { - return Object.entries(keysAndValues).map((e) => { - return { key: e[0], value: e[1] }; + return Object.entries(keysAndValues).map(([key, value]) => { + return { key, value }; }); } @@ -343,50 +374,81 @@ export function convertGlideRecord( } /** - * Data type which represents how data are returned from hashes or insterted there. - * Similar to `Record` - see {@link GlideRecord}. + * @internal + * Recursively downcast `GlideRecord` to `Record`. Use if `data` keys are always strings. */ -export type HashDataType = { - /** The hash element name. */ - field: GlideString; - /** The hash element value. */ - value: GlideString; -}[]; +export function convertGlideRecordToRecord( + data: GlideRecord, +): Record { + const res: Record = {}; + + for (const pair of data) { + let newVal = pair.value; + + if (isGlideRecord(pair.value)) { + newVal = convertGlideRecordToRecord( + pair.value as GlideRecord, + ) as T; + } else if (isGlideRecordArray(pair.value)) { + newVal = (pair.value as GlideRecord[]).map( + convertGlideRecordToRecord, + ) as T; + } + + res[pair.key as string] = newVal; + } + + return res; +} /** - * This function converts an input from HashDataType or Record types to HashDataType. - * - * @param fieldsAndValues - field names and their values. - * @returns HashDataType array containing field names and their values. + * @internal + * Check whether an object is a `GlideRecord` (see {@link GlideRecord}). */ -export function convertHashDataType( - fieldsAndValues: HashDataType | Record, -): HashDataType { - if (!Array.isArray(fieldsAndValues)) { - return Object.entries(fieldsAndValues).map((e) => { - return { field: e[0], value: e[1] }; - }); - } +export function isGlideRecord(obj?: unknown): boolean { + return ( + Array.isArray(obj) && + obj.length > 0 && + typeof obj[0] === "object" && + "key" in obj[0] && + "value" in obj[0] + ); +} - return fieldsAndValues; +/** + * @internal + * Check whether an object is a `GlideRecord[]` (see {@link GlideRecord}). + */ +function isGlideRecordArray(obj?: unknown): boolean { + return Array.isArray(obj) && obj.length > 0 && isGlideRecord(obj[0]); } +/** Represents the return type of {@link xinfoStream} command. */ +export type ReturnTypeXinfoStream = Record< + string, + | StreamEntries + | Record[]>[] +>; + /** - * This function converts an input from Record or GlideRecord types to GlideRecord. - * - * @param record - input record in either Record or GlideRecord types. - * @returns same data in GlideRecord type. + * Represents an array of Stream Entires in the response of {@link xinfoStream} command. + * See {@link ReturnTypeXinfoStream}. */ -export function convertRecordToGlideRecord( - record: Record | GlideRecord, -): GlideRecord { - if (!Array.isArray(record)) { - return Object.entries(record).map((e) => { - return { key: e[0], value: e[1] }; - }); - } +export type StreamEntries = + | GlideString + | number + | (GlideString | number | GlideString[])[][]; - return record; +/** + * @internal + * Reverse of {@link convertGlideRecordToRecord}. + */ +export function convertRecordToGlideRecord( + data: Record, +): GlideRecord { + return Object.entries(data).map(([key, value]) => { + return { key, value }; + }); } /** @@ -606,9 +668,9 @@ function toProtobufRoute( } export interface PubSubMsg { - message: string; - channel: string; - pattern?: string | null; + message: GlideString; + channel: GlideString; + pattern?: GlideString | null; } /** @@ -1042,19 +1104,21 @@ export class BaseClient { let msg: PubSubMsg | null = null; const responsePointer = pushNotification.respPointer; let nextPushNotificationValue: Record = {}; + const isStringDecoder = + (decoder ?? this.defaultDecoder) === Decoder.String; if (responsePointer) { if (typeof responsePointer !== "number") { nextPushNotificationValue = valueFromSplitPointer( responsePointer.high, responsePointer.low, - decoder === Decoder.String, + isStringDecoder, ) as Record; } else { nextPushNotificationValue = valueFromSplitPointer( 0, responsePointer, - decoder === Decoder.String, + isStringDecoder, ) as Record; } @@ -1071,7 +1135,9 @@ export class BaseClient { messageKind === "PMessage" || messageKind === "SMessage" ) { - const values = nextPushNotificationValue["values"] as string[]; + const values = nextPushNotificationValue[ + "values" + ] as GlideString[]; if (messageKind === "PMessage") { msg = { @@ -1845,7 +1911,10 @@ export class BaseClient { fieldsAndValues: HashDataType | Record, ): Promise { return this.createWritePromise( - createHSet(key, convertHashDataType(fieldsAndValues)), + createHSet( + key, + convertFieldsAndValuesToHashDataType(fieldsAndValues), + ), ); } @@ -1986,23 +2055,39 @@ export class BaseClient { return this.createWritePromise(createHExists(key, field)); } - /** Returns all fields and values of the hash stored at `key`. + /** + * Returns all fields and values of the hash stored at `key`. * * @see {@link https://valkey.io/commands/hgetall/|valkey.io} for details. * * @param key - The key of the hash. - * @returns a list of fields and their values stored in the hash. Every field name in the list is followed by its value. + * @param options - (Optional) See {@link DecoderOption}. + * @returns A list of fields and their values stored in the hash. * If `key` does not exist, it returns an empty list. * * @example * ```typescript * // Example usage of the hgetall method * const result = await client.hgetall("my_hash"); - * console.log(result); // Output: {"field1": "value1", "field2": "value2"} + * console.log(result); // Output: + * // [ + * // { field: "field1", value: "value1"}, + * // { field: "field2", value: "value2"} + * // ] * ``` */ - public async hgetall(key: GlideString): Promise> { - return this.createWritePromise(createHGetAll(key)); + public async hgetall( + key: GlideString, + options?: DecoderOption, + ): Promise { + return this.createWritePromise>( + createHGetAll(key), + options, + ).then((res) => + res.map((r) => { + return { field: r.key, value: r.value }; + }), + ); } /** Increments the number stored at `field` in the hash stored at `key` by increment. @@ -3623,8 +3708,10 @@ export class BaseClient { * - Use `value` to specify a stream entry ID. * - Use `isInclusive: false` to specify an exclusive bounded stream entry ID. This is only available starting with Valkey version 6.2.0. * - Use `InfBoundary.PositiveInfinity` to end with the maximum available ID. - * @param count - An optional argument specifying the maximum count of stream entries to return. + * @param options - (Optional) Additional parameters: + * - (Optional) `count`: the maximum count of stream entries to return. * If `count` is not provided, all stream entries in the range will be returned. + * - (Optional) `decoder`: see {@link DecoderOption}. * @returns A map of stream entry ids, to an array of entries, or `null` if `count` is non-positive. * * @example @@ -3643,9 +3730,13 @@ export class BaseClient { key: GlideString, start: Boundary, end: Boundary, - count?: number, - ): Promise | null> { - return this.createWritePromise(createXRange(key, start, end, count)); + options?: { count?: number } & DecoderOption, + ): Promise { + return this.createWritePromise | null>(createXRange(key, start, end, options?.count), options).then( + (res) => (res === null ? null : convertGlideRecordToRecord(res)), + ); } /** @@ -3663,8 +3754,10 @@ export class BaseClient { * - Use `value` to specify a stream entry ID. * - Use `isInclusive: false` to specify an exclusive bounded stream entry ID. This is only available starting with Valkey version 6.2.0. * - Use `InfBoundary.NegativeInfinity` to start with the minimum available ID. - * @param count - An optional argument specifying the maximum count of stream entries to return. + * @param options - (Optional) Additional parameters: + * - (Optional) `count`: the maximum count of stream entries to return. * If `count` is not provided, all stream entries in the range will be returned. + * - (Optional) `decoder`: see {@link DecoderOption}. * @returns A map of stream entry ids, to an array of entries, or `null` if `count` is non-positive. * * @example @@ -3683,9 +3776,16 @@ export class BaseClient { key: GlideString, end: Boundary, start: Boundary, - count?: number, - ): Promise | null> { - return this.createWritePromise(createXRevRange(key, end, start, count)); + options?: { count?: number } & DecoderOption, + ): Promise { + return this.createWritePromise | null>( + createXRevRange(key, end, start, options?.count), + options, + ).then((res) => + res === null ? null : convertGlideRecordToRecord(res), + ); } /** @@ -3897,7 +3997,7 @@ export class BaseClient { * * @param keys - The keys of the sorted sets. * @param options - (Optional) See {@link DecoderOption}. - * @returns A map of elements and their scores representing the difference between the sorted sets. + * @returns A list of elements and their scores representing the difference between the sorted sets. * If the first key does not exist, it is treated as an empty sorted set, and the command returns an empty `array`. * * @example @@ -3906,15 +4006,18 @@ export class BaseClient { * await client.zadd("zset2", {"member2": 2.0}); * await client.zadd("zset3", {"member3": 3.0}); * const result = await client.zdiffWithScores(["zset1", "zset2", "zset3"]); - * console.log(result); // Output: {"member1": 1.0} - "member1" is in "zset1" but not "zset2" or "zset3". + * console.log(result); // Output: "member1" is in "zset1" but not "zset2" or "zset3" + * // [{ element: "member1", score: 1.0 }] * ``` */ public async zdiffWithScores( keys: GlideString[], options?: DecoderOption, - ): Promise> { - // TODO GlideString in Record and add a test - return this.createWritePromise(createZDiffWithScores(keys), options); + ): Promise { + return this.createWritePromise>( + createZDiffWithScores(keys), + options, + ).then(convertGlideRecordForSortedSet); } /** @@ -4128,8 +4231,8 @@ export class BaseClient { * ```typescript * // Example usage of zrange method to retrieve members within a score range in descending order * const result = await client.zrange("my_sorted_set", { - * start: InfBoundary.NegativeInfinity, - * end: { value: 3, isInclusive: false }, + * start: { value: 3, isInclusive: false }, + * end: InfBoundary.NegativeInfinity, * type: "byScore", * }, { reverse: true }); * console.log(result); // Output: members with scores within the range of negative infinity to 3, in descending order @@ -4161,8 +4264,8 @@ export class BaseClient { * @param options - (Optional) Additional parameters: * - (Optional) `reverse`: if `true`, reverses the sorted set, with index `0` as the element with the highest score. * - (Optional) `decoder`: see {@link DecoderOption}. - * @returns A map of elements and their scores within the specified range. - * If `key` does not exist, it is treated as an empty sorted set, and the command returns an empty map. + * @returns A list of elements and their scores within the specified range. + * If `key` does not exist, it is treated as an empty sorted set, and the command returns an empty list. * * @example * ```typescript @@ -4173,29 +4276,29 @@ export class BaseClient { * type: "byScore", * }); * console.log(result); // Output: members with scores between 10 and 20 with their scores - * // {'member1': 10.5, 'member2': 15.2} + * // [{ element: 'member1', score: 10.5 }, { element: 'member2', score: 15.2 }] * ``` * @example * ```typescript * // Example usage of zrangeWithScores method to retrieve members within a score range with their scores * const result = await client.zrangeWithScores("my_sorted_set", { - * start: InfBoundary.NegativeInfinity, - * end: { value: 3, isInclusive: false }, + * start: { value: 3, isInclusive: false }, + * end: InfBoundary.NegativeInfinity, * type: "byScore", * }, { reverse: true }); - * console.log(result); // Output: members with scores within the range of negative infinity to 3, with their scores in descending order - * // {'member7': 1.5, 'member4': -2.0} + * console.log(result); // Output: members with scores within the range of negative infinity to 3, with their scores + * // [{ element: 'member7', score: 1.5 }, { element: 'member4', score: -2.0 }] * ``` */ public async zrangeWithScores( key: GlideString, rangeQuery: RangeByScore | RangeByLex | RangeByIndex, options?: { reverse?: boolean } & DecoderOption, - ): Promise> { - return this.createWritePromise( + ): Promise { + return this.createWritePromise>( createZRangeWithScores(key, rangeQuery, options?.reverse), options, - ); + ).then(convertGlideRecordForSortedSet); } /** @@ -4336,27 +4439,29 @@ export class BaseClient { * - (Optional) `aggregationType`: the aggregation strategy to apply when combining the scores of elements. * If `aggregationType` is not specified, defaults to `AggregationType.SUM`. See {@link AggregationType}. * - (Optional) `decoder`: see {@link DecoderOption}. - * @returns The resulting sorted set with scores. + * @returns A list of elements and their scores representing the intersection of the sorted sets. + * If a key does not exist, it is treated as an empty sorted set, and the command returns an empty result. * * @example * ```typescript * await client.zadd("key1", {"member1": 10.5, "member2": 8.2}); * await client.zadd("key2", {"member1": 9.5}); * const result1 = await client.zinterWithScores(["key1", "key2"]); - * console.log(result1); // Output: {'member1': 20} - "member1" with score of 20 is the result + * console.log(result1); // Output: "member1" with score of 20 is the result + * // [{ element: 'member1', score: 20 }] * const result2 = await client.zinterWithScores(["key1", "key2"], AggregationType.MAX) - * console.log(result2); // Output: {'member1': 10.5} - "member1" with score of 10.5 is the result. + * console.log(result2); // Output: "member1" with score of 10.5 is the result + * // [{ element: 'member1', score: 10.5 }] * ``` */ public async zinterWithScores( keys: GlideString[] | KeyWeight[], options?: { aggregationType?: AggregationType } & DecoderOption, - ): Promise> { - // TODO Record with GlideString and add tests - return this.createWritePromise( + ): Promise { + return this.createWritePromise>( createZInter(keys, options?.aggregationType, true), options, - ); + ).then(convertGlideRecordForSortedSet); } /** @@ -4405,26 +4510,28 @@ export class BaseClient { * - (Optional) `aggregationType`: the aggregation strategy to apply when combining the scores of elements. * If `aggregationType` is not specified, defaults to `AggregationType.SUM`. See {@link AggregationType}. * - (Optional) `decoder`: see {@link DecoderOption}. - * @returns The resulting sorted set with scores. + * @returns A list of elements and their scores representing the intersection of the sorted sets. * * @example * ```typescript * await client.zadd("key1", {"member1": 10.5, "member2": 8.2}); * await client.zadd("key2", {"member1": 9.5}); * const result1 = await client.zunionWithScores(["key1", "key2"]); - * console.log(result1); // {'member1': 20, 'member2': 8.2} + * console.log(result1); // Output: + * // [{ element: 'member1', score: 20 }, { element: 'member2', score: 8.2 }] * const result2 = await client.zunionWithScores(["key1", "key2"], "MAX"); - * console.log(result2); // {'member1': 10.5, 'member2': 8.2} + * console.log(result2); // Output: + * // [{ element: 'member1', score: 10.5}, { element: 'member2', score: 8.2 }] * ``` */ public async zunionWithScores( keys: GlideString[] | KeyWeight[], options?: { aggregationType?: AggregationType } & DecoderOption, - ): Promise> { - return this.createWritePromise( + ): Promise { + return this.createWritePromise>( createZUnion(keys, options?.aggregationType, true), options, - ); + ).then(convertGlideRecordForSortedSet); } /** @@ -4592,9 +4699,9 @@ export class BaseClient { * * @param key - The key of the sorted set. * @param options - (Optional) Additional parameters: - * - (Optional) `count`: the number of elements to pop. If not supplied, only one element will be popped. + * - (Optional) `count`: the maximum number of popped elements. If not specified, pops one member. * - (Optional) `decoder`: see {@link DecoderOption}. - * @returns A map of the removed members and their scores, ordered from the one with the lowest score to the one with the highest. + * @returns A list of the removed members and their scores, ordered from the one with the lowest score to the one with the highest. * If `key` doesn't exist, it will be treated as an empty sorted set and the command returns an empty map. * If `count` is higher than the sorted set's cardinality, returns all members and their scores. * @@ -4602,25 +4709,31 @@ export class BaseClient { * ```typescript * // Example usage of zpopmin method to remove and return the member with the lowest score from a sorted set * const result = await client.zpopmin("my_sorted_set"); - * console.log(result); // Output: {'member1': 5.0} - Indicates that 'member1' with a score of 5.0 has been removed from the sorted set. + * console.log(result); // Output: + * // 'member1' with a score of 5.0 has been removed from the sorted set + * // [{ element: 'member1', score: 5.0 }] * ``` * * @example * ```typescript * // Example usage of zpopmin method to remove and return multiple members with the lowest scores from a sorted set * const result = await client.zpopmin("my_sorted_set", 2); - * console.log(result); // Output: {'member3': 7.5 , 'member2': 8.0} - Indicates that 'member3' with a score of 7.5 and 'member2' with a score of 8.0 have been removed from the sorted set. + * console.log(result); // Output: + * // 'member3' with a score of 7.5 and 'member2' with a score of 8.0 have been removed from the sorted set + * // [ + * // { element: 'member3', score: 7.5 }, + * // { element: 'member2', score: 8.0 } + * // ] * ``` */ public async zpopmin( key: GlideString, options?: { count?: number } & DecoderOption, - ): Promise> { - // TODO GlideString in Record, add tests with binary decoder - return this.createWritePromise( + ): Promise { + return this.createWritePromise>( createZPopMin(key, options?.count), options, - ); + ).then(convertGlideRecordForSortedSet); } /** @@ -4662,9 +4775,9 @@ export class BaseClient { * * @param key - The key of the sorted set. * @param options - (Optional) Additional parameters: - * - (Optional) `count`: the number of elements to pop. If not supplied, only one element will be popped. + * - (Optional) `count`: the maximum number of popped elements. If not specified, pops one member. * - (Optional) `decoder`: see {@link DecoderOption}. - * @returns A map of the removed members and their scores, ordered from the one with the highest score to the one with the lowest. + * @returns A list of the removed members and their scores, ordered from the one with the highest score to the one with the lowest. * If `key` doesn't exist, it will be treated as an empty sorted set and the command returns an empty map. * If `count` is higher than the sorted set's cardinality, returns all members and their scores, ordered from highest to lowest. * @@ -4672,25 +4785,31 @@ export class BaseClient { * ```typescript * // Example usage of zpopmax method to remove and return the member with the highest score from a sorted set * const result = await client.zpopmax("my_sorted_set"); - * console.log(result); // Output: {'member1': 10.0} - Indicates that 'member1' with a score of 10.0 has been removed from the sorted set. + * console.log(result); // Output: + * // 'member1' with a score of 10.0 has been removed from the sorted set + * // [{ element: 'member1', score: 10.0 }] * ``` * * @example * ```typescript * // Example usage of zpopmax method to remove and return multiple members with the highest scores from a sorted set * const result = await client.zpopmax("my_sorted_set", 2); - * console.log(result); // Output: {'member2': 8.0, 'member3': 7.5} - Indicates that 'member2' with a score of 8.0 and 'member3' with a score of 7.5 have been removed from the sorted set. + * console.log(result); // Output: + * // 'member3' with a score of 7.5 and 'member2' with a score of 8.0 have been removed from the sorted set + * // [ + * // { element: 'member3', score: 7.5 }, + * // { element: 'member2', score: 8.0 } + * // ] * ``` */ public async zpopmax( key: GlideString, options?: { count?: number } & DecoderOption, - ): Promise> { - // TODO GlideString in Record, add tests with binary decoder - return this.createWritePromise( + ): Promise { + return this.createWritePromise>( createZPopMax(key, options?.count), options, - ); + ).then(convertGlideRecordForSortedSet); } /** @@ -5069,32 +5188,49 @@ export class BaseClient { * @see {@link https://valkey.io/commands/xread/|valkey.io} for more details. * * @param keys_and_ids - An object of stream keys and entry IDs to read from. - * @param options - (Optional) Parameters detailing how to read the stream - see {@link StreamReadOptions}. - * @returns A `Record` of stream keys, each key is mapped to a `Record` of stream ids, to an `Array` of entries. + * @param options - (Optional) Parameters detailing how to read the stream - see {@link StreamReadOptions} and {@link DecoderOption}. + * @returns A list of stream keys with a `Record` of stream IDs mapped to an `Array` of entries or `null` if key does not exist. * * @example * ```typescript * const streamResults = await client.xread({"my_stream": "0-0", "writers": "0-0"}); * console.log(result); // Output: - * // { - * // "my_stream": { - * // "1526984818136-0": [["duration", "1532"], ["event-id", "5"], ["user-id", "7782813"]], - * // "1526999352406-0": [["duration", "812"], ["event-id", "9"], ["user-id", "388234"]], + * // [ + * // { + * // key: "my_stream", + * // value: { + * // "1526984818136-0": [["duration", "1532"], ["event-id", "5"], ["user-id", "7782813"]], + * // "1526999352406-0": [["duration", "812"], ["event-id", "9"], ["user-id", "388234"]], + * // } * // }, - * // "writers": { - * // "1526985676425-0": [["name", "Virginia"], ["surname", "Woolf"]], - * // "1526985685298-0": [["name", "Jane"], ["surname", "Austen"]], + * // { + * // key: "writers", + * // value: { + * // "1526985676425-0": [["name", "Virginia"], ["surname", "Woolf"]], + * // "1526985685298-0": [["name", "Jane"], ["surname", "Austen"]], + * // } * // } - * // } + * // ] * ``` */ public async xread( keys_and_ids: Record | GlideRecord, - options?: StreamReadOptions, - ): Promise>> { - keys_and_ids = convertRecordToGlideRecord(keys_and_ids); - - return this.createWritePromise(createXRead(keys_and_ids, options)); + options?: StreamReadOptions & DecoderOption, + ): Promise | null> { + return this.createWritePromise + > | null>( + createXRead(convertKeysAndEntries(keys_and_ids), options), + options, + ).then( + (res) => + res?.map((k) => { + return { + key: k.key, + value: convertGlideRecordToRecord(k.value), + }; + }) ?? null, + ); } /** @@ -5106,40 +5242,62 @@ export class BaseClient { * @param consumer - The group consumer. * @param keys_and_ids - An object of stream keys and entry IDs to read from. * Use the special entry ID of `">"` to receive only new messages. - * @param options - (Optional) Parameters detailing how to read the stream - see {@link StreamReadGroupOptions}. - * @returns A map of stream keys, each key is mapped to a map of stream ids, which is mapped to an array of entries. + * @param options - (Optional) Parameters detailing how to read the stream - see {@link StreamReadGroupOptions} and {@link DecoderOption}. + * @returns A list of stream keys with a `Record` of stream IDs mapped to an `Array` of entries. * Returns `null` if there is no stream that can be served. * * @example * ```typescript * const streamResults = await client.xreadgroup("my_group", "my_consumer", {"my_stream": "0-0", "writers_stream": "0-0", "readers_stream", ">"}); * console.log(result); // Output: - * // { - * // "my_stream": { - * // "1526984818136-0": [["duration", "1532"], ["event-id", "5"], ["user-id", "7782813"]], - * // "1526999352406-0": [["duration", "812"], ["event-id", "9"], ["user-id", "388234"]], + * // [ + * // { + * // key: "my_stream", + * // value: { + * // "1526984818136-0": [["duration", "1532"], ["event-id", "5"], ["user-id", "7782813"]], + * // "1526999352406-0": [["duration", "812"], ["event-id", "9"], ["user-id", "388234"]], + * // } * // }, - * // "writers_stream": { - * // "1526985676425-0": [["name", "Virginia"], ["surname", "Woolf"]], - * // "1526985685298-0": null, // entry was deleted + * // { + * // key: "writers_stream", + * // value: { + * // "1526985676425-0": [["name", "Virginia"], ["surname", "Woolf"]], + * // "1526985685298-0": null, // entry was deleted + * // } * // }, - * // "readers_stream": {} // stream is empty - * // } + * // { + * // key: "readers_stream", // stream is empty + * // value: {} + * // } + * // ] * ``` */ public async xreadgroup( group: GlideString, consumer: GlideString, keys_and_ids: Record | GlideRecord, - options?: StreamReadGroupOptions, - ): Promise + options?: StreamReadGroupOptions & DecoderOption, + ): Promise > | null> { - keys_and_ids = convertRecordToGlideRecord(keys_and_ids); - - return this.createWritePromise( - createXReadGroup(group, consumer, keys_and_ids, options), + return this.createWritePromise + > | null>( + createXReadGroup( + group, + consumer, + convertKeysAndEntries(keys_and_ids), + options, + ), + options, + ).then( + (res) => + res?.map((k) => { + return { + key: k.key, + value: convertGlideRecordToRecord(k.value), + }; + }) ?? null, ); } @@ -5240,6 +5398,7 @@ export class BaseClient { * * @param key - The key of the stream. * @param group - The consumer group name. + * @param options - (Optional) See {@link DecoderOption}. * @returns An `Array` of `Records`, where each mapping contains the attributes * of a consumer for the given consumer group of the stream at `key`. * @@ -5261,9 +5420,12 @@ export class BaseClient { public async xinfoConsumers( key: GlideString, group: GlideString, - // TODO: change return type to be compatible with GlideString - ): Promise[]> { - return this.createWritePromise(createXInfoConsumers(key, group)); + options?: DecoderOption, + ): Promise[]> { + return this.createWritePromise[]>( + createXInfoConsumers(key, group), + options, + ).then((res) => res.map(convertGlideRecordToRecord)); } /** @@ -5272,6 +5434,7 @@ export class BaseClient { * @see {@link https://valkey.io/commands/xinfo-groups/|valkey.io} for details. * * @param key - The key of the stream. + * @param options - (Optional) See {@link DecoderOption}. * @returns An array of maps, where each mapping represents the * attributes of a consumer group for the stream at `key`. * @example @@ -5301,8 +5464,13 @@ export class BaseClient { */ public async xinfoGroups( key: string, - ): Promise[]> { - return this.createWritePromise(createXInfoGroups(key)); + options?: DecoderOption, + ): Promise[]> { + return this.createWritePromise< + GlideRecord[] + >(createXInfoGroups(key), options).then((res) => + res.map(convertGlideRecordToRecord), + ); } /** @@ -5335,11 +5503,13 @@ export class BaseClient { minIdleTime: number, ids: GlideString[], options?: StreamClaimOptions & DecoderOption, - ): Promise> { - // TODO: convert Record return type to Object array - return this.createWritePromise( + ): Promise { + return this.createWritePromise< + GlideRecord<[GlideString, GlideString][]> + >( createXClaim(key, group, consumer, minIdleTime, ids, options), - ); + options, + ).then(convertGlideRecordToRecord); } /** @@ -5354,7 +5524,9 @@ export class BaseClient { * @param minIdleTime - The minimum idle time for the message to be claimed. * @param start - Filters the claimed entries to those that have an ID equal or greater than the * specified value. - * @param count - (Optional) Limits the number of claimed entries to the specified value. + * @param options - (Optional) Additional parameters: + * - (Optional) `count`: the number of claimed entries. + * - (Optional) `decoder`: see {@link DecoderOption}. * @returns A `tuple` containing the following elements: * - A stream ID to be used as the start argument for the next call to `XAUTOCLAIM`. This ID is * equivalent to the next ID in the stream after the entries that were scanned, or "0-0" if @@ -5390,11 +5562,28 @@ export class BaseClient { consumer: GlideString, minIdleTime: number, start: GlideString, - count?: number, - ): Promise<[string, Record, string[]?]> { - // TODO: convert Record return type to Object array - return this.createWritePromise( - createXAutoClaim(key, group, consumer, minIdleTime, start, count), + options?: { count?: number } & DecoderOption, + ): Promise<[GlideString, StreamEntryDataType, GlideString[]?]> { + return this.createWritePromise< + [ + GlideString, + GlideRecord<[GlideString, GlideString][]>, + GlideString[], + ] + >( + createXAutoClaim( + key, + group, + consumer, + minIdleTime, + start, + options?.count, + ), + options, + ).then((res) => + res.length === 3 + ? [res[0], convertGlideRecordToRecord(res[1]), res[2]] + : [res[0], convertGlideRecordToRecord(res[1])], ); } @@ -5550,9 +5739,11 @@ export class BaseClient { * @see {@link https://valkey.io/commands/xinfo-stream/|valkey.io} for more details. * * @param key - The key of the stream. - * @param fullOptions - If `true`, returns verbose information with a limit of the first 10 PEL entries. + * @param options - (Optional) Additional parameters: + * - (Optional) `fullOptions`: If `true`, returns verbose information with a limit of the first 10 PEL entries. * If `number` is specified, returns verbose information limiting the returned PEL entries. * If `0` is specified, returns verbose information with no limit. + * - (Optional) `decoder`: see {@link DecoderOption}. * @returns A {@link ReturnTypeXinfoStream} of detailed stream information for the given `key`. See * the example for a sample response. * @@ -5580,27 +5771,27 @@ export class BaseClient { * const infoResult = await client.xinfoStream("my_stream", 15); // limit of 15 entries * console.log(infoResult); * // Output: { - * // length: 2, + * // 'length': 2, * // 'radix-tree-keys': 1, * // 'radix-tree-nodes': 2, * // 'last-generated-id': '1719877599564-1', * // 'max-deleted-entry-id': '0-0', * // 'entries-added': 2, * // 'recorded-first-entry-id': '1719877599564-0', - * // entries: [ [ '1719877599564-0', ['some_field", "some_value', ...] ] ], - * // groups: [ { - * // name: 'group', + * // 'entries': [ [ '1719877599564-0', ['some_field", "some_value', ...] ] ], + * // 'groups': [ { + * // 'name': 'group', * // 'last-delivered-id': '1719877599564-0', * // 'entries-read': 1, - * // lag: 1, + * // 'lag': 1, * // 'pel-count': 1, - * // pending: [ [ '1719877599564-0', 'consumer', 1722624726802, 1 ] ], - * // consumers: [ { - * // name: 'consumer', + * // 'pending': [ [ '1719877599564-0', 'consumer', 1722624726802, 1 ] ], + * // 'consumers': [ { + * // 'name': 'consumer', * // 'seen-time': 1722624726802, * // 'active-time': 1722624726802, * // 'pel-count': 1, - * // pending: [ [ '1719877599564-0', 'consumer', 1722624726802, 1 ] ], + * // 'pending': [ [ '1719877599564-0', 'consumer', 1722624726802, 1 ] ], * // } * // ] * // } @@ -5610,10 +5801,18 @@ export class BaseClient { */ public async xinfoStream( key: GlideString, - fullOptions?: boolean | number, + options?: { fullOptions?: boolean | number } & DecoderOption, ): Promise { - return this.createWritePromise( - createXInfoStream(key, fullOptions ?? false), + return this.createWritePromise< + GlideRecord< + | StreamEntries + | GlideRecord[]>[] + > + >(createXInfoStream(key, options?.fullOptions ?? false), options).then( + (xinfoStream) => + convertGlideRecordToRecord( + xinfoStream, + ) as ReturnTypeXinfoStream, ); } @@ -5695,7 +5894,7 @@ export class BaseClient { * // read messages from streamId * const readResult = await client.xreadgroup(["myfield", "mydata"], "mygroup", "my0consumer"); * // acknowledge messages on stream - * console.log(await client.xack("mystream", "mygroup", [entryId])); // Output: 1L + * console.log(await client.xack("mystream", "mygroup", [entryId])); // Output: 1 * * ``` */ @@ -5722,7 +5921,7 @@ export class BaseClient { * * * @example * ```typescript - * console.log(await client.xgroupSetId("mystream", "mygroup", "0", 1L)); // Output is "OK" + * console.log(await client.xgroupSetId("mystream", "mygroup", "0", 1)); // Output is "OK" * ``` */ public async xgroupSetId( @@ -6442,10 +6641,10 @@ export class BaseClient { * @param modifier - The element pop criteria - either {@link ScoreFilter.MIN} or * {@link ScoreFilter.MAX} to pop the member with the lowest/highest score accordingly. * @param options - (Optional) Additional parameters: - * - (Optional) `count`: the number of elements to pop. If not supplied, only one element will be popped. + * - (Optional) `count`: the maximum number of popped elements. If not specified, pops one member. * - (Optional) `decoder`: see {@link DecoderOption}. * @returns A two-element `array` containing the key name of the set from which the element - * was popped, and a member-score `Record` of the popped element. + * was popped, and a {@link SortedSetDataType} of the popped elements. * If no member could be popped, returns `null`. * * @example @@ -6453,18 +6652,25 @@ export class BaseClient { * await client.zadd("zSet1", { one: 1.0, two: 2.0, three: 3.0 }); * await client.zadd("zSet2", { four: 4.0 }); * console.log(await client.zmpop(["zSet1", "zSet2"], ScoreFilter.MAX, 2)); - * // Output: [ "zSet1", { three: 3, two: 2 } ] - "three" with score 3 and "two" with score 2 were popped from "zSet1". + * // Output: + * // "three" with score 3 and "two" with score 2 were popped from "zSet1" + * // [ "zSet1", [ + * // { element: 'three', score: 3 }, + * // { element: 'two', score: 2 } + * // ] ] * ``` */ public async zmpop( keys: GlideString[], modifier: ScoreFilter, options?: { count?: number } & DecoderOption, - ): Promise<[string, Record] | null> { - // TODO GlideString in Record, add tests with binary decoder - return this.createWritePromise( - createZMPop(keys, modifier, options?.count), - options, + ): Promise<[GlideString, SortedSetDataType] | null> { + return this.createWritePromise< + [GlideString, GlideRecord] | null + >(createZMPop(keys, modifier, options?.count), options).then((res) => + res === null + ? null + : [res[0], convertGlideRecordForSortedSet(res[1])], ); } @@ -6484,10 +6690,10 @@ export class BaseClient { * @param timeout - The number of seconds to wait for a blocking operation to complete. * A value of 0 will block indefinitely. * @param options - (Optional) Additional parameters: - * - (Optional) `count`: the number of elements to pop. If not supplied, only one element will be popped. + * - (Optional) `count`: the maximum number of popped elements. If not specified, pops one member. * - (Optional) `decoder`: see {@link DecoderOption}. * @returns A two-element `array` containing the key name of the set from which the element - * was popped, and a member-score `Record` of the popped element. + * was popped, and a {@link SortedSetDataType} of the popped elements. * If no member could be popped, returns `null`. * * @example @@ -6495,7 +6701,12 @@ export class BaseClient { * await client.zadd("zSet1", { one: 1.0, two: 2.0, three: 3.0 }); * await client.zadd("zSet2", { four: 4.0 }); * console.log(await client.bzmpop(["zSet1", "zSet2"], ScoreFilter.MAX, 0.1, 2)); - * // Output: [ "zSet1", { three: 3, two: 2 } ] - "three" with score 3 and "two" with score 2 were popped from "zSet1". + * // Output: + * // "three" with score 3 and "two" with score 2 were popped from "zSet1" + * // [ "zSet1", [ + * // { element: 'three', score: 3 }, + * // { element: 'two', score: 2 } + * // ] ] * ``` */ public async bzmpop( @@ -6503,11 +6714,14 @@ export class BaseClient { modifier: ScoreFilter, timeout: number, options?: { count?: number } & DecoderOption, - ): Promise<[string, Record] | null> { - // TODO GlideString in Record - return this.createWritePromise( - createBZMPop(keys, modifier, timeout, options?.count), - options, + ): Promise<[GlideString, SortedSetDataType] | null> { + return this.createWritePromise< + [GlideString, GlideRecord] | null + >(createBZMPop(keys, modifier, timeout, options?.count), options).then( + (res) => + res === null + ? null + : [res[0], convertGlideRecordForSortedSet(res[1])], ); } @@ -6785,9 +6999,10 @@ export class BaseClient { minMatchLen?: number; }, ): Promise> { - return this.createWritePromise( - createLCS(key1, key2, { idx: options ?? {} }), - { decoder: Decoder.String }, + return this.createWritePromise< + GlideRecord<(number | [number, number])[][] | number> + >(createLCS(key1, key2, { idx: options ?? {} })).then( + convertGlideRecordToRecord, ); } @@ -6926,28 +7141,39 @@ export class BaseClient { * Pops one or more elements from the first non-empty list from the provided `keys`. * * @see {@link https://valkey.io/commands/lmpop/|valkey.io} for more details. - * @remarks When in cluster mode, all `key`s must map to the same hash slot. + * @remarks When in cluster mode, all `keys` must map to the same hash slot. * @remarks Since Valkey version 7.0.0. * - * @param keys - An array of keys to lists. + * @param keys - An array of keys. * @param direction - The direction based on which elements are popped from - see {@link ListDirection}. - * @param count - (Optional) The maximum number of popped elements. - * @returns A `Record` of key-name mapped array of popped elements. + * @param options - (Optional) Additional parameters: + * - (Optional) `count`: the maximum number of popped elements. If not specified, pops one member. + * - (Optional) `decoder`: see {@link DecoderOption}. + * @returns A `Record` which stores the key name where elements were popped out and the array of popped elements. * * @example * ```typescript * await client.lpush("testKey", ["one", "two", "three"]); * await client.lpush("testKey2", ["five", "six", "seven"]); * const result = await client.lmpop(["testKey", "testKey2"], ListDirection.LEFT, 1L); - * console.log(result.get("testKey")); // Output: { "testKey": ["three"] } + * console.log(result); // Output: { key: "testKey", elements: ["three"] } * ``` */ public async lmpop( keys: GlideString[], direction: ListDirection, - count?: number, - ): Promise> { - return this.createWritePromise(createLMPop(keys, direction, count)); + options?: { count?: number } & DecoderOption, + ): Promise<{ key: GlideString; elements: GlideString[] } | null> { + return this.createWritePromise | null>( + createLMPop(keys, direction, options?.count), + options, + ).then((res) => + res === null + ? null + : res!.map((r) => { + return { key: r.key, elements: r.value }; + })[0], + ); } /** @@ -6955,32 +7181,41 @@ export class BaseClient { * provided `key`. `BLMPOP` is the blocking variant of {@link lmpop}. * * @see {@link https://valkey.io/commands/blmpop/|valkey.io} for more details. - * @remarks When in cluster mode, all `key`s must map to the same hash slot. + * @remarks When in cluster mode, all `keys` must map to the same hash slot. * @remarks Since Valkey version 7.0.0. * - * @param keys - An array of keys to lists. + * @param keys - An array of keys. * @param direction - The direction based on which elements are popped from - see {@link ListDirection}. * @param timeout - The number of seconds to wait for a blocking operation to complete. A value of `0` will block indefinitely. - * @param count - (Optional) The maximum number of popped elements. - * @returns - A `Record` of `key` name mapped array of popped elements. + * @param options - (Optional) Additional parameters: + * - (Optional) `count`: the maximum number of popped elements. If not specified, pops one member. + * - (Optional) `decoder`: see {@link DecoderOption}. + * @returns A `Record` which stores the key name where elements were popped out and the array of popped elements. * If no member could be popped and the timeout expired, returns `null`. * * @example * ```typescript * await client.lpush("testKey", ["one", "two", "three"]); * await client.lpush("testKey2", ["five", "six", "seven"]); - * const result = await client.blmpop(["testKey", "testKey2"], ListDirection.LEFT, 0.1, 1L); - * console.log(result.get("testKey")); // Output: { "testKey": ["three"] } + * const result = await client.blmpop(["testKey", "testKey2"], ListDirection.LEFT, 0.1, 1); + * console.log(result"testKey"); // Output: { key: "testKey", elements: ["three"] } * ``` */ public async blmpop( keys: GlideString[], direction: ListDirection, timeout: number, - count?: number, - ): Promise> { - return this.createWritePromise( - createBLMPop(keys, direction, timeout, count), + options?: { count?: number } & DecoderOption, + ): Promise<{ key: GlideString; elements: GlideString[] } | null> { + return this.createWritePromise | null>( + createBLMPop(keys, direction, timeout, options?.count), + options, + ).then((res) => + res === null + ? null + : res!.map((r) => { + return { key: r.key, elements: r.value }; + })[0], ); } @@ -7039,28 +7274,35 @@ export class BaseClient { /** * Returns the number of subscribers (exclusive of clients subscribed to patterns) for the specified channels. * - * Note that it is valid to call this command without channels. In this case, it will just return an empty map. - * The command is routed to all nodes, and aggregates the response to a single map of the channels and their number of subscriptions. - * * @see {@link https://valkey.io/commands/pubsub-numsub/|valkey.io} for more details. + * @remarks When in cluster mode, the command is routed to all nodes, and aggregates the response into a single list. * * @param channels - The list of channels to query for the number of subscribers. - * If not provided, returns an empty map. - * @returns A map where keys are the channel names and values are the number of subscribers. + * @param options - (Optional) see {@link DecoderOption}. + * @returns A list of the channel names and their numbers of subscribers. * * @example * ```typescript * const result1 = await client.pubsubNumsub(["channel1", "channel2"]); - * console.log(result1); // Output: { "channel1": 3, "channel2": 5 } + * console.log(result1); // Output: + * // [{ channel: "channel1", numSub: 3}, { channel: "channel2", numSub: 5 }] * - * const result2 = await client.pubsubNumsub(); - * console.log(result2); // Output: {} + * const result2 = await client.pubsubNumsub([]); + * console.log(result2); // Output: [] * ``` */ public async pubsubNumSub( - channels?: string[], - ): Promise> { - return this.createWritePromise(createPubSubNumSub(channels)); + channels: GlideString[], + options?: DecoderOption, + ): Promise<{ channel: GlideString; numSub: number }[]> { + return this.createWritePromise>( + createPubSubNumSub(channels), + options, + ).then((res) => + res.map((r) => { + return { channel: r.key, numSub: r.value }; + }), + ); } /** diff --git a/node/src/Commands.ts b/node/src/Commands.ts index 8775ec7889..c2ee748f8b 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -7,15 +7,16 @@ import Long from "long"; import { BaseClient, // eslint-disable-line @typescript-eslint/no-unused-vars + convertRecordToGlideRecord, GlideRecord, + GlideString, HashDataType, SortedSetDataType, -} from "src/BaseClient"; +} from "./BaseClient"; /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ -import { GlideClient } from "src/GlideClient"; +import { GlideClient } from "./GlideClient"; /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ -import { GlideClusterClient } from "src/GlideClusterClient"; -import { GlideString } from "./BaseClient"; +import { GlideClusterClient } from "./GlideClusterClient"; import { command_request } from "./ProtobufMessage"; import RequestType = command_request.RequestType; @@ -415,6 +416,24 @@ export function createHGet( return createCommand(RequestType.HGet, [key, field]); } +/** + * This function converts an input from {@link HashDataType} or `Record` types to `HashDataType`. + * + * @param fieldsAndValues - field names and their values. + * @returns HashDataType array containing field names and their values. + */ +export function convertFieldsAndValuesToHashDataType( + fieldsAndValues: HashDataType | Record, +): HashDataType { + if (!Array.isArray(fieldsAndValues)) { + return Object.entries(fieldsAndValues).map(([field, value]) => { + return { field, value }; + }); + } + + return fieldsAndValues; +} + /** * @internal */ @@ -2342,7 +2361,7 @@ export interface FunctionListOptions { /** Type of the response of `FUNCTION LIST` command. */ export type FunctionListResponse = Record< string, - GlideString | Record[] + GlideString | Record[] >[]; /** @@ -2534,6 +2553,23 @@ export enum FlushMode { ASYNC = "ASYNC", } +/** + * @internal + * This function converts an input from Record or GlideRecord types to GlideRecord. + * + * @param record - input record in either Record or GlideRecord types. + * @returns same data in GlideRecord type. + */ +export function convertKeysAndEntries( + record: Record | GlideRecord, +): GlideRecord { + if (!Array.isArray(record)) { + return convertRecordToGlideRecord(record); + } + + return record; +} + /** Optional arguments for {@link BaseClient.xread|xread} command. */ export interface StreamReadOptions { /** @@ -2615,21 +2651,6 @@ export function createXReadGroup( return createCommand(RequestType.XReadGroup, args); } -/** - * Represents a the return type for XInfo Stream in the response - */ -// TODO: change return type to be compatible with GlideString -export type ReturnTypeXinfoStream = Record< - string, - | StreamEntries - | Record[]>[] ->; - -/** - * Represents an array of Stream Entires in the response - */ -export type StreamEntries = string | number | (string | number | string[])[][]; - /** * @internal */ @@ -3934,7 +3955,7 @@ export function createPubSubNumPat(): command_request.Command { * @internal */ export function createPubSubNumSub( - channels?: string[], + channels?: GlideString[], ): command_request.Command { return createCommand(RequestType.PubSubNumSub, channels ? channels : []); } @@ -3952,7 +3973,7 @@ export function createPubsubShardChannels( * @internal */ export function createPubSubShardNumSub( - channels?: string[], + channels?: GlideString[], ): command_request.Command { return createCommand(RequestType.PubSubSNumSub, channels ? channels : []); } diff --git a/node/src/GlideClient.ts b/node/src/GlideClient.ts index f30d9909c9..ed053d2649 100644 --- a/node/src/GlideClient.ts +++ b/node/src/GlideClient.ts @@ -6,22 +6,16 @@ import * as net from "net"; import { BaseClient, BaseClientConfiguration, + convertGlideRecordToRecord, Decoder, - DecoderOption, // eslint-disable-line @typescript-eslint/no-unused-vars + DecoderOption, + GlideRecord, GlideReturnType, GlideString, PubSubMsg, ReadFrom, // eslint-disable-line @typescript-eslint/no-unused-vars } from "./BaseClient"; import { - FlushMode, - FunctionListOptions, - FunctionListResponse, - FunctionRestorePolicy, - FunctionStatsFullResponse, - InfoOptions, - LolwutOptions, - SortOptions, createClientGetName, createClientId, createConfigGet, @@ -54,6 +48,14 @@ import { createSortReadOnly, createTime, createUnWatch, + FlushMode, + FunctionListOptions, + FunctionListResponse, + FunctionRestorePolicy, + FunctionStatsFullResponse, + InfoOptions, + LolwutOptions, + SortOptions, } from "./Commands"; import { connection_request } from "./ProtobufMessage"; import { Transaction } from "./Transaction"; @@ -394,7 +396,10 @@ export class GlideClient extends BaseClient { parameters: string[], options?: DecoderOption, ): Promise> { - return this.createWritePromise(createConfigGet(parameters), options); + return this.createWritePromise>( + createConfigGet(parameters), + options, + ).then(convertGlideRecordToRecord); } /** @@ -646,7 +651,13 @@ export class GlideClient extends BaseClient { public async functionList( options?: FunctionListOptions & DecoderOption, ): Promise { - return this.createWritePromise(createFunctionList(options), options); + return this.createWritePromise[]>( + createFunctionList(options), + options, + ).then( + (res) => + res.map(convertGlideRecordToRecord) as FunctionListResponse, + ); } /** @@ -697,7 +708,13 @@ export class GlideClient extends BaseClient { public async functionStats( options?: DecoderOption, ): Promise { - return this.createWritePromise(createFunctionStats(), options); + return this.createWritePromise>( + createFunctionStats(), + options, + ).then( + (res) => + convertGlideRecordToRecord(res) as FunctionStatsFullResponse, + ); } /** diff --git a/node/src/GlideClusterClient.ts b/node/src/GlideClusterClient.ts index 8c263aff80..d9ee9d2250 100644 --- a/node/src/GlideClusterClient.ts +++ b/node/src/GlideClusterClient.ts @@ -7,11 +7,13 @@ import { BaseClient, BaseClientConfiguration, Decoder, - DecoderOption, // eslint-disable-line @typescript-eslint/no-unused-vars - GlideReturnType, + DecoderOption, + GlideRecord, GlideString, PubSubMsg, ReadFrom, // eslint-disable-line @typescript-eslint/no-unused-vars + GlideReturnType, + convertGlideRecordToRecord, } from "./BaseClient"; import { FlushMode, @@ -160,6 +162,38 @@ export type GlideClusterClientConfiguration = BaseClientConfiguration & { */ export type ClusterResponse = T | Record; +/** + * @internal + * Type which returns GLIDE core for commands routed to multiple nodes. + * Should be converted to {@link ClusterResponse}. + */ +type ClusterGlideRecord = GlideRecord | T; + +/** + * @internal + * Convert {@link ClusterGlideRecord} to {@link ClusterResponse}. + * + * @param res - Value received from Glide core. + * @param isRoutedToSingleNodeByDefault - Default routing policy. + * @param route - The route. + * @returns Converted value. + */ +function convertClusterGlideRecord( + res: ClusterGlideRecord, + isRoutedToSingleNodeByDefault: boolean, + route?: Routes, +): ClusterResponse { + const isSingleNodeResponse = + // route not given and command is routed by default to a random node + (!route && isRoutedToSingleNodeByDefault) || + // or route is given and it is a single node route + (Boolean(route) && route !== "allPrimaries" && route !== "allNodes"); + + return isSingleNodeResponse + ? (res as T) + : convertGlideRecordToRecord(res as GlideRecord); +} + export interface SlotIdTypes { /** * `replicaSlotId` overrides the `readFrom` configuration. If it's used the request @@ -398,10 +432,10 @@ export class GlideClusterClient extends BaseClient { public async info( options?: { sections?: InfoOptions[] } & RouteOption, ): Promise> { - return this.createWritePromise>( + return this.createWritePromise>( createInfo(options?.sections), { decoder: Decoder.String, ...options }, - ); + ).then((res) => convertClusterGlideRecord(res, false, options?.route)); } /** @@ -434,10 +468,10 @@ export class GlideClusterClient extends BaseClient { public async clientGetName( options?: RouteOption & DecoderOption, ): Promise> { - return this.createWritePromise>( + return this.createWritePromise>( createClientGetName(), options, - ); + ).then((res) => convertClusterGlideRecord(res, true, options?.route)); } /** @@ -508,10 +542,10 @@ export class GlideClusterClient extends BaseClient { public async clientId( options?: RouteOption, ): Promise> { - return this.createWritePromise>( + return this.createWritePromise>( createClientId(), options, - ); + ).then((res) => convertClusterGlideRecord(res, true, options?.route)); } /** @@ -545,7 +579,11 @@ export class GlideClusterClient extends BaseClient { parameters: string[], options?: RouteOption & DecoderOption, ): Promise>> { - return this.createWritePromise(createConfigGet(parameters), options); + return this.createWritePromise< + ClusterGlideRecord> + >(createConfigGet(parameters), options).then((res) => + convertGlideRecordToRecord(res as GlideRecord), + ); } /** @@ -605,7 +643,10 @@ export class GlideClusterClient extends BaseClient { message: GlideString, options?: RouteOption & DecoderOption, ): Promise> { - return this.createWritePromise(createEcho(message), options); + return this.createWritePromise>( + createEcho(message), + options, + ).then((res) => convertClusterGlideRecord(res, true, options?.route)); } /** @@ -641,10 +682,10 @@ export class GlideClusterClient extends BaseClient { public async time( options?: RouteOption, ): Promise> { - return this.createWritePromise(createTime(), { - decoder: Decoder.String, - ...options, - }); + return this.createWritePromise>( + createTime(), + options, + ).then((res) => convertClusterGlideRecord(res, true, options?.route)); } /** @@ -696,15 +737,17 @@ export class GlideClusterClient extends BaseClient { public async lolwut( options?: LolwutOptions & RouteOption, ): Promise> { - return this.createWritePromise(createLolwut(options), { - decoder: Decoder.String, - ...options, - }); + return this.createWritePromise>( + createLolwut(options), + options, + ).then((res) => convertClusterGlideRecord(res, true, options?.route)); } /** * Invokes a previously loaded function. * + * The command will be routed to a random node, unless `route` is provided. + * * @see {@link https://valkey.io/commands/fcall/|valkey.io} for details. * @remarks Since Valkey version 7.0.0. * @@ -724,12 +767,17 @@ export class GlideClusterClient extends BaseClient { args: GlideString[], options?: RouteOption & DecoderOption, ): Promise> { - return this.createWritePromise(createFCall(func, [], args), options); + return this.createWritePromise>( + createFCall(func, [], args), + options, + ).then((res) => convertClusterGlideRecord(res, true, options?.route)); } /** * Invokes a previously loaded read-only function. * + * The command will be routed to a random node, unless `route` is provided. + * * @see {@link https://valkey.io/commands/fcall/|valkey.io} for details. * @remarks Since Valkey version 7.0.0. * @@ -750,10 +798,10 @@ export class GlideClusterClient extends BaseClient { args: GlideString[], options?: RouteOption & DecoderOption, ): Promise> { - return this.createWritePromise( + return this.createWritePromise>( createFCallReadOnly(func, [], args), options, - ); + ).then((res) => convertClusterGlideRecord(res, true, options?.route)); } /** @@ -848,6 +896,8 @@ export class GlideClusterClient extends BaseClient { /** * Returns information about the functions and libraries. * + * The command will be routed to a random node, unless `route` is provided. + * * @see {@link https://valkey.io/commands/function-list/|valkey.io} for details. * @remarks Since Valkey version 7.0.0. * @@ -876,13 +926,29 @@ export class GlideClusterClient extends BaseClient { public async functionList( options?: FunctionListOptions & DecoderOption & RouteOption, ): Promise> { - return this.createWritePromise(createFunctionList(options), options); + return this.createWritePromise< + GlideRecord | GlideRecord[] + >(createFunctionList(options), options).then((res) => + res.length == 0 + ? (res as FunctionListResponse) // no libs + : ((Array.isArray(res[0]) + ? // single node response + ((res as GlideRecord[]).map( + convertGlideRecordToRecord, + ) as FunctionListResponse) + : // multi node response + convertGlideRecordToRecord( + res as GlideRecord, + )) as ClusterResponse), + ); } /** * Returns information about the function that's currently running and information about the * available execution engines. * + * The command will be routed to all primary nodes, unless `route` is provided. + * * @see {@link https://valkey.io/commands/function-stats/|valkey.io} for details. * @remarks Since Valkey version 7.0.0. * @@ -929,7 +995,14 @@ export class GlideClusterClient extends BaseClient { public async functionStats( options?: RouteOption & DecoderOption, ): Promise> { - return this.createWritePromise(createFunctionStats(), options); + return this.createWritePromise< + ClusterGlideRecord> + >(createFunctionStats(), options).then( + (res) => + convertGlideRecordToRecord( + res, + ) as ClusterResponse, + ); } /** @@ -972,10 +1045,10 @@ export class GlideClusterClient extends BaseClient { public async functionDump( options?: RouteOption, ): Promise> { - return this.createWritePromise(createFunctionDump(), { - decoder: Decoder.Bytes, - ...options, - }); + return this.createWritePromise>( + createFunctionDump(), + { decoder: Decoder.Bytes, ...options }, + ).then((res) => convertClusterGlideRecord(res, true, options?.route)); } /** @@ -1081,10 +1154,8 @@ export class GlideClusterClient extends BaseClient { * console.log("Number of keys across all primary nodes: ", numKeys); * ``` */ - public async dbsize( - options?: RouteOption, - ): Promise> { - return this.createWritePromise(createDBSize(), options); + public async dbsize(options?: RouteOption): Promise { + return this.createWritePromise(createDBSize(), options); } /** Publish a message on pubsub channel. @@ -1159,28 +1230,35 @@ export class GlideClusterClient extends BaseClient { /** * Returns the number of subscribers (exclusive of clients subscribed to patterns) for the specified shard channels. * - * Note that it is valid to call this command without channels. In this case, it will just return an empty map. - * The command is routed to all nodes, and aggregates the response to a single map of the channels and their number of subscriptions. - * * @see {@link https://valkey.io/commands/pubsub-shardnumsub/|valkey.io} for details. + * @remarks The command is routed to all nodes, and aggregates the response into a single list. * * @param channels - The list of shard channels to query for the number of subscribers. - * If not provided, returns an empty map. - * @returns A map where keys are the shard channel names and values are the number of subscribers. + * @param options - (Optional) see {@link DecoderOption}. + * @returns A list of the shard channel names and their numbers of subscribers. * * @example * ```typescript * const result1 = await client.pubsubShardnumsub(["channel1", "channel2"]); - * console.log(result1); // Output: { "channel1": 3, "channel2": 5 } + * console.log(result1); // Output: + * // [{ channel: "channel1", numSub: 3}, { channel: "channel2", numSub: 5 }] * - * const result2 = await client.pubsubShardnumsub(); - * console.log(result2); // Output: {} + * const result2 = await client.pubsubShardnumsub([]); + * console.log(result2); // Output: [] * ``` */ public async pubsubShardNumSub( - channels?: string[], - ): Promise> { - return this.createWritePromise(createPubSubShardNumSub(channels)); + channels: GlideString[], + options?: DecoderOption, + ): Promise<{ channel: GlideString; numSub: number }[]> { + return this.createWritePromise>( + createPubSubShardNumSub(channels), + options, + ).then((res) => + res.map((r) => { + return { channel: r.key, numSub: r.value }; + }), + ); } /** @@ -1295,7 +1373,10 @@ export class GlideClusterClient extends BaseClient { public async lastsave( options?: RouteOption, ): Promise> { - return this.createWritePromise(createLastSave(), options); + return this.createWritePromise>( + createLastSave(), + options, + ).then((res) => convertClusterGlideRecord(res, true, options?.route)); } /** diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 7ee83fb778..5f9ba588ce 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -10,7 +10,6 @@ import { ReadFrom, // eslint-disable-line @typescript-eslint/no-unused-vars SortedSetDataType, convertGlideRecord, - convertHashDataType, } from "./BaseClient"; import { @@ -54,7 +53,6 @@ import { RangeByLex, RangeByScore, RestoreOptions, - ReturnTypeXinfoStream, // eslint-disable-line @typescript-eslint/no-unused-vars ScoreFilter, SearchOrigin, SetOptions, @@ -71,6 +69,7 @@ import { ZAddOptions, ZScanOptions, convertElementsAndScores, + convertFieldsAndValuesToHashDataType, createAppend, createBLMPop, createBLMove, @@ -828,7 +827,10 @@ export class BaseTransaction> { fieldsAndValues: HashDataType | Record, ): T { return this.addAndReturn( - createHSet(key, convertHashDataType(fieldsAndValues)), + createHSet( + key, + convertFieldsAndValuesToHashDataType(fieldsAndValues), + ), ); } @@ -901,13 +903,15 @@ export class BaseTransaction> { return this.addAndReturn(createHExists(key, field)); } - /** Returns all fields and values of the hash stored at `key`. + /** + * Returns all fields and values of the hash stored at `key`. + * * @see {@link https://valkey.io/commands/hgetall/|valkey.io} for details. * * @param key - The key of the hash. * - * Command Response - a map of fields and their values stored in the hash. Every field name in the map is followed by its value. - * If `key` does not exist, it returns an empty map. + * Command Response - A list of fields and their values stored in the hash. + * If `key` does not exist, it returns an empty list. */ public hgetall(key: GlideString): T { return this.addAndReturn(createHGetAll(key)); @@ -1866,8 +1870,9 @@ export class BaseTransaction> { * * @param keys - The keys of the sorted sets. * - * Command Response - A map of elements and their scores representing the difference between the sorted sets. - * If the first key does not exist, it is treated as an empty sorted set, and the command returns an empty `array`. + * Command Response - A list of elements and their scores representing the difference between the sorted sets. + * If the first key does not exist, it is treated as an empty sorted set, and the command returns an empty `array`. + * The response comes in format `GlideRecord`, see {@link GlideRecord}. */ public zdiffWithScores(keys: GlideString[]): T { return this.addAndReturn(createZDiffWithScores(keys)); @@ -2006,8 +2011,9 @@ export class BaseTransaction> { * - For range queries by score, use {@link RangeByScore}. * @param reverse - If `true`, reverses the sorted set, with index `0` as the element with the highest score. * - * Command Response - A map of elements and their scores within the specified range. - * If `key` does not exist, it is treated as an empty sorted set, and the command returns an empty map. + * Command Response - A list of elements and their scores within the specified range. + * If `key` does not exist, it is treated as an empty sorted set, and the command returns an empty list. + * The response comes in format `GlideRecord`, see {@link GlideRecord}. */ public zrangeWithScores( key: GlideString, @@ -2107,7 +2113,9 @@ export class BaseTransaction> { * @param aggregationType - (Optional) Specifies the aggregation strategy to apply when combining the scores of elements. See {@link AggregationType}. * If `aggregationType` is not specified, defaults to `AggregationType.SUM`. * - * Command Response - The resulting sorted set with scores. + * Command Response - A list of elements and their scores representing the intersection of the sorted sets. + * If a key does not exist, it is treated as an empty sorted set, and the command returns an empty result. + * The response comes in format `GlideRecord`, see {@link GlideRecord}. */ public zinterWithScores( keys: GlideString[] | KeyWeight[], @@ -2147,7 +2155,8 @@ export class BaseTransaction> { * @param aggregationType - (Optional) Specifies the aggregation strategy to apply when combining the scores of elements. See {@link AggregationType}. * If `aggregationType` is not specified, defaults to `AggregationType.SUM`. * - * Command Response - The resulting sorted set with scores. + * Command Response - A list of elements and their scores representing the intersection of the sorted sets. + * The response comes in format `GlideRecord`, see {@link GlideRecord}. */ public zunionWithScores( keys: GlideString[] | KeyWeight[], @@ -2241,9 +2250,10 @@ export class BaseTransaction> { * @param key - The key of the sorted set. * @param count - Specifies the quantity of members to pop. If not specified, pops one member. * - * Command Response - A map of the removed members and their scores, ordered from the one with the lowest score to the one with the highest. - * If `key` doesn't exist, it will be treated as an empty sorted set and the command returns an empty map. - * If `count` is higher than the sorted set's cardinality, returns all members and their scores. + * Command Response - A list of the removed members and their scores, ordered from the one with the lowest score to the one with the highest. + * If `key` doesn't exist, it will be treated as an empty sorted set and the command returns an empty map. + * If `count` is higher than the sorted set's cardinality, returns all members and their scores. + * The response comes in format `GlideRecord`, see {@link GlideRecord}. */ public zpopmin(key: GlideString, count?: number): T { return this.addAndReturn(createZPopMin(key, count)); @@ -2278,9 +2288,10 @@ export class BaseTransaction> { * @param key - The key of the sorted set. * @param count - Specifies the quantity of members to pop. If not specified, pops one member. * - * Command Response - A map of the removed members and their scores, ordered from the one with the highest score to the one with the lowest. - * If `key` doesn't exist, it will be treated as an empty sorted set and the command returns an empty map. - * If `count` is higher than the sorted set's cardinality, returns all members and their scores, ordered from highest to lowest. + * Command Response - A list of the removed members and their scores, ordered from the one with the lowest score to the one with the highest. + * If `key` doesn't exist, it will be treated as an empty sorted set and the command returns an empty map. + * If `count` is higher than the sorted set's cardinality, returns all members and their scores. + * The response comes in format `GlideRecord`, see {@link GlideRecord}. */ public zpopmax(key: GlideString, count?: number): T { return this.addAndReturn(createZPopMax(key, count)); @@ -2601,8 +2612,9 @@ export class BaseTransaction> { * If `number` is specified, returns verbose information limiting the returned PEL entries. * If `0` is specified, returns verbose information with no limit. * - * Command Response - A {@link ReturnTypeXinfoStream} of detailed stream information for the given `key`. + * Command Response - Detailed stream information for the given `key`. * See example of {@link BaseClient.xinfoStream} for more details. + * The response comes in format `GlideRecord[]>[]>`, see {@link GlideRecord}. */ public xinfoStream(key: GlideString, fullOptions?: boolean | number): T { return this.addAndReturn(createXInfoStream(key, fullOptions ?? false)); @@ -2617,6 +2629,7 @@ export class BaseTransaction> { * * Command Response - An `Array` of `Records`, where each mapping represents the * attributes of a consumer group for the stream at `key`. + * The response comes in format `GlideRecord[]`, see {@link GlideRecord}. */ public xinfoGroups(key: string): T { return this.addAndReturn(createXInfoGroups(key)); @@ -2652,7 +2665,8 @@ export class BaseTransaction> { * @param count - An optional argument specifying the maximum count of stream entries to return. * If `count` is not provided, all stream entries in the range will be returned. * - * Command Response - A map of stream entry ids, to an array of entries, or `null` if `count` is non-positive. + * Command Response - A list of stream entry ids, to an array of entries, or `null` if `count` is non-positive. + * The response comes in format `GlideRecord<[GlideString, GlideString][]> | null`, see {@link GlideRecord}. */ public xrange( key: GlideString, @@ -2681,7 +2695,8 @@ export class BaseTransaction> { * @param count - An optional argument specifying the maximum count of stream entries to return. * If `count` is not provided, all stream entries in the range will be returned. * - * Command Response - A map of stream entry ids, to an array of entries, or `null` if `count` is non-positive. + * Command Response - A list of stream entry ids, to an array of entries, or `null` if `count` is non-positive. + * The response comes in format `GlideRecord<[GlideString, GlideString][]> | null`, see {@link GlideRecord}. */ public xrevrange( key: GlideString, @@ -2700,7 +2715,8 @@ export class BaseTransaction> { * @param keys_and_ids - An object of stream keys and entry IDs to read from. * @param options - (Optional) Parameters detailing how to read the stream - see {@link StreamReadOptions}. * - * Command Response - A `Record` of stream keys, each key is mapped to a `Record` of stream ids, to an `Array` of entries. + * Command Response - A list of stream keys with a `Record` of stream IDs mapped to an `Array` of entries or `null` if key does not exist. + * The response comes in format `GlideRecord>`, see {@link GlideRecord}. */ public xread( keys_and_ids: Record | GlideRecord, @@ -2726,8 +2742,9 @@ export class BaseTransaction> { * Use the special ID of `">"` to receive only new messages. * @param options - (Optional) Parameters detailing how to read the stream - see {@link StreamReadGroupOptions}. * - * Command Response - A map of stream keys, each key is mapped to a map of stream ids, which is mapped to an array of entries. + * Command Response - A list of stream keys with a `Record` of stream IDs mapped to an `Array` of entries. * Returns `null` if there is no stream that can be served. + * The response comes in format `GlideRecord>`, see {@link GlideRecord}. */ public xreadgroup( group: GlideString, @@ -2763,16 +2780,12 @@ export class BaseTransaction> { * Returns stream message summary information for pending messages matching a given range of IDs. * * @see {@link https://valkey.io/commands/xpending/|valkey.io} for details. - * Returns the list of all consumers and their attributes for the given consumer group of the - * stream stored at `key`. - * - * @see {@link https://valkey.io/commands/xinfo-consumers/|valkey.io} for details. * * @param key - The key of the stream. * @param group - The consumer group name. * * Command Response - An `array` that includes the summary of the pending messages. - * See example of {@link BaseClient.xpending|xpending} for more details. + * See example of {@link BaseClient.xpending|xpending} for more details. */ public xpending(key: GlideString, group: GlideString): T { return this.addAndReturn(createXPending(key, group)); @@ -2806,6 +2819,7 @@ export class BaseTransaction> { * * Command Response - An `Array` of `Records`, where each mapping contains the attributes * of a consumer for the given consumer group of the stream at `key`. + * The response comes in format `GlideRecord[]`, see {@link GlideRecord}. */ public xinfoConsumers(key: GlideString, group: GlideString): T { return this.addAndReturn(createXInfoConsumers(key, group)); @@ -2823,7 +2837,8 @@ export class BaseTransaction> { * @param ids - An array of entry ids. * @param options - (Optional) Stream claim options {@link StreamClaimOptions}. * - * Command Response - A `Record` of message entries that are claimed by the consumer. + * Command Response - Message entries that are claimed by the consumer. + * The response comes in format `GlideRecord<[GlideString, GlideString][]>`, see {@link GlideRecord}. */ public xclaim( key: GlideString, @@ -2888,6 +2903,8 @@ export class BaseTransaction> { * - If you are using Valkey 7.0.0 or above, the response list will also include a list containing * the message IDs that were in the Pending Entries List but no longer exist in the stream. * These IDs are deleted from the Pending Entries List. + * + * The response comes in format `[GlideString, GlideRecord<[GlideString, GlideString][]>, GlideString[]?]`, see {@link GlideRecord}. */ public xautoclaim( key: GlideString, @@ -3617,7 +3634,7 @@ export class BaseTransaction> { * @param count - (Optional) The number of elements to pop. If not supplied, only one element will be popped. * * Command Response - A two-element `array` containing the key name of the set from which the - * element was popped, and a member-score `Record` of the popped element. + * was popped, and a `GlideRecord` of the popped elements - see {@link GlideRecord}. * If no member could be popped, returns `null`. */ public zmpop( @@ -3644,7 +3661,7 @@ export class BaseTransaction> { * @param count - (Optional) The number of elements to pop. If not supplied, only one element will be popped. * * Command Response - A two-element `array` containing the key name of the set from which the element - * was popped, and a member-score `Record` of the popped element. + * was popped, and a `GlideRecord` of the popped elements - see {@link GlideRecord}. * If no member could be popped, returns `null`. */ public bzmpop( @@ -3791,8 +3808,8 @@ export class BaseTransaction> { * - (Optional) `withMatchLen`: if `true`, include the length of the substring matched for the each match. * - (Optional) `minMatchLen`: the minimum length of matches to include in the result. * - * Command Response - A `Record` containing the indices of the longest common subsequences between the - * 2 strings and the lengths of the longest common subsequences. The resulting map contains two + * Command Response - A {@link GlideRecord} containing the indices of the longest common subsequences between + * the 2 strings and the lengths of the longest common subsequences. The resulting map contains two * keys, "matches" and "len": * - `"len"` is mapped to the total length of the all longest common subsequences between the 2 strings * stored as an integer. This value doesn't count towards the `minMatchLen` filter. @@ -3876,7 +3893,7 @@ export class BaseTransaction> { * @param direction - The direction based on which elements are popped from - see {@link ListDirection}. * @param count - (Optional) The maximum number of popped elements. * - * Command Response - A `Record` of `key` name mapped array of popped elements. + * Command Response - A `Record` which stores the key name where elements were popped out and the array of popped elements. */ public lmpop( keys: GlideString[], @@ -3899,7 +3916,7 @@ export class BaseTransaction> { * `0` will block indefinitely. * @param count - (Optional) The maximum number of popped elements. * - * Command Response - A `Record` of `key` name mapped array of popped elements. + * Command Response - A `Record` which stores the key name where elements were popped out and the array of popped elements. * If no member could be popped and the timeout expired, returns `null`. */ public blmpop( @@ -3944,16 +3961,13 @@ export class BaseTransaction> { /** * Returns the number of subscribers (exclusive of clients subscribed to patterns) for the specified channels. * - * Note that it is valid to call this command without channels. In this case, it will just return an empty map. - * The command is routed to all nodes, and aggregates the response to a single map of the channels and their number of subscriptions. - * * @see {@link https://valkey.io/commands/pubsub-numsub/|valkey.io} for more details. * * @param channels - The list of channels to query for the number of subscribers. - * If not provided, returns an empty map. - * Command Response - A map where keys are the channel names and values are the number of subscribers. + * + * Command Response - A list of the channel names and their numbers of subscribers. */ - public pubsubNumSub(channels?: string[]): T { + public pubsubNumSub(channels: GlideString[]): T { return this.addAndReturn(createPubSubNumSub(channels)); } } @@ -4250,6 +4264,7 @@ export class ClusterTransaction extends BaseTransaction { * * @param pattern - A glob-style pattern to match active shard channels. * If not provided, all active shard channels are returned. + * * Command Response - A list of currently active shard channels matching the given pattern. * If no pattern is specified, all active shard channels are returned. */ @@ -4260,16 +4275,14 @@ export class ClusterTransaction extends BaseTransaction { /** * Returns the number of subscribers (exclusive of clients subscribed to patterns) for the specified shard channels. * - * Note that it is valid to call this command without channels. In this case, it will just return an empty map. - * The command is routed to all nodes, and aggregates the response to a single map of the channels and their number of subscriptions. - * * @see {@link https://valkey.io/commands/pubsub-shardnumsub|valkey.io} for more details. + * @remarks The command is routed to all nodes, and aggregates the response into a single list. * * @param channels - The list of shard channels to query for the number of subscribers. - * If not provided, returns an empty map. - * @returns A map where keys are the shard channel names and values are the number of subscribers. + * + * Command Response - A list of the shard channel names and their numbers of subscribers. */ - public pubsubShardNumSub(channels?: string[]): ClusterTransaction { + public pubsubShardNumSub(channels: GlideString[]): ClusterTransaction { return this.addAndReturn(createPubSubShardNumSub(channels)); } } diff --git a/node/tests/GlideClient.test.ts b/node/tests/GlideClient.test.ts index a56484f6fb..c0d73662ac 100644 --- a/node/tests/GlideClient.test.ts +++ b/node/tests/GlideClient.test.ts @@ -13,19 +13,20 @@ import { import { BufferReader, BufferWriter } from "protobufjs"; import { v4 as uuidv4 } from "uuid"; import { + convertGlideRecordToRecord, Decoder, + FlushMode, + FunctionRestorePolicy, GlideClient, + GlideRecord, + GlideString, HashDataType, ProtocolVersion, RequestError, + SortOrder, Transaction, } from ".."; import { RedisCluster } from "../../utils/TestUtils.js"; -import { - FlushMode, - FunctionRestorePolicy, - SortOrder, -} from "../build-ts/src/Commands"; import { command_request } from "../src/ProtobufMessage"; import { runBaseTests } from "./SharedTests"; import { @@ -372,7 +373,7 @@ describe("GlideClient", () => { const key = uuidv4(); const maxmemoryPolicyKey = "maxmemory-policy"; const config = await client.configGet([maxmemoryPolicyKey]); - const maxmemoryPolicy = String(config[maxmemoryPolicyKey]); + const maxmemoryPolicy = config[maxmemoryPolicyKey]; try { const transaction = new Transaction(); @@ -413,7 +414,7 @@ describe("GlideClient", () => { const key = uuidv4(); const maxmemoryPolicyKey = "maxmemory-policy"; const config = await client.configGet([maxmemoryPolicyKey]); - const maxmemoryPolicy = String(config[maxmemoryPolicyKey]); + const maxmemoryPolicy = config[maxmemoryPolicyKey]; try { const transaction = new Transaction(); @@ -703,17 +704,18 @@ describe("GlideClient", () => { let functionList = await client.functionList({ libNamePattern: Buffer.from(libName), + decoder: Decoder.Bytes, }); - let expectedDescription = new Map([ + let expectedDescription = new Map([ [funcName, null], ]); - let expectedFlags = new Map([ - [funcName, ["no-writes"]], + let expectedFlags = new Map([ + [funcName, [Buffer.from("no-writes")]], ]); checkFunctionListResponse( functionList, - libName, + Buffer.from(libName), expectedDescription, expectedFlags, ); @@ -763,7 +765,9 @@ describe("GlideClient", () => { newCode, ); - functionStats = await client.functionStats(); + functionStats = await client.functionStats({ + decoder: Decoder.Bytes, + }); for (const response of Object.values(functionStats)) { checkFunctionStatsResponse(response, [], 1, 2); @@ -1602,8 +1606,14 @@ describe("GlideClient", () => { if (result != null) { expect(result[0]).toEqual("0-1"); // xadd - expect(result[1]).toEqual(expectedXinfoStreamResult); - expect(result[2]).toEqual(expectedXinfoStreamFullResult); + const res1 = convertGlideRecordToRecord( + result[1] as GlideRecord<[GlideString, GlideString][]>, + ); + const res2 = convertGlideRecordToRecord( + result[2] as GlideRecord<[GlideString, GlideString][]>, + ); + expect(res1).toEqual(expectedXinfoStreamResult); + expect(res2).toEqual(expectedXinfoStreamFullResult); } client.close(); diff --git a/node/tests/GlideClientInternals.test.ts b/node/tests/GlideClientInternals.test.ts index 49eba9c577..9d8f093d0a 100644 --- a/node/tests/GlideClientInternals.test.ts +++ b/node/tests/GlideClientInternals.test.ts @@ -5,13 +5,13 @@ import { beforeAll, describe, expect, it } from "@jest/globals"; import fs from "fs"; import { - MAX_REQUEST_ARGS_LEN, createLeakedArray, createLeakedAttribute, createLeakedBigint, createLeakedDouble, createLeakedMap, createLeakedString, + MAX_REQUEST_ARGS_LEN, } from "glide-rs"; import Long from "long"; import net from "net"; @@ -22,15 +22,18 @@ import { BaseClientConfiguration, ClosingError, ClusterTransaction, + convertGlideRecordToRecord, Decoder, GlideClient, GlideClientConfiguration, GlideClusterClient, GlideClusterClientConfiguration, + GlideRecord, + GlideReturnType, InfoOptions, + isGlideRecord, Logger, RequestError, - GlideReturnType, SlotKeyTypes, TimeUnit, } from ".."; @@ -289,7 +292,10 @@ describe("SocketConnectionInternals", () => { }); describe("handling types", () => { - const test_receiving_value = async (expected: GlideReturnType) => { + const test_receiving_value = async ( + received: GlideReturnType, // value 'received' from the server + expected: GlideReturnType, // value received from rust + ) => { await testWithResources(async (connection, socket) => { socket.once("data", (data) => { const reader = Reader.create(data); @@ -306,42 +312,65 @@ describe("SocketConnectionInternals", () => { ResponseType.Value, request.callbackIdx, { - value: expected, + value: received, }, ); }); const result = await connection.get("foo", { decoder: Decoder.String, }); - expect(result).toEqual(expected); + // RESP3 map are converted to `GlideRecord` in rust lib, but elements may get reordered in this test. + // To avoid flakyness, we downcast `GlideRecord` to `Record` which can be safely compared. + expect( + isGlideRecord(result) + ? convertGlideRecordToRecord( + result as unknown as GlideRecord, + ) + : result, + ).toEqual(expected); }); }; it("should pass strings received from socket", async () => { - await test_receiving_value("bar"); + await test_receiving_value("bar", "bar"); }); it("should pass maps received from socket", async () => { - await test_receiving_value({ foo: "bar", bar: "baz" }); + await test_receiving_value( + { foo: "bar", bar: "baz" }, + { foo: "bar", bar: "baz" }, + ); }); it("should pass arrays received from socket", async () => { - await test_receiving_value(["foo", "bar", "baz"]); + await test_receiving_value( + ["foo", "bar", "baz"], + ["foo", "bar", "baz"], + ); }); it("should pass attributes received from socket", async () => { - await test_receiving_value({ - value: "bar", - attributes: { foo: "baz" }, - }); + await test_receiving_value( + { + value: "bar", + attributes: { foo: "baz" }, + }, + { + value: "bar", + attributes: [{ key: "foo", value: "baz" }], + }, + ); }); it("should pass bigints received from socket", async () => { - await test_receiving_value(BigInt("9007199254740991")); + await test_receiving_value( + BigInt("9007199254740991"), + BigInt("9007199254740991"), + ); }); it("should pass floats received from socket", async () => { - await test_receiving_value(0.75); + await test_receiving_value(0.75, 0.75); }); }); diff --git a/node/tests/GlideClusterClient.test.ts b/node/tests/GlideClusterClient.test.ts index bdf08d8cb2..2b39c62784 100644 --- a/node/tests/GlideClusterClient.test.ts +++ b/node/tests/GlideClusterClient.test.ts @@ -15,6 +15,7 @@ import { v4 as uuidv4 } from "uuid"; import { BitwiseOperation, ClusterTransaction, + convertRecordToGlideRecord, Decoder, FunctionListResponse, GlideClusterClient, @@ -26,15 +27,13 @@ import { Routes, ScoreFilter, SlotKeyTypes, -} from ".."; -import { RedisCluster } from "../../utils/TestUtils.js"; -import { FlushMode, FunctionRestorePolicy, FunctionStatsSingleResponse, GeoUnit, SortOrder, -} from "../build-ts/src/Commands"; +} from ".."; +import { RedisCluster } from "../../utils/TestUtils.js"; import { runBaseTests } from "./SharedTests"; import { checkClusterResponse, @@ -274,13 +273,14 @@ describe("GlideClusterClient", () => { client = await GlideClusterClient.createClient( getClientConfigurationOption(cluster.getAddresses(), protocol), ); - const transaction = new ClusterTransaction(); - transaction.configSet({ timeout: "1000" }); - transaction.configGet(["timeout"]); + const transaction = new ClusterTransaction() + .configSet({ timeout: "1000" }) + .configGet(["timeout"]); const result = await client.exec(transaction); - expect(intoString(result)).toEqual( - intoString(["OK", { timeout: "1000" }]), - ); + expect(result).toEqual([ + "OK", + convertRecordToGlideRecord({ timeout: "1000" }), + ]); }, TIMEOUT, ); @@ -304,8 +304,8 @@ describe("GlideClusterClient", () => { transaction.pubsubShardChannels(); expectedRes.push(["pubsubShardChannels()", []]); - transaction.pubsubShardNumSub(); - expectedRes.push(["pubsubShardNumSub()", {}]); + transaction.pubsubShardNumSub([]); + expectedRes.push(["pubsubShardNumSub()", []]); } const result = await client.exec(transaction); @@ -384,8 +384,10 @@ describe("GlideClusterClient", () => { client.sdiffstore("abc", ["zxy", "lkn"]), client.sortStore("abc", "zyx"), client.sortStore("abc", "zyx", { isAlpha: true }), - client.lmpop(["abc", "def"], ListDirection.LEFT, 1), - client.blmpop(["abc", "def"], ListDirection.RIGHT, 0.1, 1), + client.lmpop(["abc", "def"], ListDirection.LEFT, { count: 1 }), + client.blmpop(["abc", "def"], ListDirection.RIGHT, 0.1, { + count: 1, + }), client.bzpopmax(["abc", "def"], 0.5), client.bzpopmin(["abc", "def"], 0.5), client.xread({ abc: "0-0", zxy: "0-0", lkn: "0-0" }), @@ -467,7 +469,7 @@ describe("GlideClusterClient", () => { const key = uuidv4(); const maxmemoryPolicyKey = "maxmemory-policy"; const config = await client.configGet([maxmemoryPolicyKey]); - const maxmemoryPolicy = String(config[maxmemoryPolicyKey]); + const maxmemoryPolicy = config[maxmemoryPolicyKey] as string; try { const transaction = new ClusterTransaction(); @@ -508,7 +510,7 @@ describe("GlideClusterClient", () => { const key = uuidv4(); const maxmemoryPolicyKey = "maxmemory-policy"; const config = await client.configGet([maxmemoryPolicyKey]); - const maxmemoryPolicy = String(config[maxmemoryPolicyKey]); + const maxmemoryPolicy = config[maxmemoryPolicyKey] as string; try { const transaction = new ClusterTransaction(); @@ -1313,7 +1315,7 @@ describe("GlideClusterClient", () => { TIMEOUT, ); - it("function dump function restore %p", async () => { + it("function dump function restore", async () => { if (cluster.checkIfServerVersionLessThan("7.0.0")) return; diff --git a/node/tests/PubSub.test.ts b/node/tests/PubSub.test.ts index c065fa89ca..353bd07627 100644 --- a/node/tests/PubSub.test.ts +++ b/node/tests/PubSub.test.ts @@ -14,10 +14,12 @@ import { v4 as uuidv4 } from "uuid"; import { BaseClientConfiguration, ConfigurationError, + Decoder, GlideClient, GlideClientConfiguration, GlideClusterClient, GlideClusterClientConfiguration, + GlideString, ProtocolVersion, PubSubMsg, TimeoutError, @@ -29,7 +31,19 @@ import { parseEndpoints, } from "./TestUtilities"; -export type TGlideClient = GlideClient | GlideClusterClient; +type TGlideClient = GlideClient | GlideClusterClient; + +function convertGlideRecordToRecord( + data: { channel: GlideString; numSub: number }[], +): Record { + const res: Record = {}; + + for (const pair of data) { + res[pair.channel as string] = pair.numSub; + } + + return res; +} /** * Enumeration for specifying the method of PUBSUB subscription. @@ -79,15 +93,22 @@ describe("PubSub", () => { pubsubSubscriptions2?: | GlideClientConfiguration.PubSubSubscriptions | GlideClusterClientConfiguration.PubSubSubscriptions, + decoder?: Decoder, ): Promise<[TGlideClient, TGlideClient]> { let client: TGlideClient | undefined; if (clusterMode) { try { - options.pubsubSubscriptions = pubsubSubscriptions; - client = await GlideClusterClient.createClient(options); - options2.pubsubSubscriptions = pubsubSubscriptions2; - const client2 = await GlideClusterClient.createClient(options2); + client = await GlideClusterClient.createClient({ + pubsubSubscriptions: pubsubSubscriptions, + defaultDecoder: decoder, + ...options, + }); + const client2 = await GlideClusterClient.createClient({ + pubsubSubscriptions: pubsubSubscriptions2, + defaultDecoder: decoder, + ...options2, + }); return [client, client2]; } catch (error) { if (client) { @@ -98,10 +119,16 @@ describe("PubSub", () => { } } else { try { - options.pubsubSubscriptions = pubsubSubscriptions; - client = await GlideClient.createClient(options); - options2.pubsubSubscriptions = pubsubSubscriptions2; - const client2 = await GlideClient.createClient(options2); + client = await GlideClient.createClient({ + pubsubSubscriptions: pubsubSubscriptions, + defaultDecoder: decoder, + ...options, + }); + const client2 = await GlideClient.createClient({ + pubsubSubscriptions: pubsubSubscriptions2, + defaultDecoder: decoder, + ...options2, + }); return [client, client2]; } catch (error) { if (client) { @@ -136,28 +163,6 @@ describe("PubSub", () => { }; }; - function decodePubSubMsg(msg: PubSubMsg | null = null) { - if (!msg) { - return { - message: "", - channel: "", - pattern: null, - }; - } - - const stringMsg = Buffer.from(msg.message).toString("utf-8"); - const stringChannel = Buffer.from(msg.channel).toString("utf-8"); - const stringPattern = msg.pattern - ? Buffer.from(msg.pattern).toString("utf-8") - : null; - - return { - message: stringMsg, - channel: stringChannel, - pattern: stringPattern, - }; - } - async function getMessageByMethod( method: number, client: TGlideClient, @@ -166,13 +171,13 @@ describe("PubSub", () => { ) { if (method === MethodTesting.Async) { const pubsubMessage = await client.getPubSubMessage(); - return decodePubSubMsg(pubsubMessage); + return pubsubMessage; } else if (method === MethodTesting.Sync) { const pubsubMessage = client.tryGetPubSubMessage(); - return decodePubSubMsg(pubsubMessage); + return pubsubMessage; } else { if (messages && index !== null) { - return decodePubSubMsg(messages[index!]); + return messages[index!]; } throw new Error( @@ -328,8 +333,92 @@ describe("PubSub", () => { let publishingClient: TGlideClient; try { - const channel = uuidv4(); - const message = uuidv4(); + const channel = uuidv4() as GlideString; + const message = uuidv4() as GlideString; + const options = getOptions(clusterMode); + let context: PubSubMsg[] | null = null; + let callback; + + if (method === MethodTesting.Callback) { + context = []; + callback = newMessage; + } + + pubSub = createPubSubSubscription( + clusterMode, + { + [GlideClusterClientConfiguration.PubSubChannelModes + .Exact]: new Set([channel as string]), + }, + { + [GlideClientConfiguration.PubSubChannelModes.Exact]: + new Set([channel as string]), + }, + callback, + context, + ); + [listeningClient, publishingClient] = await createClients( + clusterMode, + options, + getOptions(clusterMode), + pubSub, + ); + + const result = await publishingClient.publish(message, channel); + + if (clusterMode) { + expect(result).toEqual(1); + } + + // Allow the message to propagate + await new Promise((resolve) => setTimeout(resolve, 1000)); + + const pubsubMessage = await getMessageByMethod( + method, + listeningClient, + context, + 0, + ); + + expect(pubsubMessage!.message).toEqual(message); + expect(pubsubMessage!.channel).toEqual(channel); + expect(pubsubMessage!.pattern).toBeNull(); + + await checkNoMessagesLeft(method, listeningClient, context, 1); + } finally { + await clientCleanup(publishingClient!); + await clientCleanup( + listeningClient!, + clusterMode ? pubSub! : undefined, + ); + } + }, + TIMEOUT, + ); + + /** + * Tests the basic happy path for exact PUBSUB functionality with binary. + * + * This test covers the basic PUBSUB flow using three different methods: + * Async, Sync, and Callback. It verifies that a message published to a + * specific channel is correctly received by a subscriber. + * + * @param clusterMode - Indicates if the test should be run in cluster mode. + * @param method - Specifies the method of PUBSUB subscription (Async, Sync, Callback). + */ + it.each(testCases)( + `pubsub exact happy path binary test_%p%p`, + async (clusterMode, method) => { + let pubSub: + | GlideClusterClientConfiguration.PubSubSubscriptions + | GlideClientConfiguration.PubSubSubscriptions + | null = null; + let listeningClient: TGlideClient; + let publishingClient: TGlideClient; + + try { + const channel = uuidv4() as GlideString; + const message = uuidv4() as GlideString; const options = getOptions(clusterMode); let context: PubSubMsg[] | null = null; let callback; @@ -343,11 +432,11 @@ describe("PubSub", () => { clusterMode, { [GlideClusterClientConfiguration.PubSubChannelModes - .Exact]: new Set([channel]), + .Exact]: new Set([channel as string]), }, { [GlideClientConfiguration.PubSubChannelModes.Exact]: - new Set([channel]), + new Set([channel as string]), }, callback, context, @@ -357,6 +446,8 @@ describe("PubSub", () => { options, getOptions(clusterMode), pubSub, + undefined, + Decoder.Bytes, ); const result = await publishingClient.publish(message, channel); @@ -374,9 +465,10 @@ describe("PubSub", () => { context, 0, ); - expect(pubsubMessage.message).toEqual(message); - expect(pubsubMessage.channel).toEqual(channel); - expect(pubsubMessage.pattern).toEqual(null); + + expect(pubsubMessage!.message).toEqual(Buffer.from(message)); + expect(pubsubMessage!.channel).toEqual(Buffer.from(channel)); + expect(pubsubMessage!.pattern).toBeNull(); await checkNoMessagesLeft(method, listeningClient, context, 1); } finally { @@ -410,19 +502,19 @@ describe("PubSub", () => { let publishingClient: TGlideClient | null = null; try { - const channel = uuidv4(); - const message = uuidv4(); - const message2 = uuidv4(); + const channel = uuidv4() as GlideString; + const message = uuidv4() as GlideString; + const message2 = uuidv4() as GlideString; pubSub = createPubSubSubscription( clusterMode, { [GlideClusterClientConfiguration.PubSubChannelModes - .Exact]: new Set([channel]), + .Exact]: new Set([channel as string]), }, { [GlideClientConfiguration.PubSubChannelModes.Exact]: - new Set([channel]), + new Set([channel as string]), }, ); @@ -444,12 +536,9 @@ describe("PubSub", () => { // Allow the message to propagate await new Promise((resolve) => setTimeout(resolve, 1000)); - const asyncMsgRes = await listeningClient.getPubSubMessage(); - const syncMsgRes = listeningClient.tryGetPubSubMessage(); - expect(syncMsgRes).toBeTruthy(); - - const asyncMsg = decodePubSubMsg(asyncMsgRes); - const syncMsg = decodePubSubMsg(syncMsgRes); + const asyncMsg = await listeningClient.getPubSubMessage(); + const syncMsg = listeningClient.tryGetPubSubMessage()!; + expect(syncMsg).toBeTruthy(); expect([message, message2]).toContain(asyncMsg.message); expect(asyncMsg.channel).toEqual(channel); @@ -459,7 +548,7 @@ describe("PubSub", () => { expect(syncMsg.channel).toEqual(channel); expect(syncMsg.pattern).toBeNull(); - expect(asyncMsg.message).not.toEqual(syncMsg.message); + expect(asyncMsg.message).not.toEqual(syncMsg!.message); // Assert there are no messages to read await checkNoMessagesLeft(MethodTesting.Async, listeningClient); @@ -499,12 +588,12 @@ describe("PubSub", () => { try { // Create a map of channels to random messages with shard prefix - const channelsAndMessages: Record = {}; + const channelsAndMessages: [GlideString, GlideString][] = []; for (let i = 0; i < NUM_CHANNELS; i++) { const channel = `${shardPrefix}${uuidv4()}`; const message = uuidv4(); - channelsAndMessages[channel] = message; + channelsAndMessages.push([channel, message]); } let context: PubSubMsg[] | null = null; @@ -520,11 +609,15 @@ describe("PubSub", () => { clusterMode, { [GlideClusterClientConfiguration.PubSubChannelModes - .Exact]: new Set(Object.keys(channelsAndMessages)), + .Exact]: new Set( + channelsAndMessages.map((a) => a[0].toString()), + ), }, { [GlideClientConfiguration.PubSubChannelModes.Exact]: - new Set(Object.keys(channelsAndMessages)), + new Set( + channelsAndMessages.map((a) => a[0].toString()), + ), }, callback, context, @@ -539,9 +632,7 @@ describe("PubSub", () => { ); // Publish messages to each channel - for (const [channel, message] of Object.entries( - channelsAndMessages, - )) { + for (const [channel, message] of channelsAndMessages) { const result = await publishingClient.publish( message, channel, @@ -557,25 +648,21 @@ describe("PubSub", () => { // Check if all messages are received correctly for (let index = 0; index < NUM_CHANNELS; index++) { - const pubsubMsg = await getMessageByMethod( + const pubsubMsg = (await getMessageByMethod( method, listeningClient, context, index, - ); + ))!; expect( - pubsubMsg.channel in channelsAndMessages, - ).toBeTruthy(); - expect(pubsubMsg.message).toEqual( - channelsAndMessages[pubsubMsg.channel], - ); + channelsAndMessages.find( + ([channel]) => channel === pubsubMsg.channel, + ), + ).toEqual([pubsubMsg.channel, pubsubMsg.message]); + expect(pubsubMsg.pattern).toBeNull(); - delete channelsAndMessages[pubsubMsg.channel]; } - // Check that we received all messages - expect(Object.keys(channelsAndMessages).length).toEqual(0); - // Check no messages left await checkNoMessagesLeft( method, @@ -624,12 +711,12 @@ describe("PubSub", () => { try { // Create a map of channels to random messages with shard prefix - const channelsAndMessages: Record = {}; + const channelsAndMessages: [GlideString, GlideString][] = []; for (let i = 0; i < NUM_CHANNELS; i++) { const channel = `${shardPrefix}${uuidv4()}`; const message = uuidv4(); - channelsAndMessages[channel] = message; + channelsAndMessages.push([channel, message]); } // Create PUBSUB subscription for the test @@ -637,11 +724,15 @@ describe("PubSub", () => { clusterMode, { [GlideClusterClientConfiguration.PubSubChannelModes - .Exact]: new Set(Object.keys(channelsAndMessages)), + .Exact]: new Set( + channelsAndMessages.map((a) => a[0].toString()), + ), }, { [GlideClientConfiguration.PubSubChannelModes.Exact]: - new Set(Object.keys(channelsAndMessages)), + new Set( + channelsAndMessages.map((a) => a[0].toString()), + ), }, ); @@ -654,9 +745,7 @@ describe("PubSub", () => { ); // Publish messages to each channel - for (const [channel, message] of Object.entries( - channelsAndMessages, - )) { + for (const [channel, message] of channelsAndMessages) { const result = await publishingClient.publish( message, channel, @@ -682,17 +771,13 @@ describe("PubSub", () => { ); expect( - pubsubMsg.channel in channelsAndMessages, - ).toBeTruthy(); - expect(pubsubMsg.message).toEqual( - channelsAndMessages[pubsubMsg.channel], - ); - expect(pubsubMsg.pattern).toBeNull(); - delete channelsAndMessages[pubsubMsg.channel]; - } + channelsAndMessages.find( + ([channel]) => channel === pubsubMsg?.channel, + ), + ).toEqual([pubsubMsg?.channel, pubsubMsg?.message]); - // Check that we received all messages - expect(Object.keys(channelsAndMessages).length).toEqual(0); + expect(pubsubMsg?.pattern).toBeNull(); + } // Assert there are no messages to read await checkNoMessagesLeft(MethodTesting.Async, listeningClient); @@ -741,8 +826,8 @@ describe("PubSub", () => { | null = null; let listeningClient: TGlideClient | null = null; let publishingClient: TGlideClient | null = null; - const channel = uuidv4(); - const message = uuidv4(); + const channel = uuidv4() as GlideString; + const message = uuidv4() as GlideString; const publishResponse = 1; try { @@ -759,7 +844,7 @@ describe("PubSub", () => { clusterMode, { [GlideClusterClientConfiguration.PubSubChannelModes - .Sharded]: new Set([channel]), + .Sharded]: new Set([channel as string]), }, {}, callback, @@ -783,12 +868,12 @@ describe("PubSub", () => { // Allow the message to propagate await new Promise((resolve) => setTimeout(resolve, 1000)); - const pubsubMsg = await getMessageByMethod( + const pubsubMsg = (await getMessageByMethod( method, listeningClient, context, 0, - ); + ))!; expect(pubsubMsg.message).toEqual(message); expect(pubsubMsg.channel).toEqual(channel); @@ -837,9 +922,9 @@ describe("PubSub", () => { | null = null; let listeningClient: TGlideClient | null = null; let publishingClient: TGlideClient | null = null; - const channel = uuidv4(); - const message = uuidv4(); - const message2 = uuidv4(); + const channel = uuidv4() as GlideString; + const message = uuidv4() as GlideString; + const message2 = uuidv4() as GlideString; try { // Create PUBSUB subscription for the test @@ -847,7 +932,7 @@ describe("PubSub", () => { true, { [GlideClusterClientConfiguration.PubSubChannelModes - .Sharded]: new Set([channel]), + .Sharded]: new Set([channel as string]), }, {}, ); @@ -875,12 +960,9 @@ describe("PubSub", () => { // Allow the messages to propagate await new Promise((resolve) => setTimeout(resolve, 1000)); - const asyncMsgRes = await listeningClient.getPubSubMessage(); - const syncMsgRes = listeningClient.tryGetPubSubMessage(); - expect(syncMsgRes).toBeTruthy(); - - const asyncMsg = decodePubSubMsg(asyncMsgRes); - const syncMsg = decodePubSubMsg(syncMsgRes); + const asyncMsg = await listeningClient!.getPubSubMessage(); + const syncMsg = listeningClient!.tryGetPubSubMessage()!; + expect(syncMsg).toBeTruthy(); expect([message, message2]).toContain(asyncMsg.message); expect(asyncMsg.channel).toEqual(channel); @@ -894,7 +976,7 @@ describe("PubSub", () => { // Assert there are no messages to read await checkNoMessagesLeft(MethodTesting.Async, listeningClient); - expect(listeningClient.tryGetPubSubMessage()).toBeNull(); + expect(listeningClient!.tryGetPubSubMessage()).toBeNull(); } finally { // Cleanup clients if (listeningClient) { @@ -941,12 +1023,12 @@ describe("PubSub", () => { const publishResponse = 1; // Create a map of channels to random messages with shard prefix - const channelsAndMessages: Record = {}; + const channelsAndMessages: [GlideString, GlideString][] = []; for (let i = 0; i < NUM_CHANNELS; i++) { const channel = `${shardPrefix}${uuidv4()}`; const message = uuidv4(); - channelsAndMessages[channel] = message; + channelsAndMessages.push([channel, message]); } try { @@ -964,7 +1046,7 @@ describe("PubSub", () => { { [GlideClusterClientConfiguration.PubSubChannelModes .Sharded]: new Set( - Object.keys(channelsAndMessages), + channelsAndMessages.map((a) => a[0].toString()), ), }, {}, @@ -981,9 +1063,7 @@ describe("PubSub", () => { ); // Publish messages to each channel - for (const [channel, message] of Object.entries( - channelsAndMessages, - )) { + for (const [channel, message] of channelsAndMessages) { const result = await ( publishingClient as GlideClusterClient ).publish(message, channel, true); @@ -995,26 +1075,22 @@ describe("PubSub", () => { // Check if all messages are received correctly for (let index = 0; index < NUM_CHANNELS; index++) { - const pubsubMsg = await getMessageByMethod( + const pubsubMsg = (await getMessageByMethod( method, listeningClient, context, index, - ); + ))!; expect( - pubsubMsg.channel in channelsAndMessages, - ).toBeTruthy(); - expect(pubsubMsg.message).toEqual( - channelsAndMessages[pubsubMsg.channel], - ); + channelsAndMessages.find( + ([channel]) => channel === pubsubMsg.channel, + ), + ).toEqual([pubsubMsg.channel, pubsubMsg.message]); + expect(pubsubMsg.pattern).toBeNull(); - delete channelsAndMessages[pubsubMsg.channel]; } - // Check that we received all messages - expect(Object.keys(channelsAndMessages).length).toEqual(0); - // Assert there are no more messages to read await checkNoMessagesLeft( method, @@ -1052,10 +1128,10 @@ describe("PubSub", () => { "pubsub pattern test_%p_%p", async (clusterMode, method) => { const PATTERN = `{{channel}}:*`; - const channels: Record = { - [`{{channel}}:${uuidv4()}`]: uuidv4(), - [`{{channel}}:${uuidv4()}`]: uuidv4(), - }; + const channels: [GlideString, GlideString][] = [ + [`{{channel}}:${uuidv4()}`, uuidv4()], + [`{{channel}}:${uuidv4()}`, uuidv4()], + ]; let pubSub: | GlideClusterClientConfiguration.PubSubSubscriptions @@ -1097,7 +1173,7 @@ describe("PubSub", () => { ); // Publish messages to each channel - for (const [channel, message] of Object.entries(channels)) { + for (const [channel, message] of channels) { const result = await publishingClient.publish( message, channel, @@ -1113,23 +1189,21 @@ describe("PubSub", () => { // Check if all messages are received correctly for (let index = 0; index < 2; index++) { - const pubsubMsg = await getMessageByMethod( + const pubsubMsg = (await getMessageByMethod( method, listeningClient, context, index, - ); - expect(pubsubMsg.channel in channels).toBeTruthy(); - expect(pubsubMsg.message).toEqual( - channels[pubsubMsg.channel], - ); + ))!; + expect( + channels.find( + ([channel]) => channel === pubsubMsg.channel, + ), + ).toEqual([pubsubMsg.channel, pubsubMsg.message]); + expect(pubsubMsg.pattern).toEqual(PATTERN); - delete channels[pubsubMsg.channel]; } - // Check that we received all messages - expect(Object.keys(channels).length).toEqual(0); - // Assert there are no more messages to read await checkNoMessagesLeft(method, listeningClient, context, 2); } finally { @@ -1162,10 +1236,10 @@ describe("PubSub", () => { "pubsub pattern coexistence test_%p", async (clusterMode) => { const PATTERN = `{{channel}}:*`; - const channels: Record = { - [`{{channel}}:${uuidv4()}`]: uuidv4(), - [`{{channel}}:${uuidv4()}`]: uuidv4(), - }; + const channels: [GlideString, GlideString][] = [ + [`{{channel}}:${uuidv4()}`, uuidv4()], + [`{{channel}}:${uuidv4()}`, uuidv4()], + ]; let pubSub: | GlideClusterClientConfiguration.PubSubSubscriptions @@ -1197,7 +1271,7 @@ describe("PubSub", () => { ); // Publish messages to each channel - for (const [channel, message] of Object.entries(channels)) { + for (const [channel, message] of channels) { const result = await publishingClient.publish( message, channel, @@ -1217,22 +1291,20 @@ describe("PubSub", () => { index % 2 === 0 ? MethodTesting.Async : MethodTesting.Sync; - const pubsubMsg = await getMessageByMethod( + const pubsubMsg = (await getMessageByMethod( method, listeningClient, - ); + ))!; + + expect( + channels.find( + ([channel]) => channel === pubsubMsg.channel, + ), + ).toEqual([pubsubMsg.channel, pubsubMsg.message]); - expect(Object.keys(channels)).toContain(pubsubMsg.channel); - expect(pubsubMsg.message).toEqual( - channels[pubsubMsg.channel], - ); expect(pubsubMsg.pattern).toEqual(PATTERN); - delete channels[pubsubMsg.channel]; } - // Check that we received all messages - expect(Object.keys(channels).length).toEqual(0); - // Assert there are no more messages to read await checkNoMessagesLeft(MethodTesting.Async, listeningClient); expect(listeningClient.tryGetPubSubMessage()).toBeNull(); @@ -1268,12 +1340,12 @@ describe("PubSub", () => { async (clusterMode, method) => { const NUM_CHANNELS = 256; const PATTERN = "{{channel}}:*"; - const channels: Record = {}; + const channels: [GlideString, GlideString][] = []; for (let i = 0; i < NUM_CHANNELS; i++) { const channel = `{{channel}}:${uuidv4()}`; const message = uuidv4(); - channels[channel] = message; + channels.push([channel, message]); } let pubSub: @@ -1315,7 +1387,7 @@ describe("PubSub", () => { ); // Publish messages to each channel - for (const [channel, message] of Object.entries(channels)) { + for (const [channel, message] of channels) { const result = await publishingClient.publish( message, channel, @@ -1331,23 +1403,22 @@ describe("PubSub", () => { // Check if all messages are received correctly for (let index = 0; index < NUM_CHANNELS; index++) { - const pubsubMsg = await getMessageByMethod( + const pubsubMsg = (await getMessageByMethod( method, listeningClient, context, index, - ); - expect(pubsubMsg.channel in channels).toBeTruthy(); - expect(pubsubMsg.message).toEqual( - channels[pubsubMsg.channel], - ); + ))!; + + expect( + channels.find( + ([channel]) => channel === pubsubMsg.channel, + ), + ).toEqual([pubsubMsg.channel, pubsubMsg.message]); + expect(pubsubMsg.pattern).toEqual(PATTERN); - delete channels[pubsubMsg.channel]; } - // Check that we received all messages - expect(Object.keys(channels).length).toEqual(0); - // Assert there are no more messages to read await checkNoMessagesLeft( method, @@ -1391,20 +1462,26 @@ describe("PubSub", () => { const PATTERN = "{{pattern}}:*"; // Create dictionaries of channels and their corresponding messages - const exactChannelsAndMessages: Record = {}; - const patternChannelsAndMessages: Record = {}; + const exactChannelsAndMessages: [GlideString, GlideString][] = []; + const patternChannelsAndMessages: [GlideString, GlideString][] = []; for (let i = 0; i < NUM_CHANNELS; i++) { const exactChannel = `{{channel}}:${uuidv4()}`; const patternChannel = `{{pattern}}:${uuidv4()}`; - exactChannelsAndMessages[exactChannel] = uuidv4(); - patternChannelsAndMessages[patternChannel] = uuidv4(); + const exactMessage = uuidv4(); + const patternMessage = uuidv4(); + + exactChannelsAndMessages.push([exactChannel, exactMessage]); + patternChannelsAndMessages.push([ + patternChannel, + patternMessage, + ]); } - const allChannelsAndMessages: Record = { + const allChannelsAndMessages: [GlideString, GlideString][] = [ ...exactChannelsAndMessages, ...patternChannelsAndMessages, - }; + ]; let pubSub: | GlideClusterClientConfiguration.PubSubSubscriptions @@ -1427,14 +1504,20 @@ describe("PubSub", () => { { [GlideClusterClientConfiguration.PubSubChannelModes .Exact]: new Set( - Object.keys(exactChannelsAndMessages), + exactChannelsAndMessages.map((a) => + a[0].toString(), + ), ), [GlideClusterClientConfiguration.PubSubChannelModes .Pattern]: new Set([PATTERN]), }, { [GlideClientConfiguration.PubSubChannelModes.Exact]: - new Set(Object.keys(exactChannelsAndMessages)), + new Set( + exactChannelsAndMessages.map((a) => + a[0].toString(), + ), + ), [GlideClientConfiguration.PubSubChannelModes.Pattern]: new Set([PATTERN]), }, @@ -1450,9 +1533,7 @@ describe("PubSub", () => { ); // Publish messages to all channels - for (const [channel, message] of Object.entries( - allChannelsAndMessages, - )) { + for (const [channel, message] of allChannelsAndMessages) { const result = await publishingClient.publish( message, channel, @@ -1466,33 +1547,30 @@ describe("PubSub", () => { // Allow the messages to propagate await new Promise((resolve) => setTimeout(resolve, 1000)); - const length = Object.keys(allChannelsAndMessages).length; + const length = allChannelsAndMessages.length; // Check if all messages are received correctly for (let index = 0; index < length; index++) { - const pubsubMsg: PubSubMsg = await getMessageByMethod( + const pubsubMsg: PubSubMsg = (await getMessageByMethod( method, listeningClient, context, index, - ); - const pattern = - pubsubMsg.channel in patternChannelsAndMessages - ? PATTERN - : null; + ))!; + const pattern = patternChannelsAndMessages.find( + ([channel]) => channel === pubsubMsg.channel, + ) + ? PATTERN + : null; expect( - pubsubMsg.channel in allChannelsAndMessages, - ).toBeTruthy(); - expect(pubsubMsg.message).toEqual( - allChannelsAndMessages[pubsubMsg.channel], - ); + allChannelsAndMessages.find( + ([channel]) => channel === pubsubMsg.channel, + ), + ).toEqual([pubsubMsg.channel, pubsubMsg.message]); + expect(pubsubMsg.pattern).toEqual(pattern); - delete allChannelsAndMessages[pubsubMsg.channel]; } - // Check that we received all messages - expect(Object.keys(allChannelsAndMessages).length).toEqual(0); - await checkNoMessagesLeft( method, listeningClient, @@ -1537,20 +1615,26 @@ describe("PubSub", () => { const PATTERN = "{{pattern}}:*"; // Create dictionaries of channels and their corresponding messages - const exactChannelsAndMessages: Record = {}; - const patternChannelsAndMessages: Record = {}; + const exactChannelsAndMessages: [GlideString, GlideString][] = []; + const patternChannelsAndMessages: [GlideString, GlideString][] = []; for (let i = 0; i < NUM_CHANNELS; i++) { const exactChannel = `{{channel}}:${uuidv4()}`; const patternChannel = `{{pattern}}:${uuidv4()}`; - exactChannelsAndMessages[exactChannel] = uuidv4(); - patternChannelsAndMessages[patternChannel] = uuidv4(); + const exactMessage = uuidv4(); + const patternMessage = uuidv4(); + + exactChannelsAndMessages.push([exactChannel, exactMessage]); + patternChannelsAndMessages.push([ + patternChannel, + patternMessage, + ]); } - const allChannelsAndMessages = { + const allChannelsAndMessages: [GlideString, GlideString][] = [ ...exactChannelsAndMessages, ...patternChannelsAndMessages, - }; + ]; let pubSubExact: | GlideClusterClientConfiguration.PubSubSubscriptions @@ -1581,12 +1665,18 @@ describe("PubSub", () => { { [GlideClusterClientConfiguration.PubSubChannelModes .Exact]: new Set( - Object.keys(exactChannelsAndMessages), + exactChannelsAndMessages.map((a) => + a[0].toString(), + ), ), }, { [GlideClientConfiguration.PubSubChannelModes.Exact]: - new Set(Object.keys(exactChannelsAndMessages)), + new Set( + exactChannelsAndMessages.map((a) => + a[0].toString(), + ), + ), }, callback, contextExact, @@ -1622,9 +1712,7 @@ describe("PubSub", () => { ); // Publish messages to all channels - for (const [channel, message] of Object.entries( - allChannelsAndMessages, - )) { + for (const [channel, message] of allChannelsAndMessages) { const result = await publishingClient.publish( message, channel, @@ -1642,50 +1730,41 @@ describe("PubSub", () => { // Verify messages for exact PUBSUB for (let index = 0; index < length; index++) { - const pubsubMsg = await getMessageByMethod( + const pubsubMsg = (await getMessageByMethod( method, listeningClientExact, contextExact, index, - ); + ))!; expect( - pubsubMsg.channel in exactChannelsAndMessages, - ).toBeTruthy(); - expect(pubsubMsg.message).toEqual( - exactChannelsAndMessages[pubsubMsg.channel], - ); + exactChannelsAndMessages.find( + ([channel]) => channel === pubsubMsg.channel, + ), + ).toEqual([pubsubMsg.channel, pubsubMsg.message]); + expect(pubsubMsg.pattern).toBeNull(); - delete exactChannelsAndMessages[pubsubMsg.channel]; } - // Check that we received all exact messages - expect(Object.keys(exactChannelsAndMessages).length).toEqual(0); - - length = Object.keys(patternChannelsAndMessages).length; + length = patternChannelsAndMessages.length; // Verify messages for pattern PUBSUB for (let index = 0; index < length; index++) { - const pubsubMsg = await getMessageByMethod( + const pubsubMsg = (await getMessageByMethod( method, listeningClientPattern, contextPattern, index, - ); + ))!; + expect( - pubsubMsg.channel in patternChannelsAndMessages, - ).toBeTruthy(); - expect(pubsubMsg.message).toEqual( - patternChannelsAndMessages[pubsubMsg.channel], - ); + patternChannelsAndMessages.find( + ([channel]) => channel === pubsubMsg.channel, + ), + ).toEqual([pubsubMsg.channel, pubsubMsg.message]); + expect(pubsubMsg.pattern).toEqual(PATTERN); - delete patternChannelsAndMessages[pubsubMsg.channel]; } - // Check that we received all pattern messages - expect(Object.keys(patternChannelsAndMessages).length).toEqual( - 0, - ); - // Assert no messages are left unread await checkNoMessagesLeft( method, @@ -1756,17 +1835,17 @@ describe("PubSub", () => { const SHARD_PREFIX = "{same-shard}"; // Create dictionaries of channels and their corresponding messages - const exactChannelsAndMessages: Record = {}; - const patternChannelsAndMessages: Record = {}; - const shardedChannelsAndMessages: Record = {}; + const exactChannelsAndMessages: [GlideString, GlideString][] = []; + const patternChannelsAndMessages: [GlideString, GlideString][] = []; + const shardedChannelsAndMessages: [GlideString, GlideString][] = []; for (let i = 0; i < NUM_CHANNELS; i++) { const exactChannel = `{{channel}}:${uuidv4()}`; const patternChannel = `{{pattern}}:${uuidv4()}`; const shardedChannel = `${SHARD_PREFIX}:${uuidv4()}`; - exactChannelsAndMessages[exactChannel] = uuidv4(); - patternChannelsAndMessages[patternChannel] = uuidv4(); - shardedChannelsAndMessages[shardedChannel] = uuidv4(); + exactChannelsAndMessages.push([exactChannel, uuidv4()]); + patternChannelsAndMessages.push([patternChannel, uuidv4()]); + shardedChannelsAndMessages.push([shardedChannel, uuidv4()]); } const publishResponse = 1; @@ -1791,13 +1870,17 @@ describe("PubSub", () => { { [GlideClusterClientConfiguration.PubSubChannelModes .Exact]: new Set( - Object.keys(exactChannelsAndMessages), + exactChannelsAndMessages.map((a) => + a[0].toString(), + ), ), [GlideClusterClientConfiguration.PubSubChannelModes .Pattern]: new Set([PATTERN]), [GlideClusterClientConfiguration.PubSubChannelModes .Sharded]: new Set( - Object.keys(shardedChannelsAndMessages), + shardedChannelsAndMessages.map((a) => + a[0].toString(), + ), ), }, {}, @@ -1813,10 +1896,10 @@ describe("PubSub", () => { ); // Publish messages to exact and pattern channels - for (const [channel, message] of Object.entries({ + for (const [channel, message] of [ ...exactChannelsAndMessages, ...patternChannelsAndMessages, - })) { + ]) { const result = await publishingClient.publish( message, channel, @@ -1825,9 +1908,7 @@ describe("PubSub", () => { } // Publish sharded messages - for (const [channel, message] of Object.entries( - shardedChannelsAndMessages, - )) { + for (const [channel, message] of shardedChannelsAndMessages) { const result = await ( publishingClient as GlideClusterClient ).publish(message, channel, true); @@ -1837,37 +1918,34 @@ describe("PubSub", () => { // Allow messages to propagate await new Promise((resolve) => setTimeout(resolve, 1000)); - const allChannelsAndMessages = { + const allChannelsAndMessages: [GlideString, GlideString][] = [ ...exactChannelsAndMessages, ...patternChannelsAndMessages, ...shardedChannelsAndMessages, - }; + ]; // Check if all messages are received correctly for (let index = 0; index < NUM_CHANNELS * 3; index++) { - const pubsubMsg: PubSubMsg = await getMessageByMethod( + const pubsubMsg: PubSubMsg = (await getMessageByMethod( method, listeningClient, context, index, - ); - const pattern = - pubsubMsg.channel in patternChannelsAndMessages - ? PATTERN - : null; + ))!; + const pattern = patternChannelsAndMessages.find( + ([channel]) => channel === pubsubMsg.channel, + ) + ? PATTERN + : null; expect( - pubsubMsg.channel in allChannelsAndMessages, - ).toBeTruthy(); - expect(pubsubMsg.message).toEqual( - allChannelsAndMessages[pubsubMsg.channel], - ); + allChannelsAndMessages.find( + ([channel]) => channel === pubsubMsg.channel, + ), + ).toEqual([pubsubMsg.channel, pubsubMsg.message]); + expect(pubsubMsg.pattern).toEqual(pattern); - delete allChannelsAndMessages[pubsubMsg.channel]; } - // Assert we received all messages - expect(Object.keys(allChannelsAndMessages).length).toEqual(0); - await checkNoMessagesLeft( method, listeningClient, @@ -1922,17 +2000,17 @@ describe("PubSub", () => { const SHARD_PREFIX = "{same-shard}"; // Create dictionaries of channels and their corresponding messages - const exactChannelsAndMessages: Record = {}; - const patternChannelsAndMessages: Record = {}; - const shardedChannelsAndMessages: Record = {}; + const exactChannelsAndMessages: [GlideString, GlideString][] = []; + const patternChannelsAndMessages: [GlideString, GlideString][] = []; + const shardedChannelsAndMessages: [GlideString, GlideString][] = []; for (let i = 0; i < NUM_CHANNELS; i++) { const exactChannel = `{{channel}}:${uuidv4()}`; const patternChannel = `{{pattern}}:${uuidv4()}`; const shardedChannel = `${SHARD_PREFIX}:${uuidv4()}`; - exactChannelsAndMessages[exactChannel] = uuidv4(); - patternChannelsAndMessages[patternChannel] = uuidv4(); - shardedChannelsAndMessages[shardedChannel] = uuidv4(); + exactChannelsAndMessages.push([exactChannel, uuidv4()]); + patternChannelsAndMessages.push([patternChannel, uuidv4()]); + shardedChannelsAndMessages.push([shardedChannel, uuidv4()]); } const publishResponse = 1; @@ -1973,7 +2051,9 @@ describe("PubSub", () => { { [GlideClusterClientConfiguration.PubSubChannelModes .Exact]: new Set( - Object.keys(exactChannelsAndMessages), + exactChannelsAndMessages.map((a) => + a[0].toString(), + ), ), }, {}, @@ -2013,7 +2093,9 @@ describe("PubSub", () => { { [GlideClusterClientConfiguration.PubSubChannelModes .Sharded]: new Set( - Object.keys(shardedChannelsAndMessages), + shardedChannelsAndMessages.map((a) => + a[0].toString(), + ), ), }, {}, @@ -2031,10 +2113,10 @@ describe("PubSub", () => { ); // Publish messages to exact and pattern channels - for (const [channel, message] of Object.entries({ + for (const [channel, message] of [ ...exactChannelsAndMessages, ...patternChannelsAndMessages, - })) { + ]) { const result = await publishingClient.publish( message, channel, @@ -2043,9 +2125,7 @@ describe("PubSub", () => { } // Publish sharded messages to all channels - for (const [channel, message] of Object.entries( - shardedChannelsAndMessages, - )) { + for (const [channel, message] of shardedChannelsAndMessages) { const result = await ( publishingClient as GlideClusterClient ).publish(message, channel, true); @@ -2057,71 +2137,57 @@ describe("PubSub", () => { // Verify messages for exact PUBSUB for (let index = 0; index < NUM_CHANNELS; index++) { - const pubsubMsg = await getMessageByMethod( + const pubsubMsg = (await getMessageByMethod( method, listeningClientExact, callbackMessagesExact, index, - ); + ))!; expect( - pubsubMsg.channel in exactChannelsAndMessages, - ).toBeTruthy(); - expect(pubsubMsg.message).toEqual( - exactChannelsAndMessages[pubsubMsg.channel], - ); + exactChannelsAndMessages.find( + ([channel]) => channel === pubsubMsg.channel, + ), + ).toEqual([pubsubMsg.channel, pubsubMsg.message]); + expect(pubsubMsg.pattern).toBeNull(); - delete exactChannelsAndMessages[pubsubMsg.channel]; } - // Check that we received all messages for exact PUBSUB - expect(Object.keys(exactChannelsAndMessages).length).toEqual(0); - // Verify messages for pattern PUBSUB for (let index = 0; index < NUM_CHANNELS; index++) { - const pubsubMsg = await getMessageByMethod( + const pubsubMsg = (await getMessageByMethod( method, listeningClientPattern, callbackMessagesPattern, index, - ); + ))!; + expect( - pubsubMsg.channel in patternChannelsAndMessages, - ).toBeTruthy(); - expect(pubsubMsg.message).toEqual( - patternChannelsAndMessages[pubsubMsg.channel], - ); + patternChannelsAndMessages.find( + ([channel]) => channel === pubsubMsg.channel, + ), + ).toEqual([pubsubMsg.channel, pubsubMsg.message]); + expect(pubsubMsg.pattern).toEqual(PATTERN); - delete patternChannelsAndMessages[pubsubMsg.channel]; } - // Check that we received all messages for pattern PUBSUB - expect(Object.keys(patternChannelsAndMessages).length).toEqual( - 0, - ); - // Verify messages for sharded PUBSUB for (let index = 0; index < NUM_CHANNELS; index++) { - const pubsubMsg = await getMessageByMethod( + const pubsubMsg = (await getMessageByMethod( method, listeningClientSharded, callbackMessagesSharded, index, - ); + ))!; + expect( - pubsubMsg.channel in shardedChannelsAndMessages, - ).toBeTruthy(); - expect(pubsubMsg.message).toEqual( - shardedChannelsAndMessages[pubsubMsg.channel], - ); + shardedChannelsAndMessages.find( + ([channel]) => channel === pubsubMsg.channel, + ), + ).toEqual([pubsubMsg.channel, pubsubMsg.message]); + expect(pubsubMsg.pattern).toBeNull(); - delete shardedChannelsAndMessages[pubsubMsg.channel]; } - // Check that we received all messages for sharded PUBSUB - expect(Object.keys(shardedChannelsAndMessages).length).toEqual( - 0, - ); - await checkNoMessagesLeft( method, listeningClientExact, @@ -2323,20 +2389,20 @@ describe("PubSub", () => { CHANNEL_NAME, ], ] as [TGlideClient, PubSubMsg[], string | null][]) { - const pubsubMsg = await getMessageByMethod( + const pubsubMsg = (await getMessageByMethod( method, client, callback, 0, - ); - const pubsubMsg2 = await getMessageByMethod( + ))!; + const pubsubMsg2 = (await getMessageByMethod( method, client, callback, 1, - ); + ))!; - expect(pubsubMsg.message).not.toEqual(pubsubMsg2.message); + expect(pubsubMsg.message).not.toEqual(pubsubMsg2!.message); expect([MESSAGE_PATTERN, MESSAGE_EXACT]).toContain( pubsubMsg.message, ); @@ -2350,12 +2416,12 @@ describe("PubSub", () => { } // Verify message for sharded PUBSUB - const pubsubMsgSharded = await getMessageByMethod( + const pubsubMsgSharded = (await getMessageByMethod( method, listeningClientSharded, callbackMessagesSharded, 0, - ); + ))!; expect(pubsubMsgSharded.message).toEqual(MESSAGE_SHARDED); expect(pubsubMsgSharded.channel).toEqual(CHANNEL_NAME); expect(pubsubMsgSharded.pattern).toBeNull(); @@ -2516,18 +2582,18 @@ describe("PubSub", () => { [clientExact, callbackMessagesExact, null], [clientPattern, callbackMessagesPattern, CHANNEL_NAME], ] as [TGlideClient, PubSubMsg[], string | null][]) { - const pubsubMsg = await getMessageByMethod( + const pubsubMsg = (await getMessageByMethod( method, client, callback, 0, - ); - const pubsubMsg2 = await getMessageByMethod( + ))!; + const pubsubMsg2 = (await getMessageByMethod( method, client, callback, 1, - ); + ))!; expect(pubsubMsg.message).not.toEqual(pubsubMsg2.message); expect([MESSAGE_PATTERN, MESSAGE_EXACT]).toContain( @@ -2719,18 +2785,18 @@ describe("PubSub", () => { [clientExact, callbackMessagesExact, null], [clientPattern, callbackMessagesPattern, CHANNEL_NAME], ] as [TGlideClient, PubSubMsg[], string | null][]) { - const pubsubMsg = await getMessageByMethod( + const pubsubMsg = (await getMessageByMethod( method, client, callback, 0, - ); - const pubsubMsg2 = await getMessageByMethod( + ))!; + const pubsubMsg2 = (await getMessageByMethod( method, client, callback, 1, - ); + ))!; expect(pubsubMsg.message).not.toEqual(pubsubMsg2.message); expect([MESSAGE_PATTERN, MESSAGE_EXACT]).toContain( @@ -2745,12 +2811,12 @@ describe("PubSub", () => { expect(pubsubMsg2.pattern).toEqual(pattern); } - const shardedMsg = await getMessageByMethod( + const shardedMsg = (await getMessageByMethod( method, clientSharded, callbackMessagesSharded, 0, - ); + ))!; expect(shardedMsg.message).toEqual(MESSAGE_SHARDED); expect(shardedMsg.channel).toEqual(CHANNEL_NAME); @@ -3554,9 +3620,12 @@ describe("PubSub", () => { ); } - expect( - await client.pubsubNumSub([channel1, channel2, channel3]), - ).toEqual({ + let subscribers = await client.pubsubNumSub([ + channel1, + channel2, + channel3, + ]); + expect(convertGlideRecordToRecord(subscribers)).toEqual({ [channel1]: 0, [channel2]: 0, [channel3]: 0, @@ -3577,13 +3646,13 @@ describe("PubSub", () => { ); // Test pubsubNumsub - const subscribers = await client2.pubsubNumSub([ + subscribers = await client2.pubsubNumSub([ channel1, channel2, channel3, channel4, ]); - expect(subscribers).toEqual({ + expect(convertGlideRecordToRecord(subscribers)).toEqual({ [channel1]: 1, [channel2]: 2, [channel3]: 3, @@ -3591,8 +3660,8 @@ describe("PubSub", () => { }); // Test pubsubNumsub with no channels - const emptySubscribers = await client2.pubsubNumSub(); - expect(emptySubscribers).toEqual({}); + const emptySubscribers = await client2.pubsubNumSub([]); + expect(emptySubscribers).toEqual([]); } finally { if (client1) { await clientCleanup( @@ -3741,6 +3810,12 @@ describe("PubSub", () => { it.each([true])( "test pubsub shardnumsub_%p", async (clusterMode) => { + const minVersion = "7.0.0"; + + if (cmeCluster.checkIfServerVersionLessThan(minVersion)) { + return; // Skip test if server version is less than required + } + let pubSub1: GlideClusterClientConfiguration.PubSubSubscriptions | null = null; let pubSub2: GlideClusterClientConfiguration.PubSubSubscriptions | null = @@ -3789,19 +3864,11 @@ describe("PubSub", () => { client = await GlideClusterClient.createClient( getOptions(clusterMode), ); - const minVersion = "7.0.0"; - if (cmeCluster.checkIfServerVersionLessThan(minVersion)) { - return; // Skip test if server version is less than required - } - - expect( - await (client as GlideClusterClient).pubsubShardNumSub([ - channel1, - channel2, - channel3, - ]), - ).toEqual({ + let subscribers = await ( + client as GlideClusterClient + ).pubsubShardNumSub([channel1, channel2, channel3]); + expect(convertGlideRecordToRecord(subscribers)).toEqual({ [channel1]: 0, [channel2]: 0, [channel3]: 0, @@ -3822,10 +3889,10 @@ describe("PubSub", () => { ); // Test pubsubShardnumsub - const subscribers = await ( + subscribers = await ( client4 as GlideClusterClient ).pubsubShardNumSub([channel1, channel2, channel3, channel4]); - expect(subscribers).toEqual({ + expect(convertGlideRecordToRecord(subscribers)).toEqual({ [channel1]: 1, [channel2]: 2, [channel3]: 3, @@ -3835,8 +3902,8 @@ describe("PubSub", () => { // Test pubsubShardnumsub with no channels const emptySubscribers = await ( client4 as GlideClusterClient - ).pubsubShardNumSub(); - expect(emptySubscribers).toEqual({}); + ).pubsubShardNumSub([]); + expect(emptySubscribers).toEqual([]); } finally { if (client1) { await clientCleanup(client1, pubSub1 ? pubSub1 : undefined); @@ -3946,12 +4013,17 @@ describe("PubSub", () => { * * @param clusterMode - Indicates if the test should be run in cluster mode. */ - it.each([true])( + it.each([true, false])( "test pubsub numsub and shardnumsub separation_%p", async (clusterMode) => { - let pubSub1: GlideClusterClientConfiguration.PubSubSubscriptions | null = - null; - let pubSub2: GlideClusterClientConfiguration.PubSubSubscriptions | null = + //const clusterMode = false; + const minVersion = "7.0.0"; + + if (cmeCluster.checkIfServerVersionLessThan(minVersion)) { + return; // Skip test if server version is less than required + } + + let pubSub: GlideClusterClientConfiguration.PubSubSubscriptions | null = null; let client1: TGlideClient | null = null; let client2: TGlideClient | null = null; @@ -3960,7 +4032,7 @@ describe("PubSub", () => { const regularChannel = "regular_channel"; const shardChannel = "shard_channel"; - pubSub1 = createPubSubSubscription( + pubSub = createPubSubSubscription( clusterMode, { [GlideClusterClientConfiguration.PubSubChannelModes @@ -3968,58 +4040,49 @@ describe("PubSub", () => { [GlideClusterClientConfiguration.PubSubChannelModes .Sharded]: new Set([shardChannel]), }, - {}, - ); - pubSub2 = createPubSubSubscription( - clusterMode, { [GlideClusterClientConfiguration.PubSubChannelModes .Exact]: new Set([regularChannel]), - [GlideClusterClientConfiguration.PubSubChannelModes - .Sharded]: new Set([shardChannel]), }, - {}, ); [client1, client2] = await createClients( clusterMode, getOptions(clusterMode), getOptions(clusterMode), - pubSub1, - pubSub2, + pubSub, + pubSub, ); - const minVersion = "7.0.0"; - - if (cmeCluster.checkIfServerVersionLessThan(minVersion)) { - return; // Skip test if server version is less than required - } - // Test pubsubNumsub const regularSubscribers = await client2.pubsubNumSub([ regularChannel, shardChannel, ]); - expect(regularSubscribers).toEqual({ + expect(convertGlideRecordToRecord(regularSubscribers)).toEqual({ [regularChannel]: 2, [shardChannel]: 0, }); // Test pubsubShardnumsub - const shardSubscribers = await ( - client2 as GlideClusterClient - ).pubsubShardNumSub([regularChannel, shardChannel]); - expect(shardSubscribers).toEqual({ - [regularChannel]: 0, - [shardChannel]: 2, - }); + if (clusterMode) { + const shardSubscribers = await ( + client2 as GlideClusterClient + ).pubsubShardNumSub([regularChannel, shardChannel]); + expect( + convertGlideRecordToRecord(shardSubscribers), + ).toEqual({ + [regularChannel]: 0, + [shardChannel]: 2, + }); + } } finally { if (client1) { - await clientCleanup(client1, pubSub1 ? pubSub1 : undefined); + await clientCleanup(client1, pubSub!); } if (client2) { - await clientCleanup(client2, pubSub2 ? pubSub2 : undefined); + await clientCleanup(client2, pubSub!); } } }, diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index bf4c791cc7..0f6fecb6c2 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -8,7 +8,6 @@ // represents a running server instance. See first 2 test cases as examples. import { expect, it } from "@jest/globals"; -import { GlideRecord, HashDataType } from "src/BaseClient"; import { v4 as uuidv4 } from "uuid"; import { BaseClientConfiguration, @@ -31,8 +30,10 @@ import { GeospatialData, GlideClient, GlideClusterClient, + GlideRecord, GlideReturnType, GlideString, + HashDataType, InfBoundary, InfoOptions, InsertPosition, @@ -49,15 +50,13 @@ import { Transaction, UnsignedEncoding, UpdateByScore, + convertElementsAndScores, + convertFieldsAndValuesToHashDataType, + convertGlideRecordToRecord, parseInfoResponse, -} from "../"; +} from ".."; import { RedisCluster } from "../../utils/TestUtils"; -import { - Client, - GetAndSetRandomValue, - compareMaps, - getFirstResult, -} from "./TestUtilities"; +import { Client, GetAndSetRandomValue, getFirstResult } from "./TestUtilities"; export type BaseClient = GlideClient | GlideClusterClient; @@ -138,9 +137,11 @@ export function runBaseTests(config: { `Check protocol version is RESP3`, async () => { await runTest(async (client: BaseClient) => { - const result = (await client.customCommand(["HELLO"])) as { - proto: number; - }; + const result = convertGlideRecordToRecord( + (await client.customCommand([ + "HELLO", + ])) as GlideRecord, + ); expect(result?.proto).toEqual(3); }, ProtocolVersion.RESP3); }, @@ -151,9 +152,11 @@ export function runBaseTests(config: { `Check possible to opt-in to RESP2`, async () => { await runTest(async (client: BaseClient) => { - const result = (await client.customCommand(["HELLO"])) as { - proto: number; - }; + const result = convertGlideRecordToRecord( + (await client.customCommand([ + "HELLO", + ])) as GlideRecord, + ); expect(result?.proto).toEqual(2); }, ProtocolVersion.RESP2); }, @@ -1237,13 +1240,13 @@ export function runBaseTests(config: { await runTest(async (client: BaseClient) => { const prevTimeout = (await client.configGet([ "timeout", - ])) as Record; + ])) as Record; expect(await client.configSet({ timeout: "1000" })).toEqual( "OK", ); const currTimeout = (await client.configGet([ "timeout", - ])) as Record; + ])) as Record; expect(currTimeout).toEqual({ timeout: "1000" }); /// Revert to the pervious configuration expect( @@ -1824,14 +1827,29 @@ export function runBaseTests(config: { }; expect(await client.hset(key, fieldValueMap)).toEqual(2); - expect(await client.hgetall(key)).toEqual({ - [field1]: value, - [field2]: value, - }); + expect(await client.hgetall(key)).toEqual( + convertFieldsAndValuesToHashDataType({ + [field1]: value, + [field2]: value, + }), + ); + + expect( + await client.hgetall(key, { decoder: Decoder.Bytes }), + ).toEqual([ + { + field: Buffer.from(field1), + value: Buffer.from(value), + }, + { + field: Buffer.from(field2), + value: Buffer.from(value), + }, + ]); expect( await client.hgetall(Buffer.from("nonExistingKey")), - ).toEqual({}); + ).toEqual([]); }, protocol); }, config.timeout, @@ -4314,23 +4332,33 @@ export function runBaseTests(config: { expect(await client.zdiff([nonExistingKey, key3])).toEqual([]); let result = await client.zdiffWithScores([key1, key2]); - const expected = { + const expected = convertElementsAndScores({ one: 1.0, three: 3.0, - }; - expect(compareMaps(result, expected)).toBe(true); + }); + expect(result).toEqual(expected); + // same with byte[] - result = await client.zdiffWithScores([ - key1, - Buffer.from(key2), + result = await client.zdiffWithScores( + [key1, Buffer.from(key2)], + { decoder: Decoder.Bytes }, + ); + expect(result).toEqual([ + { + element: Buffer.from("one"), + score: 1.0, + }, + { + element: Buffer.from("three"), + score: 3.0, + }, ]); - expect(compareMaps(result, expected)).toBe(true); result = await client.zdiffWithScores([key1, key3]); - expect(compareMaps(result, {})).toBe(true); + expect(result).toEqual([]); result = await client.zdiffWithScores([nonExistingKey, key3]); - expect(compareMaps(result, {})).toBe(true); + expect(result).toEqual([]); // invalid arg - key list must not be empty await expect(client.zdiff([])).rejects.toThrow(RequestError); @@ -4386,8 +4414,11 @@ export function runBaseTests(config: { start: 0, end: -1, }); - const expected1 = { one: 1.0, three: 3.0 }; - expect(compareMaps(result1, expected1)).toBe(true); + const expected1 = convertElementsAndScores({ + one: 1.0, + three: 3.0, + }); + expect(result1).toEqual(expected1); expect( await client.zdiffstore(Buffer.from(key4), [ @@ -4400,7 +4431,9 @@ export function runBaseTests(config: { start: 0, end: -1, }); - expect(compareMaps(result2, { four: 4.0 })).toBe(true); + expect(result2).toEqual( + convertElementsAndScores({ four: 4.0 }), + ); expect( await client.zdiffstore(key4, [Buffer.from(key1), key3]), @@ -4409,7 +4442,7 @@ export function runBaseTests(config: { start: 0, end: -1, }); - expect(compareMaps(result3, {})).toBe(true); + expect(result3).toEqual([]); expect( await client.zdiffstore(key4, [nonExistingKey, key1]), @@ -4418,7 +4451,7 @@ export function runBaseTests(config: { start: 0, end: -1, }); - expect(compareMaps(result4, {})).toBe(true); + expect(result4).toEqual([]); // invalid arg - key list must not be empty await expect(client.zdiffstore(key4, [])).rejects.toThrow( @@ -4487,7 +4520,9 @@ export function runBaseTests(config: { two: 2.5, three: 3.5, }; - expect(zunionstoreMapMax).toEqual(expectedMapMax); + expect(zunionstoreMapMax).toEqual( + convertElementsAndScores(expectedMapMax), + ); } async function zunionStoreWithMinAggregation(client: BaseClient) { @@ -4515,7 +4550,9 @@ export function runBaseTests(config: { two: 2.0, three: 3.5, }; - expect(zunionstoreMapMin).toEqual(expectedMapMin); + expect(zunionstoreMapMin).toEqual( + convertElementsAndScores(expectedMapMin), + ); } async function zunionStoreWithSumAggregation(client: BaseClient) { @@ -4541,7 +4578,11 @@ export function runBaseTests(config: { two: 4.5, three: 3.5, }; - expect(zunionstoreMapSum).toEqual(expectedMapSum); + expect(zunionstoreMapSum).toEqual( + convertElementsAndScores(expectedMapSum).sort( + (a, b) => a.score - b.score, + ), + ); } async function zunionStoreBasicTest(client: BaseClient) { @@ -4566,7 +4607,7 @@ export function runBaseTests(config: { three: 4.0, two: 5.0, }; - expect(zunionstoreMap).toEqual(expectedMap); + expect(zunionstoreMap).toEqual(convertElementsAndScores(expectedMap)); } async function zunionStoreWithWeightsAndAggregation(client: BaseClient) { @@ -4603,7 +4644,9 @@ export function runBaseTests(config: { three: 7.0, two: 9.0, }; - expect(zunionstoreMapMultiplied).toEqual(expectedMapMultiplied); + expect(zunionstoreMapMultiplied).toEqual( + convertElementsAndScores(expectedMapMultiplied), + ); } async function zunionStoreEmptyCases(client: BaseClient) { @@ -4634,7 +4677,9 @@ export function runBaseTests(config: { one: 1.0, two: 2.0, }; - expect(zunionstore_map_nonexistingkey).toEqual(expectedMapMultiplied); + expect(zunionstore_map_nonexistingkey).toEqual( + convertElementsAndScores(expectedMapMultiplied), + ); // Empty list check await expect(client.zunionstore("{xyz}", [])).rejects.toThrow(); @@ -4789,13 +4834,13 @@ export function runBaseTests(config: { end: -1, }); - expect( - compareMaps(result, { + expect(result).toEqual( + convertElementsAndScores({ one: 1.0, two: 2.0, three: 3.0, }), - ).toBe(true); + ); expect( await client.zrange( Buffer.from(key), @@ -4808,7 +4853,7 @@ export function runBaseTests(config: { ); expect( await client.zrangeWithScores(key, { start: 3, end: 1 }), - ).toEqual({}); + ).toEqual([]); }, protocol); }, config.timeout, @@ -4835,13 +4880,13 @@ export function runBaseTests(config: { type: "byScore", }); - expect( - compareMaps(result, { + expect(result).toEqual( + convertElementsAndScores({ one: 1.0, two: 2.0, three: 3.0, }), - ).toBe(true); + ); expect( await client.zrange( key, @@ -4897,7 +4942,7 @@ export function runBaseTests(config: { }, { reverse: true }, ), - ).toEqual({}); + ).toEqual([]); expect( await client.zrangeWithScores(key, { @@ -4905,7 +4950,7 @@ export function runBaseTests(config: { end: { value: 3, isInclusive: false }, type: "byScore", }), - ).toEqual({}); + ).toEqual([]); }, protocol); }, config.timeout, @@ -5225,7 +5270,7 @@ export function runBaseTests(config: { start: 0, end: 1, }), - ).toEqual({}); + ).toEqual([]); // test against a non-sorted set - throw RequestError expect(await client.set(key, "value")).toEqual("OK"); @@ -5281,7 +5326,9 @@ export function runBaseTests(config: { one: 2, two: 3, }; - expect(compareMaps(zinterstoreMapMax, expectedMapMax)).toBe(true); + expect(zinterstoreMapMax).toEqual( + convertElementsAndScores(expectedMapMax), + ); // Intersection results are aggregated by the MIN score of elements expect( @@ -5292,7 +5339,9 @@ export function runBaseTests(config: { one: 1, two: 2, }; - expect(compareMaps(zinterstoreMapMin, expectedMapMin)).toBe(true); + expect(zinterstoreMapMin).toEqual( + convertElementsAndScores(expectedMapMin), + ); // Intersection results are aggregated by the SUM score of elements expect( @@ -5303,7 +5352,9 @@ export function runBaseTests(config: { one: 3, two: 5, }; - expect(compareMaps(zinterstoreMapSum, expectedMapSum)).toBe(true); + expect(zinterstoreMapSum).toEqual( + convertElementsAndScores(expectedMapSum), + ); } async function zinterstoreBasicTest(client: BaseClient) { @@ -5327,7 +5378,7 @@ export function runBaseTests(config: { one: 3, two: 5, }; - expect(compareMaps(zinterstoreMap, expectedMap)).toBe(true); + expect(zinterstoreMap).toEqual(convertElementsAndScores(expectedMap)); } async function zinterstoreWithWeightsAndAggregation(client: BaseClient) { @@ -5363,9 +5414,9 @@ export function runBaseTests(config: { one: 6, two: 10, }; - expect( - compareMaps(zinterstoreMapMultiplied, expectedMapMultiplied), - ).toBe(true); + expect(zinterstoreMapMultiplied).toEqual( + convertElementsAndScores(expectedMapMultiplied), + ); } async function zinterstoreEmptyCases(client: BaseClient) { @@ -5452,7 +5503,7 @@ export function runBaseTests(config: { two: 4.5, }; expect(resultZinterWithScores).toEqual( - expectedZinterWithScores, + convertElementsAndScores(expectedZinterWithScores), ); }, protocol); }, @@ -5476,13 +5527,19 @@ export function runBaseTests(config: { // Intersection results are aggregated by the MAX score of elements const zinterWithScoresResults = await client.zinterWithScores( [key1, key2], - { aggregationType: "MAX" }, + { aggregationType: "MAX", decoder: Decoder.Bytes }, ); - const expectedMapMax = { - one: 1.5, - two: 2.5, - }; - expect(zinterWithScoresResults).toEqual(expectedMapMax); + const expected = [ + { + element: Buffer.from("one"), + score: 1.5, + }, + { + element: Buffer.from("two"), + score: 2.5, + }, + ]; + expect(zinterWithScoresResults).toEqual(expected); }, protocol); }, config.timeout, @@ -5511,7 +5568,9 @@ export function runBaseTests(config: { one: 1.0, two: 2.0, }; - expect(zinterWithScoresResults).toEqual(expectedMapMin); + expect(zinterWithScoresResults).toEqual( + convertElementsAndScores(expectedMapMin), + ); }, protocol); }, config.timeout, @@ -5540,7 +5599,9 @@ export function runBaseTests(config: { one: 2.5, two: 4.5, }; - expect(zinterWithScoresResults).toEqual(expectedMapSum); + expect(zinterWithScoresResults).toEqual( + convertElementsAndScores(expectedMapSum), + ); }, protocol); }, config.timeout, @@ -5572,7 +5633,9 @@ export function runBaseTests(config: { one: 6, two: 11, }; - expect(zinterWithScoresResults).toEqual(expectedMapSum); + expect(zinterWithScoresResults).toEqual( + convertElementsAndScores(expectedMapSum), + ); }, protocol); }, config.timeout, @@ -5596,7 +5659,7 @@ export function runBaseTests(config: { key1, "{testKey}-non_existing_key", ]), - ).toEqual({}); + ).toEqual([]); // Empty list check zinter await expect(client.zinter([])).rejects.toThrow(); @@ -5663,7 +5726,9 @@ export function runBaseTests(config: { three: 3.5, }; expect(resultZunionWithScores).toEqual( - expectedZunionWithScores, + convertElementsAndScores(expectedZunionWithScores).sort( + (a, b) => a.score - b.score, + ), ); }, protocol); }, @@ -5687,14 +5752,23 @@ export function runBaseTests(config: { // Union results are aggregated by the MAX score of elements const zunionWithScoresResults = await client.zunionWithScores( [key1, Buffer.from(key2)], - { aggregationType: "MAX" }, + { aggregationType: "MAX", decoder: Decoder.Bytes }, ); - const expectedMapMax = { - one: 1.5, - two: 2.5, - three: 3.5, - }; - expect(zunionWithScoresResults).toEqual(expectedMapMax); + const expected = [ + { + element: Buffer.from("one"), + score: 1.5, + }, + { + element: Buffer.from("two"), + score: 2.5, + }, + { + element: Buffer.from("three"), + score: 3.5, + }, + ]; + expect(zunionWithScoresResults).toEqual(expected); }, protocol); }, config.timeout, @@ -5724,7 +5798,9 @@ export function runBaseTests(config: { two: 2.0, three: 3.5, }; - expect(zunionWithScoresResults).toEqual(expectedMapMin); + expect(zunionWithScoresResults).toEqual( + convertElementsAndScores(expectedMapMin), + ); }, protocol); }, config.timeout, @@ -5754,7 +5830,11 @@ export function runBaseTests(config: { two: 4.5, three: 3.5, }; - expect(zunionWithScoresResults).toEqual(expectedMapSum); + expect(zunionWithScoresResults).toEqual( + convertElementsAndScores(expectedMapSum).sort( + (a, b) => a.score - b.score, + ), + ); }, protocol); }, config.timeout, @@ -5787,7 +5867,11 @@ export function runBaseTests(config: { two: 11, three: 7, }; - expect(zunionWithScoresResults).toEqual(expectedMapSum); + expect(zunionWithScoresResults).toEqual( + convertElementsAndScores(expectedMapSum).sort( + (a, b) => a.score - b.score, + ), + ); }, protocol); }, config.timeout, @@ -5815,7 +5899,7 @@ export function runBaseTests(config: { key1, "{testKey}-non_existing_key", ]), - ).toEqual(membersScores1); + ).toEqual(convertElementsAndScores(membersScores1)); // Empty list check zunion await expect(client.zunion([])).rejects.toThrow(); @@ -6074,20 +6158,29 @@ export function runBaseTests(config: { const key = uuidv4(); const membersScores = { a: 1, b: 2, c: 3 }; expect(await client.zadd(key, membersScores)).toEqual(3); - expect(await client.zpopmin(Buffer.from(key))).toEqual({ - a: 1.0, - }); + expect(await client.zpopmin(Buffer.from(key))).toEqual( + convertElementsAndScores({ a: 1.0 }), + ); expect( - compareMaps(await client.zpopmin(key, { count: 3 }), { - b: 2.0, - c: 3.0, + await client.zpopmin(key, { + count: 3, + decoder: Decoder.Bytes, }), - ).toBe(true); - expect(await client.zpopmin(key)).toEqual({}); + ).toEqual([ + { + element: Buffer.from("b"), + score: 2.0, + }, + { + element: Buffer.from("c"), + score: 3.0, + }, + ]); + expect(await client.zpopmin(key)).toEqual([]); expect(await client.set(key, "value")).toEqual("OK"); await expect(client.zpopmin(key)).rejects.toThrow(); - expect(await client.zpopmin("notExsitingKey")).toEqual({}); + expect(await client.zpopmin("notExsitingKey")).toEqual([]); }, protocol); }, config.timeout, @@ -6100,20 +6193,29 @@ export function runBaseTests(config: { const key = uuidv4(); const membersScores = { a: 1, b: 2, c: 3 }; expect(await client.zadd(key, membersScores)).toEqual(3); - expect(await client.zpopmax(Buffer.from(key))).toEqual({ - c: 3.0, - }); + expect(await client.zpopmax(Buffer.from(key))).toEqual( + convertElementsAndScores({ c: 3.0 }), + ); expect( - compareMaps(await client.zpopmax(key, { count: 3 }), { - b: 2.0, - a: 1.0, + await client.zpopmax(key, { + count: 3, + decoder: Decoder.Bytes, }), - ).toBe(true); - expect(await client.zpopmax(key)).toEqual({}); + ).toEqual([ + { + element: Buffer.from("b"), + score: 2.0, + }, + { + element: Buffer.from("a"), + score: 1.0, + }, + ]); + expect(await client.zpopmax(key)).toEqual([]); expect(await client.set(key, "value")).toEqual("OK"); await expect(client.zpopmax(key)).rejects.toThrow(); - expect(await client.zpopmax("notExsitingKey")).toEqual({}); + expect(await client.zpopmax("notExsitingKey")).toEqual([]); }, protocol); }, config.timeout, @@ -6660,7 +6762,7 @@ export function runBaseTests(config: { Buffer.from(key), { isInclusive: false, value: streamId2 }, { value: "5" }, - 1, + { count: 1 }, ), ).toEqual({ [streamId3]: [["f3", "v3"]] }); @@ -6669,7 +6771,7 @@ export function runBaseTests(config: { key, { value: "5" }, { isInclusive: false, value: streamId2 }, - 1, + { count: 1 }, ), ).toEqual({ [streamId3]: [["f3", "v3"]] }); } @@ -6683,7 +6785,7 @@ export function runBaseTests(config: { key, InfBoundary.NegativeInfinity, InfBoundary.PositiveInfinity, - 10, + { count: 10 }, ), ).toEqual({}); expect( @@ -6691,7 +6793,7 @@ export function runBaseTests(config: { key, InfBoundary.PositiveInfinity, InfBoundary.NegativeInfinity, - 10, + { count: 10 }, ), ).toEqual({}); @@ -6716,7 +6818,7 @@ export function runBaseTests(config: { key, InfBoundary.NegativeInfinity, InfBoundary.PositiveInfinity, - 0, + { count: 0 }, ), ).toEqual(null); expect( @@ -6724,7 +6826,7 @@ export function runBaseTests(config: { key, InfBoundary.NegativeInfinity, InfBoundary.PositiveInfinity, - -1, + { count: -1 }, ), ).toEqual(null); expect( @@ -6732,7 +6834,7 @@ export function runBaseTests(config: { key, InfBoundary.PositiveInfinity, InfBoundary.NegativeInfinity, - 0, + { count: 0 }, ), ).toEqual(null); expect( @@ -6740,7 +6842,7 @@ export function runBaseTests(config: { key, InfBoundary.PositiveInfinity, InfBoundary.NegativeInfinity, - -1, + { count: -1 }, ), ).toEqual(null); @@ -6979,38 +7081,38 @@ export function runBaseTests(config: { const field2 = "bar"; const field3 = "barvaz"; - const timestamp_1_1 = await client.xadd(key1, [ + const timestamp_1_1 = (await client.xadd(key1, [ [field1, "foo1"], [field3, "barvaz1"], - ]); + ])) as string; expect(timestamp_1_1).not.toBeNull(); - const timestamp_2_1 = await client.xadd(key2, [ + const timestamp_2_1 = (await client.xadd(key2, [ [field2, "bar1"], - ]); + ])) as string; expect(timestamp_2_1).not.toBeNull(); - const timestamp_1_2 = await client.xadd(key1, [ + const timestamp_1_2 = (await client.xadd(key1, [ [field1, "foo2"], - ]); - const timestamp_2_2 = await client.xadd(key2, [ + ])) as string; + const timestamp_2_2 = (await client.xadd(key2, [ [field2, "bar2"], - ]); - const timestamp_1_3 = await client.xadd(key1, [ + ])) as string; + const timestamp_1_3 = (await client.xadd(key1, [ [field1, "foo3"], [field3, "barvaz3"], - ]); - const timestamp_2_3 = await client.xadd(key2, [ + ])) as string; + const timestamp_2_3 = (await client.xadd(key2, [ [field2, "bar3"], - ]); + ])) as string; const result = await client.xread( [ { key: Buffer.from(key1), - value: timestamp_1_1 as string, + value: timestamp_1_1, }, { key: key2, - value: Buffer.from(timestamp_2_1 as string), + value: Buffer.from(timestamp_2_1), }, ], { @@ -7020,32 +7122,42 @@ export function runBaseTests(config: { const expected = { [key1]: { - [timestamp_1_2 as string]: [[field1, "foo2"]], - [timestamp_1_3 as string]: [ + [timestamp_1_2]: [[field1, "foo2"]], + [timestamp_1_3]: [ [field1, "foo3"], [field3, "barvaz3"], ], }, [key2]: { - [timestamp_2_2 as string]: [["bar", "bar2"]], - [timestamp_2_3 as string]: [["bar", "bar3"]], + [timestamp_2_2]: [["bar", "bar2"]], + [timestamp_2_3]: [["bar", "bar3"]], }, }; - expect(result).toEqual(expected); + expect(convertGlideRecordToRecord(result!)).toEqual(expected); // key does not exist expect(await client.xread({ [key3]: "0-0" })).toBeNull(); expect( - await client.xread({ - [key2]: timestamp_2_1 as string, - [key3]: "0-0", - }), - ).toEqual({ - [key2]: { - [timestamp_2_2 as string]: [["bar", "bar2"]], - [timestamp_2_3 as string]: [["bar", "bar3"]], + await client.xread( + { + [key2]: timestamp_2_1, + [key3]: "0-0", + }, + { decoder: Decoder.Bytes }, + ), + ).toEqual([ + { + key: Buffer.from(key2), + value: { + [timestamp_2_2]: [ + [Buffer.from("bar"), Buffer.from("bar2")], + ], + [timestamp_2_3]: [ + [Buffer.from("bar"), Buffer.from("bar3")], + ], + }, }, - }); + ]); // key is not a stream expect(await client.set(key3, uuidv4())).toEqual("OK"); @@ -7096,10 +7208,17 @@ export function runBaseTests(config: { // read the entire stream for the consumer and mark messages as pending expect( - await client.xreadgroup( - Buffer.from(group), - Buffer.from(consumer), - [{ key: Buffer.from(key1), value: Buffer.from(">") }], + convertGlideRecordToRecord( + (await client.xreadgroup( + Buffer.from(group), + Buffer.from(consumer), + [ + { + key: Buffer.from(key1), + value: Buffer.from(">"), + }, + ], + ))!, ), ).toEqual({ [key1]: { @@ -7113,7 +7232,11 @@ export function runBaseTests(config: { // now xreadgroup returns one empty entry and one non-empty entry expect( - await client.xreadgroup(group, consumer, { [key1]: "0" }), + convertGlideRecordToRecord( + (await client.xreadgroup(group, consumer, { + [key1]: "0", + }))!, + ), ).toEqual({ [key1]: { [entry1]: null, @@ -7131,7 +7254,11 @@ export function runBaseTests(config: { ["e", "f"], ])) as string; expect( - await client.xreadgroup(group, consumer, { [key1]: ">" }), + convertGlideRecordToRecord( + (await client.xreadgroup(group, consumer, { + [key1]: ">", + }))!, + ), ).toEqual({ [key1]: { [entry3]: [["e", "f"]], @@ -7150,10 +7277,12 @@ export function runBaseTests(config: { // read both keys expect( - await client.xreadgroup(group, consumer, { - [key1]: "0", - [key2]: "0", - }), + convertGlideRecordToRecord( + (await client.xreadgroup(group, consumer, { + [key1]: "0", + [key2]: "0", + }))!, + ), ).toEqual({ [key1]: { [entry1]: null, @@ -7184,7 +7313,11 @@ export function runBaseTests(config: { "OK", ); expect( - await client.xreadgroup(group, "_", { [key3]: "0-0" }), + convertGlideRecordToRecord( + (await client.xreadgroup(group, "_", { + [key3]: "0-0", + }))!, + ), ).toEqual({ [key3]: {}, }); @@ -7251,10 +7384,9 @@ export function runBaseTests(config: { id: streamId1_1, }), ).toEqual(streamId1_1); - const fullResult = (await client.xinfoStream( - Buffer.from(key), - 1, - )) as { + const fullResult = (await client.xinfoStream(Buffer.from(key), { + fullOptions: 1, + })) as { length: number; "radix-tree-keys": number; "radix-tree-nodes": number; @@ -7367,7 +7499,9 @@ export function runBaseTests(config: { expect(result["last-entry"]).toEqual(null); // XINFO STREAM FULL called against empty stream. Negative count values are ignored. - const fullResult = await client.xinfoStream(key, -3); + const fullResult = await client.xinfoStream(key, { + fullOptions: -3, + }); expect(fullResult["length"]).toEqual(0); expect(fullResult["entries"]).toEqual([]); expect(fullResult["groups"]).toEqual([]); @@ -7377,20 +7511,20 @@ export function runBaseTests(config: { client.xinfoStream(nonExistentKey), ).rejects.toThrow(); await expect( - client.xinfoStream(nonExistentKey, true), + client.xinfoStream(nonExistentKey, { fullOptions: true }), ).rejects.toThrow(); await expect( - client.xinfoStream(nonExistentKey, 2), + client.xinfoStream(nonExistentKey, { fullOptions: 2 }), ).rejects.toThrow(); // Key exists, but it is not a stream await client.set(stringKey, "boofar"); await expect(client.xinfoStream(stringKey)).rejects.toThrow(); await expect( - client.xinfoStream(stringKey, true), + client.xinfoStream(stringKey, { fullOptions: true }), ).rejects.toThrow(); await expect( - client.xinfoStream(stringKey, 2), + client.xinfoStream(stringKey, { fullOptions: 2 }), ).rejects.toThrow(); }, protocol); }, @@ -8223,7 +8357,7 @@ export function runBaseTests(config: { const nonExistingKey = uuidv4(); const maxmemoryPolicyKey = "maxmemory-policy"; const config = await client.configGet([maxmemoryPolicyKey]); - const maxmemoryPolicy = String(config[maxmemoryPolicyKey]); + const maxmemoryPolicy = config[maxmemoryPolicyKey] as string; try { expect( @@ -8258,7 +8392,7 @@ export function runBaseTests(config: { const nonExistingKey = uuidv4(); const maxmemoryPolicyKey = "maxmemory-policy"; const config = await client.configGet([maxmemoryPolicyKey]); - const maxmemoryPolicy = String(config[maxmemoryPolicyKey]); + const maxmemoryPolicy = config[maxmemoryPolicyKey] as string; try { expect( @@ -8945,11 +9079,13 @@ export function runBaseTests(config: { { start: 0, end: -1 }, { reverse: true }, ), - ).toEqual({ - edge2: 236529.17986494553, - Palermo: 166274.15156960033, - Catania: 0.0, - }); + ).toEqual( + convertElementsAndScores({ + edge2: 236529.17986494553, + Palermo: 166274.15156960033, + Catania: 0.0, + }), + ); // test search by box, unit: feet, from member, with limited count 2, with hash const feet = 400 * 3280.8399; @@ -8984,10 +9120,12 @@ export function runBaseTests(config: { ).toEqual(2); expect( await client.zrangeWithScores(key2, { start: 0, end: -1 }), - ).toEqual({ - Palermo: 3479099956230698, - edge1: 3479273021651468, - }); + ).toEqual( + convertElementsAndScores({ + Palermo: 3479099956230698, + edge1: 3479273021651468, + }), + ); // test search by box, unit: miles, from geospatial position, with limited ANY count to 1 const miles = 250; @@ -9268,14 +9406,24 @@ export function runBaseTests(config: { expect( await client.zmpop([key1, key2], ScoreFilter.MAX), - ).toEqual([key1, { b1: 2 }]); + ).toEqual([key1, convertElementsAndScores({ b1: 2 })]); expect( await client.zmpop( [Buffer.from(key2), key1], ScoreFilter.MAX, - { count: 10 }, + { + count: 10, + decoder: Decoder.Bytes, + }, ), - ).toEqual([key2, { a2: 0.1, b2: 0.2 }]); + ).toEqual([ + Buffer.from(key2), + + [ + { element: Buffer.from("b2"), score: 0.2 }, + { element: Buffer.from("a2"), score: 0.1 }, + ], + ]); expect( await client.zmpop([nonExistingKey], ScoreFilter.MIN), @@ -9319,7 +9467,9 @@ export function runBaseTests(config: { }); if (result) { - expect(result[1]).toEqual(entries); + expect(result[1]).toEqual( + convertElementsAndScores(entries), + ); } }, protocol); }, @@ -9552,7 +9702,7 @@ export function runBaseTests(config: { expect( await client.bzmpop([key1, key2], ScoreFilter.MAX, 0.1), - ).toEqual([key1, { b1: 2 }]); + ).toEqual([key1, convertElementsAndScores({ b1: 2 })]); expect( await client.bzmpop( [key2, Buffer.from(key1)], @@ -9560,9 +9710,16 @@ export function runBaseTests(config: { 0.1, { count: 10, + decoder: Decoder.Bytes, }, ), - ).toEqual([key2, { a2: 0.1, b2: 0.2 }]); + ).toEqual([ + Buffer.from(key2), + [ + { element: Buffer.from("b2"), score: 0.2 }, + { element: Buffer.from("a2"), score: 0.1 }, + ], + ]); // ensure that command doesn't time out even if timeout > request timeout (250ms by default) expect( @@ -9620,7 +9777,9 @@ export function runBaseTests(config: { ); if (result) { - expect(result[1]).toEqual(entries); + expect(result[1]).toEqual( + convertElementsAndScores(entries), + ); } }, protocol); }, @@ -10242,14 +10401,13 @@ export function runBaseTests(config: { await client.xgroupCreate(key, groupName1, "0-0"), ).toEqual("OK"); - expect( - await client.xreadgroup( - groupName1, - consumer1, - { [key]: ">" }, - { count: 1 }, - ), - ).toEqual({ + let xreadgroup = await client.xreadgroup( + groupName1, + consumer1, + { [key]: ">" }, + { count: 1 }, + ); + expect(convertGlideRecordToRecord(xreadgroup!)).toEqual({ [key]: { [streamId1]: [ ["entry1_field1", "entry1_value1"], @@ -10276,19 +10434,37 @@ export function runBaseTests(config: { consumer2, ), ).toBeTruthy(); - expect( - await client.xreadgroup(groupName1, consumer2, { + xreadgroup = await client.xreadgroup( + groupName1, + consumer2, + { [key]: ">", - }), - ).toEqual({ - [key]: { - [streamId2]: [ - ["entry2_field1", "entry2_value1"], - ["entry2_field2", "entry2_value2"], - ], - [streamId3]: [["entry3_field1", "entry3_value1"]], }, - }); + { decoder: Decoder.Bytes }, + ); + expect(xreadgroup).toEqual([ + { + key: Buffer.from(key), + value: { + [streamId2]: [ + [ + Buffer.from("entry2_field1"), + Buffer.from("entry2_value1"), + ], + [ + Buffer.from("entry2_field2"), + Buffer.from("entry2_value2"), + ], + ], + [streamId3]: [ + [ + Buffer.from("entry3_field1"), + Buffer.from("entry3_value1"), + ], + ], + }, + }, + ]); // Verify that xinfo_consumers contains info for 2 consumers now result = await client.xinfoConsumers(key, groupName1); @@ -10422,17 +10598,12 @@ export function runBaseTests(config: { ], ); - expect( - await client.customCommand([ - "XREADGROUP", - "GROUP", - groupName1, - consumer1, - "STREAMS", - key, - ">", - ]), - ).toEqual({ + const xreadgroup = await client.xreadgroup( + groupName1, + consumer1, + { [key]: ">" }, + ); + expect(convertGlideRecordToRecord(xreadgroup!)).toEqual({ [key]: { [streamId1]: [ ["entry1_field1", "entry1_value1"], @@ -10468,14 +10639,9 @@ export function runBaseTests(config: { ], ); - expect( - await client.customCommand([ - "XACK", - key, - groupName1, - streamId1, - ]), - ).toEqual(1); + expect(await client.xack(key, groupName1, [streamId1])).toEqual( + 1, + ); // once message ack'ed, pending counter decreased expect(await client.xinfoGroups(key)).toEqual( cluster.checkIfServerVersionLessThan("7.0.0") @@ -10549,9 +10715,11 @@ export function runBaseTests(config: { ).toBe("OK"); expect( - await client.xreadgroup(groupName, consumerName, { - [key]: ">", - }), + convertGlideRecordToRecord( + (await client.xreadgroup(groupName, consumerName, { + [key]: ">", + }))!, + ), ).toEqual({ [key]: { [streamid1_0]: [["f0", "v0"]], @@ -10590,7 +10758,7 @@ export function runBaseTests(config: { consumerName, { [key]: ">" }, ); - expect(newResult).toEqual({ + expect(convertGlideRecordToRecord(newResult!)).toEqual({ [key]: { [streamid1_2]: [["f2", "v2"]], }, @@ -10643,13 +10811,7 @@ export function runBaseTests(config: { }), ).toEqual("OK"); expect( - await client.customCommand([ - "xgroup", - "createconsumer", - key, - group, - "consumer", - ]), + await client.xgroupCreateConsumer(key, group, "consumer"), ).toEqual(true); expect( @@ -10671,7 +10833,11 @@ export function runBaseTests(config: { ).toEqual("0-2"); expect( - await client.xreadgroup(group, "consumer", { [key]: ">" }), + convertGlideRecordToRecord( + (await client.xreadgroup(group, "consumer", { + [key]: ">", + }))!, + ), ).toEqual({ [key]: { "0-1": [ @@ -10767,7 +10933,11 @@ export function runBaseTests(config: { ).toEqual("0-2"); expect( - await client.xreadgroup(group, "consumer", { [key]: ">" }), + convertGlideRecordToRecord( + (await client.xreadgroup(group, "consumer", { + [key]: ">", + }))!, + ), ).toEqual({ [key]: { "0-1": [ @@ -10879,7 +11049,11 @@ export function runBaseTests(config: { ).toEqual("0-2"); expect( - await client.xreadgroup(group, "consumer", { [key]: ">" }), + convertGlideRecordToRecord( + (await client.xreadgroup(group, "consumer", { + [key]: ">", + }))!, + ), ).toEqual({ [key]: { "0-1": [ @@ -10897,7 +11071,7 @@ export function runBaseTests(config: { Buffer.from("consumer"), 0, Buffer.from("0-0"), - 1, + { count: 1 }, ); let expected: typeof result = [ "0-2", @@ -10998,9 +11172,11 @@ export function runBaseTests(config: { await client.xgroupCreate(key, groupName, stream_id0), ).toBe("OK"); expect( - await client.xreadgroup(groupName, consumerName, { - [key]: ">", - }), + convertGlideRecordToRecord( + (await client.xreadgroup(groupName, consumerName, { + [key]: ">", + }))!, + ), ).toEqual({ [key]: { [stream_id1_0]: [["f0", "v0"]], @@ -11042,9 +11218,11 @@ export function runBaseTests(config: { // read the last unacknowledged entry expect( - await client.xreadgroup(groupName, consumerName, { - [key]: ">", - }), + convertGlideRecordToRecord( + (await client.xreadgroup(groupName, consumerName, { + [key]: ">", + }))!, + ), ).toEqual({ [key]: { [stream_id1_2]: [["f2", "v2"]] } }); // deleting the consumer, returns 1 since the last entry still hasn't been acknowledged @@ -11106,16 +11284,14 @@ export function runBaseTests(config: { const multiKeyArray = [key2, key1]; const count = 1; const lpushArgs = ["one", "two", "three", "four", "five"]; - const expected = { [key1]: ["five"] }; - const expected2 = { [key2]: ["one", "two"] }; + const expected = { key: key1, elements: ["five"] }; + const expected2 = { key: key2, elements: ["one", "two"] }; // nothing to be popped expect( - await client.lmpop( - singleKeyArray, - ListDirection.LEFT, + await client.lmpop(singleKeyArray, ListDirection.LEFT, { count, - ), + }), ).toBeNull(); // pushing to the arrays to be popped @@ -11129,7 +11305,10 @@ export function runBaseTests(config: { // popping multiple elements from the right expect( - await client.lmpop(multiKeyArray, ListDirection.RIGHT, 2), + await client.lmpop(multiKeyArray, ListDirection.RIGHT, { + count: 2, + decoder: Decoder.String, + }), ).toEqual(expected2); // Key exists, but is not a set @@ -11144,7 +11323,7 @@ export function runBaseTests(config: { // pushing to the arrays to be popped expect(await client.lpush(key3, lpushArgs)).toEqual(5); - const expectedWithKey3 = { [key3]: ["five"] }; + const expectedWithKey3 = { key: key3, elements: ["five"] }; // checking correct result from popping expect( @@ -11163,14 +11342,17 @@ export function runBaseTests(config: { // pushing to the arrays to be popped expect(await client.lpush(key4, lpushArgs)).toEqual(5); - const expectedWithKey4 = { [key4]: ["one", "two"] }; + const expectedWithKey4 = { + key: Buffer.from(key4), + elements: [Buffer.from("one"), Buffer.from("two")], + }; // checking correct result from popping expect( await client.lmpop( multiKeyArrayWithKey3AndKey4, ListDirection.RIGHT, - 2, + { count: 2, decoder: Decoder.Bytes }, ), ).toEqual(expectedWithKey4); }, protocol); @@ -11193,8 +11375,8 @@ export function runBaseTests(config: { const multiKeyArray = [key2, key1]; const count = 1; const lpushArgs = ["one", "two", "three", "four", "five"]; - const expected = { [key1]: ["five"] }; - const expected2 = { [key2]: ["one", "two"] }; + const expected = { key: key1, elements: ["five"] }; + const expected2 = { key: key2, elements: ["one", "two"] }; // nothing to be popped expect( @@ -11202,7 +11384,7 @@ export function runBaseTests(config: { singleKeyArray, ListDirection.LEFT, 0.1, - count, + { count }, ), ).toBeNull(); @@ -11225,14 +11407,16 @@ export function runBaseTests(config: { multiKeyArray, ListDirection.RIGHT, 0.1, - 2, + { count: 2, decoder: Decoder.String }, ), ).toEqual(expected2); // Key exists, but is not a set expect(await client.set(nonListKey, "blmpop")).toBe("OK"); await expect( - client.blmpop([nonListKey], ListDirection.RIGHT, 0.1, 1), + client.blmpop([nonListKey], ListDirection.RIGHT, 0.1, { + count: 1, + }), ).rejects.toThrow(RequestError); // Test with single binary key array as input @@ -11241,7 +11425,7 @@ export function runBaseTests(config: { // pushing to the arrays to be popped expect(await client.lpush(key3, lpushArgs)).toEqual(5); - const expectedWithKey3 = { [key3]: ["five"] }; + const expectedWithKey3 = { key: key3, elements: ["five"] }; // checking correct result from popping expect( @@ -11261,7 +11445,10 @@ export function runBaseTests(config: { // pushing to the arrays to be popped expect(await client.lpush(key4, lpushArgs)).toEqual(5); - const expectedWithKey4 = { [key4]: ["one", "two"] }; + const expectedWithKey4 = { + key: Buffer.from(key4), + elements: [Buffer.from("one"), Buffer.from("two")], + }; // checking correct result from popping expect( @@ -11269,7 +11456,7 @@ export function runBaseTests(config: { multiKeyArrayWithKey3AndKey4, ListDirection.RIGHT, 0.1, - 2, + { count: 2, decoder: Decoder.Bytes }, ), ).toEqual(expectedWithKey4); }, protocol); @@ -11338,9 +11525,11 @@ export function runBaseTests(config: { // read the entire stream for the consumer and mark messages as pending expect( - await client.xreadgroup(groupName, consumer, { - [key]: ">", - }), + convertGlideRecordToRecord( + (await client.xreadgroup(groupName, consumer, { + [key]: ">", + }))!, + ), ).toEqual({ [key]: { [streamid1 as string]: [["field1", "value1"]], diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index fddab2bc09..0e1b9d54d6 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -37,6 +37,7 @@ import { TimeUnit, Transaction, UnsignedEncoding, + convertRecordToGlideRecord, } from ".."; /* eslint-disable @typescript-eslint/no-explicit-any */ @@ -411,17 +412,20 @@ export function compareMaps( */ export function checkFunctionListResponse( response: FunctionListResponse, - libName: string, - functionDescriptions: Map, - functionFlags: Map, - libCode?: string, + libName: GlideString, + functionDescriptions: Map, + functionFlags: Map, + libCode?: GlideString, ) { - // TODO rework after #1953 https://github.com/valkey-io/valkey-glide/pull/1953 expect(response.length).toBeGreaterThan(0); let hasLib = false; for (const lib of response) { - hasLib = lib["library_name"] == libName; + hasLib = + typeof libName === "string" + ? libName === lib["library_name"] + : (libName as Buffer).compare(lib["library_name"] as Buffer) == + 0; if (hasLib) { const functions = lib["functions"]; @@ -430,15 +434,16 @@ export function checkFunctionListResponse( for (const functionData of functions) { const functionInfo = functionData as Record< string, - string | string[] + GlideString | GlideString[] >; - const name = functionInfo["name"] as string; - const flags = functionInfo["flags"] as string[]; + const name = functionInfo["name"] as GlideString; + const flags = functionInfo["flags"] as GlideString[]; + expect(functionInfo["description"]).toEqual( - functionDescriptions.get(name), + functionDescriptions.get(name.toString()), ); - expect(flags).toEqual(functionFlags.get(name)); + expect(flags).toEqual(functionFlags.get(name.toString())); } if (libCode) { @@ -689,8 +694,8 @@ export async function transactionTest( responseData.push(["pubsubChannels()", []]); baseTransaction.pubsubNumPat(); responseData.push(["pubsubNumPat()", 0]); - baseTransaction.pubsubNumSub(); - responseData.push(["pubsubNumSub()", {}]); + baseTransaction.pubsubNumSub([]); + responseData.push(["pubsubNumSub()", []]); baseTransaction.flushall(); responseData.push(["flushall()", "OK"]); @@ -808,7 +813,10 @@ export async function transactionTest( baseTransaction.hget(key4, field); responseData.push(["hget(key4, field)", value]); baseTransaction.hgetall(key4); - responseData.push(["hgetall(key4)", { [field]: value }]); + responseData.push([ + "hgetall(key4)", + convertRecordToGlideRecord({ [field]: value }), + ]); baseTransaction.hdel(key4, [field]); responseData.push(["hdel(key4, [field])", 1]); baseTransaction.hmget(key4, [field]); @@ -1021,7 +1029,12 @@ export async function transactionTest( baseTransaction.zrangeWithScores(key8, { start: 0, end: -1 }); responseData.push([ "zrangeWithScores(key8, { start: 0, end: -1 })", - { member2: 3, member3: 3.5, member4: 4, member5: 5 }, + convertRecordToGlideRecord({ + member2: 3, + member3: 3.5, + member4: 4, + member5: 5, + }), ]); baseTransaction.zadd(key12, { one: 1, two: 2 }); responseData.push(["zadd(key12, { one: 1, two: 2 })", 2]); @@ -1063,7 +1076,10 @@ export async function transactionTest( baseTransaction.zdiff([key13, key12]); responseData.push(["zdiff([key13, key12])", ["three"]]); baseTransaction.zdiffWithScores([key13, key12]); - responseData.push(["zdiffWithScores([key13, key12])", { three: 3.5 }]); + responseData.push([ + "zdiffWithScores([key13, key12])", + convertRecordToGlideRecord({ three: 3.5 }), + ]); baseTransaction.zdiffstore(key13, [key13, key13]); responseData.push(["zdiffstore(key13, [key13, key13])", 0]); baseTransaction.zunionstore(key5, [key12, key13]); @@ -1086,12 +1102,14 @@ export async function transactionTest( baseTransaction.zinterWithScores([key27, key26]); responseData.push([ "zinterWithScores([key27, key26])", - { one: 2, two: 4 }, + convertRecordToGlideRecord({ one: 2, two: 4 }), ]); baseTransaction.zunionWithScores([key27, key26]); responseData.push([ "zunionWithScores([key27, key26])", - { one: 2, two: 4, three: 3.5 }, + convertRecordToGlideRecord({ one: 2, two: 4, three: 3.5 }).sort( + (a, b) => a.value - b.value, + ), ]); } } else { @@ -1114,9 +1132,15 @@ export async function transactionTest( 4, ]); baseTransaction.zpopmin(key8); - responseData.push(["zpopmin(key8)", { member2: 3.0 }]); + responseData.push([ + "zpopmin(key8)", + convertRecordToGlideRecord({ member2: 3.0 }), + ]); baseTransaction.zpopmax(key8); - responseData.push(["zpopmax(key8)", { member5: 5 }]); + responseData.push([ + "zpopmax(key8)", + convertRecordToGlideRecord({ member5: 5 }), + ]); baseTransaction.zadd(key8, { member6: 6 }); responseData.push(["zadd(key8, {member6: 6})", 1]); baseTransaction.bzpopmax([key8], 0.5); @@ -1148,20 +1172,26 @@ export async function transactionTest( baseTransaction.zintercard([key8, key14], 1); responseData.push(["zintercard([key8, key14], 1)", 0]); baseTransaction.zmpop([key14], ScoreFilter.MAX); - responseData.push(["zmpop([key14], MAX)", [key14, { two: 2.0 }]]); + responseData.push([ + "zmpop([key14], MAX)", + [key14, convertRecordToGlideRecord({ two: 2.0 })], + ]); baseTransaction.zmpop([key14], ScoreFilter.MAX, 1); - responseData.push(["zmpop([key14], MAX, 1)", [key14, { one: 1.0 }]]); + responseData.push([ + "zmpop([key14], MAX, 1)", + [key14, convertRecordToGlideRecord({ one: 1.0 })], + ]); baseTransaction.zadd(key14, { one: 1.0, two: 2.0 }); responseData.push(["zadd(key14, { one: 1.0, two: 2.0 })", 2]); baseTransaction.bzmpop([key14], ScoreFilter.MAX, 0.1); responseData.push([ "bzmpop([key14], ScoreFilter.MAX, 0.1)", - [key14, { two: 2.0 }], + [key14, convertRecordToGlideRecord({ two: 2.0 })], ]); baseTransaction.bzmpop([key14], ScoreFilter.MAX, 0.1, 1); responseData.push([ "bzmpop([key14], ScoreFilter.MAX, 0.1, 1)", - [key14, { one: 1.0 }], + [key14, convertRecordToGlideRecord({ one: 1.0 })], ]); } @@ -1183,18 +1213,24 @@ export async function transactionTest( baseTransaction.xlen(key9); responseData.push(["xlen(key9)", 3]); baseTransaction.xrange(key9, { value: "0-1" }, { value: "0-1" }); - responseData.push(["xrange(key9)", { "0-1": [["field", "value1"]] }]); + responseData.push([ + "xrange(key9)", + convertRecordToGlideRecord({ "0-1": [["field", "value1"]] }), + ]); baseTransaction.xrevrange(key9, { value: "0-1" }, { value: "0-1" }); - responseData.push(["xrevrange(key9)", { "0-1": [["field", "value1"]] }]); + responseData.push([ + "xrevrange(key9)", + convertRecordToGlideRecord({ "0-1": [["field", "value1"]] }), + ]); baseTransaction.xread({ [key9]: "0-1" }); responseData.push([ 'xread({ [key9]: "0-1" })', - { - [key9]: { + convertRecordToGlideRecord({ + [key9]: convertRecordToGlideRecord({ "0-2": [["field", "value2"]], "0-3": [["field", "value3"]], - }, - }, + }), + }), ]); baseTransaction.xtrim(key9, { method: "minid", @@ -1229,7 +1265,11 @@ export async function transactionTest( baseTransaction.xreadgroup(groupName1, consumer, { [key9]: ">" }); responseData.push([ 'xreadgroup(groupName1, consumer, {[key9]: ">"})', - { [key9]: { "0-2": [["field", "value2"]] } }, + convertRecordToGlideRecord({ + [key9]: convertRecordToGlideRecord({ + "0-2": [["field", "value2"]], + }), + }), ]); baseTransaction.xpending(key9, groupName1); responseData.push([ @@ -1248,7 +1288,7 @@ export async function transactionTest( baseTransaction.xclaim(key9, groupName1, consumer, 0, ["0-2"]); responseData.push([ 'xclaim(key9, groupName1, consumer, 0, ["0-2"])', - { "0-2": [["field", "value2"]] }, + convertRecordToGlideRecord({ "0-2": [["field", "value2"]] }), ]); baseTransaction.xclaim(key9, groupName1, consumer, 0, ["0-2"], { isForce: true, @@ -1257,7 +1297,7 @@ export async function transactionTest( }); responseData.push([ 'xclaim(key9, groupName1, consumer, 0, ["0-2"], { isForce: true, retryCount: 0, idle: 0})', - { "0-2": [["field", "value2"]] }, + convertRecordToGlideRecord({ "0-2": [["field", "value2"]] }), ]); baseTransaction.xclaimJustId(key9, groupName1, consumer, 0, ["0-2"]); responseData.push([ @@ -1279,8 +1319,19 @@ export async function transactionTest( responseData.push([ 'xautoclaim(key9, groupName1, consumer, 0, "0-0", 1)', gte(version, "7.0.0") - ? ["0-0", { "0-2": [["field", "value2"]] }, []] - : ["0-0", { "0-2": [["field", "value2"]] }], + ? [ + "0-0", + convertRecordToGlideRecord({ + "0-2": [["field", "value2"]], + }), + [], + ] + : [ + "0-0", + convertRecordToGlideRecord({ + "0-2": [["field", "value2"]], + }), + ], ]); baseTransaction.xautoclaimJustId(key9, groupName1, consumer, 0, "0-0"); responseData.push([ @@ -1561,10 +1612,15 @@ export async function transactionTest( baseTransaction.functionStats(); responseData.push([ "functionStats()", - { + convertRecordToGlideRecord({ running_script: null, - engines: { LUA: { libraries_count: 1, functions_count: 1 } }, - }, + engines: convertRecordToGlideRecord({ + LUA: convertRecordToGlideRecord({ + libraries_count: 1, + functions_count: 1, + }), + }), + }), ]); baseTransaction.functionDelete(libName); responseData.push(["functionDelete(libName)", "OK"]); @@ -1600,7 +1656,7 @@ export async function transactionTest( ["lcsLen(key1, key3)", 0], [ "lcsIdx(key1, key2)", - { + convertRecordToGlideRecord({ matches: [ [ [1, 3], @@ -1608,11 +1664,11 @@ export async function transactionTest( ], ], len: 3, - }, + }), ], [ "lcsIdx(key1, key2, {minMatchLen: 1})", - { + convertRecordToGlideRecord({ matches: [ [ [1, 3], @@ -1620,15 +1676,21 @@ export async function transactionTest( ], ], len: 3, - }, + }), ], [ "lcsIdx(key1, key2, {withMatchLen: true})", - { matches: [[[1, 3], [0, 2], 3]], len: 3 }, + convertRecordToGlideRecord({ + matches: [[[1, 3], [0, 2], 3]], + len: 3, + }), ], [ "lcsIdx(key1, key2, {withMatchLen: true, minMatchLen: 1})", - { matches: [[[1, 3], [0, 2], 3]], len: 3 }, + convertRecordToGlideRecord({ + matches: [[[1, 3], [0, 2], 3]], + len: 3, + }), ], ["del([key1, key2, key3])", 3], );