"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 DnssdNames_exports = {};
__export(DnssdNames_exports, {
  DnssdNames: () => DnssdNames
});
module.exports = __toCommonJS(DnssdNames_exports);
var import_DnsCodec = require("#codec/DnsCodec.js");
var import_MatterError = require("#MatterError.js");
var import_Time = require("#time/Time.js");
var import_Timestamp = require("#time/Timestamp.js");
var import_TimeUnit = require("#time/TimeUnit.js");
var import_Lifetime = require("#util/Lifetime.js");
var import_Observable = require("#util/Observable.js");
var import_Scheduler = require("#util/Scheduler.js");
var import_DnssdName = require("./DnssdName.js");
var import_DnssdSolicitor = require("./DnssdSolicitor.js");
/**
 * @license
 * Copyright 2022-2026 Matter.js Authors
 * SPDX-License-Identifier: Apache-2.0
 */
const STAGED_IP_RECORDS_MAX_HOSTS = 250;
const STAGED_IP_RECORDS_MIN_HOSTS = 200;
const FILTER_NAMES_OWNER = "dnssd-filters";
const TRACKED_NAMES_OWNER = "dnssd-tracked";
class DnssdNames {
  #socket;
  #lifetime;
  #entropy;
  #filters = /* @__PURE__ */ new Set();
  #filterNames = /* @__PURE__ */ new Map();
  #filtersFacade;
  #solicitor;
  #observers = new import_Observable.ObserverGroup();
  #names = /* @__PURE__ */ new Map();
  #expiration;
  #discovered = new import_Observable.Observable();
  #goodbyeProtectionWindow;
  #minTtl;
  #ttlGraceFactor;
  /**
   * A/AAAA records for hostnames that don't yet have a DnssdName; replayed when the name is later created via
   * SRV dependency.  Expired entries are pruned by a timer and filtered on consumption.
   */
  #stagedIpRecords = /* @__PURE__ */ new Map();
  #stagedIpExpirationTimer;
  // Points at handleMessage's newlyDiscovered list while a packet is being processed so replay-triggered
  // discoveries join the same end-of-packet emit batch
  #currentBatch;
  constructor({
    socket,
    lifetime = import_Lifetime.Lifetime.process,
    entropy,
    filter,
    filterNames,
    goodbyeProtectionWindow,
    minTtl,
    ttlGraceFactor
  }) {
    this.#socket = socket;
    this.#lifetime = lifetime.join("mdns names");
    this.#entropy = entropy;
    if (filter) {
      this.#addFilter(filter, filterNames);
    }
    this.#solicitor = new import_DnssdSolicitor.QueryMulticaster(this);
    this.#goodbyeProtectionWindow = goodbyeProtectionWindow ?? DnssdNames.defaults.goodbyeProtectionWindow;
    this.#minTtl = minTtl ?? DnssdNames.defaults.minTtl;
    const effectiveGraceFactor = ttlGraceFactor ?? import_DnssdName.DEFAULT_TTL_GRACE_FACTOR;
    if (!(effectiveGraceFactor >= 1)) {
      throw new import_MatterError.ImplementationError(`ttlGraceFactor must be >= 1.0, got ${effectiveGraceFactor}`);
    }
    this.#ttlGraceFactor = effectiveGraceFactor;
    this.#expiration = new import_Scheduler.Scheduler({
      name: "expiration scheduler",
      lifetime: this.#lifetime,
      timeOf: (a) => a.expiresAt,
      run: (record) => {
        const discoveryName = this.maybeGet(record.name);
        if (discoveryName) {
          discoveryName.deleteRecord(record);
        }
      }
    });
    this.#nameContext = {
      delete: (name) => {
        const known = this.maybeGet(name.qname);
        if (known === name) {
          this.#delete(name);
        }
      },
      registerForExpiration: (record) => this.#expiration.add(record),
      unregisterForExpiration: (record) => this.#expiration.delete(record),
      get: (qname) => this.get(qname),
      ttlGraceFactor: this.#ttlGraceFactor
    };
    this.#observers.on(this.#socket.receipt, this.#handleMessage.bind(this));
    this.#stagedIpExpirationTimer = import_Time.Time.getPeriodicTimer(
      "Staged IP record expiration",
      (0, import_TimeUnit.Minutes)(1),
      this.#pruneStagedIpRecords.bind(this)
    );
    this.#stagedIpExpirationTimer.utility = true;
    this.#stagedIpExpirationTimer.start();
    this.#updateFilterNames();
  }
  #pruneStagedIpRecords() {
    if (this.#stagedIpRecords.size === 0) {
      return;
    }
    const now = import_Time.Time.nowMs;
    for (const [key, staged] of this.#stagedIpRecords) {
      const live = staged.filter(
        ({ record, receivedAt }) => now - receivedAt < record.ttl * this.#ttlGraceFactor
      );
      if (live.length === 0) {
        this.#stagedIpRecords.delete(key);
      } else if (live.length !== staged.length) {
        this.#stagedIpRecords.set(key, live);
      }
    }
    if (this.#stagedIpRecords.size > STAGED_IP_RECORDS_MAX_HOSTS) {
      const keys = this.#stagedIpRecords.keys();
      while (this.#stagedIpRecords.size > STAGED_IP_RECORDS_MIN_HOSTS) {
        const { value, done } = keys.next();
        if (done) break;
        this.#stagedIpRecords.delete(value);
      }
    }
  }
  #handleMessage(message) {
    let newlyDiscovered;
    try {
      newlyDiscovered = this.#processMessage(message);
    } finally {
      this.#currentBatch = void 0;
    }
    for (const name of newlyDiscovered ?? []) {
      if (name.isDiscovered) {
        this.#discovered.emit(name);
      }
    }
  }
  #processMessage(message) {
    const records = [...message.answers, ...message.additionalRecords];
    const filtered = new Set(records);
    const sourceIntf = message.sourceIntf;
    let goodbyesBefore;
    const newlyDiscovered = [];
    this.#currentBatch = newlyDiscovered;
    let packetRelevant = false;
    const handleRecord = (record) => {
      filtered.delete(record);
      packetRelevant = true;
      const name = this.get(record.name);
      if (record.ttl) {
        if (record.ttl < this.#minTtl) {
          record = { ...record, ttl: this.#minTtl };
        }
        const wasDiscovered = name.isDiscovered;
        name.installRecord(record, { sourceIntf });
        if (!wasDiscovered && name.isDiscovered) {
          newlyDiscovered.push(name);
        }
      } else {
        if (goodbyesBefore === void 0) {
          goodbyesBefore = (0, import_Timestamp.Timestamp)(import_Time.Time.nowMs - this.#goodbyeProtectionWindow);
        }
        name.deleteRecord(record, goodbyesBefore);
      }
    };
    for (const record of records) {
      if (this.#filters.size > 0) {
        let accepted = false;
        for (const f of this.#filters) {
          if (f(record)) {
            accepted = true;
            break;
          }
        }
        if (!accepted) {
          continue;
        }
      }
      handleRecord(record);
    }
    let filteredBeforePass;
    do {
      filteredBeforePass = filtered.size;
      for (const record of filtered) {
        if (!this.has(record.name)) {
          continue;
        }
        handleRecord(record);
      }
    } while (filteredBeforePass > filtered.size);
    for (const record of filtered) {
      if (record.recordType !== import_DnsCodec.DnsRecordType.A && record.recordType !== import_DnsCodec.DnsRecordType.AAAA) {
        continue;
      }
      if (record.ttl !== 0) {
        continue;
      }
      const key = record.name.toLowerCase();
      if (this.#names.has(key)) {
        continue;
      }
      const staged = this.#stagedIpRecords.get(key);
      if (staged === void 0) {
        continue;
      }
      const remaining = staged.filter(
        (s) => !(s.record.recordType === record.recordType && s.record.value === record.value)
      );
      if (remaining.length === 0) {
        this.#stagedIpRecords.delete(key);
      } else {
        this.#stagedIpRecords.set(key, remaining);
      }
    }
    if (packetRelevant) {
      for (let record of filtered) {
        if (record.recordType !== import_DnsCodec.DnsRecordType.A && record.recordType !== import_DnsCodec.DnsRecordType.AAAA) {
          continue;
        }
        if (record.ttl === 0) {
          continue;
        }
        const key = record.name.toLowerCase();
        if (this.#names.has(key)) {
          continue;
        }
        if (record.ttl < this.#minTtl) {
          record = { ...record, ttl: this.#minTtl };
        }
        const staged = this.#stagedIpRecords.get(key) ?? [];
        const existing = staged.findIndex(
          (s) => s.record.recordType === record.recordType && s.record.value === record.value
        );
        if (existing === -1) {
          staged.push({ record, receivedAt: import_Time.Time.nowMs, sourceIntf });
        } else {
          staged[existing] = { record, receivedAt: import_Time.Time.nowMs, sourceIntf };
        }
        this.#stagedIpRecords.delete(key);
        this.#stagedIpRecords.set(key, staged);
      }
    }
    return newlyDiscovered;
  }
  /**
   * Test for existence of name.
   */
  has(name) {
    return this.#names.has(name.toLowerCase());
  }
  /**
   * Retrieve the {@link DnssdName} for {@link name}.
   *
   * This will create the name if it does not exist, and if you do not add an observer then it will not automatically
   * delete if there are no records.  So if you may not use the record test for existence with {@link has} first.
   */
  get(qname) {
    let name = this.maybeGet(qname);
    if (name === void 0) {
      name = new import_DnssdName.DnssdName(qname, this.#nameContext);
      const key = qname.toLowerCase();
      this.#names.set(key, name);
      this.#socket.addRelevantName(TRACKED_NAMES_OWNER, key);
      const staged = this.#stagedIpRecords.get(key);
      if (staged !== void 0) {
        this.#stagedIpRecords.delete(key);
        const now = import_Time.Time.nowMs;
        for (const { record, receivedAt, sourceIntf } of staged) {
          if (now - receivedAt < record.ttl * this.#ttlGraceFactor) {
            name.installRecord(record, { sourceIntf, installedAt: receivedAt });
          }
        }
        if (name.isDiscovered) {
          if (this.#currentBatch !== void 0) {
            this.#currentBatch.push(name);
          } else {
            this.#discovered.emit(name);
          }
        }
      }
    }
    return name;
  }
  /**
   * Retrieve the {@link DnssdName} if known.
   */
  maybeGet(name) {
    return this.#names.get(name.toLowerCase());
  }
  /**
   * All currently discovered {@link DnssdName}s.
   */
  get discoveredNames() {
    const names = this.#names;
    return (function* () {
      for (const name of names.values()) {
        if (name.isDiscovered) {
          yield name;
        }
      }
    })();
  }
  #delete(name) {
    const key = name.qname.toLowerCase();
    this.#names.delete(key);
    this.#socket.removeRelevantName(TRACKED_NAMES_OWNER, key);
  }
  /**
   * Wait for all workers and close all names.
   */
  async close() {
    var _stack = [];
    try {
      const _closing = __using(_stack, this.#lifetime.closing());
      this.#socket.unregisterRelevantNames(FILTER_NAMES_OWNER);
      this.#socket.unregisterRelevantNames(TRACKED_NAMES_OWNER);
      this.#stagedIpExpirationTimer.stop();
      this.#stagedIpRecords.clear();
      this.#observers.close();
      await this.#expiration.close();
      for (const name of this.#names.values()) {
        await name.close();
        this.#delete(name);
      }
      await this.#solicitor.close();
    } catch (_) {
      var _error = _, _hasError = true;
    } finally {
      __callDispose(_stack, _error, _hasError);
    }
  }
  /**
   * Dynamic ingress filters. Records accepted by ANY registered filter are processed; with no filters, all records
   * are accepted. Pass the names a filter accepts (e.g. its service types) so the socket can drop irrelevant packets
   * before decoding them; a filter added without names disables that optimization (the socket accepts everything).
   */
  get filters() {
    if (this.#filtersFacade === void 0) {
      const filters = this.#filters;
      this.#filtersFacade = {
        add: (filter, names) => this.#addFilter(filter, names),
        delete: (filter) => this.#deleteFilter(filter),
        has: (filter) => filters.has(filter),
        get size() {
          return filters.size;
        },
        [Symbol.iterator]: () => filters[Symbol.iterator]()
      };
    }
    return this.#filtersFacade;
  }
  #addFilter(filter, names) {
    this.#filters.add(filter);
    this.#filterNames.set(filter, names === void 0 || names === "all" ? "all" : [...names]);
    this.#updateFilterNames();
  }
  #deleteFilter(filter) {
    const deleted = this.#filters.delete(filter);
    if (deleted) {
      this.#filterNames.delete(filter);
      this.#updateFilterNames();
    }
    return deleted;
  }
  /**
   * Publish the union of the filters' declared service types to the socket. Tracked names (SRV-target hostnames and
   * the like) are registered separately and incrementally via {@link DnssdNames.get}/{@link #delete}. Registers
   * "all" — disabling the pre-filter — whenever a filter was added without names, or no filter is registered (every
   * record is accepted).
   */
  #updateFilterNames() {
    if (this.#filters.size === 0) {
      this.#socket.registerRelevantNames(FILTER_NAMES_OWNER, "all");
      return;
    }
    const names = /* @__PURE__ */ new Set();
    for (const filterNames of this.#filterNames.values()) {
      if (filterNames === "all") {
        this.#socket.registerRelevantNames(FILTER_NAMES_OWNER, "all");
        return;
      }
      for (const name of filterNames) {
        names.add(name);
      }
    }
    this.#socket.registerRelevantNames(FILTER_NAMES_OWNER, names);
  }
  get socket() {
    return this.#socket;
  }
  /**
   * Emits when a {@link DnssdName} is first discovered.
   */
  get discovered() {
    return this.#discovered;
  }
  /**
   * Shared solicitor.
   *
   * We offer solicitation in this object so there is not redundant solicitation across interested parties.
   */
  get solicitor() {
    return this.#solicitor;
  }
  get entropy() {
    return this.#entropy;
  }
  #nameContext;
}
((DnssdNames2) => {
  DnssdNames2.defaults = {
    goodbyeProtectionWindow: (0, import_TimeUnit.Seconds)(1),
    minTtl: (0, import_TimeUnit.Seconds)(15)
    // This is the value that Apple uses
  };
})(DnssdNames || (DnssdNames = {}));
//# sourceMappingURL=DnssdNames.js.map
