nquic_protocol (nquic v1.2.0)
View SourcePure functional QUIC protocol state machine.
Provides the core QUIC protocol logic without any process or I/O dependencies. Takes data in, returns events and packets out. Can be driven by any process (gen_statem, gen_server, or direct loop).
The caller is responsible for:
- Sending UDP packets returned by
flush/1 - Scheduling timers from timeout actions
- Delivering events to the application
- Registering/unregistering connection IDs in dispatch tables
Usage
{ok, Events, State1} = nquic_protocol:handle_packet(Bin, Source, State),
{ok, Packets, State2} = nquic_protocol:flush(State1),
[nquic_socket:send(Socket, Peer, Pkt) || Pkt <- Packets].
Summary
Functions
Tear down the listener-side dispatch routing for this connection.
Pure state operation paired with the side-effecting CID
dispatch_unregister/2 calls in
nquic_conn_migration:finalize_server_migration/1. Library-mode callers invoke this after
seeing local_migration_validated so subsequent CID frame handling
(RETIRE_CONNECTION_ID, key rotation issuance) stops consulting a
dispatch table that is no longer authoritative; the kernel already
routes new packets by 4-tuple to the per-connection FD.
Returns the dispatch table reference that was previously set
(undefined if none), letting the caller run the matching
dispatch_unregister loop without re-reading state.
Queue a CONNECTION_CLOSE frame with a transport error code.
Queue a CONNECTION_CLOSE frame with an application error code.
Close the send side of a stream by latching FIN.
The actual FIN-bearing STREAM frame is emitted by the next drain at flush
time (see nquic_protocol_streams_send:drain_pending_sends/1). Send-side
cleanup happens later, once the FIN frame has been queued and the stream
reaches the data_sent state.
Encrypt all queued frames into QUIC packets and return them.
Returns {ok, [iodata()], State, [timeout_action()]} with packets
and timer actions when there are frames to send, or {ok, State}
if there is nothing to flush.
The timer actions restart the PTO only when the flush sent an
ack-eliciting packet (RFC 9002 §6.2). A flush of ACK-only packets
leaves the PTO timer as it is.
The ACK of one application packet stays pending when the flush has no
other frame to send (nquic_protocol_ack:is_ack_due/1). The next frame
that the application queues carries it, or the ack_delay timer sends
it.
When the pacer blocks, the flush sends the Initial and Handshake frames,
the pending ACK and the 1-RTT control frames, and keeps STREAM and
DATAGRAM data queued. The timer actions then include the pace timer.
After a PTO or on entry to recovery, the next packet can exceed the
congestion window (RFC 9002 §6.2.4 and §7.3.2). That flush sends as if
the pacer passed, still bounded by the window, and also sets the pace
timer.
The packets are produced in encryption-level order: Initial first,
then Handshake, then 1-RTT (application). Each is a separate datagram;
there is no in-datagram coalescing today, so the wrapper sends each
as an independent UDP datagram.
Handle a timer expiration before the connection is established.
Process an incoming UDP packet.
Source is the sockaddr map from the socket recv (e.g.
#{family => inet, addr => {127,0,0,1}, port => 4433}).
Returns events for the caller to handle, may queue internal frames
(ACKs, window updates) that flush/1 will encrypt, and returns
timeout actions for the caller to schedule.
Process an incoming UDP packet with an ECN codepoint from IP header.
Process an incoming UDP packet without computing timer actions.
Same as handle_packet/3 but skips
nquic_protocol_timer:compute_timer_actions/1 at the end. Use this
for batched recv loops where timer computation is deferred to the
final packet. The caller must call
nquic_protocol_timer:compute_timer_actions/1 after the batch
completes.
Process an incoming packet without timer actions, with ECN codepoint.
Handle a timer expiry. Returns events, updated state, and new timer actions. The caller should deliver events and schedule returned timers.
Get connection information as a map.
Get all local connection IDs.
Get the original destination connection ID (server connections).
Open a new stream. Returns {ok, StreamId, State} on success.
Project path-level statistics from the connection state.
Combines RTT estimator state (smoothed_rtt, rttvar, min_rtt,
latest_rtt), congestion window state (cwnd, bytes_in_flight,
ssthresh), lifetime packet counters, and ECN state into a flat map
suitable for routing decisions and operational dashboards.
Get the current peer address.
Get the peer's TLS certificate in DER form, or undefined if none.
Return IDs of peer-initiated streams that have buffered application data.
Read and consume buffered data from a stream. Returns the accumulated data and whether FIN has been received. Clears the stream's application buffer so subsequent calls return new data only.
Reset a stream with an application error code.
Clear the cached timer values so
nquic_protocol_timer:compute_timer_actions/1 re-emits set_timer
actions for every live timer.
Use this at an ownership boundary (for example on library-mode
takeover/1), where the previous owner's timers were not transferred
and the new owner must schedule them afresh.
Send an unreliable DATAGRAM frame (RFC 9221). Checks that datagrams were negotiated, that the data fits within the peer's max_datagram_frame_size, and that the congestion window allows it. Datagrams are not retransmitted on loss and are not flow-controlled.
Queue a STREAM frame for sending. Call flush/1 to encrypt and retrieve packets.
On flow-control or congestion-control block, returns {error, Reason, State}
with the stream recorded in the state's blocked_streams set. Callers that
need the updated state (to observe a subsequent {stream_writable, _}
event) should propagate it; callers that don't can discard the third
element.
Queue at most send_buffer_high_water - byte_size(pending_send_data)
bytes of DataBin onto the stream's outbound buffer, capped further by
the peer's connection / stream flow-control windows. Returns the number
of bytes accepted; the caller is responsible for parking the unaccepted
tail (e.g. as a send_waiter) so it can be queued later when the drain
or a peer flow-control update frees room.
fin is only latched when the entire input fits; a partial accept
leaves FIN un-latched so the eventual final byte carries the FIN.
On stream / connection flow-control rejection (peer's window is full),
returns {error, Reason, State} with the stream recorded in
blocked_streams so a later stream_writable event can fire.
True when the connection's socket has been connect(2)-bound to its
peer 4-tuple. Set after server_per_conn_fd migration (RFC 9000 §9):
callers exporting such a connection into library mode must drive the
send path through socket:send/2 instead of sendto.
Types
-type event() :: {stream_data, nquic:stream_id()} | {stream_opened, nquic:stream_id()} | {stream_reset, nquic:stream_id(), non_neg_integer()} | {stop_sending, nquic:stream_id(), non_neg_integer()} | connection_closed | {datagram_received, binary()} | {new_session_ticket, binary()} | {new_token_received, binary()} | {stream_writable, nquic:stream_id()} | {state_transition, handshake | established} | connected | listener_established | {migrate_to_preferred, nquic_transport:preferred_address()} | local_migration_validated.
-type state() :: #conn_state{role :: client | server, scid :: nquic:connection_id(), dcid :: nquic:connection_id(), odcid :: nquic:connection_id() | undefined, retry_scid :: nquic:connection_id() | undefined, retry_token :: binary(), version :: non_neg_integer(), version_preference :: [non_neg_integer()], socket :: nquic_socket:t() | undefined, peer :: nquic_socket:sockaddr() | undefined, select_info :: nquic_socket:select_info() | undefined, pn_spaces :: #{nquic_packet:space() => map()}, app_next_pn :: non_neg_integer(), app_largest_received :: integer(), app_largest_received_us :: integer() | undefined, loss_state :: nquic_loss:loss_state() | undefined, dispatch_table :: nquic_dispatch:t() | undefined, listener :: pid() | undefined, connect_waiters :: [gen_server:from()], local_params :: #transport_params{original_destination_connection_id :: nquic:connection_id() | undefined, max_idle_timeout :: non_neg_integer(), stateless_reset_token :: binary() | undefined, max_udp_payload_size :: pos_integer(), initial_max_data :: non_neg_integer(), initial_max_stream_data_bidi_local :: non_neg_integer(), initial_max_stream_data_bidi_remote :: non_neg_integer(), initial_max_stream_data_uni :: non_neg_integer(), initial_max_streams_bidi :: non_neg_integer(), initial_max_streams_uni :: non_neg_integer(), ack_delay_exponent :: 0..20, max_ack_delay :: non_neg_integer(), disable_active_migration :: boolean(), preferred_address :: nquic_transport:preferred_address() | undefined, active_connection_id_limit :: non_neg_integer(), initial_source_connection_id :: nquic:connection_id() | undefined, retry_source_connection_id :: nquic:connection_id() | undefined, version_information :: nquic_transport:version_information() | undefined, max_datagram_frame_size :: non_neg_integer() | undefined}, remote_params :: #transport_params{original_destination_connection_id :: nquic:connection_id() | undefined, max_idle_timeout :: non_neg_integer(), stateless_reset_token :: binary() | undefined, max_udp_payload_size :: pos_integer(), initial_max_data :: non_neg_integer(), initial_max_stream_data_bidi_local :: non_neg_integer(), initial_max_stream_data_bidi_remote :: non_neg_integer(), initial_max_stream_data_uni :: non_neg_integer(), initial_max_streams_bidi :: non_neg_integer(), initial_max_streams_uni :: non_neg_integer(), ack_delay_exponent :: 0..20, max_ack_delay :: non_neg_integer(), disable_active_migration :: boolean(), preferred_address :: nquic_transport:preferred_address() | undefined, active_connection_id_limit :: non_neg_integer(), initial_source_connection_id :: nquic:connection_id() | undefined, retry_source_connection_id :: nquic:connection_id() | undefined, version_information :: nquic_transport:version_information() | undefined, max_datagram_frame_size :: non_neg_integer() | undefined} | undefined, server_packet_processed :: boolean(), owner :: pid() | undefined, owner_mon :: reference() | undefined, deferred_flush_pending :: boolean(), pending_ack_count :: non_neg_integer(), ack_immediate :: boolean(), last_idle_ms :: non_neg_integer() | infinity | undefined, last_pto_ms :: nquic_protocol_timer:loss_detection_timer() | undefined, recv_ecn :: nquic_socket:ecn_mark(), pmtud :: nquic_pmtud:pmtud_state() | undefined, gso_size :: undefined | pos_integer(), max_payload_size :: pos_integer(), server_per_conn_fd :: boolean(), proactive_cids :: boolean(), socket_connected :: boolean(), self_migration_pending :: boolean(), metrics_counters :: nquic_metrics:conn_counters() | undefined, spin_enabled :: boolean(), peer_spin :: 0..1, new_token_enabled :: boolean(), new_token_lifetime :: pos_integer(), qlog :: undefined | nquic_qlog:qlog_state(), close_kind :: undefined | local | peer | idle_timeout | protocol_error, crypto :: #conn_crypto{tls_state :: term(), keys :: nquic_keys:key_map(), app_send_keys :: map() | undefined, app_recv_keys :: map() | undefined, crypto_buffer :: #{nquic_packet:space() => {non_neg_integer(), binary(), list()}}, cipher :: aes_128_gcm | aes_256_gcm | chacha20_poly1305, cipher_suites :: [aes_128_gcm | aes_256_gcm | chacha20_poly1305] | undefined, key_phase :: boolean(), key_update_pending :: boolean(), client_app_secret :: binary() | undefined, server_app_secret :: binary() | undefined, old_read_keys :: #{key := binary(), iv := binary()} | undefined, zero_rtt_accepted :: boolean(), replay_protection :: module() | undefined, session_ticket :: map() | undefined, resumption_secret :: binary() | undefined, session_cache :: atom() | false | {module, module()} | undefined, token_cache :: atom() | false | {module, module()}, alpn :: [binary()] | undefined, hostname :: string() | binary() | undefined, cert :: binary() | undefined, cert_chain :: [binary()], key :: any() | undefined, verify :: verify_none | verify_peer, cacerts :: [binary()], peer_cert :: binary() | undefined, static_key :: binary() | undefined}, streams_state :: #conn_streams{streams :: #{nquic:stream_id() => #stream_state{stream_id :: nquic:stream_id(), type :: bidi | uni, send_state :: ready | send | data_sent | data_recvd | reset_sent | reset_recvd, send_offset :: non_neg_integer(), send_max_data :: non_neg_integer(), last_stream_data_blocked :: non_neg_integer(), pending_send_data :: [binary()], pending_send_size :: non_neg_integer(), pending_send_fin :: boolean(), recv_state :: recv | size_known | data_recvd | reset_recvd | data_read | reset_read, recv_offset :: non_neg_integer(), recv_max_offset :: non_neg_integer(), recv_window :: non_neg_integer(), recv_buffer :: gb_trees:tree(non_neg_integer(), {binary(), boolean()}), app_buffer :: iodata(), app_buffer_size :: non_neg_integer()}}, next_bidi_stream :: nquic:stream_id() | undefined, next_uni_stream :: nquic:stream_id() | undefined, peer_max_streams_bidi :: non_neg_integer(), peer_max_streams_uni :: non_neg_integer(), local_max_streams_bidi :: non_neg_integer(), local_max_streams_uni :: non_neg_integer(), last_sent_max_streams_bidi :: non_neg_integer(), last_sent_max_streams_uni :: non_neg_integer(), max_peer_bidi_stream_id :: non_neg_integer() | undefined, max_peer_uni_stream_id :: non_neg_integer() | undefined, opened_peer_bidi_count :: non_neg_integer(), opened_peer_uni_count :: non_neg_integer(), closed_peer_bidi_wm :: integer(), closed_peer_uni_wm :: integer(), closed_peer_streams :: #{nquic:stream_id() => true}, recv_waiters :: #{nquic:stream_id() => gen_statem:from()}, accept_stream_waiters :: queue:queue(gen_statem:from()), pending_streams :: queue:queue(nquic:stream_id()), blocked_streams :: #{nquic:stream_id() => true}, pending_send_streams :: #{nquic:stream_id() => true}, send_buffer_high_water :: pos_integer(), send_timeout :: timeout(), send_waiters :: queue:queue(nquic_conn_send_waiters:t())}, flow :: #conn_flow{local_max_data :: non_neg_integer(), remote_max_data :: non_neg_integer(), data_sent :: non_neg_integer(), data_received :: non_neg_integer(), last_data_blocked :: non_neg_integer(), pending_initial_frames :: [nquic_frame:t()], pending_handshake_frames :: [nquic_frame:t()], pending_app_frames :: [nquic_frame:t()], pending_app_pre_encoded :: [{non_neg_integer(), iodata(), nquic_frame:t()}], queued_app_send_bytes :: non_neg_integer(), cwnd_exempt :: boolean()}, path :: #conn_path_mgmt{path_state :: nquic_path:state() | undefined, peer_cids :: #{non_neg_integer() => #{cid := nquic:connection_id(), token := binary()}}, local_cids :: #{non_neg_integer() => nquic:connection_id()}, local_cid_seq :: non_neg_integer(), peer_retire_prior_to :: non_neg_integer(), anti_amp_bytes_received :: non_neg_integer(), anti_amp_bytes_sent :: non_neg_integer(), address_validated :: boolean()}}.
-type timeout_action() :: {set_timer, timer_type(), non_neg_integer()} | {cancel_timer, timer_type()}.
-type timer_type() :: idle | pto | path_validation | draining | ack_delay | pmtud | pace.
Functions
-spec clear_dispatch_table(state()) -> {nquic_dispatch:t() | undefined, state()}.
Tear down the listener-side dispatch routing for this connection.
Pure state operation paired with the side-effecting CID
dispatch_unregister/2 calls in
nquic_conn_migration:finalize_server_migration/1. Library-mode callers invoke this after
seeing local_migration_validated so subsequent CID frame handling
(RETIRE_CONNECTION_ID, key rotation issuance) stops consulting a
dispatch table that is no longer authoritative; the kernel already
routes new packets by 4-tuple to the per-connection FD.
Returns the dispatch table reference that was previously set
(undefined if none), letting the caller run the matching
dispatch_unregister loop without re-reading state.
-spec close(non_neg_integer(), binary(), state()) -> {ok, state()}.
Queue a CONNECTION_CLOSE frame with a transport error code.
-spec close_app(non_neg_integer(), binary(), state()) -> {ok, state()}.
Queue a CONNECTION_CLOSE frame with an application error code.
-spec close_stream(nquic:stream_id(), state()) -> {ok, state()} | {error, term()}.
Close the send side of a stream by latching FIN.
The actual FIN-bearing STREAM frame is emitted by the next drain at flush
time (see nquic_protocol_streams_send:drain_pending_sends/1). Send-side
cleanup happens later, once the FIN frame has been queued and the stream
reaches the data_sent state.
-spec error_code(nquic_error:any_reason()) -> non_neg_integer().
-spec error_to_reason_phrase(nquic_error:any_reason()) -> binary().
-spec flush(state()) -> {ok, [iodata()], state(), [timeout_action()]} | {ok, state()}.
Encrypt all queued frames into QUIC packets and return them.
Returns {ok, [iodata()], State, [timeout_action()]} with packets
and timer actions when there are frames to send, or {ok, State}
if there is nothing to flush.
The timer actions restart the PTO only when the flush sent an
ack-eliciting packet (RFC 9002 §6.2). A flush of ACK-only packets
leaves the PTO timer as it is.
The ACK of one application packet stays pending when the flush has no
other frame to send (nquic_protocol_ack:is_ack_due/1). The next frame
that the application queues carries it, or the ack_delay timer sends
it.
When the pacer blocks, the flush sends the Initial and Handshake frames,
the pending ACK and the 1-RTT control frames, and keeps STREAM and
DATAGRAM data queued. The timer actions then include the pace timer.
After a PTO or on entry to recovery, the next packet can exceed the
congestion window (RFC 9002 §6.2.4 and §7.3.2). That flush sends as if
the pacer passed, still bounded by the window, and also sets the pace
timer.
The packets are produced in encryption-level order: Initial first,
then Handshake, then 1-RTT (application). Each is a separate datagram;
there is no in-datagram coalescing today, so the wrapper sends each
as an independent UDP datagram.
-spec get_draining_timeout(state()) -> non_neg_integer().
-spec get_idle_timeout(non_neg_integer(), non_neg_integer()) -> pos_integer() | infinity.
-spec handle_handshake_timeout(initial | handshake, timer_type(), state()) -> {ok, [event()], state(), [timeout_action()]} | {error, term(), state()}.
Handle a timer expiration before the connection is established.
The pto timer is the loss detection timer. When a loss time is set,
the packets lost at that time are queued again
(nquic_protocol_send:handle_loss_timeout/2). Otherwise it is a PTO:
the probes go to every space in [initial, handshake] with
ack-eliciting packets in flight, and resend the CRYPTO data of those
packets (nquic_protocol_send:queue_handshake_probes/2).
Phase is the encryption level that the owner drives. It selects the
probe level only when nothing is in flight and no Handshake keys exist.
The owner tracks Phase from the {state_transition, handshake} event
and switches to the established handle_timeout/2 when it observes
connected.
-spec handle_packet(binary(), nquic_socket:sockaddr(), state()) -> {ok, [event()], state(), [timeout_action()]} | {error, term(), state()}.
Process an incoming UDP packet.
Source is the sockaddr map from the socket recv (e.g.
#{family => inet, addr => {127,0,0,1}, port => 4433}).
Returns events for the caller to handle, may queue internal frames
(ACKs, window updates) that flush/1 will encrypt, and returns
timeout actions for the caller to schedule.
-spec handle_packet(binary(), nquic_socket:sockaddr(), state(), nquic_socket:ecn_mark()) -> {ok, [event()], state(), [timeout_action()]} | {error, term(), state()}.
Process an incoming UDP packet with an ECN codepoint from IP header.
-spec handle_packet_notimers(binary(), nquic_socket:sockaddr(), state()) -> {ok, [event()], state()} | {error, term(), state()}.
Process an incoming UDP packet without computing timer actions.
Same as handle_packet/3 but skips
nquic_protocol_timer:compute_timer_actions/1 at the end. Use this
for batched recv loops where timer computation is deferred to the
final packet. The caller must call
nquic_protocol_timer:compute_timer_actions/1 after the batch
completes.
-spec handle_packet_notimers(binary(), nquic_socket:sockaddr(), state(), nquic_socket:ecn_mark()) -> {ok, [event()], state()} | {error, term(), state()}.
Process an incoming packet without timer actions, with ECN codepoint.
-spec handle_timeout(timer_type(), state()) -> {ok, [event()], state(), [timeout_action()]} | {error, term(), state()}.
Handle a timer expiry. Returns events, updated state, and new timer actions. The caller should deliver events and schedule returned timers.
The pto timer is the loss detection timer of RFC 9002 Appendix A.8.
When a loss time is set, the packets lost at that time are queued again
and the timer restarts. Otherwise it is a PTO. The 1-RTT probe carries
new data, the oldest unacknowledged data or a PING
(nquic_protocol_send:queue_app_probe/1), and the next flush can send
one packet over the congestion window and the pacer.
While Handshake keys exist and the Handshake space has ack-eliciting
packets in flight, a PTO probes the Handshake space as
handle_handshake_timeout/3 does (RFC 9002 Appendix A.8). This resends
a lost client Finished before the handshake is confirmed.
Get connection information as a map.
-spec local_cids(state()) -> [nquic:connection_id()].
Get all local connection IDs.
-spec odcid(state()) -> nquic:connection_id() | undefined.
Get the original destination connection ID (server connections).
-spec open_stream(#{type => bidi | uni}, state()) -> {ok, nquic:stream_id(), state()} | {error, term()}.
Open a new stream. Returns {ok, StreamId, State} on success.
-spec path_stats(state()) -> nquic_loss:path_stats().
Project path-level statistics from the connection state.
Combines RTT estimator state (smoothed_rtt, rttvar, min_rtt,
latest_rtt), congestion window state (cwnd, bytes_in_flight,
ssthresh), lifetime packet counters, and ECN state into a flat map
suitable for routing decisions and operational dashboards.
-spec peer(state()) -> nquic_socket:sockaddr() | undefined.
Get the current peer address.
Get the peer's TLS certificate in DER form, or undefined if none.
-spec pending_stream_ids(state()) -> [nquic:stream_id()].
Return IDs of peer-initiated streams that have buffered application data.
-spec read_stream(nquic:stream_id(), state()) -> {ok, binary(), boolean(), state()} | {error, term()}.
Read and consume buffered data from a stream. Returns the accumulated data and whether FIN has been received. Clears the stream's application buffer so subsequent calls return new data only.
-spec reset_stream(nquic:stream_id(), non_neg_integer(), state()) -> {ok, state()} | {error, term()}.
Reset a stream with an application error code.
Clear the cached timer values so
nquic_protocol_timer:compute_timer_actions/1 re-emits set_timer
actions for every live timer.
Use this at an ownership boundary (for example on library-mode
takeover/1), where the previous owner's timers were not transferred
and the new owner must schedule them afresh.
-spec scale_ack_delay(non_neg_integer(), #transport_params{original_destination_connection_id :: nquic:connection_id() | undefined, max_idle_timeout :: non_neg_integer(), stateless_reset_token :: binary() | undefined, max_udp_payload_size :: pos_integer(), initial_max_data :: non_neg_integer(), initial_max_stream_data_bidi_local :: non_neg_integer(), initial_max_stream_data_bidi_remote :: non_neg_integer(), initial_max_stream_data_uni :: non_neg_integer(), initial_max_streams_bidi :: non_neg_integer(), initial_max_streams_uni :: non_neg_integer(), ack_delay_exponent :: 0..20, max_ack_delay :: non_neg_integer(), disable_active_migration :: boolean(), preferred_address :: nquic_transport:preferred_address() | undefined, active_connection_id_limit :: non_neg_integer(), initial_source_connection_id :: nquic:connection_id() | undefined, retry_source_connection_id :: nquic:connection_id() | undefined, version_information :: nquic_transport:version_information() | undefined, max_datagram_frame_size :: non_neg_integer() | undefined} | undefined) -> non_neg_integer().
-spec send_datagram(binary(), state()) -> {ok, state()} | {error, nquic_error:any_reason()}.
Send an unreliable DATAGRAM frame (RFC 9221). Checks that datagrams were negotiated, that the data fits within the peer's max_datagram_frame_size, and that the congestion window allows it. Datagrams are not retransmitted on loss and are not flow-controlled.
-spec send_stream(nquic:stream_id(), iodata(), fin | nofin, state()) -> {ok, state()} | {error, term()} | {error, term(), state()}.
Queue a STREAM frame for sending. Call flush/1 to encrypt and retrieve packets.
On flow-control or congestion-control block, returns {error, Reason, State}
with the stream recorded in the state's blocked_streams set. Callers that
need the updated state (to observe a subsequent {stream_writable, _}
event) should propagate it; callers that don't can discard the third
element.
-spec send_stream_capped(nquic:stream_id(), iodata(), fin | nofin, state()) -> {ok, non_neg_integer(), state()} | {error, term()} | {error, term(), state()}.
Queue at most send_buffer_high_water - byte_size(pending_send_data)
bytes of DataBin onto the stream's outbound buffer, capped further by
the peer's connection / stream flow-control windows. Returns the number
of bytes accepted; the caller is responsible for parking the unaccepted
tail (e.g. as a send_waiter) so it can be queued later when the drain
or a peer flow-control update frees room.
fin is only latched when the entire input fits; a partial accept
leaves FIN un-latched so the eventual final byte carries the FIN.
On stream / connection flow-control rejection (peer's window is full),
returns {error, Reason, State} with the stream recorded in
blocked_streams so a later stream_writable event can fire.
True when the connection's socket has been connect(2)-bound to its
peer 4-tuple. Set after server_per_conn_fd migration (RFC 9000 §9):
callers exporting such a connection into library mode must drive the
send path through socket:send/2 instead of sendto.