"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
  for (var name in all)
    __defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
  if (from && typeof from === "object" || typeof from === "function") {
    for (let key of __getOwnPropNames(from))
      if (!__hasOwnProp.call(to, key) && key !== except)
        __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
  }
  return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var ServerSubscription_exports = {};
__export(ServerSubscription_exports, {
  DEFAULT_RANDOMIZATION_WINDOW: () => DEFAULT_RANDOMIZATION_WINDOW,
  INTERNAL_INTERVAL_PUBLISHER_LIMIT: () => INTERNAL_INTERVAL_PUBLISHER_LIMIT,
  MAX_INTERVAL_PUBLISHER_LIMIT: () => MAX_INTERVAL_PUBLISHER_LIMIT,
  MIN_INTERVAL: () => MIN_INTERVAL,
  ServerSubscription: () => ServerSubscription,
  ServerSubscriptionConfig: () => ServerSubscriptionConfig
});
module.exports = __toCommonJS(ServerSubscription_exports);
var import_NodeActivity = require("#behavior/context/NodeActivity.js");
var import_RemoteActorContext = require("#behavior/context/server/RemoteActorContext.js");
var import_general = require("#general");
var import_model = require("#model");
var import_protocol = require("#protocol");
var import_types = require("#types");
/**
 * @license
 * Copyright 2022-2025 Matter.js Authors
 * SPDX-License-Identifier: Apache-2.0
 */
