ClusterCommandObjects.java

package redis.clients.jedis;

import redis.clients.jedis.params.IParams;
import redis.clients.jedis.params.MSetExParams;
import redis.clients.jedis.params.ScanParams;
import redis.clients.jedis.resps.HotkeysInfo;
import redis.clients.jedis.params.HotkeysParams;
import redis.clients.jedis.resps.ScanResult;
import redis.clients.jedis.util.JedisClusterCRC16;
import redis.clients.jedis.util.JedisClusterHashTag;
import redis.clients.jedis.util.KeyValue;

import java.util.*;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.stream.Collectors;

import static redis.clients.jedis.Protocol.Command.*;
import static redis.clients.jedis.Protocol.Keyword.TYPE;

public class ClusterCommandObjects extends CommandObjects {

  private static final String CLUSTER_UNSUPPORTED_MESSAGE = "Not supported in cluster mode.";

  private static final String SCAN_PATTERN_MESSAGE = "Cluster mode only supports SCAN command"
      + " with MATCH pattern containing hash-tag ( curly-brackets enclosed string )";

  @Override
  public final CommandObject<ScanResult<String>> scan(String cursor) {
    throw new IllegalArgumentException(SCAN_PATTERN_MESSAGE);
  }

  @Override
  public final CommandObject<ScanResult<String>> scan(String cursor, ScanParams params) {
    String match = params.match();
    if (match == null || !JedisClusterHashTag.isClusterCompliantMatchPattern(match)) {
      throw new IllegalArgumentException(SCAN_PATTERN_MESSAGE);
    }
    return new CommandObject<>(commandArguments(SCAN).add(cursor).addParams(params).addHashSlotKey(match), BuilderFactory.SCAN_RESPONSE);
  }

  @Override
  public final CommandObject<ScanResult<String>> scan(String cursor, ScanParams params, String type) {
    String match = params.match();
    if (match == null || !JedisClusterHashTag.isClusterCompliantMatchPattern(match)) {
      throw new IllegalArgumentException(SCAN_PATTERN_MESSAGE);
    }
    return new CommandObject<>(commandArguments(SCAN).add(cursor).addParams(params).addHashSlotKey(match).add(TYPE).add(type), BuilderFactory.SCAN_RESPONSE);
  }

  @Override
  public final CommandObject<ScanResult<byte[]>> scan(byte[] cursor) {
    throw new IllegalArgumentException(SCAN_PATTERN_MESSAGE);
  }

  @Override
  public final CommandObject<ScanResult<byte[]>> scan(byte[] cursor, ScanParams params) {
    byte[] match = params.binaryMatch();
    if (match == null || !JedisClusterHashTag.isClusterCompliantMatchPattern(match)) {
      throw new IllegalArgumentException(SCAN_PATTERN_MESSAGE);
    }
    return new CommandObject<>(commandArguments(SCAN).add(cursor).addParams(params).addHashSlotKey(match), BuilderFactory.SCAN_BINARY_RESPONSE);
  }

  @Override
  public final CommandObject<ScanResult<byte[]>> scan(byte[] cursor, ScanParams params, byte[] type) {
    byte[] match = params.binaryMatch();
    if (match == null || !JedisClusterHashTag.isClusterCompliantMatchPattern(match)) {
      throw new IllegalArgumentException(SCAN_PATTERN_MESSAGE);
    }
    return new CommandObject<>(commandArguments(SCAN).add(cursor).addParams(params).addHashSlotKey(match).add(TYPE).add(type), BuilderFactory.SCAN_BINARY_RESPONSE);
  }

  @Override
  public final CommandObject<Long> waitReplicas(int replicas, long timeout) {
    throw new UnsupportedOperationException(CLUSTER_UNSUPPORTED_MESSAGE);
  }

  @Override
  public CommandObject<KeyValue<Long, Long>> waitAOF(long numLocal, long numReplicas, long timeout) {
    throw new UnsupportedOperationException(CLUSTER_UNSUPPORTED_MESSAGE);
  }

  @Override
  public CommandObject<String> hotkeysStart(HotkeysParams params) {
    throw new UnsupportedOperationException(CLUSTER_UNSUPPORTED_MESSAGE);
  }

