// gRPC hedging, as proposal A6 defines it: attempts sent in parallel, a new one every
// `delay` until one of them answers with a status that settles the call. Pure; the
// managed channel launches, waits and cancels.

///|
/// A method's hedging policy (A6 `hedgingPolicy`): `attempts` is `maxAttempts`,
/// `delay` is `hedgingDelay`, `codes` is `nonFatalStatusCodes`.
pub(all) struct Hedging {
  attempts : Int32
  delay : @moondate.Span
  codes : Array[@wire.Status]
} derive(Eq, Debug)

///|
pub extend Hedging with Eq::{equal, not_equal}

///|
pub extend Hedging with Debug::{to_repr}

///|
/// A6's example policy: four attempts half a second apart, `UNAVAILABLE`, `INTERNAL`
/// and `ABORTED` letting the others carry on.
pub let hedging : Hedging = {
  attempts: 4,
  delay: @moondate.Span::new(millis=500L),
  codes: [Unavailable, Internal, Aborted],
}

///|
/// A policy by A6's field names.
pub fn Hedging::new(
  attempts~ : Int32 = hedging.attempts,
  delay~ : @moondate.Span = hedging.delay,
  codes~ : Array[@wire.Status] = hedging.codes,
) -> Hedging {
  { attempts, delay, codes, }
}

///|
/// Whether another attempt may start when `started` have, under the policy and the
/// client's `limit`.
pub fn Hedging::more(
  self : Hedging,
  started : Int32,
  limit~ : Int32 = limit,
) -> Bool {
  started < (if self.attempts < limit { self.attempts } else { limit })
}

///|
/// Whether an attempt ending with `status` settles the call: OK does, and so does any
/// status outside `codes`. A non-fatal one sends the next attempt at once instead.
pub fn Hedging::commits(self : Hedging, status : Int32) -> Bool {
  status == @wire.Status::code(Ok) ||
  !self.codes.iter().any(c => c.code() == status)
}