///|
/// Builder for commands that should run inside one Redis transaction.
///
/// Add commands to a `Multi`, keep the returned tokens, then pass the `Multi`
/// to `Transaction::exec` and read each result with `MultiResult::get`.
///
/// Example:
/// ```moonbit nocheck
/// let multi = @redis.Multi()
///
/// let old_value = multi.get("account:1")
///
/// let wrote = multi.set("account:1", "updated")
/// ```
struct Multi {
  commands : Array[ReadOnlyArray[Bytes]]
}

///|
/// Creates an empty transaction command builder.
pub fn Multi::Multi() -> Multi {
  { commands: [] }
}

///|
/// Result handle for one command added to `Multi`.
///
/// Use the token with `MultiResult::get` after `Transaction::exec` succeeds.
struct MultiToken[T] {
  index : Int
  decode : (RawValue) -> T raise
}

///|
/// Result set returned by a successful `Transaction::exec`.
struct MultiResult {
  values : ReadOnlyArray[RawValue]
}

///|
/// Transaction context passed to `Client::transaction`.
///
/// Use it only inside the callback passed to `Client::transaction`.
struct Transaction {
  r : @resp.Reader
  w : @resp.Writer
  version : @resp.Version
  lock : @async.Semaphore
  mut done : Bool
}

///|
fn copy_bytes_views(args : ArrayView[BytesView]) -> Array[Bytes] {
  let data : Array[Bytes] = Array::new(capacity=args.length())
  for part in args {
    data.push(part.to_owned())
  }
  data
}

///|
fn response_error(err : Bytes) -> Error {
  try decode_utf8(err) catch {
    err => err
  } noraise {
    msg => ServerError(msg)
  }
}

///|
async fn read_direct_response(
  r : @resp.Reader,
  version : @resp.Version,
) -> RawValue {
  for ;; {
    match r.read_response(version~) {
      Reply(value) => return value.raw()
      Push(_) => continue
    }
  }
  panic()
}

///|
async fn execute_direct_command(
  w : @resp.Writer,
  r : @resp.Reader,
  version : @resp.Version,
  cmd : ReadOnlyArray[Bytes],
) -> RawValue {
  w.write_command(cmd)
  read_direct_response(r, version)
}

///|
fn[T] Multi::queue(self : Multi, spec : Command[T]) -> MultiToken[T] {
  let index = self.commands.length()
  self.commands.push(ReadOnlyArray::from_array(spec.args))
  { index, decode: spec.res_decode_fn }
}

///|
/// Adds a custom command to this transaction builder.
///
/// Example:
/// ```moonbit nocheck
/// let multi = @redis.Multi()
///
/// let size = multi.raw_command([b"DBSIZE"], value => value.as_int())
/// ```
pub fn[T] Multi::raw_command(
  self : Multi,
  args : ArrayView[BytesView],
  decode : (RawValue) -> T raise,
) -> MultiToken[T] {
  self.queue(Command(copy_bytes_views(args), decode))
}

///|
pub fn Multi::get(self : Multi, key : String) -> MultiToken[String?] {
  self.queue(Command::get(key))
}

///|
pub fn Multi::set(
  self : Multi,
  key : String,
  value : String,
  ex? : Int,
  px? : Int,
  nx? : Bool,
  xx? : Bool,
) -> MultiToken[Bool] {
  self.queue(Command::set(key, value, ex?, px?, nx?, xx?))
}

///|
pub fn Multi::set_get(
  self : Multi,
  key : String,
  value : String,
  ex? : Int,
  px? : Int,
  nx? : Bool,
  xx? : Bool,
) -> MultiToken[String?] {
  self.queue(Command::set_get(key, value, ex?, px?, nx?, xx?))
}

///|
pub fn Multi::setnx(
  self : Multi,
  key : String,
  value : String,
) -> MultiToken[Bool] {
  self.queue(Command::setnx(key, value))
}

///|
pub fn Multi::setex(
  self : Multi,
  key : String,
  seconds : Int,
  value : String,
) -> MultiToken[Unit] {
  self.queue(Command::setex(key, seconds, value))
}

///|
pub fn Multi::psetex(
  self : Multi,
  key : String,
  milliseconds : Int,
  value : String,
) -> MultiToken[Unit] {
  self.queue(Command::psetex(key, milliseconds, value))
}

///|
pub fn Multi::mget(
  self : Multi,
  keys : ArrayView[String],
) -> MultiToken[Array[String?]] {
  self.queue(Command::mget(keys))
}

///|
pub fn Multi::mset(
  self : Multi,
  items : ArrayView[(String, String)],
) -> MultiToken[Unit] {
  self.queue(Command::mset(items))
}

///|
pub fn Multi::msetnx(
  self : Multi,
  items : ArrayView[(String, String)],
) -> MultiToken[Bool] {
  self.queue(Command::msetnx(items))
}

///|
pub fn Multi::incr(self : Multi, key : String) -> MultiToken[Int] {
  self.queue(Command::incr(key))
}

///|
pub fn Multi::incrby(
  self : Multi,
  key : String,
  increment : Int,
) -> MultiToken[Int] {
  self.queue(Command::incrby(key, increment))
}

///|
pub fn Multi::incrbyfloat(
  self : Multi,
  key : String,
  increment : Double,
) -> MultiToken[Double] {
  self.queue(Command::incrbyfloat(key, increment))
}

///|
pub fn Multi::decr(self : Multi, key : String) -> MultiToken[Int] {
  self.queue(Command::decr(key))
}