  @Override
  public CommandObject<String> hotkeysStop() {
    throw new UnsupportedOperationException(CLUSTER_UNSUPPORTED_MESSAGE);
  }

  @Override
  public CommandObject<String> hotkeysReset() {
    throw new UnsupportedOperationException(CLUSTER_UNSUPPORTED_MESSAGE);
  }

  @Override
  public CommandObject<HotkeysInfo> hotkeysGet() {
    throw new UnsupportedOperationException(CLUSTER_UNSUPPORTED_MESSAGE);
  }

  /**
   * Groups key-value pairs by their hash slot and creates separate CommandArguments for each slot.
   * This enables commands with multiple keys to be properly distributed across Redis cluster nodes
   * by ensuring that each resulting command only operates on keys that hash to the same slot.
   *
   * @param args the original command arguments to copy (the command itself will be preserved)
   * @param keysValues variable number of key-value pairs to be grouped (must be even length)
   * @param params additional parameters for the command (may be null)
   * @return a list of CommandArguments objects, each containing only keys/values that belong to the same hash slot
   * @throws IllegalArgumentException if keysValues has odd length
   */
  protected List<CommandArguments> groupArgumentsByKeyValueHashSlot(CommandArguments args, String[] keysValues, IParams params) {
    return groupArgumentsByKeyValueHashSlotImpl(
        args,
        keysValues,
        params,
        JedisClusterCRC16::getSlot,
        CommandArguments::key,
        CommandArguments::add,
        false
    );
  }

  /**
   * Groups key-value pairs by their hash slot and creates separate CommandArguments for each slot.
   * This enables commands with multiple keys to be properly distributed across Redis cluster nodes
   * by ensuring that each resulting command only operates on keys that hash to the same slot.
   *
   * @param args the original command arguments to copy (the command itself will be preserved)
   * @param keysValues variable number of key-value pairs to be grouped (must be even length)
   * @param params additional parameters for the command (may be null)
   * @return a list of CommandArguments objects, each containing only keys/values that belong to the same hash slot
   * @throws IllegalArgumentException if keysValues has odd length
   */
  protected List<CommandArguments> groupArgumentsByKeyValueHashSlot(CommandArguments args, byte[][] keysValues, IParams params) {
    return groupArgumentsByKeyValueHashSlotImpl(
        args,
        keysValues,
        params,
        JedisClusterCRC16::getSlot,
        CommandArguments::key,
        CommandArguments::add,
        false
    );
  }

  /**
   * Groups keys by their hash slot and creates separate CommandArguments for each slot.
   * This enables commands with multiple keys (like DEL, EXISTS, MGET) to be properly distributed
   * across Redis cluster nodes by ensuring that each resulting command only operates on keys
   * that hash to the same slot.
   *
   * <p><b>Order Preservation:</b> This method preserves the order of keys as they appear in the input array.
   * Consecutive keys that hash to the same slot are grouped together into a single CommandArguments,
   * but non-consecutive keys with the same slot are kept in separate CommandArguments to maintain order.
   * For example, if input keys map to slots [A, B, A], the result will be 3 separate CommandArguments
   * (not 2), ensuring that when results are concatenated, they match the original input key order.</p>
   *
   * @param args the original command arguments to copy (the command itself will be preserved)
   * @param keys variable number of keys to be grouped
   * @param params additional parameters for the command (may be null)
   * @return a list of CommandArguments objects, each containing only keys that belong to the same hash slot,
   *         preserving the original key order
   */
  protected List<CommandArguments> groupArgumentsByKeyHashSlot(CommandArguments args, String[] keys, IParams params) {
    return groupArgumentsByKeyValueHashSlotImpl(
        args,
        keys,
        params,
        JedisClusterCRC16::getSlot,
        CommandArguments::key,
        null,
        false
    );
  }

