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 { DnsRecordType } from "#codec/DnsCodec.js";
import { ImplementationError } from "#MatterError.js";
import { Time } from "#time/Time.js";
import { Timestamp } from "#time/Timestamp.js";
import { Minutes, Seconds } from "#time/TimeUnit.js";
import { Lifetime } from "#util/Lifetime.js";
import { Observable, ObserverGroup } from "#util/Observable.js";
import { Scheduler } from "#util/Scheduler.js";
import { DEFAULT_TTL_GRACE_FACTOR, DnssdName } from "./DnssdName.js";
import { QueryMulticaster } from "./DnssdSolicitor.js";
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 ObserverGroup();
  #names = /* @__PURE__ */ new Map();
  #expiration;
  #discovered = new 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 = 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 QueryMulticaster(this);
    this.#goodbyeProtectionWindow = goodbyeProtectionWindow ?? DnssdNames.defaults.goodbyeProtectionWindow;
    this.#minTtl = minTtl ?? DnssdNames.defaults.minTtl;
    const effectiveGraceFactor = ttlGraceFactor ?? DEFAULT_TTL_GRACE_FACTOR;
    if (!(effectiveGraceFactor >= 1)) {
      throw new ImplementationError(`ttlGraceFactor must be >= 1.0, got ${effectiveGraceFactor}`);
    }
    this.#ttlGraceFactor = effectiveGraceFactor;
    this.#expiration = new 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 = Time.getPeriodicTimer(
      "Staged IP record expiration",
      Minutes(1),
      this.#pruneStagedIpRecords.bind(this)
    );
    this.#stagedIpExpirationTimer.utility = true;
    this.#stagedIpExpirationTimer.start();
    this.#updateFilterNames();
  }
  #pruneStagedIpRecords() {
    if (this.#stagedIpRecords.size === 0) {
      return;
    }
    const now = 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 = Timestamp(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 !== DnsRecordType.A && record.recordType !== 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 !== DnsRecordType.A && record.recordType !== 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: Time.nowMs, sourceIntf });
        } else {
          staged[existing] = { record, receivedAt: 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 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 = 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: Seconds(1),
    minTtl: Seconds(15)
    // This is the value that Apple uses
  };
})(DnssdNames || (DnssdNames = {}));
export {
  DnssdNames
};
//# sourceMappingURL=DnssdNames.js.map