///|
pub fn Multi::decrby(
  self : Multi,
  key : String,
  decrement : Int,
) -> MultiToken[Int] {
  self.queue(Command::decrby(key, decrement))
}

///|
pub fn Multi::append(
  self : Multi,
  key : String,
  value : String,
) -> MultiToken[Int] {
  self.queue(Command::append(key, value))
}

///|
pub fn Multi::strlen(self : Multi, key : String) -> MultiToken[Int] {
  self.queue(Command::strlen(key))
}

///|
pub fn Multi::getrange(
  self : Multi,
  key : String,
  start : Int,
  end : Int,
) -> MultiToken[String] {
  self.queue(Command::getrange(key, start, end))
}

///|
pub fn Multi::setrange(
  self : Multi,
  key : String,
  offset : Int,
  value : String,
) -> MultiToken[Int] {
  self.queue(Command::setrange(key, offset, value))
}

///|
pub fn Multi::del(self : Multi, keys : ArrayView[String]) -> MultiToken[Int] {
  self.queue(Command::del(keys))
}

///|
pub fn Multi::exists(self : Multi, keys : ArrayView[String]) -> MultiToken[Int] {
  self.queue(Command::exists(keys))
}

///|
pub fn Multi::expire(
  self : Multi,
  key : String,
  seconds : Int,
) -> MultiToken[Bool] {
  self.queue(Command::expire(key, seconds))
}

///|
pub fn Multi::pexpire(
  self : Multi,
  key : String,
  milliseconds : Int,
) -> MultiToken[Bool] {
  self.queue(Command::pexpire(key, milliseconds))
}

///|
pub fn Multi::ttl(self : Multi, key : String) -> MultiToken[Int] {
  self.queue(Command::ttl(key))
}

///|
pub fn Multi::pttl(self : Multi, key : String) -> MultiToken[Int] {
  self.queue(Command::pttl(key))
}

///|
pub fn Multi::persist(self : Multi, key : String) -> MultiToken[Bool] {
  self.queue(Command::persist(key))
}

///|
pub fn Multi::rename(
  self : Multi,
  key : String,
  new_key : String,
) -> MultiToken[Unit] {
  self.queue(Command::rename(key, new_key))
}

///|
pub fn Multi::renamenx(
  self : Multi,
  key : String,
  new_key : String,
) -> MultiToken[Bool] {
  self.queue(Command::renamenx(key, new_key))
}

///|
pub fn Multi::type_(self : Multi, key : String) -> MultiToken[String] {
  self.queue(Command::type_(key))
}

///|
pub fn Multi::keys(self : Multi, pattern : String) -> MultiToken[Array[String]] {
  self.queue(Command::keys(pattern))
}

///|
pub fn Multi::randomkey(self : Multi) -> MultiToken[String?] {
  self.queue(Command::randomkey())
}

///|
pub fn Multi::hget(
  self : Multi,
  key : String,
  field : String,
) -> MultiToken[String?] {
  self.queue(Command::hget(key, field))
}

///|
pub fn Multi::hset(
  self : Multi,
  key : String,
  field : String,
  value : String,
) -> MultiToken[Int] {
  self.queue(Command::hset(key, field, value))
}

///|
pub fn Multi::hmset(
  self : Multi,
  key : String,
  fields : ArrayView[(String, String)],
) -> MultiToken[Unit] {
  self.queue(Command::hmset(key, fields))
}

///|
pub fn Multi::hdel(
  self : Multi,
  key : String,
  fields : ArrayView[String],
) -> MultiToken[Int] {
  self.queue(Command::hdel(key, fields))
}

///|
pub fn Multi::hexists(
  self : Multi,
  key : String,
  field : String,
) -> MultiToken[Bool] {
  self.queue(Command::hexists(key, field))
}

///|
pub fn Multi::hgetall(
  self : Multi,
  key : String,
) -> MultiToken[Map[String, String]] {
  self.queue(Command::hgetall(key))
}

///|
pub fn Multi::hkeys(self : Multi, key : String) -> MultiToken[Array[String]] {
  self.queue(Command::hkeys(key))
}

///|
pub fn Multi::hvals(self : Multi, key : String) -> MultiToken[Array[String]] {
  self.queue(Command::hvals(key))
}

///|
pub fn Multi::hlen(self : Multi, key : String) -> MultiToken[Int] {
  self.queue(Command::hlen(key))
}

///|
pub fn Multi::hmget(
  self : Multi,
  key : String,
  fields : ArrayView[String],
) -> MultiToken[Array[String?]] {
  self.queue(Command::hmget(key, fields))
}

///|
pub fn Multi::hincrby(
  self : Multi,
  key : String,
  field : String,
  increment : Int,
) -> MultiToken[Int] {
  self.queue(Command::hincrby(key, field, increment))
}

///|
pub fn Multi::hincrbyfloat(
  self : Multi,
  key : String,
  field : String,
  increment : Double,
) -> MultiToken[Double] {
  self.queue(Command::hincrbyfloat(key, field, increment))
}

///|
pub fn Multi::hstrlen(
  self : Multi,
  key : String,
  field : String,
) -> MultiToken[Int] {
  self.queue(Command::hstrlen(key, field))
}

///|
pub fn Multi::lpush(
  self : Multi,
  key : String,
  values : ArrayView[String],
) -> MultiToken[Int] {
  self.queue(Command::lpush(key, values))
}