  /**
   * Groups keys by their hash slot and creates separate CommandArguments for each slot.
   * This enables commands with multiple keys (like DEL, EXISTS, MGET) to be properly distributed
   * across Redis cluster nodes by ensuring that each resulting command only operates on keys
   * that hash to the same slot.
   *
   * <p><b>Order Preservation:</b> This method preserves the order of keys as they appear in the input array.
   * Consecutive keys that hash to the same slot are grouped together into a single CommandArguments,
   * but non-consecutive keys with the same slot are kept in separate CommandArguments to maintain order.
   * For example, if input keys map to slots [A, B, A], the result will be 3 separate CommandArguments
   * (not 2), ensuring that when results are concatenated, they match the original input key order.</p>
   *
   * @param args the original command arguments to copy (the command itself will be preserved)
   * @param keys variable number of keys to be grouped
   * @param params additional parameters for the command (may be null)
   * @return a list of CommandArguments objects, each containing only keys that belong to the same hash slot,
   *         preserving the original key order
   */
  protected List<CommandArguments> groupArgumentsByKeyHashSlot(CommandArguments args, byte[][] keys, IParams params) {
    return groupArgumentsByKeyValueHashSlotImpl(
        args,
        keys,
        params,
        JedisClusterCRC16::getSlot,
        CommandArguments::key,
        null,
        false
    );
  }

  /**
   * Groups key-value pairs by their hash slot and creates separate CommandArguments for each slot.
   * Inserts the key count after the command but before the keys (for commands like MSETEX).
   *
   * <p><b>Order Preservation:</b> This method preserves the order of key-value pairs as they appear
   * in the input array. Consecutive pairs that hash to the same slot are grouped together, but
   * non-consecutive pairs with the same slot are kept in separate CommandArguments to maintain order.</p>
   *
   * @param args the original command arguments to copy (the command itself will be preserved)
   * @param keysValues variable number of key-value pairs to be grouped (must be even length)
   * @param params additional parameters for the command (may be null)
   * @return a list of CommandArguments objects, each containing only keys/values that belong to the same hash slot
   * @throws IllegalArgumentException if keysValues has odd length
   */
  protected List<CommandArguments> groupArgumentsByKeyValueHashSlotWithKeyCount(CommandArguments args, String[] keysValues, IParams params) {
    return groupArgumentsByKeyValueHashSlotImpl(
        args,
        keysValues,
        params,
        JedisClusterCRC16::getSlot,
        CommandArguments::key,
        CommandArguments::add,
        true
    );
  }

  /**
   * Groups key-value pairs by their hash slot and creates separate CommandArguments for each slot.
   * Inserts the key count after the command but before the keys (for commands like MSETEX).
   *
   * @param args the original command arguments to copy (the command itself will be preserved)
   * @param keysValues variable number of key-value pairs to be grouped (must be even length)
   * @param params additional parameters for the command (may be null)
   * @return a list of CommandArguments objects, each containing only keys/values that belong to the same hash slot
   * @throws IllegalArgumentException if keysValues has odd length
   */
  protected List<CommandArguments> groupArgumentsByKeyValueHashSlotWithKeyCount(CommandArguments args, byte[][] keysValues, IParams params) {
    return groupArgumentsByKeyValueHashSlotImpl(
        args,
        keysValues,
        params,
        JedisClusterCRC16::getSlot,
        CommandArguments::key,
        CommandArguments::add,
        true
    );
  }

