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 { DiscoveryData } from "#common/Scanner.js";
import { getOperationalDeviceQname } from "#mdns/MdnsConsts.js";
import { PeerAddress } from "#peer/PeerAddress.js";
import { SessionParameters } from "#session/SessionParameters.js";
import {
  Abort,
  AbortedError,
  AsyncObservable,
  BasicMultiplex,
  BasicSet,
  ClosedError,
  Diagnostic,
  Instant,
  IpService,
  isIpNetworkChannel,
  Logger,
  Millis,
  ObserverGroup,
  QuietObservable,
  Time,
  Timestamp
} from "@matter/general";
import { BasicInformation } from "@matter/types/clusters/basic-information";
import { PeerAddressMonitor } from "./PeerAddressMonitor.js";
import { PeerUnreachableError } from "./PeerCommunicationError.js";
import { PeerConnection } from "./PeerConnection.js";
import { ObservablePeerDescriptor } from "./PeerDescriptor.js";
import { PeerExchangeProvider } from "./PeerExchangeProvider.js";
import { PeerTimingParameters } from "./PeerTimingParameters.js";
const logger = Logger.get("Peer");
class Peer {
  #lifetime;
  #descriptor;
  #context;
  #sessions = new BasicSet();
  #workers;
  #isSaving = false;
  #interaction;
  #protocol;
  #physicalProperties;
  #abort = new Abort();
  #connecting;
  #service;
  #observers = new ObserverGroup();
  #exchangeProvider;
  #updated = AsyncObservable();
  #addressMonitor;
  constructor(descriptor, context) {
    this.#lifetime = context.join(descriptor.address.toString());
    this.#workers = new BasicMultiplex();
    this.#descriptor = new ObservablePeerDescriptor(descriptor, () => {
      if (this.#isSaving) {
        return;
      }
      this.#isSaving = true;
      this.#workers.add(this.#save());
    });
    this.#service = new IpService(
      getOperationalDeviceQname(
        context.sessions.fabricFor(descriptor.address).globalId,
        descriptor.address.nodeId
      ),
      Diagnostic.via(this.address.toString()),
      context.names
    );
    if (descriptor.operationalAddress) {
      this.#service.status.isReachable = true;
    }
    this.#context = context;
    this.#observers.on(this.#service.changed, () => {
      this.#descriptor.discoveryData = {
        ...this.#descriptor.discoveryData,
        ...DiscoveryData(this.#service.parameters)
      };
      this.#addressCheck.schedule();
    });
    this.#observers.on(this.#sessions.added, (session) => {
      const updateNetworkAddress = (networkAddress) => {
        this.#descriptor.operationalAddress = networkAddress;
      };
      session.closing.on(() => {
        this.#sessions.delete(session);
      });
      if (!session.isClosed) {
        const { channel } = session.channel;
        if (isIpNetworkChannel(channel)) {
          updateNetworkAddress(channel.networkAddress);
          channel.networkAddressChanged.on(updateNetworkAddress);
        }
      }
      this.#descriptor.sessionParameters = session.parameters;
    });
  }
  /**
   * Emits when metadata changes.
   */
  get updated() {
    return this.#updated;
  }
  get lifetime() {
    return this.#lifetime;
  }
  get fabric() {
    return this.#context.sessions.fabricFor(this.address);
  }
  get interaction() {
    return this.#interaction;
  }
  set interaction(interaction) {
    this.#interaction = interaction;
  }
  get protocol() {
    return this.#protocol;
  }
  set protocol(protocol) {
    this.#protocol = protocol;
  }
  get physicalProperties() {
    return this.#physicalProperties;
  }
  set physicalProperties(props) {
    this.#physicalProperties = props;
  }
  get basicInformation() {
    return this.#protocol?.[0]?.[BasicInformation.id]?.readState({});
  }
  get limits() {
    return {
      caseSessionsPerFabric: 3,
      subscriptionsPerFabric: 3,
      ...this.basicInformation?.capabilityMinima
    };
  }
  get address() {
    return this.#descriptor.address;
  }
  get descriptor() {
    return this.#descriptor;
  }
  get sessions() {
    return this.#sessions;
  }
  get subscriptions() {
    return [...this.#sessions].flatMap((session) => [...session.subscriptions]);
  }
  get exchangeProvider() {
    if (this.#exchangeProvider === void 0) {
      this.#exchangeProvider = new PeerExchangeProvider(this, this.#context);
    }
    return this.#exchangeProvider;
  }
  get service() {
    return this.#service;
  }
  /**
   * "Best guess" {@link SessionParameters} for the peer based on available information.
   */
  get sessionParameters() {
    const bi = this.basicInformation;
    const dd = this.descriptor.discoveryData;
    return SessionParameters({
      dataModelRevision: bi?.dataModelRevision,
      maxPathsPerInvoke: bi?.maxPathsPerInvoke,
      specificationVersion: bi?.specificationVersion,
      idleInterval: dd?.SII,
      activeInterval: dd?.SAI,
      activeThreshold: dd?.SAT,
      ...this.#descriptor.sessionParameters
    });
  }
  get network() {
    return this.#context.networks.forPeer(this);
  }
  /**
   * Time that node has been unreachable.
   *
   * If we are actively attempting to connect to the peer, this is the time since the connection process started.
   * Otherwise it is zero.
   */
  get timeOffline() {
    if (this.service.status.connectionInitiatedAt === void 0) {
      return 0;
    }
    return Timestamp.delta(this.service.status.connectionInitiatedAt, Time.nowMs);
  }
  /**
   * Obtain a session with the peer, establishing anew as necessary.
   */
  async connect(options) {
    if (PeerAddress.isGroup(this.address)) {
      return await this.#context.sessions.groupSessionForAddress(this.address, this.#context.exchanges);
    }
    while (true) {
      var _stack = [];
      try {
        const session = this.newestSession;
        if (session) {
          return session;
        }
        this.#initiateConnection(options);
        let timeout = options?.connectionTimeout ?? (options?.abort ? void 0 : this.#context.timing.defaultConnectionTimeout);
        if (timeout === void 0 || timeout === Infinity) {
          timeout = void 0;
        } else if (timeout <= 0) {
          timeout = Instant;
        } else if (!options?.kick) {
          timeout = Millis(timeout - this.timeOffline);
        }
        const localAbort = __using(_stack, new Abort({
          abort: [this.#abort, options?.abort],
          timeout,
          timeoutHandler: () => {
            throw new PeerUnreachableError(this.timeOffline);
          }
        }));
        localAbort.throwIfAborted();
        await localAbort.race(this.#connecting?.done);
        localAbort.throwIfAborted();
      } catch (_) {
        var _error = _, _hasError = true;
      } finally {
        __callDispose(_stack, _error, _hasError);
      }
    }
  }
  /**
   * Kick the connection process.
   *
   * Aborts the current CASE handshake exchange and restarts it from scratch with a fresh MRP
   * backoff. Rate-limited; repeated calls within {@link PeerTimingParameters.kickRestartCooldown}
   * are suppressed.
   */
  kick() {
    this.#connecting?.kick("connect");
  }
  /**
   * Abort any outstanding connection attempts.
   */
  async disconnect() {
    if (this.#connecting) {
      var _stack = [];
      try {
        const _disconnecting = __using(_stack, this.#lifetime.join("disconnecting"));
        this.#connecting.abort();
        await this.#connecting.done;
      } catch (_) {
        var _error = _, _hasError = true;
      } finally {
        __callDispose(_stack, _error, _hasError);
      }
    }
  }
  /**
   * Permanently forget the peer.
   */
  async delete() {
    logger.info("Removing", Diagnostic.strong(this.toString()));
    try {
      await this.close();
    } catch (error) {
      AbortedError.accept(error);
    }
    await this.#context.sessions.deleteResumptionRecord(this.address);
  }
  /**
   * Close the peer without removing persistent state.
   */
  async close() {
    var _stack = [];
    try {
      const _lifetime = __using(_stack, this.#lifetime.closing());
      this.#observers.close();
      this.#addressMonitor?.stop();
      this.#abort(new ClosedError("Peer closed"));
      for (const session of this.#context.sessions.sessionsFor(this.address)) {
        await session.initiateClose();
      }
      await this.#workers;
      await this.#service.close();
      this.#context.closed(this);
    } catch (_) {
      var _error = _, _hasError = true;
    } finally {
      __callDispose(_stack, _error, _hasError);
    }
  }
  toString() {
    return this.address.toString();
  }
  get hasSession() {
    return !!this.sessions.find((session) => !session.isClosing && !session.isPeerLost);
  }
  async #save() {
    var _stack = [];
    try {
      const _lifetime = __using(_stack, this.#lifetime.join("saving"));
      this.#isSaving = false;
      await this.#updated.emit(this);
    } catch (_) {
      var _error = _, _hasError = true;
    } finally {
      __callDispose(_stack, _error, _hasError);
    }
  }
  get newestSession() {
    let found;
    for (const session of this.#sessions) {
      if (session.isClosing || session.isPeerLost) {
        continue;
      }
      if (!found || found.timestamp < session.timestamp) {
        found = session;
      }
    }
    return found;
  }
  get #addressCheck() {
    if (this.#addressMonitor === void 0) {
      this.#addressMonitor = new PeerAddressMonitor(
        this,
        this.#context.timing.addressChangeStabilizationDelay,
        this.#context.timing.addressChangeProbeCooldown,
        this.#abort,
        (work) => this.#workers.add(work)
      );
    }
    return this.#addressMonitor;
  }
  #initiateConnection(options) {
    if (this.#connecting) {
      if (options?.kick) {
        this.kick();
      }
      return;
    }
    const abort = new Abort({ abort: this.#abort });
    const added = this.#sessions.added.use(() => abort());
    const timing = options?.timing ? PeerTimingParameters.merge(this.#context.timing, options.timing) : this.#context.timing;
    const kicker = new QuietObservable({
      minimumEmitInterval: timing.kickThrottleInterval,
      skipSuppressedEmits: true
    });
    this.#connecting = {
      abort,
      done: PeerConnection(this, this.#context, {
        network: options?.network,
        timing: options?.timing,
        handleError: options?.handleError,
        abort,
        kicker
      }).finally(() => {
        this.#connecting = void 0;
        abort.close();
        added[Symbol.dispose]();
      }),
      kick(origin) {
        kicker.emit(origin);
      }
    };
    this.#workers.add(this.#connecting);
  }
}
export {
  Peer
};
//# sourceMappingURL=Peer.js.map