///|
pub fn Multi::rpush(
  self : Multi,
  key : String,
  values : ArrayView[String],
) -> MultiToken[Int] {
  self.queue(Command::rpush(key, values))
}

///|
pub fn Multi::lpop(self : Multi, key : String) -> MultiToken[String?] {
  self.queue(Command::lpop(key))
}

///|
pub fn Multi::rpop(self : Multi, key : String) -> MultiToken[String?] {
  self.queue(Command::rpop(key))
}

///|
pub fn Multi::lpop_count(
  self : Multi,
  key : String,
  count : Int,
) -> MultiToken[Array[String]] {
  self.queue(Command::lpop_count(key, count))
}

///|
pub fn Multi::rpop_count(
  self : Multi,
  key : String,
  count : Int,
) -> MultiToken[Array[String]] {
  self.queue(Command::rpop_count(key, count))
}

///|
pub fn Multi::llen(self : Multi, key : String) -> MultiToken[Int] {
  self.queue(Command::llen(key))
}

///|
pub fn Multi::lrange(
  self : Multi,
  key : String,
  start : Int,
  stop : Int,
) -> MultiToken[Array[String]] {
  self.queue(Command::lrange(key, start, stop))
}

///|
pub fn Multi::lindex(
  self : Multi,
  key : String,
  index : Int,
) -> MultiToken[String?] {
  self.queue(Command::lindex(key, index))
}

///|
pub fn Multi::lset(
  self : Multi,
  key : String,
  index : Int,
  value : String,
) -> MultiToken[Unit] {
  self.queue(Command::lset(key, index, value))
}

///|
pub fn Multi::ltrim(
  self : Multi,
  key : String,
  start : Int,
  stop : Int,
) -> MultiToken[Unit] {
  self.queue(Command::ltrim(key, start, stop))
}

///|
pub fn Multi::lrem(
  self : Multi,
  key : String,
  count : Int,
  value : String,
) -> MultiToken[Int] {
  self.queue(Command::lrem(key, count, value))
}

///|
pub fn Multi::linsert_before(
  self : Multi,
  key : String,
  pivot : String,
  value : String,
) -> MultiToken[Int] {
  self.queue(Command::linsert_before(key, pivot, value))
}

///|
pub fn Multi::linsert_after(
  self : Multi,
  key : String,
  pivot : String,
  value : String,
) -> MultiToken[Int] {
  self.queue(Command::linsert_after(key, pivot, value))
}

///|
pub fn Multi::ping(self : Multi, message? : String) -> MultiToken[String] {
  self.queue(Command::ping(message?))
}

///|
pub fn Multi::echo(self : Multi, message : String) -> MultiToken[String] {
  self.queue(Command::echo(message))
}

///|
pub fn Multi::scan(
  self : Multi,
  cursor : String,
  pattern? : String,
  count? : Int,
) -> MultiToken[(String, Array[String])] {
  self.queue(Command::scan(cursor, pattern?, count?))
}

///|
pub fn Multi::hscan(
  self : Multi,
  key : String,
  cursor : String,
  pattern? : String,
  count? : Int,
) -> MultiToken[(String, Map[String, String])] {
  self.queue(Command::hscan(key, cursor, pattern?, count?))
}

///|
pub fn Multi::sadd(
  self : Multi,
  key : String,
  members : ArrayView[String],
) -> MultiToken[Int] {
  self.queue(Command::sadd(key, members))
}

///|
pub fn Multi::srem(
  self : Multi,
  key : String,
  members : ArrayView[String],
) -> MultiToken[Int] {
  self.queue(Command::srem(key, members))
}

///|
pub fn Multi::smembers(
  self : Multi,
  key : String,
) -> MultiToken[@hashset.HashSet[String]] {
  self.queue(Command::smembers(key))
}

///|
pub fn Multi::sismember(
  self : Multi,
  key : String,
  value : String,
) -> MultiToken[Bool] {
  self.queue(Command::sismember(key, value))
}

///|
pub fn Multi::smismember(
  self : Multi,
  key : String,
  members : ArrayView[String],
) -> MultiToken[Array[Bool]] {
  self.queue(Command::smismember(key, members))
}

///|
pub fn Multi::scard(self : Multi, key : String) -> MultiToken[Int] {
  self.queue(Command::scard(key))
}

///|
pub fn Multi::spop(self : Multi, key : String) -> MultiToken[String?] {
  self.queue(Command::spop(key))
}

///|
pub fn Multi::spop_count(
  self : Multi,
  key : String,
  count : Int,
) -> MultiToken[@hashset.HashSet[String]] {
  self.queue(Command::spop_count(key, count))
}

///|
pub fn Multi::srandmember(self : Multi, key : String) -> MultiToken[String?] {
  self.queue(Command::srandmember(key))
}

///|
pub fn Multi::srandmember_count(
  self : Multi,
  key : String,
  count : Int,
) -> MultiToken[Array[String]] {
  self.queue(Command::srandmember_count(key, count))
}

///|
pub fn Multi::smove(
  self : Multi,
  source : String,
  destination : String,
  value : String,
) -> MultiToken[Bool] {
  self.queue(Command::smove(source, destination, value))
}

///|
pub fn Multi::sdiff(
  self : Multi,
  keys : ArrayView[String],
) -> MultiToken[@hashset.HashSet[String]] {
  self.queue(Command::sdiff(keys))
}

///|
pub fn Multi::sinter(
  self : Multi,
  keys : ArrayView[String],
) -> MultiToken[@hashset.HashSet[String]] {
  self.queue(Command::sinter(keys))
}