  /**
   * Internal helper method that implements the common logic for grouping keys (and optionally values) by hash slot.
   * When valueAdder is null, this method processes keys only (for commands like DEL, EXISTS, MGET).
   * When valueAdder is provided, this method processes key-value pairs (for commands like MSET).
   *
   * <p><b>Order Preservation:</b> This method preserves the order of keys as they appear in the input array.
   * Consecutive keys that hash to the same slot are grouped together into a single CommandArguments,
   * but non-consecutive keys with the same slot are kept in separate CommandArguments to maintain order.
   * For example, if input keys map to slots [A, B, A], the result will be 3 separate CommandArguments
   * (not 2), preserving the original key order in the concatenated results.</p>
   *
   * @param <T> the type of key/value elements (String or byte[])
   * @param args the original command arguments to copy (the command itself will be preserved)
   * @param keysOrKeysValues array of keys (when valueAdder is null) or key-value pairs (when valueAdder is provided)
   * @param params additional parameters for the command (may be null)
   * @param slotCalculator function to calculate the hash slot for a key
   * @param keyAdder function to add a key to CommandArguments
   * @param valueAdder function to add a value to CommandArguments (may be null for key-only operations)
   * @param insertKeyCount if true, inserts the number of keys after the command but before the keys (for commands like MSETEX)
   * @return a list of CommandArguments objects, each containing only keys/values that belong to the same hash slot,
   *         preserving the original key order
   * @throws IllegalArgumentException if valueAdder is provided and keysOrKeysValues has odd length
   */
  private <T> List<CommandArguments> groupArgumentsByKeyValueHashSlotImpl(
      CommandArguments args,
      T[] keysOrKeysValues,
      IParams params,
      Function<T, Integer> slotCalculator,
      BiConsumer<CommandArguments, T> keyAdder,
      BiConsumer<CommandArguments, T> valueAdder,
      boolean insertKeyCount) {

    boolean keyValueMode = valueAdder != null;
    int step = keyValueMode ? 2 : 1;

    if (keyValueMode && keysOrKeysValues.length % 2 != 0) {
      throw new IllegalArgumentException("keysValues must contain an even number of elements (key-value pairs)");
    }

    if (keysOrKeysValues.length == 0) {
      return new ArrayList<>();
    }

    // Wrap slotCalculator to apply keyPreProcessor transformation before slot calculation.
    // This ensures keys are grouped by their actual slot (after preprocessing), not the original key's slot.
    Function<T, Integer> effectiveSlotCalculator = keyPreProcessor != null
        ? key -> calculateSlotFromPreprocessedKey(keyPreProcessor.actualKey(key))
        : slotCalculator;

    // Group consecutive keys with the same slot together, preserving input order
    // Non-consecutive keys with the same slot will be in separate commands
    List<CommandArguments> result = new ArrayList<>();

    int currentSlot = -1;
    List<T> currentGroup = new ArrayList<>();

    for (int i = 0; i < keysOrKeysValues.length; i += step) {
      T key = keysOrKeysValues[i];
      int slot = effectiveSlotCalculator.apply(key);

      if (slot != currentSlot && !currentGroup.isEmpty()) {
        // Slot changed - finalize the current group
        result.add(createCommandArgsForGroup(args, currentGroup, params, keyAdder, valueAdder, insertKeyCount, step));
        currentGroup = new ArrayList<>();
      }

      currentSlot = slot;
      currentGroup.add(key);
      if (keyValueMode) {
        currentGroup.add(keysOrKeysValues[i + 1]);
      }
    }

    // Finalize the last group
    if (!currentGroup.isEmpty()) {
      result.add(createCommandArgsForGroup(args, currentGroup, params, keyAdder, valueAdder, insertKeyCount, step));
    }

    return result;
  }

  /**
   * Calculates the hash slot for a preprocessed key.
   *
   * @param preprocessedKey the key after preprocessing (may be String, byte[], or Rawable)
   * @return the hash slot for the key
   */
  private int calculateSlotFromPreprocessedKey(Object preprocessedKey) {
    if (preprocessedKey instanceof byte[]) {
      return JedisClusterCRC16.getSlot((byte[]) preprocessedKey);
    } else if (preprocessedKey instanceof String) {
      return JedisClusterCRC16.getSlot((String) preprocessedKey);
    } else if (preprocessedKey instanceof redis.clients.jedis.args.Rawable) {
      return JedisClusterCRC16.getSlot(((redis.clients.jedis.args.Rawable) preprocessedKey).getRaw());
    }
    throw new IllegalArgumentException("Unsupported key type: " + preprocessedKey.getClass().getName());
  }

