# `XGW.Peer.SendHandle`
[🔗](https://github.com/Lazarus404/xgw/blob/main/lib/xgw/peer/send_handle.ex#L1)

Hot-path outbound RTP without GenServer hops on Peer or Listener.

## Why a send handle exists

After DTLS, every media packet must be **SRTP-encrypted** and written to UDP
(or TURN-wrapped). Calling into the Peer GenServer for each packet adds
latency and mailbox pressure. Instead, Peer publishes encrypt state into an
ETS table; forwarders call `send_rtp/3` with a handle from
`XGW.Peer.outbound_handle/1`.

ICE-TCP has no hot handle (`{:error, :use_genserver}`); fall back to
`XGW.Peer.send_rtp_async/3`.

Benches/tests may call `publish_passthrough/2` (RTP) or
`publish_dc_passthrough/2` (datachannel) to register a synchronous MFA sink
so Forwarder takes the SendHandle success path without a Peer GenServer hop.

Datachannel production egress uses the per-Peer `XGW.Peer.Datachannel` process
(`Peer.send_datachannel_async/4` casts there when published). A full SCTP-in-ETS
hot path is not shipped (`ssl_sock` ownership).

ponytail: v1 assumes one packet writer per Peer outbound (forwarder). ROC
write-back uses generation-matched `select_replace` so Peer republish cannot
be clobbered. Multi-writer CAS across forwarders is the upgrade path.

## RFCs

* RFC 3711 -- SRTP encrypt on the outbound hot path
* RFC 8656 -- TURN ChannelData / Send Indication when the path is relay

# `t`

```elixir
@type t() :: %XGW.Peer.SendHandle{generation: pos_integer(), peer: pid()}
```

Opaque handle used by `send_rtp/3`.

## Fields

* `:peer` - Peer GenServer pid whose ETS row backs this handle
* `:generation` - publish generation; stale handles fail closed after republish

# `__struct__`
*struct* 

Hot-path send handle.

## Fields

* `:peer` - Peer GenServer pid whose ETS row backs this handle
* `:generation` - publish generation; stale handles fail closed after republish

# `ensure_table!`

```elixir
@spec ensure_table!() :: :ok
```

Creates the ETS table (idempotent). Called from `XGW.Application`.

# `fetch`

```elixir
@spec fetch(pid()) :: {:ok, t()} | {:error, :not_ready | :use_genserver}
```

Looks up a published handle for `peer`.

Returns `{:error, :not_ready}` when missing, or `{:error, :use_genserver}` for
ICE-TCP / GenServer-only rows.

# `invalidate`

```elixir
@spec invalidate(pid()) :: :ok
```

Removes any ETS row for `peer`.

# `publish_dc_passthrough`

```elixir
@spec publish_dc_passthrough(pid(), {module(), atom(), term()}) :: :ok
```

Publishes a plaintext datachannel passthrough sink for benches/tests.

Distinct from RTP `publish_passthrough/2` so MFA arity does not clash
(`apply(mod, fun, [sink, channel_ref, data, type])` where `type` is
`:string` | `:binary`).

# `publish_passthrough`

```elixir
@spec publish_passthrough(pid(), {module(), atom(), pid()}) :: :ok
```

Publishes a plaintext passthrough sink for benches/tests (no SRTP/UDP).

Forwarder prefers `send_rtp/3` when a handle exists. The MFA is invoked
synchronously as `apply(mod, fun, [sink, ssrc, rtp])` so latency probes can
record without a Peer GenServer hop. Used by latency benches and FakePeer tests.

# `publish_peer_state`

```elixir
@spec publish_peer_state(pid(), map()) :: :ok
```

Publishes or refreshes the hot-path row from Peer GenServer state.

Preserves existing ETS encrypt contexts across path republish. TCP selected
transport stores a GenServer-only marker (no UDP hot path).

# `publish_trunk_passthrough`

```elixir
@spec publish_trunk_passthrough(
  pid(),
  {module(), atom(), pid()},
  {module(), atom(), pid()}
) :: :ok
```

Publishes RTP and datachannel passthrough MFAs in one ETS row so Forwarder
can use both hot paths for the same peer pid (trunk pipe).

# `send_datachannel`

```elixir
@spec send_datachannel(t() | pid(), reference(), binary(), :string | :binary) ::
  :ok | {:error, term()}
```

Hot-path datachannel send when a `:dc_passthrough` row is published.

`type` is `:string` (PPI 51) or `:binary` (PPI 53); passed to the MFA as the
fourth argument so benches can observe PPI. Production Peer fallback uses the
same type via `XGW.Peer.send_datachannel_async/4`.

Returns `{:error, :not_ready}` when missing, `{:error, :stale}` on generation
mismatch, or `{:error, :use_genserver}` for GenServer-only / RTP rows (caller
should fall back to `XGW.Peer.send_datachannel_async/4`).

# `send_rtp`

```elixir
@spec send_rtp(t() | pid(), non_neg_integer(), binary() | XMediaLib.Rtp.t()) ::
  :ok | {:error, term()}
```

Encrypts and sends one RTP packet on the hot path.

On success: no Peer or Listener GenServer involvement.

---

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