///|
pub fn Multi::sunion(
  self : Multi,
  keys : ArrayView[String],
) -> MultiToken[@hashset.HashSet[String]] {
  self.queue(Command::sunion(keys))
}

///|
pub fn Multi::sdiffstore(
  self : Multi,
  destination : String,
  keys : ArrayView[String],
) -> MultiToken[Int] {
  self.queue(Command::sdiffstore(destination, keys))
}

///|
pub fn Multi::sinterstore(
  self : Multi,
  destination : String,
  keys : ArrayView[String],
) -> MultiToken[Int] {
  self.queue(Command::sinterstore(destination, keys))
}

///|
pub fn Multi::sunionstore(
  self : Multi,
  destination : String,
  keys : ArrayView[String],
) -> MultiToken[Int] {
  self.queue(Command::sunionstore(destination, keys))
}

///|
pub fn Multi::sscan(
  self : Multi,
  key : String,
  cursor : String,
  pattern? : String,
  count? : Int,
) -> MultiToken[(String, @hashset.HashSet[String])] {
  self.queue(Command::sscan(key, cursor, pattern?, count?))
}

///|
pub fn Multi::zadd(
  self : Multi,
  key : String,
  items : ArrayView[(String, Double)],
) -> MultiToken[Int] {
  self.queue(Command::zadd(key, items))
}

///|
pub fn Multi::zrem(
  self : Multi,
  key : String,
  members : ArrayView[String],
) -> MultiToken[Int] {
  self.queue(Command::zrem(key, members))
}

///|
pub fn Multi::zscore(
  self : Multi,
  key : String,
  value : String,
) -> MultiToken[Double?] {
  self.queue(Command::zscore(key, value))
}

///|
pub fn Multi::zcard(self : Multi, key : String) -> MultiToken[Int] {
  self.queue(Command::zcard(key))
}

///|
pub fn Multi::zcount(
  self : Multi,
  key : String,
  min : Double,
  max : Double,
) -> MultiToken[Int] {
  self.queue(Command::zcount(key, min, max))
}

///|
pub fn Multi::zrank(
  self : Multi,
  key : String,
  value : String,
) -> MultiToken[Int?] {
  self.queue(Command::zrank(key, value))
}

///|
pub fn Multi::zrevrank(
  self : Multi,
  key : String,
  value : String,
) -> MultiToken[Int?] {
  self.queue(Command::zrevrank(key, value))
}

///|
pub fn Multi::zrange(
  self : Multi,
  key : String,
  start : Int,
  stop : Int,
) -> MultiToken[Array[String]] {
  self.queue(Command::zrange(key, start, stop))
}

///|
pub fn Multi::zrange_with_scores(
  self : Multi,
  key : String,
  start : Int,
  stop : Int,
) -> MultiToken[Array[(String, Double)]] {
  self.queue(Command::zrange_with_scores(key, start, stop))
}

///|
pub fn Multi::zrevrange(
  self : Multi,
  key : String,
  start : Int,
  stop : Int,
) -> MultiToken[Array[String]] {
  self.queue(Command::zrevrange(key, start, stop))
}

///|
pub fn Multi::zrevrange_with_scores(
  self : Multi,
  key : String,
  start : Int,
  stop : Int,
) -> MultiToken[Array[(String, Double)]] {
  self.queue(Command::zrevrange_with_scores(key, start, stop))
}

///|
pub fn Multi::zrangebyscore(
  self : Multi,
  key : String,
  min : Double,
  max : Double,
  offset? : Int,
  count? : Int,
) -> MultiToken[Array[String]] raise {
  self.queue(Command::zrangebyscore(key, min, max, offset?, count?))
}

///|
pub fn Multi::zrangebyscore_with_scores(
  self : Multi,
  key : String,
  min : Double,
  max : Double,
  offset? : Int,
  count? : Int,
) -> MultiToken[Array[(String, Double)]] raise {
  self.queue(Command::zrangebyscore_with_scores(key, min, max, offset?, count?))
}

///|
pub fn Multi::zincrby(
  self : Multi,
  key : String,
  increment : Double,
  value : String,
) -> MultiToken[Double] {
  self.queue(Command::zincrby(key, increment, value))
}

///|
pub fn Multi::zremrangebyrank(
  self : Multi,
  key : String,
  start : Int,
  stop : Int,
) -> MultiToken[Int] {
  self.queue(Command::zremrangebyrank(key, start, stop))
}

///|
pub fn Multi::zremrangebyscore(
  self : Multi,
  key : String,
  min : Double,
  max : Double,
) -> MultiToken[Int] {
  self.queue(Command::zremrangebyscore(key, min, max))
}

///|
pub fn Multi::zpopmin(
  self : Multi,
  key : String,
  count? : Int,
) -> MultiToken[Array[(String, Double)]] {
  self.queue(Command::zpopmin(key, count?))
}

///|
pub fn Multi::zpopmax(
  self : Multi,
  key : String,
  count? : Int,
) -> MultiToken[Array[(String, Double)]] {
  self.queue(Command::zpopmax(key, count?))
}

///|
pub fn Multi::zscan(
  self : Multi,
  key : String,
  cursor : String,
  pattern? : String,
  count? : Int,
) -> MultiToken[(String, Array[(String, Double)])] {
  self.queue(Command::zscan(key, cursor, pattern?, count?))
}

