nquic_protocol (nquic v1.2.0)

View Source

Pure 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

event()

-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.

state()

-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()}}.

timeout_action()

-type timeout_action() :: {set_timer, timer_type(), non_neg_integer()} | {cancel_timer, timer_type()}.

timer_type()

-type timer_type() :: idle | pto | path_validation | draining | ack_delay | pmtud | pace.

Functions

clear_dispatch_table/1

-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.

close(ErrorCode, ReasonPhrase, State)

-spec close(non_neg_integer(), binary(), state()) -> {ok, state()}.

Queue a CONNECTION_CLOSE frame with a transport error code.

close_app(ErrorCode, ReasonPhrase, State)

-spec close_app(non_neg_integer(), binary(), state()) -> {ok, state()}.

Queue a CONNECTION_CLOSE frame with an application error code.

close_stream(StreamID, State)

-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.

error_code/1

-spec error_code(nquic_error:any_reason()) -> non_neg_integer().

error_to_reason_phrase/1

-spec error_to_reason_phrase(nquic_error:any_reason()) -> binary().

flush/1

-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.

get_draining_timeout/1

-spec get_draining_timeout(state()) -> non_neg_integer().

get_idle_timeout/2

-spec get_idle_timeout(non_neg_integer(), non_neg_integer()) -> pos_integer() | infinity.

handle_handshake_timeout/3

-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.

handle_packet(PacketBin, Source, State)

-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.

handle_packet(PacketBin, Source, State, ECN)

-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.

handle_packet_notimers(PacketBin, Source, State)

-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.

handle_packet_notimers(PacketBin, Source, State, ECN)

-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.

handle_timeout/2

-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.

info(StateName, State)

-spec info(atom(), state()) -> map().

Get connection information as a map.

local_cids/1

-spec local_cids(state()) -> [nquic:connection_id()].

Get all local connection IDs.

odcid/1

-spec odcid(state()) -> nquic:connection_id() | undefined.

Get the original destination connection ID (server connections).

open_stream(Opts, State)

-spec open_stream(#{type => bidi | uni}, state()) -> {ok, nquic:stream_id(), state()} | {error, term()}.

Open a new stream. Returns {ok, StreamId, State} on success.

path_stats/1

-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.

peer/1

-spec peer(state()) -> nquic_socket:sockaddr() | undefined.

Get the current peer address.

peercert/1

-spec peercert(state()) -> binary() | undefined.

Get the peer's TLS certificate in DER form, or undefined if none.

pending_stream_ids/1

-spec pending_stream_ids(state()) -> [nquic:stream_id()].

Return IDs of peer-initiated streams that have buffered application data.

read_stream/2

-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.

reset_stream(StreamID, ErrorCode, State)

-spec reset_stream(nquic:stream_id(), non_neg_integer(), state()) -> {ok, state()} | {error, term()}.

Reset a stream with an application error code.

reset_timer_cache(State)

-spec reset_timer_cache(state()) -> state().

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.

scale_ack_delay/2

-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().

send_datagram/2

-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.

send_stream(StreamID, DataBin, Fin, State)

-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.

send_stream_capped(StreamID, DataBin, Fin, State)

-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.

socket_connected/1

-spec socket_connected(state()) -> boolean().

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.