const logger = import_general.Logger.get("ServerSubscription");
const MAX_INTERVAL_PUBLISHER_LIMIT = import_general.Hours.one;
const INTERNAL_INTERVAL_PUBLISHER_LIMIT = (0, import_general.Minutes)(3);
const MIN_INTERVAL = (0, import_general.Seconds)(2);
const DEFAULT_RANDOMIZATION_WINDOW = (0, import_general.Seconds)(10);
var ServerSubscriptionConfig;
((ServerSubscriptionConfig2) => {
  function of(options) {
    return {
      maxInterval: options?.maxInterval ?? INTERNAL_INTERVAL_PUBLISHER_LIMIT,
      minInterval: import_general.Duration.max(options?.minInterval ?? MIN_INTERVAL, MIN_INTERVAL),
      randomizationWindow: options?.randomizationWindow ?? DEFAULT_RANDOMIZATION_WINDOW
    };
  }
  ServerSubscriptionConfig2.of = of;
})(ServerSubscriptionConfig || (ServerSubscriptionConfig = {}));
class ServerSubscription extends import_protocol.Subscription {
  #context;
  #lastUpdateTime = import_general.Timestamp.zero;
  #updateTimer;
  #sendDelayTimer = import_general.Time.getTimer(
    `Subscription ${this.id} delay`,
    (0, import_general.Millis)(50),
    () => this.#triggerSendUpdate()
  );
  #outstandingAttributeUpdates;
  #outstandingEventsMinNumber;
  #changeHandlers = new import_general.ObserverGroup();
  #sendUpdatesActivated = false;
  #seededClusterDetails = /* @__PURE__ */ new Map();
  #latestSeededEventNumber = (0, import_types.EventNumber)(0);
  #sendInterval;
  #minIntervalFloor;
  #maxIntervalCeiling;
  #peerAddress;
  #sendNextUpdateImmediately = false;
  #sendUpdateErrorCounter = 0;
  #currentUpdatePromise;
  constructor(options) {
    const {
      id,
      context,
      criteria,
      minIntervalFloor,
      maxIntervalCeiling,
      subscriptionOptions,
      useAsMaxInterval,
      useAsSendInterval
    } = options;
    super(context.session, id, criteria);
    this.#context = context;
    this.#peerAddress = this.session.peerAddress;
    this.#minIntervalFloor = minIntervalFloor;
    this.#maxIntervalCeiling = maxIntervalCeiling;
    let maxInterval;
    let sendInterval;
    if (useAsMaxInterval !== void 0 && useAsSendInterval !== void 0) {
      maxInterval = useAsMaxInterval;
      sendInterval = useAsSendInterval;
    } else {
      ({ maxInterval, sendInterval } = this.#determineSendingIntervals(
        subscriptionOptions.minInterval,
        subscriptionOptions.maxInterval,
        subscriptionOptions.randomizationWindow
      ));
    }
    this.maxInterval = maxInterval;
    this.#sendInterval = sendInterval;
    this.#updateTimer = import_general.Time.getTimer(
      `Subscription ${this.id} update`,
      this.#sendInterval,
      () => this.#prepareDataUpdate()
    );
  }
  #determineSendingIntervals(subscriptionMinInterval, subscriptionMaxInterval, subscriptionRandomizationWindow) {
    const maxInterval = import_general.Duration.min(
      import_general.Millis.floor(
        (0, import_general.Millis)(
          import_general.Duration.max(
            subscriptionMinInterval,
            import_general.Duration.max(
              this.#minIntervalFloor,
              import_general.Duration.min(subscriptionMaxInterval, this.#maxIntervalCeiling)
            )
          ) + subscriptionRandomizationWindow * Math.random()
        )
      ),
      MAX_INTERVAL_PUBLISHER_LIMIT
    );
    let sendInterval = import_general.Millis.floor((0, import_general.Millis)(maxInterval / 2));
    if (sendInterval < import_general.Minutes.one) {
      sendInterval = import_general.Duration.max(this.#minIntervalFloor, import_general.Millis.floor((0, import_general.Millis)(maxInterval * 0.8)));
    }
    if (sendInterval < subscriptionMinInterval) {
      logger.warn(
        `Determined subscription send interval of ${import_general.Duration.format(sendInterval)} is too low. Using maxInterval (${import_general.Duration.format(maxInterval)}) instead.`
      );
      sendInterval = subscriptionMinInterval;
    }
    return { maxInterval, sendInterval };
  }
  #addOutstandingAttributes(endpointId, clusterId, changedAttrs) {
    if (!changedAttrs.length) {
      return;
    }
    this.#outstandingAttributeUpdates = this.#outstandingAttributeUpdates ?? {};
    this.#outstandingAttributeUpdates[endpointId] = this.#outstandingAttributeUpdates[endpointId] ?? {};
    this.#outstandingAttributeUpdates[endpointId][clusterId] = this.#outstandingAttributeUpdates[endpointId][clusterId] ?? /* @__PURE__ */ new Set();
    for (const attributeId of changedAttrs) {
      this.#outstandingAttributeUpdates[endpointId][clusterId].add(attributeId);
    }
  }
  #handleClusterStateChanges(endpointId, clusterId, changedAttrs, version) {
    if (this.isClosed || !changedAttrs.length) {
      return;
    }
    if (this.#seededClusterDetails !== void 0) {
      const seededVersion = this.#seededClusterDetails.get(`${endpointId}-${clusterId}`);
      if (seededVersion === void 0 || seededVersion === version) {
        return;
      }
    }
    this.#addOutstandingAttributes(endpointId, clusterId, changedAttrs);
    this.#prepareDataUpdate();
  }
  #handleAddedEvents(occurrence) {
    if (this.isClosed) {
      return;
    }
    if (this.#outstandingEventsMinNumber === void 0 || this.#latestSeededEventNumber !== void 0) {
      this.#outstandingEventsMinNumber = occurrence.number;
    }
    if (this.#sendEventUrgently(occurrence)) {
      this.#prepareDataUpdate();
    }
  }
  #sendEventUrgently({ endpointId, clusterId, eventId }) {
    return (this.criteria.eventRequests ?? []).some(
      ({ endpointId: reqEndpointId, clusterId: reqClusterId, eventId: reqEventId, isUrgent }) => isUrgent && (reqEndpointId === void 0 || reqEndpointId === endpointId) && (reqClusterId === void 0 || reqClusterId === clusterId) && (reqEventId === void 0 || reqEventId === eventId)
    );
  }
  get sendInterval() {
    return this.#sendInterval;
  }
  get minIntervalFloor() {
    return this.#minIntervalFloor;
  }
  get maxIntervalCeiling() {
    return this.#maxIntervalCeiling;
  }
  activate() {
    super.activate();
    if (this.criteria.eventFilters !== void 0) this.criteria.eventFilters.length = 0;
    if (this.criteria.dataVersionFilters !== void 0) this.criteria.dataVersionFilters.length = 0;
    this.#sendUpdatesActivated = true;
    if (this.#outstandingEventsMinNumber !== void 0 && this.#latestSeededEventNumber !== void 0) {
      if (this.#latestSeededEventNumber < this.#outstandingEventsMinNumber) {
        this.#outstandingEventsMinNumber = (0, import_types.EventNumber)(BigInt(this.#latestSeededEventNumber) + BigInt(1));
      } else {
        this.#outstandingEventsMinNumber = void 0;
      }
    }
    this.#latestSeededEventNumber = void 0;
    this.#seededClusterDetails = void 0;
    if (this.#outstandingAttributeUpdates !== void 0 || this.#outstandingEventsMinNumber !== void 0) {
      this.#triggerSendUpdate();
    }
    this.#updateTimer = import_general.Time.getTimer(
      "Subscription update",
      this.#sendInterval,
      () => this.#prepareDataUpdate()
    ).start();
  }
  /**
   * Check if data should be sent straight away or delayed because the minimum interval is not reached. Delay real
   * sending by 50ms in any case to make sure to catch all updates.
   */
  #prepareDataUpdate() {
    if (this.#sendDelayTimer.isRunning || this.isClosed) {
      return;
    }
    if (!this.#sendUpdatesActivated) {
      return;
    }
    this.#updateTimer.stop();
    const now = import_general.Time.nowMs;
    const timeSinceLastUpdate = (0, import_general.Millis)(now - this.#lastUpdateTime);
    if (timeSinceLastUpdate < this.#minIntervalFloor) {
      this.#updateTimer = import_general.Time.getTimer(
        "Subscription update",
        (0, import_general.Millis)(this.#minIntervalFloor - timeSinceLastUpdate),
        () => this.#prepareDataUpdate()
      ).start();
      return;
    }
    this.#sendDelayTimer.start();
    this.#updateTimer = import_general.Time.getTimer(
      `Subscription update ${this.id}`,
      this.#sendInterval,
      () => this.#prepareDataUpdate()
    ).start();
  }
  #triggerSendUpdate(onlyWithData = false) {
    if (this.#currentUpdatePromise !== void 0) {
      logger.debug("Sending update already in progress, delaying update ...");
      this.#sendNextUpdateImmediately = true;
      return;
    }
    this.#currentUpdatePromise = this.#sendUpdate(onlyWithData).catch((error) => logger.warn("Sending subscription update failed:", error)).finally(() => this.#currentUpdatePromise = void 0);
  }
  /**
   * Determine all attributes that have changed since the last update and send them out to the subscriber.
   */
  async #sendUpdate(onlyWithData = false) {
    const attributeFilter = this.#outstandingAttributeUpdates;
    this.#outstandingAttributeUpdates = void 0;
    const eventsMinNumber = this.#outstandingEventsMinNumber;
    this.#outstandingEventsMinNumber = void 0;
    if (onlyWithData && attributeFilter === void 0 && eventsMinNumber === void 0) {
      return;
    }
    this.#lastUpdateTime = import_general.Time.nowMs;
    try {
      if (await this.#sendUpdateMessage(attributeFilter, eventsMinNumber, onlyWithData)) {
        this.#sendUpdateErrorCounter = 0;
      }
    } catch (error) {
      if (this.isClosed) {
        return;
      }
      this.#sendUpdateErrorCounter++;
      logger.info(
        `Error sending subscription update message (error count=${this.#sendUpdateErrorCounter}):`,
        error instanceof import_general.MatterError && error.message || error
      );
      if (this.#sendUpdateErrorCounter <= 2) {
        if (attributeFilter !== void 0) {
          for (const [endpointId, clusters] of Object.entries(attributeFilter)) {
            for (const [clusterId, attributes] of Object.entries(clusters)) {
              this.#addOutstandingAttributes(
                (0, import_types.EndpointNumber)(parseInt(endpointId)),
                (0, import_types.ClusterId)(parseInt(clusterId)),
                Array.from(attributes)
              );
            }
          }
        }
        if (eventsMinNumber !== void 0) {
          this.#outstandingEventsMinNumber = eventsMinNumber;
        }
      } else {
        logger.info(
          `Sending update failed 3 times in a row, canceling subscription ${this.id} and let controller subscribe again.`
        );
        this.#sendNextUpdateImmediately = false;
        if (error instanceof import_general.NoResponseTimeoutError || error instanceof import_general.NetworkError || error instanceof import_protocol.NoChannelError) {
          this.isCanceledByPeer = true;
          await this.destroy();
          return;
        } else {
          throw error;
        }
      }
    }
    if (this.#sendNextUpdateImmediately) {
      logger.debug("Sending delayed update immediately after last one was sent.");
      this.#sendNextUpdateImmediately = false;
      await this.#sendUpdate(true);
    }
  }
  /**
   * Returns an iterator that yields the data reports and events data for the given read request.
   */
  async *#processAttributesAndEventsReport(context, suppressStatusReports = false) {
    const request = {
      ...this.criteria,
      interactionModelRevision: import_model.Specification.INTERACTION_MODEL_REVISION
      // irrelevant here, set to our version
    };
    const delayedStatusReports = new Array();
    let hasValuesInResponse = false;
    let validAttributes = 0;
    let validEvents = 0;
    const session = (0, import_RemoteActorContext.RemoteActorContext)(context).beginReadOnly();
    try {
      if (import_protocol.Read.containsAttribute(request)) {
        const attributeReader = new import_protocol.AttributeReadResponse(this.#context.node.protocol, session);
        for (const chunk of attributeReader.process(request)) {
          for (const report of chunk) {
            if (report.kind === "attr-status") {
              if (suppressStatusReports) {
                continue;
              }
              if (!hasValuesInResponse) {
                delayedStatusReports.push(report);
                continue;
              }
            } else if (!hasValuesInResponse && report.kind === "attr-value") {
              for (const delayedReport of delayedStatusReports) {
                yield import_protocol.InteractionServerMessenger.convertServerInteractionReport(delayedReport);
              }
              delayedStatusReports.length = 0;
              hasValuesInResponse = true;
            }
            if (this.#seededClusterDetails !== void 0 && report.kind === "attr-value") {
              const {
                path: { endpointId, clusterId },
                version
              } = report;
              this.#seededClusterDetails.set(`${endpointId}-${clusterId}`, version);
            }
            yield import_protocol.InteractionServerMessenger.convertServerInteractionReport(report);
          }
        }
        validAttributes = attributeReader.counts.existent;
      }
      if (import_protocol.Read.containsEvent(request)) {
        const eventReader = new import_protocol.EventReadResponse(this.#context.node.protocol, session);
        for await (const chunk of eventReader.process(request)) {
          for (const report of chunk) {
            if (report.kind === "event-status") {
              if (suppressStatusReports) {
                continue;
              }
              if (!hasValuesInResponse) {
                delayedStatusReports.push(report);
                continue;
              }
            } else if (!hasValuesInResponse && report.kind === "event-value") {
              for (const delayedReport of delayedStatusReports) {
                yield import_protocol.InteractionServerMessenger.convertServerInteractionReport(delayedReport);
              }
              delayedStatusReports.length = 0;
              hasValuesInResponse = true;
            }
            if (this.#latestSeededEventNumber !== void 0 && report.kind === "event-value") {
              this.#latestSeededEventNumber = report.number;
            }
            yield import_protocol.InteractionServerMessenger.convertServerInteractionReport(report);
          }
        }
        validEvents = eventReader.counts.existent;
      }
      if (validAttributes === 0 && validEvents === 0) {
        throw new import_types.StatusResponseError(
          "Subscription failed because no attributes or events are matching the query",
          import_types.StatusCode.InvalidAction
        );
      } else if (!hasValuesInResponse && delayedStatusReports.length) {
        for (const delayedReport of delayedStatusReports) {
          yield import_protocol.InteractionServerMessenger.convertServerInteractionReport(delayedReport);
        }
      }
    } finally {
      session[Symbol.dispose]();
    }
    this.#lastUpdateTime = import_general.Time.nowMs;
  }
  async sendInitialReport(messenger, readContext, suppressStatusReports) {
    this.#updateTimer.stop();
    if (this.criteria.attributeRequests?.length) {
      this.#changeHandlers.on(
        this.#context.node.protocol.attrsChanged,
        this.#handleClusterStateChanges.bind(this)
      );
    }
    if (this.criteria.eventRequests?.length) {
      this.#changeHandlers.on(this.#context.node.protocol.eventHandler.added, this.#handleAddedEvents.bind(this));
    }
    await messenger.sendDataReport({
      baseDataReport: {
        suppressResponse: false,
        // we always need proper response for initial report
        subscriptionId: this.id,
        interactionModelRevision: import_model.Specification.INTERACTION_MODEL_REVISION
      },
      forFabricFilteredRead: this.criteria.isFabricFiltered,
      payload: this.#processAttributesAndEventsReport(readContext, suppressStatusReports)
    });
  }
  async #flush() {
    this.#sendDelayTimer.stop();
    if (this.#outstandingAttributeUpdates !== void 0 || this.#outstandingEventsMinNumber !== void 0) {
      logger.debug(`Flushing subscription ${this.id}${this.isClosed ? " (for closing)" : ""}`);
      this.#triggerSendUpdate(true);
      if (this.#currentUpdatePromise) {
        await this.#currentUpdatePromise;
      }
    }
  }
  async destroy() {
    this.#sendUpdatesActivated = false;
    this.#changeHandlers.close();
    this.#updateTimer.stop();
    this.#sendDelayTimer.stop();
    await super.destroy();
  }
  /**
   * Closes the subscription and flushes all outstanding data updates if requested.
   */
  async close(flush = false, cancelledByPeer = false) {
    if (this.isClosed) {
      return;
    }
    if (cancelledByPeer) {
      this.isCanceledByPeer = true;
    }
    await this.destroy();
    if (flush) {
      await this.#flush();
    }
    if (this.#currentUpdatePromise) {
      await this.#currentUpdatePromise;
    }
  }
  /**
   * Iterates over all attributes and events that have changed since the last update and sends them to
   * the controller.
   * A thrown exception will cancel the sending process immediately.
   */
  async *#iterateDataUpdate(exchange, attributeFilter, eventsMinNumber) {
    const request = {
      ...this.criteria,
      interactionModelRevision: import_model.Specification.INTERACTION_MODEL_REVISION
      // irrelevant here, set to our version
    };
    const session = (0, import_RemoteActorContext.RemoteActorContext)({
      activity: exchange[import_NodeActivity.NodeActivity.activityKey],
      fabricFiltered: request.isFabricFiltered,
      message: {},
      exchange,
      node: this.#context.node
    }).beginReadOnly();
    try {
      if (attributeFilter !== void 0 && import_protocol.Read.containsAttribute(request)) {
        const attributeReader = new import_protocol.AttributeSubscriptionResponse(
          this.#context.node.protocol,
          session,
          attributeFilter
        );
        for (const chunk of attributeReader.process(request)) {
          for (const report of chunk) {
            yield import_protocol.InteractionServerMessenger.convertServerInteractionReport(report);
          }
        }
      }
      if (eventsMinNumber !== void 0 && import_protocol.Read.containsEvent(request)) {
        request.eventFilters = [{ eventMin: eventsMinNumber }];
        const eventReader = new import_protocol.EventReadResponse(this.#context.node.protocol, session);
        for await (const chunk of eventReader.process(request)) {
          for (const report of chunk) {
            if (report.kind === "event-status") {
              continue;
            }
            yield import_protocol.InteractionServerMessenger.convertServerInteractionReport(report);
          }
        }
      }
    } finally {
      session[Symbol.dispose]();
    }
  }
  async #sendUpdateMessage(attributeFilter, eventsMinNumber, onlyWithData) {
    const exchange = this.#context.initiateExchange(this.#peerAddress, import_types.INTERACTION_PROTOCOL_ID);
    if (exchange === void 0) return false;
    const messenger = new import_protocol.InteractionServerMessenger(exchange);
    try {
      if (attributeFilter === void 0 && eventsMinNumber === void 0) {
        await messenger.sendDataReport({
          baseDataReport: {
            suppressResponse: true,
            // suppressResponse true for empty DataReports
            subscriptionId: this.id,
            interactionModelRevision: import_model.Specification.INTERACTION_MODEL_REVISION
          },
          forFabricFilteredRead: this.criteria.isFabricFiltered,
          waitForAck: !this.isClosed
          // Do not wait for ack when closed
        });
      } else {
        await messenger.sendDataReport({
          baseDataReport: {
            suppressResponse: false,
            // Non-empty data reports always need to send response
            subscriptionId: this.id,
            interactionModelRevision: import_model.Specification.INTERACTION_MODEL_REVISION
          },
          forFabricFilteredRead: this.criteria.isFabricFiltered,
          payload: this.#iterateDataUpdate(exchange, attributeFilter, eventsMinNumber),
          waitForAck: !this.isClosed,
          // Do not wait for ack when closed
          suppressEmptyReport: onlyWithData
        });
      }
    } catch (error) {
      if (import_types.StatusResponseError.is(error, import_types.StatusCode.InvalidSubscription, import_types.StatusCode.Failure)) {
        logger.info(`Subscription ${this.id} cancelled by peer.`);
        this.isCanceledByPeer = true;
        await this.close(false);
      } else {
        import_types.StatusResponseError.accept(error);
        logger.info(`Subscription ${this.id} update failed:`, error);
        await this.close(false);
      }
    } finally {
      await messenger.close();
    }
    return true;
  }
}
//# sourceMappingURL=ServerSubscription.js.map
