# `Portals.Stream.State`
[🔗](https://github.com/Ziinc/portals/blob/v0.1.0/lib/portals/stream.ex#L190)

The per-stream credit and terminal-state machine, owned functionally
by `Portals.Connection` (like the pending-request table) rather than
by a process of its own.

Two completely independent windows:

  * **send window** — `send_credit` bytes the peer has granted us and
    `send_frames` frames we have sent that the peer has not yet
    acknowledged, capped by `max_frames`;
  * **receive window** — `recv_credit` bytes we have granted the peer
    and `recv_frames` frames it has sent that our owner has not yet
    consumed, capped by `max_frames`.

Every byte figure is the *complete* encoded frame size, including the
4-byte length prefix.

# `direction_state`

```elixir
@type direction_state() :: :open | :closed
```

# `t`

```elixir
@type t() :: %Portals.Stream.State{
  id: non_neg_integer(),
  inbound: direction_state(),
  max_credit: pos_integer(),
  max_frames: pos_integer(),
  outbound: direction_state(),
  owner: pid(),
  owner_ref: reference() | nil,
  recv_bytes: non_neg_integer(),
  recv_credit: non_neg_integer(),
  recv_frames: non_neg_integer(),
  send_credit: non_neg_integer(),
  send_frames: non_neg_integer(),
  send_waiters: :queue.queue()
}
```

# `add_send_credit`

```elixir
@spec add_send_credit(t(), non_neg_integer(), non_neg_integer()) ::
  {:ok, t()} | {:error, :credit_limit_exceeded}
```

Apply a `CREDIT` frame received from the peer.

# `charge_send`

```elixir
@spec charge_send(t(), pos_integer()) ::
  {:ok, t()} | {:error, :outbound_closed | :insufficient_credit | :frame_limit}
```

Charge one outbound frame of `bytes` encoded bytes against the send
window. Never sends: the caller must only put the frame on the wire
when this returns `{:ok, state}`.

# `consume_inbound`

```elixir
@spec consume_inbound(t(), non_neg_integer(), non_neg_integer()) ::
  {t(), non_neg_integer()}
```

The owner consumed `frames` inbound frames totalling `bytes` encoded
bytes. Returns the replenished state and the number of bytes to grant
back to the peer in a `CREDIT` frame.

# `fully_half_closed?`

```elixir
@spec fully_half_closed?(t()) :: boolean()
```

True when both directions are half-closed; the terminal frame may still be outstanding.

# `grant`

```elixir
@spec grant(t()) :: {t(), non_neg_integer()}
```

The initial (or replenishing) grant this side owes the peer: the
unused portion of our receive window. Returns `{state, bytes}`;
`bytes` is `0` when there is nothing new to grant.

# `half_close_inbound`

```elixir
@spec half_close_inbound(t()) :: {:ok, t()} | :already_closed
```

Record the peer's `HALF_CLOSE` of its sending direction. Idempotent.

# `half_close_outbound`

```elixir
@spec half_close_outbound(t()) :: {:ok, t()} | :already_closed
```

Close only this side's sending direction. Idempotent.

# `new`

```elixir
@spec new(non_neg_integer(), pid(), keyword()) :: t()
```

A fresh stream with no credit in either direction.

# `park_sender`

```elixir
@spec park_sender(t(), term()) :: t()
```

Park a blocked sender until credit arrives.

# `pop_sender`

```elixir
@spec pop_sender(t()) :: {:ok, term(), t()} | :empty
```

Pop the next parked sender, if any.

# `record_inbound`

```elixir
@spec record_inbound(t(), pos_integer()) ::
  {:ok, t()} | {:error, :inbound_closed | :credit_exceeded | :frame_limit}
```

Record one inbound `STREAM_DATA` frame of `bytes` encoded bytes.
Rejects anything the peer was not entitled to send.

# `take_senders`

```elixir
@spec take_senders(t()) :: {[term()], t()}
```

Every parked sender, in order, leaving the queue empty.

---

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