///|
/// The per-attempt deadline around a transport the caller supplied, timed
/// by the client's clock: the whole buffered exchange, or the response head
/// of a streaming one. Expiry raises `Timeout(phase=Unknown, ms)`, since the
/// deadline cannot see which phase the inner transport was in, except for a
/// deadline of zero or less, which expires before the exchange begins
/// (`Pool`); the inner transport's own failures pass through.
priv struct AttemptDeadline {
  inner : &@ghttp.Transport
  clock : &@hclock.Clock
  timeout_ms : Int32
}

///|
impl @ghttp.Transport for AttemptDeadline with fn send(self, request) {
  self.bounded(() => self.inner.send(request))
}

///|
impl @ghttp.Transport for AttemptDeadline with fn send_stream(self, request) {
  self.bounded(() => self.inner.send_stream(request))
}

///|
/// Runs `exchange` until it finishes or the deadline passes; a nonpositive
/// deadline expires at once.
async fn[X] AttemptDeadline::bounded(
  self : AttemptDeadline,
  exchange : async () -> X raise @ghttp.HttpError,
) -> X raise @ghttp.HttpError {
  if self.timeout_ms <= 0 {
    // Nothing was sent: the deadline was over before the exchange began.
    raise @ghttp.HttpError::Timeout(phase=Pool, ms=self.timeout_ms)
  }
  let expired = @ghttp.HttpError::Timeout(phase=Unknown, ms=self.timeout_ms)
  // The exchange's failure travels as a value, so it keeps its type.
  let outcome = @transport.with_timeout_opt(self.clock, self.timeout_ms, () => {
    try exchange() catch {
      error => Err(error)
    } noraise {
      value => Ok(value)
    }
  }) catch {
    // The timer itself failed; the exchange may have been under way.
    error => raise Network(phase=Unknown, message=error.to_string())
  }
  match outcome {
    Some(Ok(value)) => value
    Some(Err(error)) => raise error
    None => raise expired
  }
}