nquic_conn_timers (nquic v1.0.0)
View Sourcegen_statem timer adapters for the QUIC connection state machine.
Translates nquic_protocol's abstract timer actions into gen_statem
timeout actions, computes the idle / PTO timer values for the
current state, and runs the PTO probe handler for the
initial / handshake states (the established state delegates PTO to
nquic_protocol:handle_timeout/2).
Summary
Functions
Ensure idle + PTO timers are armed on a handler result.
Handle a PTO firing in the initial / handshake states.
Queues a PING probe at the relevant encryption level, flushes, and
restarts the PTO timer. The established state delegates PTO to
nquic_protocol:handle_timeout/2 instead.
Convert a user-facing idle_timeout option to the transport-parameter
value: infinity becomes 0 (RFC 9000 §18.2: "no limit advertised").
Compute the gen_statem idle_timeout action for the current state.
RFC 9000 §10.1: effective timeout is min(local, remote); 0 means
disabled, in which case no timer is armed (returned as an empty list).
Compute the gen_statem pto_timeout action for the current state.
When no ack-eliciting packet is in flight, returns a PTO cancellation.
Translate nquic_protocol's timer actions into gen_statem timeout actions.
Tail-recursive with accumulator; order doesn't matter for gen_statem
actions. nquic_protocol only emits cancel_timer for the PTO timer
today; other timers replace themselves implicitly when re-armed. If
the protocol starts cancelling more types, Dialyzer flags the missing
clauses.
Functions
-spec ensure_handshake_timers(gen_statem:event_handler_result(dynamic())) -> gen_statem:event_handler_result(dynamic()).
Ensure idle + PTO timers are armed on a handler result.
Used after the client's first Initial send so the loss-detection
timer is armed even if no packet arrived synchronously (the send-only
path skips process_datagram, which is where these timers are
normally set). Subsequent PTO or process_datagram calls replace
these actions idempotently.
-spec handle_pto(initial | handshake, #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(), 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(), last_idle_ms :: non_neg_integer() | infinity | undefined, last_pto_ms :: non_neg_integer() | cancel | 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_packet:space() | rtt0 => 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()}, 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()}}) -> gen_statem:event_handler_result(term()).
Handle a PTO firing in the initial / handshake states.
Queues a PING probe at the relevant encryption level, flushes, and
restarts the PTO timer. The established state delegates PTO to
nquic_protocol:handle_timeout/2 instead.
-spec idle_timeout_to_param(timeout() | non_neg_integer()) -> non_neg_integer().
Convert a user-facing idle_timeout option to the transport-parameter
value: infinity becomes 0 (RFC 9000 §18.2: "no limit advertised").
-spec set_idle_timer(#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(), 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(), last_idle_ms :: non_neg_integer() | infinity | undefined, last_pto_ms :: non_neg_integer() | cancel | 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_packet:space() | rtt0 => 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()}, 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()}}) -> [gen_statem:action()].
Compute the gen_statem idle_timeout action for the current state.
RFC 9000 §10.1: effective timeout is min(local, remote); 0 means
disabled, in which case no timer is armed (returned as an empty list).
-spec set_pto_timer(#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(), 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(), last_idle_ms :: non_neg_integer() | infinity | undefined, last_pto_ms :: non_neg_integer() | cancel | 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_packet:space() | rtt0 => 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()}, 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()}}) -> [gen_statem:action()].
Compute the gen_statem pto_timeout action for the current state.
When no ack-eliciting packet is in flight, returns a PTO cancellation.
-spec timer_actions_to_statem([nquic_protocol:timeout_action()]) -> [gen_statem:action()].
Translate nquic_protocol's timer actions into gen_statem timeout actions.
Tail-recursive with accumulator; order doesn't matter for gen_statem
actions. nquic_protocol only emits cancel_timer for the PTO timer
today; other timers replace themselves implicitly when re-armed. If
the protocol starts cancelling more types, Dialyzer flags the missing
clauses.