///|
/// Reads the result for a queued command.
///
/// Raises if Redis returned an error for that command or if the response cannot
/// be decoded as the token's expected type.
///
/// Example:
/// ```moonbit nocheck
/// let value = result.get(old_value)
/// ```
pub fn[T] MultiResult::get(
  self : MultiResult,
  token : MultiToken[T],
) -> T raise {
  guard token.index < self.values.length() else {
    raise UnexpectedResponse("transaction result index out of bounds")
  }
  (token.decode)(self.values[token.index])
}

///|
/// Runs a callback in a Redis transaction session.
///
/// Use `Transaction::watch` before building a `Multi` when optimistic locking
/// is needed. The returned value is whatever the callback returns.
///
/// Example:
/// ```moonbit nocheck
/// client.transaction(tx => {
///   tx.watch(["account:1"])
///   let multi = @redis.Multi()
///   let wrote = multi.set("account:1", "updated")
///
///   guard tx.exec(multi) is Some(result) else {
///     fail("transaction aborted")
///   }
///   result.get(wrote)
/// })
/// ```
pub async fn[T] Client::transaction(
  self : Client,
  f : async (Transaction) -> T,
) -> T {
  let conn = self.dial()
  defer conn.close()
  let r = @resp.Reader(
    conn,
    buffer_size=self.config.read_buffer_size,
    max_depth=self.config.resp_max_depth,
  )
  let w = @resp.Writer(conn, buffer_size=self.config.write_buffer_size)
  let version = self.handshake(r, w)
  let tx = { r, w, version, lock: @async.Semaphore::new(1), done: false }
  f(tx)
}

///|
fn Transaction::ensure_open(self : Transaction) -> Unit raise {
  guard !self.done else {
    raise UnexpectedResponse("transaction already completed")
  }
}

///|
async fn Transaction::execute(
  self : Transaction,
  cmd : Array[Bytes],
) -> RawValue {
  self.ensure_open()
  self.lock.acquire()
  defer self.lock.release()
  self.ensure_open()
  execute_direct_command(
    self.w,
    self.r,
    self.version,
    ReadOnlyArray::from_array(cmd),
  )
}

///|
async fn[T] Transaction::run(self : Transaction, spec : Command[T]) -> T {
  (spec.res_decode_fn)(self.execute(spec.args))
}

///|
/// Watches keys for optimistic locking.
///
/// If any watched key changes before `exec`, `Transaction::exec` returns
/// `None`.
pub async fn Transaction::watch(
  self : Transaction,
  keys : ArrayView[String],
) -> Unit {
  self.execute(ArgsBuilder(b"WATCH")..push_strings(keys).build()).expect_ok()
}

///|
/// Runs one custom command immediately on the transaction connection.
///
/// This is useful for commands not covered by typed transaction helpers.
pub async fn Transaction::raw_command(
  self : Transaction,
  args : ArrayView[BytesView],
) -> RawValue {
  self.execute(copy_bytes_views(args))
}

///|
pub async fn Transaction::get(self : Transaction, key : String) -> String? {
  self.run(Command::get(key))
}

///|
pub async fn Transaction::set(
  self : Transaction,
  key : String,
  value : String,
  ex? : Int,
  px? : Int,
  nx? : Bool,
  xx? : Bool,
) -> Bool {
  self.run(Command::set(key, value, ex?, px?, nx?, xx?))
}

///|
pub async fn Transaction::set_get(
  self : Transaction,
  key : String,
  value : String,
  ex? : Int,
  px? : Int,
  nx? : Bool,
  xx? : Bool,
) -> String? {
  self.run(Command::set_get(key, value, ex?, px?, nx?, xx?))
}

///|
pub async fn Transaction::setnx(
  self : Transaction,
  key : String,
  value : String,
) -> Bool {
  self.run(Command::setnx(key, value))
}

///|
pub async fn Transaction::setex(
  self : Transaction,
  key : String,
  seconds : Int,
  value : String,
) -> Unit {
  self.run(Command::setex(key, seconds, value))
}

///|
pub async fn Transaction::psetex(
  self : Transaction,
  key : String,
  milliseconds : Int,
  value : String,
) -> Unit {
  self.run(Command::psetex(key, milliseconds, value))
}

///|
pub async fn Transaction::mget(
  self : Transaction,
  keys : ArrayView[String],
) -> Array[String?] {
  self.run(Command::mget(keys))
}

///|
pub async fn Transaction::mset(
  self : Transaction,
  items : ArrayView[(String, String)],
) -> Unit {
  self.run(Command::mset(items))
}

///|
pub async fn Transaction::msetnx(
  self : Transaction,
  items : ArrayView[(String, String)],
) -> Bool {
  self.run(Command::msetnx(items))
}

///|
pub async fn Transaction::incr(self : Transaction, key : String) -> Int {
  self.run(Command::incr(key))
}

///|
pub async fn Transaction::incrby(
  self : Transaction,
  key : String,
  increment : Int,
) -> Int {
  self.run(Command::incrby(key, increment))
}

///|
pub async fn Transaction::incrbyfloat(
  self : Transaction,
  key : String,
  increment : Double,
) -> Double {
  self.run(Command::incrbyfloat(key, increment))
}

///|
pub async fn Transaction::decr(self : Transaction, key : String) -> Int {
  self.run(Command::decr(key))
}

///|
pub async fn Transaction::decrby(
  self : Transaction,
  key : String,
  decrement : Int,
) -> Int {
  self.run(Command::decrby(key, decrement))
}