  /**
   * Helper method to create a CommandArguments for a group of keys/values.
   */
  private <T> CommandArguments createCommandArgsForGroup(
      CommandArguments args,
      List<T> groupedElements,
      IParams params,
      BiConsumer<CommandArguments, T> keyAdder,
      BiConsumer<CommandArguments, T> valueAdder,
      boolean insertKeyCount,
      int step) {

    boolean keyValueMode = valueAdder != null;
    CommandArguments slotArgs = commandArguments(args.getCommand());

    // Insert key count after command but before keys (e.g., numkeys for MSETEX)
    if (insertKeyCount) {
      int keyCount = groupedElements.size() / step;
      slotArgs.add(keyCount);
    }

    // Add keys (and optionally values) for this slot
    for (int i = 0; i < groupedElements.size(); i += step) {
      keyAdder.accept(slotArgs, groupedElements.get(i));
      if (keyValueMode) {
        valueAdder.accept(slotArgs, groupedElements.get(i + 1));
      }
    }

    // Add params if provided
    if (params != null) {
      slotArgs.addParams(params);
    }

    return slotArgs;
  }

  // ==================== Multi-Shard Command Methods ====================
  // These methods split commands across multiple Redis cluster shards based on key hash slots.
  // They return List<CommandObject<T>> where each CommandObject targets keys in the same hash slot.

  /**
   * Creates multiple DEL command objects, one for each hash slot group.
   * This enables the DEL command to be executed across multiple Redis cluster shards
   * when keys hash to different slots.
   *
   * @param keys the keys to delete
   * @return a list of CommandObject instances, each containing keys that belong to the same hash slot
   */
  public List<CommandObject<Long>> delMultiShard(String... keys) {
    CommandArguments args = commandArguments(DEL);
    List<CommandArguments> groupedArgs = groupArgumentsByKeyHashSlot(args, keys, null);
    return groupedArgs.stream()
        .map(cmdArgs -> new CommandObject<>(cmdArgs, BuilderFactory.LONG))
        .collect(Collectors.toList());
  }

  /**
   * Creates multiple DEL command objects, one for each hash slot group.
   * This enables the DEL command to be executed across multiple Redis cluster shards
   * when keys hash to different slots.
   *
   * @param keys the keys to delete
   * @return a list of CommandObject instances, each containing keys that belong to the same hash slot
   */
  public List<CommandObject<Long>> delMultiShard(byte[]... keys) {
    CommandArguments args = commandArguments(DEL);
    List<CommandArguments> groupedArgs = groupArgumentsByKeyHashSlot(args, keys, null);
    return groupedArgs.stream()
        .map(cmdArgs -> new CommandObject<>(cmdArgs, BuilderFactory.LONG))
        .collect(Collectors.toList());
  }

  /**
   * Creates multiple EXISTS command objects, one for each hash slot group.
   * This enables the EXISTS command to be executed across multiple Redis cluster shards
   * when keys hash to different slots.
   *
   * @param keys the keys to check for existence
   * @return a list of CommandObject instances, each containing keys that belong to the same hash slot
   */
  public List<CommandObject<Long>> existsMultiShard(String... keys) {
    CommandArguments args = commandArguments(EXISTS);
    List<CommandArguments> groupedArgs = groupArgumentsByKeyHashSlot(args, keys, null);
    return groupedArgs.stream()
        .map(cmdArgs -> new CommandObject<>(cmdArgs, BuilderFactory.LONG))
        .collect(Collectors.toList());
  }

  /**
   * Creates multiple EXISTS command objects, one for each hash slot group.
   * This enables the EXISTS command to be executed across multiple Redis cluster shards
   * when keys hash to different slots.
   *
   * @param keys the keys to check for existence
   * @return a list of CommandObject instances, each containing keys that belong to the same hash slot
   */
  public List<CommandObject<Long>> existsMultiShard(byte[]... keys) {
    CommandArguments args = commandArguments(EXISTS);
    List<CommandArguments> groupedArgs = groupArgumentsByKeyHashSlot(args, keys, null);
    return groupedArgs.stream()
        .map(cmdArgs -> new CommandObject<>(cmdArgs, BuilderFactory.LONG))
        .collect(Collectors.toList());
  }

