///|
/// Resolve an already named instance without requiring a PTR response.
/// The timeout covers initial resolution; after success it remains subscribed
/// until cancel/close so updates keep the same event contract as browse.
pub fn Engine::resolve(
  self : Engine,
  instance : @model.DnsName,
  scope : String,
  now : Int64,
  timeout_ms~ : Int32 = 5000,
) -> Int32 raise {
  guard !self.closed else { raise Closed }
  let labels = instance.labels()
  guard labels.length() == 4 else { raise InvalidServiceType }
  let service_type = @model.DnsName::from_labels([
    labels[1],
    labels[2],
    labels[3],
  ])
  guard valid_service_type(service_type) else { raise InvalidServiceType }
  guard scope.length() > 0 && scope.length() <= 256 else {
    raise ResourceLimit("scope length outside 1..256")
  }
  guard timeout_ms > 0 && timeout_ms <= 3600000 else {
    raise ResourceLimit("resolve timeout outside 1..3600000ms")
  }
  guard self.interests.length() < 64 && self.next_id < 2147483647 else {
    raise ResourceLimit("subscription limit (64)")
  }
  let removed = self.cache.advance(now)
  self.discard_burst()
  self.now_ms = now
  let id = self.next_id
  self.next_id += 1
  self.interests.push({
    id,
    scope,
    service_type,
    browse_name: service_type,
    instance: Some(instance),
    deadline: Some(now + timeout_ms.to_int64()),
  })
  self.reconcile("resolve_started", removals=removed)
  id
}

///|
fn Engine::expire_resolvers(self : Engine) -> Bool {
  let expired = self.interests.filter(s => {
    s.deadline is Some(time) && time <= self.now_ms
  })
  for sub in expired {
    self.emit(ResolveTimedOut(sub.id))
  }
  self.interests = self.interests.filter(s => {
    !(s.deadline is Some(time) && time <= self.now_ms)
  })
  !expired.is_empty()
}