"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __knownSymbol = (name, symbol) => (symbol = Symbol[name]) ? symbol : /* @__PURE__ */ Symbol.for("Symbol." + name);
var __typeError = (msg) => {
  throw TypeError(msg);
};
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 __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();
};
var MdnsClient_exports = {};
__export(MdnsClient_exports, {
  MdnsClient: () => MdnsClient
});
module.exports = __toCommonJS(MdnsClient_exports);
var import_general = require("#general");
var import_PeerAddress = require("#peer/PeerAddress.js");
var import_types = require("#types");
var import_MdnsConsts = require("./MdnsConsts.js");
/**
 * @license
 * Copyright 2022-2026 Matter.js Authors
 * SPDX-License-Identifier: Apache-2.0
 */
const logger = import_general.Logger.get("MdnsClient");
const MDNS_EXPIRY_GRACE_PERIOD_FACTOR = 1.05;
const GOODBYE_PROTECTION_WINDOW = (0, import_general.Millis)(1e3);
const RECORD_MIN_TTL = (0, import_general.Seconds)(15);
const START_ANNOUNCE_INTERVAL = (0, import_general.Seconds)(1.5);
class MdnsClient {
  #lifetime;
  #operationalInitialQueries;
  type = import_general.ChannelType.UDP;
  /** Active announces by queryId with queries and known answers */
  #activeAnnounceQueries = /* @__PURE__ */ new Map();
  /** Known IP addresses by network interface */
  #discoveredIpRecords = /* @__PURE__ */ new Map();
  /** Known operational device records by Matter Qname */
  #operationalDeviceRecords = /* @__PURE__ */ new Map();
  /** Known commissionable device records by queryId */
  #commissionableDeviceRecords = /* @__PURE__ */ new Map();
  /** Waiters for specific queryIds to resolve a promise when a record is discovered */
  #recordWaiters = /* @__PURE__ */ new Map();
  #queryTimer;
  #queryCounter = 0;
  #nextAnnounceInterval = START_ANNOUNCE_INTERVAL;
  #periodicTimer;
  #closing = false;
  #socket;
  #targetCriteriaProviders = new import_general.BasicSet();
  #scanForCommissionableDevices = false;
  #hasCommissionableWaiters = false;
  #operationalScanTargets = /* @__PURE__ */ new Set();
  #observers = new import_general.ObserverGroup();
  /** True, if any node is interested in MDNS traffic, else we ignore all traffic */
  #listening = false;
  constructor(socket, lifetime = import_general.Lifetime.process) {
    this.#lifetime = lifetime.join("mdns client");
    this.#socket = socket;
    this.#observers.on(this.#socket.receipt, this.#handleMessage.bind(this));
    this.#periodicTimer = import_general.Time.getPeriodicTimer(
      "Discovered node expiration",
      import_general.Minutes.one,
      () => this.#expire()
    ).start();
    this.#observers.on(this.#targetCriteriaProviders.added, () => this.#handleChangedScanTargets());
    this.#observers.on(this.#targetCriteriaProviders.deleted, () => this.#handleChangedScanTargets());
  }
  /** Set to add or delete criteria for MDNS discovery */
  get targetCriteriaProviders() {
    return this.#targetCriteriaProviders;
  }
  #handleChangedScanTargets() {
    this.#updateScanTargets();
    logger.info(
      "MDNS Scan targets updated :",
      import_general.Diagnostic.dict({
        commissionable: this.#scanForCommissionableDevices,
        operational: this.#operationalScanTargets
      })
    );
  }
  /** Update the MDNS scan criteria state and collect the desired operational targets */
  #updateScanTargets() {
    if (this.#closing) {
      return;
    }
    const formerTargets = new Set(this.#operationalScanTargets);
    const initialSendQueries = /* @__PURE__ */ new Set();
    this.#operationalScanTargets.clear();
    let cacheCommissionableDevices = false;
    for (const criteria of this.#targetCriteriaProviders) {
      const { operationalTargets, commissionable } = criteria;
      cacheCommissionableDevices = cacheCommissionableDevices || commissionable;
      for (const { fabricId, nodeId } of operationalTargets) {
        let target;
        let scanTarget;
        if (nodeId === void 0) {
          target = import_types.GlobalFabricId.strOf(fabricId).toUpperCase();
          scanTarget = (0, import_MdnsConsts.getFabricQname)(fabricId);
        } else {
          target = `${import_types.GlobalFabricId.strOf(fabricId)}-${import_types.NodeId.strOf(nodeId)}`.toUpperCase();
          scanTarget = (0, import_MdnsConsts.getOperationalDeviceQname)(fabricId, nodeId);
        }
        this.#operationalScanTargets.add(target);
        if (!formerTargets.has(target)) {
          initialSendQueries.add(scanTarget);
        }
      }
    }
    this.#scanForCommissionableDevices = cacheCommissionableDevices;
    for (const queryId of this.#recordWaiters.keys()) {
      this.#registerOperationalQuery(queryId);
    }
    this.#updateListeningStatus();
    if (initialSendQueries.size > 0) {
      logger.debug(
        `Sending initial operational mDNS queries for ${initialSendQueries.size} new operational targets`
      );
      if (this.#operationalInitialQueries === void 0) {
        this.#operationalInitialQueries = new import_general.BasicMultiplex();
      }
      this.#operationalInitialQueries.add(
        this.#socket.send({
          messageType: import_general.DnsMessageType.Query,
          queries: Array.from(initialSendQueries.values()).map((target) => ({
            name: target,
            recordClass: import_general.DnsRecordClass.IN,
            recordType: import_general.DnsRecordType.PTR
          }))
        })
      );
    }
  }
  /** Update the status if we care about MDNS messages or not */
  #updateListeningStatus() {
    const formerListenStatus = this.#listening;
    this.#listening = this.#scanForCommissionableDevices || this.#operationalScanTargets.size > 0 || this.#recordWaiters.size > 0 || this.#activeAnnounceQueries.size > 0;
    if (!this.#listening) {
      this.#discoveredIpRecords.clear();
      this.#operationalDeviceRecords.clear();
      this.#commissionableDeviceRecords.clear();
    }
    if (this.#listening !== formerListenStatus) {
      logger.debug(`MDNS Scanner ${this.#listening ? "started" : "stopped"} listening for MDNS messages`);
    }
  }
  #effectiveTTL(ttl) {
    return (0, import_general.Millis)(ttl * MDNS_EXPIRY_GRACE_PERIOD_FACTOR);
  }
  /**
   * Sends out one DNS-SD query for all collected announce records and start a timer for the next query with doubled
   * interval, maximum 60min, as per MDNS specs. The already known answers are tried to be sent in the query as long
   * as they fit into a maximum 1500 byte long packet (as defined in MDNS specs), else they are split into more
   * packets and the query is sent as Truncated query.
   */
  async #sendQueries() {
    this.#queryTimer?.stop();
    const allQueries = Array.from(this.#activeAnnounceQueries.values());
    const queries = allQueries.flatMap(({ queries: queries2 }) => queries2);
    const answers = allQueries.flatMap(
      ({ answers: answers2 }) => Object.values(answers2).flatMap(
        (answer) => Object.values(answer).flatMap((records) => Array.isArray(records) ? records : records.values())
      )
    );
    this.#queryTimer = import_general.Time.getTimer(
      "MDNS discovery",
      this.#nextAnnounceInterval,
      () => this.#sendQueries()
    ).start();
    logger.debug(
      `Sending ${queries.length} query records for ${this.#activeAnnounceQueries.size} queries with ${answers.length} known answers. Re-Announce in ${import_general.Duration.format(this.#nextAnnounceInterval)}`
    );
    const nextAnnounceInterval = (0, import_general.Millis)(this.#nextAnnounceInterval * 2);
    this.#nextAnnounceInterval = import_general.Duration.min(nextAnnounceInterval, import_general.Hours.one);
    await this.#socket.send({
      messageType: import_general.DnsMessageType.Query,
      queries,
      answers
    });
  }
  /**
   * Set new DnsQuery records to the list of active queries to discover devices in the network and start sending them
   * out. When an entry already exists, the query is overwritten and answers are always added.
   */
  #setQueryRecords(queryId, queries, answers = {}) {
    const activeExistingQuery = this.#activeAnnounceQueries.get(queryId);
    if (activeExistingQuery) {
      const { queries: existingQueries } = activeExistingQuery;
      const newQueries = queries.filter(
        (query) => !existingQueries.some(
          (existingQuery) => existingQuery.name === query.name && existingQuery.recordType === query.recordType && existingQuery.recordClass === query.recordClass
        )
      );
      if (newQueries.length === 0) {
        return false;
      }
      queries = [...newQueries, ...existingQueries];
      answers = this.#combineStructuredAnswers(activeExistingQuery.answers, answers);
    }
    this.#activeAnnounceQueries.set(queryId, { queries, answers });
    logger.debug(`Set ${queries.length} query records for query ${queryId}: ${import_general.Diagnostic.json(queries)}`);
    this.#queryTimer?.stop();
    this.#nextAnnounceInterval = START_ANNOUNCE_INTERVAL;
    this.#queryTimer = import_general.Time.getTimer("MDNS discovery", import_general.Instant, () => this.#sendQueries()).start();
    return true;
  }
  /**
   * Combines the known answers from all active queries and the known IP addresses from the network
   * interface into one data package
   */
  #getActiveQueryEarlierAnswers(netInterface) {
    return this.#combineStructuredAnswers(
      ...[...this.#activeAnnounceQueries.values()].map(({ answers }) => answers),
      this.#discoveredIpRecords.get(netInterface) ?? {}
    );
  }
  /**
   * Remove a query from the list of active queries because discovery has finished or timed out and stop sending it
   * out. If it was the last query announcing will stop completely.
   */
  #removeQuery(queryId) {
    this.#activeAnnounceQueries.delete(queryId);
    if (this.#activeAnnounceQueries.size === 0) {
      logger.debug(`Removing last query ${queryId} and stopping announce timer`);
      this.#queryTimer?.stop();
      this.#nextAnnounceInterval = START_ANNOUNCE_INTERVAL;
    } else {
      logger.debug(`Removing query ${queryId}`);
    }
  }
  /**
   * Returns the list of all targets (IP/port) discovered for a queried operational device record.
   */
  #getOperationalDeviceRecords(deviceMatterQname) {
    const device = this.#operationalDeviceRecords.get(deviceMatterQname);
    if (device === void 0) {
      return void 0;
    }
    const { addresses } = device;
    if (addresses.size === 0) {
      return void 0;
    }
    return {
      ...device,
      addresses: this.#sortServerEntries(Array.from(addresses.values())).map(({ ip, port }) => ({
        ip,
        port,
        type: "udp"
      }))
    };
  }
  /**
   * Sort the list of found IP/ports and make sure link-local IPv6 addresses come first, IPv6 next and IPv4 last.
   *
   * @param entries
   */
  #sortServerEntries(entries) {
    return entries.sort((a, b) => {
      const aIsIPv6 = (0, import_general.isIPv6)(a.ip);
      const bIsIPv6 = (0, import_general.isIPv6)(b.ip);
      if (aIsIPv6 && !bIsIPv6) {
        return -1;
      } else if (!aIsIPv6 && bIsIPv6) {
        return 1;
      } else if (aIsIPv6 && bIsIPv6) {
        if (a.ip.startsWith("fd") && !b.ip.startsWith("fd")) {
          return -1;
        } else if (!a.ip.startsWith("fd") && b.ip.startsWith("fd")) {
          return 1;
        } else if (a.ip.startsWith("fe80:") && !b.ip.startsWith("fe80:")) {
          return -1;
        } else if (!a.ip.startsWith("fe80:") && b.ip.startsWith("fe80:")) {
          return 1;
        }
      }
      return 0;
    });
  }
  #registerOperationalQuery(queryId) {
    const separator = queryId.indexOf(".");
    if (separator !== -1) {
      this.#operationalScanTargets.add(queryId.substring(0, separator));
    } else {
      throw new import_general.InternalError(`Invalid queryId ${queryId} for operational device, no separator found`);
    }
  }
  /**
   * Registers a deferred promise for a specific queryId together with a timeout and return the promise.
   * The promise will be resolved when the timer runs out latest.
   */
  async #registerWaiterPromise(queryId, commissionable, timeout, responder, resolveOnUpdatedRecords = true, cancelResolver) {
    const id = this.#queryCounter = (this.#queryCounter + 1) % 4294967295;
    const { promise, resolver } = (0, import_general.createPromise)();
    const timer = timeout !== void 0 ? import_general.Time.getTimer("MDNS timeout", timeout, () => {
      cancelResolver?.();
      this.#finishWaiter(queryId, true, false, id);
    }).start() : void 0;
    this.#listening = true;
    const waiters = this.#recordWaiters.get(queryId) ?? [];
    waiters.push({
      id,
      resolver,
      responder,
      timer,
      resolveOnUpdatedRecords,
      cancelResolver,
      commissionable
    });
    this.#recordWaiters.set(queryId, waiters);
    this.#hasCommissionableWaiters = this.#hasCommissionableWaiters || commissionable;
    if (!commissionable) {
      this.#registerOperationalQuery(queryId);
    }
    logger.info(
      `Registered waiter for query ${queryId} (${id}) with ${timeout !== void 0 ? `timeout ${timeout}` : "no timeout"}${resolveOnUpdatedRecords ? "" : " (not resolving on updated records)"}`
    );
    return await promise;
  }
  /**
   * Remove a waiter promise for a specific queryId and stop the connected timer. If required, also resolve the
   * promise.
   */
  #finishWaiter(queryId, resolvePromise, isUpdatedRecord = false, finishId) {
    const waiters = this.#recordWaiters.get(queryId);
    if (waiters === void 0) {
      return;
    }
    const waitersLeft = new Array();
    let commissionableRecordFinished = false;
    for (const waiter of waiters) {
      if (finishId !== void 0 && waiter.id !== finishId) {
        waitersLeft.push(waiter);
        continue;
      }
      const { timer, resolver, responder, resolveOnUpdatedRecords, commissionable } = waiter;
      if (isUpdatedRecord && !resolveOnUpdatedRecords) {
        waitersLeft.push(waiter);
        continue;
      }
      logger.info(`Finishing waiter for query ${queryId} (${waiter.id}), resolving: ${resolvePromise}`);
      commissionableRecordFinished = commissionableRecordFinished || commissionable;
      timer?.stop();
      if (resolvePromise) {
        resolver(responder?.());
      }
    }
    if (waitersLeft.length !== 0) {
      this.#recordWaiters.set(queryId, waitersLeft);
      return;
    }
    this.#recordWaiters.delete(queryId);
    this.#removeQuery(queryId);
    if (!this.#closing) {
      this.#hasCommissionableWaiters = false;
      let hasOperationalWaiters = false;
      loop: for (const waiters2 of this.#recordWaiters.values()) {
        for (const { commissionable } of waiters2) {
          if (commissionable) {
            this.#hasCommissionableWaiters = true;
            if (hasOperationalWaiters) {
              break loop;
            }
          } else {
            hasOperationalWaiters = true;
            if (this.#hasCommissionableWaiters) {
              break loop;
            }
          }
        }
      }
      if (!commissionableRecordFinished) {
        this.#updateScanTargets();
      } else {
        this.#updateListeningStatus();
      }
    }
  }
  /** Returns weather a waiter promise is registered for a specific queryId. */
  #hasWaiter(queryId) {
    return this.#recordWaiters.has(queryId);
  }
  /**
   * Method to find an operational device (already commissioned) and return a promise with the list of discovered
   * IP/ports or an empty array if not found.
   */
  async findOperationalDevice(fabric, nodeId, timeout, ignoreExistingRecords = false) {
    if (this.#closing) {
      throw new import_general.ImplementationError("Cannot discover operational device because scanner is closing.");
    }
    const deviceMatterQname = (0, import_MdnsConsts.getOperationalDeviceQname)(fabric.globalId, nodeId);
    let storedDevice = this.#getOperationalDeviceRecords(deviceMatterQname);
    if (storedDevice === void 0 || ignoreExistingRecords) {
      var _stack = [];
      try {
        const _finding = __using(_stack, this.#lifetime.join(
          "finding peer",
          (0, import_PeerAddress.PeerAddress)({ fabricIndex: fabric.fabricIndex, nodeId })
        ));
        if (storedDevice) {
          const device = this.#operationalDeviceRecords.get(deviceMatterQname);
          device?.addresses.clear();
        }
        const promise = this.#registerWaiterPromise(
          deviceMatterQname,
          false,
          timeout,
          () => this.#getOperationalDeviceRecords(deviceMatterQname)
        );
        this.#setQueryRecords(deviceMatterQname, [
          {
            name: deviceMatterQname,
            recordClass: import_general.DnsRecordClass.IN,
            recordType: import_general.DnsRecordType.SRV
          }
        ]);
        storedDevice = await promise;
      } catch (_) {
        var _error = _, _hasError = true;
      } finally {
        __callDispose(_stack, _error, _hasError);
      }
    }
    return storedDevice;
  }
  cancelOperationalDeviceDiscovery(fabric, nodeId, resolvePromise = true) {
    const deviceMatterQname = (0, import_MdnsConsts.getOperationalDeviceQname)(fabric.globalId, nodeId);
    this.#finishWaiter(deviceMatterQname, resolvePromise);
  }
  cancelCommissionableDeviceDiscovery(identifier, resolvePromise = true) {
    const queryId = this.#buildCommissionableQueryIdentifier(identifier);
    const waiters = this.#recordWaiters.get(queryId) ?? [];
    for (const { cancelResolver } of waiters) {
      cancelResolver?.();
    }
    this.#finishWaiter(queryId, resolvePromise);
  }
  getDiscoveredOperationalDevice({ globalId }, nodeId) {
    return this.#getOperationalDeviceRecords((0, import_MdnsConsts.getOperationalDeviceQname)(globalId, nodeId));
  }
  /**
   * Returns the metadata and list of all target addresses (IP/port) discovered for a queried commissionable device
   * record.
   */
  #getCommissionableDeviceRecords(identifier) {
    const storedRecords = Array.from(this.#commissionableDeviceRecords.values());
    const foundRecords = new Array();
    if ("instanceId" in identifier) {
      foundRecords.push(...storedRecords.filter(({ instanceId }) => instanceId === identifier.instanceId));
    } else if ("longDiscriminator" in identifier) {
      foundRecords.push(...storedRecords.filter(({ D }) => D === identifier.longDiscriminator));
    } else if ("shortDiscriminator" in identifier) {
      foundRecords.push(...storedRecords.filter(({ SD }) => SD === identifier.shortDiscriminator));
    } else if ("vendorId" in identifier && "productId" in identifier) {
      foundRecords.push(
        ...storedRecords.filter(({ V, P }) => V === identifier.vendorId && P === identifier.productId)
      );
    } else if ("vendorId" in identifier) {
      foundRecords.push(...storedRecords.filter(({ V }) => V === identifier.vendorId));
    } else if ("deviceType" in identifier) {
      foundRecords.push(...storedRecords.filter(({ DT }) => DT === identifier.deviceType));
    } else if ("productId" in identifier) {
      foundRecords.push(...storedRecords.filter(({ P }) => P === identifier.productId));
    } else if (Object.keys(identifier).length === 0) {
      foundRecords.push(...storedRecords.filter(({ CM }) => CM === 1 || CM === 2));
    }
    return foundRecords.filter(({ addresses }) => addresses.size > 0).map((record) => {
      return {
        ...record,
        addresses: this.#sortServerEntries(Array.from(record.addresses.values())).map(({ ip, port }) => ({
          ip,
          port,
          type: "udp"
        })),
        discoveredAt: void 0,
        ttl: void 0
      };
    });
  }
  /**
   * Builds an identifier string for commissionable queries based on the given identifier object.
   * Some identifiers are identical to the official DNS-SD identifiers, others are custom.
   */
  #buildCommissionableQueryIdentifier(identifier) {
    if ("instanceId" in identifier) {
      return (0, import_MdnsConsts.getCommissionableDeviceQname)(identifier.instanceId);
    }
    if ("longDiscriminator" in identifier) {
      return (0, import_MdnsConsts.getLongDiscriminatorQname)(identifier.longDiscriminator);
    }
    if ("shortDiscriminator" in identifier) {
      return (0, import_MdnsConsts.getShortDiscriminatorQname)(identifier.shortDiscriminator);
    }
    if ("vendorId" in identifier && "productId" in identifier) {
      return `_VP${identifier.vendorId}+${identifier.productId}`;
    }
    if ("vendorId" in identifier) {
      return (0, import_MdnsConsts.getVendorQname)(identifier.vendorId);
    }
    if ("deviceType" in identifier) {
      return (0, import_MdnsConsts.getDeviceTypeQname)(identifier.deviceType);
    }
    if ("productId" in identifier) {
      return `_P${identifier.productId}`;
    }
    return (0, import_MdnsConsts.getCommissioningModeQname)();
  }
  #extractInstanceId(instanceName) {
    const instanceNameSeparator = instanceName.indexOf(".");
    if (instanceNameSeparator !== -1) {
      return instanceName.substring(0, instanceNameSeparator);
    }
    return instanceName;
  }
  /**
   * Check all options for a query identifier and return the most relevant one with an active query
   */
  #findCommissionableQueryIdentifier(instanceName, record) {
    if (this.#closing) {
      throw new import_general.ImplementationError("Cannot discover commissionable device because scanner is closing.");
    }
    const instanceQueryId = this.#buildCommissionableQueryIdentifier({
      instanceId: this.#extractInstanceId(instanceName)
    });
    if (this.#activeAnnounceQueries.has(instanceQueryId)) {
      return instanceQueryId;
    }
    const longDiscriminatorQueryId = this.#buildCommissionableQueryIdentifier({ longDiscriminator: record.D });
    if (this.#activeAnnounceQueries.has(longDiscriminatorQueryId)) {
      return longDiscriminatorQueryId;
    }
    const shortDiscriminatorQueryId = this.#buildCommissionableQueryIdentifier({ shortDiscriminator: record.SD });
    if (this.#activeAnnounceQueries.has(shortDiscriminatorQueryId)) {
      return shortDiscriminatorQueryId;
    }
    if (record.V !== void 0 && record.P !== void 0) {
      const vendorProductIdQueryId = this.#buildCommissionableQueryIdentifier({
        vendorId: (0, import_types.VendorId)(record.V),
        productId: record.P
      });
      if (this.#activeAnnounceQueries.has(vendorProductIdQueryId)) {
        return vendorProductIdQueryId;
      }
    }
    if (record.V !== void 0) {
      const vendorIdQueryId = this.#buildCommissionableQueryIdentifier({ vendorId: (0, import_types.VendorId)(record.V) });
      if (this.#activeAnnounceQueries.has(vendorIdQueryId)) {
        return vendorIdQueryId;
      }
    }
    if (record.DT !== void 0) {
      const deviceTypeQueryId = this.#buildCommissionableQueryIdentifier({ deviceType: record.DT });
      if (this.#activeAnnounceQueries.has(deviceTypeQueryId)) {
        return deviceTypeQueryId;
      }
    }
    if (record.P !== void 0) {
      const productIdQueryId = this.#buildCommissionableQueryIdentifier({ productId: record.P });
      if (this.#activeAnnounceQueries.has(productIdQueryId)) {
        return productIdQueryId;
      }
    }
    const commissioningModeQueryId = this.#buildCommissionableQueryIdentifier({});
    if (this.#activeAnnounceQueries.has(commissioningModeQueryId)) {
      return commissioningModeQueryId;
    }
    return void 0;
  }
  #getCommissionableQueryRecords(identifier) {
    const names = new Array();
    names.push(import_MdnsConsts.MATTER_COMMISSION_SERVICE_QNAME);
    if ("instanceId" in identifier) {
      names.push((0, import_MdnsConsts.getCommissionableDeviceQname)(identifier.instanceId));
    } else if ("longDiscriminator" in identifier) {
      names.push((0, import_MdnsConsts.getLongDiscriminatorQname)(identifier.longDiscriminator));
    } else if ("shortDiscriminator" in identifier) {
      names.push((0, import_MdnsConsts.getShortDiscriminatorQname)(identifier.shortDiscriminator));
    } else if ("vendorId" in identifier) {
      names.push((0, import_MdnsConsts.getVendorQname)(identifier.vendorId));
    } else if ("deviceType" in identifier) {
      names.push((0, import_MdnsConsts.getDeviceTypeQname)(identifier.deviceType));
    } else {
      names.push((0, import_MdnsConsts.getCommissioningModeQname)());
    }
    return names.map((name) => ({ name, recordClass: import_general.DnsRecordClass.IN, recordType: import_general.DnsRecordType.PTR }));
  }
  /**
   * Discovers commissionable devices based on a defined identifier for maximal given timeout, but returns the
   * first found entries. If already a discovered device matches in the cache the response is returned directly and
   * no query is triggered. If no record exists a query is sent out and the promise gets fulfilled as soon as at least
   * one device is found. If no device is discovered in the defined timeframe an empty array is returned. When the
   * promise got fulfilled no more queries are send out, but more device entries might be added when discovered later.
   * These can be requested by the getCommissionableDevices method.
   */
  async findCommissionableDevices(identifier, timeout = (0, import_general.Seconds)(5), ignoreExistingRecords = false) {
    let storedRecords = ignoreExistingRecords ? [] : this.#getCommissionableDeviceRecords(identifier).filter(({ addresses }) => addresses.length > 0);
    if (storedRecords.length === 0) {
      var _stack = [];
      try {
        const finding = __using(_stack, this.#lifetime.join("finding commissionable"));
        Object.assign(finding.details, identifier);
        const queryId = this.#buildCommissionableQueryIdentifier(identifier);
        const promise = this.#registerWaiterPromise(
          queryId,
          true,
          timeout,
          () => this.#getCommissionableDeviceRecords(identifier)
        );
        this.#setQueryRecords(queryId, this.#getCommissionableQueryRecords(identifier));
        storedRecords = await promise;
      } catch (_) {
        var _error = _, _hasError = true;
      } finally {
        __callDispose(_stack, _error, _hasError);
      }
    }
    return storedRecords;
  }
  /**
   * Discovers commissionable devices based on a defined identifier and returns the first found entries.
   * If an own cancelSignal promise is used the discovery can only be cancelled via this signal!
   */
  async findCommissionableDevicesContinuously(identifier, callback, timeout, cancelSignal) {
    var _stack = [];
    try {
      const discoveredDevices = /* @__PURE__ */ new Set();
      const discoveryEndTime = timeout ? import_general.Time.nowMs + timeout : void 0;
      const queryId = this.#buildCommissionableQueryIdentifier(identifier);
      this.#setQueryRecords(queryId, this.#getCommissionableQueryRecords(identifier));
      let queryResolver;
      if (cancelSignal === void 0) {
        const { promise, resolver } = (0, import_general.createPromise)();
        cancelSignal = promise;
        queryResolver = resolver;
      }
      let canceled = false;
      cancelSignal?.then(
        () => {
          canceled = true;
          this.#finishWaiter(queryId, true);
        },
        (cause) => {
          logger.warn("Unexpected error canceling commissioning", cause);
        }
      );
      const criteria = { commissionable: true, operationalTargets: [] };
      this.targetCriteriaProviders.add(criteria);
      const finding = __using(_stack, this.#lifetime.join("finding commissionable"));
      Object.assign(finding.details, identifier);
      try {
        let lastDiscoveredDevices = void 0;
        while (!canceled) {
          this.#getCommissionableDeviceRecords(identifier).forEach((device) => {
            const { deviceIdentifier } = device;
            if (!discoveredDevices.has(deviceIdentifier)) {
              discoveredDevices.add(deviceIdentifier);
              callback(device);
            }
          });
          let remainingTime;
          if (discoveryEndTime !== void 0) {
            remainingTime = import_general.Seconds.ceil((0, import_general.Timespan)(import_general.Time.nowMs, discoveryEndTime).duration);
            if (remainingTime <= 0) {
              break;
            }
          }
          lastDiscoveredDevices = await this.#registerWaiterPromise(
            queryId,
            true,
            remainingTime,
            () => this.#getCommissionableDeviceRecords(identifier),
            false,
            queryResolver
          );
        }
        return lastDiscoveredDevices ?? this.#getCommissionableDeviceRecords(identifier);
      } finally {
        this.targetCriteriaProviders.delete(criteria);
      }
    } catch (_) {
      var _error = _, _hasError = true;
    } finally {
      __callDispose(_stack, _error, _hasError);
    }
  }
  getDiscoveredCommissionableDevices(identifier) {
    return this.#getCommissionableDeviceRecords(identifier);
  }
  /**
   * Close all connections, end all timers, and resolve all pending promises.
   */
  async close() {
    var _stack = [];
    try {
      const _closing = __using(_stack, this.#lifetime.closing());
      this.#closing = true;
      this.#observers.close();
      this.#periodicTimer.stop();
      this.#queryTimer?.stop();
      await this.#operationalInitialQueries?.close();
      [...this.#recordWaiters.entries()].forEach(([queryId, waiters]) => {
        for (const { timer, id } of waiters) {
          this.#finishWaiter(queryId, !!timer, false, id);
        }
      });
    } catch (_) {
      var _error = _, _hasError = true;
    } finally {
      __callDispose(_stack, _error, _hasError);
    }
  }
  /** Converts the discovery data into a structured format for performant access. */
  #structureAnswers(...answersList) {
    const structuredAnswers = {};
    const discoveredAt = import_general.Time.nowMs;
    answersList.forEach(
      (answers) => answers.forEach((answer) => {
        const { name, recordType } = answer;
        if (answer.ttl > 0 && answer.ttl < RECORD_MIN_TTL) {
          answer = { ...answer, ttl: RECORD_MIN_TTL };
        }
        if (name.endsWith(import_MdnsConsts.MATTER_SERVICE_QNAME)) {
          structuredAnswers.operational = structuredAnswers.operational ?? {};
          structuredAnswers.operational[recordType] = structuredAnswers.operational[recordType] ?? [];
          structuredAnswers.operational[recordType].push({
            discoveredAt,
            ...answer
          });
        } else if (name.endsWith(import_MdnsConsts.MATTER_COMMISSION_SERVICE_QNAME)) {
          structuredAnswers.commissionable = structuredAnswers.commissionable ?? {};
          structuredAnswers.commissionable[recordType] = structuredAnswers.commissionable[recordType] ?? [];
          structuredAnswers.commissionable[recordType].push({
            discoveredAt,
            ...answer
          });
        } else if (recordType === import_general.DnsRecordType.AAAA) {
          structuredAnswers.addressesV6 = structuredAnswers.addressesV6 ?? {};
          structuredAnswers.addressesV6[name] = structuredAnswers.addressesV6[name] ?? /* @__PURE__ */ new Map();
          structuredAnswers.addressesV6[name].set(answer.value, {
            discoveredAt,
            ...answer
          });
        } else if (this.#socket.supportsIpv4 && recordType === import_general.DnsRecordType.A) {
          structuredAnswers.addressesV4 = structuredAnswers.addressesV4 ?? {};
          structuredAnswers.addressesV4[name] = structuredAnswers.addressesV4[name] ?? /* @__PURE__ */ new Map();
          structuredAnswers.addressesV4[name].set(answer.value, {
            discoveredAt,
            ...answer
          });
        }
      })
    );
    return structuredAnswers;
  }
  /**
   * Merge a record into a map with goodbye protection.
   * Returns true if the record was processed (added or deleted), false if skipped due to protection.
   */
  #mergeRecordWithGoodbyeProtection(targetMap, key, record, now) {
    const existingRecord = targetMap.get(key);
    if (!existingRecord || existingRecord.discoveredAt < record.discoveredAt) {
      if (record.ttl === 0) {
        if (existingRecord && now - existingRecord.discoveredAt < GOODBYE_PROTECTION_WINDOW) {
          return;
        }
        targetMap.delete(key);
      } else {
        targetMap.set(key, record);
      }
    }
  }
  #combineStructuredAnswers(...answersList) {
    const combinedAnswers = {};
    const now = import_general.Time.nowMs;
    for (const answers of answersList) {
      if (answers.operational) {
        combinedAnswers.operational = combinedAnswers.operational ?? {};
        for (const [recordType, records] of Object.entries(answers.operational)) {
          combinedAnswers.operational[recordType] = combinedAnswers.operational[recordType] ?? /* @__PURE__ */ new Map();
          for (const record of records) {
            this.#mergeRecordWithGoodbyeProtection(
              combinedAnswers.operational[recordType],
              record.name,
              record,
              now
            );
          }
        }
      }
      if (answers.commissionable) {
        combinedAnswers.commissionable = combinedAnswers.commissionable ?? {};
        for (const [recordType, records] of Object.entries(answers.commissionable)) {
          combinedAnswers.commissionable[recordType] = combinedAnswers.commissionable[recordType] ?? /* @__PURE__ */ new Map();
          for (const record of records) {
            this.#mergeRecordWithGoodbyeProtection(
              combinedAnswers.commissionable[recordType],
              record.name,
              record,
              now
            );
          }
        }
      }
      if (answers.addressesV6) {
        combinedAnswers.addressesV6 = combinedAnswers.addressesV6 ?? {};
        for (const [name, records] of Object.entries(answers.addressesV6)) {
          combinedAnswers.addressesV6[name] = combinedAnswers.addressesV6[name] ?? /* @__PURE__ */ new Map();
          for (const record of records.values()) {
            this.#mergeRecordWithGoodbyeProtection(
              combinedAnswers.addressesV6[name],
              record.value,
              record,
              now
            );
          }
        }
      }
      if (this.#socket.supportsIpv4 && answers.addressesV4) {
        combinedAnswers.addressesV4 = combinedAnswers.addressesV4 ?? {};
        for (const [name, records] of Object.entries(answers.addressesV4)) {
          combinedAnswers.addressesV4[name] = combinedAnswers.addressesV4[name] ?? /* @__PURE__ */ new Map();
          for (const record of records.values()) {
            this.#mergeRecordWithGoodbyeProtection(
              combinedAnswers.addressesV4[name],
              record.value,
              record,
              now
            );
          }
        }
      }
    }
    const result = {};
    if (combinedAnswers.operational) {
      result.operational = Object.fromEntries(
        Object.entries(combinedAnswers.operational).map(([recordType, records]) => [
          recordType,
          Array.from(records.values())
        ])
      );
    }
    if (combinedAnswers.commissionable) {
      result.commissionable = Object.fromEntries(
        Object.entries(combinedAnswers.commissionable).map(([recordType, records]) => [
          recordType,
          Array.from(records.values())
        ])
      );
    }
    if (combinedAnswers.addressesV6) {
      result.addressesV6 = combinedAnswers.addressesV6;
    }
    if (this.#socket.supportsIpv4 && combinedAnswers.addressesV4) {
      result.addressesV4 = combinedAnswers.addressesV4;
    }
    return result;
  }
  /**
   * Main method to handle all incoming DNS messages.
   * It will parse the message and check if it contains relevant discovery records.
   */
  #handleMessage(message) {
    if (this.#closing) return;
    if (message === void 0) return;
    if (!import_general.DnsMessageType.isResponse(message.messageType)) return;
    const answers = this.#structureAnswers([...message.answers, ...message.additionalRecords]);
    const formerAnswers = this.#getActiveQueryEarlierAnswers(message.sourceIntf);
    this.#handleOperationalRecords(answers, formerAnswers, message.sourceIntf);
    this.#handleCommissionableRecords(answers, formerAnswers, message.sourceIntf);
    this.#updateIpRecords(answers, message.sourceIntf);
  }
  /**
   * Update IP address records in a target map with goodbye protection.
   * Returns true if any records were updated.
   */
  #updateIpAddressRecords(sourceAddresses, targetAddresses, now) {
    if (!sourceAddresses || !targetAddresses) {
      return false;
    }
    let updated = false;
    for (const [target, ipAddresses] of Object.entries(sourceAddresses)) {
      const targetMap = targetAddresses[target];
      if (targetMap === void 0) {
        continue;
      }
      for (const [ip, record] of Object.entries(ipAddresses)) {
        if (record.ttl === 0) {
          const existingRecord = targetMap.get(ip);
          if (existingRecord) {
            const recordAge = now - existingRecord.discoveredAt;
            if (recordAge < GOODBYE_PROTECTION_WINDOW) {
              continue;
            }
            targetMap.delete(ip);
            updated = true;
          }
        } else {
          targetMap.set(ip, record);
          updated = true;
        }
      }
    }
    return updated;
  }
  /**
   * Update the discovered matter relevant IP records with the new data from the DNS message.
   */
  #updateIpRecords(answers, netInterface) {
    const interfaceRecords = this.#discoveredIpRecords.get(netInterface);
    if (interfaceRecords === void 0) {
      return;
    }
    const now = import_general.Time.nowMs;
    const updatedV6 = this.#updateIpAddressRecords(answers.addressesV6, interfaceRecords.addressesV6, now);
    const updatedV4 = this.#socket.supportsIpv4 && this.#updateIpAddressRecords(answers.addressesV4, interfaceRecords.addressesV4, now);
    if (updatedV6 || updatedV4) {
      this.#discoveredIpRecords.set(netInterface, interfaceRecords);
    }
  }
  /**
   * Register Matter relevant IP records for later usage.
   */
  #registerIpRecords(ipAddresses, netInterface) {
    const interfaceRecords = this.#discoveredIpRecords.get(netInterface) ?? {};
    for (const record of ipAddresses) {
      const { recordType, name, value: ip, ttl } = record;
      if (ttl === 0) continue;
      if (recordType === import_general.DnsRecordType.AAAA) {
        interfaceRecords.addressesV6 = interfaceRecords.addressesV6 ?? {};
        interfaceRecords.addressesV6[name] = interfaceRecords.addressesV6[name] ?? /* @__PURE__ */ new Map();
        interfaceRecords.addressesV6[name].set(ip, record);
      } else if (this.#socket.supportsIpv4 && recordType === import_general.DnsRecordType.A) {
        interfaceRecords.addressesV4 = interfaceRecords.addressesV4 ?? {};
        interfaceRecords.addressesV4[name] = interfaceRecords.addressesV4[name] ?? /* @__PURE__ */ new Map();
        interfaceRecords.addressesV4[name].set(ip, record);
      }
    }
    this.#discoveredIpRecords.set(netInterface, interfaceRecords);
  }
  #handleIpRecords(answers, target, netInterface) {
    const ipRecords = new Array();
    answers.forEach((answer) => {
      if (answer.addressesV6?.[target]) {
        ipRecords.push(...answer.addressesV6[target].values());
      }
      if (this.#socket.supportsIpv4 && answer.addressesV4?.[target]) {
        ipRecords.push(...answer.addressesV4[target].values());
      }
    });
    if (ipRecords.length === 0) {
      return [];
    }
    this.#registerIpRecords(ipRecords, netInterface);
    const collectedIps = /* @__PURE__ */ new Map();
    ipRecords.forEach((record) => {
      const { value, ttl } = record;
      if (value.startsWith("fe80::")) {
        collectedIps.set(value, { value: `${value}%${netInterface}`, ttl });
      } else {
        collectedIps.set(value, { value, ttl });
      }
    });
    return [...collectedIps.values()];
  }
  #handleOperationalRecords(answers, formerAnswers, netInterface) {
    if (!answers.operational) return;
    const operationalTxtRecords = answers.operational[import_general.DnsRecordType.TXT] ?? [];
    operationalTxtRecords.forEach((record) => this.#handleOperationalTxtRecord(record, netInterface));
    let operationalSrvRecords = answers.operational[import_general.DnsRecordType.SRV] ?? [];
    if (!operationalSrvRecords.length && formerAnswers.operational) {
      operationalSrvRecords = formerAnswers.operational[import_general.DnsRecordType.SRV] ?? [];
    }
    if (operationalSrvRecords.length) {
      operationalSrvRecords.forEach(
        (record) => this.#handleOperationalSrvRecord(record, answers, formerAnswers, netInterface)
      );
    }
  }
  #matchesOperationalCriteria(matterName) {
    const nameParts = matterName.match(/^([\dA-F]{16})-([\dA-F]{16})\._matter\._tcp\.local$/i);
    if (!nameParts) {
      return false;
    }
    const operationalId = nameParts[1];
    const nodeId = nameParts[2];
    return this.#operationalScanTargets.has(operationalId) || this.#operationalScanTargets.has(`${operationalId}-${nodeId}`);
  }
  /**
   * Handle goodbye (TTL=0) for an operational device record with protection against out-of-order packets.
   * Returns true if the goodbye was processed (record deleted or protected), false if no action needed.
   */
  #handleOperationalDeviceGoodbye(matterName, netInterface, now) {
    const existingRecord = this.#operationalDeviceRecords.get(matterName);
    if (!existingRecord) {
      return false;
    }
    const recordAge = now - existingRecord.discoveredAt;
    if (recordAge < GOODBYE_PROTECTION_WINDOW) {
      return true;
    }
    logger.debug(
      `Removing operational device ${matterName} from cache (interface ${netInterface}) because of ttl=0`
    );
    this.#operationalDeviceRecords.delete(matterName);
    return true;
  }
  #handleOperationalTxtRecord(record, netInterface) {
    const { name: matterName, value, ttl } = record;
    const now = import_general.Time.nowMs;
    if (ttl === 0) {
      this.#handleOperationalDeviceGoodbye(matterName, netInterface, now);
      return;
    }
    const discoveredAt = now;
    if (!Array.isArray(value)) return;
    if (!this.#operationalDeviceRecords.has(matterName) && !this.#matchesOperationalCriteria(matterName)) {
      return;
    }
    const txtData = this.#parseTxtRecord(record);
    if (txtData === void 0) return;
    let device = this.#operationalDeviceRecords.get(matterName);
    if (device !== void 0) {
      device = {
        ...device,
        discoveredAt,
        ttl,
        ...txtData
      };
    } else {
      logNewService(matterName, "operational", txtData);
      device = {
        deviceIdentifier: matterName,
        addresses: /* @__PURE__ */ new Map(),
        discoveredAt,
        ttl,
        ...txtData
      };
    }
    this.#operationalDeviceRecords.set(matterName, device);
  }
  #handleOperationalSrvRecord(record, answers, formerAnswers, netInterface) {
    const {
      name: matterName,
      ttl,
      value: { target, port }
    } = record;
    const now = import_general.Time.nowMs;
    if (ttl === 0) {
      this.#handleOperationalDeviceGoodbye(matterName, netInterface, now);
      return;
    }
    const ips = this.#handleIpRecords([formerAnswers, answers], target, netInterface);
    const deviceExisted = this.#operationalDeviceRecords.has(matterName);
    if (!deviceExisted && !this.#matchesOperationalCriteria(matterName)) {
      return;
    }
    const discoveredAt = now;
    const device = this.#operationalDeviceRecords.get(matterName) ?? {
      deviceIdentifier: matterName,
      addresses: /* @__PURE__ */ new Map(),
      discoveredAt,
      ttl
    };
    const ipsInitiallyEmpty = device.addresses.size === 0;
    const { addresses } = device;
    if (ips.length > 0) {
      for (const { value: ip, ttl: ttl2 } of ips) {
        if (ttl2 === 0) {
          const existingAddress = addresses.get(ip);
          if (existingAddress) {
            const addressAge = now - existingAddress.discoveredAt;
            if (addressAge < GOODBYE_PROTECTION_WINDOW) {
              continue;
            }
            logger.debug(
              `Removing IP ${ip} for operational device ${matterName} from cache (interface ${netInterface}) because of ttl=0`
            );
            addresses.delete(ip);
          }
          continue;
        }
        const address = addresses.get(ip) ?? { ip, port, type: "udp" };
        address.discoveredAt = discoveredAt;
        address.ttl = ttl2;
        addresses.set(address.ip, address);
      }
      device.addresses = addresses;
      if (ipsInitiallyEmpty) {
        logNewAddresses(matterName, "operational", netInterface, addresses);
      }
      this.#operationalDeviceRecords.set(matterName, device);
    }
    if (addresses.size === 0 && this.#hasWaiter(matterName)) {
      const queries = [{ name: target, recordClass: import_general.DnsRecordClass.IN, recordType: import_general.DnsRecordType.AAAA }];
      if (this.#socket.supportsIpv4) {
        queries.push({ name: target, recordClass: import_general.DnsRecordClass.IN, recordType: import_general.DnsRecordType.A });
      }
      if (this.#setQueryRecords(matterName, queries, answers)) {
        logger.debug(
          `Requesting IP addresses for operational device ${matterName} (interface ${netInterface}).`
        );
      }
    } else if (addresses.size > 0) {
      this.#finishWaiter(matterName, true, deviceExisted);
    }
  }
  /**
   * Handle goodbye (TTL=0) for a commissionable device record with protection against out-of-order packets.
   * Returns true if the goodbye should be skipped (record is protected), false if processed or no record exists.
   */
  #handleCommissionableDeviceGoodbye(name, netInterface, now) {
    const existingRecord = this.#commissionableDeviceRecords.get(name);
    if (!existingRecord) {
      return false;
    }
    const recordAge = now - existingRecord.discoveredAt;
    if (recordAge < GOODBYE_PROTECTION_WINDOW) {
      return true;
    }
    logger.debug(`Removing commissionable device ${name} from cache (interface ${netInterface}) because of ttl=0`);
    this.#commissionableDeviceRecords.delete(name);
    return false;
  }
  #handleCommissionableRecords(answers, formerAnswers, netInterface) {
    if (!this.#scanForCommissionableDevices && !this.#hasCommissionableWaiters) {
      return;
    }
    let commissionableRecords = answers.commissionable ?? {};
    if (!commissionableRecords[import_general.DnsRecordType.SRV]?.length && !commissionableRecords[import_general.DnsRecordType.TXT]?.length) {
      commissionableRecords = formerAnswers.commissionable ?? {};
      if (!commissionableRecords[import_general.DnsRecordType.SRV]?.length && !commissionableRecords[import_general.DnsRecordType.TXT]?.length)
        return;
    }
    const queryMissingDataForInstances = /* @__PURE__ */ new Set();
    const now = import_general.Time.nowMs;
    const txtRecords = commissionableRecords[import_general.DnsRecordType.TXT] ?? [];
    for (const record of txtRecords) {
      const { name, ttl } = record;
      if (ttl === 0) {
        this.#handleCommissionableDeviceGoodbye(name, netInterface, now);
        continue;
      }
      const txtRecord = this.#parseCommissionableTxtRecord(record);
      if (txtRecord === void 0) continue;
      const instanceId = this.#extractInstanceId(name);
      const parsedRecord = {
        ...txtRecord,
        instanceId,
        deviceIdentifier: instanceId
      };
      if (parsedRecord.D !== void 0 && parsedRecord.SD === void 0) {
        parsedRecord.SD = parsedRecord.D >> 8 & 15;
      }
      if (parsedRecord.VP !== void 0) {
        const VpValueArr = parsedRecord.VP.split("+");
        parsedRecord.V = VpValueArr[0] !== void 0 ? parseInt(VpValueArr[0]) : void 0;
        parsedRecord.P = VpValueArr[1] !== void 0 ? parseInt(VpValueArr[1]) : void 0;
      }
      const storedRecord = this.#commissionableDeviceRecords.get(name);
      if (storedRecord === void 0) {
        queryMissingDataForInstances.add(name);
        logNewService(name, "commissionable", parsedRecord);
      } else {
        parsedRecord.addresses = storedRecord.addresses;
      }
      this.#commissionableDeviceRecords.set(name, parsedRecord);
    }
    const srvRecords = commissionableRecords[import_general.DnsRecordType.SRV] ?? [];
    for (const record of srvRecords) {
      const storedRecord = this.#commissionableDeviceRecords.get(record.name);
      if (storedRecord === void 0) continue;
      const {
        value: { target, port },
        ttl
      } = record;
      if (ttl === 0) {
        this.#handleCommissionableDeviceGoodbye(record.name, netInterface, now);
        continue;
      }
      const recordAddressesKnown = storedRecord.addresses.size > 0;
      const ips = this.#handleIpRecords([formerAnswers, answers], target, netInterface);
      if (ips.length > 0) {
        for (const { value: ip, ttl: ttl2 } of ips) {
          if (ttl2 === 0) {
            const existingAddress = storedRecord.addresses.get(ip);
            if (existingAddress) {
              const addressAge = now - existingAddress.discoveredAt;
              if (addressAge < GOODBYE_PROTECTION_WINDOW) {
                continue;
              }
              logger.debug(
                `Removing IP ${ip} for commissionable device ${record.name} from cache (interface ${netInterface}) because of ttl=0`
              );
              storedRecord.addresses.delete(ip);
            }
            continue;
          }
          const matterServer = storedRecord.addresses.get(ip) ?? { ip, port, type: "udp" };
          matterServer.discoveredAt = now;
          matterServer.ttl = ttl2;
          storedRecord.addresses.set(ip, matterServer);
        }
      }
      this.#commissionableDeviceRecords.set(record.name, storedRecord);
      if (storedRecord.addresses.size === 0) {
        const queryId2 = this.#findCommissionableQueryIdentifier("", storedRecord);
        if (queryId2 === void 0) continue;
        const queries = [{ name: target, recordClass: import_general.DnsRecordClass.IN, recordType: import_general.DnsRecordType.AAAA }];
        if (this.#socket.supportsIpv4) {
          queries.push({ name: target, recordClass: import_general.DnsRecordClass.IN, recordType: import_general.DnsRecordType.A });
        }
        logger.debug(
          `Requesting IP addresses for commissionable device ${record.name} (interface ${netInterface}).`
        );
        this.#setQueryRecords(queryId2, queries, answers);
      } else if (!recordAddressesKnown) {
        logNewAddresses(record.name, "commissionable", netInterface, storedRecord.addresses);
      }
      if (storedRecord.addresses.size === 0) continue;
      const queryId = this.#findCommissionableQueryIdentifier(record.name, storedRecord);
      if (queryId === void 0) continue;
      queryMissingDataForInstances.delete(record.name);
      this.#finishWaiter(queryId, true, recordAddressesKnown);
    }
    if (queryMissingDataForInstances.size !== 0) {
      for (const name of Array.from(queryMissingDataForInstances.values())) {
        const storedRecord = this.#commissionableDeviceRecords.get(name);
        if (storedRecord === void 0) continue;
        const queryId = this.#findCommissionableQueryIdentifier("", storedRecord);
        if (queryId === void 0) continue;
        logger.debug(`Requesting more records for commissionable device ${name} (interface ${netInterface}).`);
        this.#setQueryRecords(
          queryId,
          [{ name, recordClass: import_general.DnsRecordClass.IN, recordType: import_general.DnsRecordType.ANY }],
          answers
        );
      }
    }
  }
  #parseTxtRecord(record) {
    const { value } = record;
    const result = {};
    if (Array.isArray(value)) {
      for (const item of value) {
        const [key, value2] = item.split("=");
        if (key === void 0 || value2 === void 0) continue;
        if (["T", "D", "CM", "DT", "PH", "ICD"].includes(key)) {
          const intValue = parseInt(value2);
          if (!Number.isFinite(intValue)) continue;
          result[key] = intValue;
        } else if (["VP", "DN", "RI", "PI"].includes(key)) {
          result[key] = value2;
        } else if (["SII", "SAI", "SAT"].includes(key)) {
          const intValue = parseInt(value2);
          if (!Number.isFinite(intValue)) continue;
          result[key] = intValue;
          result[key] = (0, import_general.Millis)(intValue);
        }
      }
    }
    if (result.T === void 0) {
      result.T = 0;
    } else if (!(result.T & ~1 & 6)) {
      result.T = 0;
    }
    if (result.ICD === void 0) {
      result.ICD = 0;
    }
    return result;
  }
  #parseCommissionableTxtRecord(record) {
    const { value, ttl } = record;
    if (!Array.isArray(value)) return void 0;
    const txtRecord = this.#parseTxtRecord(record);
    if (txtRecord === void 0 || txtRecord.D === void 0 || txtRecord.CM === void 0) {
      return void 0;
    }
    return {
      addresses: /* @__PURE__ */ new Map(),
      discoveredAt: import_general.Time.nowMs,
      ttl,
      ...txtRecord
    };
  }
  #expire() {
    const now = import_general.Time.nowMs;
    [...this.#operationalDeviceRecords.entries()].forEach(([recordKey, { addresses, discoveredAt, ttl }]) => {
      const expires = discoveredAt + this.#effectiveTTL(ttl);
      if (now <= expires) {
        [...addresses.entries()].forEach(([key, { discoveredAt: discoveredAt2, ttl: ttl2 }]) => {
          if (now < discoveredAt2 + this.#effectiveTTL(ttl2)) return;
          addresses.delete(key);
        });
      }
      if (now > expires && !addresses.size) {
        this.#operationalDeviceRecords.delete(recordKey);
      }
    });
    [...this.#commissionableDeviceRecords.entries()].forEach(([recordKey, { addresses, discoveredAt, ttl }]) => {
      const expires = discoveredAt + this.#effectiveTTL(ttl);
      if (now <= expires) {
        [...addresses.entries()].forEach(([key, { discoveredAt: discoveredAt2, ttl: ttl2 }]) => {
          if (now < discoveredAt2 + this.#effectiveTTL(ttl2)) return;
          addresses.delete(key);
        });
      }
      if (now > expires && !addresses.size) {
        this.#commissionableDeviceRecords.delete(recordKey);
      }
    });
    [...this.#activeAnnounceQueries.values()].forEach(({ answers }) => this.#expireStructuredAnswers(answers, now));
    this.#discoveredIpRecords.forEach((answers) => this.#expireStructuredAnswers(answers, now));
  }
  #expireStructuredAnswers(data, now) {
    if (data.operational) {
      Object.keys(data.operational).forEach((recordType) => {
        const type = parseInt(recordType);
        data.operational[type] = data.operational[type].filter(
          ({ discoveredAt, ttl }) => now < discoveredAt + this.#effectiveTTL(ttl)
        );
        if (data.operational[type].length === 0) {
          delete data.operational[type];
        }
      });
    }
    if (data.commissionable) {
      Object.keys(data.commissionable).forEach((recordType) => {
        const type = parseInt(recordType);
        data.commissionable[type] = data.commissionable[type].filter(
          ({ discoveredAt, ttl }) => now < discoveredAt + this.#effectiveTTL(ttl)
        );
        if (data.commissionable[type].length === 0) {
          delete data.commissionable[type];
        }
      });
    }
    if (data.addressesV6) {
      Object.keys(data.addressesV6).forEach((name) => {
        for (const [ip, { discoveredAt, ttl }] of data.addressesV6[name].entries()) {
          if (now < discoveredAt + this.#effectiveTTL(ttl)) continue;
          data.addressesV6[name].delete(ip);
        }
        if (data.addressesV6[name].size === 0) {
          delete data.addressesV6[name];
        }
      });
    }
    if (data.addressesV4) {
      Object.keys(data.addressesV4).forEach((name) => {
        for (const [ip, { discoveredAt, ttl }] of data.addressesV4[name].entries()) {
          if (now < discoveredAt + this.#effectiveTTL(ttl)) continue;
          data.addressesV4[name].delete(ip);
        }
        if (data.addressesV4[name].size === 0) {
          delete data.addressesV4[name];
        }
      });
    }
  }
  static discoveryDataDiagnostics(data, kind) {
    return import_general.Diagnostic.dict({
      kind,
      SII: data.SII !== void 0 ? import_general.Duration.format(data.SII) : void 0,
      SAI: data.SAI !== void 0 ? import_general.Duration.format(data.SAI) : void 0,
      SAT: data.SAT !== void 0 ? import_general.Duration.format(data.SAT) : void 0,
      T: data.T,
      DT: data.DT,
      PH: data.PH,
      ICD: data.ICD,
      VP: data.VP,
      DN: data.DN,
      RI: data.RI,
      PI: data.PI
    });
  }
  static deviceAddressDiagnostics(addresses) {
    const diagnostic = Array();
    for (const address of addresses.values()) {
      if (diagnostic.length) {
        diagnostic.push(", ");
      }
      diagnostic.push(import_general.ServerAddress.diagnosticFor(address));
    }
    return import_general.Diagnostic.squash(...diagnostic);
  }
}
function logNewService(service, kind, data) {
  logger.debug("Found device", import_general.Diagnostic.strong(service), MdnsClient.discoveryDataDiagnostics(data, kind));
}
function logNewAddresses(service, kind, intf, addresses) {
  logger.debug(
    "Added",
    addresses.size,
    "addresses for",
    import_general.Diagnostic.strong(service),
    import_general.Diagnostic.dict({
      kind,
      addrs: MdnsClient.deviceAddressDiagnostics(addresses),
      intf
    })
  );
}
//# sourceMappingURL=MdnsClient.js.map