  /**
   * Creates multiple MGET command objects, one for each hash slot group.
   * This enables the MGET command to be executed across multiple Redis cluster shards
   * when keys hash to different slots.
   *
   * <p><b>Order Preservation:</b> The returned commands preserve the order of keys as they appear
   * in the input array. When the results from all commands are concatenated in order, the values
   * will correspond positionally to the input keys.</p>
   *
   * @param keys the keys to retrieve values for
   * @return a list of CommandObject instances, each containing keys that belong to the same hash slot
   */
  public List<CommandObject<List<String>>> mgetMultiShard(String... keys) {
    CommandArguments args = commandArguments(MGET);
    List<CommandArguments> groupedArgs = groupArgumentsByKeyHashSlot(args, keys, null);
    return groupedArgs.stream()
        .map(cmdArgs -> new CommandObject<>(cmdArgs, BuilderFactory.STRING_LIST))
        .collect(Collectors.toList());
  }

  /**
   * Creates multiple MGET command objects, one for each hash slot group.
   * This enables the MGET command to be executed across multiple Redis cluster shards
   * when keys hash to different slots.
   *
   * <p><b>Order Preservation:</b> The returned commands preserve the order of keys as they appear
   * in the input array. When the results from all commands are concatenated in order, the values
   * will correspond positionally to the input keys.</p>
   *
   * @param keys the keys to retrieve values for
   * @return a list of CommandObject instances, each containing keys that belong to the same hash slot
   */
  public List<CommandObject<List<byte[]>>> mgetMultiShard(byte[]... keys) {
    CommandArguments args = commandArguments(MGET);
    List<CommandArguments> groupedArgs = groupArgumentsByKeyHashSlot(args, keys, null);
    return groupedArgs.stream()
        .map(cmdArgs -> new CommandObject<>(cmdArgs, BuilderFactory.BINARY_LIST))
        .collect(Collectors.toList());
  }

  /**
   * Creates multiple MSET command objects, one for each hash slot group.
   * This enables the MSET command to be executed across multiple Redis cluster shards
   * when keys hash to different slots.
   *
   * @param keysvalues alternating keys and values (key1, value1, key2, value2, ...)
   * @return a list of CommandObject instances, each containing key-value pairs that belong to the same hash slot
   */
  public List<CommandObject<String>> msetMultiShard(String... keysvalues) {
    CommandArguments args = commandArguments(MSET);
    List<CommandArguments> groupedArgs = groupArgumentsByKeyValueHashSlot(args, keysvalues, null);
    return groupedArgs.stream()
        .map(cmdArgs -> new CommandObject<>(cmdArgs, BuilderFactory.STRING))
        .collect(Collectors.toList());
  }

  /**
   * Creates multiple MSET command objects, one for each hash slot group.
   * This enables the MSET command to be executed across multiple Redis cluster shards
   * when keys hash to different slots.
   *
   * @param keysvalues alternating keys and values (key1, value1, key2, value2, ...)
   * @return a list of CommandObject instances, each containing key-value pairs that belong to the same hash slot
   */
  public List<CommandObject<String>> msetMultiShard(byte[]... keysvalues) {
    CommandArguments args = commandArguments(MSET);
    List<CommandArguments> groupedArgs = groupArgumentsByKeyValueHashSlot(args, keysvalues, null);
    return groupedArgs.stream()
        .map(cmdArgs -> new CommandObject<>(cmdArgs, BuilderFactory.STRING))
        .collect(Collectors.toList());
  }

  /**
   * Creates multiple TOUCH command objects, one for each hash slot group.
   * This enables the TOUCH command to be executed across multiple Redis cluster shards
   * when keys hash to different slots.
   *
   * @param keys the keys to touch
   * @return a list of CommandObject instances, each containing keys that belong to the same hash slot
   */
  public List<CommandObject<Long>> touchMultiShard(String... keys) {
    CommandArguments args = commandArguments(TOUCH);
    List<CommandArguments> groupedArgs = groupArgumentsByKeyHashSlot(args, keys, null);
    return groupedArgs.stream()
        .map(cmdArgs -> new CommandObject<>(cmdArgs, BuilderFactory.LONG))
        .collect(Collectors.toList());
  }