///|
pub async fn Transaction::append(
  self : Transaction,
  key : String,
  value : String,
) -> Int {
  self.run(Command::append(key, value))
}

///|
pub async fn Transaction::strlen(self : Transaction, key : String) -> Int {
  self.run(Command::strlen(key))
}

///|
pub async fn Transaction::getrange(
  self : Transaction,
  key : String,
  start : Int,
  end : Int,
) -> String {
  self.run(Command::getrange(key, start, end))
}

///|
pub async fn Transaction::setrange(
  self : Transaction,
  key : String,
  offset : Int,
  value : String,
) -> Int {
  self.run(Command::setrange(key, offset, value))
}

///|
pub async fn Transaction::del(
  self : Transaction,
  keys : ArrayView[String],
) -> Int {
  self.run(Command::del(keys))
}

///|
pub async fn Transaction::exists(
  self : Transaction,
  keys : ArrayView[String],
) -> Int {
  self.run(Command::exists(keys))
}

///|
pub async fn Transaction::expire(
  self : Transaction,
  key : String,
  seconds : Int,
) -> Bool {
  self.run(Command::expire(key, seconds))
}

///|
pub async fn Transaction::pexpire(
  self : Transaction,
  key : String,
  milliseconds : Int,
) -> Bool {
  self.run(Command::pexpire(key, milliseconds))
}

///|
pub async fn Transaction::ttl(self : Transaction, key : String) -> Int {
  self.run(Command::ttl(key))
}

///|
pub async fn Transaction::pttl(self : Transaction, key : String) -> Int {
  self.run(Command::pttl(key))
}

///|
pub async fn Transaction::persist(self : Transaction, key : String) -> Bool {
  self.run(Command::persist(key))
}

///|
pub async fn Transaction::rename(
  self : Transaction,
  key : String,
  new_key : String,
) -> Unit {
  self.run(Command::rename(key, new_key))
}

///|
pub async fn Transaction::renamenx(
  self : Transaction,
  key : String,
  new_key : String,
) -> Bool {
  self.run(Command::renamenx(key, new_key))
}

///|
pub async fn Transaction::type_(self : Transaction, key : String) -> String {
  self.run(Command::type_(key))
}

///|
pub async fn Transaction::keys(
  self : Transaction,
  pattern : String,
) -> Array[String] {
  self.run(Command::keys(pattern))
}

///|
pub async fn Transaction::randomkey(self : Transaction) -> String? {
  self.run(Command::randomkey())
}

///|
pub async fn Transaction::ping(self : Transaction, message? : String) -> String {
  self.run(Command::ping(message?))
}

///|
pub async fn Transaction::echo(self : Transaction, message : String) -> String {
  self.run(Command::echo(message))
}

///|
pub async fn Transaction::scan(
  self : Transaction,
  cursor : String,
  pattern? : String,
  count? : Int,
) -> (String, Array[String]) {
  self.run(Command::scan(cursor, pattern?, count?))
}

///|
pub async fn Transaction::hget(
  self : Transaction,
  key : String,
  field : String,
) -> String? {
  self.run(Command::hget(key, field))
}

///|
pub async fn Transaction::hset(
  self : Transaction,
  key : String,
  field : String,
  value : String,
) -> Int {
  self.run(Command::hset(key, field, value))
}

///|
pub async fn Transaction::hmset(
  self : Transaction,
  key : String,
  fields : ArrayView[(String, String)],
) -> Unit {
  self.run(Command::hmset(key, fields))
}

///|
pub async fn Transaction::hdel(
  self : Transaction,
  key : String,
  fields : ArrayView[String],
) -> Int {
  self.run(Command::hdel(key, fields))
}

///|
pub async fn Transaction::hexists(
  self : Transaction,
  key : String,
  field : String,
) -> Bool {
  self.run(Command::hexists(key, field))
}

///|
pub async fn Transaction::hgetall(
  self : Transaction,
  key : String,
) -> Map[String, String] {
  self.run(Command::hgetall(key))
}

///|
pub async fn Transaction::hkeys(
  self : Transaction,
  key : String,
) -> Array[String] {
  self.run(Command::hkeys(key))
}

///|
pub async fn Transaction::hvals(
  self : Transaction,
  key : String,
) -> Array[String] {
  self.run(Command::hvals(key))
}

///|
pub async fn Transaction::hlen(self : Transaction, key : String) -> Int {
  self.run(Command::hlen(key))
}

///|
pub async fn Transaction::hmget(
  self : Transaction,
  key : String,
  fields : ArrayView[String],
) -> Array[String?] {
  self.run(Command::hmget(key, fields))
}

///|
pub async fn Transaction::hincrby(
  self : Transaction,
  key : String,
  field : String,
  increment : Int,
) -> Int {
  self.run(Command::hincrby(key, field, increment))
}

///|
pub async fn Transaction::hincrbyfloat(
  self : Transaction,
  key : String,
  field : String,
  increment : Double,
) -> Double {
  self.run(Command::hincrbyfloat(key, field, increment))
}

///|
pub async fn Transaction::hstrlen(
  self : Transaction,
  key : String,
  field : String,
) -> Int {
  self.run(Command::hstrlen(key, field))
}

///|
pub async fn Transaction::hscan(
  self : Transaction,
  key : String,
  cursor : String,
  pattern? : String,
  count? : Int,
) -> (String, Map[String, String]) {
  self.run(Command::hscan(key, cursor, pattern?, count?))
}

