# `Quic.Recovery`
[🔗](https://github.com/gsmlg-dev/http_fetch/blob/v1.0.0-rc.9/apps/elixir_quic/lib/quic/recovery.ex#L1)

Explicit-time QUIC packet-number spaces and loss recovery state.

Packet numbers are allocated by `reserve/4` and are never reused. A packet
moves through `:reserved`, `:queued`, `:sent`, then `:acked`, `:lost`, or
`:failed`. ACKs received before a local send receipt are retained as a bounded
pending acknowledgement and resolved when the receipt arrives.

Receive ACK history retains the newest `max_ack_ranges` (1..256) ranges per
space. A monotonic receive floor discards older packet numbers, including
previously unseen delayed packets; reliable data can be retransmitted under
a fresh packet number. Discarding an encryption space resets its receive epoch.

Packet-number spaces stay global across migration. Each reserved packet pins
its send path, and at most two independent RTT/congestion controllers remain
live. The selected controller occupies `rtt`/`congestion` for compatibility;
`paths` stores only its one inactive peer. Cross-path ACKs do not sample RTT.

# `ack_result`

```elixir
@type ack_result() :: %{
  acked: [non_neg_integer()],
  lost: [{packet_space(), non_neg_integer()}],
  rtt_sample: pos_integer() | nil
}
```

# `initial_state`

```elixir
@type initial_state() :: %Quic.Recovery{
  active_path: path_id(),
  congestion: %Quic.Congestion.NewReno{
    acked_bytes: 0,
    bytes_in_flight: 0,
    cwnd: term(),
    max_datagram_size: term(),
    mss: number(),
    ssthresh: term()
  },
  deadline: nil,
  max_ack_ranges: 1..256,
  max_sent_packets: term(),
  path_defaults: %{
    rtt: Quic.Recovery.RTT.t(),
    congestion: Quic.Congestion.NewReno.initial_state()
  },
  paths: %{},
  pto_grants: %{},
  rtt: %Quic.Recovery.RTT{
    ack_delay_exponent: term(),
    latest: nil,
    max_ack_delay: term(),
    min: nil,
    pto_count: 0,
    smoothed: nil,
    variance: nil
  },
  spaces: map(),
  timer_generation: 0
}
```

# `packet_space`

```elixir
@type packet_space() :: :initial | :handshake | :application
```

# `path_controller`

```elixir
@type path_controller() :: %{
  rtt: Quic.Recovery.RTT.t(),
  congestion: Quic.Congestion.NewReno.t()
}
```

# `path_id`

```elixir
@type path_id() :: term()
```

# `t`

```elixir
@type t() :: %Quic.Recovery{
  active_path: term(),
  congestion: term(),
  deadline: term(),
  max_ack_ranges: term(),
  max_sent_packets: term(),
  path_defaults: path_controller(),
  paths: term(),
  pto_grants: %{optional({packet_space(), non_neg_integer()}) =&gt; path_id()},
  rtt: term(),
  spaces: term(),
  timer_generation: term()
}
```

# `add_path`

```elixir
@spec add_path(t(), path_id()) :: {:ok, t()} | {:error, :path_limit}
```

# `arm`

Recompute the next deadline from actual sent packets, without advancing time.

# `configure_peer_ack`

```elixir
@spec configure_peer_ack(t(), non_neg_integer(), 0..20) ::
  {:ok, t()} | {:error, :invalid_ack_parameters}
```

Install connection-wide peer ACK parameters without copying RTT estimates.

# `discard_space`

Discard an encryption space's outstanding work while preserving allocated packet numbers.

# `drop_path`

```elixir
@spec drop_path(t(), path_id()) ::
  {:ok, t()} | {:error, :active_path | :unknown_path | :path_busy}
```

# `local_send`

```elixir
@spec local_send(t(), atom(), non_neg_integer(), :ok | {:error, term()}, integer()) ::
  {:ok, t(), [atom()]} | {:error, atom()}
```

# `new`

```elixir
@spec new(keyword()) :: initial_state()
```

# `note_received`

```elixir
@spec note_received(t(), atom(), non_neg_integer()) :: {:ok, t()} | {:error, atom()}
```

Record an authenticated packet number received in a packet-number space.

# `on_time`

```elixir
@spec on_time(t(), integer()) :: {:ok, t(), map()}
```

# `path`

```elixir
@spec path(t(), path_id()) :: path_controller() | nil
```

An independent path controller, or nil after retirement.

# `pto_duration`

```elixir
@spec pto_duration(t()) :: pos_integer()
```

Current probe timeout duration in microseconds, before PTO backoff.

# `pto_duration`

```elixir
@spec pto_duration(t(), path_id()) :: pos_integer()
```

# `receive_ack`

```elixir
@spec receive_ack(t(), packet_space(), map(), integer()) ::
  {:ok, t(), ack_result()} | {:error, atom()}
```

# `receive_ack`

```elixir
@spec receive_ack(t(), packet_space(), map(), integer(), path_id()) ::
  {:ok, t(), ack_result()} | {:error, atom()}
```

# `reserve`

```elixir
@spec reserve(t(), atom(), map(), non_neg_integer()) ::
  {:ok, t(), Quic.Recovery.Packet.t()} | {:error, atom()}
```

# `reserve_probe`

```elixir
@spec reserve_probe(
  t(),
  packet_space(),
  non_neg_integer(),
  map(),
  integer(),
  integer()
) ::
  {:ok, t(), Quic.Recovery.Packet.t()} | {:error, atom()}
```

Reserve one fresh packet for an original selected by the latest expired PTO.

The one-shot grant is tied to the original packet and its send path. Probe
bytes count as additional flight without increasing the congestion window;
ordinary reservations cannot use this allowance. The caller supplies the
actual protected-packet size limit and only the selected retry may use it.

# `retire_space`

```elixir
@spec retire_space(t(), :initial | :handshake) :: t()
```

Permanently retire Initial/Handshake recovery, preserving the next packet number.

# `select_path`

```elixir
@spec select_path(t(), path_id()) :: {:ok, t()} | {:error, :unknown_path}
```

# `selected_path`

```elixir
@spec selected_path(t()) :: path_id()
```

The selected send path; packet-number spaces are shared by all paths.

# `timer_expired?`

```elixir
@spec timer_expired?(t(), non_neg_integer(), integer()) :: boolean()
```

# `transition`

```elixir
@spec transition(t(), atom(), non_neg_integer(), atom()) ::
  {:ok, t()} | {:error, atom()}
```

---

*Consult [api-reference.md](api-reference.md) for complete listing*
