var __knownSymbol = (name, symbol) => (symbol = Symbol[name]) ? symbol : /* @__PURE__ */ Symbol.for("Symbol." + name);
var __typeError = (msg) => {
  throw TypeError(msg);
};
var __using = (stack, value, async) => {
  if (value != null) {
    if (typeof value !== "object" && typeof value !== "function") __typeError("Object expected");
    var dispose, inner;
    if (async) dispose = value[__knownSymbol("asyncDispose")];
    if (dispose === void 0) {
      dispose = value[__knownSymbol("dispose")];
      if (async) inner = dispose;
    }
    if (typeof dispose !== "function") __typeError("Object not disposable");
    if (inner) dispose = function() {
      try {
        inner.call(this);
      } catch (e) {
        return Promise.reject(e);
      }
    };
    stack.push([async, dispose, value]);
  } else if (async) {
    stack.push([async]);
  }
  return value;
};
var __callDispose = (stack, error, hasError) => {
  var E = typeof SuppressedError === "function" ? SuppressedError : function(e, s, m, _) {
    return _ = Error(m), _.name = "SuppressedError", _.error = e, _.suppressed = s, _;
  };
  var fail = (e) => error = hasError ? new E(e, error, "An error was suppressed during disposal") : (hasError = true, e);
  var next = (it) => {
    while (it = stack.pop()) {
      try {
        var result = it[1] && it[1].call(it[2]);
        if (it[0]) return Promise.resolve(result).then(next, (e) => (fail(e), next()));
      } catch (e) {
        fail(e);
      }
    }
    if (hasError) throw error;
  };
  return next();
};
/**
 * @license
 * Copyright 2022-2026 Matter.js Authors
 * SPDX-License-Identifier: Apache-2.0
 */