///|
pub async fn Transaction::lpush(
  self : Transaction,
  key : String,
  values : ArrayView[String],
) -> Int {
  self.run(Command::lpush(key, values))
}

///|
pub async fn Transaction::rpush(
  self : Transaction,
  key : String,
  values : ArrayView[String],
) -> Int {
  self.run(Command::rpush(key, values))
}

///|
pub async fn Transaction::lpop(self : Transaction, key : String) -> String? {
  self.run(Command::lpop(key))
}

///|
pub async fn Transaction::rpop(self : Transaction, key : String) -> String? {
  self.run(Command::rpop(key))
}

///|
pub async fn Transaction::lpop_count(
  self : Transaction,
  key : String,
  count : Int,
) -> Array[String] {
  self.run(Command::lpop_count(key, count))
}

///|
pub async fn Transaction::rpop_count(
  self : Transaction,
  key : String,
  count : Int,
) -> Array[String] {
  self.run(Command::rpop_count(key, count))
}

///|
pub async fn Transaction::llen(self : Transaction, key : String) -> Int {
  self.run(Command::llen(key))
}

///|
pub async fn Transaction::lrange(
  self : Transaction,
  key : String,
  start : Int,
  stop : Int,
) -> Array[String] {
  self.run(Command::lrange(key, start, stop))
}

///|
pub async fn Transaction::lindex(
  self : Transaction,
  key : String,
  index : Int,
) -> String? {
  self.run(Command::lindex(key, index))
}

///|
pub async fn Transaction::lset(
  self : Transaction,
  key : String,
  index : Int,
  value : String,
) -> Unit {
  self.run(Command::lset(key, index, value))
}

///|
pub async fn Transaction::ltrim(
  self : Transaction,
  key : String,
  start : Int,
  stop : Int,
) -> Unit {
  self.run(Command::ltrim(key, start, stop))
}

///|
pub async fn Transaction::lrem(
  self : Transaction,
  key : String,
  count : Int,
  value : String,
) -> Int {
  self.run(Command::lrem(key, count, value))
}

///|
pub async fn Transaction::linsert_before(
  self : Transaction,
  key : String,
  pivot : String,
  value : String,
) -> Int {
  self.run(Command::linsert_before(key, pivot, value))
}

///|
pub async fn Transaction::linsert_after(
  self : Transaction,
  key : String,
  pivot : String,
  value : String,
) -> Int {
  self.run(Command::linsert_after(key, pivot, value))
}

///|
pub async fn Transaction::sadd(
  self : Transaction,
  key : String,
  members : ArrayView[String],
) -> Int {
  self.run(Command::sadd(key, members))
}

///|
pub async fn Transaction::srem(
  self : Transaction,
  key : String,
  members : ArrayView[String],
) -> Int {
  self.run(Command::srem(key, members))
}

///|
pub async fn Transaction::smembers(
  self : Transaction,
  key : String,
) -> @hashset.HashSet[String] {
  self.run(Command::smembers(key))
}

///|
pub async fn Transaction::sismember(
  self : Transaction,
  key : String,
  value : String,
) -> Bool {
  self.run(Command::sismember(key, value))
}

///|
pub async fn Transaction::smismember(
  self : Transaction,
  key : String,
  members : ArrayView[String],
) -> Array[Bool] {
  self.run(Command::smismember(key, members))
}

///|
pub async fn Transaction::scard(self : Transaction, key : String) -> Int {
  self.run(Command::scard(key))
}

///|
pub async fn Transaction::spop(self : Transaction, key : String) -> String? {
  self.run(Command::spop(key))
}

///|
pub async fn Transaction::spop_count(
  self : Transaction,
  key : String,
  count : Int,
) -> @hashset.HashSet[String] {
  self.run(Command::spop_count(key, count))
}

///|
pub async fn Transaction::srandmember(
  self : Transaction,
  key : String,
) -> String? {
  self.run(Command::srandmember(key))
}

///|
pub async fn Transaction::srandmember_count(
  self : Transaction,
  key : String,
  count : Int,
) -> Array[String] {
  self.run(Command::srandmember_count(key, count))
}

///|
pub async fn Transaction::smove(
  self : Transaction,
  source : String,
  destination : String,
  value : String,
) -> Bool {
  self.run(Command::smove(source, destination, value))
}

///|
pub async fn Transaction::sdiff(
  self : Transaction,
  keys : ArrayView[String],
) -> @hashset.HashSet[String] {
  self.run(Command::sdiff(keys))
}

///|
pub async fn Transaction::sinter(
  self : Transaction,
  keys : ArrayView[String],
) -> @hashset.HashSet[String] {
  self.run(Command::sinter(keys))
}

///|
pub async fn Transaction::sunion(
  self : Transaction,
  keys : ArrayView[String],
) -> @hashset.HashSet[String] {
  self.run(Command::sunion(keys))
}

///|
pub async fn Transaction::sdiffstore(
  self : Transaction,
  destination : String,
  keys : ArrayView[String],
) -> Int {
  self.run(Command::sdiffstore(destination, keys))
}

///|
pub async fn Transaction::sinterstore(
  self : Transaction,
  destination : String,
  keys : ArrayView[String],
) -> Int {
  self.run(Command::sinterstore(destination, keys))
}

///|
pub async fn Transaction::sunionstore(
  self : Transaction,
  destination : String,
  keys : ArrayView[String],
) -> Int {
  self.run(Command::sunionstore(destination, keys))
}