  /**
   * Creates multiple TOUCH command objects, one for each hash slot group.
   * This enables the TOUCH command to be executed across multiple Redis cluster shards
   * when keys hash to different slots.
   *
   * @param keys the keys to touch
   * @return a list of CommandObject instances, each containing keys that belong to the same hash slot
   */
  public List<CommandObject<Long>> touchMultiShard(byte[]... keys) {
    CommandArguments args = commandArguments(TOUCH);
    List<CommandArguments> groupedArgs = groupArgumentsByKeyHashSlot(args, keys, null);
    return groupedArgs.stream()
        .map(cmdArgs -> new CommandObject<>(cmdArgs, BuilderFactory.LONG))
        .collect(Collectors.toList());
  }

  /**
   * Creates multiple UNLINK command objects, one for each hash slot group.
   * This enables the UNLINK command to be executed across multiple Redis cluster shards
   * when keys hash to different slots.
   *
   * @param keys the keys to unlink
   * @return a list of CommandObject instances, each containing keys that belong to the same hash slot
   */
  public List<CommandObject<Long>> unlinkMultiShard(String... keys) {
    CommandArguments args = commandArguments(UNLINK);
    List<CommandArguments> groupedArgs = groupArgumentsByKeyHashSlot(args, keys, null);
    return groupedArgs.stream()
        .map(cmdArgs -> new CommandObject<>(cmdArgs, BuilderFactory.LONG))
        .collect(Collectors.toList());
  }

  /**
   * Creates multiple UNLINK command objects, one for each hash slot group.
   * This enables the UNLINK command to be executed across multiple Redis cluster shards
   * when keys hash to different slots.
   *
   * @param keys the keys to unlink
   * @return a list of CommandObject instances, each containing keys that belong to the same hash slot
   */
  public List<CommandObject<Long>> unlinkMultiShard(byte[]... keys) {
    CommandArguments args = commandArguments(UNLINK);
    List<CommandArguments> groupedArgs = groupArgumentsByKeyHashSlot(args, keys, null);
    return groupedArgs.stream()
        .map(cmdArgs -> new CommandObject<>(cmdArgs, BuilderFactory.LONG))
        .collect(Collectors.toList());
  }

  /**
   * Creates multiple MSETEX command objects, one for each hash slot group.
   * This enables the MSETEX command to be executed across multiple Redis cluster shards
   * when keys hash to different slots. Each command preserves the provided parameters
   * (expiration, NX/XX conditions).
   *
   * @param params the MSETEX parameters (expiration, NX/XX conditions)
   * @param keysvalues alternating keys and values (key1, value1, key2, value2, ...)
   * @return a list of CommandObject instances, each containing key-value pairs that belong to the same hash slot
   */
  public List<CommandObject<Boolean>> msetexMultiShard(MSetExParams params, String... keysvalues) {
    CommandArguments args = commandArguments(MSETEX);
    List<CommandArguments> groupedArgs = groupArgumentsByKeyValueHashSlotWithKeyCount(args, keysvalues, params);
    return groupedArgs.stream()
        .map(cmdArgs -> new CommandObject<>(cmdArgs, BuilderFactory.BOOLEAN))
        .collect(Collectors.toList());
  }

  /**
   * Creates multiple MSETEX command objects, one for each hash slot group.
   * This enables the MSETEX command to be executed across multiple Redis cluster shards
   * when keys hash to different slots. Each command preserves the provided parameters
   * (expiration, NX/XX conditions).
   *
   * @param params the MSETEX parameters (expiration, NX/XX conditions)
   * @param keysvalues alternating keys and values (key1, value1, key2, value2, ...)
   * @return a list of CommandObject instances, each containing key-value pairs that belong to the same hash slot
   */
  public List<CommandObject<Boolean>> msetexMultiShard(MSetExParams params, byte[]... keysvalues) {
    CommandArguments args = commandArguments(MSETEX);
    List<CommandArguments> groupedArgs = groupArgumentsByKeyValueHashSlotWithKeyCount(args, keysvalues, params);
    return groupedArgs.stream()
        .map(cmdArgs -> new CommandObject<>(cmdArgs, BuilderFactory.BOOLEAN))
        .collect(Collectors.toList());
  }

}