import { Message, SessionType } from "#codec/MessageCodec.js";
import { Mark } from "#common/Mark.js";
import {
  AsyncObservableValue,
  createPromise,
  CRYPTO_AEAD_MIC_LENGTH_BYTES,
  DataReadQueue,
  Diagnostic,
  Duration,
  hex,
  Instant,
  InternalError,
  Logger,
  MatterFlowError,
  Millis,
  Time
} from "#general";
import { GroupSession } from "#session/GroupSession.js";
import { Session } from "#session/Session.js";
import {
  GroupId,
  SECURE_CHANNEL_PROTOCOL_ID,
  SecureMessageType,
  StatusCode,
  StatusResponseError
} from "#types";
import { RetransmissionLimitReachedError, SessionClosedError, UnexpectedMessageError } from "./errors.js";
import { MRP } from "./MRP.js";
const logger = Logger.get("MessageExchange");
const MATTER_MESSAGE_OVERHEAD = 26 + 12 + CRYPTO_AEAD_MIC_LENGTH_BYTES;
class MessageExchange {
  static fromInitialMessage(context, initialMessage) {
    const { session } = context;
    return new MessageExchange({
      context,
      isInitiator: false,
      peerSessionId: session.id,
      nodeId: initialMessage.packetHeader.destNodeId,
      peerNodeId: initialMessage.packetHeader.sourceNodeId,
      exchangeId: initialMessage.payloadHeader.exchangeId,
      protocolId: initialMessage.payloadHeader.protocolId
    });
  }
  static initiate(context, exchangeId, protocolId) {
    const { session } = context;
    return new MessageExchange({
      context,
      isInitiator: true,
      peerSessionId: session.peerSessionId,
      nodeId: session.nodeId,
      peerNodeId: session.peerNodeId,
      exchangeId,
      protocolId
    });
  }
  #context;
  #isInitiator;
  #messagesQueue = new DataReadQueue();
  #lifetime;
  #receivedMessageToAck;
  #receivedMessageAckTimer = Time.getTimer("ack receipt timeout", MRP.STANDALONE_ACK_TIMEOUT, () => {
    if (this.#receivedMessageToAck !== void 0) {
      const messageToAck = this.#receivedMessageToAck;
      this.#receivedMessageToAck = void 0;
      this.#sendStandaloneAckForMessage(messageToAck).catch(
        (error) => logger.error("An error happened when sending a standalone ack", error)
      );
    }
  });
  #sentMessageToAck;
  #sentMessageAckSuccess;
  #sentMessageAckFailure;
  #retransmissionTimer;
  #retransmissionCounter = 0;
  #totalRetransmissionCounter = 0;
  // counter for all messages within this exchange
  #messageSendCounter = 0;
  #messageReceivedCounter = 0;
  #closeTimer;
  #isDestroyed = false;
  #timedInteractionTimer;
  #used;
  #peerSessionId;
  #nodeId;
  #peerNodeId;
  #exchangeId;
  #protocolId;
  #closed = AsyncObservableValue();
  #closing = AsyncObservableValue();
  #channel;
  constructor(config) {
    const { context, isInitiator, peerSessionId, nodeId, peerNodeId, exchangeId, protocolId } = config;
    this.#context = context;
    this.#isInitiator = isInitiator;
    this.#peerSessionId = peerSessionId;
    this.#nodeId = nodeId;
    this.#peerNodeId = peerNodeId;
    this.#exchangeId = exchangeId;
    this.#protocolId = protocolId;
    const { activeThreshold, activeInterval, idleInterval } = this.session.parameters;
    this.#used = !isInitiator;
    const { session } = context;
    logger.debug(
      "New exchange",
      isInitiator ? Mark.OUTBOUND : Mark.INBOUND,
      this.via,
      Diagnostic.dict({
        protocol: this.#protocolId,
        peerSess: Session.idStrOf(this.#peerSessionId),
        SAT: activeThreshold !== void 0 ? Duration.format(activeThreshold) : void 0,
        SAI: activeInterval !== void 0 ? Duration.format(activeInterval) : void 0,
        SII: idleInterval !== void 0 ? Duration.format(idleInterval) : void 0,
        maxTrans: MRP.MAX_TRANSMISSIONS,
        exchangeFlags: Diagnostic.asFlags({
          MRP: this.session.usesMrp,
          I: this.isInitiator
        })
      })
    );
    session.addExchange(this);
    this.#lifetime = this.#context.session.join("exchange", Diagnostic.via(hex.word(this.id)));
  }
  get context() {
    return this.#context;
  }
  get isInitiator() {
    return this.#isInitiator;
  }
  /** Emits when the exchange is actually closed. This happens after all Retries and Communication are done. */
  get closed() {
    return this.#closed;
  }
  get considerClosed() {
    return this.#closed.value || this.#isInitiator && this.#closing.value;
  }
  /**
   * Emit when the exchange is closing, but not yet closed. We only wait for acks and retries to happen, but the
   * actual interaction logic is already done.
   */
  get closing() {
    return this.#closing;
  }
  get id() {
    return this.#exchangeId;
  }
  get idStr() {
    return hex.word(this.#exchangeId);
  }
  get session() {
    return this.context.session;
  }
  get channel() {
    if (this.#channel === void 0) {
      this.#channel = this.session.channel;
    }
    return this.#channel;
  }
  /**
   * Max Payload size of the exchange which bases on the maximum payload size of the channel reduced by Matter
   * protocol overhead.
   */
  get maxPayloadSize() {
    return this.channel.maxPayloadSize - MATTER_MESSAGE_OVERHEAD;
  }
  join(...name) {
    return this.#lifetime.join(...name);
  }
  async onMessageReceived(message, duplicate = false) {
    logger.debug("Message", Mark.INBOUND, Message.diagnosticsOf(this, message, { duplicate }));
    this.#messageReceivedCounter++;
    if (message.payloadHeader.requiresAck && !this.session.usesMrp) {
      logger.debug("Ignoring ack-required flag because MRP is not used for this exchange");
      message.payloadHeader.requiresAck = false;
    }
    const {
      packetHeader: { messageId },
      payloadHeader: { requiresAck, ackedMessageId, protocolId, messageType }
    } = message;
    const isStandaloneAck = SecureMessageType.isStandaloneAck(protocolId, messageType);
    if (protocolId !== this.#protocolId && !isStandaloneAck) {
      throw new MatterFlowError(
        `Drop received a message for an unexpected protocol. Expected: ${this.#protocolId}, received: ${protocolId}`
      );
    }
    this.session.notifyActivity(true);
    if (duplicate) {
      if (requiresAck) {
        await this.#sendStandaloneAckForMessage(message);
      }
      return;
    }
    if (messageId === this.#sentMessageToAck?.payloadHeader.ackedMessageId) {
      var _stack = [];
      try {
        const _acking = __using(_stack, this.join("resending ack"));
        this.#messageSendCounter++;
        await this.channel.send(this.#sentMessageToAck, this);
        return;
      } catch (_) {
        var _error = _, _hasError = true;
      } finally {
        __callDispose(_stack, _error, _hasError);
      }
    }
    const sentMessageIdToAck = this.#sentMessageToAck?.packetHeader.messageId;
    if (sentMessageIdToAck !== void 0) {
      if (ackedMessageId === void 0) {
        throw new MatterFlowError("Previous message ack is missing");
      } else if (ackedMessageId !== sentMessageIdToAck) {
        if (isStandaloneAck) {
        } else {
          throw new MatterFlowError(
            `Incorrect ack received. Expected ${hex.fixed(sentMessageIdToAck, 8)}, received: ${hex.fixed(ackedMessageId, 8)}`
          );
        }
      } else {
        this.#retransmissionTimer?.stop();
        this.#retransmissionCounter = 0;
        this.#sentMessageAckSuccess?.(message);
        this.#sentMessageAckSuccess = void 0;
        this.#sentMessageAckFailure = void 0;
        this.#sentMessageToAck = void 0;
        if (isStandaloneAck && this.#closeTimer !== void 0) {
          return this.#close();
        }
      }
    }
    if (isStandaloneAck) {
      return;
    }
    if (requiresAck) {
      if (this.#receivedMessageToAck !== void 0) {
        this.#receivedMessageAckTimer.stop();
        await this.#sendStandaloneAckForMessage(this.#receivedMessageToAck);
      }
      this.#receivedMessageToAck = message;
      this.#receivedMessageAckTimer.start();
    }
    this.#messagesQueue.write(message);
  }
  async send(messageType, payload, options = {}) {
    var _stack2 = [];
    try {
      const {
        expectAckOnly = false,
        disableMrpLogic,
        expectedProcessingTime = MRP.DEFAULT_EXPECTED_PROCESSING_TIME,
        includeAcknowledgeMessageId,
        logContext,
        protocolId = this.#protocolId
      } = options;
      if (!this.session.usesMrp && includeAcknowledgeMessageId !== void 0) {
        throw new InternalError("Cannot include an acknowledge message ID when MRP is not used");
      }
      let { requiresAck } = options;
      if (requiresAck && !(this.session.usesMrp || this.session.isPeerLost)) {
        requiresAck = false;
      }
      const isStandaloneAck = SecureMessageType.isStandaloneAck(protocolId, messageType);
      if (isStandaloneAck) {
        if (!this.session.usesMrp) {
          return;
        }
        if (requiresAck) {
          throw new MatterFlowError("A standalone ack may not require acknowledgement.");
        }
      }
      if (this.#sentMessageToAck !== void 0 && !isStandaloneAck) {
        throw new MatterFlowError(
          `The previous message ${Message.via(this, this.#sentMessageToAck)} has not been acked yet, cannot send a new message.`
        );
      }
      this.#used = true;
      this.#messageSendCounter++;
      this.session.notifyActivity(false);
      let ackedMessageId = includeAcknowledgeMessageId;
      if (ackedMessageId === void 0 && this.session.usesMrp) {
        ackedMessageId = this.#receivedMessageToAck?.packetHeader.messageId;
        if (ackedMessageId !== void 0) {
          this.#receivedMessageAckTimer.stop();
          this.#receivedMessageToAck = void 0;
        }
      }
      let packetHeader;
      if (this.session.type === SessionType.Unicast) {
        packetHeader = {
          sessionId: this.#peerSessionId,
          sessionType: SessionType.Unicast,
          messageId: await this.session.getIncrementedMessageCounter(),
          destNodeId: this.#peerNodeId,
          sourceNodeId: this.#nodeId,
          hasPrivacyEnhancements: false,
          isControlMessage: false,
          hasMessageExtensions: false
        };
      } else if (this.session.type === SessionType.Group) {
        const session = this.session;
        if (!GroupSession.is(session)) {
          throw new InternalError("Session is not a GroupSession, but session type is Group.");
        }
        const destGroupId = GroupId.fromNodeId(this.#peerNodeId);
        if (destGroupId === 0) {
          throw new InternalError(`Invalid GroupId extracted from NodeId ${this.#peerNodeId}`);
        }
        packetHeader = {
          sessionId: this.#peerSessionId,
          sessionType: SessionType.Group,
          messageId: await session.getIncrementedMessageCounter(),
          destGroupId,
          sourceNodeId: this.#nodeId,
          // We are the source node, so use our NodeId
          hasPrivacyEnhancements: false,
          isControlMessage: false,
          hasMessageExtensions: false
        };
      } else {
        throw new InternalError(`Unknown session type: ${this.session.type}`);
      }
      const message = {
        packetHeader,
        payloadHeader: {
          exchangeId: this.#exchangeId,
          protocolId,
          messageType,
          isInitiatorMessage: this.isInitiator,
          requiresAck: requiresAck ?? (this.session.usesMrp && !isStandaloneAck),
          ackedMessageId,
          hasSecuredExtension: false
        },
        payload
      };
      let ackPromise;
      if (this.session.usesMrp && message.payloadHeader.requiresAck && !disableMrpLogic) {
        this.#sentMessageToAck = message;
        this.#retransmissionTimer = Time.getTimer(
          `retransmitting ${Message.via(this, message)}`,
          this.channel.getMrpResubmissionBackOffTime(0),
          () => this.#retransmitMessage(message, expectedProcessingTime)
        );
        const { promise, resolver, rejecter } = createPromise();
        ackPromise = promise;
        this.#sentMessageAckSuccess = resolver;
        this.#sentMessageAckFailure = rejecter;
      }
      const sending = __using(_stack2, this.join("sending", Diagnostic.strong(Message.via(this, message))));
      await this.channel.send(message, this, logContext);
      if (ackPromise !== void 0) {
        var _stack = [];
        try {
          this.#retransmissionCounter = 0;
          this.#retransmissionTimer?.start();
          const _waiting = __using(_stack, sending.join("waiting for ack"));
          const responseMessage = await ackPromise;
          this.#sentMessageAckSuccess = void 0;
          this.#sentMessageAckFailure = void 0;
          if (responseMessage) {
            const {
              payloadHeader: { protocolId: protocolId2, messageType: messageType2 }
            } = responseMessage;
            if (expectAckOnly && !SecureMessageType.isStandaloneAck(protocolId2, messageType2)) {
              throw new UnexpectedMessageError("Expected ack only", this.session, responseMessage);
            }
          }
        } catch (_) {
          var _error = _, _hasError = true;
        } finally {
          __callDispose(_stack, _error, _hasError);
        }
      }
    } catch (_2) {
      var _error2 = _2, _hasError2 = true;
    } finally {
      __callDispose(_stack2, _error2, _hasError2);
    }
  }
  nextMessage(options) {
    let timeout;
    if (options?.timeout !== void 0) {
      timeout = options.timeout;
    } else if (this.#messagesQueue.size > 0) {
      timeout = Instant;
    } else {
      timeout = this.channel.calculateMaximumPeerResponseTime(
        this.session.parameters,
        this.context.localSessionParameters,
        options?.expectedProcessingTime
      );
    }
    return this.#messagesQueue.read({ timeout });
  }
  async #sendStandaloneAckForMessage(message) {
    const {
      packetHeader: { messageId },
      payloadHeader: { requiresAck }
    } = message;
    if (!requiresAck || !this.session.usesMrp) return;
    await this.send(SecureMessageType.StandaloneAck, new Uint8Array(0), {
      includeAcknowledgeMessageId: messageId,
      protocolId: SECURE_CHANNEL_PROTOCOL_ID
    });
  }
  #retransmitMessage(message, expectedProcessingTime) {
    this.#retransmissionCounter++;
    this.#totalRetransmissionCounter++;
    if (this.considerClosed || this.#retransmissionCounter >= MRP.MAX_TRANSMISSIONS) {
      if (expectedProcessingTime && !this.considerClosed) {
        const finalWaitTime = Millis(
          this.channel.calculateMaximumPeerResponseTime(
            this.session.parameters,
            this.context.localSessionParameters,
            expectedProcessingTime
          ) - (this.#retransmissionTimer?.interval ?? Instant)
        );
        if (finalWaitTime > 0) {
          this.#retransmissionCounter--;
          this.#totalRetransmissionCounter--;
          logger.debug(
            `Message ${Message.via(this, message)}: Wait additional ${Duration.format(finalWaitTime)} for processing time and peer resubmissions after all our resubmissions`
          );
          this.#retransmissionTimer = Time.getTimer(
            `waiting after resubmissions for ${Message.via(this, message)}`,
            finalWaitTime,
            () => this.#retransmitMessage(message)
          ).start();
          return;
        }
      }
      if (this.#sentMessageToAck !== void 0 && this.#sentMessageAckFailure !== void 0) {
        this.#receivedMessageToAck = void 0;
        this.#sentMessageAckFailure(new RetransmissionLimitReachedError());
        this.#sentMessageAckFailure = void 0;
        this.#sentMessageAckSuccess = void 0;
      }
      if (this.#closeTimer !== void 0) {
        this.#close().catch((error) => logger.error("Error closing exchange", error));
      }
      return;
    }
    this.#messageSendCounter++;
    this.session.notifyActivity(false);
    this.context.retry(this.#retransmissionCounter);
    const resubmissionBackoffTime = this.channel.getMrpResubmissionBackOffTime(this.#retransmissionCounter);
    this.channel.send(message, this, {
      "retrans#": this.#retransmissionCounter,
      backoff: Duration.format(resubmissionBackoffTime)
    }).then(() => this.#initializeResubmission(message, resubmissionBackoffTime, expectedProcessingTime)).catch((error) => {
      logger.error(`Error retransmitting ${Message.via(this, message)}:`, error);
      if (error instanceof SessionClosedError) {
        this.#close().catch((error2) => logger.error("An error happened when closing the exchange", error2));
      } else {
        this.#initializeResubmission(message, resubmissionBackoffTime, expectedProcessingTime);
      }
    });
  }
  #initializeResubmission(message, resubmissionBackoffTime, expectedProcessingTimeMs) {
    this.#retransmissionTimer = Time.getTimer(
      "Message retransmission",
      resubmissionBackoffTime,
      () => this.#retransmitMessage(message, expectedProcessingTimeMs)
    ).start();
  }
  [Symbol.asyncDispose]() {
    return this.destroy();
  }
  async destroy() {
    if (this.#isDestroyed) {
      return;
    }
    this.#isDestroyed = true;
    if (this.#closeTimer === void 0 && this.#receivedMessageToAck !== void 0) {
      this.#receivedMessageAckTimer.stop();
      const messageToAck = this.#receivedMessageToAck;
      this.#receivedMessageToAck = void 0;
      try {
        await this.#sendStandaloneAckForMessage(messageToAck);
      } catch (error) {
        logger.error("An error happened when closing the exchange", error);
      }
    }
    await this.#close();
  }
  startTimedInteraction(timeout) {
    if (this.#timedInteractionTimer !== void 0 && this.#timedInteractionTimer.isRunning) {
      this.#timedInteractionTimer.stop();
      throw new StatusResponseError(
        "Timed interaction already running for this exchange",
        StatusCode.InvalidAction
      );
    }
    logger.debug(
      "Starting timed interaction",
      Mark.INBOUND,
      this.channel.name,
      Diagnostic.dict({ exId: this.#exchangeId, timeout: Duration.format(timeout) })
    );
    this.#timedInteractionTimer = Time.getTimer("Timed interaction", timeout, () => {
      logger.debug(
        "Timed interaction timeout!",
        Diagnostic.dict({ exId: this.#exchangeId, via: this.channel.name })
      );
    }).start();
  }
  clearTimedInteraction() {
    if (this.#timedInteractionTimer !== void 0) {
      logger.debug(
        "Clearing timed interaction",
        Diagnostic.dict({ exId: this.#exchangeId, via: this.channel.name })
      );
      this.#timedInteractionTimer.stop();
      this.#timedInteractionTimer = void 0;
    }
  }
  hasTimedInteraction() {
    return this.#timedInteractionTimer !== void 0;
  }
  hasActiveTimedInteraction() {
    return this.#timedInteractionTimer !== void 0 && this.#timedInteractionTimer.isRunning;
  }
  hasExpiredTimedInteraction() {
    return this.#timedInteractionTimer !== void 0 && !this.#timedInteractionTimer.isRunning;
  }
  /**
   * Closes the exchange.
   * If force is true, the exchange will be closed immediately, even if there are still messages to send.
   * If force is false, the exchange will be closed only after all messages have been sent.
   */
  async close(force = false) {
    if (this.#isDestroyed) {
      return;
    }
    this.#lifetime.closing();
    if (this.#closeTimer !== void 0) {
      if (force) {
        this.#closeTimer.stop();
        return this.#close();
      }
      return;
    }
    if (!this.#used) {
      logger.info(this.via, `Exchange never used, closing directly`);
      return this.#close();
    }
    await this.#closing.emit(true);
    if (this.#receivedMessageToAck !== void 0) {
      this.#receivedMessageAckTimer.stop();
      const messageToAck = this.#receivedMessageToAck;
      this.#receivedMessageToAck = void 0;
      try {
        await this.#sendStandaloneAckForMessage(messageToAck);
      } catch (error) {
        logger.error(this.via, `Unhandled error closing exchange`, error);
      }
      if (force) {
        return this.#close();
      }
    } else if (this.#sentMessageToAck === void 0 || force) {
      return this.#close();
    }
    let maxResubmissionTime = Instant;
    for (let i = this.#retransmissionCounter; i <= MRP.MAX_TRANSMISSIONS; i++) {
      maxResubmissionTime = Millis(
        maxResubmissionTime + this.channel.getMrpResubmissionBackOffTime(i, void 0, true)
      );
    }
    this.#closeTimer = Time.getTimer(
      `Exchange ${this.via} close`,
      maxResubmissionTime,
      async () => await this.#close()
    ).start();
  }
  async #close() {
    var _stack = [];
    try {
      const _closing = __using(_stack, this.#lifetime.closing());
      this.#retransmissionTimer?.stop();
      this.#sentMessageAckSuccess?.(void 0);
      this.#closeTimer?.stop();
      this.#timedInteractionTimer?.stop();
      this.#messagesQueue.close();
      await this.#closed.emit(true);
    } catch (_) {
      var _error = _, _hasError = true;
    } finally {
      __callDispose(_stack, _error, _hasError);
    }
  }
  get via() {
    if (this.session === void 0) {
      return Diagnostic.via(`${Mark.EXCHANGE}${this.idStr}`);
    }
    return Diagnostic.via(`${this.session.via}${Mark.EXCHANGE}${this.idStr}`);
  }
  /**
   * Expose some diagnostics for logging.
   * For simply one message exchanges without retransmissions nothing will be exposed
   */
  get diagnostics() {
    if (this.#totalRetransmissionCounter === 0 && this.#messageSendCounter === 1 && this.#messageReceivedCounter === 1) {
      return "";
    }
    return `${this.#messageReceivedCounter}${Mark.TRANSFERRED}${this.#messageSendCounter}${this.#totalRetransmissionCounter > 0 ? `+${this.#totalRetransmissionCounter}` : ""}`;
  }
}
export {
  MATTER_MESSAGE_OVERHEAD,
  MessageExchange
};
//# sourceMappingURL=MessageExchange.js.map