///|
pub async fn Transaction::sscan(
  self : Transaction,
  key : String,
  cursor : String,
  pattern? : String,
  count? : Int,
) -> (String, @hashset.HashSet[String]) {
  self.run(Command::sscan(key, cursor, pattern?, count?))
}

///|
pub async fn Transaction::zadd(
  self : Transaction,
  key : String,
  items : ArrayView[(String, Double)],
) -> Int {
  self.run(Command::zadd(key, items))
}

///|
pub async fn Transaction::zrem(
  self : Transaction,
  key : String,
  members : ArrayView[String],
) -> Int {
  self.run(Command::zrem(key, members))
}

///|
pub async fn Transaction::zscore(
  self : Transaction,
  key : String,
  value : String,
) -> Double? {
  self.run(Command::zscore(key, value))
}

///|
pub async fn Transaction::zcard(self : Transaction, key : String) -> Int {
  self.run(Command::zcard(key))
}

///|
pub async fn Transaction::zcount(
  self : Transaction,
  key : String,
  min : Double,
  max : Double,
) -> Int {
  self.run(Command::zcount(key, min, max))
}

///|
pub async fn Transaction::zrank(
  self : Transaction,
  key : String,
  value : String,
) -> Int? {
  self.run(Command::zrank(key, value))
}

///|
pub async fn Transaction::zrevrank(
  self : Transaction,
  key : String,
  value : String,
) -> Int? {
  self.run(Command::zrevrank(key, value))
}

///|
pub async fn Transaction::zrange(
  self : Transaction,
  key : String,
  start : Int,
  stop : Int,
) -> Array[String] {
  self.run(Command::zrange(key, start, stop))
}

///|
pub async fn Transaction::zrange_with_scores(
  self : Transaction,
  key : String,
  start : Int,
  stop : Int,
) -> Array[(String, Double)] {
  self.run(Command::zrange_with_scores(key, start, stop))
}

///|
pub async fn Transaction::zrevrange(
  self : Transaction,
  key : String,
  start : Int,
  stop : Int,
) -> Array[String] {
  self.run(Command::zrevrange(key, start, stop))
}

///|
pub async fn Transaction::zrevrange_with_scores(
  self : Transaction,
  key : String,
  start : Int,
  stop : Int,
) -> Array[(String, Double)] {
  self.run(Command::zrevrange_with_scores(key, start, stop))
}

///|
pub async fn Transaction::zrangebyscore(
  self : Transaction,
  key : String,
  min : Double,
  max : Double,
  offset? : Int,
  count? : Int,
) -> Array[String] {
  self.run(Command::zrangebyscore(key, min, max, offset?, count?))
}

///|
pub async fn Transaction::zrangebyscore_with_scores(
  self : Transaction,
  key : String,
  min : Double,
  max : Double,
  offset? : Int,
  count? : Int,
) -> Array[(String, Double)] {
  self.run(Command::zrangebyscore_with_scores(key, min, max, offset?, count?))
}

///|
pub async fn Transaction::zincrby(
  self : Transaction,
  key : String,
  increment : Double,
  value : String,
) -> Double {
  self.run(Command::zincrby(key, increment, value))
}

///|
pub async fn Transaction::zremrangebyrank(
  self : Transaction,
  key : String,
  start : Int,
  stop : Int,
) -> Int {
  self.run(Command::zremrangebyrank(key, start, stop))
}

///|
pub async fn Transaction::zremrangebyscore(
  self : Transaction,
  key : String,
  min : Double,
  max : Double,
) -> Int {
  self.run(Command::zremrangebyscore(key, min, max))
}

///|
pub async fn Transaction::zpopmin(
  self : Transaction,
  key : String,
  count? : Int,
) -> Array[(String, Double)] {
  self.run(Command::zpopmin(key, count?))
}

///|
pub async fn Transaction::zpopmax(
  self : Transaction,
  key : String,
  count? : Int,
) -> Array[(String, Double)] {
  self.run(Command::zpopmax(key, count?))
}

///|
pub async fn Transaction::zscan(
  self : Transaction,
  key : String,
  cursor : String,
  pattern? : String,
  count? : Int,
) -> (String, Array[(String, Double)]) {
  self.run(Command::zscan(key, cursor, pattern?, count?))
}

///|
/// Executes commands added to `multi` with Redis `MULTI`/`EXEC`.
///
/// Returns `None` when watched keys changed before `EXEC`. Returns
/// `Some(result)` on success; read individual command results with
/// `MultiResult::get`.
pub async fn Transaction::exec(
  self : Transaction,
  multi : Multi,
) -> MultiResult? {
  self.ensure_open()
  self.lock.acquire()
  defer self.lock.release()
  self.ensure_open()
  self.done = true
  // The transaction session owns a short-lived Redis connection. WATCH state is
  // intentionally not unwatched here: EXEC clears it, and closing the temporary
  // connection clears any remaining WATCH state. If transaction connections are
  // pooled later, the pool must reset connections with UNWATCH/DISCARD before
  // reuse.
  execute_direct_command(self.w, self.r, self.version, [b"MULTI"]).expect_ok()
  for cmd in multi.commands {
    guard execute_direct_command(self.w, self.r, self.version, cmd).as_string() ==
      "QUEUED" else {
      raise UnexpectedResponse("expected QUEUED")
    }
  }
  let result = execute_direct_command(self.w, self.r, self.version, [b"EXEC"])
  if result.is_null() {
    None
  } else {
    let values = result.as_array(value => value)
    Some({ values: ReadOnlyArray::from_array(values) })
  }
}